Skip to content

Commit 2e54cff

Browse files
committed
improve wording, more discussion
- improve wording of Migrating section - consider `Index<Range<_>>` - more prominent discosure of T-libs-api decisions - more discussion fo drawbacks - better address the "use legacy types as iterators" alternative - add inherent `map` rationale - address implicit conversions
1 parent bf83d5b commit 2e54cff

File tree

1 file changed

+159
-16
lines changed

1 file changed

+159
-16
lines changed

text/3550-new-range.md

Lines changed: 159 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -92,47 +92,61 @@ range.for_each(|n| {
9292
There exist `From` impls for converting from the new range types to the legacy range types.
9393

9494
### Migrating
95+
[migrating]: #migrating
9596

96-
`cargo fix --edition 2024` can handle migrating many use cases to the new range types. This includes most places where a `RangeBounds` or `Iterator` is expected:
97+
In many cases, no changes need to be made at all. This includes most places where a `RangeBounds` or `IntoIterator` is expected:
9798
```rust
9899
pub fn takes_range(range: impl std::ops::RangeBounds<usize>) { ... }
99100
takes_range(0..5); // No changes necessary
101+
102+
pub fn takes_iter(range: impl IntoIterator<usize>) { ... }
103+
takes_iter(0..5); // No changes necessary
100104
```
101105

102106
And most places where `Iterator` methods were used directly on the range:
103107
```rust
104108
for n in (0..5).rev() { ... } // No changes necessary
109+
for n in (0..5).map(|x| x * 2) { ... } // No changes necessary
110+
```
111+
112+
In other cases, `cargo fix --edition 2024` will insert `.into_iter()` as necessary:
113+
114+
```rust
115+
pub fn takes_iter(range: impl Iterator<usize>) { ... }
116+
takes_iter((0..5).into_iter()); // Add `.into_iter()`
105117

106118
// Before
107119
(0..5).for_each(...);
108120
// After
109121
(0..5).into_iter().for_each(...); // Add `.into_iter()`
110122
```
111123

112-
But it is impossible to generally handle everything, and there are cases which fall back to converting to the legacy types:
124+
Or fall back to converting to the legacy types:
113125

114126
```rust
115127
// Before
116128
pub fn takes_range(range: std::ops::Range<usize>) { ... }
117129
takes_range(0..5);
118130
// After
119131
pub fn takes_range(range: std::ops::range::legacy::Range<usize>) { ... }
120-
takes_range(std::ops::range::legacy::Range::from(0..5));
132+
takes_range((0..5).to_legacy());
121133
```
122134

123-
To reduce the need for conversions, we recommend the following:
135+
#### Libraries
136+
137+
To reduce the need for the above conversions, we recommend making the following changes to libraries:
124138

125-
- Change any function parameters from legacy `Range*` types to `impl RangeBounds<_>`
126-
Or where `RangeBounds` is not applicable, `impl Into<Range*>`
139+
- Change any function parameters from legacy `Range*` types to `impl Into<Range*>`
140+
Or if applicable, `impl RangeBounds<_>`
127141

128142
```rust
129143
// Before
130144
pub fn takes_range(range: std::ops::Range<usize>) { ... }
131145

132146
// After
133-
pub fn takes_range(range: impl std::ops::RangeBounds<usize>) { ... }
134-
// Or
135147
pub fn takes_range(range: impl Into<std::ops::range::legacy::Range<usize>>) { ... }
148+
// Or
149+
pub fn takes_range(range: impl std::ops::RangeBounds<usize>) { ... }
136150
```
137151

138152
- Change any trait bounds that assume `Range*: Iterator` to use `IntoIterator` instead
@@ -185,6 +199,19 @@ range.for_each(|n| {
185199
});
186200
```
187201

202+
- When ranges are used with `std::ops::Index`, add impls for the new range types
203+
204+
```rust
205+
// Before
206+
use std::ops::{Index, Range};
207+
impl Index<Range<usize>> for Bar { ... }
208+
209+
// After
210+
use std::ops::{Index, range::Range, range::legacy};
211+
impl Index<legacy::Range<usize>> for Bar { ... }
212+
impl Index<Range<usize>> for Bar { ... }
213+
```
214+
188215
## Diagnostics
189216

190217
There is a substantial amount of educational material in the wild which assumes the the range types implement `Iterator`. If a user references this outdated material, it is important that compiler errors guide them to the new solution.
@@ -205,6 +232,8 @@ error[E0599]: `Range<usize>` is not an iterator
205232
# Reference-level explanation
206233
[reference-level-explanation]: #reference-level-explanation
207234

235+
**Note:** The exact names and module paths in this RFC are for demonstration purposes only, and can be finalized by _T-libs-api_ after the proposal is accepted.
236+
208237
Add replacement types only for the current `Range`, `RangeFrom`, and `RangeInclusive`.
209238

210239
The [**Range Expressions** page in the Reference](https://doc.rust-lang.org/reference/expressions/range-expr.html) will change to read as follows
@@ -356,12 +385,62 @@ impl<Idx> From<range::legacy::RangeFrom<Idx>> for range::RangeFrom<Idx>
356385
impl<Idx> TryFrom<range::legacy::RangeInclusive<Idx>> for range::RangeInclusive<Idx>
357386
```
358387

388+
The new types should have inherent methods to match the most common usages of `Iterator` methods. `map` and `rev` are the bare minimum; we leave the exact set to be finalized by _T-libs-api_ after the proposal is accepted.
389+
390+
```rust
391+
impl<Idx> Range<Idx> {
392+
/// Shorthand for `.into_iter().map(...)`
393+
pub fn map<B, F>(self, f: F) -> iter::Map<<Self as IntoIterator>::IntoIter, F>
394+
where
395+
Self: IntoIterator,
396+
F: FnMut(Idx) -> B,
397+
{
398+
self.into_iter().map(f)
399+
}
400+
401+
/// Shorthand for `.into_iter().rev()`
402+
pub fn rev(self) -> iter::Rev<<Self as IntoIterator>::IntoIter>
403+
where
404+
Self: IntoIterator,
405+
<Self as IntoIterator>::IntoIter: DoubleEndedIterator,
406+
{
407+
self.into_iter().rev()
408+
}
409+
}
410+
```
411+
412+
Finally, the new types should have an inherent method for converting new types to the legacy types. This can avoid the type inference issues associated with `.into()`, helping to keep auto-migrations concise.
413+
```rust
414+
impl<Idx> Range<Idx> {
415+
/// Shorthand for `legacy::Range::from(self)`
416+
pub fn to_legacy(self) -> legacy::Range<Idx> {
417+
legacy::Range::from(self)
418+
}
419+
}
420+
```
421+
359422
# Drawbacks
360423
[drawbacks]: #drawbacks
361424

362-
This change has the potential to cause a significant amount of churn in the ecosystem. The largest source of this churn will be cases where ranges are assumed to be `Iterator`. Unlike normal syntax churn, changes will be required to support the new range types, even on older editions.
425+
This change has the potential to cause a significant amount of churn in the ecosystem. There are two main sources of churn:
426+
- where ranges are assumed to be `Iterator`
427+
- when `Index<legacy::Range<_>>` is implemented directly
363428

364-
For example: while experimenting with implementing the library part of these changes, we had to modify [`quote`](https://github.com/pitaj/quote/commit/44feebf0594b255a511ff20890a7acbf4d6aeed1) and [`rustc-rayon`](https://github.com/pitaj/rustc-rayon/commit/e76e554512ce25abb48f4118576ede5d7a457918).
429+
Changes will be required to support the new range types, even on older editions. See the [migrating section](#migrating) for specifics.
430+
431+
### Ranges assumed to be `Iterator`
432+
433+
This is not uncommon in the ecosystem. For instance, both [`rustc-rayon`](https://github.com/pitaj/rustc-rayon/commit/e76e554512ce25abb48f4118576ede5d7a457918) and [`quote`](https://github.com/pitaj/quote/commit/44feebf0594b255a511ff20890a7acbf4d6aeed1) needed patches for this during experimentation.
434+
435+
### `impl Index<Range<_>> for X`
436+
437+
A [Github search for this pattern](https://github.com/search?type=code&q=language%3Arust+NOT+is%3Afork+%28%22Index%3CRange%3C%22+OR+%22Index%3Cops%3A%3ARange%3C%22+OR+%22Index%3Cstd%3A%3Aops%3A%3ARange%3C%22+OR+%22Index%3Ccore%3A%3Aops%3A%3ARange%3C%22+OR+%22Index%3CRangeInclusive%3C%22+OR+%22Index%3Cops%3A%3ARangeInclusive%3C%22+OR+%22Index%3Cstd%3A%3Aops%3A%3ARangeInclusive%3C%22+OR+%22Index%3Ccore%3A%3Aops%3A%3ARangeInclusive%3C%22+OR+%22Index%3CRangeFrom%3C%22+OR+%22Index%3Cops%3A%3ARangeFrom%3C%22+OR+%22Index%3Cstd%3A%3Aops%3A%3ARangeFrom%3C%22+OR+%22Index%3Ccore%3A%3Aops%3A%3ARangeFrom%3C%22%29) yields 784 files, almost all of which appear to be true matches. It's hard to say how many of those are published libraries, but it does indicate that this could have a significant impact.
438+
439+
## Mitigation
440+
441+
To mitigate these drawbacks, we recommend introducing and stabilizing an MVP of the new types as soon as possible, well before Edition 2024 releases (even before the implementation of the syntax feature is complete). This will give libraries time to issue updates supporting the new range types.
442+
443+
Some users may depend on libraries that are not updated before Edition 2024. These users do not just have to accept adding explicit conversions to their code. They also have the option to stay on a prior edition.
365444

366445
# Rationale and alternatives
367446
[rationale-and-alternatives]: #rationale-and-alternatives
@@ -370,9 +449,14 @@ For example: while experimenting with implementing the library part of these cha
370449

371450
`Copy` iterators are a large footgun. It was decided to [remove `Copy` from all iterators back in 2015](https://github.com/rust-lang/rust/pull/21809), and that decision is unlikely to be reversed.
372451

373-
Another spin on this is to specialize `IntoIterator` on these range types and lint whenever the `Iterator` impl is used. However, that would likely be a breaking change and this kind of specialization may never be stabilized (and might not even be sound).
452+
That said, there are a few possibilities:
453+
- Sophisticated lint to catch when an iterator is problematically copied
454+
- Language or library feature to allow `Copy` structs to have certain non-`Copy` fields
455+
- Specialize `IntoIterator` on these range types and lint whenever the `Iterator` impl is used
374456

375-
Neither of these approaches would resolve the issue of `RangeInclusive` being larger than necessary for range purposes.
457+
None of these approaches would resolve the following serious issues:
458+
- `RangeInclusive` being larger than necessary for range purposes
459+
- Incorrect `ExactSizeIterator` implementations
376460

377461
## Name the new types something besides `Range`
378462

@@ -390,7 +474,62 @@ We believe that it is best to keep the `Range` naming for several reasons:
390474

391475
We could choose to make `new_range.into_iter()` resolve to a legacy range type. This would reduce the number of new types we need to add to the standard library.
392476

393-
But the legacy range types have a much larger API surface than other `Iterator`s in the standard library, which typically only implement the various iterator traits and maybe have a `remainder` method. Using the legacy range types this way would break convention and could reduce optimization potential for the iterators, possibly limiting performance.
477+
But the legacy range types have a much larger API surface than other `Iterator`s in the standard library, which typically only implement the various iterator traits and maybe have a `remainder` method. Specifically, there are no iterator types in the standard library which have public fields. Nor do any implement `PartialEq`, `Eq`, `Hash`, `Index`, or `IndexMut`.
478+
479+
`RangeInclusive` especially must take care with equality, hashing, and indexing because it can be exhausted. By removing those impls from the iterator for it, we can prevent that misuse entirely.
480+
481+
One of the strongest arguments for new types is the incorrect `ExactSizeIterator` implementations for `Range<u32 | i32>` and `RangeInclusive<u16 | i16>`. These can be excluded if new iterator types are introduced.
482+
483+
Finally, the cost of adding these iterator types is extremely low, given we're already adding a set of new types for the ranges themselves.
484+
485+
## Inherent `map` should map the bounds, not return an iterator
486+
487+
Some argue that inherent `map` should not return an iterator. Some say that they may expect it to map each bound individually (`(1..11).map(|x| x*2)` -> `2..22`). Others say these methods should return `IntoIterator` types instead.
488+
489+
However, making them return an iterator has many benefits:
490+
- Matches existing behavior
491+
- Reduces code churn
492+
- Act as an entry point for other iterator methods
493+
494+
Adding these convenience methods is unlikely to cause confusion because of how common this pattern already is (if anything, the opposite is true). Plus, it's pretty easy to tell based on the function signature what is going on, and it's simple to document.
495+
496+
Changing the meaning of `(1..11).map(...)` is a huge hazard. There is a lot of existing code, documentation, etc that uses it in the `Iterator` sense. It would be incredibly confusing, especially to a newcomer, to have it do something totally different between editions. Especially since in many cases it could silently change meaning:
497+
498+
```rust
499+
// Edition 2021
500+
for n in (1..11).map(|n| n*2) {
501+
// n = 2, 4, 6, ...., 16, 18, 20
502+
}
503+
// Edition 2024?
504+
for n in (1..11).map(|n| n*2) {
505+
// n = 2, 3, 4, 5, 6, 7, ...., 15, 16, 17, 18, 19, 20, 21
506+
}
507+
```
508+
509+
If there is demand for a method that maps the bounds, it should be added under a different name, such as `map_bounds` , perhaps even as a method on `RangeBounds`.
510+
511+
## Implicit conversions (coercions)
512+
513+
This proposal specifically avoids involving any form of implicit conversion. Adding coercions from the new to legacy types would have a few benefits:
514+
515+
- Avoid explicit conversions when migrating automatically to Edition 2024
516+
- Few (if any) library changes needed to support the new types
517+
518+
Coercions would effectively eliminate the main drawback of this RFC. However, adding implicit conversions has severe drawbacks of its own:
519+
520+
- Makes it harder to reason about code
521+
- Further blurs the line between language and library
522+
- Affects type inference
523+
524+
In this specific case, the coercion would also need to be considered during trait resolution to be significantly useful, which is not currently done in other cases like deref coercion.
525+
526+
### Range literal
527+
528+
We could treat range expressions as a kind of literal, and only "coerce" them into the legacy range types at the point of the range syntax. Similar to integer literals, the concrete type would be chosen based on context, like how `4` can be used anywhere expecting any integer type.
529+
530+
This would have fewer serious downsides than coercions, but both approaches add a large cost for implementation in the compiler.
531+
532+
We don't consider the downsides of either approach to be justified given the relative rarity of libraries needing changes in the first place, the ease of adding explicit conversions when necessary, and the option for users to continue to use prior editions while waiting for library support.
394533

395534
# Prior art
396535
[prior-art]: #prior-art
@@ -400,10 +539,14 @@ The [copy-range](https://docs.rs/copy-range) crate provides types similar to tho
400539
# Unresolved questions
401540
[unresolved-questions]: #unresolved-questions
402541

403-
The set of inherent methods copied from `Iterator` present on the new range types is left open for the **libs-api** team to decide after this proposal is accepted and before stabilization.
542+
We leave the following items to be decided by the **libs-api** team after this proposal is accepted and before stabilization:
404543

405-
- Should other range-related items (like `RangeBounds`) also be moved under `std::ops::range`?
406-
- Should `RangeFrom` even implement `IntoIterator`, or should it require an explicit `.iter()` call? Using it as an iterator [can be a footgun](https://github.com/rust-lang/libs-team/issues/304), usually people want `start..=MAX` instead.
544+
- The set of inherent methods copied from `Iterator` present on the new range types
545+
- The exact module paths and type names
546+
+ `std::ops::range::` is long and heavily nested, perhaps `std::range::` would be better?
547+
+ `IterRange`, `IterRangeInclusive` or just `Iter`, `IterInclusive`?
548+
- Should other range-related items (like `RangeBounds`) also be moved under the `range` module?
549+
- Should `RangeFrom` even implement `IntoIterator`, or should it require an explicit `.iter()` call? Using it as an iterator [can be a footgun](https://github.com/rust-lang/libs-team/issues/304), usually people want `start..=MAX` instead. Also, it is inconsistent with `RangeTo`, which doesn't implement `IntoIterator` either.
407550
- Should there be a way to get an iterator that modifies the range in place, rather than taking the range by value? That would allow things like `range.by_ref().next()`.
408551
- Should there be an infallible conversion from legacy to new `RangeInclusive`?
409552
```rust

0 commit comments

Comments
 (0)