range_step_inclusive(a, b, c) => (a .. b).inclusive().step(c)
This unifies our “range” syntax as well as continues our trend of “composition over combinations”. I started poking at a proper implementation/RFC for this change but got caught up in details of the precise/traits/bounds these would require, and other fiddly details. So I’m making a quick issue on this topic just to get the idea out there for more people to poke at.
Some sketches of ideas:
// basic trait we need for `.step()`. Takes Amount as an input because
// it's not clear if there's a "right" choice for Amount, and if it should
// be unique. e.g. one could support StepBy i16 and u16 on a i16
// (i16 is more natural to StepBy, but insufficient to span its own range).
// Mandating only usize seems otherwise wrong.
trait StepBy<Amount> : Step {
// equivalent to calling `step` `amount` times. Returns Err
// if some boundary condition like overflow occurs.
fn step_by(&mut self, amount: &Amount) -> Result<(), ()>;
// equivalent to calling `step_back` `amount` times. Returns Err
// if some boundary condition like underflow occurs.
fn step_back_by(&mut self, amount: &Amount) -> Result<(), ()>;
// steps_between_by???
}
Should step().inclusive() and inclusive().step() work?
Can/should Inclusive be written as a wrapper that accepts either Range or RangeStep generically and does the right thing? Thus avoiding the need to create a RangeStepInclusive type?
Should .step(x) permit x to be negative?
Should that then “work” for start > stop? Or should such behaviour be relegated to "use rev"?
If we allow backwards ranges without rev then you need some way to check for x being “negative”.
If we don’t allow it, at what level do we enforce it? Unspecified behaviour? Still require a notion of negative just to check?
Should inclusive be encouraged for “other” range uses (e.g. overloaded array indexing) or “only” for iteration? (Note, I believe inclusive needs an extra boolean flag to remember if it has yielded stop yet).
Another question is: we could conceivably support ... for inclusive ranges, since we already use that notation with that meaning in pattern matching. (This came up in a recent thread that I can’t find right now). Should we do that instead of .inclusive?
well there would be differences between .rev().step(x) and .step(x).rev() if x is not a divisor of the range length. so .step(-x) would probably be .rev().step(x), but might cause more confusion than it's worth.
I assume inclusive/step would take their ranges by value so the output would be None (and the compiler would warn that the output of x.inclusive() is unused).
I assume that inclusive() and step() would return new iterators, not modify an existing iterator in-place. So the answer should be that the x.inclusive(); statement would have no effect, and this program will print None.
@theme the fields of range are public, so the effects of calling next and next_back are well-observable: next moves start forward, next_back moves stop backward. So calling next will make the range 1…1 which should work through inclusive.
A step operator would be nice also. Here, % could be considered similar to modulus, though I’m not proposing this. It looks really weird even if it could work.
range_step(a, b, c) => a..b%c
Additionally, I am not sure how to implement .inclusive()exactly correctly without a performance hit. I believe that even a slight performance hit on range (say, 1 additional instruction per iteration) would have a measurable effect due to the fact that some programs spend most of their time looping in ranges.
Examples of hard-to-handle code:
This should output None:
let mut x = 0_u8..1_u8;
let _ = x.next();
let _ = x.next();
let mut y = x.inclusive();
println!("{:?}", y.next());
This should also output None:
let mut x = 0_u8..1_u8;
for _ in 0_i32..256_i32 {
let _ = x.next();
}
let mut y = x.inclusive();
println!("{:?}", y.next());
Wouldn’t it make more sense to prevent calling these after the range starts iterating? We can have two range types like Range<Initial> and Range<Started> and implement .inclusive() and .step() only for Range<Initial>. HTTP library Hyper does this to prevent changing HTTP headers after response has been sent.
If you wanted to do that, you’d probably want Range and RangeIter. where Range::iter(self) returns a RangeIter (transform methods such as inclusive() and step() would be defined on Range). However, to make this ergonomic, we’d probably want IntoIterator first.
It should be noted that @aturon is changing the way Range iteration works. I think Step is dying altogether, and you just need to impl Iterator for Range<MyType>.