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.