From: Brian Candler Date: 2004-10-10T19:23:38+09:00 Subject: Re: Range behavior (Re: [RCR] New [] Semantics) On Sun, Oct 10, 2004 at 04:25:54AM +0900, trans. (T. Onoma) wrote: > 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.