Project

General

Profile

Actions

Feature #18368

open

Range#step semantics for non-Numeric ranges

Added by zverok (Victor Shepelev) about 2 months ago. Updated 13 days ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:106314]

Description

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:

(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:

('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:

(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, 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)?

Updated by mame (Yusuke Endoh) 14 days ago

This topic was discussed at the dev-meeting yesterday.

A naive implementation (using begin + step iteratively) will allow the following behavior.

([]..).step([1]).take(3)        #=> [[], [1], [1, 1]]
(Set[1]..).step(Set[2]).take(3) #=> [Set[1], Set[1, 2], Set[1,2]]

matz (Yukihiro Matsumoto) was okay to allow (timestamp1...timestamp2).step(3.hours), but wanted to prohibit the above behavior. We need to find a reasonable semantics to allow timestamp ranges and to deny container ranges.

Updated by zverok (Victor Shepelev) 13 days ago

mame (Yusuke Endoh) matz (Yukihiro Matsumoto)
I believe that "step implemented with +" is clear and useful semantics which might help with much more than time calculations:

require 'numo/narray'

p (Numo::NArray[1, 2]..).step(Numo::NArray[0.1, 0.1]).take(5)
# [Numo::Int32#shape=[2] [1, 2], 
#  Numo::DFloat#shape=[2] [1.1, 2.1],
#  Numo::DFloat#shape=[2] [1.2, 2.2],
#  Numo::DFloat#shape=[2] [1.3, 2.3],
#  Numo::DFloat#shape=[2] [1.4, 2.4]]

What's unfortunate in mame (Yusuke Endoh)'s example is rather that we traditionally reuse + in collections for concatenation (it isn't even commutative!), but that's just how things are.

While stepping with array concatenation might be considered weird, I don't think it would lead to any real bugs/weird code; and it is easy to explain by "it is just what + does".
We actually have this in different places too, like, this work (with semantics not really clear):

([1]..[3]).cover?([1.5]) # => true

Updated by Eregon (Benoit Daloze) 13 days ago

One way to achieve the same result currently is Enumerator.produce:

require 'time'
Enumerator.produce(Time.parse('2021-12-01')) { _1 + 24*60*60 }.take_while { _1 <= Time.parse('2021-12-24') }

Somewhat related to https://bugs.ruby-lang.org/issues/18136#note-15 (where <= can't be used).

But I think step should just use + and < (for exclude_end?)/<=, I don't see any reason to prevent the above cases, ([]..).step([1]).take(3) can actually be useful.

Actions

Also available in: Atom PDF