What we’re going to do and why
If you’re a Ruby programmer, you almost certainly use Proc
objects all the time, although you might not always be consciously aware of it. Blocks, which are ubiquitous in Ruby, and lambdas, which are used for things like Rails scopes, both involve Proc
objects.
In this post we’re going to take a close look at Proc
objects. First we’ll do a Proc
object “hello world” to see what we’re dealing with. Then we’ll unpack the definition of Proc
objects that the official Ruby docs give us. Lastly we’ll see how Proc
objects relate to other concepts like blocks and lambdas.
A Proc object “hello world”
Before we talk about what Proc
objects are and how they’re used, let’s take a look at a Proc
object and mess around with it a little bit, just to see what one looks like.
The official Ruby docs provide a pretty good Proc
object “hello world” example:
square = Proc.new { |x| x**2 }
We can see how this Proc
object works by opening up an irb
console and defining the Proc
object there.
> square = Proc.new { |x| x**2 }
=> #<Proc:0x00000001333a8660 (irb):1>
> square.call(3)
=> 9
> square.call(4)
=> 16
> square.call(5)
=> 25
We can kind of intuitively understand how this works. A Proc
object behaves somewhat like a method: you define some behavior and then you can use that behavior repeatedly wherever you want.
Now that we have a loose intuitive understanding, let’s get a firmer grasp on what Proc
objects are all about.
Understanding Proc objects more deeply
The official Ruby docs’ definition of Proc objects
According to the official Ruby docs on Procs objects, “a Proc object is an encapsulation of a block of code, which can be stored in a local variable, passed to a method or another Proc, and can be called.”
This definition is a bit of a mouthful. When I encounter wordy definitions like this, I like to separate them into chunks to make them easier to understand.
The Ruby Proc object definition, broken into chunks
A Proc
object is:
- an encapsulation of a block of code
- which can be stored in a local variable
- or passed to a method or another Proc
- and can be called
Let’s take these things one-by-one.
A Proc object is an encapsulation of a block of code
What could it mean for something to be an encapsulation of a block of code? In general, when you “encapsulate” something, you metaphorically put it in a capsule. Things that are in capsules are isolated from whatever’s on the outside of the capsule. Encapsulating something also implies that it’s “packaged up”.
So when the docs say that a Proc
object is “an encapsulation of a block of code”, they must mean that the code in a Proc
object is packaged up and isolated from the code outside it.
A Proc object can be stored in a local variable
For this one let’s look at an example, straight from the docs:
square = Proc.new { |x| x**2 }
As we can see, this piece of code creates a Proc
object and stores it in a local variable called square
. So this part of the definition, that a Proc
object can be stored in a local variable, seems easy enough to understand.
A Proc object can be passed to a method or another Proc
This one’s a two-parter so let’s take each part individually. First let’s focus on “A Proc
object can be passed to another method”.
Here’s a method which can accept a Proc
object. The method is followed by the definition of two Proc
objects: square
, which squares whatever number you give it, and double
, which doubles whatever number you give it.
def perform_operation_on(number, operation)
operation.call(number)
end
square = Proc.new { |x| x**2 }
double = Proc.new { |x| x * 2 }
puts perform_operation_on(5, square)
puts perform_operation_on(5, double)
If you were to run this code you would get the following output:
25
10
So that’s what it means to pass a Proc
object into a method. Instead of passing data as a method argument like normal, you can pass behavior. Or, to put it another way, you can pass an encapsulation of a block of code. It’s then up to that method to execute that encapsulated block of code whenever and however it sees fit.
If we want to pass a Proc
object into another Proc
object, the code looks pretty similar to our other example above.
perform_operation_on = Proc.new do |number, operation|
operation.call(number)
end
square = Proc.new { |x| x**2 }
double = Proc.new { |x| x * 2 }
puts perform_operation_on.call(5, square)
puts perform_operation_on.call(5, double)
The only difference between this example and the one above it is that, in this example, perform_operation_on
is defined as a Proc
object rather than a method. The ultimate behavior is exactly the same though.
A Proc object can be called
This last part of the definition of a Proc
object, “a Proc
object can be called”, is perhaps obvious at this point but let’s address it anyway for completeness’ sake.
A Proc
object can be called using the #call
method. Here’s an example.
square = Proc.new { |x| x**2 }
puts square.call(3)
There are other ways to call a Proc
object but they’re not important for understanding Proc
objects conceptually.
Closures
In order to fully understand Proc
objects, we need to understand something called closures. The concept of a closure is a broader concept that’s not unique to Ruby.
Closures are too nuanced a concept to be included in the scope of this article, unfortunately. If you’d like to understand closures, I’d suggest checking out my other post, Understanding Ruby closures.
But the TL;DR version is that a closure is a record which stores a function plus (potentially) some variables.
Proc objects and blocks
Every block in Ruby is a Proc
object, loosely speaking. Here’s a custom method that accepts a block as an argument.
def my_method(&block)
puts block.class
end
my_method { "hello" }
If you were to run the above code, the output would be:
Proc
That’s because the block we passed when calling my_method
is a Proc
object.
Below is an example that’s functionally equivalent to the above. The &
in front of my_proc
converts the Proc
object into a block.
def my_method(&block)
puts block.class
end
my_proc = Proc.new { "hello" }
my_method &my_proc
By the way, if you’re curious about the &
at the beginning of &block
and &my_proc
, I have a whole post about that here.
Proc objects and lambdas
Lambdas are also Proc
objects. This can be proven by running the following in an irb
console:
> my_lambda = lambda { |x| x**2 }
=> #<Proc:0x00000001241e82a8 (irb):1 (lambda)>
> my_lambda.class
=> Proc
The difference between lambdas and Proc
objects is that the two have certain subtle differences in behavior. For example, in lambdas, return
means “exit from this lambda”. In regular Proc
objects, return
means “exit from embracing method”. I won’t go into detail on the differences between lambdas and Proc
objects because it’s outside the scope of what I’m trying to convey in this post. A have a different post that describes the differences between procs and lambdas.
Takeaways
- A
Proc
object is an encapsulation of a block of code, which can be stored in a local variable, passed to a method or another Proc, and can be called. - A closure is a record which stores a function plus some variables.
Proc
objects are closures. - Blocks are
Proc
objects. - Lambdas are
Proc
objects too, although a special kind ofProc
object with subtly different behavior.
Best explanation I’ve seen about procs, lambdas and closures. Thank you!
Thanks!
Hey man, thanks for article.
I’m begins in Ruby and learned much with you.
Good Week.
Glad it helped!
Though there is Official documentation, still looking forward to your article (if any) about “Proc objects vs. lambdas”
It’s coming!
Super cristal clear, thanks!