Understanding Ruby Blocks
Published: Feb 14, 2022
Last updated: Feb 14, 2022
If you understand the basics of Ruby enumerables
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:
- Using the
lambda
keyword. - 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:
- If we use the
lambda
keyword, we can pass arguments to the block using the|
syntax. - 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:
- Arguments: proc objects don't care how many arguments you pass to them. Lambdas do and will raise an
ArgumentError
. - 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
- Default arguments: both support default arguments in the same many as Ruby methods.
- 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
Dennis O'Keeffe
Melbourne, Australia
1,200+ PEOPLE ALREADY JOINED ❤️️
Get fresh posts + news direct to your inbox.
No spam. We only send you relevant content.
Understanding Ruby Blocks
Introduction