Ruby: proc # call vs yield

Quali sono le differenze comportamentali tra le seguenti due implementazioni in Ruby del metodo thrice ?

 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" } 

Per “differenze comportamentali” includo la gestione degli errori, le prestazioni, il supporto degli strumenti, ecc.

Penso che il primo sia in realtà uno zucchero sintattico dell’altro. In altre parole non c’è differenza comportamentale.

Ciò che il secondo modulo consente è però di “salvare” il blocco in una variabile. Quindi il blocco può essere chiamato in un altro momento – callback.


Ok. Questa volta sono andato e ho fatto un rapido punto di riferimento:

 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 

I risultati sono interessanti:

  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) 

Questo dimostra che l’uso di block.call è quasi 2x più lento rispetto all’utilizzo di yield .

La differenza comportamentale tra diversi tipi di chiusure di rubini è stata ampiamente documentata

Ecco un aggiornamento per Ruby 2.x.

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

Mi sono stancato di scrivere i benchmark manualmente, quindi ho creato un piccolo modulo runner chiamato benchable

 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 

Produzione

  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) 

Penso che la cosa più sorprendente qui è che bench_yield è più lento di bench_proc . Vorrei avere un po ‘più di comprensione del perché questo sta accadendo.

Forniscono diversi messaggi di errore se si dimentica di passare un blocco:

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

Ma si comportano allo stesso modo se si tenta di passare un argomento “normale” (non di blocco):

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

Le altre risposte sono abbastanza approfondite e Closures in Ruby copre ampiamente le differenze funzionali. Ero curioso di sapere quale metodo avrebbe funzionato al meglio per i metodi che potevano accettare un blocco, così ho scritto alcuni benchmark (uscendo da questo post di Paul Mucur ). Ho confrontato tre metodi:

  • & blocca nella firma del metodo
  • Usando &Proc.new
  • yield avvolgimento in un altro blocco

Ecco il codice:

 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 

Le prestazioni erano simili tra Ruby 2.0.0p247 e 1.9.3p392. Ecco i risultati per 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) 

Aggiungere un parametro esplicito &block quando non viene sempre utilizzato rallenta il metodo. Se il blocco è facoltativo, non aggiungerlo alla firma del metodo. E, per passare i blocchi in giro, avvolgere la yield in un altro blocco è più veloce.

Detto questo, questi sono i risultati per un milione di iterazioni, quindi non preoccuparti troppo. Se un metodo rende il tuo codice più chiaro a scapito di un milionesimo di secondo, usalo comunque.

Ho scoperto che i risultati sono diversi a seconda che tu costringa Ruby a build il blocco o meno (ad es. Un proc preesistente).

 require 'benchmark/ips' puts "Ruby #{RUBY_VERSION} at #{Time.now}" puts firstname = 'soundarapandian' middlename = 'rathinasamy' lastname = 'arumugam' def do_call(&block) block.call end def do_yield(&block) yield end def do_yield_without_block yield end existing_block = proc{} Benchmark.ips do |x| x.report("block.call") do |i| buffer = String.new while (i -= 1) > 0 do_call(&existing_block) end end x.report("yield with block") do |i| buffer = String.new while (i -= 1) > 0 do_yield(&existing_block) end end x.report("yield") do |i| buffer = String.new while (i -= 1) > 0 do_yield_without_block(&existing_block) end end x.compare! end 

Dà i risultati:

 Ruby 2.3.1 at 2016-11-15 23:55:38 +1300 Warming up -------------------------------------- block.call 266.502ki/100ms yield with block 269.487ki/100ms yield 262.597ki/100ms Calculating ------------------------------------- block.call 8.271M (± 5.4%) i/s - 41.308M in 5.009898s yield with block 11.754M (± 4.8%) i/s - 58.748M in 5.011017s yield 16.206M (± 5.6%) i/s - 80.880M in 5.008679s Comparison: yield: 16206091.2 i/s yield with block: 11753521.0 i/s - 1.38x slower block.call: 8271283.9 i/s - 1.96x slower 

Se cambi do_call(&existing_block) a do_call{} scoprirai che in entrambi i casi è circa 5 volte più lento. Penso che la ragione di ciò dovrebbe essere ovvia (perché Ruby è costretto a build un Proc per ogni invocazione).

A proposito, solo per aggiornare questo al giorno corrente utilizzando:

 ruby 1.9.2p180 (2011-02-18 revision 30909) [x86_64-linux] 

Su Intel i7 (1,5 anni oldish).

 user system total real 0.010000 0.000000 0.010000 ( 0.015555) 0.030000 0.000000 0.030000 ( 0.024416) 0.120000 0.000000 0.120000 ( 0.121450) 0.240000 0.000000 0.240000 ( 0.239760) 

Ancora 2 volte più lento. Interessante.