Minimal interface for Range#include? support

Issue

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!

Solution

The documentation is incorrect or (rather, I suspect) outdated. Range#cover? works the way you expect [bold emphasis mine]:

cover?(object)true or false

Returns true if the given argument is within self, 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 like cover?

[…]

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 Integers, 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 Ranges and it is not the first time this has led to bugs and/or non-intuitive behavior:

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.

Answered By – Jörg W Mittag

Answer Checked By – Willingham (AngularFixing Volunteer)

Leave a Reply

Your email address will not be published.