Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make RangeInclusive just a two-field struct (amend 1192) #1980

Merged
merged 4 commits into from
May 22, 2017
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 64 additions & 30 deletions text/1192-inclusive-ranges.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,21 @@ more dots means more elements.

```rust
pub enum RangeInclusive<T> {
Empty {
at: T,
},
NonEmpty {
start: T,
end: T,
}
pub start: T,
pub end: T,
}

pub struct RangeToInclusive<T> {
pub end: T,
}
```

Writing `a...b` in an expression desugars to `std::ops::RangeInclusive::NonEmpty { start: a, end: b }`. Writing `...b` in an
Writing `a...b` in an expression desugars to
`std::ops::RangeInclusive { start: a, end: b }`. Writing `...b` in an
expression desugars to `std::ops::RangeToInclusive { end: b }`.

`RangeInclusive` implements the standard traits (`Clone`, `Debug`
etc.), and implements `Iterator`. The `Empty` variant is to allow the
`Iterator` implementation to work without hacks (see Alternatives).
etc.), and implements `Iterator`.

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

This `struct` definition is maximally consistent with the existing `Range`.
`a..b` and `a...b` are the same size and have the same fields, just with
the expected difference in semantics.

The range `a...b` contains all `x` where `a <= x && x <= b`. As such, an
inclusive range is non-empty _iff_ `a <= b`. When the range is iterable,
a non-empty range will produce at least one item when iterated. Because
`T::MAX...T::MAX` is a non-empty range, the iteration needs extra handling
compared to a half-open `Range`. As such, `.next()` on an empty range
`y...y` will produce the value `y` and adjust the range such that
`!(start <= end)`. Providing such a range is not a burden on the `T` type as
any such range is acceptable, and only `PartialOrd` is required so
it can be satisfied with an incomparable value `n` with `!(n <= n)`.
A caller must not, in general, expect any particular `start` or `end`
after iterating, and is encouraged to detect empty ranges with
`ExactSizeIterator::is_empty` instead of by observing fields directly.

Note that because ranges are not required to be well-formed, they have a
much stronger bound than just needing successor function: they require a
`b is-reachable-from a` predicate (as `a <= b`). Providing that efficiently
for a DAG walk, or even a simpler forward list walk, is a substantially
harder thing to do than providing a pair `(x, y)` such that `!(x <= y)`.

Implementation note: For currently-iterable types, the initial implementation
of this will have the range become `1...0` after yielding the final value,
as that can be done using the `replace_one` and `replace_zero` methods on
the existing (but unstable) [`Step` trait][step_trait]. It's expected,
however, that the trait will change to allow more type-appropriate `impl`s.
For example, a `num::BitInt` may rather become empty by incrementing `start`,
as `Range` does, since it doesn't to need to worry about overflow. Even for
primitives, it could be advantageous to choose a different implementation,
perhaps using `.overflowing_add(1)` and swapping on overflow, or `a...a`
could become `(a+1)...a` where possible and `a...(a-1)` otherwise.

[step_trait]: https://github.com/rust-lang/rust/issues/27741

# Drawbacks

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

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

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

# Alternatives

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

The `Empty` variant could be omitted, leaving two options:

- `RangeInclusive` could be a struct including a `finished` field.
This makes it easier for the struct to always be iterable, as the extra
field is set once the ends match. But having the extra field in a
language-level desugaring, catering to one library use-case is a little
non-"hygienic". It is especially strange that the field isn't consistent
across the different `...` desugarings. And the presence of the public
field encourages checkinging it, which can be misleading as
`r.finished == false` does not guarantee that `r.count() > 0`.
- `RangeInclusive` could be an enum with `Empty` and `NonEmpty` variants.
This is cleaner than the `finished` field, but still has the problem that
there's no invariant maintained: while an `Empty` range is definitely empty,
a `NonEmpty` range might actually be empty. And requiring matching on every
use of the type is less ergonomic. For example, the clamp RFC would
naturally use a `RangeInclusive` parameter, but because it still needs
to `assert!(start <= end)` in the `NonEmpty` arm, the noise of the `Empty`
vs `NonEmpty` match provides it no value.
- `a...b` only implements `IntoIterator`, not `Iterator`, by
converting to a different type that does have the field. However,
this means that `a.. .b` behaves differently to `a..b`, so
`(a...b).map(|x| ...)` doesn't work (the `..` version of that is
used reasonably often, in the author's experience)
- `a...b` can implement `Iterator` for types that can be stepped
backwards: the only case that is problematic things cases like
`x...255u8` where the endpoint is the last value in the type's
range. A naive implementation that just steps `x` and compares
against the second value will never terminate: it will yield 254
(final state: `255...255`), 255 (final state: `0...255`), 0 (final
state: `1...255`). I.e. it will wrap around because it has no way to
detect whether 255 has been yielded or not. However, implementations
of `Iterator` can detect cases like that, and, after yielding `255`,
backwards-step the second piece of state to `255...254`.

This means that `a...b` can only implement `Iterator` for types that
can be stepped backwards, which isn't always guaranteed, e.g. types
might not have a unique predecessor (walking along a DAG).
- The name of the `end` field could be different, perhaps `last`, to reflect
its different (inclusive) semantics from the `end` (exclusive) field on
the other ranges.

# Unresolved questions

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

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