Proc.new Trick
Ruby’s Proc.new without an explicit block
All Ruby developers know code like this, where a method takes a block, and the unary operator &
converts the block
into a Proc
that can be called with #call
:
def random_method(&block)
block.call
endrandom_method {"Hello"}# => Hello
Similarly, all Ruby developers know code like this, in which a method can yield
to any block that is passed to it:
def random_method_two
yield
endrandom_method_two {"Hello"}# => Hello
However, there’s another way to pass and call blocks in methods:
def random_method_three
prok = Proc.new
prok.call
endrandom_method_three {"Hello From Three"}
This method looks like it would raise an error because Proc.new
must take a block, and when it doesn’t, Ruby raises this error:
Proc.new
# => ArgumentError: tried to create Proc object without a block
But if we run the following code, even though we haven’t explicitly passed a block to Proc.new
, Ruby raises no error:
def random_method_three
prok = Proc.new
prok.call
endrandom_method_three {"Hello From Three"}# => Hello From Three
What’s happening here? Proc.new
will look for a block in its context, find the block we passed into random_method_three
, and run Proc.new
with that block.
This is useful when you would like to create and use a Proc
object inside of a method but would like ‘choose’ when to use the Proc
. This avoids the performance overhead of using the &block
construction, which automatically converts any block passed to a method into a Proc
object regardless of whether you want the Proc
to be called in the method.
For example, take these two methods:
def uses_ampersand(trigger, &block)
if trigger && block_given?
block.call
end
enddef doesnt_use_ampersand(trigger)
if trigger && block_given?
a = Proc.new
a.call
end
end
The method uses_ampersand(trigger, &block)
will convert any block passed to it into a Proc
object regardless of whether trigger
is truthy or falsy, while doesnt_use_ampersand(trigger)
will only create a Proc
object if trigger
is truthy. This difference in behavior results in a non-negligible performance difference. We can see this by using Ruby’s benchmark
library.
The set up:
require 'benchmark'def uses_ampersand(trigger, &block)
if trigger && block_given?
block.call
end
enddef doesnt_use_ampersand(trigger)
if trigger && block_given?
a = Proc.new
a.call
end
endn = 4000000Benchmark.bmbm do |test_case|
test_case.report("Uses &block") do
n.times do
uses_ampersand(false) do
"hello from block (won't get called)"
end
end
end
test_case.report("Uses Proc.new") do
n.times do
doesnt_use_ampersand(false) do
"hello from block (won't get called)"
end
end
end
end
When run, we see the performance difference:
$ ruby proc_comparison.rbRehearsal -------------------------------------------------
Uses &block 4.680000 0.200000 4.880000 ( 5.014182)
Uses Proc.new 0.570000 0.000000 0.570000 ( 0.583753)
---------------------------------------- total: 5.450000sec user system total real
Uses &block 4.500000 0.160000 4.660000 ( 4.721222)
Uses Proc.new 0.590000 0.010000 0.600000 ( 0.669252)
So, we see the method that explicitly takes a block and uses the &
operator to convert that block — every time — into a Proc
is ~10x slower than the method that selectively uses the block only when trigger
is truthy. This can be explained by the fact that uses_ampersand(trigger, &block)
will always create a Proc
from the block that is passed into it, while doesnt_use_ampersand(trigger)
will only sometimes — when we want it to, by passing in a truthy trigger
— create a Proc
.
I hope you found this helpful.