I would find this very confusing. step_by
isn't a general every_nth
iterator adaptor. It specifically applies to ranges, and specifies what value should be added each iteration on the way from begin
to end
. Every call to next
on a range checks if begin
is equal to or past end
. If so, it returns None
. If not, it returns Some(begin)
and updates begin
in the range to be begin + step
. I think a negative step should work exactly the same way, with begin
naturally being greater than end
to start.
Well I guess that's a matter of perspective. I just wanted to say how I felt about it But in the end it doesn't matter much, once a form will be imposed it will be picked up real fast by everyone.
I donāt consider whether the first or the second of these is currently intuitive as particularly relevant (people always adapt with time). With rules, both are clear:
// Ranges always go up. `step` indicates step direction.
(0..5).step(-1);
// Ranges may go up or down. Step direction must agree
// with the range.
(5..0).step(-1);
What is relevant is how inverting the range interacts with the [a, b)
set notation by inverting it. Especially if the triple ...
is added meaning [a, b]
. This is much less obvious than the number order.
// Sometimes the right is skipped. Sometimes the left.
(0..5).step(+1) // [0,5)
(0..5).step(-1) // (0,5] in reverse order
(0...5).step(+1) // [0,5]
(0...5).step(-1) // [0,5] in reverse order
// Only the right number is ever skipped.
(0..5).step(+1) // [0,5)
(5..0).step(-1) // [5,0)
(0...5).step(+1) // [0,5]
(5...0).step(-1) // [5,0]
Of note: English speakers always read text left to right. Subverting the order requires extra krow (work: read right to left).
Unless we want step_by(_)
to include an if
, itās going to be (5..0).step_by(-1)
.
@llogiq, thatās not going to work for unsigned types given the current step_by
definition.
True. Would it be possible to overload it for integral types - e.g. with i32 for u32 and so on?
We could choose a different Step definition as I noted above:
trait Step {
type Step = Self;
fn step(&self, by: &Step) -> Option<Self>;
fn steps_between(start: &Self, end: &Self, by: &Step) -> Option<usize>;
}
Also, the problem isnāt having an if statement in step_by
, but having one in the iterator. If we allowed (5..0).step_by(1)
, one would expect (5..0)
to behave the same. In (5..0).step_by(1)
, we can have some logic to create a āreverse stepā if necessary when the iterator is constructed so thereās no performance issue. However, we donāt have the chance to do this when constructing (5..0)
so weād have to check the direction on every iteration.
What if we had up_by
and down_by
instead of step_by
on unsigned types? This would remove all additional checks even on creation.
Signed types could then have step_by
which would create an UpBy
or DownBy
depending on argument sign.
Edit: Just wanted to add: This would also have the benefit of making the intent clearer. Lints could easily pick up up_by or down_by with negative constants.
In principle this is fine since weāre already relying on iterator adaptors being inlined into the ground, and non-constant steps are, to my knowledge, the exception.
I use MATLAB a lot for prototyping machine learning algorithms, and find that the inclusive range is most intuitive from an application/algorithm development perspective. That is:
1:5 // {1, 2, 3, 4, 5} => mathematically, [1,5] for integers
1:1:5 // same as above
5:1 // empty array []; range has step = 1 by default
5:-1:1 // {5, 4, 3, 2, 1}
etc.
MATLAB also has similar behaviour for floating point numbers (actually any number in MATLAB is double by default).
If Rust were to adopt the inclusive range like in MATLAB, it would eliminate any differences between a .step_by(-1)
and .rev()
. Not to mention, that will also make Rust very attractive when porting the prototypes to operational applications.
Disclaimer: I'm a newbie to Rust and still unfamiliar with all its features and progress to date. Also, not being a computer scientist, I'm unaware of the benefits of a range as mentioned in ...
Late to the party, but hereās two cents:
I, for one, am happy with the current implementation. It may be counter-intuitive at first, but once one learns how it works, there are very few āgotchasā. The current implementation guarantees (0..100).step_by(x).next()
to be Some(0)
or None
, while implementing step_by(-x)
as step_by(x).rev()
could give a 100. Also, what does (0..).step_by(-1)
return with that implementation? I want to be able to use (0..).step_by(prime)
occasionally.
Additionally, I am against splitting the implementation into two parts when one will do, but that is just an opinion.
in case you have variables. a..b could then either be a forward or a reverse range
How about fix
method to flip the range if necessary to restore b ā„ a invariant?
impl Range<T> {
fn fix(self) -> Self {
if self.to >= self.from {
self
} else {
Range { from: self.to, to: self.from }
}
}
}
Would it be useful, how do you think?
Perhaps, but the problem here is the exact inverse: Some have suggested that ranges behave as if they were .fix()
ed all the time. This has a few unfortunate consequences in some settings.
What kind of error are you thinking?
EDIT: s/are/were -- I didn't realize how old this thread was
It could only possibly panic, right?
Ideally compile-time checks would be great, but we donāt really have any great tooling for that.
We already have two clippy lints (one for step_by(0)
and one for negative bounds without step_by(..)
). It should be fairly straightforward to extend this to cover other cases, but no one has done it yet.
Personally, I think this should be valid but yield no elements (since 0 is already past 10 when stepping by a negative amount).
This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.