Understanding Blocks, Procs & Lambdas
In this post you will learn everything you need to know about Ruby procs, blocks & lambdas.
Understanding Blocks
Blocks are very prevalent in Ruby, you can think of them as little anonymous functions that can be passed into methods.
Blocks are enclosed in a do
/ end
statement or between brackets {}
, and they can have multiple arguments. The argument names are defined between two pipe | characters.
If you have used each before, then you have used blocks! Here is an example:
# single line blocks
[1, 2, 3].each { |num| puts num }
#multi-line blocks
[1, 2, 3].each do |num|
puts num
end
So how does a method work with a block, and how can it know if a block is available? Well, to answer the first question, you need to use the yield
keyword. When you use yield
, the code inside the block will be executed.
def print_once
yield
end
print_once { puts "Block is being run" }
# Output
# Block is being run
Yield
can be used multiple times, which will result in the block being executed as many times as you call yield.
def print_twice
yield
yield
end
print_twice { puts "Hello" }
# Output
# Hello
# Hello
It is also possible to use yield with any number of arguments. These arguments can then be used by the block.
def one_two_three
yield 1
yield 2
yield 3
end
one_two_three { |number| puts number * 10 }
# Output
# 10, 20, 30
Blocks can also be explicit instead of implicit. What this means is that you can name the block and pass it around if you need to. Here is an example:
def explicit_block(&block)
block.call # Same as yield
end
explicit_block { puts "Explicit block called" }
# Output
# Explicit block called
If you try to yield
without a block you will get a no block given (yield) error. You can check if a block has been passed in with the block_given?
method.
def do_something_with_block
return "No block given" unless block_given?
yield
end
Blocks are used as arguments to other methods (like #each
), just like the normal arguments that you see between the parentheses... they just happen to always be listed last and on their own because they tend to take up multiple lines. Don't think of them as anything too special. The #each
method isn't special either, it's just built to accept a block as an argument.
How does #each
take a block then? Through the magic of the yield
statement, which basically says "run the block right here". When you write your own methods, you don't even need to specially declare that you'd like to accept a block. It will just be there waiting for you when you call yield
inside your method.
yield
can pass parameters to your block as well. See this made-up version of the #each
method to get an idea of what's happening under the hood. We'll put this method into the Array class so you can call it directly on an array (like [1,2,3].my_each) instead of having to take the array as an argument like my_each([1,2,3]):
class Array
def my_each
i = 0
while i < self.size
yield(self[i])
i+=1
end
self
end
end
As you can see, we iterate over the array that our #my_each
method was called on (which can be grabbed using self
). Then we call the block that got passed to #my_each
and pipe in whatever member of the original array we are currently on. Last, we just return the original array because that's what #each
does. We would run it just the same way as #each
:
> [1,2,3].my_each { |num| print "#{num}!" }
1! 2! 3! => [1,2,3]
Which operates in that case just like all these lines:
class Array
def my_each
i = 0
while i < self.size
print "#{self[i]}!" # Our block got "subbed in" here
i+=1
end
self
end
end
So one reason blocks are great is because you can write a sort of generic method like #each
which wraps your block in code that says what to do with it. Another use case is when creating methods where you want to optionally be able to override how they "work" internally by supplying your own block -- #sort
lets you supply your own block to determine how to actually order the items of the array if you want to!
What if you want to pass TWO blocks to your function? What if you want to save your block to a variable so you can use it again later? That's a job for Procs, aka Procedures! Actually, a block is a Proc
(which is the class name for a block) and they rhyme just to confuse you. The block is sort of like a stripped-down and temporary version of a Proc that Ruby included just to make it really easy to use things like those #each
iterators.
A Proc is just a block that you save to a variable, thereby giving it a bit more permanence:
> my_proc = Proc.new { |arg1| print "#{arg1}! " }
Use that block of code (now called a Proc) as an input to a function by prepending it with an apersand &
:
> [1,2,3].each(&my_proc)
1! 2! 3! =>[1,2,3]
It's the same as passing the block like you did before!
When you create your own function to accept procs, the guts need to change a little bit because you'll need to use #call
instead of yield
inside (because which proc would yield
run if you had more than one?). #call
literally just runs the Proc that is called on. You can give it arguments as well to pass on to the Proc:
> my_proc.call("howdy ") # edit: note that this is the same `my_proc` as above
howdy => nil
Most of the time, using a block is more than sufficient, especially in your early projects. Once you start seeing the need for using a Proc (like passing multiple arguments or saving it for later as a callback), you'll have Procs there waiting for you.
Blocks and Procs are both a type of "closure". A closure is basically a formal, computer-science-y way of saying "a chunk of code that you can pass around but which hangs onto the variables that you gave it when you first called it". It's the blanket term used to refer to blocks and Procs and...
There are two other similar closures to be aware of but about which you certainly don't need to be an expert because they're used in less typical applications. The first of these is a lambda
. If Procs are sort of a more-fleshed-out version of blocks, then lambdas are sort of a more-fleshed-out version of Procs. They are one step closer to being actual methods themselves, but still technically count as anonymous functions. If you're coming from Javascript, anonymous functions shouldn't be anything new to you.
Just to focus on the differences between lambdas and Procs, a lambda acts more like a real method. What does that mean?
- A lambda gives you more flexibility with what it returns (like if you want to
return
multiple values at once) because you can safely use the explicit return statement inside of one. With lambdas,return
will only return from the lambda itself and not the enclosing method, which is what happens if you usereturn
inside a block or Proc. - Lambdas are also much stricter than Procs about you passing them the correct number of arguments (you'll get an error if you pass the wrong number).
Here's a simple example to show you the syntax of a lambda (btw, there's nothing special to lambdas about placing the #call
after the end
, if you hadn't seen that done before, it's just like method chaining):
> lambda do |word|
> puts word
> return word # you can do this in lambdas not Procs
> end.call("howdy ")
howdy => "howdy " # not nil because we gave it a return
The second additional closure is called a Method because, well, it's the closest of the four (blocks, Procs, lambdas, and Methods) to an actual method. Which it is. "Method"'s (capitalized because they're actually a class of their own) are really just a convenient way to pass a normal method to another normal method by wrapping the symbol of its name in the word method()
So what? To use the same example as we have been so far:
Edit: Note that #my_each
has been modified for this example to now take an argument, which the standard #each
does not. We're using #my_each
below.
class Array
def my_each(some_method)
i = 0
while i < self.size
some_method.call(self[i])
i+=1
end
end
self
end
def print_stuff(word)
print "#{word}! "
end
> [1,2,3].my_each(method(:print_stuff)) # symbolize the name!
1! 2! 3! => nil
To summarize:
- Blocks are unnamed little code chunks you can drop into other methods. Used all the time.
- Procs are identical to blocks but you can store them in variables, which lets you pass them into functions as explicit arguments and save them for later. Used explicitly sometimes.
- Lambdas are really full methods that just haven't been named. Used rarely.
- Methods are a way of taking actual named methods and passing them around as arguments to or returns from other methods in your code. Used rarely.
- Closure is just the umbrella term for all four of those things, which all somehow involve passing around chunks of code.