【问题标题】:Impementation of the Ruby <=> CombinatorRuby <=> 组合器的实现
【发布时间】:2010-10-27 07:41:20
【问题描述】:

经常有人想在产品数据类型上实现&lt;=&gt;(比较,或“宇宙飞船”)运算符,即具有多个字段的类(所有这些(我们希望!)已经有&lt;=&gt;已实现),按一定顺序比较字段。

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 提供了一种特别好的方法:

导入 Data.Monoid (mappend) 导入 Data.Ord(比较) -- 来自标准库: -- 数据排序 = LT |情商 |燃气轮机 数据 D = D { f3 :: Int, f2 :: Double, f1 :: Char } 推导 Show compareD :: D -> D -> 排序 compareD = foldl1 mappend [比较 f1,比较 f2,比较 f3]

(对于不熟悉fold的人,以上扩展为

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

它产生一个函数,可以应用于两个Ds,产生一个Ordering。)

compareD 的定义非常简单,显然是正确的,即使没有静态类型检查,我也不觉得需要对其进行单元测试。

实际上,这个问题可能比这个更有趣,因为我可能不想只使用标准的&lt;=&gt; 运算符,而是在不同的时间以不同的方式排序,例如:

sortByOrderings :: [a -> a -> 排序] -> [a] -> [a] sortByOrderings = sortBy 。 foldl1 映射 sortByF3F1 = sortByOrderings [比较 f3,比较 f1] sortByF2F3 = sortByOrderings [比较 f2,比较 f3]

所以,问题:

  1. 在 Ruby 中实现这类事情的典型方式是什么?
  2. 仅使用标准库中定义的内容最好的方法是什么?
  3. 相比之下,与上面的 Haskell 代码有多接近,它的可靠性如何?如有必要,如何确保字段具有正确实现的&lt;=&gt;&lt;&gt; 运算符?

顺便说一句,虽然这是一个 Ruby 问题,但如果本网站的长辈们同意,我很高兴考虑讨论有关 Haskell 技术的主题。请随时评论这是否合适,如果合适,请将此帖子标记为“haskell”。

【问题讨论】:

    标签: ruby combinators spaceship-operator


    【解决方案1】:

    为了使自定义排序规则更易于管理,我做了以下工作:在我需要排序的所有类中,我定义了返回数组的“to_sort”方法,然后重写 以使用 to_sort:

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

    因此,对任意数组的排序(包括任意数组、任意其他数组和 Whathaveyours 的异构数组,所有这些数组都实现了特定于类型的 to_sort 函数和相同的 覆盖)只是在内部转移到对数组数组进行排序。

    【讨论】:

    • 注意,如果你的任何实例变量是nil,这将会爆炸。
    【解决方案2】:

    这是你的想法的即兴表演。它不定义任何额外的常量,允许您使用实例变量和方法的任意组合来比较两个对象,在不相等时提前退出,并包含 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
    

    如果您愿意,您可以在其中插入一些额外的错误检查以确保 用于比较的值实际上响应&lt;=&gt;(使用respond_to? '&lt;=&gt;'),并尝试 在没有的情况下给出更清晰的错误消息。

    【讨论】:

    • 好东西。它不像 Haskell 那样让我很容易做到sortBy,但它确实在处理默认比较方面做得很好!
    • 请记住,您可以使用与 Haskell 的 List.sortBy 几乎相同的方式使用 Enumerable#sort(只是没有柯里化,抱歉)。并且 Enumerable#sort_by 允许您在排序时定义排序键。
    【解决方案3】:

    我采用了与 rampion 类似的方法,但 想处理属性可能为 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
    

    【讨论】:

      【解决方案4】:

      好吧,这里有一个对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 中的方法返回的对象正确实现&lt;&gt;。但是,作为 Ruby,这可能很好,不是吗?

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

      【讨论】:

      • 这个版本意味着所有对象都会respond_to?(:),但大多数会引发NoMethodError。这不是一个好主意。您可以尝试将 : 的定义移动到 :spaceship_uses 调用中,这样可以解决问题。只需使用 define_method(:) do ... end
      • 另外,不要忘记包含 Comparagble,以便为您定义其他比较运算符。
      • 这是一个包含我和 Gaius 更改的修改:gist.github.com/114798
      • 嘿,rampion,这真是太棒了,红宝石代码。太棒了! (或者 Brava!如果你是个女孩,但毕竟你的个人资料照片上有 小胡子 :))
      猜你喜欢
      • 2014-07-31
      • 2020-02-14
      • 2013-07-23
      • 2017-11-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-02-09
      相关资源
      最近更新 更多