Project

General

Profile

Feature #18368

Updated by zverok (Victor Shepelev) over 1 year ago

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  
 #=> [#<Tod::TimeOfDay 08:00:00>, #<Tod::TimeOfDay 08:30:00>, #<Tod::TimeOfDay 09:00:00>, #<Tod::TimeOfDay 09:30:00>, #<Tod::TimeOfDay 10:00:00>] 


 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`.

Back