Implementação do rubi <=> Combinator
-
22-08-2019 - |
Pergunta
Não raro, se quer implementar a <=>
(comparação, ou "nave espacial") operador em um tipo de dados de produto, ou seja, uma classe com vários campos (todos os quais (esperamos!) Já <=>
implementado), comparando os campos em uma determinada ordem.
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
Isto é tanto tedioso e propenso a erros, especialmente com um monte de campos. bastante propenso a erros É que eu freqüentemente sinto que eu deveria teste de unidade que função, que apenas contribuem para o tédio e detalhamento.
Haskell oferece uma particularmente agradável maneira de fazer isso:
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]
(Para aqueles não familiarizados com fold
, o acima se expande para
comparing f1 `mappend` comparing f2 `mappend` comparing f3
que produz uma função que pode ser aplicado a duas D
s, para produzir um Ordering
.)
O defintion de compareD
é tão simples que é obviamente correta, e eu não sinto a necessidade de unidade de teste que mesmo sem a verificação de tipo estático.
Na verdade, a questão pode ser ainda um pouco mais interessante do que isso, já que não pode querer usar apenas o operador <=>
padrão, mas tipo de maneiras diferentes em momentos diferentes, p.ex.:.
sortByOrderings :: [a -> a -> Ordering] -> [a] -> [a] sortByOrderings = sortBy . foldl1 mappend sortByF3F1 = sortByOrderings [comparing f3, comparing f1] sortByF2F3 = sortByOrderings [comparing f2, comparing f3]
Assim, as perguntas:
- O que é a maneira típica de implementar esse tipo de coisa em Ruby?
- O que é a melhor maneira de fazê-lo usando apenas o que está definido nas bibliotecas padrão?
- Quão perto se pode obter o código Haskell acima, e quão confiável é, em comparação? Se necessário, como se pode garantir que os campos têm uma
<=>
adequadamente implementadas ou operadores<
e>
?
Incidentalmente, enquanto esta é uma questão Ruby, eu estou feliz em considerar a discussão das técnicas Haskell on-tema, se os anciãos deste site assim o decidirem. Sinta-se livre para comentar se isso é apropriado ou não e, se for, tag este post 'haskell' também.
Solução
Aqui está um riff em sua idéia. Ela não define quaisquer constantes extras, permite a utilização de qualquer combinação de variáveis ??e métodos de instância para comparar dois objetos, tem saída no início não-igual, e inclui todos os métodos definidos por Comparável.
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
Se você quiser, você pode inserir alguma verificação de erros extra lá para se certificar de que
os valores utilizados para comparar realmente responder a <=>
(usando respond_to? '<=>'
), e tentar
dar mensagens de erro mais claras no caso Qquando eles não.
Outras dicas
Aqui está o que eu faço para fazer a triagem regras mais manejável costume: em todas as minhas aulas eu já precisa classificar, eu defino "to_sort" métodos que os arrays de retorno, e em seguida, substituir <=> ao uso to_sort:
class Whatever
def to_sort
[@mainkey,@subkey,@subsubkey]
end
def <=>(o)
self.to_sort <=> o.to_sort
end
end
Assim triagem qualquer matriz de Whatevers (incluindo matrizes heterogéneas de Whatevers e Whateverothers e Whathaveyours, todos os quais implementam específica do tipo to_sort funções e esta mesma substituição <=>) apenas recai internamente para classificar um conjunto de matrizes.
Eu levei uma abordagem semelhante à rampion, mas queria lidar com o caso onde os atributos poderia ser 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
Bem, aqui está um truque rápido em uma extensão para Object
para que isso aconteça no que parece ser uma razoavelmente boa forma.
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
Isto, obviamente, não lidar com quaisquer problemas de digitação, para se certificar de que <
e >
são adequadamente aplicadas para os objetos retornados pelos métodos em SPACESHIP_USES
. Mas, então, o ganho, sendo Ruby, este é provavelmente muito bem, não é?
comentários curtos podem comentar sobre isso, mas eu estaria interessado em ver discussão detalhada e extensões em outras respostas.