Skip to content

Commit a01974b

Browse files
authored
Merge pull request #1980 from scottmcm/simpler-rangeinclusive
Make RangeInclusive just a two-field struct (amend 1192)
2 parents 4daf640 + d6ea75b commit a01974b

File tree

1 file changed

+64
-30
lines changed

1 file changed

+64
-30
lines changed

text/1192-inclusive-ranges.md

+64-30
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,21 @@ more dots means more elements.
2727

2828
```rust
2929
pub enum RangeInclusive<T> {
30-
Empty {
31-
at: T,
32-
},
33-
NonEmpty {
34-
start: T,
35-
end: T,
36-
}
30+
pub start: T,
31+
pub end: T,
3732
}
3833

3934
pub struct RangeToInclusive<T> {
4035
pub end: T,
4136
}
4237
```
4338

44-
Writing `a...b` in an expression desugars to `std::ops::RangeInclusive::NonEmpty { start: a, end: b }`. Writing `...b` in an
39+
Writing `a...b` in an expression desugars to
40+
`std::ops::RangeInclusive { start: a, end: b }`. Writing `...b` in an
4541
expression desugars to `std::ops::RangeToInclusive { end: b }`.
4642

4743
`RangeInclusive` implements the standard traits (`Clone`, `Debug`
48-
etc.), and implements `Iterator`. The `Empty` variant is to allow the
49-
`Iterator` implementation to work without hacks (see Alternatives).
44+
etc.), and implements `Iterator`.
5045

5146
The use of `...` in a pattern remains as testing for inclusion
5247
within that range, *not* a struct match.
@@ -57,6 +52,42 @@ now would be `1. ..` i.e. a floating point number on the left,
5752
however, fortunately, it is actually tokenised like `1 ...`, and is
5853
hence an error with the current compiler.
5954

55+
This `struct` definition is maximally consistent with the existing `Range`.
56+
`a..b` and `a...b` are the same size and have the same fields, just with
57+
the expected difference in semantics.
58+
59+
The range `a...b` contains all `x` where `a <= x && x <= b`. As such, an
60+
inclusive range is non-empty _iff_ `a <= b`. When the range is iterable,
61+
a non-empty range will produce at least one item when iterated. Because
62+
`T::MAX...T::MAX` is a non-empty range, the iteration needs extra handling
63+
compared to a half-open `Range`. As such, `.next()` on an empty range
64+
`y...y` will produce the value `y` and adjust the range such that
65+
`!(start <= end)`. Providing such a range is not a burden on the `T` type as
66+
any such range is acceptable, and only `PartialOrd` is required so
67+
it can be satisfied with an incomparable value `n` with `!(n <= n)`.
68+
A caller must not, in general, expect any particular `start` or `end`
69+
after iterating, and is encouraged to detect empty ranges with
70+
`ExactSizeIterator::is_empty` instead of by observing fields directly.
71+
72+
Note that because ranges are not required to be well-formed, they have a
73+
much stronger bound than just needing successor function: they require a
74+
`b is-reachable-from a` predicate (as `a <= b`). Providing that efficiently
75+
for a DAG walk, or even a simpler forward list walk, is a substantially
76+
harder thing to do than providing a pair `(x, y)` such that `!(x <= y)`.
77+
78+
Implementation note: For currently-iterable types, the initial implementation
79+
of this will have the range become `1...0` after yielding the final value,
80+
as that can be done using the `replace_one` and `replace_zero` methods on
81+
the existing (but unstable) [`Step` trait][step_trait]. It's expected,
82+
however, that the trait will change to allow more type-appropriate `impl`s.
83+
For example, a `num::BitInt` may rather become empty by incrementing `start`,
84+
as `Range` does, since it doesn't to need to worry about overflow. Even for
85+
primitives, it could be advantageous to choose a different implementation,
86+
perhaps using `.overflowing_add(1)` and swapping on overflow, or `a...a`
87+
could become `(a+1)...a` where possible and `a...(a-1)` otherwise.
88+
89+
[step_trait]: https://github.com/rust-lang/rust/issues/27741
90+
6091
# Drawbacks
6192

