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

  1. Qual è il modo tipico di attuare questo genere di cose in Ruby?
  2. Qual è il modo più bello di farlo utilizzando solo ciò che è definito nelle librerie standard?
  3. 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.

È stato utile?

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.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top