A comprehensive guide to the anonymous functions

A comprehensive guide to the anonymous functions

Blocks, Lambdas and Procs

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.

BlockLambdaProc
Checks the number of arguments-YesNo
Returns context-Like a regular methodCan't return from the top-level context
Assigns to variableNot possiblePossiblePossible

Didn't get enough of Ruby?

Check out our free books about Ruby to level up your skills and become a better software developer.