Project

General

Profile

Actions

Feature #13933

closed

Add Range#empty?

Added by ted (Ted Johansson) over 6 years ago. Updated 5 months ago.

Status:
Rejected
Assignee:
-
Target version:
-
[ruby-core:82948]

Description

Range already responds to #size. It would be nice if it also responded to predicate #empty? :-)


Related issues 1 (0 open1 closed)

Related to Ruby master - Feature #19839: Need a method to check if two ranges overlapClosedActions

Updated by marcandre (Marc-Andre Lafortune) over 6 years ago

Note that Range#size currently returns nil for ranges of string, so it's not clear what empty? would return for ranges of strings.

Updated by shan (Shannon Skipper) over 6 years ago

Range mixes in Enumerable so you can use #none?.

('a'..'b').none? #=> false
('b'..'a').none? #=> true

Updated by shevegen (Robert A. Heiler) over 6 years ago

I think the semantics between ".none?" is not the same as ".empty?". At
the least the latter appears to be more explicit to me.

But it's also not that important to me personally, I am fine either way. :-)

Updated by Dan0042 (Daniel DeLorme) 6 months ago

Since 6 years ago, beginless and endless ranges have been added, so #none? is no longer sufficient.

(nil..0).none?  #=> TypeError (can't iterate from NilClass)

And as mentioned above Range#size doesn't work for String ranges.

So I think it would be good to have Range#empty?, with cover-based semantics rather than succ-based:

(1..0).empty?                #=> true
(1..1).empty?                #=> false
(1...1).empty?               #=> true
("a".."b").empty?            #=> false
("b".."a").empty?            #=> true
(nil..0).empty?              #=> false  
(..-Float::INFINITY).empty?  #=> false  
(...-Float::INFINITY).empty? #=> true

Updated by nobu (Nobuyoshi Nakada) 6 months ago

Dan0042 (Daniel DeLorme) wrote in #note-5:

(...-Float::INFINITY).empty? #=> true

How do you define this?
Range#empty? must know if the element is a Float, and negative infinity?

Updated by Dan0042 (Daniel DeLorme) 6 months ago

It was an ad-hoc example that just seemed to me like it should return true.

As to how to define it... maybe we can say that numeric ranges are equivalent to begin == -Float::INFINITY if beginless, and end == +Float::INFINITY if endless. Then it would be like saying (k...k).empty? with k == -Float::INFINITY

I'm not sure. Currently,

i = Float::INFINITY
(i..i).cover?(i)     #=> true
(i...i).cover?(i)    #=> false
(i..).cover?(i)      #=> true
(i...).cover?(i)     #=> true
(-i..-i).cover?(-i)  #=> true
(-i...-i).cover?(-i) #=> false
(..-i).cover?(-i)    #=> true
(...-i).cover?(-i)   #=> false

but I'm not sure all those really make sense either.

Updated by akr (Akira Tanaka) 6 months ago

I described that this is difficult to implement in https://bugs.ruby-lang.org/issues/19839#note-18.

The problem is not only with -Float::INFINITY.
There are many minimum values other than -Float::INFINITY : [], "", true, false, Object.new, etc.
Of course, a user may define a class with a minimum value.

Range#empty? should work with them.

Updated by Dan0042 (Daniel DeLorme) 6 months ago

akr (Akira Tanaka) wrote in #note-8:

Range#empty? should work with them.

Ok for Float::INFINITY, [], "", but I don't think true, false, Object.new are relevant. A Range object normally has a begin and end, and if the values are not comparable (like "a"..1) we get "ArgumentError (bad value for range)". With true..true we happen to coincidentally pass that validation because true<=>true is 0 (and arguably it should be nil but that's another topic). But semantically, as a range, true..true doesn't mean anything. The introduction of beginless/endless ranges also mean that we can now create all kinds of ranges that skip the normal validation.

(true..true) and (true..) and (..true) are not semantically meaningful ranges, there is no use case for them, and therefore it doesn't matter that #include? raises an error, and it shouldn't matter what is returned by #empty?

But unlike true, arrays truly are comparable. The Comparable module is not mixed in, but <=> can return -1 and 1. So [] is truly a minimum value for arrays. It could be special-cased. Although I'm not sure it's worth the trouble. Or maybe we can define a range "r" as empty if r.begin.nil? and r.exclude_end? and r.end.respond_to?(:empty?) and r.end.empty?

Random idea: beginless/endless ranges may only be created for objects that mix in Comparable. So (Time.now..) is ok but (Object.new..) raises "ArgumentError (bad value for range)"

Updated by duerst (Martin Dürst) 6 months ago

