Question

ne est pas rare, on veut mettre en œuvre le <=> (comparaison, ou « vaisseau spatial ») opérateur sur un produit type de données, par exemple, une classe avec plusieurs champs (dont tous (nous l'espérons!) Ont déjà <=> mis en œuvre), la comparaison les champs dans un certain ordre.

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

est à la fois fastidieuse et sujette aux erreurs, en particulier avec beaucoup de champs. Il est assez sujette aux erreurs que je me sens souvent que je devrais test unitaire que la fonction, ce qui ajoute à la pénibilité et verbosité.

Haskell offre une manière particulièrement agréable de faire ceci:

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]

(Pour ceux qui ne connaissent fold, le développe ci-dessus pour

comparing f1 `mappend` comparing f2 `mappend` comparing f3

qui produit une fonction qui peut être appliqué à deux Ds, pour produire un Ordering).

La défintion de compareD est si simple qu'il est évidemment correct, et je ne sentirait pas la nécessité de test unitaire même sans vérification de type statique.

En fait, la question peut être même un peu plus intéressant que cela, car je veux pas simplement utiliser l'opérateur standard <=>, mais sorte de différentes manières à différents moments, par exemple:.

sortByOrderings :: [a -> a -> Ordering] -> [a] -> [a]
sortByOrderings = sortBy . foldl1 mappend

sortByF3F1 = sortByOrderings [comparing f3, comparing f1]
sortByF2F3 = sortByOrderings [comparing f2, comparing f3]

Alors, les questions suivantes:

  1. Quelle est la manière typique de mettre en œuvre ce genre de chose en Ruby?
  2. Quelle est la plus belle façon de le faire en utilisant simplement ce qui est défini dans les bibliothèques standard?
  3. À quelle distance peut-on obtenir au code Haskell ci-dessus, et comment est-il fiable, en comparaison? Le cas échéant, comment peut-on faire en sorte que les champs ont un <=> correctement mis en œuvre ou les opérateurs de < et >?

Soit dit en passant, tout cela est une question Ruby, je suis heureux d'envisager la discussion des techniques Haskell sur-sujet si les anciens de ce site sont d'accord. S'il vous plaît ne hésitez pas à dire si c'est approprié ou non et, si elle est, étiquette ce poste « haskell » ainsi.

Était-ce utile?

La solution

Voici un riff sur votre idée. Il ne définit pas de constantes supplémentaires, vous permet d'utiliser une combinaison de variables et méthodes exemple pour comparer deux objets, a la sortie tôt sans égal, et comprend toutes les méthodes définies par 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 vous voulez, vous pouvez insérer une erreur supplémentaire de vérifier là-bas pour vous assurer que les valeurs utilisées pour comparer répondent effectivement à <=> (en utilisant respond_to? '<=>'), et essayer de donnent des messages plus clairs d'erreur dans le cas Llorsque ils ne le font pas.

Autres conseils

Voici ce que je fais pour établir des règles de tri personnalisée plus facile à gérer: besoin de toutes mes classes, je jamais trier, je définir des méthodes « de to_sort » qui renvoient des tableaux, puis passer outre à utiliser to_sort <=>:

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

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

tri Ainsi, toute gamme de Whatevers (y compris des réseaux hétérogènes de Whatevers et Whateverothers et Whathaveyours, qui tous mettent en oeuvre des fonctions de to_sort spécifiques de type et de cette même <=> override) seulement dévolue à l'intérieur de tri d'un tableau de tableaux.

Je pris une approche similaire à celle raiponce, mais voulais gérer le cas où les attributs pourraient être 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

Exemple d'utilisation:

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

Eh bien, voici un hack rapide à une extension de Object pour y arriver dans ce qui semble être un moyen raisonnablement agréable.

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

Bien sûr, cela ne traite pas des problèmes de frappe, pour vous assurer que < et > sont correctement mis en œuvre pour les objets retournés par les méthodes SPACESHIP_USES. Mais gagner alors, étant Ruby, cela est probablement très bien, est-il pas?

commentaires courts peuvent commenter, mais je serais intéressé à voir la discussion détaillée et extensions dans d'autres réponses.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top