Interaction with the Ruby combinator <=>

It is often required to implement the operator <=> (comparison or "spaceship") in the product data type, i.e. a class with several fields (all of which (hopefully!) already have <=> implemented), comparing the fields in a specific order.

 def <=>(o) f1 < o.f1 && (return -1) f1 > o.f1 && (return 1) f2 < o.f2 && (return -1) f2 > o.f2 && (return 1) return 0 end 

This is tedious and error prone, especially with lots of fields. This is error prone enough, and I often feel like I should unit test this function, which simply adds to the tediousness and verbosity.

Haskell offers a particularly good way to do this:

  import Data.Monoid (mappend)
 import Data.Ord (comparing)

 - From the standard library:
 - data Ordering = LT |  Eq |  GT

 data D = D {f3 :: Int, f2 :: Double, f1 :: Char} deriving Show

 compareD :: D -> D -> Ordering
 compareD = foldl1 mappend [comparing f1, comparing f2, comparing f3]

(For those not familiar with fold , the above extends to

 comparing f1 `mappend` comparing f2 `mappend` comparing f3 

which creates a function that can be applied to two D s, to create Ordering .)

The definition of compareD so simple that it is obviously correct, and I would not feel the need for a unit test even without checking the static type.

Actually, the question may even be a little more interesting than this, since I do not want to use only the standard operator <=> , but sort it differently at different times, for example:

  sortByOrderings :: [a -> a -> Ordering] -> [a] -> [a]
 sortByOrderings = sortBy.  foldl1 mappend

 sortByF3F1 = sortByOrderings [comparing f3, comparing f1]
 sortByF2F3 = sortByOrderings [comparing f2, comparing f3]

So the questions are:

  • What is a typical way to implement this kind of thing in Ruby?
  • What is the best way to do this using what is defined in standard libraries?
  • How close can you get the Haskell code above and how reliable is it in comparison? If necessary, how can one ensure that the fields have correctly implemented <=> or < and > operators?

Incidentally, although this is a Ruby question, I will gladly consider discussing Haskell methods on the topic if the elders of this site agree. Please feel free to comment on whether this is suitable or not, and if so, check this 'haskell' post.

+6
source share
4 answers

Here's a riff on your idea. It does not define any additional constants, allows you to use any combination of instance variables and methods to compare two objects, has an early output at an uneven level, and includes all methods defined by Comparable.

 class Object def self.compare_by(*symbols) include Comparable dispatchers = symbols.map do |symbol| if symbol.to_s =~ /^@/ lambda { |o| o.instance_variable_get(symbol) } else lambda { |o| o.__send__(symbol) } end end define_method('<=>') do |other| dispatchers.inject(0) do |_,dispatcher| comp = dispatcher[self] <=> dispatcher[other] break comp if comp != 0 comp end end end end class T def initialize(name,f1,f2,f3) @name,@f1, @f2, @f3 = name,f1, f2, f3; end def f1 puts "checking #@name f1" @f1 end def f3 puts "checking #@name f3" @f3 end compare_by :f1, :@f2, :f3 end w = T.new('x',1,1,2) x = T.new('x',1,2,3) y = T.new('y',2,3,4) z = T.new('z',2,3,5) pw < x #=> checking x f1 # checking x f1 # true px == y #=> checking x f1 # checking y f1 # false py <= z #=> checking y f1 # checking z f1 # checking y f3 # checking z f3 # true 

If you want, you can insert additional error checking there to make sure that the values ​​used for comparison actually respond to <=> (using respond_to? '<=>' ), and try to give more clear error messages in case if they do not.

+7
source

Here's what I do to make custom sorting rules more manageable: on all my classes that I ever need to sort, I define to_sort methods that return arrays and then override <=> to use to_sort:

 class Whatever def to_sort [@mainkey,@subkey,@subsubkey] end def <=>(o) self.to_sort <=> o.to_sort end end 

Thus, the sorting of any Whatevers array (including the heterogeneous arrays of Whatevers and Whateverothers and Whathaveyours, all of which implement type-specific functions to_sort and this is the same <=> override) simply divides internally by sorting the array of arrays.

+8
source

I used a similar approach like rampion, but wanted to handle the case where attributes could be nil .

 module ComparableBy def comparable_by(*attributes) include Comparable define_method(:<=>) do |other| return if other.nil? attributes.each do |attribute| left = self.__send__(attribute) right = other.__send__(attribute) return -1 if left.nil? return 1 if right.nil? comparison = left <=> right return comparison unless comparison == 0 end return 0 end end end 

Usage example

 SomeObject = Struct.new(:a, :b, :c) do extend ComparableBy comparable_by :a, :b, :c end 
+2
source

Well, here quickly hack the extension to Object so that this happens in what seems like a pretty nice way.

 class Object def self.spaceship_uses(*methods) self.const_set(:SPACESHIP_USES, methods) end def <=>(o) raise(NoMethodError, "undefined method `<=>' for #{self.inspect}") \ unless self.class.const_defined?(:SPACESHIP_USES) self.class.const_get(:SPACESHIP_USES).each { |sym| self.send(sym) < o.send(sym) && (return -1) self.send(sym) > o.send(sym) && (return 1) } return 0 end end class T def initialize(f1, f2) @f1, @f2 = f1, f2; end attr_reader :f1, :f2 spaceship_uses :f1, :f2 end 

This, of course, does not concern any typing problems, to make sure that < and > correctly implemented for objects returned by methods in SPACESHIP_USES . But then, having received, being Ruby, this is probably good, isn't it?

Brief comments may comment, but I would be interested to see detailed discussions and extensions in other answers.

0
source

All Articles