Dan0042 (Daniel DeLorme) wrote in #note-9:

because true<=>true is 0 (and arguably it should be nil but that's another topic).

Are you saying that true shouldn't be equal to true? That would be strange, at least to me.

Updated by Dan0042 (Daniel DeLorme) 6 months ago

duerst (Martin Dürst) wrote in #note-10:

Are you saying that true shouldn't be equal to true? That would be strange, at least to me.

I understand that it appears logical, but <=> is a comparison operator, used to compare with other values during sort/min/max. It's pretty much useless if the only valid comparison is with itself. What's the use case for [true,true].min == true but [true,anything_else].min #=> ArgumentError ? I can't think of anything reasonable. At least if true<=>true was nil, that would tell us this value is not comparable with any other value. But I'm not suggesting we change this now, it seems too backward incompatible.

Updated by akr (Akira Tanaka) 6 months ago

Dan0042 (Daniel DeLorme) wrote in #note-9:

akr (Akira Tanaka) wrote in #note-8:

Range#empty? should work with them.

Ok for Float::INFINITY, [], "", but I don't think true, false, Object.new are relevant. A Range object normally has a begin and end, and if the values are not comparable (like "a"..1) we get "ArgumentError (bad value for range)". With true..true we happen to coincidentally pass that validation because true<=>true is 0 (and arguably it should be nil but that's another topic). But semantically, as a range, true..true doesn't mean anything. The introduction of beginless/endless ranges also mean that we can now create all kinds of ranges that skip the normal validation.

(true..true) and (true..) and (..true) are not semantically meaningful ranges, there is no use case for them, and therefore it doesn't matter that #include? raises an error, and it shouldn't matter what is returned by #empty?

There is an actual example of (x <=> x) = nil, Float::NAN.

% ruby -e 'x = Float::NAN; p x <=> x'    
nil

From experience with Float::NAN, I think it is not a good idea to expand such objects.
It makes the behavior of Array#<=> inconsistent.

% ruby -e 'a = [Float::NAN]; p a <=> a'                  
0

Since the (only) element of the array, Float::NAN, is not comparable,
the array should not be comparable.
But a <=> a returns 0 which means it is comparable and equal.
It is because Array#<=> returns 0 if the given argument is the receiver itself, regardless of its elements.
We need to choose efficiency or consistency.
Ruby chooses efficiency here: Array#<=> returns 0 without examining its elements.
Expanding Floag::NAN'like behavior makes this inconsistency more visible.

I agree that objects comparable only with itself have not much usage for range.
But their order is well-defined as a partial order.
There is no reason to break it.

But unlike true, arrays truly are comparable. The Comparable module is not mixed in, but <=> can return -1 and 1. So [] is truly a minimum value for arrays. It could be special-cased. Although I'm not sure it's worth the trouble. Or maybe we can define a range "r" as empty if r.begin.nil? and r.exclude_end? and r.end.respond_to?(:empty?) and r.end.empty?

You ignored the user-defined classes I mentioned.
A user may define a class with a minimum value.
The minimum value may not have empty? method.

Updated by Dan0042 (Daniel DeLorme) 6 months ago

akr (Akira Tanaka) wrote in #note-12:

From experience with Float::NAN, I think it is not a good idea to expand such objects.
It makes the behavior of Array#<=> inconsistent.

I agree but I think this is a bit different from what I was talking about. Float::NAN is a float that is not comparable with other floats. This is not the same case as a value that is not comparable with anything else.

There is no reason to break it.

I agree; as I said, I'm not suggesting we change this now.

Or maybe we can define a range "r" as empty if r.begin.nil? and r.exclude_end? and r.end.respond_to?(:empty?) and r.end.empty?

You ignored the user-defined classes I mentioned.
A user may define a class with a minimum value.
The minimum value may not have empty? method.

How do you define which is the minimum value? I suggested the above as a possible way to define this minimum value. So a custom class would need to have #empty? that returns true in order to define "this is the minimum". But it was just a suggestion. Or maybe each class could have a MINIMUM constant like String::MINIMUM = "", etc.

More importantly, I think that (...minimum).empty? is such an edge case of an edge case that it's not worth worrying too much. Are you suggesting that Range#empty? should not exist just because it's not possible to perfectly handle this extreme edge case for every possible class? Would it really be so bad if it returned true false for user-defined classes?

Updated by akr (Akira Tanaka) 6 months ago

Dan0042 (Daniel DeLorme) wrote in #note-13:

akr (Akira Tanaka) wrote in #note-12:

From experience with Float::NAN, I think it is not a good idea to expand such objects.
It makes the behavior of Array#<=> inconsistent.

