On Thu, Oct 07, 2004 at 11:11:11PM +0900, trans. (T. Onoma) wrote: > Off the cuff I see two options: 1) An internal flag: > discrete vs. continuous 2) Or two separate classes, as you suggest. But there > is more to it... I can understand why there is not an explicit flag at the moment, because generally it's implicit: - you can't use the continuous methods if the lower and upper bounds don't respond to spaceship* - you can't use the discrete methods if the lower bound doesn't respond to 'succ', or the upper bound doesn't respond to spaceship* [*] Actually, I might not have those the right way round. I don't know, without looking at the source, whether it is bounds <=> member or member <=> bounds which is tested. irb(main):001:0> (3.3..5.3).each {|x| puts x} TypeError: cannot iterate from Float Since the discrete methods rely on succ, == and spaceship all being available, then I can't think of any case where a range would work usefully for the discrete case but not the continuous case. The flag you propose might be useful if you want to make the semantics of === different for discrete/continuous ranges. However it seems reasonable for === to mean 'continuous range', because that's the case which almost always is available. Actually, although Range#each is useful, I can see little use for Range#member?. Why iterate through a range stepwise when there is almost always a better way to test membership? (It may be domain-specific though, e.g. if bounds are Integer then test for elem.to_i == elem) If Range#member? is not particularly useful, that's another reason why it should not be the default for ===. And it could be inherited from Enumerable, rather than having its own implementation. > This becomes increasing interesting (or frustrating depending on your slant) > when we consider the further advantages of having a NumericRange --notice > that its advantage is specific to discreteness (member modulo). But all > Ranges are based on succ and are therefore by definition discrete --only > Numeric ranges can be Continuous. I don't see in principle why you can't have a continuous range foo..bar where foo and bar are not Numeric, but do respond to <=>. Can't think of a useful example though :-) > Consider further how succ determines successive members --a Range is an > indeterminate ordered set built by iteration. Oddly one defines a Range with > a first and last argument, but iterations are supposed to be defined by a > seed (first) and the number of successive iterations. And there is good > reason for this: there is no way to be sure that any given _last_ is a member > of the set! Look at this: Indeed - which is why a Ruby discrete range relies on spaceship to determine whether the end of iteration has been reached. > > class ShrinkyDink > def initialize(x); @x = x.to_i; end > def succ; @x - 1; end > def <=>(b); @x <=> b; end > end > > rng = ShrinkyDink.new(0)...ShrinkyDink.new(100) This is a bit odd though. Your 'succ' method returns an Integer, not another instance of ShrinkyDink. Do you mean this? def succ; self.class.new(@x-1); end > Not only is ShrinkyDink.new(100) not a member of the range, but nothing > "inbetween" the two is either! It's not a 'member', although ShrinkyDink(-50) is. And ShrinkyDink(50) is included within the range, but it's not a member. Argh! class ShrinkyDink attr_reader :x def initialize(x); @x = x.to_i; end def succ; ShrinkyDink.new(@x - 1); end def <=>(b); @x <=> b.x; end end rng = ShrinkyDink.new(0)...ShrinkyDink.new(100) p rng.include?(ShrinkyDink.new(50)) # true p rng.include?(ShrinkyDink.new(150)) # false # p rng.member?(ShrinkyDink.new(-50)) # would be true, except for Ruby bug # rng.each { |a| p a } # infinite loop! There is an implied contract that #succ takes you 'closer' to the upper bound. Unfortunately the direction of the spaceship test is fixed. In your case rng = ShrinkyDink.new(100)...ShrinkyDink.new(0) might have been useful in the discrete case, except that it never iterates because ShrinkyDink.new(100) > ShrinkyDink.new(0) by your spaceship definition. It's not useful in the continuous case either, because all values are out of range. So, how else could discrete ranges be handled? Maybe if they provided either (a) an iteration count [as you suggested]; or (b) a final element, which can be tested using == alone; or (c) a proc which determines when the iteration is complete rather than forcing the test { |e| e < upperbound } or { |e| e <= upperbound } which happens now. ------------------------------------------------------------------------- class DiscreteRange1 include Enumerable def initialize(lower, count) @lower = lower @count = count end def each val = @lower @count.times do yield val val = val.succ end end end class DiscreteRange2 include Enumerable def initialize(lower, final, exclude_end=false) @lower = lower @final = final @exclude_end = exclude_end end def each val = @lower while val != @final yield val val = val.succ end yield val unless @exclude_end end end class DiscreteRange3 include Enumerable def initialize(lower, &cond) @lower = lower @cond = cond end def each val = @lower while @cond.call(val) yield val val = val.succ end end end DiscreteRange1.new(10,5).each { |x| puts x } DiscreteRange2.new(10,14).each { |x| puts x } DiscreteRange3.new(10) { |x| x < 15 }.each { |x| puts x } ------------------------------------------------------------------------- The third is the most general pattern. Current Ruby ranges could be simulated by DiscreteRange3.new(lower) { |x| x < upper } # exclude_end DiscreteRange3.new(lower) { |x| x <= upper } # not exclude_end So, all very interesting, but where does this leave (3..5) ? - we want interval semantics: case n when (0...3) # xxx when (3..7.5) # xxx end - we want discrete iterator semantics b = (0..3).to_a - there's no reason why the upper bound couldn't be a proc: (0..proc{|x| x<3}).to_a - if the upper bound is not a proc, then ISTM that DiscreteRange2 would be a reasonable pattern, allowing your ShrinkyDinks to iterate backwards. However it would break (0..3.5).to_a # becomes infinite - ranges with a length, rather than an upper bound, are definitely of interest because of the parallel with substrings and array slices: a[3,2] # from 3 for 2 elements and so I wonder if having an explicit object for them could tidy this area up. That's more than enough thinking aloud though! Regards, Brian.