I'd like to make my yearless dates class play nice with Range#include?
, according to the docs, all it has to implement is <=>
:
class Count
include Comparable
attr_reader :value
def initialize(value)
@value = value
end
def <=>(other)
value <=> other.value
end
end
Let's try on Ruby 3.1.1:
(Count.new(1)..Count.new(5)).include? Count.new(3)
# => in `each': can't iterate from Count (TypeError)
I don't get why it's trying to iterate here, each
should not be necessary to figure out inclusion.
Any idea what am I doing wrong here? Thanks for your hints!
CodePudding user response:
The documentation is incorrect or (rather, I suspect) outdated. Range#cover?
works the way you expect [bold emphasis mine]:
cover?(object)
→true
orfalse
Returns
true
if the given argument is withinself
,false
otherwise.With non-range argument object, evaluates with
<=
and<
.
The documentation for Range#include?
contains a somewhat ominous statement [bold emphasis mine]:
If begin and end are numeric,
include?
behaves likecover?
[…]
But when not numeric, the two methods may differ:
('a'..'d').include?('cc') # => false ('a'..'d').cover?('cc') # => true
Here you can see the difference: Range#cover?
evaluates to true
because 'a' <= 'cc' && 'cc' <= 'd'
, whereas Range#include?
evaluates to false
because ('a'..'d').to_a == ['a', 'b', 'c', 'd']
and thus ('a'..'d').each.include?('cc')
is falsey.
Note that the introductory example using Time
still works because Time
is explicitly special-cased in the spec.
There is a spec which says both Range#include?
and Range#cover?
use <=>
, but it is only tested with Integer
s, for which we know from the ominous documentation above that Range#include?
and Range#cover?
behave the same.
There is quite a lot of special-casing going on for Range
s and it is not the first time this has led to bugs and/or non-intuitive behavior:
- Ruby: Can't Iterate From Time Despite Responding to Succ / Bug #18237 Remove unnecessary checks for
Time
inRange#each
as per the comment / https://github.com/ruby/spec/pull/852 / https://github.com/ruby/ruby/pull/4928 - https://bugs.ruby-lang.org/issues/18155
- https://bugs.ruby-lang.org/issues/18577
- https://bugs.ruby-lang.org/issues/18580
- https://github.com/ruby/dev-meeting-log/blob/master/DevMeeting-2022-02-17.md#bug-18580-rangeinclude-inconsistency-for-beginless-string-ranges-zverok
Personally, I am not a big fan of all this special-casing. I assume it is done for performance reasons, but the way to get better performance is not to add weird special cases to the language specification, it is to remove them which makes the language simpler and thus easier to optimize. Or, put another way: at any given point in time, a compiler writer can either spend the time implementing weird special cases or awesome optimizations, but not both. XRuby, Ruby.NET, MacRuby, MagLev, JRuby, IronRuby, TruffleRuby, Rubinius, Topaz, and friends have shown that the way to get high-performance Ruby is a powerful compiler, not weird hand-rolled special-cased C code.
I would file a bug, if only to get some clarification into the docs and specs.