In Ruby Magic we love to dive into the magic behind the things we use every day to understand how they work. In this edition, we’ll explore the differences between blocks, procs and lambdas.
In programming languages with first-class functions, functions can be stored in variables and passed as arguments to other functions. Functions can even use other functions as their return values.
A closure is a first-class function with an environment. The environment is a mapping to the variables that existed when the closure was created. The closure will retain its access to these variables, even if they’re defined in another scope.
Ruby doesn’t have first-class functions, but it does have closures in the form of blocks, procs and lambdas. Blocks are used for passing blocks of code to methods, and procs and lambda’s allow storing blocks of code in variables.
Blocks
In Ruby, blocks are snippets of code that can be created to be executed later. Blocks are passed to methods that yield them within the do
and end
keywords. One of the many examples is the #each
method, which loops over enumerable objects.
[1,2,3].each do |n|
puts "#{n}!"
end
[1,2,3].each { |n| puts "#{n}!" } # the one-line equivalent.
In this example, a block is passed to the Array#each
method, which runs the block for each item in the array and prints it to the console.
def each
i = 0
while i < size
yield at(i)
i += 1
end
end
In this simplified example of Array#each
, in the while
loop, yield
is called to execute the passed block for every item in the array. Note that this method has no arguments, as the block is passed to the method implicitly.
Implicit Blocks and the yield
Keyword
In Ruby, methods can take blocks implicitly and explicitly. Implicit block passing works by calling the yield
keyword in a method. The yield
keyword is special. It finds and calls a passed block, so you don't have to add the block to the list of arguments the method accepts.
Because Ruby allows implicit block passing, you can call all methods with a block. If it doesn’t call yield
, the block is ignored.
irb> "foo bar baz".split { p "block!" }
=> ["foo", "bar", "baz"]
If the called method does yield, the passed block is found and called with any arguments that were passed to the yield
keyword.
def each
return to_enum(:each) unless block_given?
i = 0
while i < size
yield at(i)
i += 1
end
end
This example returns an instance of Enumerator
unless a block is given.
The yield
and block_given?
keywords find the block in the current scope. This allows passing blocks implicitly, but prevents the code from accessing the block directly as it's not stored in a variable.
Explicitly Passing Blocks
We can explicitly accept a block in a method by adding it as an argument using an ampersand parameter (usually called &block
). Since the block is now explicit, we can use the #call
method directly on the resulting object instead of relying on yield
.
The &block
argument is not a proper argument, so calling this method with anything else than a block will produce an ArgumentError
.
def each_explicit(&block)
return to_enum(:each) unless block
i = 0
while i < size
block.call at(i)
i += 1
end
end
When a block is passed like this and stored in a variable, it is automatically converted to a proc.
Procs
A "proc" is an instance of the Proc
class, which holds a code block to be executed, and can be stored in a variable. To create a proc, you call Proc.new
and pass it a block.
proc = Proc.new { |n| puts "#{n}!" }
Since a proc can be stored in a variable, it can also be passed to a method just like a normal argument. In that case, we don't use the ampersand, as the proc is passed explicitly.
def run_proc_with_random_number(proc)
proc.call(random)
end
proc = Proc.new { |n| puts "#{n}!" }
run_proc_with_random_number(proc)
Instead of creating a proc and passing that to the method, you can use Ruby’s ampersand parameter syntax that we saw earlier and use a block instead.
def run_proc_with_random_number(&proc)
proc.call(random)
end
run_proc_with_random_number { |n| puts "#{n}!" }
Note the added ampersand to the argument in the method. This will convert a passed block to a proc object and store it in a variable in the method scope.
Tip: While it's useful to have the proc in the method in some situations, the conversion of a block to a proc produces a performance hit. Whenever possible, use implicit blocks instead.
#to_proc
Symbols, hashes and methods can be converted to procs using their #to_proc
methods. A frequently seen use of this is passing a proc created from a symbol to a method.
[1,2,3].map(&:to_s)
[1,2,3].map {|i| i.to_s }
[1,2,3].map {|i| i.send(:to_s) }
This example shows three equivalent ways of calling #to_s
on each element of the array. In the first one, a symbol, prefixed with an ampersand, is passed, which automatically converts it to a proc by calling its #to_proc
method. The last two show what that proc could look like.
class Symbol
def to_proc
Proc.new { |i| i.send(self) }
end
end
Although this is a simplified example, the implementation of Symbol#to_proc
shows what’s happening under the hood. The method returns a proc which takes one argument and sends self
to it. Since self
is the symbol in this context, it calls the Integer#to_s
method.
Lambdas
Lambdas are essentially procs with some distinguishing factors. They are more like "regular" methods in two ways: they enforce the number of arguments passed when they're called and they use "normal" returns.
When calling a lambda that expects an argument without one, or if you pass an argument to a lambda that doesn't expect it, Ruby throws an ArgumentError
.
irb> lambda (a) { a }.call
ArgumentError: wrong number of arguments (given 0, expected 1)
from (irb):8:in `block in irb_binding'
from (irb):8
from /Users/jeff/.asdf/installs/ruby/2.3.0/bin/irb:11:in `<main>'
Also, a lambda treats the return keyword the same way a method does. When calling a proc, the program yields control to the code block in the proc. So, if the proc returns, the current scope returns. If a proc is called inside a function and calls return
, the function immediately returns as well.
def return_from_proc
a = Proc.new { return 10 }.call
puts "This will never be printed."
end
This function will yield control to the proc, so when it returns, the function returns. Calling the function in this example will never print the output and return 10.
def return_from_lambda
a = lambda { return 10 }.call
puts "The lambda returned #{a}, and this will be printed."
end
When using a lambda, it will be printed. Calling return
in the lambda will behave like calling return
in a method, so the a
variable is populated with 10
and the line is printed to the console.
Blocks, procs and lambdas
Now that we’ve gone all the way into both blocks, procs and lambdas, let’s zoom back out and summarize the comparison.
- Blocks are used extensively in Ruby for passing bits of code to functions. By using the
yield
keyword, a block can be implicitly passed without having to convert it to a proc. - When using parameters prefixed with ampersands, passing a block to a method results in a proc in the method's context. Procs behave like blocks, but they can be stored in a variable.
- Lambdas are procs that behave like methods, meaning they enforce arity and return as methods instead of in their parent scope.
This concludes our look into closures in Ruby. There's more to learn about closures like lexical scopes and bindings, but we'll keep that for a future episode. In the meantime, please let us know what you'd like to read about in a future installment of Ruby Magic, closures or otherwise.
Top comments (3)
Okay, this is a piece that I'll need some time to chew on! Very detailed; I just need to read it s-l-o-w-l-y...
Hey Arit! 👋
Take your time, and please be sure to let us know if there's something we can explain better. :)
Great post Jeff!