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.