2009-05-19 50 views
6

很少有人希望在产品数据类型上实现<=>(比较或“飞船”)运算符,即具有多个字段的类(所有这些(我们希望!有<=>实施),比较按特定顺序的字段。Ruby的实现<=> Combinator

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 

这既烦琐又容易出错,特别是在很多领域。这很容易出错,我经常觉得我应该单元测试这个功能,这只会增加单调乏味和冗长。

Haskell中提供这样的一种特别好的方法:

 
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] 

(对于那些不熟悉fold,上述膨胀以

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

其产生可被应用到两个功能D s,产生一个Ordering。)

compareD的定义是如此简单,以至于显然是正确的,即使没有静态类型检查,我也不会觉得需要进行单元测试。

其实,问题可能比这个稍微更有趣,因为我可能不希望只使用标准<=>运营商,但那种在不同的时间不同的方式,如:

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

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

所以,问题:

  1. 在Ruby中实现这种事情的典型方法是什么?
  2. 使用标准库中定义的内容的最好方法是什么?
  3. 与上面的Haskell代码有多接近,相比之下它有多可靠?如果有必要,如何确保这些字段具有正确实施的<=><>运营商?

令人难以置信的是,虽然这是一个Ruby问题,但我很高兴考虑讨论Haskell在主题上的技巧,如果这个网站的长老如此赞同。请随时评论这是否合适,如果是的话,也可以标记这篇文章'haskell'。

回答

7

这是你的想法的一个即兴。它没有定义任何额外的常量,允许您使用实例变量和方法的任意组合来比较两个对象,在不等于的情况下提前退出,并且包含由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's f1" 
     @f1 
    end 
    def f3 
     puts "checking #@name's 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) 

p w < x #=> checking x's f1 
      # checking x's f1 
      # true 
p x == y #=> checking x's f1 
      # checking y's f1 
      # false 
p y <= z #=> checking y's f1 
      # checking z's f1 
      # checking y's f3 
      # checking z's f3 
      # true 

如果你愿意,你可以插入一些额外的错误在那里检查,确保使用 的值进行比较其实到<=>(使用respond_to? '<=>')作出回应,并试图 的情况下能提供更清晰的错误信息他们没有。

0

好吧,这里是一个快速入门到Object的扩展,使得这似乎是一个相当不错的方式。

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 

这当然不处理任何类型的问题,以确保<>由方法SPACESHIP_USES返回的对象是正确执行。但是,作为Ruby来说,这可能很好,不是吗?

简短的评论可以对此发表评论,但我会有兴趣在其他答案中看到详细的讨论和扩展。

+2

这个版本意味着所有对象的respond_to?(:<=>),但大多数人会提出一个NoMethodError。这不是一个好主意。您可以尝试将:<=>的定义移动到:spaceship_uses调用中,以解决问题。只需使用define_method(:<=>)do ... end – 2009-05-20 03:51:33

+0

也不要忘记包含Comparagble,所以您的其他比较运算符是为您定义的。 – rampion 2009-05-20 12:45:22

+0

这是一个修改,其中包括我和Gaius的变化:http://gist.github.com/114798 – rampion 2009-05-20 13:04:39

8

这是我怎么做才能让自定义排序规则更加易于管理:在我所有的课我以往任何时候都需要梳理,我定义“to_sort”方法返回数组,然后覆盖< =>使用to_sort:

class Whatever 
    def to_sort 
    [@mainkey,@subkey,@subsubkey] 
    end 

    def <=>(o) 
    self.to_sort <=> o.to_sort 
    end 
end 

因此,对任何一个Whatevers数组(包括异构的Whatevers和Whateverless和Whathaveyours数组,其中所有这些都实现类型特定的to_sort函数以及这个相同的代码)进行排序只是在内部对数组进行排序。

2

我采取了类似的方法,但想要处理的情况下,属性可能是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 

实例应用:

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