2009-11-05 16 views
64

Non sono sicuro del miglior idioma per i richiami in stile C in Ruby - o se c'è qualcosa di ancora migliore (e meno come C). In C, vorrei fare qualcosa del tipo:Come implementare un "callback" in Ruby?

void DoStuff(int parameter, CallbackPtr callback) 
{ 
    // Do stuff 
    ... 
    // Notify we're done 
    callback(status_code) 
} 

Qual è un buon equivalente Ruby? In sostanza, voglio chiamare un metodo passato in classe, quando una certa condizione è soddisfatta all'interno di "DoStuff"

+0

Questo potrebbe essere utile: https://github.com/krisleech/wisper – Kris

risposta

79

Il rubino equivalente, che non è idiomatica, sarebbe:

def my_callback(a, b, c, status_code) 
    puts "did stuff with #{a}, #{b}, #{c} and got #{status_code}" 
end 

def do_stuff(a, b, c, callback) 
    sum = a + b + c 
    callback.call(a, b, c, sum) 
end 

def main 
    a = 1 
    b = 2 
    c = 3 
    do_stuff(a, b, c, method(:my_callback)) 
end 

L'approccio idiomatica sarebbe passare un blocco invece di un riferimento a un metodo. Un vantaggio che un blocco ha su un metodo indipendente è il contesto: un blocco è un closure, quindi può fare riferimento a variabili dall'ambito in cui è stato dichiarato. Questo riduce il numero di parametri che do_stuff deve passare al callback. Ad esempio:

def do_stuff(a, b, c, &block) 
    sum = a + b + c 
    yield sum 
end 

def main 
    a = 1 
    b = 2 
    c = 3 
    do_stuff(a, b, c) { |status_code| 
    puts "did stuff with #{a}, #{b}, #{c} and got #{status_code}" 
    } 
end 
+13

Se Stai usando rendimento, non hai bisogno e blocco nella lista degli argomenti. – Douglas

+33

Mi piace ancora usare la notazione '& block' perché allora diventa chiaro che il metodo prende un blocco solo guardando la prima riga della definizione. –

+0

concordato con commenti @Douglas; il blocco & mi ha fatto diventare tutto strano :( –

73

Questo "blocco idiomatico" è una parte fondamentale del Ruby quotidiano ed è frequentemente trattato in libri e tutorial. Il numero Ruby information section fornisce collegamenti a risorse di apprendimento [online] utili.


Il modo idiomatica è quello di utilizzare un blocco:

def x(z) 
    yield z # perhaps used in conjunction with #block_given? 
end 
x(3) {|y| y*y} # => 9 

O forse convertito in un Proc; qui mi dimostrano che il "blocco", convertito in Proc implicitamente con &block, è solo un altro valore "callable":

def x(z, &block) 
    callback = block 
    callback.call(z) 
end 

# look familiar? 
x(4) {|y| y * y} # => 16 

(utilizzare solo il modulo qui sopra per salvare il blocco-ora-Proc per un uso successivo o in altri casi particolari in quanto aggiunge in testa e il rumore sintassi)

Tuttavia, un lambda può essere utilizzato altrettanto facilmente (ma questo non è idiomatica):.

def x(z,fn) 
    fn.call(z) 
end 

# just use a lambda (closure) 
x(5, lambda {|y| y * y}) # => 25 

Mentre gli approcci di cui sopra possono tutti involucro "chiamando a Metodo" perché creano chiusure, vincolata Methods può anche essere trattato come prima classe oggetti richiamabili:

class A 
    def b(z) 
    z*z 
    end 
end 

callable = A.new.method(:b) 
callable.call(6) # => 36 

# and since it's just a value... 
def x(z,fn) 
    fn.call(z) 
end 
x(7, callable) # => 49 

Inoltre, a volte è utile utilizzare il metodo #send (in particolare se un metodo è conosciuta con il nome). Qui salva un oggetto Method intermedio creato nell'ultimo esempio; Ruby è un sistema che trasmette messaggi:

# Using A from previous 
def x(z, a): 
    a.__send__(:b, z) 
end 
x(8, A.new) # => 64 

Felice codifica!

+0

Questa è stata anche una bella resa: http://www.rubytapas.com/episodes/35-Callable – Manav

3

Quindi, questo può essere molto "non-Ruby", e io non sono uno sviluppatore "professionista" Ruby, quindi se voi ragazzi stanno andando a schiaffeggiare essere, si prega di essere gentile :)

