Using the spaceship operator

Discussion in 'Ruby' started by RichardOnRails, Apr 21, 2011.

  1. Hi,

    Here an abstract version of my app, in which I have an array of two-
    element arrays I'd like to sort on the second element of each pair
    using the spaceship operator:

    $data = [ [:A, 12], [:B, 30], [:C, 4] ]

    Class MyTest < Array
    def <=>(other)
    self[1] <=> other[1]
    end

    @sorted_array = $date
    end

    How can I make this work?

    Thanks in advance,
    Richard
     
    RichardOnRails, Apr 21, 2011
    #1
    1. Advertisements

  2. Instead of doing it that way, consider using sort_by instead to sort
    by the second element:

    ruby-1.9.2-p180 :001 > fruits =3D [[:apple, 100], [:banana, 150],
    [:tomato, 80], [:kumquat, 180]]
    =3D> [[:apple, 100], [:banana, 150], [:tomato, 80], [:kumquat, 180]]

    ruby-1.9.2-p180 :002 > fruits.sort_by { |f| f[1] }
    =3D> [[:tomato, 80], [:apple, 100], [:banana, 150], [:kumquat, 180]]

    ~ jf
     
    John Feminella, Apr 21, 2011
    #2
    1. Advertisements

  3. RichardOnRails wrote in post #994206:
    Your way *can* be made to work, but it's ugly.

    class MyElement < Array
    def <=>(other)
    self[1] <=> other[1]
    end
    end

    a = [ MyElement.new([:A,12]),
    MyElement.new([:B,30]),
    MyElement.new([:C,4]),
    ]
    a.sort!
    p a

    The point is that the <=> operator is applied to pairs of elements in
    the array, not to the container object itself.

    As a general rule I'd say: don't subclass standard Ruby classes. Use
    delegation instead. So if you want a container object with Array-like
    properties but special behaviour, then make it have an Array, rather
    than be an Array.

    class MyContainer
    def initialize(a = [])
    @a = a
    end
    def sort!
    @a = @a.sort_by { |elem| elem[1] }
    end
    ... delegate other methods as required,
    ... or use SimpleDelegator
    end

    In the longer term you'll find this a much more flexible way of
    composing objects and behaviour. The only reason programming classes
    tell you to subclass is because of the inflexible typing systems in
    certain other languages.

    Ruby has "duck typing": there is no need for your class to inherit from
    Array, it just has to implement the behaviour of Array that you're
    interested in.
     
    Brian Candler, Apr 21, 2011
    #3
  4. Hi John and Brian,

    Thank you very much for your extremely insightful answers.

    I've been oscillating between:
    Metaprogramming Ruby
    The Well-Oriented Rubyist
    Design Patterns in Ruby

    But I didn't see anything in them for my sorting problem, so I Googled
    for it. Thus I found this concise method, but I couldn't figure out
    how to make "self" be my array of arrays.

    John: Thanks for that sort_by method using a block to return the
    specific value (from each element) I want the sort method to use.
    Very tidy!

    Brian: Thanks for
    1. addressing my key issue about how to make "self" be my array, so to
    speak. Question, would that redefinition of <=> contaminate uses of
    "sort" in other top-level classes? I could test for an answer, but I
    might miss something.
    2. using "delegation", which I've seen in those textbooks but not yet
    internalized.

    Best wishes,
    Richard
     
    RichardOnRails, Apr 21, 2011
    #4
  5. RichardOnRails

    7stud -- Guest

    Brian Candler wrote in post #994252:
    In other words, your spaceship operator only gets applied to objects of
    your class--not any ole' array.
     
    7stud --, Apr 21, 2011
    #5
  6. RichardOnRails wrote in post #994287:
    No. I only redefined <=> within the class MyElement, so it only affects
    what happens when you call <=> on an object of class MyElement.

    When you call sort on an Array (that is, your outer array which includes
    the pairs), internally it does a quicksort. The quicksort calls a.<=>(b)
    for various pairs of elements a and b within the array.

    Basically it means passing through method calls to the underlying
    object. You can do this explicitly for each method of interest:

    class MyElement
    def initialize(a)
    @a = a
    end
    def size
    @a.size
    end
    def [](index)
    @a[index]
    end
    def []=(index,val)
    @a[index] = val
    end
    ... etc
    end

    This gives you an opportunity to customise the behaviour in any way you
    like, or have one method call combine the results from invoking multiple
    underlying objects, or whatever you like.

    If you find it tedious to repeat lots of method definitions, then you
    can look at method_missing, or delegate.rb in the Ruby standard library.
    There are examples in the source code. In this case:

    require 'delegate'
    class MyElement < DelegateClass(Array)
    def <=>(other)
    self[1] <=> other[1]
    end
    end

    This creates a new class which passes all unknown methods to the
    underlying object.

    a = MyElement.new([:x, :y])
    puts a[1]

    You have called the '[]' method on your object of class MyElement, and
    it's automatically passed through to the '[]' method on the underlying
    Array object.
     
    Brian Candler, Apr 21, 2011
    #6
  7. 7stud: Thanks for your (comforting :BG) explanation
    Brian: Thanks for your expanded explanation

    While I'm still playing with delegation, I've taken "ownership" of a
    couple of the ideas previously offered, most notably "sort_by" and:
    class Meta < Array
    def <=>(other)
    self[1] <=> other[1]
    end
    end

    a = []
    [ [:A,12], [:B,30], [:C,4] ]. each { |element| a <<
    Meta.new(element) }
    p a.sort

    I'm eternally grateful for this website. I'm a retired programmer
    relatively new to Ruby and Rails. I'd quit programming except for the
    generosity of members of this NG.

    Best wishes,
    Richard
     
    RichardOnRails, Apr 22, 2011
    #7
  8. RichardOnRails wrote in post #994401:
    And a slightly tidier way is to use 'map' (aliased as 'collect') which
    iterates and accumulates the output array in one go:

    a = [ [:A,12], [:B,30], [:C,4] ].map { |element| Meta.new(element) }
    p a.sort

    Regards,

    Brian.
     
    Brian Candler, Apr 26, 2011
    #8
    1. Advertisements

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments (here). After that, you can post your question and our members will help you out.