Ruby <=> 的实现 组合器

发布于 2024-07-20 06:30:53 字数 1704 浏览 8 评论 0 原文

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

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 的函数,生成一个 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”。

Not infrequently, one wants to implement the <=> (comparison, or "spaceship") operator on a product data type, i.e., a class with multiple fields (all of which (we hope!) already have <=> implemented), comparing the fields in a certain 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 both tedious and error-prone, especially with a lot of fields. It's error-prone enough that I frequently feel I should unit test that function, which just adds to the tediousness and verbosity.

Haskell offers a particularly nice way of doing 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 expands to

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

which produces a function that can be applied to two Ds, to produce an Ordering.)

The defintion of compareD is so simple that it's obviously correct, and I wouldn't feel the need to unit test it even without static type checking.

Actually, the question may be even slightly more interesting than this, since I may not want to use just the standard <=> operator, but sort in different ways at different times, e.g.:

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:

  1. What's the typical way of implementing this sort of thing in Ruby?
  2. What's the nicest way of doing it using just what's defined in the standard libraries?
  3. How close can one get to the Haskell code above, and how reliable is it, in comparison? If necessary, how can one ensure that the fields have a properly implemented <=> or < and > operators?

Incidently, while this is a Ruby question, I'm happy to consider discussion of the Haskell techniques on-topic if the elders of this site so agree. Please feel free to comment on whether that's appropriate or not and, if it is, tag this post 'haskell' as well.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

等风来 2024-07-27 06:30:54

为了使自定义排序规则更易于管理,我做了以下操作:在我需要排序的所有类上,我定义返回数组的“to_sort”方法,然后覆盖 <=>; 使用 to_sort:

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

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

因此,对任何 Whats 数组进行排序(包括 Whatvers 和 Whathaveothers 和 Whathaveyours 的异构数组,所有这些都实现特定于类型的 to_sort 函数和相同的 <=> 覆盖)只是在内部转移到对数组数组进行排序。

Here's what I do to make custom sorting rules more manageable: on all my classes 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 sorting any array of Whatevers (including heterogeneous arrays of Whatevers and Whateverothers and Whathaveyours, all of which implement type-specific to_sort functions and this same <=> override) just devolves internally to sorting an array of arrays.

分開簡單 2024-07-27 06:30:54

这是你的想法的即兴表演。 它没有定义任何额外的常量,允许您使用实例变量和方法的任意组合来比较两个对象,在不等于时提前退出,并包含 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? '<=>'),并尝试
在没有提供的情况下给出更清晰的错误消息。

Here's a riff on your idea. It doesn't define any extra constants, allows you to use any combination of instance variables and methods to compare two objects, has early exit on not-equal, and includes all the 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'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

If you wanted, you could insert some extra error checking in there to make sure that
the values used to compare actually respond to <=> (using respond_to? '<=>'), and try to
give clearer error messages in the case wwhere they don't.

等待我真够勒 2024-07-27 06:30:54

我采用了与 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

I took a similar approach as 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

Example Usage:

SomeObject = Struct.new(:a, :b, :c) do
  extend ComparableBy
  comparable_by :a, :b, :c
end
乱世争霸 2024-07-27 06:30:54

好吧,这里有一个对 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,这可能没问题,不是吗?

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

Well, here's a quick hack at an extension to Object to make this happen in what seems to be a reasonably 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 doesn't deal with any typing issues, to make sure that < and > are properly implemented for the objects returned by the methods in SPACESHIP_USES. But then gain, being Ruby, this is probably fine, isn't it?

Short comments can comment on this, but I'd be interested in seeing detailed discussion and extensions in other answers.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文