web-dev-qa-db-fra.com

Comment implémenter un "callback" en Ruby?

Je ne suis pas sûr du meilleur idiome pour les rappels de style C en Ruby - ou s’il ya encore mieux (et moins comme C). En C, je ferais quelque chose comme:

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

Quel est un bon équivalent Ruby? Je veux essentiellement appeler une méthode passée en classe, lorsqu'une certaine condition est remplie dans "DoStuff"

69
Justicle

L'équivalent Ruby, qui n'est pas idiomatique, serait:

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'approche idiomatique serait de passer un bloc au lieu d'une référence à une méthode. L'un des avantages d'un bloc par rapport à une méthode autonome est le contexte - un bloc est une fermeture , de sorte qu'il peut faire référence à des variables du champ dans lequel il a été déclaré. Cela réduit le nombre de paramètres que do_stuff doit transmettre au rappel. Par exemple:

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
83
dstnbrkr

Ce "bloc idiomatique" est une partie essentielle du quotidien Ruby et est fréquemment traité dans des livres et des tutoriels. La section Ruby information information fournit des liens vers des ressources d’apprentissage utiles [en ligne].


La manière idiomatique est d'utiliser un bloc:

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

Ou peut-être converti en Proc ; je montre ici que le "bloc", converti implicitement en Proc avec &block, est juste une autre valeur "appelable":

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

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

(Utilisez uniquement le formulaire ci-dessus pour enregistrer le bloc block-now-Proc en vue d'une utilisation ultérieure ou dans d'autres cas particuliers, car il ajoute une surcharge et un bruit de syntaxe.)

Cependant, un lambda peut être utilisé aussi facilement (mais ce n’est pas idiomatique):

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

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

Alors que les approches ci-dessus peuvent toutes wrap "appeler une méthode" car elles créent des fermetures, bound Methods peut également être traité comme des objets appelables de première classe:

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

De plus, il est parfois utile d’utiliser la méthode #send (en particulier si une méthode est connue par son nom). Ici, il enregistre un objet Méthode intermédiaire créé dans le dernier exemple. Ruby est un système de transmission de messages:

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

Bonne codage!

76
user166390

Exploré le sujet un peu plus et mis à jour le code.

La version suivante tente de généraliser la technique, tout en restant extrêmement simplifiée et incomplète.

J'ai surtout volé, trouvé l'inspiration dans la mise en œuvre des rappels de DataMapper, ce qui me semble assez complet et magnifique. 

Je suggère fortement de consulter le code @ http://github.com/datamapper/dm-core/blob/master/lib/dm-core/support/hook.rb

Quoi qu’il en soit, essayer de reproduire la fonctionnalité à l’aide du module Observable s’est avéré très intéressant et instructif ..___ Quelques remarques:

  • la méthode ajoutée semble être obligatoire, car les méthodes de l'instance d'origine ne sont pas disponibles au moment de l'enregistrement des rappels.
  • la classe incluse est faite à la fois observée et auto-observatrice
  • l'exemple est limité aux méthodes d'instance, ne prend pas en charge les blocs, les arguments, etc.

code:

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...

Cette présentation simple de l'utilisation de Observable pourrait être utile: http://www.oreillynet.com/Ruby/blog/2006/01/Ruby_design_patterns_observer.html

6
LucaB

Donc, cela peut être très "un-Ruby", et je ne suis pas un développeur Ruby "professionnel", alors si vous allez être fous, soyez gentils s'il vous plait :)

Ruby a un module intégré appelé Observer. Je ne l'ai pas trouvé facile à utiliser, mais pour être juste, je ne lui ai pas donné beaucoup de chance. Dans mes projets, j'ai eu recours à la création de mon propre type EventHandler (oui, j'utilise beaucoup le C #). Voici la structure de 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

Donc, pour utiliser cela, je l'expose en tant que membre en lecture seule d'une classe:

class Foo

  attr_reader :some_value_changed

  def initialize
    @some_value_changed = EventHandler.new
  end

end

Les clients de la classe "Foo" peuvent s'abonner à un événement comme celui-ci:

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

Je suis sûr que ce n'est pas idiomatique, Ruby, et je ne fais que transformer mon expérience en C # dans une nouvelle langue, mais cela a fonctionné pour moi.

3
Ed S.

Je sais que ceci est un ancien post, mais les autres qui le trouvent peuvent trouver ma solution utile.

http://chrisshepherddev.blogspot.com/2015/02/callbacks-in-pure-Ruby-prepend-over.html

0
Chris Shepherd

J'implémente souvent des rappels en Ruby, comme dans l'exemple suivant. C'est très confortable à utiliser.

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
0
henning

Si vous souhaitez utiliser ActiveSupport (de Rails), vous avez une implémentation simple.

class ObjectWithCallbackHooks
  include ActiveSupport::Callbacks
  define_callbacks :initialize # Your object supprots an :initialize callback chain

  include ObjectWithCallbackHooks::Plugin 

  def initialize(*)
    run_callbacks(:initialize) do # run `before` callbacks for :initialize
      puts "- initializing" # then run the content of the block
    end # then after_callbacks are ran
  end
end

module ObjectWithCallbackHooks::Plugin
  include ActiveSupport::Concern

  included do 
    # This plugin injects an "after_initialize" callback 
    set_callback :initialize, :after, :initialize_some_plugin
  end
end
0
Cyril Duchon-Doris