Ruby <=> 组合器的实现
-
22-08-2019 - |
题
人们常常希望实施 <=>
(比较或“太空飞船”)产品数据类型上的运算符,即具有多个字段的类(所有这些(我们希望!)已经有 <=>
已实现),按一定顺序比较字段。
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]
那么,问题是:
- 在 Ruby 中实现此类事情的典型方法是什么?
- 使用标准库中定义的内容最好的方法是什么?
- 相比之下,我们可以与上面的 Haskell 代码有多接近,以及它的可靠性如何?如有必要,如何确保这些领域得到正确实施
<=>
或者<
和>
运营商?
顺便说一句,虽然这是一个 Ruby 问题,但如果本网站的前辈们同意的话,我很乐意考虑对 Haskell 技术进行主题讨论。请随意评论这是否合适,如果合适,也请将此帖子标记为“haskell”。
解决方案
这是你的想法的即兴表演。它没有定义任何额外的常量,允许您使用实例变量和方法的任意组合来比较两个对象,在不等于时提前退出,并包含 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? '<=>'
),并尝试在没有哪个情况下给出更清晰的错误消息。
其他提示
以下是我为使自定义排序规则更易于管理而所做的事情:在我需要排序的所有类上,我定义返回数组的“to_sort”方法,然后重写 <=> 以使用 to_sort:
class Whatever
def to_sort
[@mainkey,@subkey,@subsubkey]
end
def <=>(o)
self.to_sort <=> o.to_sort
end
end
因此,对任何 Whats 数组进行排序(包括Whatevers、Whateverothers 和 Whathaveyours 的异构数组,所有这些数组都实现特定于类型的 to_sort 函数和相同的 <=> 覆盖)只是在内部转移到对数组数组进行排序。
我采取了与 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
好吧,这是一个扩展的快速破解 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,这可能没问题,不是吗?
简短的评论可以对此发表评论,但我有兴趣在其他答案中看到详细的讨论和扩展。