Ruby ha un built -int modulo chiamato Observer. Non ho trovato facile da usare, ma ad essere sinceri non ho dato una grande possibilità. Nei miei progetti ho fatto ricorso alla creazione del mio tipo EventHandler (sì, io uso molto C#). Ecco la struttura di base:

class EventHandler 

    def initialize 
    @client_map = {} 
    end 

    def add_listener(id, func) 
    (@client_map[id.hash] ||= []) << func 
    end 

    def remove_listener(id) 
    return @client_map.delete(id.hash) 
    end 

    def alert_listeners(*args) 
    @client_map.each_value { |v| v.each { |func| func.call(*args) } } 
    end 

end 

Quindi, per utilizzare questo che è stato esposto come membro sola lettura di una classe:

class Foo 

    attr_reader :some_value_changed 

    def initialize 
    @some_value_changed = EventHandler.new 
    end 

end 

clienti della classe "Foo" possono abbonarsi a un evento come questo :

foo.some_value_changed.add_listener(self, lambda { some_func }) 

sono sicuro che questo non è idiomatico Ruby e io sono solo shoehorning mia C# esperienza in una nuova lingua, ma h come ha funzionato per me.

0

Spesso realizzo callback in Ruby come nell'esempio seguente. È molto comodo da usare.

class Foo 
    # Declare a callback. 
    def initialize 
    callback(:on_die_cast) 
    end 

    # Do some stuff. 
    # The callback event :on_die_cast is triggered. 
    # The variable "die" is passed to the callback block. 
    def run 
     while(true) 
     die = 1 + rand(6) 
     on_die_cast(die) 
     sleep(die) 
     end 
    end 

    # A method to define callback methods. 
    # When the latter is called with a block, it's saved into a instance variable. 
    # Else a saved code block is executed. 
    def callback(*names) 
     names.each do |name| 
     eval <<-EOF 
      @#{name} = false 
      def #{name}(*args, &block) 
       if(block) 
        @#{name} = block 
       elsif(@#{name}) 
        @#{name}.call(*args) 
       end 
      end 
     EOF 
     end 
    end 
end 

foo = Foo.new 

# What should be done when the callback event is triggered? 
foo.on_die_cast do |number| 
    puts(number) 
end 

foo.run 
5

Ha esplorato un po 'di più l'argomento e aggiornato il codice.

La seguente versione è un tentativo di generalizzare la tecnica, pur rimanendo estremamente semplificata e incompleta.

In gran parte ho rubato - orlo, trovato ispirazione - l'implementazione di callback di DataMapper, che mi sembra del tutto completo e bello.

vi consiglio vivamente di dare un'occhiata al codice @http://github.com/datamapper/dm-core/blob/master/lib/dm-core/support/hook.rb

In ogni caso, cercando di riprodurre la funzionalità utilizzando il modulo Observable era abbastanza coinvolgente e istruttiva. Alcune note:

metodo
  • aggiunto sembra essere bisogno, perché i metodi di istanza originali non sono disponibili al momento della registrazione dei callback
  • della classe tra cui è fatto sia osservato e di auto-osservatore
  • la esempio è limitato ai metodi di istanza, non supporta i blocchi, args ecc

codice:

require 'observer' 

module SuperSimpleCallbacks 
    include Observable 

    def self.included(klass) 
    klass.extend ClassMethods 
    klass.initialize_included_features 
    end 

    # the observed is made also observer 
    def initialize 
    add_observer(self) 
    end 

    # TODO: dry 
    def update(method_name, callback_type) # hook for the observer 
    case callback_type 
    when :before then self.class.callbacks[:before][method_name.to_sym].each{|callback| send callback} 
    when :after then self.class.callbacks[:after][method_name.to_sym].each{|callback| send callback} 
    end 
    end 

    module ClassMethods 
    def initialize_included_features 
     @callbacks = Hash.new 
     @callbacks[:before] = Hash.new{|h,k| h[k] = []} 
     @callbacks[:after] = @callbacks[:before].clone 
     class << self 
     attr_accessor :callbacks 
     end 
    end 

    def method_added(method) 
     redefine_method(method) if is_a_callback?(method) 
    end 

    def is_a_callback?(method) 
     registered_methods.include?(method) 
    end 

    def registered_methods 
     callbacks.values.map(&:keys).flatten.uniq 
    end 

    def store_callbacks(type, method_name, *callback_methods) 
     callbacks[type.to_sym][method_name.to_sym] += callback_methods.flatten.map(&:to_sym) 
    end 

    def before(original_method, *callbacks) 
     store_callbacks(:before, original_method, *callbacks) 
    end 

    def after(original_method, *callbacks) 
     store_callbacks(:after, original_method, *callbacks) 
    end 

    def objectify_and_remove_method(method) 
     if method_defined?(method.to_sym) 
     original = instance_method(method.to_sym) 
     remove_method(method.to_sym) 
     original 
     else 
     nil 
     end 
    end 

    def redefine_method(original_method) 
     original = objectify_and_remove_method(original_method) 
     mod = Module.new 
     mod.class_eval do 
     define_method(original_method.to_sym) do 
      changed; notify_observers(original_method, :before) 
      original.bind(self).call if original 
      changed; notify_observers(original_method, :after) 
     end 
     end 
     include mod 
    end 
    end 
end 


class MyObservedHouse 
    include SuperSimpleCallbacks 

    before :party, [:walk_dinosaure, :prepare, :just_idle] 
    after :party, [:just_idle, :keep_house, :walk_dinosaure] 

    before :home_office, [:just_idle, :prepare, :just_idle] 
    after :home_office, [:just_idle, :walk_dinosaure, :just_idle] 

    before :second_level, [:party] 

    def home_office 
    puts "learning and working with ruby...".upcase 
    end 

    def party 
    puts "having party...".upcase 
    end 

    def just_idle 
    puts "...." 
    end 

    def prepare 
    puts "preparing snacks..." 
    end 

    def keep_house 
    puts "house keeping..." 
    end 

    def walk_dinosaure 
    puts "walking the dinosaure..." 
    end 

    def second_level 
    puts "second level..." 
    end 
end 

MyObservedHouse.new.tap do |house| 
    puts "-------------------------" 
    puts "-- about calling party --" 
    puts "-------------------------" 

    house.party 

    puts "-------------------------------" 
    puts "-- about calling home_office --" 
    puts "-------------------------------" 

    house.home_office 

    puts "--------------------------------" 
    puts "-- about calling second_level --" 
    puts "--------------------------------" 

    house.second_level 
end 
# => ... 
# ------------------------- 
# -- about calling party -- 
# ------------------------- 
# walking the dinosaure... 
# preparing snacks... 
# .... 
# HAVING PARTY... 
# .... 
# house keeping... 
# walking the dinosaure... 
# ------------------------------- 
# -- about calling home_office -- 
# ------------------------------- 
# .... 
# preparing snacks... 
# .... 
# LEARNING AND WORKING WITH RUBY... 
# .... 
# walking the dinosaure... 
# .... 
# -------------------------------- 
# -- about calling second_level -- 
# -------------------------------- 
# walking the dinosaure... 
# preparing snacks... 
# .... 
# HAVING PARTY... 
# .... 
# house keeping... 
# walking the dinosaure... 
# second level... 

Questa semplice presentazione del l'uso di Observable potrebbe essere utile: http://www.oreillynet.com/ruby/blog/2006/01/ruby_design_patterns_observer.html

+0

Questa è una grande risorsa, grazie – gregf

+0

Spero non ti dispiaccia ma ho criptato il tuo codice e rielaborato un po 'per il mio progetto attuale.E' semplicistico al momento ma ci vai - Sentitevi liberi di criticare/scoraggiare, ecc. https://github.com/davesims/Simple-AOP/blob/master/lib/simple_aop.rb –

+0

Una domanda con cui ho lavorato: perché avete usato Observable ?Ho avuto un nome di metodo in conflitto con la classe con cui avevo bisogno di integrarmi, e semplicemente chiamando un metodo di istanza (trigger_callbacks) funzionava bene. –

Problemi correlati