L'attuazione del Rubino <=> Combinator
-
22-08-2019 - |
Domanda
Non di rado, si vuole implementare il <=>
(confronto, o "nave spaziale") Operatore su un tipo di dati di prodotto, vale a dire, una classe con più campi (tutti di che (speriamo!) Hanno già <=>
implementato), mettendo a confronto i campi in un certo ordine.
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
Questo è sia noioso e soggetto a errori, soprattutto con un sacco di campi. E 'soggetto a errori abbastanza che mi sento spesso che dovrei unità di prova quella funzione, che aggiunge solo per la noia e la verbosità.
Haskell offre una particolarmente piacevole modo di fare questo:
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]
(Per chi non ha familiarità con fold
, quanto sopra si espande a
comparing f1 `mappend` comparing f2 `mappend` comparing f3
che produce una funzione che può essere applicato a due D
s, per produrre un Ordering
.)
alla definizione di compareD
è così semplice che è ovviamente corretto, e non vorrei sentire il bisogno di test di unità anche senza il controllo di tipo statico.
In realtà, la questione può essere anche un po 'più interessante di questo, dal momento che non può decidere di utilizzare solo l'operatore <=>
standard, ma specie in modi diversi in tempi diversi, per esempio:.
sortByOrderings :: [a -> a -> Ordering] -> [a] -> [a] sortByOrderings = sortBy . foldl1 mappend sortByF3F1 = sortByOrderings [comparing f3, comparing f1] sortByF2F3 = sortByOrderings [comparing f2, comparing f3]
Quindi, le domande:
- Qual è il modo tipico di attuare questo genere di cose in Ruby?
- Qual è il modo più bello di farlo utilizzando solo ciò che è definito nelle librerie standard?
- Quanto vicino è possibile ottenere uno per il codice Haskell sopra, e come affidabile è, in confronto? Se necessario, come si può garantire che i campi hanno un
<=>
correttamente attuato o operatori<
e>
?
Per inciso, mentre questa è una domanda Ruby, sono felice di prendere in considerazione la discussione delle tecniche di Haskell in-topic se gli anziani di questo sito sono d'accordo. Non esitate a commentare se questo è opportuno o meno e, se lo è, tag questo post 'Haskell' pure.
Soluzione
Ecco un riff sulla vostra idea. Essa non definisce alcuna costanti in più, consente di utilizzare qualsiasi combinazione di variabili e metodi di istanza per confrontare due oggetti, ha l'uscita nella fase iniziale non-uguali, e comprende tutti i metodi definiti da Paragonabile.
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 si volesse, si potrebbe inserire qualche errore in più check-in là per fare in modo che
i valori utilizzati per confrontare realmente rispondere alle <=>
(utilizzando respond_to? '<=>'
), e cercano di
danno messaggi di errore più chiari nel caso qQualora non lo fanno.
Altri suggerimenti
Ecco quello che faccio per rendere l'ordinamento personalizzato regole più gestibile: su tutte le mie classi ho mai bisogno di ordinare, definisco metodi "to_sort" che restituiscono array, e quindi sovrascrivere <=> utilizzare to_sort:
class Whatever
def to_sort
[@mainkey,@subkey,@subsubkey]
end
def <=>(o)
self.to_sort <=> o.to_sort
end
end
Così classificare qualsiasi matrice di Whatevers (compresi gli array eterogenei di Whatevers e Whateverothers e Whathaveyours, tutte implementano funzioni to_sort tipiche specifiche e questo stesso <=> override) solo devolve internamente per classificare un array di array.
Ho preso un approccio simile a quello raperonzolo, ma voluto gestire il caso in cui gli attributi potrebbero essere 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
Esempio di utilizzo:
SomeObject = Struct.new(:a, :b, :c) do
extend ComparableBy
comparable_by :a, :b, :c
end
Bene, ecco un trucco veloce ad una proroga per Object
per rendere questo accada in quello che sembra essere un modo ragionevolmente bello.
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
Questo, naturalmente, non si occupa di tutti i problemi di battitura, per assicurarsi che <
e >
sono implementate correttamente per gli oggetti restituiti dai metodi di SPACESHIP_USES
. Ma poi ottenere, essendo Ruby, questo è probabilmente bene, non è vero?
commenti brevi commenti su questo, ma sarei interessato a vedere la discussione dettagliata e le estensioni in altre risposte.