Anonymous functions are an integral part of Ruby identity and provide flexibility for any kind and size of the codebase. This article is a deep dive into blocks, procs, and lambdas that will help you to understand how these functions differ from each other and how you can benefit from using them.
I will start by explaining each type of function, and at the end of the article, there will be time to compare them. All my notes are based on years of hands-on experience at iRonin.IT - a top software development company, where we provide custom software development and IT staff augmentation services for a wide array of technologies.
Blocks
Let's begin with some real-world and practical examples straight away. If you would like to build a straightforward method that will measure the execution time of a given piece of code, you can use a block:
def measure_time
start_time = Time.now.to_i
yield
end_time = Time.now.to_i - start_time
puts "#{end_time} seconds"
end
Let's test it:
measure_time do
sleep(2)
end
# 2 seconds
# => nil
It's working as expected. Looking at the above code, you can spot two unique expressions: yield
and do/end
. These are characteristic points of block expressions. Let's take a closer look at them.
Wrapping code into a block
There are two ways of wrapping the code into a block. The first one you saw above, is suitable for code that takes more than one line:
mesure_time do
call_method_a
call_method_b
end
and the second one is great for using blocks with a single line of code:
measure_time { call_method_a }
If you wonder what will happen if you will wrap a code into a block with a method that does not provide a block, you can be surprised. Nothing will happen as the code inside the block won't be executed:
def measure_time; end
measure_time { puts 'hello' }
# => nil
Passing a block into the method
We just discussed the blocks that are named implicit. It means that the block parameter was not named. But it can also be named, and then it becomes explicit:
def measure_time(&tested_expression)
tested_expression.call
end
The block won't be executed unless you will execute the call method on it. Block that is passed to the method with the &
character becomes a proc. I will discuss procs a little bit later in this article.
What's important, the block should always be passed as a last argument in the method; otherwise, you will receive an error.
Yield or not to yield
In the very first code example used in this article, we used a yield
word inside a block. When the yield
is called, then the code inside the block is immediately executed. You can call yield
as many times as you want, and Ruby will run the same code each time:
def yield_example
puts 'before'
yield
puts 'middle'
yield
puts 'after'
end
yield_example { puts rand(100) }
# before
# 82
# middle
# 87
# after
# => nil
If you are unsure if the block will always be passed, you can use block_given?
to verify that:
def yield_example
puts "block given" if block_given?
end
yield_example
# => nil
yield_example {}
# block given
# => nil
Yield and arguments
Let's consider the following piece of code:
[1, 2, 3].each do |element|
puts element
end
The each
method accepts a block and provides one block argument, a currently processed element from the array. The each_with_index
method yields two block arguments where the second one is the position of the current element in the array.
Let's implement our implementation of the each_with_index
method:
class Array
def my_each_with_index
for i in self do
yield(i, self.index(i))
end
end
end
Now, we can use it the same way we are using the standard each_with_index
method:
[5, 6, 8].my_each_with_index do |el, i|
puts "element #{el} has index #{i}"
end
# element 5 has index 0
# element 6 has index 1
# element 8 has index 2
# => [5, 6, 8]
Procs
I mentioned that when the block is passed to a method, it becomes a proc:
def measure_time(&tested_expression)
tested_expression.call
end
We can convert the following code:
measure_time do
puts "call my block"
end
to a new Proc
instance:
my_proc = Proc.new { puts "call my block" }
my_proc.call
Proc doesn't care about the arguments
Proc can accept arguments that are passed then to the call
method, but don't expect any error if you would pass too many arguments or none of them:
proc = Proc.new { |x, y| puts "x: #{x}, y: #{y}" }
proc.call
# => nil
Of course, you can verify the presence of the argument inside the proc and raise an error if needed, but then it's a better idea to use Lambda which cares about the number of the arguments.
Return or not to return
How the return works with Proc is dependent on the context. To demonstrate that behavior let's start with defining a simple proc that returns a value:
proc = Proc.new { return :first_name }
If we would call that proc in the context of another method, it will behave as expected:
def some_method
proc = Proc.new { return :first_name }
proc.call
return :last_name
end
some_method
# => :first_name
However, calling the return in a top-level context will result in the error:
proc = Proc.new { return :first_name }
proc.call
# => LocalJumpError: unexpected return
Lambda
Lambda behaves like an older sister for the Proc. The truth is that Lambda is a Proc, but just a special version of it. It behaves similarly, but her behavior is more predictable. We can create a lambda using the lambda
keyword or ->
character:
my_lambda = lambda { puts "I'm lambda" }
my_lambda.call
my_lambda = -> { puts "I'm lambda" }
my_lambda.call
The syntax is also a little bit different when it comes to defining the arguments:
my_lambda = lambda { |x, y| puts "x: #{x}, y: #{y}" }
my_lambda = ->(x, y) { puts "x: #{x}, y: #{y}" }
Arguments policy
Unlike Proc, Lambda cares about the number of arguments you pass to the call
method. When it comes to the arguments validation policy lambda behaves like a normal method:
my_lambda = lambda { |x, y| puts "x: #{x}, y: #{y}" }
my_lambda.call(1)
# => ArgumentError: wrong number of arguments (given 1, expected 2)
my_lambda.call(1, 2)
# => x: 1, y: 2
Return
The return
key works quite the opposite of how it works in Proc. When you call return with lambda in the top-level, then it works like a method return:
my_lambda = lambda { return 1 }
my_lambda.call
# => 1
In Proc, we receive an error in a similar situation. While in Proc, the return is respected when calling inside the method, with lambda is ignored:
def my_method
my_lambda = lambda { return 1 }
my_lambda.call
2
end
my_method
# => 2
If you would like to return the lambda call value, you have to call the return explicitly:
return my_lambda.call
If you do not like to call
Lambda, as well as Proc, can be invoked by calling the call
method on it. If you don't want to use it, you have some alternatives at your disposal:
my_lambda = lambda { puts "Hello world" }
my_lambda.()
my_lambda.[]
my_lambda.===
I would stick to the call
method because it is a self-explanatory and commonly used approach in other situations and classes.
Other interesting features of Procs and Lambdas
We go through a relatively detailed explanation of how the Procs and Lambdas are working, but I haven't covered some other useful and interesting features. Here they are.
Default argument values
Like in the normal method, it is possible in both Proc and Lambda to define default values for the arguments:
my_proc = Proc.new { |x = 1, y = 2| [x, y] }
my_proc.call
# => [1, 2]
my_proc.call(3)
# => [3, 2]
The same format applies to lambdas.
Transition into a block
Everybody knows the map
method that we can invoke on the array:
[1, 2, 3].map do |element|
element + 2
end
# => [3, 4, 5]
The map
method accepts a block. Since we can turn lambda and proc to block we can make the above call more interesting:
my_proc = Proc.new { |x| x + 2}
[1, 2, 3].map(&my_proc)
# => [3, 4, 5]
Yes, a one-liner is also possible:
[1, 2, 3].map(&->(x) { x + 2})
# => [3, 4, 5]
To sum up: to convert proc or lambda to block, pass the expression with the &
character in front of it.
Anonymous functions around us
In the popular Ruby code, like libraries of frameworks, lambdas are very heavily used. For example, in the Ruby on Rails framework, lambdas are used to define scopes in the model:
class User < ApplicationRecord
scope :confirmed, -> { where(confirmed: true) }
end
And to define conditions for the validations:
class User < ApplicationRecord
validates :email, if: -> { phone_number.blank? }
end
When looking at the Ruby standard library, we can spot lambdas in the code responsible for parsing CSV files. Lambdas are used as converters for values inside the CSV:
require 'csv'
CSV::Converters[:integer].call("1")
# => 1
CSV::Converters[:date].call("2021-01-01")
# => #<DateTime …>
Those three examples are just a drop in the ocean of Ruby's lambdas that are commonly used.
The comparison of anonymous functions
Since all anonymous functions are similar in some way, it is good to have a high-level comparison to be aware of when we should use a given solution.
Block | Lambda | Proc | |
Checks the number of arguments | - | Yes | No |
Returns context | - | Like a regular method | Can't return from the top-level context |
Assigns to variable | Not possible | Possible | Possible |
Didn't get enough of Ruby?
Check out our free books about Ruby to level up your skills and become a better software developer.