Ruby Closures

Understanding Blocks, Procs, Methods, and Arity

Marwan Zaarab
7 min readJun 5, 2022

In this article, we will dive into the world of Ruby, exploring closures, blocks, procs, methods, and arity. Understanding these concepts is crucial for writing efficient and maintainable Ruby code, as they form the building blocks of the language’s flexibility and expressiveness.

Definitions

Closures are implemented in Ruby by instantiating an object from the Proc class, by using blocks or lambdas.

Blocks in Ruby are essentially segments of code enclosed within curly braces {...} or defined between a do...end statement. Blocks can be passed as arguments to methods, and the code within the block is executed when and where the method yields to it.

# Define a block
{ |x| puts x * 2 }

# Pass the block to a method
[1, 2, 3].each { |x| puts x * 2 }

Procs are an encapsulation of a block of code. The only major difference is that they can be stored in a local variable prior to being passed to a method or another Proc. They can also be called.

# Define a Proc
my_proc = Proc.new { |x| puts x * 2 }

# Call the Proc
my_proc.call(5) # Output: 10

Similarities (Blocks, Procs)

  • Both can encapsulate a chunk of code for execution at a later time.
  • They bind to surrounding artifacts, such as variables, methods and constants, creating a closure.
  • This closure forms an enclosure around objects, allowing them to be referenced when executed.

Consider the following example:

  1. Execution starts at method invocation, on line 8.
  2. The hello method is called with two arguments: a string (var) and a block. Note that the block is an implicit parameter, not explicitly defined in the method.
  3. At the time of method invocation, the block is appended and forms a binding with the surrounding objects in scope — here, the variable var pointing to "Jack".
  4. Execution then shifts to line 1, where the method’s local variable var is reassigned the string "Jolene". The block is passed implicitly without being assigned to any variable.
  5. The execution continues to the method implementation, initializing a new local variable var, and immediately yields to the block.
  6. The block on line 8 executes and, as it is the last expression evaluated within the method, its return value is “Hello Jack”.

Now, let’s have a look at a similar example using a Proc this time:

  • The ampersand &, when prefixed to an argument in a method invocation, first checks if that argument is already a Proc.
  • In this example, it is, so it converts the Proc into a block and appends it to the method upon invocation.
  • The local variable name, initialized to "Joe” on line 6, is used within string interpolation in the Proc object created on line 7.
  • Subsequently, on line 9, name gets reassigned to "Jack”.
  • Similarly, the local variable n, initialized before my_proc, is reassigned to "Hey #{name}!" on line 8.
  • Despite the reassignment of name occurring after the creation of my_proc, the method #hello returns "Hello Jack! Hey Joe!".

This highlights a fundamental difference in how local variables and procs store values. Local variables bind to the values of objects at the time and location of their initialization, hence n is unaware of what name points to after line 8.

Conversely, Proc objects retain the scope in which they were created until they are called. They record any variables initialized prior to the Proc and any subsequent reassignments.

When executed within a method, the encapsulated code of a block or proc runs in the scope it was created, not in the new inner scope formed by the method.

This demonstrates the unique way Proc objects encapsulate and execute code, preserving the original scope and reflecting changes in variable assignments.

However, reassignments happening after that method invocation will not be shown, unless a method is invoked once again after that reassignment.

You can think of a Proc as taking a snapshot of a scene. Just like a snapshot captures a moment in time, a Proc captures the current state of variables and context when it is defined. When you “call” the Proc, it executes the code using the captured state, just like viewing a snapshot transports you back to that moment.

While both blocks and procs bind to their surrounding artifacts, procs hold the additional power of being able to be defined and saved to a local variable prior to being passed in to a method. This allows them to be more flexible and reusable.

Procs can also be returned from a method and stored within a local variable in the main scope. Have a look at the following example, which allows you to calculate custom ranges of factorials:

While you can’t attach a code block to a method and call it later like you do with Procs, you can attach a code block to a method, convert it to a proc and call the proc later. You do that by explicitly defining the block in the method parameters by prefixing it with an &.

