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" }
# Rails
Rails.application.routes.draw do
resources :people
end
# Rake
task :test do
ruby "test/unittest.rb"
end
# Bundler
group :development do
gem 'pry-byebug'
end
Blocks, all over the place 😰😱
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"
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)
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
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
-
Outer/parent - where variable
message
is defined - 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)
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 }
Executing blocks
Whenever you see yield
keyword that means execute the block, for example
def speak
yield
end
speak
method will execute any block we give it
speak { puts "Y" }
#=> Y
If we don't pass the block to the method, ruby will raise an exception.
speak
# => LocalJumpError (no block given (yield))
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
We can also pass arguments to the block to get similar behavior to this
speak("ruby") { |message| puts "Hello #{message}" }
def speak(message)
yield(message)
end
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.
✌️
Top comments (4)
Hmm, interesting. Didn't know about this.
Thanks Selver!
Great post =)
Thanks Lucas!