75 votes

Ruby : Proc#call vs yield

Quelles sont les différences de comportement entre les deux implémentations suivantes en Ruby de la fonction thrice méthode ?

module WithYield
  def self.thrice
    3.times { yield }      # yield to the implicit block argument
  end
end

module WithProcCall
  def self.thrice(&block)  # & converts implicit block to an explicit, named Proc
    3.times { block.call } # invoke Proc#call
  end
end

WithYield::thrice { puts "Hello world" }
WithProcCall::thrice { puts "Hello world" }

Par "différences de comportement", j'entends la gestion des erreurs, les performances, la prise en charge des outils, etc.

51voto

jpastuszek Points 522

Je pense que le premier est en fait un sucre syntaxique de l'autre. En d'autres termes, il n'y a pas de différence de comportement.

Ce que la deuxième forme permet cependant, c'est de "sauvegarder" le bloc dans une variable. Le bloc peut alors être appelé à un autre moment - callback.


Ok. Cette fois-ci, j'ai fait une évaluation rapide :

require 'benchmark'

class A
  def test
    10.times do
      yield
    end
  end
end

class B
  def test(&block)
    10.times do
      block.call
    end
  end
end

Benchmark.bm do |b|
  b.report do
    a = A.new
    10000.times do
      a.test{ 1 + 1 }
    end
  end

  b.report do
    a = B.new
    10000.times do
      a.test{ 1 + 1 }
    end
  end

  b.report do
    a = A.new
    100000.times do
      a.test{ 1 + 1 }
    end
  end

  b.report do
    a = B.new
    100000.times do
      a.test{ 1 + 1 }
    end
  end

end

Les résultats sont intéressants :

      user     system      total        real
  0.090000   0.040000   0.130000 (  0.141529)
  0.180000   0.060000   0.240000 (  0.234289)
  0.950000   0.370000   1.320000 (  1.359902)
  1.810000   0.570000   2.380000 (  2.430991)

Cela montre qu'en utilisant bloc.appel est presque 2x plus lent que l'utilisation de rendement .

24voto

user83510 Points 3885

La différence de comportement entre les différents types de fermetures rubis a été largement documenté

9voto

naomik Points 10423

Voici une mise à jour pour Ruby 2.x

ruby 2.0.0p247 (2013-06-27 révision 41674) [x86_64-darwin12.3.0]

J'en ai eu marre d'écrire des benchmarks manuellement, alors j'ai créé un petit module runner appelé bancable

require 'benchable' # https://gist.github.com/naomik/6012505

class YieldCallProc
  include Benchable

  def initialize
    @count = 10000000    
  end

  def bench_yield
    @count.times { yield }
  end

  def bench_call &block
    @count.times { block.call }
  end

  def bench_proc &block
    @count.times &block
  end

end

YieldCallProc.new.benchmark

Sortie

                      user     system      total        real
bench_yield       0.930000   0.000000   0.930000 (  0.928682)
bench_call        1.650000   0.000000   1.650000 (  1.652934)
bench_proc        0.570000   0.010000   0.580000 (  0.578605)

Je pense que la chose la plus surprenante ici est que bench_yield est plus lent que bench_proc . J'aimerais comprendre un peu mieux pourquoi cela se produit.

6voto

Sam Stokes Points 7118

Ils donnent des messages d'erreur différents si vous oubliez de passer un bloc :

> WithYield::thrice
LocalJumpError: no block given
        from (irb):3:in `thrice'
        from (irb):3:in `times'
        from (irb):3:in `thrice'

> WithProcCall::thrice
NoMethodError: undefined method `call' for nil:NilClass
        from (irb):9:in `thrice'
        from (irb):9:in `times'
        from (irb):9:in `thrice'

Mais ils se comportent de la même manière si vous essayez de passer un argument "normal" (non-bloqué) :

> WithYield::thrice(42)
ArgumentError: wrong number of arguments (1 for 0)
        from (irb):19:in `thrice'

> WithProcCall::thrice(42)
ArgumentError: wrong number of arguments (1 for 0)
        from (irb):20:in `thrice'

6voto

cbrauchli Points 1076

Les autres réponses sont assez complètes et Les fermetures en Ruby couvre largement les différences fonctionnelles. J'étais curieux de savoir quelle méthode serait la plus performante pour les méthodes qui éventuellement accepter un bloc, donc j'ai écrit quelques benchmarks (en partant de ce billet de Paul Mucur ). J'ai comparé trois méthodes :

  • &block dans la signature de la méthode
  • Utilisation de &Proc.new
  • Emballage yield dans un autre bloc

Voici le code :

require "benchmark"

def always_yield
  yield
end

def sometimes_block(flag, &block)
  if flag && block
    always_yield &block
  end
end

def sometimes_proc_new(flag)
  if flag && block_given?
    always_yield &Proc.new
  end
end

def sometimes_yield(flag)
  if flag && block_given?
    always_yield { yield }
  end
end

a = b = c = 0
n = 1_000_000
Benchmark.bmbm do |x|
  x.report("no &block") do
    n.times do
      sometimes_block(false) { "won't get used" }
    end
  end
  x.report("no Proc.new") do
    n.times do
      sometimes_proc_new(false) { "won't get used" }
    end
  end
  x.report("no yield") do
    n.times do
      sometimes_yield(false) { "won't get used" }
    end
  end

  x.report("&block") do
    n.times do
      sometimes_block(true) { a += 1 }
    end
  end
  x.report("Proc.new") do
    n.times do
      sometimes_proc_new(true) { b += 1 }
    end
  end
  x.report("yield") do
    n.times do
      sometimes_yield(true) { c += 1 }
    end
  end
end

Les performances étaient similaires entre Ruby 2.0.0p247 et 1.9.3p392. Voici les résultats pour la version 1.9.3 :

                  user     system      total        real
no &block     0.580000   0.030000   0.610000 (  0.609523)
no Proc.new   0.080000   0.000000   0.080000 (  0.076817)
no yield      0.070000   0.000000   0.070000 (  0.077191)
&block        0.660000   0.030000   0.690000 (  0.689446)
Proc.new      0.820000   0.030000   0.850000 (  0.849887)
yield         0.250000   0.000000   0.250000 (  0.249116)

L'ajout d'un &block param lorsqu'il n'est pas toujours utilisé ralentit vraiment la méthode. Si le bloc est optionnel, ne l'ajoutez pas à la signature de la méthode. Et, pour faire circuler les blocs, il faut envelopper yield dans un autre bloc est le plus rapide.

Cela dit, il s'agit des résultats d'un million d'itérations, alors ne vous en faites pas trop. Si une méthode rend votre code plus clair au détriment d'un millionième de seconde, utilisez-la quand même.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X