Skip to content

Commit cc4ef57

Browse files
committed
Address comments: move more details to design, address post-iteration end
1 parent b9ecb4e commit cc4ef57

File tree

1 file changed

+41
-34
lines changed

1 file changed

+41
-34
lines changed

text/1192-inclusive-ranges.md

+41-34
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,26 @@ This `struct` definition is maximally consistent with the existing `Range`.
5757
`a..b` and `a...b` are the same size and have the same fields, just with
5858
the expected difference in semantics.
5959

60+
The range `a...b` contains all `x` where `a <= x && x <= b`. As such, an
61+
inclusive range is non-empty _iff_ `a <= b`. When the range is iterable,
62+
a non-empty range will produce at least one item when iterated. Because
63+
`T::MAX...T::MAX` is a non-empty range, the iteration needs extra handling
64+
compared to a half-open `Range`. As such, `.next()` on an empty range
65+
`y...y` will produce the value `y` and replace the range with the canonical
66+
empty range for the type. Using methods on the the existing (but unstable)
67+
[`Step` trait][step_trait], that's `1...0` for all currently-iterable
68+
value types. Providing such a range is not a burden on the `T` type as
69+
any such range is acceptable, and only `PartialOrd` is required so
70+
it can be satisfied with an incomparable value `n` with `!(n <= n)`.
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 that providing a pair `(x, y)` such that `!(x <= y)`.
77+
78+
[step_trait]: https://github.com/rust-lang/rust/issues/27741
79+
6080
# Drawbacks
6181

6282
There's a mismatch between pattern-`...` and expression-`...`, in that
@@ -66,32 +86,9 @@ semantically.)
6686

6787
The `...` vs. `..` distinction is the exact inversion of Ruby's syntax.
6888

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
89+
This proposal makes the post-iteration values of the `start` and `end` fields
90+
constant, and thus useless. Some of the alternatives would expose the
91+
last value returned from the iteration, through a more complex interface.
9592

9693
# Alternatives
9794

@@ -110,20 +107,30 @@ reevaluated for usefulness and conflicts with other proposed syntax.
110107
field is set once the ends match. But having the extra field in a
111108
language-level desugaring, catering to one library use-case is a little
112109
non-"hygienic". It is especially strange that the field isn't consistent
113-
across the different `...` desugarings.
110+
across the different `...` desugarings. And the presence of the public
111+
field encourages checkinging it, which can be misleading as
112+
`r.finished == false` does not guarantee that `r.count() > 0`.
114113
- `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.
114+
This is cleaner than the `finished` field, but still has the problem that
115+
there's no invariant maintained: while an `Empty` range is definitely empty,
116+
a `NonEmpty` range might actually be empty. And requiring matching on every
117+
use of the type is less ergonomic. For example, the clamp RFC would
118+
naturally use a `RangeInclusive` parameter, but because it still needs
119+
to `assert!(start <= end)` in the `NonEmpty` arm, the noise of the `Empty`
120+
vs `NonEmpty` match provides it no value.
122121
- `a...b` only implements `IntoIterator`, not `Iterator`, by
123122
converting to a different type that does have the field. However,
124123
this means that `a.. .b` behaves differently to `a..b`, so
125124
`(a...b).map(|x| ...)` doesn't work (the `..` version of that is
126125
used reasonably often, in the author's experience)
126+
- Different choices for the end of iteration are also possible. While neither
127+
`start` nor `end` can always have the last value of the iteration while
128+
still producing an empty range (consider what happens with `MIN...MIN`
129+
and `MAX...MAX`), they could be closer. For example, `a...a` could become
130+
`(a+1)...a` where possible, and `a...(a-1)` otherwise.
131+
- The name of the `end` field could be different, perhaps `last`, to reflect
132+
its different (inclusive) semantics from the `end` (exclusive) field on
133+
the other ranges.
127134

128135
# Unresolved questions
129136

0 commit comments

Comments
 (0)