Back to home

Understanding Ruby Blocks main image

Understanding Ruby Blocks

If you understand the basics of Ruby enumerables, then you are ready to look at the concept of Ruby blocks (including procs and lambdas).

Blocks in Ruby are anonymous functions that can be passed to methods (such as our enumerable method map).

They can be enclosed by our standard do/end statement or inline using brackets {}.

Ruby blocks can also define arguments that are passed to the block between two pipes |.

As denoted by the linked resource: if you have ever used the each method, you will know that the do/end statement is the most common way to define blocks and you will have used them before!

# A Ruby block using the `do/end` statement [1,2,3].each do |num| puts num + 1 # => 2, 3, 4 end

We can inline this block to be even shorter with the bracket {} notation:

# Using the bracket notation [1,2,3].each { |num| puts num + 1 } # => 2, 3, 4

We can spruce things up with our Ruby blocks by using them in our own methods or even storing blocks as variables.

The yield keyword

yield is a keyword that can be called within a method to return execution back to the accompanying block. For example:

def my_method puts "Hello" yield puts "World" end my_method do puts "Goodbye" end # Prints: # Hello # Goodbye # World

In the above example, you can see that the usage of yield enables us to execute the puts "Goodbye" statement that was defined within the do/end block.

We could even use yield to pass arguments to the block:

def my_method yield "Bob" end my_method do |name| puts "Hello #{name}" end # Prints: Hello Bob # Same output as above my_method { |name| puts "Hello #{name}" } # Prints: Hello Bob

yield can also be defined multiple times within a method to denote that we wish to execute multiple blocks.

def my_method yield "Bob" yield "Sue" end my_method { |name| puts "Hello #{name}" } # Prints: # Hello Bob # Hello Sue

Where could this be useful? We could combine the use of blocks with enumerators to create a method that will iterate over an array and execute a block for each element.

@players = ["Bob", "Sue", "Jim"] def players_statement @players.each do |name| yield name end end players_statement { |name| puts "Player: #{name}" } # Prints: # Player: Bob # Player: Sue # Player: Jim

We could even hoist the puts from the block into the method definition:

@players = ["Bob", "Sue", "Jim"] def players_statement @players.each do |name| puts yield name end end players_statement { |name| "Player: #{name}" } # Prints: # Player: Bob # Player: Sue # Player: Jim

What ever is yielded can be used in the initial method definition. This gives us control over how we use the return values.

Handling undefined blocks

What happens if we try to execute a block that has not been defined?

def my_method yield end my_method # => `my_method': no block given (yield) (LocalJumpError)

We have an error as the result. We can therefore use the block_given? method to check if a block has been defined.

def my_method if block_given? puts "block given" else puts "no block given" end puts "executed regardless" end my_method # => no block given # => executed regardless my_method {} # => block given # => executed regardless

An example of a function that implements the block_given? behavior is #count.

Lambda blocks

Lambdas in Ruby give us the capability of assigning a block to a variable.

This enables use to treat blocks as first-class citizens and use them in different methods without the overhead of repeating yourself.

We can declare lambdas in two ways:

  1. Using the lambda keyword.
  2. Using the -> syntax. This is known as "stabby lambda" syntax and is more commonly used.

Here is an example of both in action:

lambda_one = lambda { puts "hello lambda" } lambda_two = -> { puts "hello stabby lambda" } lambda_one.call # => hello lambda lambda_two.call # => hello stabby lambda

As seen above, we can use the #call method to execute the lambda.

Passing arguments to lambda blocks

Depending on which syntax we use, we can pass arguments to the lambda block:

  1. If we use the lambda keyword, we can pass arguments to the block using the | syntax.
  2. If we use the -> syntax, we can pass arguments to the block using the bracket () syntax.

As a demonstration:

print_name = lambda { |name| puts "Hello #{name}" } print_name.call "Bob" # => Hello Bob print_name = -> (name) { puts "Hello #{name}" } print_name.call "Bob" # => Hello Bob

Invoking lambda functions

For the sake of completion, this is a demonstration of how to invoke a lambda function in different ways:

my_name = -> (name) { puts "Hello #{name}" } my_name.call("Bob") my_name.("Bob") my_name["Bob"] my_name.=== "Bob"

In practice, it is best to stick with the call method. This syntax helps us to understand what is going on.

The proc object

proc is an object that can store blocks and pass them around like variables.

Note: A lambda is a type of proc object with some distinct differences. This will be covered further below.

proc_example = Proc.new { puts "Hello proc object" } proc_example.call # => Hello proc object

Instead of using Proc.new, we can also simply use the keyword proc.

proc_example = proc { puts "Hello proc object" } proc_example.call # => Hello proc object

Arguments for proc objects can be defined within pipes:

a_proc = proc { |name, age| puts "Person #{name} is age #{age}" } a_proc.call "Bob", 23 # => Person Bob is age 23

Understanding the different between lambdas and procs

There are some key differences that are paramount that you commit to memory:

  1. Arguments: proc objects don't care how many arguments you pass to them. Lambdas do and will raise an ArgumentError.
  2. Return values: procs return from the context in which it is called. Lambdas return the value of the return expression.

a_lambda = -> { return 1 } a_lambda.call # => 1 a_proc = Proc.new { return } a_proc.call # => localJumpError (unexpected return) def my_method a_proc = Proc.new { return } puts "this line will be printed" # Note: does not return form top-level context, # therefore there is no error. a_proc.call puts "this line is never reached" end my_method #=> this line will be printed

Similarities between lambdas and procs

  1. Default arguments: both support default arguments in the same many as Ruby methods.
  2. Both proc objects and lambdas can be used as first-class citizens and therefore as arguments to a method.

An example of default arguments:

my_proc = Proc.new { |name="Bob"| puts name } my_proc.call # => Bob my_lambda = ->(name="Bob") { puts name } my_lambda.call # => Bob

Passing a proc object/lambda to a method:

def my_method(arg) arg.call end my_lambda = -> { puts "lambda" } my_proc = Proc.new { puts "proc" } # Our defined lambda and proc being used as arguments to `my_method`. my_method(my_lambda) # => lambda my_method(my_proc) # => proc

Capturing blocks

Blocks call also be referenced within a method so that we may do something with it.

def my_method(&block) block.call end my_method { puts "Hello" } # => Hello

We are creating an explicit block when we define the argument &block. Implicit blocks are created when we don't define the argument.

An example of an implicit block:

def my_method yield end my_method { puts "Hello" } # => Hello

It is worth notice that we can still use yield with an explicit block, although it is frowned upon.

The capturing syntax & works as Ruby will call a method #to_proc on whatever is assigned to the variable, so our explicit block becomes a proc object, hence why we can invoke call.

An example given of where the & is used in the wild:

arr = ["1", "2", "3"] arr.map(&:to_i) # => [1, 2, 3]

We to the symbol :to_i into a proc object that is called on each argument mapped over in the provided example.

Using the & the other way around

We can use & on a proc object to convert it into a block.

def my_method yield end my_proc = Proc.new { puts "Hello" } my_method(&my_proc) # => Hello

If you just used my_method(my_proc), it would result in an error.

It will also result in an error if the defined method (i.e. my_method in our case) is expected an argument.

Built-in currying with lambdas

The final thing I wanted to touch on was the capability to curry a lambda.

add = -> (x, y) { x + y } add_two = add.curry.call(2) add_two.call(3) # => 5

This means that we can simplify and reuse more functional approaches to calling re-used utility functions.

Resources and further reading

Personal image

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on workingoutloud.dev, Den Dribbles and LandPad .

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.