Proc.new Trick

Ruby’s Proc.new without an explicit block

Andrew Livingston
3 min readFeb 23, 2017

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
end
random_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
end
random_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
end
random_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
end
random_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
end
def 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
end
def doesnt_use_ampersand(trigger)
if trigger && block_given?
a = Proc.new
a.call
end
end
n = 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.

--

--