Update: The ability to call Proc.new
without a block was removed in Ruby 3.0.0 so the technique described below will only work on versions of Ruby prior to that.
There are two main ways to receive blocks in a method in Ruby:
the first is to use the yield
keyword like so:
def speak
puts yield
end
speak { "Hello" }
# Hello
# => nil
The other is to prefix the last argument in a method signature with an
ampersand which will then create a Proc
object from any block passed
in. This object can then be executed with the call
method like so:
def speak(&block)
puts block.call
end
speak { "Hello" }
# Hello
# => nil
The problem with the second approach is that instantiating a new Proc
object incurs
a surprisingly heavy performance penalty as detailed by Aaron Patterson in his
excellent RubyConf X presentation, “ZOMG WHY IS THIS CODE SO SLOW?”
(beginning around the 30 minute mark or from slide 181).
This can easily be verified with the following benchmark, block_benchmark.rb
:
require "benchmark"
def speak_with_block(&block)
block.call
end
def speak_with_yield
yield
end
n = 1_000_000
Benchmark.bmbm do |x|
x.report("&block") do
n.times { speak_with_block { "ook" } }
end
x.report("yield") do
n.times { speak_with_yield { "ook" } }
end
end
The results of this on my own machine are as follows (the numbers themselves aren’t as important as their difference):
$ ruby block_benchmark.rb
Rehearsal ------------------------------------------
&block 1.410000 0.020000 1.430000 ( 1.430050)
yield 0.290000 0.000000 0.290000 ( 0.291750)
--------------------------------- total: 1.720000sec
user system total real
&block 1.420000 0.030000 1.450000 ( 1.452686)
yield 0.290000 0.000000 0.290000 ( 0.292179)
So it is clearly preferable to choose yield
over &block
but what if you need to
pass a block to another method?
For example, here is a class that implements a method tell_ape
which delegates to
another, more generic method named tell
. This sort of pattern is commonly done
using method_missing
but I’ll keep the methods explicit for
simplicity:
class Monkey
# Monkey.tell_ape { "ook!" }
# ape: ook!
# => nil
def self.tell_ape(&block)
tell("ape", &block)
end
def self.tell(name, &block)
puts "#{name}: #{block.call}"
end
end
Such a thing is not possible with the yield
keyword:
class Monkey
# Monkey.tell_ape { "ook!" }
# ArgumentError: wrong number of arguments (2 for 1)
def self.tell_ape
tell("ape", yield)
end
def self.tell(name)
puts "#{name}: #{yield}"
end
end
Neither does it work by using an ampersand:
class Monkey
# Monkey.tell_ape { "ook!" }
# TypeError: wrong argument type String (expected Proc)
def self.tell_ape
tell("ape", &yield)
end
def self.tell(name)
puts "#{name}: #{yield}"
end
end
However, there is a way to only create a Proc
object when needed and that is
to use the little known behaviour of Proc.new
as explained in
Aaron Patterson’s aforementioned presentation.
If Proc.new
is called from inside a method without any arguments of its own,
it will return a new Proc
containing the block given to its surrounding method.
def speak
puts Proc.new.call
end
speak { "Hello" }
# Hello
# => nil
This means that it is now possible to pass a block between methods without using the
&block
parameter:
class Monkey
# Monkey.tell_ape { "ook!" }
# ape: ook!
# => nil
def self.tell_ape
tell("ape", &Proc.new)
end
def self.tell(name)
puts "#{name}: #{yield}"
end
end
Of course, if you do use Proc.new
then you lose the performance benefit of using
only yield
(as Proc
objects are being created as with &block
) but it does
avoid unnecessary creation of Proc objects when you don’t need them. This can be
demonstrated with the following benchmark, proc_new_benchmark.rb
:
require "benchmark"
def sometimes_block(flag, &block)
if flag && block
block.call
end
end
def sometimes_proc_new(flag)
if flag && block_given?
Proc.new.call
end
end
n = 1_000_000
Benchmark.bmbm do |x|
x.report("&block") do
n.times do
sometimes_block(false) { "won't get used" }
end
end
x.report("Proc.new") do
n.times do
sometimes_proc_new(false) { "won't get used" }
end
end
end
Which makes the following rather significant difference:
$ ruby code/proc_new_benchmark.rb
Rehearsal --------------------------------------------
&block 1.080000 0.160000 1.240000 ( 1.237644)
Proc.new 0.160000 0.000000 0.160000 ( 0.156077)
----------------------------------- total: 1.400000sec
user system total real
&block 1.090000 0.080000 1.170000 ( 1.178771)
Proc.new 0.160000 0.000000 0.160000 ( 0.155053)
The key here is that using &block
will always create a new Proc
object,
even if we don’t make use of it. By using Proc.new
only when we actually
need it, we can avoid the cost of this object instantiation entirely.
That said, there is a potential trade-off here between performance and
readability: it is clear from the sometimes_block
method signature that it
takes a block and therefore will presumably do something with it; the same cannot
be said for the more efficient sometimes_proc_new
.
In the end, it comes down to your specific requirements but it is still a useful language feature to know.