Rust의 RangeFrom 타입의 역사와 오버플로 동작, 그리고 새로운 범위 타입으로의 변화 배경을 살펴봅니다.
Clippy 린트를 작업하던 중 RangeFrom 타입의 구현에 무언가 문제가 있다는 생각이 들기 시작했습니다. 이 생각을 어디엔가 정리하기 위해 글을 쓰기로 했습니다. 제 생각은 주로 초기에 결정된 설계 지점 하나에 관한 것이므로, 먼저 이 타입의 역사를 살펴보며 오늘날 사용하는 형태가 어떻게 되었는지, 그리고 새로운 범위 타입과 함께 어떻게 발전할지를 알아보는 것이 좋겠다고 판단했습니다.
2부에서는 설계에 관한 제 생각을 조금 떠들고, 제 의견도 일부 이야기할 것입니다. 다만 이 글에서 제가 집중하는 내용을 보면 무엇에 관해 떠들고 싶은지는 꽤 분명할 수도 있습니다.
범위는 때때로 구간이라고도 불리는 수학적 구성 개념입니다. 이 글에서는 정수 범위만 살펴보겠습니다. Rust에는 몇 가지 서로 다른 종류의 범위가 존재합니다.
.. 전체 범위이며, 이는 와 같습니다. Rust에서 이 타입은 RangeFull이라고 합니다...m 위쪽으로 제한된 범위이며, 이는 와 같습니다. Rust에서 이 타입은 RangeTo라고 합니다.n.. 위쪽으로 제한되지 않은 범위이며, 이는 와 같습니다. Rust에서 이 타입은 RangeFrom이라고 합니다.n..m 제한된 범위이며, 이는 와 같습니다. Rust에서 이 타입은 Range이라고 합니다...=m은 와 같고, n..=m은 와 같습니다.이들은 여러 용도로 사용할 수 있습니다. 원래는 슬라이싱을 위해 추가되었으며, 예를 들어 x[2..5]로 필드 2, 3, 4를 가리키는 슬라이스를 얻을 수 있습니다.
그중 일부는 반복할 수도 있으므로, 1부터 10까지 출력하고 싶다면 다음과 같이 작성할 수 있습니다.
for i in 1..=10 {
println!("{i}");
}
직접적으로든 미래에 간접적으로든 반복자를 구현하는 것은 RangeFrom, Range, 그리고 이들의 포괄 버전뿐입니다.
이 글 연작의 나머지에서는 주로 RangeFrom 타입에 집중하겠습니다.
RangeFrom 타입 역사Rust는 RFC0198에서 슬라이싱용 범위를 도입했고, 이는 rust#17318에서 구현되었습니다. 이후 2014-11-03에 시작된 RFC0439가 뒤를 이었으며, 이 문서는 cmp와 ops 모듈에 상당히 많은 변경을 가했습니다. 이 RFC는 해당 모듈의 많은 트레이트를 오늘날 사용하는 트레이트로 바꿉니다. 그리고 작은 장에, 단지 슬라이싱을 위한 특수 사례가 아니라 실제 타입으로 범위를 추가하기 위해 제안된 변경 사항이 숨어 있습니다. 슬라이스 개정입니다.
이 RFC의 구현은 rust#19148에서 추적되며, 2014-12-13에 기여자 @nrc는 범위와 범위를 이용한 인덱싱을 구현하겠다고 작성합니다. 범위 타입이 Iterator를 구현해야 하는지, 아니면 나중에 다시 다룰 수도 있는 반복자를 반환하는 Self::iter 메서드를 가져야 하는지를 두고 rust#19794에서 범위 표기법 추가에 관한 약간의 논의가 있었습니다.
이는 새로운 범위 타입을 추가하는 풀 리퀘스트 rust#19858로 이어집니다. 다음은 그 풀 리퀘스트의 core에 있던 RangeFrom의 전체 구현입니다[^1].
/// A range which is only bounded below.
#[deriving(Copy)]
#[lang="range_from"]
pub struct RangeFrom<Idx> {
/// The lower bound of the range (inclusive).
pub start: Idx,
}
impl<Idx: Clone + Step> Iterator<Idx> for RangeFrom<Idx> {
#[inline]
fn next(&mut self) -> Option<Idx> {
// Deliberately overflow so we loop forever.
let result = self.start.clone();
self.start.step();
return Some(result);
}
}
여기서 인덱스 타입의 끝에 도달했을 때 의도적인 오버플로가 있었다는 점을 최초 구현부터 확인할 수 있습니다. 흥미롭게도 Step 트레이트 역시 이 풀 리퀘스트에서 도입되었으며, 이 글을 작성하는 시점에도 여전히 불안정적입니다.
그런데 반복자가 오버플로하도록 만들기로 한 결정은 왜 내려졌을까요? 당시의 논의 대부분이 IRC에서 이루어졌기 때문에 답하기가 다소 어렵습니다. rust-irc-logs 같은 로그 아카이브가 존재하지만, 범위 구현 시기 전후의 로그는 누락되어 있습니다.
수년 전의 일임에도 어떤 근거를 기억하는지 듣기 위해 @nrc에게 연락했습니다. 그는 n..이 영원히 반복한다는 의미여야 하는지, 아니면 n..=MAX여야 하는지를 두고 논의가 있었고 전자를 선택했다고 기억했습니다. 이유에 대해서는 C 스타일 for 루프와 일치한다는 것이 가장 그럴듯한 추측이었습니다.
이 구현의 또 다른 점은 오버플로 검사가 켜져 있으면 정수의 마지막 값을 절대로 산출하지 않는다는 것입니다. self.start가 255와 같다면 255를 산출하기 전에 패닉이 발생합니다. 이는 rust#25708 같은 이슈로 이어지는 몇몇 이상한 현상을 낳습니다.
위 풀 리퀘스트는 2014-12-24에 병합되었고, 이틀 뒤인 2014-12-26에 이에 관한 이슈가 열렸습니다. "Take care of boundaries in Step trait" #20249는 @bluss가 열었습니다.
이 이슈 스레드에서 다음 두 메시지가 제 눈에 띕니다.
이는 의도적인 것이었고 IRC에서 여러 사람과 논의했습니다. 다수는 열린 끝 구간이 최댓값에 의해 제한되기보다 영원히 반복해야 한다고 느꼈습니다. …
하지만 다가오는 오버플로 규칙과 오버플로 검사에서는 구간이 이런 식으로 반복하지 않을 것입니다. 경계 검사를 수동으로 삽입하는 편이 더 현명해 보이며, 반복도 그 지점에서 종료해야 한다고 생각합니다.
스레드의 논의는 결국 오버플로하지 않는 책임은 최종 사용자에게 있다는 결론에 이르는 듯하며, 이후 기본 디버그 프로필에서 오버플로가 패닉을 일으키도록 만든 뒤 닫혔습니다.
2015년 후반에는 오버플로에 관한 또 다른 이슈 rust#25696가 열립니다. 이 이슈와 앞서 언급한 rust#25708은 나중에 오버플로에 관한 주석을 추가한 풀 리퀘스트 rust#32592에 의해 닫힙니다. 이 풀 리퀘스트는 /r/rust 서브레딧에 올라온 질문 I don't understand why this for loop is giving me an "arithmetic operation overflowed" 때문에 만들어졌습니다. Reddit 스레드에서는 도움이 되는 몇몇 사용자가 설명했고, 이것이 간과된 사항인지에 관한 논의도 있었지만, 지금은 삭제된 사용자가 이 주제는 많이 논의되었다고 말하며 이를 일축합니다. 저는 그 논의를 찾지 못했습니다.
이 풀 리퀘스트에서 검토자인 @alexchricton은 다음과 같이 묻습니다.
이것은
0..범위에 대해서만 무한 루프를 만들겠죠?1..같은 것은 어느 시점에 0으로 오버플로할 것이고, 그러면 반복이 멈춰야 하지 않나요?
이에 구현자 tbu-는 다음과 같이 답합니다.
아니요, 항상 무한 루프입니다(패닉이 발생하지 않는다면). …
결국 추가된 주석은 상당히 비확정적이며 다음과 같습니다.
/// Note: Currently, no overflow checking is done for the iterator
/// implementation; if you use an integer range and the integer overflows, it
/// might panic in debug mode or create an endless loop in release mode. This
/// overflow behavior might change in the future.
몇 년 후인 2020-05-20에 이 주석을 수정하는 풀 리퀘스트 rust#72368이 게시됩니다. 이 풀 리퀘스트는 주석을 변경하고 앞으로 변경할 수 없도록 안정화합니다. 새 주석은 다음과 같습니다.
/// *Note*: Overflow in the [`Iterator`] implementation (when the contained
/// data type reaches its numerical limit) is allowed to panic, wrap, or
/// saturate. This behavior is defined by the implementation of the [`Step`]
/// trait. For primitive integers, this follows the normal rules, and respects
/// the overflow checks profile (panic in debug, wrap in release). Note also
/// that overflow happens earlier than you might assume: the overflow happens
/// in the call to `next` that yields the maximum value, as the range must be
/// set to a state to yield the next value.
이는 작동 방식을 설명하는 문서일 뿐 실제 반복자의 동작을 변경하지는 않습니다.
그리고 이것이 오늘날 std::ops::RangeFrom의 문서에 있는 내용이기도 합니다.
2023년 11월에는 RangeFrom의 오버플로 동작을 다루는 API 변경 제안이 열렸습니다. libs-team#304입니다. 이 글을 작성하는 시점에 변경에 관한 합의는 아직 이루어지지 않았지만, 일부 논의는 있었습니다.
new_range2023년 11월, “Fixing Range by 2027”이라는 제목의 사전 RFC가 열립니다. 이 사전 RFC에는 범위 타입의 몇몇 문제를 해결하는 제안이 있습니다. 두 가지 주요 문제는 범위 타입이 Copy가 아니라는 점과 반복자가 Range* 타입에 직접 구현되어 있다는 점입니다. (다시 돌아오겠다고 했던 내용입니다.) 스레드의 대부분의 댓글은 이 두 문제에 관한 것입니다. 오버플로 동작에 관해서는 아마도 오버플로하지 않고 T::MAX에서 끝나야 한다고 주장하는 댓글이 하나 있지만, 아무도 이를 이어받아 더 논의하지는 않습니다. RFC의 풀 리퀘스트는 2025-12-28에 열렸고, 2026-05-20에 병합된 다음 3550-new-range로 RFC 책에 추가되었습니다. 이 RFC는 RangeFrom의 오버플로 동작을 libs-api 팀이 해결해야 할 미해결 질문으로 남깁니다. libs-api 팀은 2024-06-04 회의에서 이 질문을 다루며, 회의록은 HackMD에서 볼 수 있습니다. 약간의 혼란으로 이 질문을 겉보기에는 두 번 다루게 되었지만, 결국 현재 구현을 대체로 유지하기로 결정한 듯합니다. 단, 오버플로 검사가 켜져 있을 때도 마지막 값은 산출해야 하며, 검사가 꺼져 있을 때는 그냥 오버플로해야 한다고 결정했습니다. @Amanieu는 이 이슈에 요약을 작성했습니다. 요약입니다. 이후 2026-05-28 Rust 1.96.0 릴리스와 함께 새 타입이 안정화되었습니다.
이 글을 작성하는 시점에서 현재 상황은 여기까지입니다.
RangeFrom 역사에서 제가 놓친 중요한 부분이 있다고 생각하신다면 알려 주세요. 이 글을 업데이트하거나 후속 글에 담도록 하겠습니다.
읽어 주셔서 감사합니다. 아마 곧 2부를 쓸 것입니다. 언제나 그렇듯, 의견은 제 소개 페이지에 적힌 이메일로 보내실 수 있습니다.
이 글에 관한 제 _떠듦_을 들어야 했던 모든 분, 특히 글에 대한 피드백을 도와준
에게 사과를 전합니다.
#[deriving(Copy)]도 다시 다룰 수 있습니다 ↩