Skip to content

Commit 6f20623

Browse files
committed
Amend 1192 (RangeInclusive) to just be a two-field struct
Rationale: 1. Having the variants make trying to use a RangeInclusive unergonomic. For example, `fn clamp(self, range: RangeInclusive<Self>)` is forced to deal with the `Empty` case. 2. The variants don't actually provide any offsetting safety or additional performance, since everything is pub so it's totally possible for a "`NonEmpty`" range to contain no elements. 3. This makes it more consistent with (half-open) `Range`. 4. The extra flag/variant is not actually needed in order to make it iterable, even using the existing Step trait. And supposing a more cut-down trait, generating `a, b` such that `!(a <= b)` is easier than other fundamental requirements of safe range iterability.
1 parent f0883a6 commit 6f20623

File tree

1 file changed

+49
-28
lines changed

1 file changed

+49
-28
lines changed

text/1192-inclusive-ranges.md

+49-28
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,17 @@ 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`
@@ -57,6 +53,10 @@ now would be `1. ..` i.e. a floating point number on the left,
5753
however, fortunately, it is actually tokenised like `1 ...`, and is
5854
hence an error with the current compiler.
5955

56+
This `struct` definition is maximally consistent with the existing `Range`.
57+
`a..b` and `a...b` are the same size and have the same fields, just with
58+
the expected difference in semantics.
59+
6060
# Drawbacks
6161

6262
There's a mismatch between pattern-`...` and expression-`...`, in that
@@ -66,10 +66,32 @@ semantically.)
6666

6767
The `...` vs. `..` distinction is the exact inversion of Ruby's syntax.
6868

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.
69+
Not having a separate marker for `finished` or `empty` implies a requirement
70+
on `T` that it's possible to provide values such that `b...a` is an empty
71+
range. But a separate marker is a false invariant: whether a `finished`
72+
field on the struct or a `Empty` variant of an enum, the range `10...0` still
73+
desugars to a `RangeInclusive` with `finised: false` or of the `NonEmpty`
74+
variant. And the fields are public, so even fixing the desugar cannot
75+
guarantee the invariant. As a result, all code using a `RangeInclusive`
76+
must still check whether a "`NonEmpty`" or "un`finished`" is actually finished.
77+
The "can produce an empty range" requirement is not a hardship. It's trivial
78+
for anything that can be stepped forward and backward, as all things which are
79+
iterable in `std` are today. But ther are other possibilities as well. The
80+
proof-of-concept implementation for this change is done using the `replace_one`
81+
and `replace_zero` methods of the (existing but unstable) `Step` trait, as
82+
`1...0` is of course an empty range. Something weirder, like walking along a
83+
DAG, could use the fact that `PartialOrd` is sufficient, and produce a range
84+
similar in character to `NaN...NaN`, which is empty as `(NaN <= NaN) == false`.
85+
The exact details of what is required to make a range iterable is outside the
86+
scope of this RFC, and will be decided in the [`step_by` issue][step_by].
87+
88+
Note that iterable ranges today have a much stronger bound than just
89+
steppability: they require a `b is-reachable-from a` predicate (as `a <= b`).
90+
Providing that efficiently for a DAG walk, or even a simpler forward list
91+
walk, is a substantially harder thing to do that providing a pair `(x, y)`
92+
such that `!(x <= y)`.
93+
94+
[step_by]: https://github.com/rust-lang/rust/issues/27741
7395

7496
# Alternatives
7597

@@ -83,28 +105,25 @@ This RFC proposes single-ended syntax with only an end, `...b`, but not
83105
with only a start (`a...`) or unconstrained `...`. This balance could be
84106
reevaluated for usefulness and conflicts with other proposed syntax.
85107

86-
The `Empty` variant could be omitted, leaving two options:
87-
88108
- `RangeInclusive` could be a struct including a `finished` field.
109+
This makes it easier for the struct to always be iterable, as the extra
110+
field is set once the ends match. But having the extra field in a
111+
language-level desugaring, catering to one library use-case is a little
112+
non-"hygienic". It is especially strange that the field isn't consistent
113+
across the different `...` desugarings.
114+
- `RangeInclusive` could be an enum with `Empty` and `NonEmpty` variants.
115+
This is cleaner than the `finished` field, but makes all uses of the
116+
type substantially more complex. For example, the clamp RFC would
117+
naturally use a `RangeInclusive` parameter, but then the
118+
unreliable-`Empty` vs `NonEmpty` distinction provides no value. It does
119+
prevent looking at `start` after iteration has completed, but that is
120+
of questionable value when `Range` allows it without issue, and disallowing
121+
looking at `start` while allowing looking at `end` feels inconsistent.
89122
- `a...b` only implements `IntoIterator`, not `Iterator`, by
90123
converting to a different type that does have the field. However,
91124
this means that `a.. .b` behaves differently to `a..b`, so
92125
`(a...b).map(|x| ...)` doesn't work (the `..` version of that is
93126
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).
108127

109128
# Unresolved questions
110129

@@ -114,3 +133,5 @@ None so far.
114133

115134
* In rust-lang/rfcs#1320, this RFC was amended to change the `RangeInclusive`
116135
type from a struct with a `finished` field to an enum.
136+
* In rust-lang/rfcs#TODO, this RFC was amended to change the `RangeInclusive`
137+
type from an enum to a struct with just `start` and `end` fields.

0 commit comments

Comments
 (0)