DEV Community

Cover image for Introduction to Ruby blocks
Dino for Wizard Health

Posted on

Introduction to Ruby blocks

Blocks are ubiquitous in Ruby. They are one of the defining and most powerful characteristics of the Ruby language. It is almost impossible to write any meaningful Ruby code without the blocks. They appear everywhere from ruby core library methods such as enumerations to Rails to Rake to almost every gem out there.

# Core lib
10.times { puts "Ruby" }
Enter fullscreen mode Exit fullscreen mode
# Rails
Rails.application.routes.draw do
  resources :people
end
Enter fullscreen mode Exit fullscreen mode
# Rake
task :test do
  ruby "test/unittest.rb"
end
Enter fullscreen mode Exit fullscreen mode
# Bundler
group :development do
  gem 'pry-byebug'
end
Enter fullscreen mode Exit fullscreen mode

Blocks, all over the place ๐Ÿ˜ฐ๐Ÿ˜ฑ

Blocks, everywere

Yet, they can be so hard to understand for beginners and even experienced programmers that are just starting with Ruby.

In this post, we will look at what are the closures, blocks and basic usage patterns.

Let's begin!

Scopes

Lexical scope

Lexical scope serves to answer one important question: What is the value of this variable at this line?

def speak
  message = "in the bottle"
  message
end

speak
# => "in the bottle"
Enter fullscreen mode Exit fullscreen mode

In this example, we created a local variable inside the method, the variable is in the scope of the method, it is created there.

message = "in the bottle"

def speak
  message
end

speak
# => NameError (undefined local variable or method `message' for main:Object)
Enter fullscreen mode Exit fullscreen mode

In this example, we created the variable outside the method scope. When we call the variable inside the method we get an error: Variable is out of the scope.

Local variables are tightly scoped, we can't access a variable outside itself unless it's passed as an argument, or we define an instance variable but that is a story for another day.

Inherited scopes

Yeah, yeah...we got it scoping rules explained above are nothing new and they are easy to grasp. But, wait how does this work then:

message = "in the bottle"

2.times do
  puts message
end

# => in the bottle
# => in the bottle
Enter fullscreen mode Exit fullscreen mode

Blocks, Procs, Lambdas are different. Blocks create scope between do..end(same as method), but they inherit references to the local variables in the context where they are created.

In our example we have 2 scopes

  1. Outer/parent - where variable message is defined
  2. Inner/child - scope inside the block

Our block is created in the context of parent scope and he will carry a reference to the local variable defined in that scope, thus making message variable accessible from within the inner scope.

When block calls variable from the outer/parent scope, that variable is called a free variable.

Turns out, when function whose body(lexical scope) references a variable(free variable) that is declared in the parent scope we got a CLOSURE.

Blocks

Blocks are effectively a type of closure. Blocks capture pieces of code that can be passed into methods to be executed later, they act like anonymous functions.

The ability to encapsulate behavior into blocks and pass it to the methods is an extremely useful technique. This lets you separate general and specific pieces of your code.

Let us imagine we are developers for the NBA. We are building a register of the most popular players:

class Player
  attr_reader :name, :team, :salary, :age

  def initialize(name, team, salary, age)
    @name   = name
    @team   = team
    @salary = salary
    @age    = age
  end
end

players = []
palyers << Player.new("Lebron James", "Lakers", salary: 5000, age: 34)
palyers << Player.new("Kevin Durant", "Nets", salary: 4000, age: 31)
palyers << Player.new("James Harden", "Rockets", salary: 6000, age: 30)
Enter fullscreen mode Exit fullscreen mode

We have a requirement to display players sorted by age or salary or name.

Instead of tying our class with specific sorting implementation we can leverage the power of blocks and separating general from specific like this:

players.sort_by { |player| player.age }
players.sort_by { |player| player.salary }
players.sort_by { |player| player.name }
Enter fullscreen mode Exit fullscreen mode

Executing blocks

Whenever you see yield keyword that means execute the block, for example

def speak
  yield
end
Enter fullscreen mode Exit fullscreen mode

speak method will execute any block we give it

speak { puts "Y" }
#=> Y
Enter fullscreen mode Exit fullscreen mode

If we don't pass the block to the method, ruby will raise an exception.

speak 
# => LocalJumpError (no block given (yield))
Enter fullscreen mode Exit fullscreen mode

Luckily ruby gives as a nice way to check whether the block was passed with block_given?

def speak
  if block_given?
    yield
  else
    "No block is given"
  end
end

speak { puts "Y" }
#=> Y 

speak
#=> No block is given
Enter fullscreen mode Exit fullscreen mode

We can also pass arguments to the block to get similar behavior to this

speak("ruby") { |message| puts "Hello #{message}" }
Enter fullscreen mode Exit fullscreen mode
def speak(message)
  yield(message)
end
Enter fullscreen mode Exit fullscreen mode

Those with a sharp eye will notice that message variable is a free variable here.

That is all for now, in the next posts I will talk more about procs, lambdas, and interesting patterns that we can leverage procs, lambdas, and blocks for.

โœŒ๏ธ

Discussion (4)

Collapse
selver_maric profile image
Selver Maric

Hmm, interesting. Didn't know about this.

Collapse
dixpac profile image
Dino Author

Thanks Selver!

Collapse
lucasprag profile image
Lucas Arantes

Great post =)

Collapse
dixpac profile image
Dino Author

Thanks Lucas!