If you've been programming in Ruby for a while, you've definitely come across blocks, or one of their flavors via Procs and lambdas. If you're coming from another programming language, you might recognize them as closures, anonymous functions, or even arrow functions.
Though all these concepts and fancy words may sound overwhelming and confusing, especially if you're a beginner, the basic concepts are not that complicated. What's more, Ruby makes it a pleasure to work with them, with its elegant, beautiful, and expressive syntax.
In this article, we'll learn just how much fun it is to work with closures and anonymous functions in Ruby. We'll also explore why and when you should use them in your code.
This is a huge article. Here's the list of topics we'll explore:
- What are closures and anonymous functions?
- A block of code
- Procs and Lambdas
- The difference between procs and lambdas
- Capture variables outside scope
- Pass them around as callbacks
- Practical use cases
Let's begin. We've got a lot of ground to cover.
What are Closures and Anonymous Functions?
To quote Jon Skeet, one of my programming heroes from my .NET days,
To put it very simply, closures allow you to encapsulate some behaviour, pass it around like any other object, and still have access to the context in which they were first declared.
This allows you to separate out control structures, logical operators, etc. from the details of how they're going to be used. The ability to access the original context is what separates closures from normal objects.
The Beauty of Closures
If you're an absolute beginner and the above paragraphs went over your head, don't worry. Here's an example that might help you understand better.
Consider a simple function in Ruby that adds two numbers.
def add(a, b)
a + b
end
You can run this as follows:
add(3, 4) # returns 7
What if instead of executing this function immediately, you want to store it in a variable, or pass it to another function, and execute it later? This would let you do so many cool things, like deciding at runtime whether to call it or not based on certain conditions, such as user input or application state.
Closures (also called anonymous/arrow functions) let you accomplish this. In Ruby, they're known as blocks, procs, or lambdas.
There are a few big benefits of using blocks, procs, and lambdas in Ruby. You can:
- Assign them to variables and pass them around.
- Capture variables from outside their scope (hence called closures).
- Implement callbacks (just like JavaScript).
Let's inspect the blocks first.
A Block of Code
Blocks are one of the coolest features of Ruby. Think of them as functions. You can store them in variables, wrap in an object, pass them around to other functions, and call them dynamically. You can use code blocks to implement callbacks, just like in JavaScript.
There are two ways to create blocks in Ruby.
# this is a code block (used for single-line code)
{ |name| puts "hello #{name}" }
# this is also a code block (used for multi-line code),
do |name|
puts "hello #{name}"
puts "how are you?"
end
Although you can use them interchangeably, the best practice is to use the first version with braces { }
for single-line blocks and do..end
version for multi-line blocks.
But you don't use blocks in isolation, as shown above. You have to pass the block to another method by putting it after the method call. In the example below, we're calling the perform
method and passing a block to it.
perform { |a, b| a + b }
# OR
perform do |a, b|
a + b
end
The block won't be executed immediately. The perform
method has to invoke the block using Ruby's yield
keyword.
def perform
puts 'performing the operation you provided via block'
yield # executes the block
puts 'operation completed'
end
Here's another example that greets a person given their name, and also prints a custom greeting message that the caller can pass via a block.
def greet(name)
puts "hey, #{name}"
yield # call the block
puts "bye!"
end
greet("Matz") { puts "Thanks for creating Ruby" }
greet("David") do
puts "Thanks for creating Rails"
end
# Output
#
# hey, Matz
# Thanks for creating Ruby
# bye!
#
# hey, David
# Thanks for creating Rails
# bye!
What happens if a method calls yield
when the caller has not provided a block?
def perform
puts 'performing'
yield
puts 'performed'
end
# oops! forgot to provide the block...
perform
# Output
# no block given (yield) (LocalJumpError)
As you can see, Ruby throws an error: "no block given".
To prevent this, you should always check if a block was passed before using the yield
keyword. For this, use the block_given?
method defined in the Kernel
module (so you can call it as it is, without any object).
def build_response
response = create_response_somehow
yield response if block_given?
do_cleanup
end
build_response do |response|
# handle response
end
Keep in mind: Ruby won't execute the block unless and until you execute it with the yield
keyword.
The above example also demonstrated that you can pass one or more arguments to the call to yield
, and Ruby will pass them to the block. Let's look at another example:
def add(a, b)
sum = a + b
yield sum
end
add(3, 4) { |sum| puts "result: #{sum}" }
add(4, 5) do |sum|
puts "result: #{sum}"
end
### Output
# result: 7
# result: 9
I think that's enough information about blocks for now. Let's explore their cousins, procs and lambdas.
Procs and Lambdas
A Proc is an object that wraps a piece of code (block), allowing us to store the block in a local variable, pass it around, and execute it later. A lambda is very similar to a proc (in fact, it's an instance of Proc
), with a few differences that we'll see later.
You can create a Proc
instance using the following syntax. The Proc
objects created with the lambda syntax (3rd and 4th options below) are called lambdas.
# This is a Proc object
Proc.new { |a, b| a + b }
# or a Proc shorthand
proc { |a, b| a + b }
# A Proc known as a lambda
lambda { |a, b| a + b }
# also a lambda (arrow version)
->(a, b) { a + b }
Interestingly, lambdas are instances of Procs. So behind the scenes, they're all Procs, with a few differences between them.
Procs and lambdas differ from the blocks in the sense that you can directly assign them to a variable that can be passed around. For example,
adder = ->(a, b) { a + b }
# pass to another function
perform(adder)
# call it
result = adder.call(3, 4) # OR adder[3, 4]
Once you have a Proc object, you can execute its code by invoking its methods call
, yield
, or []
.
operation = proc { |arg| puts "a proc with argument: #{arg}" }
operation.call 10 # a block with argument: 10
operation.yield 5 # a block with argument: 5
operation[2] # a block with argument: 2
If you wanted to write the previous greeting example using a lambda or a Proc, you have to treat it as a separate argument in the function definition.
def greet(name, handler)
puts "hey, #{name}"
handler.call
puts "bye!"
end
greet "David", lambda { puts "Thanks for creating Rails" }
greet "Taylor", -> { puts "Thanks for creating Laravel" }
greet "Adam", proc { puts "Thanks for creating Tailwind CSS" }
greet "Anders", Proc.new { puts "Thanks for creating C#" }
In fact, when you pass a block to a method with a parameter with the &
character as a prefix, the block becomes a Proc. This lets you make the block explicit and treat it as a variable (which you can also pass around), as follows:
# blk is an instance of Proc
def build_response(data, &blk)
response = create_response_somehow(data)
blk.call(response)
do_cleanup
end
build_response(data) do |response|
# handle response
end
Keep in mind: The block should always be passed as a last argument in the method. Otherwise, you will receive an error.
To check if a Proc
object is a lambda, call the Proc#lambda?
method on it.
proc_one = proc { puts 'a proc' }
puts proc_one.lambda? # false
proc_two = lambda { puts 'a lambda' }
puts proc_two.lambda? # true
proc_three = -> { puts 'a lambda literal' }
puts proc_three.lambda? # true
What's The Difference Between Procs and Lambdas in Ruby?
Though they might look and feel the same, Procs and lambdas have a few differences.
Difference 1: The return keyword
In a proc, the return keyword returns from the scope where the proc itself was defined. In contrast, the return keyword only returns from the lambda.
I know that didn't make any sense. Let's look an example:
def run_proc
p = proc { return 10 }
p.call # returns from the run_proc method
20
end
def run_lambda
p = lambda { return 10 }
p.call # returns 10 and continues method execution
20
end
result = run_proc
puts result # 10
result = run_lambda
puts result # 20
Difference 2: Checking the Arguments
The second difference between procs and lambdas concerns the way they check their arguments.
A non-lambda proc doesn't care about its arguments.
p = proc { |a, b| puts a, b }
p.call
# nil
p.call 10
# 10
p.call(10, 20)
# 10
# 20
In contrast, for a lambda, Ruby will check that the number of supplied arguments matches the expected parameters.
l = ->(a, b) { puts a, b }
l.call
# Error: wrong number of arguments (given 0, expected 2) (ArgumentError)
l.call 10
# Error: wrong number of arguments (given 1, expected 2) (ArgumentError)
l.call(10, 20)
# 10
# 20
Generally speaking, lambdas are more intuitive than procs because they’re more similar to methods. They’re pretty strict about arity, and they simply exit when you call the return
keyword.
For this reason, many Rubyists use lambdas as a first choice unless they need the specific features of procs.
Next, let's inspect why and when you might want to use blocks in Ruby.
Capture Variables Outside Scope
This example shows how a lambda can capture variables outside its scope.
name = 'ruby'
printer = -> { puts name }
printer.call # ruby
This might seem trivial. However, if you look closely, the variable name
exists outside the scope of the lambda, and yet the lambda can access it.
A regular Ruby function can not access variables outside its scope.
name = 'ruby'
def print
puts name
end
# undefined local variable or method 'name' for main:Object (NameError)
print
This is a pretty powerful feature, and Ruby's metaprogramming takes it to the next level by allowing you to create classes and modules on the fly, using variables defined outside the scope of regular classes and modules.
num = 1
Runner = Class.new do
puts "#{num} from class"
define_method :run do
puts "#{num} from method"
end
end
Runner.new.run
# Output
# ==============
# 1 from class
# 1 from method
This is also known as "flattening the scope". For more details, check out the following article:
Finally, you can pass these blocks, procs, and lambdas as callbacks, just like in JavaScript. Rails abundantly uses this feature. Let's see how.
Pass Them Around as Callbacks
At this point, you might be wondering when you may want to use a block or a lambda. After all, if all you're trying to do is this:
name = 'ruby'
printer = -> { puts name }
printer.call
You could just do this, right?
name = 'ruby'
puts name
Well, this is a trivial example just to make a point. However, in real-world programming, you'd do something like this:
def scope(name, condition)
puts 'condition: ' + name.to_s
if condition.call
puts 'creating scope'
end
puts 'scope was added'
end
# somewhere else in your code
scope :custom, -> { user.admin? }
It is the power of passing around custom logic (check if user is an admin) as callbacks that make blocks so important. They allow a caller to decide what logic executes inside the body of a function, and when.
Rails makes abundant use of blocks, procs, and lambdas. As you can see below, we're directly passing a lambda to the scope
and validates
methods.
class User < ApplicationRecord
scope :confirmed, -> { where(confirmed: true) }
end
class User < ApplicationRecord
validates :email, if: -> { phone_number.blank? }
end
Blocks and lambdas make your Ruby code very expressive and concise, also enabling you to create domain-specific languages (DSL) like Rails.
If you're a JavaScript developer, think of the above code as similar to this code:
scope(CONFIRMED, () => where({ confirmed: true }))
validates(EMAIL, { if: () => phone_number.blank? })
You can notice how pretty it looks in Ruby.
Practical Use Cases
To solidify our understanding, let's look at a few common use cases for blocks.
Run Common Code Before and After
Let's say you always want to execute some code before and after a function runs. For example, creating a lock on a file, performing some operation that could vary, and then releasing a lock.
def handle
before_logic
# varying logic
after_logic
end
You could parameterize the varying operation via a block, and then call the before and after code around the block execution, as follows:
def handle
before_logic
yield
after_logic
end
Now you can call the handle
method as follows:
handle do
# work with a resource
end
You'll have the guarantee that some logic was executed before and after the body of the block.
Benchmark a Piece of Code
Let's say you want to calculate how long it took to perform a few pieces of code blocks. Now you could gather the start and end times for each of those blocks to measure speed, but a better solution is to create a function that accepts a block and handles the measurements.
def benchmark(name, operation)
start_time = Time.now.to_i
operation.call
duration = Time.now.to_i - start_time
puts "#{name} took #{duration} seconds"
end
Now you can use this function as follows:
benchmark :nap, -> { sleep(2) }
# nap took 2 seconds
As you can see, blocks are a very useful tool to make your APIs more fluent and the code more expressive and concise.
I hope you liked this article and you learned something new.
As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I look forward to hearing from you.
If you'd like to receive future articles directly in your email, please subscribe to my blog. If you're already a subscriber, thank you.