Ruby refresher: proc - lambda - block
This is one of those posts that might make you think “Again?” when you read a title of it. Still, every now and then the question pops up — “What’s the difference between proc
and lambda
?” and then I can’t really remember details. That’s why I’m writing this post — so that I can remember the answer at last. Secondly, chances are it will serve as a refresher for all those out there who can’t remember it either!
proc vs lambda
Both are closures.
Both respond to the call
method.
Both are instances of the Proc
class.
You can use them as anonymous functions, yet…
Scope
Let’s start with lambda
.
The outcome is pretty predictable. We call the lambda
, the "Hey there!"
is assigned to result and our console prints $ Let’s print this! Result: Hey there!
.
What about proc
?
Wait a second! The console prints $ Hey there!
right away!
So what happened?
Contrary to lambdas
, procs
don’t have their own scope to return from. That’s why they’re executed as if they’re code was cut out of their bodies and pasted in the place where they were called. In our example, proc
’s return is executed as if it was return_from_proc
’s own return. As a result, the execution never gets to the Let's print this! (...)
line!
Argument checking
Take a look at the example below. Both lambdas
and procs
accept two arguments (a
and b
) and return an array of them.
Just like before, the results are quite predictable.
$ (1, 2): [1, 2]
$ medium.rb:1:in `block in <main>': wrong number of arguments (given 3, expected 2) (ArgumentError)
from medium.rb:3:in `<main>'
Obviously, If I comment out the second call, the program will throw a similar exception at the third call. Just like a method.
It’s not the case for procs
, though.
Let’s see what our console tells us now…
$ (1, 2): [1, 2]
$ (1, 2, 3): [1, 2]
$ (1): [1, nil]
Whoa! procs
don’t check the number of arguments.
You passed too many? Cool, it will take only the number it needs. Too few? No problem, it will use nils
.
Alternative syntaxes
These are some alternative syntaxes you can come across
Proc.new { |name| "I'm, #{name}!" }
equalsproc { |name| "I'm, #{name}!" }
lambda { |name| "hey, #{name}!" }
equals->(name) { "hey, #{name}!" }
Verdict
In my humble opinion, the difference in scopes is the most important reason to use lambdas
over procs
. If you ever find yourself thinking of using this trick proc
offers, please don’t. I’m also not a fan of the trick with argument checking. The very fact they’re called tricks in the official docs actually speaks for itself.
Blocks
Basics
Intuitively, block
is nothing more than a block of code. What’s interesting though, is the way it works with Ruby’s closures (methods, procs
, lambdas
).
To illustrate this, let’s write a function we could use in a similar way to the map
function.
What you can see in lines 3 and 8–10 are blocks
— lines of code wrapped in do
end
or curly braces. You can pass a block
to any closure although the closure won’t always use the block
. As you can see, Enumerable#map
(line 3) and our MyMap#call
closures do use blocks
.
Let’s focus on MyMap#call
then. The block
’s argument is element
and it returns an increment of it. How do we pass an argument to the block
though? Line 3 — yield
keyword does exactly that. It takes an item and passes it to the block
as element
, then returns the block
’s result. That’s why if you print the result you’ll get:
$ [2, 3, 4]
Catch a block!
If you think about it, blocks
are quite similar to procs
.
Both blocks
and procs
are essentially procedures that accept arguments.
So what if we want to somehow… store a block
as an object? Operator &
to the rescue!
It turns out all closures accept an additional implicit argument — a block
!
If we write the argument implicitly with &
in the front of it, we get access to a proc
that holds the block
’s content. Then the &
operator transforms the block
into a proc
!
Build a block?
Actually, &
works both ways.
I bet you saw a line like this many, many times:
Article.all.map(&:author)
&
performs an inverse transformation here — it takes a closure or its symbol and transforms it into a block
which is what map
accepts.
Knowing that, we can refactor our example:
First, let’s read the MyMap#call
method. It takes items
and, as with any closure, a block
. It captures the block
as a proc
using &
operator.
Then, it transforms it back to a block
so it can be passed to the map
.
increment
is a lambda
that accepts an element and returns it’s increment.
Then we just need to transform this lambda
to a block
using &
operator and pass it to our call
method.
Last words
I hope you liked this quick Ruby refresher. These kind of Ruby nuances might be a bit confusing but hopefully you found them also exciting. My advice is not to get too excited and start overusing these things.
lambdas
are cool but extracting a block
like I did in the last example is usually overkill. If it‘s big, it is often better to simply extract parts of it into other methods/classes.
All in all, I believe every Ruby developer should be aware of the differences between blocks
, procs
and lambdas
. It might allow you to avoid some hard-to-catch bug. Other time it’ll help you read some gem’s insides. And who knows, maybe it will lead to a really nice designed, robust code?