Feature #14799
closedStartless range
Description
On introduction of endless range at #12912, "startless range" was discussed this way:
@sowieso (So Wieso): Not having the opposite (
..5
and..-2
) feels like this is rather a hack than a thoroughly planned feature.
@duerst (Martin Dürst): I don't understand the need for a
..5
Range. The feature is called "endless range". Although mathematically, it's possible to think about startless ranges, they don't work in a program. Maybe some programming languages have..5
as a shortcut for0..5
, but that's in any way a usual, bounded, range with a start and an end. It's conceptually totally different from5..
, which is a range with a start but no end, an unbound range.
In the context of that ticket (ranges used mostly for slicing arrays) having ..5
was indeed hard to justify, but there are other cases when ..5
being -Infinity..5
is absolutely reasonable:
case release_date
when ..1.year.ago
puts "ancient"
when 1.year.ago..3.months.ago
puts "old"
when 3.months.ago..Date.today
puts "recent"
when Date.today..
puts "upcoming"
end
log.map(&:logged_at).grep(..Date.new(1980)) # => outliers due to bad log parsing...
E.g., whenever case equality operator is acting, having startless range to express "below this value" is the most concise and readable way. Also, for expressing constants (mostly decorative, but very readable):
# Celsius degrees
WORK_RANGES = {
..-10 => :turn_off,
-10..0 => :energy_saving,
0..20 => :main,
20..35 => :cooling,
35.. => :turn_off
}
In addition, my related proposal #14784 suggests that this kind of ranges could be utilized by more powerful clamp too:
updated_at.clamp(..Date.today)
Uncertainty points:
- Would it be hard to add to parser? I am not sure, I am not very good at it :(
- Should
..
be a thing? I guess not, unless there would be convincing real-life examples, which for me it is hard to think of.
Files
Updated by shyouhei (Shyouhei Urabe) over 6 years ago
No strong opinion on this. However let me leave one question: how should Range#each
work for this kind of ranges?
Updated by zverok (Victor Shepelev) over 6 years ago
how should Range#each work for this kind of ranges?
Most probably it should not (the same as Enumerable#reverse_each
or last(5)
doesn't have a sense for already implemented endless ranges), just raise.
Updated by shyouhei (Shyouhei Urabe) over 6 years ago
zverok (Victor Shepelev) wrote:
how should
Range#each
work for this kind of ranges?Most probably it should not (the same as
Enumerable#reverse_each
orlast(5)
doesn't have a sense for already implemented endless ranges), just raise.
Sounds reasonable, except it seems endless range does not raise for #reverse_each :)
zsh % ruby -ve '(1..).reverse_each {|i| p i }'
ruby 2.6.0dev (2018-05-29 trunk 63514) [x86_64-darwin15]
^CTraceback (most recent call last):
2: from -e:1:in `<main>'
1: from -e:1:in `reverse_each'
-e:1:in `each': Interrupt
Updated by zverok (Victor Shepelev) over 6 years ago
except it seems endless range does not raise for #reverse_each :)
Funny!
Though, in fact, raises some big questions about Enumerable
implementations. As far as I can understand, most of Enumerable
's methods aren't redefined in Range
, which leaves a room for optimization (and sometimes a huge room): last(N)
and reverse_each
are just work by converting enumerable to an Array, and then taking the last elements. Probably, providing custom #reverse_each
for where it is acceptable, and making #last
rely on this could be both more effective, and produce more reasonable result in edge cases (like open -- semi-infinite -- sequences).
Updated by mame (Yusuke Endoh) over 6 years ago
- File beginless-range.patch beginless-range.patch added
- Status changed from Open to Assigned
- Assignee set to matz (Yukihiro Matsumoto)
I tried begin-less range once, and it caused many parser conflicts, so I gave up.
However, I've tried it again and created a patch with no conflicts.
A proof-of-concept patch is attached. It uses the damn lexer state, so I'd like nobu and yui-knk to review it.
Note that the main focus of this patch is parse.y, so we need more work to let many builtin methods to support begin-less range.
Matz, what do you think?
Updated by janosch-x (Janosch Müller) over 6 years ago
Rails devs could also make use of this, e.g. in queries.
Updated by DarkWiiPlayer (Dennis Fischer) over 6 years ago
This does make sense when considering the reverse of the range, in other words, counting down from N to -infinity. This reasoning has two problems though:
- Ranges have no method that reverse them
- Ruby doesn't seem to like N..M ranges where M < N (
(5..1).each {|x| puts x}
simply prints5..1
in ruby 2.5
Intuitively, counting down like that seems like a pretty common use case, but I cannot actually recall ever needing it, and it could also be simulated with a forward-counting range and just multiplying the values with -1 before using them, which shouldn't really have any considerable impact on speed.
EDIT: Overriding #each
to raise an exception and only allowing #reverse_each with startless ranges might make them somewhat useful.
Updated by znz (Kazuhiro NISHIYAMA) over 6 years ago
I think version guard of ruby/spec is one of usages.
For example:
ruby_version_is ""..."2.6" do
# ...
end
Updated by Eregon (Benoit Daloze) over 6 years ago
znz (Kazuhiro NISHIYAMA) wrote:
I think version guard of ruby/spec is one of usages.
For example:
ruby_version_is ""..."2.6" do # ... end
Right, this would be a nice way to express version ranges like
ruby_version_is ..."2.6" do
# ...
end
ruby_version_is "2.4"... do
# ...
end
However for the case of ruby/spec, we'll have to wait until 2.5 is out of life to actually use those, as the syntax does not work on 2.5 and earlier.
Updated by mame (Yusuke Endoh) over 6 years ago
I agree that it is a good use case.
But notice that we will not be able to write ruby_version_is ..."2.6" do
because it parses as (ruby_version_is..."2.6") do
.
We need to write parentheses: ruby_version_is(..."2.6") do
.
Updated by Eregon (Benoit Daloze) over 6 years ago
@mame (Yusuke Endoh) Ah, that is quite unfortunate (it does not look so nice with the parens), but thank you for mentioning the caveat.
I guess much like Range literals in general, it's often needed to have parens around them.
Updated by knu (Akinori MUSHA) over 6 years ago
Let me point out that with this patch applied, (nil..nil).cover?(1)
would become returning true rather than raising an error. Note that nil..nil
has been valid for all past versions of ruby.
Updated by matz (Yukihiro Matsumoto) almost 6 years ago
I have some concerns, especially ones with syntax rules. Let us try it in the trunk, and see if it works well or not.
Matz.
Updated by nobu (Nobuyoshi Nakada) almost 6 years ago
BDOT2
and BDOT3
don't seem necessary.
Updated by mame (Yusuke Endoh) over 5 years ago
- Status changed from Assigned to Closed
Committed at r67422. Please give it a try and let me know if you find any issue.
Updated by naruse (Yui NARUSE) over 5 years ago
- Related to Bug #15745: There is no symmetry in the beginless range and the endless range using `Range#inspect` added