Understanding anonymous functions: Blocks, Procs, and Lambdas in Ruby
Developers often encounter situations where they need to dynamically manipulate code execution. Ruby provides powerful tools such as blocks, procs, and lambdas that allow developers to achieve this level of flexibility. In this article, we'll delve into the intricacies of these constructs, exploring their features, use cases, and differences. By the end, you'll have a solid understanding of when to use each of these tools to enhance your Ruby programming skills.
Blocks
Implicit Call Blocks
In Ruby, a block is a piece of code that can be passed to a method for execution. It provides a way to define custom behavior within a method call. The code snippet below demonstrates the concept of an implicit call block:
# blocks - implicit call block
def with_logger_implicit
puts 'logging started'
yield if block_given?
puts 'logging ended'
end
with_logger_implicit do
a = []
100.times do
a << "hello"
end
end
In this example, the with_logger_implicit
method defines a block of code that is executed between the logging statements. The yield if block_given?
line invokes the block if it's provided during the method call. This allows developers to encapsulate custom logic within the block and execute it within the context of the method.
Explicit Call Blocks
Ruby also supports explicit call blocks, where a block is passed explicitly with the &
symbol and invoked with the call method. Here's how it works:
# blocks - explicit call to block - method .call needed to run the code
# character '&' means that make it as 'Proc' object
def with_logger_explicit(&block)
puts 'logging started'
block.call
puts 'logging ended'
end
with_logger_explicit do
# explicit call to block
a = []
100.times do
a << "hello"
end
end
By using the &block
parameter, the method accepts a block, and block.call
explicitly calls it. This approach provides finer control over when, how the block is named, and how the block is executed.
Procs
A Proc is a block of code wrapped in an object so that it can be stored in a variable and passed around like any other object. Unlike blocks, procs are not bound to a particular method. They can take any number of arguments and even handle default values. Consider the following:
# Proc - a block that is wrapped in a proc object. Proc doesnt care about number of arguments
# adding return inside proc will be OK unless it's not in the top-level context - will raise LocalJumpError
proc = Proc.new { |x = '111', y = '222'| puts "proc:: x: #{x}, y: #{y}" }
proc.call
# => proc:: x: 111, y: 222 (default values)
proc.call(1, 2, 3, 4)
# => proc:: x: 1, y: 2 (no errors even if more arguments than it should be)
The code above demonstrates a proc with default values for its arguments. Procs are incredibly flexible and can be used to encapsulate reusable blocks of code, as in the double_proc
example:
# proc usage:
double_proc = Proc.new { |x| x * 2 }
[1, 2, 3].map(&double_proc)
# => [2,4,6]
Lambdas
Lambdas are similar to procs, but there are some key differences. A lambda enforces strict argument validation, and it behaves more like a regular method in terms of argument count. Let's take a look:
# Lambda is a kind of type of Proc that cares about number of arguments
# Lambda returns values like a regular method
lambda_long = lambda { |x = '111', y = '222'| puts "lambda:: x: #{x}, y: #{y}" }
lambda_short = ->(x, y) { puts "lambda:: x: #{x}, y: #{y}" }
lambda_long.call
# => lambda:: x: 111, y: 222
lambda_short.call(1, 2)
# => lambda:: x: 1, y: 2
lambda_short.call(1, 2, 3, 4)
# => ArgumentError: wrong number of arguments (given 4, expected 2)
# lambda sample usage:
add_two = ->(x) { puts x + 2 }
[1, 2, 3].map(&add_two) # => [3,4,5]
# or oneliner
[1, 2, 3].map(&->(x) { puts x + 2 }) # => [3,4,5]
In the example above, the lambda enforces the correct number of arguments, preventing unexpected behaviour due to argument mismatch. Attempting to pass the wrong number of arguments will result in an ArgumentError
.
In Summary
Understanding the intricacies of blocks, procs, and lambdas is essential for experienced Ruby developers. Blocks provide a way to define custom behavior within methods, both implicitly and explicitly. Procs provide a versatile way to wrap blocks into objects, allowing for reusable and flexible code structures. On the other hand, lambdas add a layer of strictness by enforcing argument counts, making them behave more like regular methods.
By incorporating these constructs into your Ruby programming toolkit, you'll be equipped to handle a wide range of scenarios and create more elegant, dynamic, and powerful code.