I agree but I think this is a bit different from what I was talking about. Float::NAN is a float that is not comparable with other floats. This is not the same case as a value that is not comparable with anything else.

Float::NAN is not comparable with anything else, not only floats.

% ruby -e 'p Float::NAN <=> "foo"'               
nil
% ruby -e 'p Float::NAN <=> []'     
nil

Or maybe we can define a range "r" as empty if r.begin.nil? and r.exclude_end? and r.end.respond_to?(:empty?) and r.end.empty?

You ignored the user-defined classes I mentioned.
A user may define a class with a minimum value.
The minimum value may not have empty? method.

How do you define which is the minimum value? I suggested the above as a possible way to define this minimum value. So a custom class would need to have #empty? that returns true in order to define "this is the minimum". But it was just a suggestion. Or maybe each class could have a MINIMUM constant like String::MINIMUM = "", etc.

I described an idea in https://bugs.ruby-lang.org/issues/19839#note-18 :
minimum? method.

I feel empty? can sometimes be a wrong name.
minimum? represent the property directly.

More importantly, I think that (...minimum).empty? is such an edge case of an edge case that it's not worth worrying too much. Are you suggesting that Range#empty? should not exist just because it's not possible to perfectly handle this extreme edge case for every possible class? Would it really be so bad if it returned true for user-defined classes?

I think a bug report like my comment will coming someday.
Ruby developers (especially matz) need to respond to it.

So, we compare the usefulness of the method and the difficulties of responding to the bug report.

Also, this problem can be expanded if we introduce Range#exclude_begin? (with neko operator).
https://bugs.ruby-lang.org/issues/12133
In that case, we need maximum? as well.
But we also need a method to check the existence between two objects because [1] ^..^ [1, ""] is empty (no objects between [1] and [1, ""]).

Updated by Dan0042 (Daniel DeLorme) 6 months ago

akr (Akira Tanaka) wrote in #note-14:

Float::NAN is not comparable with anything else, not only floats.

Sorry, maybe I didn't express myself well, I meant that floats are usually comparable with other floats, with the exception of NaN. Whereas one Object.new is never comparable with another Object.new

I described an idea in https://bugs.ruby-lang.org/issues/19839#note-18 :
minimum? method.

I feel empty? can sometimes be a wrong name.
minimum? represent the property directly.

Yes, minimum? is cleaner and "the right way"; empty? is more like a hack that works with existing builtin classes with no changes needed. All 3 options you described are perfectly fine.

I think a bug report like my comment will coming someday.
Ruby developers (especially matz) need to respond to it.

By "not worth worrying too much" I did not mean we should ignore or postpone it. I just meant we can pick any of the options without worrying too much about getting the perfect one.

  • empty? method
  • minimum? method
  • MINIMUM constant
  • hard code minimum values
  • document the limitation (even this, while not ideal, is "good enough" imho)
  • a mix of the above (ex: hardcode for builtins, minimum? method for user-defined classes)

Also, this problem can be expanded if we introduce Range#exclude_begin? (with neko operator).
https://bugs.ruby-lang.org/issues/12133
In that case, we need maximum? as well.
But we also need a method to check the existence between two objects because [1] ^..^ [1, ""] is empty (no objects between [1] and [1, ""]).

Huh, "neko" operator... that's cute 🐱

Updated by kyanagi (Kouhei Yanagita) 5 months ago

While this is slightly off-topic, it's relevant to MINIMUM, so I'm noting it here.

0.0..Float::INFINITY represents the entire range of floating-point numbers that is greater or equal to 0.0.
Therefore, semantically, the range represented by 0.0..Float::INFINITY should be the same as 0.0..,
but the following results contradict this:

(0.0..Float::INFINITY).cover?(0.0..) # => false
(0.0..).cover?(0.0..Float::INFINITY) # => true

To make this work "correctly", we need the information that Float::INFINITY is the maximum value.

If there is any handling regarding the minimum value, it might be necessary to have a similar approach for
the maximum value as well.

Updated by matz (Yukihiro Matsumoto) 5 months ago

  • Status changed from Open to Rejected

One drawback of Range#empty? is that we have several corner cases that cannot determine emptiness correctly (e.g., regarding exclude_end ranges or infinity).
Upon the fact we don't see the real-world use-case yet, we reject the proposal for the moment.

FYI, Range#overlap? has similar corner cases, it's better to document clearly somewhere.

Matz.

Actions #18

Updated by mame (Yusuke Endoh) 5 months ago

  • Related to Feature #19839: Need a method to check if two ranges overlap added
Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0