6293
There's a mismatch between pattern-`...` and expression-`...`, in that
@@ -66,10 +97,9 @@ semantically.)
6697

6798
The `...` vs. `..` distinction is the exact inversion of Ruby's syntax.
6899

69-
Having an extra field in a language-level desugaring, catering to one
70-
library use-case is a little non-"hygienic". It is especially strange
71-
that the field isn't consistent across the different `...`
72-
desugarings.
100+
This proposal makes the post-iteration values of the `start` and `end` fields
101+
constant, and thus useless. Some of the alternatives would expose the
102+
last value returned from the iteration, through a more complex interface.
73103

74104
# Alternatives
75105

@@ -83,28 +113,30 @@ This RFC proposes single-ended syntax with only an end, `...b`, but not
83113
with only a start (`a...`) or unconstrained `...`. This balance could be
84114
reevaluated for usefulness and conflicts with other proposed syntax.
85115

86-
The `Empty` variant could be omitted, leaving two options:
87-
88116
- `RangeInclusive` could be a struct including a `finished` field.
117+
This makes it easier for the struct to always be iterable, as the extra
118+
field is set once the ends match. But having the extra field in a
119+
language-level desugaring, catering to one library use-case is a little
120+
non-"hygienic". It is especially strange that the field isn't consistent
121+
across the different `...` desugarings. And the presence of the public
122+
field encourages checkinging it, which can be misleading as
123+
`r.finished == false` does not guarantee that `r.count() > 0`.
124+
- `RangeInclusive` could be an enum with `Empty` and `NonEmpty` variants.
125+
This is cleaner than the `finished` field, but still has the problem that
126+
there's no invariant maintained: while an `Empty` range is definitely empty,
127+
a `NonEmpty` range might actually be empty. And requiring matching on every
128+
use of the type is less ergonomic. For example, the clamp RFC would
129+
naturally use a `RangeInclusive` parameter, but because it still needs
130+
to `assert!(start <= end)` in the `NonEmpty` arm, the noise of the `Empty`
131+
vs `NonEmpty` match provides it no value.
89132
- `a...b` only implements `IntoIterator`, not `Iterator`, by
90133
converting to a different type that does have the field. However,
91134
this means that `a.. .b` behaves differently to `a..b`, so
92135
`(a...b).map(|x| ...)` doesn't work (the `..` version of that is
93136
used reasonably often, in the author's experience)
94-
- `a...b` can implement `Iterator` for types that can be stepped
95-
backwards: the only case that is problematic things cases like
96-
`x...255u8` where the endpoint is the last value in the type's
97-
range. A naive implementation that just steps `x` and compares
98-
against the second value will never terminate: it will yield 254
99-
(final state: `255...255`), 255 (final state: `0...255`), 0 (final
100-
state: `1...255`). I.e. it will wrap around because it has no way to
101-
detect whether 255 has been yielded or not. However, implementations
102-
of `Iterator` can detect cases like that, and, after yielding `255`,
103-
backwards-step the second piece of state to `255...254`.
104-
105-
This means that `a...b` can only implement `Iterator` for types that
106-
can be stepped backwards, which isn't always guaranteed, e.g. types
107-
might not have a unique predecessor (walking along a DAG).
137+
- The name of the `end` field could be different, perhaps `last`, to reflect
138+
its different (inclusive) semantics from the `end` (exclusive) field on
139+
the other ranges.
108140

109141
# Unresolved questions
110142

@@ -114,3 +146,5 @@ None so far.
114146

115147
* In rust-lang/rfcs#1320, this RFC was amended to change the `RangeInclusive`
116148
type from a struct with a `finished` field to an enum.
149+
* In rust-lang/rfcs#1980, this RFC was amended to change the `RangeInclusive`
150+
type from an enum to a struct with just `start` and `end` fields.

0 commit comments

Comments
 (0)