Here, &my_block functions as a reference for the block. When handling blocks in this manner, they must always be the final parameter defined in the method and prefixed with an &. This makes them an optional parameter — we do not have to call or yield to the block — and the method will not throw an ArgumentError. This syntax signals to the method that any block passed into it upon method invocation can be used as a Proc. This allows us to call it like a Proc or yield to it like a regular block.

Note about Binding and Scope

Blocks inherit their parent scope at the time and place they are defined. Anything initialized after that point is not accessible from within the block. This is sometimes referred to as “lexical scope”. However, changes to variables that were initialized prior to the block or proc are accessible. That is, up to the point that the code block is executed.

Blocks vs. Procs

When do we want to use blocks?

  1. Defer part of a method implementation to the method caller.
  2. Defer the decision of which flags to support and let the method caller decide at method invocation time.
  3. Sandwich code: methods that need to perform some before and after actions (timing, opening/closing files, resource management)

Instead of modifying your method directly, you can allow developers using your method to come in after the method is fully defined and inject additional code in the middle of the method. This allows for higher flexibility in refining the method implementation without modifying the method implementation for everyone else.

For example, the select method is built in a way that allows for much more flexibility than if it was defined for specific use cases, e.g., select_evens, select_divisible_by_three, etc.

Arity of Blocks, Procs and Methods

Arity refers to the rules regarding the number of arguments that you must pass to an object or method.

While methods and lambdas have strict arity, blocks and Proc objects have lenient arity — meaning that they are flexible in the number of arguments you choose to pass or not pass to them. Any extra arguments are ignored and any unused block parameters will be set to nil.

# Lenient Arity
my_proc = Proc.new { |x, y| puts x + y }
my_proc.call(1, 2) # Works
my_proc.call(1) # Works, y is nil

Methods and lambdas on the other hand enforce argument count. Their arity is strict, which means you must pass the exact number of arguments that is expected, as defined in their parameters.

# Strict Arity
my_lambda = lambda { |x, y| puts x + y }
my_lambda.call(1, 2) # Works
my_lambda.call(1) # ArgumentError

Return values of blocks

Blocks, just like methods, return a value that corresponds to the last evaluated expression in their closure. If you initialize a variable inside a method and assign to it the result of yielding to a block, it will be assigned to the return value from the block.

What if the return keyword is used within a block?

A lambda returns from the code block it contains (the lambda itself). A Proc returns from the calling object’s scope (everyone_return method and then main).

The above works inside a method, but had we executed a return from a Proc closure in the main scope, it would return nothing and simply exit — or raise a LocalJumpError (unexpected return) if you try it in an IRB.

Symbol#to_proc

  • So far, we’ve seen & as syntactic sugar for to_proc. When prepended to a symbol, it becomes a syntactic shortcut that returns aProc object and subsequently a block that responds to the given method by the symbol.
  • However, it does not work outside of this context. For example, the following colorize = &:color would raise a SyntaxError. It is most frequently used in passing a Proc created from a symbol to a method.

The method returns a Proc, which takes one argument and sends self to it.
self is the symbol in this context, which calls the Integer#to_s method.

In other words:

  1. & calls to_proc on the the object identified by the symbol (:to_s)
  2. the resulting Proc is then coerced into a block for the method being called (#map in the example above)

Note: Unfortunately, this shortcut doesn’t work for methods that take arguments.

Symbol#to_proc is particularly useful when you want to perform a single method call over each element of an array:

# Without Symbol#to_proc
[‘ruby’, ‘java’, ‘python’].map { |x| x.length }

# With Symbol#to_proc
[‘ruby’, ‘java’, ‘python’].map(&:length)

Summary

In conclusion, understanding closures, blocks, procs, methods, and arity is fundamental for any Ruby developer. These concepts allow for expressive and flexible code, enabling Rubyists to solve complex problems in an elegant manner.

--

--