tags:

views:

908

answers:

3

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.

+3  A: 

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.

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
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
I did mine on MRI 1.8.7p174 x86_64-linux.
you're also missing a third case : `def test( 10.times( end`, which should test out the same as the yield case.
rampion
+1  A: 

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
+2  A: 

The behavioral difference between different types of ruby closures has been extensively documented

That's a good link - will have to read it in detail later. Thanks!
Sam Stokes