La mise en œuvre du Ruby Combinator <=>
-
22-08-2019 - |
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 D
s, 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:
- Quelle est la manière typique de mettre en œuvre ce genre de chose en Ruby?
- Quelle est la plus belle façon de le faire en utilisant simplement ce qui est défini dans les bibliothèques standard?
- À 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.
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.