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

  1. O que é a maneira típica de implementar esse tipo de coisa em Ruby?
  2. O que é a melhor maneira de fazê-lo usando apenas o que está definido nas bibliotecas padrão?
  3. 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.

Foi útil?

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

Exemplo de Uso:

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.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top