From: "zverok (Victor Shepelev) via ruby-core" Date: 2024-08-22T06:44:24+00:00 Subject: [ruby-core:118916] [Ruby master Feature#18368] Range#step semantics for non-Numeric ranges Issue #18368 has been updated by zverok (Victor Shepelev). @matz I can certainly implement the specialization, but just to clarify: are there any evidence that people use `("a".."z").step(3)` in any meaningful way? (Unfortunately, I have no way of doing the investigation myself, but probably those with access to ���search through all existing gems code��� server might shed some light on it?) What I am concerned about is that having it specialized just for strings has an intuition-breaking consequences and would certainly be perceived like a ���bug��� somewhere down the road. We had the same with switching from `include?` to `cover?` as a `Range#===` implementation: * In 2.6, strings were excluded from the change * ...but after several reports, it turned out that it is better to make string ranges behave consistently with everything else, which happened in 2.7. In general, Range already have a history of such specializations (like specifically considering `Time` ���linear��� value in 2.3 to fix `case Time.now ...` behavior, before generic switching to `cover?` fixed it consistenlty), and I believe it is mostly confusing to the language���s users: some core class behaving a ���special��� way that can���t be imitated by any other class, breaking all and every ���duck typing��� intuition. So, a big question for me is if the specialization is justified by some existing use cases, or by a generic caution against breaking things (and in the latter case, who guarantees that in some codebase nobody have relied on some _custom_ object range and integer step behavior?.. IDK) So, I see these options here: 1. Leave the ���consistent first��� option: no specialization for `String` (unless there is a strong proof that some popular gem or a widespread idiom is hurt by this); 2. Specialize it for strings to only work with numeric steps, and document that ���strings are special, deal with it��� 3. Specialize it for strings to work with both numeric _and_ string steps (by explicit type-checking ���if the range is string & the step is integer���)���that���s what @knu asks about in #18368#note-33. Maybe even avoid documenting the special behavior, or document it is discouraged (so the old code���if it exists���wouldn���t break, but the new code wouldn���t rely on it) 4. Maybe make it always work with integer steps as a fallback (though there is no way to reliably check ���whether this particular class supports `obj+int` directly, or we need to switch back to the ���number of steps��� behavior���) 5. Rethink the decision in general (again, if there is a firm ground for concerns over the compatibility), and introduce _another_ method that will behave like the currently implemented `#step` relying on `#+`, reverting `#step` to the old behavior. Of those, I would honestly prefer to avoid (2) (very confusing for new users, teaching, and general language���s image) and (5); I don���t think (4) is generally possible; which leaves us with (1) (how is it now) and (3) (additional specialization for strings which _also_ supports numeric steps on the basis of range begin and step class, in addition to supporting string steps). TBH, I still believe there would be very low amount of incompatibility in existing code, but I might genuinely miss some common knowledge. ---------------------------------------- Feature #18368: Range#step semantics for non-Numeric ranges https://bugs.ruby-lang.org/issues/18368#change-109485 * Author: zverok (Victor Shepelev) * Status: Open ---------------------------------------- I am sorry if the question had already been discussed, can't find the relevant topic. "Intuitively", this looks (for me) like a meaningful statement: ```ruby (Time.parse('2021-12-01')..Time.parse('2021-12-24')).step(1.day).to_a # ^^^^^ or just 24*60*60 ``` Unfortunately, it doesn't work with "TypeError (can't iterate from Time)". Initially it looked like a bug for me, but after digging a bit into code/docs, I understood that `Range#step` has an odd semantics of "advance the begin N times with `#succ`, and yield the result", with N being always integer: ```ruby ('a'..'z').step(3).first(5) # => ["a", "d", "g", "j", "m"] ``` The fact that semantic is "odd" is confirmed by the fact that for Float it is redefined to do what I "intuitively" expected: ```ruby (1.0..7.0).step(0.3).first(5) # => [1.0, 1.3, 1.6, 1.9, 2.2] ``` (Like with [`Range#===` some time ago](https://bugs.ruby-lang.org/issues/14575), I believe that to be a strong proof of the wrong generic semantics, if for numbers the semantics needed to be redefined completely.) Another thing to note is that "skip N elements" seem to be rather "generically Enumerable-related" yet it isn't defined on `Enumerable` (because nobody needs this semantics, typically!) Hence, two questions: * Can we redefine generic `Range#step` to new semantics (of using `begin + step` iteratively)? It is hard to imagine the amount of actual usage of the old behavior (with String?.. to what end?) in the wild * If the answer is "no", can we define a new method with new semantics, like, IDK, `Range#over(span)`? **UPD:** More examples of useful behavior (it is NOT only about core `Time` class): ```ruby require 'active_support/all' (1.minute..20.minutes).step(2.minutes).to_a #=> [1 minute, 3 minutes, 5 minutes, 7 minutes, 9 minutes, 11 minutes, 13 minutes, 15 minutes, 17 minutes, 19 minutes] require 'tod' (Tod::TimeOfDay.parse("8am")..Tod::TimeOfDay.parse("10am")).step(30.minutes).to_a #=> [#, #, #, #, #] require 'matrix' (Vector[1, 2, 3]..).step(Vector[1, 1, 1]).take(3) #=> [Vector[1, 2, 3], Vector[2, 3, 4], Vector[3, 4, 5]] require 'unitwise' (Unitwise(0, 'km')..Unitwise(1, 'km')).step(Unitwise(100, 'm')).map(&:to_s) #=> ["0 km", "1/10 km", "1/5 km", "3/10 km", "2/5 km", "0.5 km", "3/5 km", "7/10 km", "4/5 km", "9/10 km", "1 km"] ``` **UPD:** Responding to discussion points: **Q:** Matz is concerned that the proposed simple definition will be confusing with the classes where `+` is redefined as concatenation. **A:** I believe that simplicity of semantics and ease of explaining ("it just uses `+` underneath, whatever `+` does, will be performed") will make the confusion minimal. **Q:** Why not introduce new API requirement (like "class of range's `begin` should implement `increment` method, and then it will be used in `step`) **A:** require *every* gem author to change *every* of their objects' behavior. For that, they should be aware of the change, consider it important enough to care, clearly understand the necessary semantics of implementation, have a resource to release a new version... Then all users of all such gems would be required to upgrade. The feature would be DOA (dead-on-arrival). The two alternative ways I am suggesting: change the behavior of `#step` or introduce a new method with desired behavior: 1. Easy to explain and announce 2. Require no other code changes to immediately become useful 3. With something like [backports](https://github.com/marcandre/backports) or [ruby-next](https://github.com/ruby-next/ruby-next) easy to start using even in older Ruby version, making the code more expressive even before it would be possible for some particular app/compny to upgrade to (say) 3.2 All examples of behavior from the code above are real `irb` output with monkey-patched `Range#step`, demonstrating how little change will be needed to code outside of the `Range`. -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/