I have given the usage question some thought. I think the most common uses are
1) for Interval, then 2) as Array index, then 3) simple iterator on Integers.
Please let me know if you can think of any other common use cases. For 1 and
2 all that really matters is the two bound points.
Those are the common cases I can think of too.
Note: Consider a special EvenInt class with a succ of: {|x| x+2}, make a Range
of those and use it as an Array index argument,
arr = [1,2,3,4,5,6,7,8]
arr[EvenInt.new(2)..EvenInt.new(4)]
would you expect [2,3,6,8]? I haven't tried it but my bet is you'll get
[2,3,4,5,6,7,8] instead.
An array is indexed by an Integer, not an EvenInt. I would expect it to do
the same as
arr[EvenInt.new(2).to_i .. EvenInt.new(4).to_i]
and thus be the same as
arr[2 .. 4]
returning [3,4,5].
Right, here's a quick test to see:
class Foo
def initialize(x)
@x = x
end
def to_i
@x / 10
end
end
arr = [1,2,3,4,5,6,7,8]
arr[Foo.new(20)..Foo.new(40)]
#>> ArgumentError: bad value for range
Nope, so I'm wrong. Perhaps it does iterate over the range after all. OK,
let's add:
class Foo
def succ
self.class.new(@x+1)
end
attr_reader :x
def <=>(y)
@x <=> y.x
end
end
arr[Foo.new(20)..Foo.new(40)]
#>> TypeError: cannot convert Foo into Integer
Hmm, closer, but no banana. Next:
class Foo
alias :to_int :to_i
end
arr[Foo.new(20)..Foo.new(40)]
#=> [3, 4, 5]
Bingo, but that's strange:
(Foo.new(20)..Foo.new(40)).each {|x| p x}
generates 21 elements, but we only have 3 elements to our array. Perhaps
it's not iterating over the range after all?
Let's try a fresh object without 'succ':
class Bar
def initialize(x)
@x = x
end
def to_int
@x / 10
end
end
arr[Bar.new(20)..Bar.new(40)]
#>> ArgumentError: bad value for range
Put the spaceship back in:
class Bar
attr_reader :x
def <=>(y)
puts "Spaceship! #{x.inspect}<=>#{y.x.inspect}"
@x <=> y.x
end
end
arr[Bar.new(20)..Bar.new(40)]
# Spaceship! 20<=>40
#=> [3, 4, 5]
Ok, so now we have the answer.
arr[range]
is essentially the same as
arr[range.first.to_int .. range.last.to_int]
However the lower and upper bounds are compared once with the spaceship
operator first (*before* they are converted to integers). Oh yes, that's
because of the overloaded use of ranges:
arr[5..-2]
means "from arr[5] to character arr[size-2] (inclusive)"
arr[Bar.new(20)..Bar.new(-20)]
# Spaceship! 20<=>-20
# => [3, 4, 5, 6, 7]
Anyway, you can see here that a Range is definitely being treated as just a
pair of values (lower and upper), not as anything iterative at all.
Aside: Perhaps the conversion to integers should take place before the
comparison?
Now, consider, how often have you use any of these on a Range:
collect, detect, each_with_index, entries,
find, find_all, grep, map, member?,
reject, select, sort
The pattern
(0..9).collect { |c| ... }
isn't uncommon. Things like find/find_all/member? seem pretty useless to me.
sort is clearly no use at all
So maybe Enumerable isn't really needed in Range, and would work just fine
with only a couple of useful methods toward that end, like a specially
defined #each --I'm not even sure #succ is really a good match for this
either.
to_a is very important. each and map/collect would be the only other two I'd
see as important. Perhaps 'min' and 'max' could alias to 'first' and 'last',
since they're so trivially implemented (although that would give three
aliases: begin/first/min, end/last/max. I think Range#begin and Range#end
are dangerous method names, given that they are also Ruby keywords)
For all those much less common Enumerable uses of a Range, one would be better
off using a dedicated class anyway --a real Iterator. So why not just provide
one of those in the standard libs, instead of trying to get Range to serve
triple duty.
Or drop Enumerable from Range, and give it a handful of duck-type methods:
each
to_a
map / collect
(min)
(max)
plus its own non-enumerable 'include?' of course.
Mixing in the real Enumerable saves a little bit of coding, but like you
say, it adds a lot of methods which have little or no value and perhaps
that's what's confusing.
However I don't really object to a Range being Enumerable in the full sense,
even if some of the methods don't make sense; I think it's important to
distinguish Range#include? / member? from Enumberable#include? / member?
Regards,
Brian.