Blogged by Ujihisa. Standard methods of programming and thoughts including Clojure, Vim, LLVM, Haskell, Ruby and Mathematics written by a Japanese programmer. github/ujihisa

Thursday, May 7, 2009

About Array#<=> (a.k.a. Spacecraft Operator)

The documentations about Array#<=> are:

For example:

[1, 2, 3, 4] <=> [1, 2, 3, 3] #=> 1
[1, 2, 3, 4] <=> [1, 2, 3, 4] #=> 0
[1, 2, 3, 4] <=> [1, 2, 3, 5] #=> -1
[1, 2, 3, 4] <=> [1, 2, 3] #=> 1

The implementation in MRI 1.9 is:

VALUE
rb_ary_cmp(VALUE ary1, VALUE ary2)
{
    long len;
    VALUE v;

    ary2 = to_ary(ary2);
    if (ary1 == ary2) return INT2FIX(0);
    v = rb_exec_recursive(recursive_cmp, ary1, ary2);
    if (v != Qundef) return v;
    len = RARRAY_LEN(ary1) - RARRAY_LEN(ary2);
    if (len == 0) return INT2FIX(0);
    if (len > 0) return INT2FIX(1);
    return INT2FIX(-1);
}

static VALUE
recursive_cmp(VALUE ary1, VALUE ary2, int recur)
{
    long i, len;

    if (recur) return Qnil;
    len = RARRAY_LEN(ary1);
    if (len > RARRAY_LEN(ary2)) {
        len = RARRAY_LEN(ary2);
    }
    for (i=0; i<len; i++) {
        VALUE v = rb_funcall(rb_ary_elt(ary1, i), id_cmp, 1, rb_ary_elt(ary2, i));
        if (v != INT2FIX(0)) {
            return v;
        }
    }
    return Qundef;
}

I translated it from C to Ruby literally:

class Array
  def yet_another_cmp(you)
    you = you.to_ary
    return 0 if self == you
    v = nil
    (0...[self.length, you.length].min).each do |i|
      v = self[i] <=> you[i]
      return v if v != 0
    end
    len = self.length - you.length
    return 0 if len == 0
    return 1 if len > 0
    -1
  end
end

It works exactly the same.

You may think it is easy to write a simpler equivalent implement with Array#zip, but unfortunately I found that it was not so simple. The following is the simplest code I can write. Of course it works exactly the same as original <=>.

class Array
  def simple_cmp(you)
    self.zip(you) {|x, y|
      break if (x && y).nil?
      (v = x <=> y) == 0 or return v
    }
    self.length <=> you.length
  end
end

In conclusion, Array#<=> itself is complicated one. Enjoy your space travel!

4 comments:

  1. Indeed, it's not trivial to compare arrays.

    Since you have such a good eye at finding my errors in rubyspecs, I hope you won't mind if note a couple of differences in your "translation".

    First, the test for equality should be using equal?, not == (otherwise you'd loop forever!). More importantly, the Ruby code handles recursive arrays, so the ruby translation is actually much more complicated!

    Finally, your "zip" version won't work for arrays containing nil values, like [nil, :foo].yet_another_cmp [nil, :bar] # ==> 0, should be 1

    Checking the ruby code led me to find a cute quirk of Ruby 1.8.7/1.9. Here's the quiz for you: without writing any method/block/lambda, can you find ways to have:
    x == y # ==> true
    y == x # ==> false

    There are many ways!
    I'll post a bug report tomorrow (although I'm not sure they will consider it a bug!)

    ReplyDelete
  2. I saw merely the tip of the iceberg! I never imagine about [nil, ...].

    Although I had been thinking about the asymmetricity quiz until now, I couldn't find it. At first I suspected the following is the answer, but not.

    a, b = [], []
    a << a
    b << a

    I will keep thinking until I'll read your post at ruby-core.

    ReplyDelete
  3. Posted an update on my blog, check it out! http://blog.marc-andre.ca

    ReplyDelete
  4. Cooool! I read ruby-core. And then I tried the quiz on your blog, and I got a lot of mistakes. Ruby is a maximum-surprising language!

    ReplyDelete

Followers