84

What are the behavioural differences between the following two implementations in Ruby of the thrice method?

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

By "behavioural differences" I include error handling, performance, tool support, etc.

Mohsen Nosratinia
  • 9,844
  • 1
  • 27
  • 52
Sam Stokes
  • 14,617
  • 7
  • 36
  • 33
  • 10
    Side note: `def thrice(&block)` is more self-documenting, particularly vs a `yield` buried somewhere in a large method. – Nathan Long Oct 22 '13 at 13:45
  • 1
    The behavioral difference between different types of ruby closures [has been extensively documented](http://innig.net/software/ruby/closures-in-ruby.rb) – cldwalker Sep 11 '09 at 21:59
  • A note on the link provided by cldwalker...It is wrong. You can pass multiple blocks (i.e. closures) to a method. (Also hard to take seriously someone who who refers to "compiling" Ruby.) Do you get the same convenient syntactic sugar for passing multiple blocks? No. Can you do it? Yes, easily. – Huliax Mar 30 '21 at 02:12
  • @cldwalker, yes, it was a good document, but the diff between yield and Proc.call was not clearly articulated in the doc. – jiggysoo Jul 05 '21 at 13:39

5 Answers5

54

I think the first one is actually a syntactic sugar of the other. In other words there is no behavioural difference.

What the second form allows though is to "save" the block in a variable. Then the block can be called at some other point in time - callback.


Ok. This time I went and did a quick benchmark:

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

The results are interesting:

      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)

This shows that using block.call is almost 2x slower than using yield.

jpastuszek
  • 799
  • 5
  • 8
  • 9
    I think Ruby would be more consistent if that were true (i.e. if `yield` were just syntactic sugar for `Proc#call`) but I don't think it's true. e.g. there's the different error handling behaviour (see my answer below). I've also seen it suggested (e.g. http://stackoverflow.com/questions/764134/rubys-yield-feature-in-relation-to-computer-science/765126#765126) that `yield` is more efficient, because it doesn't have to first create a `Proc` object and then invoke its `call` method. – Sam Stokes Sep 11 '09 at 10:37
  • Re update with benchmarks: yeah, I did some benchmarks too and got `Proc#call` being _more_ than 2x as slow as `yield`, on MRI 1.8.6p114. On JRuby (1.3.0, JVM 1.6.0_16 Server VM) the difference was even more striking: `Proc#call` was about *8x* as slow as `yield`. That said, `yield` on JRuby was twice as fast as `yield` on MRI. – Sam Stokes Sep 11 '09 at 13:36
  • I did mine on MRI 1.8.7p174 x86_64-linux. – jpastuszek Sep 11 '09 at 15:24
  • 3
    you're also missing a third case : `def test(&block) ; 10.times(&block) ; end`, which should test out the same as the yield case. – rampion Sep 12 '09 at 17:11
  • 1
    Benchmarks above approximate Ruby v2.1.2 also; `block.call` is ~1.7x slower than `yield`. – Gav May 10 '14 at 16:01
  • @jpastuszek You said: `What the second form allows though is to "save" the block in a variable.` I tried this: `variable_name = yield block_name` which also saved the result of the yielded block to a variable. same like: `variable_name = block_name.call` does. – Khalil Gharbaoui Jun 13 '20 at 20:00
8

The other answers are pretty thorough and Closures in Ruby extensively covers the functional differences. I was curious about which method would perform best for methods that optionally accept a block, so I wrote some benchmarks (going off this Paul Mucur post). I compared three methods:

  • &block in method signature
  • Using &Proc.new
  • Wrapping yield in another block

Here is the 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

Performance was similar between Ruby 2.0.0p247 and 1.9.3p392. Here are the results for 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)

Adding an explicit &block param when it's not always used really does slow down the method. If the block is optional, do not add it to the method signature. And, for passing blocks around, wrapping yield in another block is fastest.

That said, these are the results for a million iterations, so don't worry about it too much. If one method makes your code clearer at the expense of a millionth of a second, use it anyway.

cbrauchli
  • 1,555
  • 15
  • 25
7

They give different error messages if you forget to pass a block:

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

But they behave the same if you try to pass a "normal" (non-block) argument:

> 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'
Sam Stokes
  • 14,617
  • 7
  • 36
  • 33
2

I found that the results are different depending on whether you force Ruby to construct the block or not (e.g. a pre-existing proc).

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

Gives the results:

Ruby 2.3.1 at 2016-11-15 23:55:38 +1300

Warming up --------------------------------------
          block.call   266.502k i/100ms
    yield with block   269.487k i/100ms
               yield   262.597k i/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

If you change do_call(&existing_block) to do_call{} you'll find it's about 5x slower in both cases. I think the reason for this should be obvious (because Ruby is forced to construct a Proc for each invocation).

ioquatix
  • 1,411
  • 17
  • 32
0

BTW, just to update this to current day using:

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

On Intel i7 (1.5 years 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)

Still 2x slower. Interesting.

Travis Reeder
  • 38,611
  • 12
  • 87
  • 87