La aplicación de la Rubí <=> Combinator
-
22-08-2019 - |
Pregunta
No es infrecuente que se quiere poner en práctica el <=>
(comparación, o "nave espacial") del operador en un tipo de datos de productos, es decir, una clase con múltiples campos (todos los cuales (esperamos!) Ya han <=>
implementado), comparando los campos en un orden determinado.
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
Esto es tedioso y propenso a errores, sobre todo con una gran cantidad de campos. Es propenso a errores basta con que frecuencia siento que debería unidad de prueba esa función, lo que aumenta el tedio y la verbosidad.
Haskell ofrece una particular buena manera de hacer esto:
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 aquellos que no están familiarizados con fold
, lo anterior se expande a
comparing f1 `mappend` comparing f2 `mappend` comparing f3
que produce una función que se puede aplicar a dos D
s, para producir un Ordering
.)
El Defintion de compareD
es tan simple que obviamente es correcta, y no se sentiría la necesidad de probar la unidad que incluso sin la comprobación de tipo estático.
En realidad, la pregunta puede ser incluso un poco más interesante que esto, ya que puede que no desee utilizar sólo el operador <=>
estándar, pero una especie de diferentes maneras en diferentes momentos, por ejemplo:.
sortByOrderings :: [a -> a -> Ordering] -> [a] -> [a] sortByOrderings = sortBy . foldl1 mappend sortByF3F1 = sortByOrderings [comparing f3, comparing f1] sortByF2F3 = sortByOrderings [comparing f2, comparing f3]
Por lo tanto, las preguntas:
- ¿Cuál es la forma típica de la implementación de este tipo de cosas en Ruby?
- ¿Cuál es la mejor manera de hacerlo utilizando sólo lo que se define en las bibliotecas estándar?
- ¿Qué tan cerca puede llegar a uno el código Haskell anterior, y el grado de fiabilidad es que, en comparación? Si es necesario, ¿cómo se puede asegurar que los campos tienen una
<=>
implementado correctamente o los operadores<
y>
?
A propósito, si bien esto es una pregunta Ruby, estoy feliz de tener en cuenta la discusión de las técnicas de Haskell en tema de interés si los ancianos de este sitio así lo acuerdan. Siéntase libre de comentar si eso es adecuado o no y, si lo es, etiqueta de este post 'Haskell' también.
Solución
Aquí hay un riff en su idea. No define ninguna constante de más, le permite utilizar cualquier combinación de variables y métodos de instancia para comparar dos objetos, tiene salida desde el principio no igual, e incluye todos los métodos definidos por 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
Si quisiera, podría insertar algún error en las comprobaciones de allí para asegurarse de que
los valores utilizados para comparar en realidad responden a <=>
(usando respond_to? '<=>'
), y tratan de
dan mensajes de error más claros en el caso cCuando no lo hacen.
Otros consejos
Esto es lo que he hecho para que las reglas de ordenación más manejable por el usuario: en todas mis clases siempre hay que solucionar, defino métodos "to_sort" que devuelven matrices, y luego anular <=> utilizar to_sort:
class Whatever
def to_sort
[@mainkey,@subkey,@subsubkey]
end
def <=>(o)
self.to_sort <=> o.to_sort
end
end
Así clasificar cualquier conjunto de Whatevers (incluyendo matrices heterogéneas de Whatevers y Whateverothers y Whathaveyours, todos los cuales implementan funciones to_sort específico del tipo y este mismo <=> override) justo recae internamente para clasificar una matriz de matrices.
Me tomó un enfoque similar al rampion, pero quería manejar el caso en que los atributos podrían 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
Ejemplo de Uso:
SomeObject = Struct.new(:a, :b, :c) do
extend ComparableBy
comparable_by :a, :b, :c
end
Bueno, aquí hay un truco rápido en una extensión de Object
para que esto suceda en lo que parece ser una manera bastante agradable.
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
Por supuesto, esto no se ocupa de las cuestiones de escritura, para asegurarse de que <
y >
aplicación adecuada para los objetos devueltos por los métodos en SPACESHIP_USES
. Pero entonces ganar, siendo Ruby, esto es probablemente muy bien, ¿verdad?
comentarios cortos pueden comentar sobre esto, pero estaría interesado en ver la discusión detallada y extensiones en otras respuestas.