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 Ds, 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:

  1. ¿Cuál es la forma típica de la implementación de este tipo de cosas en Ruby?
  2. ¿Cuál es la mejor manera de hacerlo utilizando sólo lo que se define en las bibliotecas estándar?
  3. ¿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.

¿Fue útil?

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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top