This tutorial is out of date and no longer maintained.
Closures are, without a doubt, among Ruby’s killer features.
Blocks, procs, lambdas, and methods available in Ruby are collectively called closures.
As useful as they can be, their slight differences at times, make you feel they are just different names for the same Ruby construct.
However, if you look closely, they have their differences and it takes an eager eye to figure them out.
During the course of this tutorial, we are going to analyze Ruby closures and you will learn:
The simplest kind of closure is a block.
A block is a chunk of code that can be passed to an object/method and is executed under the context of that object.
If you have worked with arrays, you have probably used an array’s each
method that allows you to iterate through the array’s contents.
arr = [1, 2, 3, 4, 5]
arr.each do | element |
puts element
end
And here is the output.
Output1
2
3
4
5
In this case, the code between do
and end
which is puts element
is a block.
Though this is a very simple block with a single line of code, blocks can be as long as hundred lines of code depending on what you are trying to achieve using a block.
Here is an example using an array’s each_index
method.
arr = [1, 2, 3, 4, 5]
arr.each_index do | index |
puts "The element at #{index} is #{arr[index]}"
puts "The square of #{arr[index]} is #{arr[index]**2}"
puts "The cube of #{arr[index]} is #{arr[index]**3}"
end
The code above produces the output.
OutputThe element at 0 is 1
The square of 1 is 1
The cube of 1 is 1
The element at 1 is 2
The square of 2 is 4
The cube of 2 is 8
The element at 2 is 3
The square of 3 is 9
The cube of 3 is 27
The element at 3 is 4
The square of 4 is 16
The cube of 4 is 64
The element at 4 is 5
The square of 5 is 25
The cube of 5 is 125
Notice the variables element
and index
that is enclosed within the |
operator to pass into the block in both of our examples.
Depending on how the block is written, you can pass one or more variables into your blocks.
The delete_if
method of the Hash
class is an example. It deletes all elements from the hash for which the block evaluates to true.
hash = {a: 1, b: 2, c: 3, d: :d, e: :e, f: :f}
hash = hash.delete_if do | key, value|
key == value
end
In this case, the hash
variable is reduced to {a: 1, b: 2, c: 3}
.
There are a number of object methods in Ruby that accept blocks and execute them under the context of the object they are passed to.
I highly recommend you read the official Ruby documentation whenever working with any object. It is the best source to learn an object’s specifics and will also help you come across methods that accept blocks as a parameter.
So, knowing how to pass blocks to the core object methods is all good, but in a way, it limits our possibilities if we do not know how to write our own methods that accept blocks.
From our discussion on blocks so far, you might think that you are about to embark on the most complex parts of this tutorial. But this isn’t the case.
Before we move to writing our own block-accepting methods, let us write a simple Post
class that we can use to build upon throughout this tutorial.
class Post
attr_accessor :title, :content, :author, :publish_date
def initialize(title, content, author, publish_date)
@title = title
@content = content
@author = author
@publish_date = publish_date
end
end
We have created a post class with the attributes title
, content
, author
, and publish_date
. Using the attr_accessor
method, we have made all these four attributes readable and writable. The object constructor which is the initialize
method accepts all four of them as a parameter and simply sets the corresponding instance variables.
The next thing I want to talk about is the yield
keyword.
As simple as it may sound, writing your own block-accepting methods is just a matter of using the yield
keyword.
To demonstrate, I am going to write a block_inspect
method on our post class, which will accept a block and pass in the post object’s instance variable names and values to the given block.
class Post
attr_accessor :title, :content, :author, :publish_date
def initialize(title, content, author, publish_date)
@title = title
@content = content
@author = author
@publish_date = publish_date
end
def block_inspect
self.instance_variables.each do |instance_variable|
stringified_instance_variable_name = instance_variable.to_s.sub('@', '')
yield(stringified_instance_variable_name, self.instance_variable_get(instance_variable))
end
end
end
To better understand our block_inspect
method, let us first talk about the instance_variables
and instance_variable_get
methods.
Both the instance_variables
and instance_variable_get
methods are part of the core Object
class. Since all objects inherit from Object
in Ruby, they are automatically available on our post class.
The instance_variables
method returns an array of symbolized instance variable names.
In the case of the post class, it would return [:@title, :@content, :@author, :@publish_date]
.
The instance_variable_get
method simply returns the value for each of these instance variables. All you have to do is pass in the instance variable name.
So, instance_variable_get(:@title)
would return the post’s title and so on.
Inside our block_inspect
method, we have called self.instance_variables
which would return us the array [:@title, :@content, :@author, :@publish_date]
.
So basically, our code resolves to [:@title, :@content, :@author, :@publish_date].each
which as we saw earlier, allows us to work with each element of this array.
Since we want our post attribute names to be human-readable, we need to convert them to strings and remove the prepended @
symbol. This is exactly what the line stringified_instance_variable_name = instance_variable.to_s.sub('@', '')
does, saving the resulting string to the variable stringified_instance_variable_name
.
The next part yield(stringified_instance_variable_name, self.instance_variable_get(instance_variable))
is pretty simple and interesting.
By using the yield
keyword, we are simply instructing the compiler to take the block that was passed to this method, and pass it the post instance variable’s name and value which are stringified_instance_variable_name
, and self.instance_variable_get(instance_variable)
respectively. These variables correspond to the variables passed to the block enclosed within the |
operator.
Since we have used the each
method, this is done for all the post’s instance variables.
Let’s see it in action.
post = Post.new("Title", "Content", "Author", "Publish_Date")
post.block_inspect do |attribute, value|
puts "#{attribute} = #{value}"
end
The above code produces the output.
Outputtitle = Title
content = Content
author = Author
publish_date = Publish_Date
Though it may seem our block_inspect
method is complete, there is one more thing we can add to it to make it neater.
First, let’s outline the problem if we leave our block_inspect
method as it is.
post = Post.new("Title", "Content", "Author", "Publish_Date")
post.block_inspect
If you execute the above code, you will be greeted with the no block given (yield) (LocalJumpError)
error.
This means our block_inspect
method simply assumes a block is passed every time it is called.
However, imagine if you are writing a REST API. The LocalJumpError
, whenever encountered, would break your whole app for no reason which sounds very silly.
To remedy that, we simply need to tell our block_inspect
method to execute the block if it is given using the block_given?
method.
Here is our re-written block_inspect
method.
def block_inspect
self.instance_variables.each do |instance_variable|
stringified_instance_variable_name = instance_variable.to_s.sub('@', '')
if block_given?
yield(stringified_instance_variable_name, self.instance_variable_get(instance_variable))
end
end
end
Or a much cleaner way to do this would be.
def block_inspect
self.instance_variables.each do |instance_variable|
stringified_instance_variable_name = instance_variable.to_s.sub('@', '')
yield(stringified_instance_variable_name, self.instance_variable_get(instance_variable)) if block_given?
end
end
Now, try the block_inspect
method again without passing any block. The LocalJumpError
is no longer encountered and everything functions as it should.
Apart from yield
, we can use a block’s call
method to write block-accepting functions but by using it, we have to explicitly declare in our method definition that a block will be passed to it.
Let’s edit the block_inspect
method once again to understand how that is done.
def block_inspect(&block)
self.instance_variables.each do |instance_variable|
stringified_instance_variable_name = instance_variable.to_s.sub('@', '')
block.call(stringified_instance_variable_name, self.instance_variable_get(instance_variable)) if block_given?
end
end
Starting with the method signature, we have explicitly added block
to the parameters list. The only change apart from that is calling the block using block.call
and passing in the instance variable names and values so that they are available inside our block.
Also, notice that we have added &
before block
when specifying the function parameters. It means that blocks need to be passed by reference, and not value.
The simplest way to understand procs (short for procedures) is when you save your blocks to a variable, it is called a proc.
In other words, a block is actually a proc, only that it has been declared in-line and not saved to a variable.
Here is a proof of concept.
Let us write a very simple function that accepts a block as a parameter. We will only be echoing the class of the passed block inside the function.
def show_class(&block)
puts "The block class is #{block.class}" if block_given?
yield if block_given?
end
show_class do
puts "Hi! from inside the block"
end
The above code produces the output.
OutputThe block class is Proc
Hi! from inside the block
Voilà!
Let us add another method to our post class to do the same inspection using a proc.
def proc_inspect(block)
self.instance_variables.each do |instance_variable|
stringified_instance_variable_name = instance_variable.to_s.sub('@', '')
block.call(stringified_instance_variable_name, self.instance_variable_get(instance_variable))
end
end
Our proc_inspect
method seems pretty similar to the block_inspect
method, the only changes we have made is to remove the &
before the function parameter and the block_given?
conditional.
Now, our proc_inspect
method behaves just like any other function and will be able to figure out the number of parameters needed.
If a proc is not passed to the function, there will be a wrong number of arguments
error instead of a LocalJumpError
.
Here is the proc_inspect
method in action.
proc_inspect = Proc.new do |attribute, value|
puts "#{attribute} = #{value}"
end
post = Post.new("Title", "Content", "Author", "Publish_Date")
post.proc_inspect(proc_inspect)
The above code produces the same exact output as before.
As mentioned earlier, procs are blocks that can be saved to a variable so that is exactly what is happening in the first line of code.
The next two lines simply create a post object and pass it to the proc object using the proc_inspect
method.
Procs can also make your code reusable.
Imagine if you wrote a 100-line block that goes through an object, detailing its various specifics such as the attribute names and values and the memory each of them consumes.
In such a case, writing a 100-line block in-line everywhere it needs to be used sounds very untidy and impractical.
For such scenarios and similar, you can create a proc and simply call it wherever needed.
Lambdas will seem very similar to procs but before we talk about their differences, let us see their practical implementation.
Add another method lambda_inspect
to the post class.
def lambda_inspect(lambda)
self.instance_variables.each do |instance_variable|
stringified_instance_variable_name = instance_variable.to_s.sub('@', '')
lambda.call(stringified_instance_variable_name, self.instance_variable_get(instance_variable))
end
end
It is exactly like the proc_inspect
method and even calling it will not outline any differences except for a few.
lambda_inspect = lambda do |attribute, value|
puts "#{attribute} = #{value}"
end
post.lambda_inspect(lambda_inspect)
So what are the differences?
Well, two.
The first one is a proc does not report an error if the number of parameters passed does not match the number of parameters it accepts whereas a lambda does.
Let me show you.
proc_inspect = Proc.new do |attribute, value, answer_to_life_and_universe|
puts "#{attribute} = #{value}"
puts "Answer to life and universe is #{answer_to_life_and_universe.class}"
end
post = Post.new("Title", "Content", "Author", "Publish_Date")
post.proc_inspect(proc_inspect)
And here is the output.
Outputtitle = Title
Answer to life and universe is NilClass
content = Content
Answer to life and universe is NilClass
author = Author
Answer to life and universe is NilClass
publish_date = Publish_Date
Answer to life and universe is NilClass
As you can see, the class of answer_to_life_and_universe
is printed NilClass
for all the iterations. We could also have printed the variable answer_to_life_and_universe
using puts answer_to_life_and_universe
but since it is NIL
, nothing is printed out.
Now, let us do the same thing using a lambda.
lambda_inspect = lambda do |attribute, value, answer_to_life_and_universe|
puts "#{attribute} = #{value}"
puts "Answer to life and universe is #{answer_to_life_and_universe.class}"
end
post = Post.new("Title", "Content", "Author", "Publish_Date")
post.lambda_inspect(lambda_inspect)
You will be greeted with the wrong number of arguments (given 2, expected 3)
error and the compiler will be all over you like an angry girlfriend.
The second difference has to do with the return
keyword.
When you use return
inside a proc (which is inside a function), it behaves as though using return
inside the function, halting further execution and exiting the function with the returned value.
Whereas in the case of lambdas, only the value is returned and the function resumes execution as normal.
As you know by now, I love to show things using code.
def proc_return
Proc.new do
return "Inside proc !!!"
end.call
return "Inside proc_return !!!"
end
def lambda_return
lambda do
return "Inside lambda_inspect !!!"
end.call
return "Inside lambda_return !!!"
end
puts proc_return
puts lambda_return
Which produces the output.
OutputInside proc !!!
Inside lambda_return !!!
Functions and object methods can be collectively called methods.
There is not much to discuss regarding the specifics of methods.
However, with respect to closures, it is worth mentioning that (tongue twister alert!) the method method
makes methods usable in place of blocks, procs, and lambdas.
def print_object_property(attribute, value)
puts "#{attribute} = #{value}"
end
post = Post.new("Title", "Content", "Author", "Publish_Date")
post.block_inspect(&(method(:print_object_property)))
puts "=========================="
post.proc_inspect(method(:print_object_property))
puts "=========================="
post.lambda_inspect(method(:print_object_property))
The output of the above code shows our function is used in place of block, proc, and lambda flawlessly.
Outputtitle = Title
content = Content
author = Author
publish_date = Publish_Date
==========================
title = Title
content = Content
author = Author
publish_date = Publish_Date
==========================
title = Title
content = Content
author = Author
publish_date = Publish_Date
I hope you enjoyed my dissection of Ruby closures and that it has opened up the language’s new horizons unto you.
Understanding closures is a big leap for any Rubyist, be it a beginner or a more seasoned developer, so pat yourself on the back.
Though it will take you some considerable amount of experience to fully understand the best use-cases for them.
I hope you found this tutorial interesting and knowledgeable. Until my next piece, happy coding!
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!