Understanding Closures in Ruby
A closure in computer science is a piece of code that carries its creation context around with it. In Ruby, closures include code blocks or methods that have variables linked to the scope environment. This is a sensitive topic to all developers, especially those who are adapting to the functional paradigm. <!--more-->
Prerequisites
To follow along with this tutorial, it is vital to have the following:
- Ruby installed on your computer.
- A basic understanding of Ruby programming.
- Some knowledge in using the interactive Ruby console.
Overview
- Closures
- Closure use cases
- Blocks
- Relationship between blocks and closures
- Procs
- The difference between a lambda and a proc
Closures
To get a clear picture of what closures are, we need to understand first-class functions
, free variables
, and lexical environment
.
A first-class function
is a method that can be treated as an object
and passed as a parameter
to another function.
A free variable
is not declared in the function parent scope but can still be accessed inside the function.
Lexical scoping
refers to the visibility of variables. Lexical scope
also known as eyeball_scoping
is the ability to identify a variable in a program by reading through the code.
Try this in your interactive console:
parent_scope = "I'm available everywhere"
3.times do
inner_scope = "Only accessed in the scope above: -"
puts "#{inner_scope} #{parent_scope}}"
end
The code above demonstrates lexical scoping
. The inner_scope
is only visible from the block where it's defined. When you try to access it outside the block, Ruby will throw an exception.
Therefore, we can define closures
as a block of code that can be used later and stores variables in an environment in which it was created.
We use the following rules when identifying a closure:
- It needs to be a function.
- The function body should reference some variable.
- The variable should be declared in a parent scope.
Closure use cases
- Closures can be used to simulate classes in Ruby.
- Closures also help to implement callbacks in Ruby.
To have a good understanding of our topic, Let's look at Ruby blocks and callable objects.
Ruby Blocks
Blocks
are used to capture code that can accept arguments and be executed later.
In Ruby, blocks
can be delimited by curly braces
or by the do/end
keyword pair. They can also act as anonymous functions.
Let's explore the yield
keyword and block_given?()
method. It is important to understand how these two concepts relate to closures.
Encapsulating behavior into blocks and passing it into methods is a powerful programming technique.
yield
when defined inside a block
simply means execute the block
.
def do_it
yield
end
do_it { puts "I'm doing it" }
When you try to call the do_it
method without a block, the console will show an error.
We can capture the exception
using the block_given?()
method. In this case, the function will only be executed when a block is provided.
def do_it
yield if block_given?
end
do_it
Relationship between closures and blocks
In Ruby, blocks
act as anonymous functions.
Blocks contain local variables that eliminate variable collusion. This happens when one gives a global variable the same name as that in the block scope.
x = "Global variable"
1.times { x = "Block variable... conflicts and is modified" }
puts x #Block variable... conflicts and is modified
When you run the snippet above, you will get an unexpected output. This is because we have assigned a variable in the global scope
with the same name as that in the block scope
.
We can provide a parameter in the declared block to avoid this issue, as shown below:
x = "Global variable"
1.times { |;x| x = "Block parameter prevents variable overriding" }
puts x #Global variable
Procs
In the introduction, we discussed first-class functions. These methods are usually supported by procs
.
Procs are simply callable objects. A block that you can create, store and pass around as method arguments. It is also executed just like a method.
Procs can be accessed using Proc#call(args)
, (args)()
, and lambdas
.
A Proc object is created upon instantiation of the Proc
class:
pr = Proc.new { puts "Inside a Proc's block" }
When you call the above proc
using pr.call
, the code block becomes the body of the proc
and is, therefore, executed.
Procs have a lot of similarities with lambdas. However, note that a proc is not necessarily a lambda.
We create a lambda function using the lambda
keyword, as shown below:
lambda { |x, y| x + y }.call(x, y)
In Ruby, lambdas can also be defined as stabby lambdas. This is illustrated below:
->(x, y) { x + y }.call(x, y)
The difference between a lambda and a proc
Arity
Lambdas, unlike procs, expect an exact number of arguments to be passed:
l = lambda { |a, b| puts "x: #{a}, y: #{b}" } # number of args
p = proc { |a, b| puts "x: #{a}, y: #{b}" }
l.call("Ruby", "closures") #invoking object
p.call("Ruby", "closures")
When we supply one argument to the proc
, it will not throw an exception since there are no restrictions on the parameters.
Unlike procs, lambdas will throw an exception when arguments are not inserted correctly.
Return semantic
Procs always return from their creation context which may be problematic.
Lambdas are preferred over procs since they have the same behavioral pattern as normal methods. This is demonstrated below:
class ReturnSemantic
def method_that_calls_proc_or_lambda(callable_object)
puts "Calling #{proc_or_lambda(callable_object)}"
callable_object.call
puts "#{proc_or_lambda(callable_object)} gets called"
end
def proc_or_lambda(proc_like_thing)
proc_like_thing.lambda? ? "Lambda" : "Proc"
end
end
In the example above, method_that_calls_proc_or_lambda()
is responsible for passing a callable object as an argument. It invokes the callable object thus, expects return values. The results returned from this method will differ if the callable object is a proc or a lambda.
proc_or_lambda()
uses a ternary operator to identify if the argument passed in is a proc or a lambda.
c = ReturnSemantic.new
c.method_that_calls_proc_or_lambda lambda { return }
When one provides a lambda as a parameter, the method will return the last execution of the puts
statement.
When try it with a proc, it returns a LocalJumpError
, as shown below:
c = ReturnSemantic.new
c.method_that_calls_proc_or_lambda proc { return }
Conclusion
Closures are indeed powerful in the hands of a developer. One can write functional and readable code in Ruby using these components. You can read more about closures in Ruby from here.
Further reading
Peer Review Contributions by: Wanja Mike