2009-05-19 18 views
6

No es infrecuente que se quiera implementar el operador <=> (comparación o "nave espacial") en un tipo de datos de producto, es decir, una clase con múltiples campos (¡esperamos!) tiene <=> implementado), comparando los campos en un cierto orden.Impulso de Ruby <=> Combinator

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, especialmente con muchos campos. Es lo suficientemente propenso a errores que a menudo siento que debería probar esa función en una unidad, lo que simplemente aumenta la tediosidad 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 puede ser aplicado a dos D s, para producir una Ordering.)

El Defintion de compareD es tan simple que obviamente es correcto, y no sentiría la necesidad de probarlo unitariamente incluso sin verificació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:

  1. ¿Cuál es la forma típica de implementar este tipo de cosas en Ruby?
  2. ¿Cuál es la mejor forma de hacerlo utilizando solo lo que se define en las bibliotecas estándar?
  3. ¿Cuán cerca se puede llegar al código Haskell anterior, y qué tan confiable es, en comparación? Si es necesario, ¿cómo se puede asegurar que los campos tengan implementados correctamente los operadores <=> o < y >?

Incidentalmente, si bien esta es una pregunta de Ruby, estoy contento de considerar la discusión de las técnicas de Haskell sobre el tema si los mayores de este sitio lo aceptan. Por favor, siéntase libre de comentar si es apropiado o no y, si lo es, etiquete también esta publicación como 'haskell'.

Respuesta

7

Aquí hay un riff sobre su idea. No define ninguna constante adicional, le permite usar cualquier combinación de variables de instancia y métodos para comparar dos objetos, tiene salida anticipada en no iguales 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 de las comprobaciones de allí para asegurarse de que los valores usados ​​para comparar realmente responder a <=> (usando respond_to? '<=>'), y tratar de dar mensajes de error más claros en el caso donde no lo hacen

+0

Cosas geniales. No me da la misma cosa fácil de 'ordenar por' que tiene Haskell, ¡pero sin duda hace un gran trabajo al lidiar con las comparaciones predeterminadas! –

+0

Tenga en cuenta que puede usar el tipo de enumeración Enumerable casi de la misma manera que Haskell's List.sortBy (solo sin currying, lo siento). Y Enumerable # sort_by le permite definir la clave de clasificación en el tiempo de ordenamiento. – rampion

+0

+1, uno de los mejores pedacitos de código de Ruby que he visto en SO – Allyn

0

Bueno, aquí hay un truco rápido en una extensión de Object para que esto suceda de una manera razonablemente 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 > correcta ejecución de los objetos devueltos por los métodos de SPACESHIP_USES. Pero luego gana, siendo Ruby, esto probablemente esté bien, ¿no?

Los comentarios breves pueden comentar al respecto, pero me gustaría ver discusiones detalladas y extensiones en otras respuestas.

+2

Esta versión significa que todos los objetos se respond_to? (: <=>), pero la mayoría va a levantar una NoMethodError. Esa no es una buena idea. Puede intentar mover la definición de: <=> a la llamada: spaceship_uses, que solucionaría el problema. Solo use define_method (: <=>) do ... end –

+0

, no olvide incluir Comparagble, para que sus otros operadores de comparación estén definidos para usted. – rampion

+0

He aquí una modificación que incluye los cambios de mi y Gaius: http://gist.github.com/114798 – rampion

8

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 anulan < => utilizar to_sort:

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

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

Por lo tanto, la ordenación de cualquier conjunto de Whatevers (incluidas matrices heterogéneas de Whatevers y Whateverothers y Whathaveyours, todas las cuales implementan funciones específicas de tipo a_sort y esta misma < => reemplazan internamente para ordenar una matriz de matrices.

+0

Nota, esto explotará si cualquiera de tus variables de instancia es 'nil'. –

+0

Sí, de hecho parche NilClass para esta y algunas otras cosas, también ... –

2

Tomé un enfoque similar al rampion, pero quería manejar el caso donde los atributos podrían 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 

Ejemplo de Uso:

SomeObject = Struct.new(:a, :b, :c) do 
    extend ComparableBy 
    comparable_by :a, :b, :c 
end