Table of Contents
-
Introduction to Blocks in Ruby
- What is a Block?
- Syntax of Blocks
- How Blocks Work with Methods
- Examples of Built-in Methods Using Blocks
-
- What is a Proc?
- Creating Procs
- Executing Procs
- Procs as First-Class Citizens
-
Lambdas: A Special Kind of Proc
- What is a Lambda?
- Creating Lambdas
- How Lambdas Differ from Regular Procs
-
Key Differences Between Blocks, Procs, and Lambdas
- Arity (Argument Handling)
- Return Behavior
- Syntax and Usage Context
-
Practical Use Cases for Blocks and Procs
- Iteration and Collection Processing
- Code Reusability and Abstraction
- Custom Control Structures
- Callbacks and Event Handling
-
- Passing Blocks Explicitly with
&block - Converting Blocks to Procs (and Vice Versa)
- Closures: Lexical Scoping in Blocks and Procs
- Proc Composition
- Passing Blocks Explicitly with
1. Introduction to Blocks in Ruby
What is a Block?
A block is a chunk of code enclosed in curly braces {} or do...end that can be passed to a method. Unlike objects, blocks are not first-class citizens in Ruby (they cannot be stored in variables or passed directly to other methods), but they are seamlessly integrated with Ruby’s method invocation syntax.
Blocks enable methods to defer execution of logic to the caller, promoting flexibility. For example, the built-in each method uses a block to define what to do with each element of a collection, while the each method itself handles how to iterate.
Syntax of Blocks
Ruby supports two syntaxes for blocks:
-
Single-line blocks: Enclosed in curly braces
{}, ideal for short, one-line logic.[1, 2, 3].each { |num| puts num * 2 } # Output: 2, 4, 6 -
Multi-line blocks: Enclosed in
do...end, preferred for longer, multi-line logic.[1, 2, 3].each do |num| squared = num **2 puts "Square of #{num} is #{squared}" end # Output: # Square of 1 is 1 # Square of 2 is 4 # Square of 3 is 9
Blocks can accept parameters, specified between vertical pipes |param1, param2|. In the examples above, |num| is a parameter that receives each element of the collection during iteration.
How Blocks Work with Methods
A method can execute a block using the yield keyword. When a method encounters yield, it invokes the block passed to it. If no block is provided, yield will raise a LocalJumpError unless guarded by block_given?, which checks if a block was passed.
Example: Defining a method that uses a block
def greet
puts "Hello!"
yield if block_given? # Execute block only if provided
puts "Goodbye!"
end
# Call with a block
greet { puts "Nice to meet you!" }
# Output:
# Hello!
# Nice to meet you!
# Goodbye!
# Call without a block (no error, thanks to block_given?)
greet
# Output:
# Hello!
# Goodbye!
Examples of Built-in Methods Using Blocks
Ruby’s standard library relies heavily on blocks for iteration and collection processing. Common examples include:
-
each: Executes the block for each element in a collection.["apple", "banana", "cherry"].each { |fruit| puts "I like #{fruit}" } -
map: Transforms elements using the block and returns a new collection.numbers = [1, 2, 3] doubled = numbers.map { |n| n * 2 } # doubled = [2, 4, 6] -
select: Filters elements based on a block that returnstrue/false.even_numbers = (1..10).select { |n| n.even? } # even_numbers = [2, 4, 6, 8, 10] -
times: Executes the blockntimes (wherenis the receiver).3.times { puts "Ruby is fun!" } # Prints "Ruby is fun!" 3 times
2. Introduction to Procs
What is a Proc?
A proc (short for “procedure”) is an instance of Ruby’s Proc class. Unlike blocks, procs are first-class objects—they can be stored in variables, passed as arguments to methods, and returned from methods. Procs bridge the gap between blocks (ephemeral code chunks) and objects (persistent, manipulable entities).
Creating Procs
Procs can be created in two primary ways:
1.** Using Proc.new **:
add = Proc.new { |a, b| a + b }
2.** Using the proc keyword **(shorthand for Proc.new):
multiply = proc { |a, b| a * b }
Executing Procs
Procs are executed using the call method, or via shorthand syntax like [], ===, or .(args):
add = proc { |a, b| a + b }
add.call(2, 3) # => 5
add[4, 5] # => 9 (equivalent to call)
add.===(6, 7) # => 13 (rarely used, but valid)
add.(8, 9) # => 17 (modern shorthand)
Procs as First-Class Citizens
Since procs are objects, they can be:
- Stored in variables or data structures (e.g., arrays, hashes).
- Passed as arguments to methods.
- Returned from methods (enabling higher-order functions).
Example: Storing procs in an array
operations = [
proc { |x| x + 1 },
proc { |x| x * 2 },
proc { |x| x **3 }
]
result = 2
operations.each { |op| result = op.call(result) }
puts result # ((((2 + 1) * 2)** 3) → (3 * 2 = 6; 6³ = 216) → Output: 216
3. Lambdas: A Special Kind of Proc
What is a Lambda?
A lambda is a type of Proc with stricter behavior. Think of it as a “function-like” proc, with rules for argument handling and return behavior that mimic regular methods.
Creating Lambdas
Lambdas can be created in two ways:
-
Using the
lambdakeyword:greet = lambda { |name| "Hello, #{name}!" } -
Using the
->(stab) syntax (shorthand forlambda):multiply = ->(a, b) { a * b } # Parentheses for parameters are optional but recommended
How Lambdas Differ from Regular Procs
Lambdas and regular procs share most features but differ in two critical ways:
1. Arity (Argument Strictness)
Lambdas enforce strict argument counts: if you pass the wrong number of arguments, they raise an ArgumentError. Regular procs are lenient—extra arguments are ignored, and missing arguments are set to nil.
Example: Arity Comparison
# Regular proc (lenient)
lenient_proc = proc { |a, b| a + b }
lenient_proc.call(5) # => 5 + nil = nil (no error)
lenient_proc.call(5, 10, 15) # => 5 + 10 = 15 (ignores 15)
# Lambda (strict)
strict_lambda = lambda { |a, b| a + b }
strict_lambda.call(5) # ArgumentError: wrong number of arguments (given 1, expected 2)
strict_lambda.call(5, 10, 15) # ArgumentError: wrong number of arguments (given 3, expected 2)
2. Return Behavior
When a lambda encounters return, it exits only the lambda itself and returns control to the calling context. A regular proc, however, exits the method that called the proc.
Example: Return Behavior
def test_proc
my_proc = proc { return "Exiting proc!" }
my_proc.call
"This line never runs" # Proc's return exits the method
end
def test_lambda
my_lambda = lambda { return "Exiting lambda!" }
my_lambda.call
"This line runs!" # Lambda's return exits only the lambda
end
test_proc # => "Exiting proc!"
test_lambda # => "This line runs!"
4. Key Differences Between Blocks, Procs, and Lambdas
To avoid confusion, let’s summarize the core differences:
| Feature | Blocks | Procs | Lambdas |
|---|---|---|---|
| Type | Not an object (syntax construct) | Proc object | Proc object (specialized) |
| Arity | Lenient (ignores extra args) | Lenient | Strict (raises ArgumentError) |
| Return Behavior | Exits the enclosing method | Exits the enclosing method | Exits only the lambda |
| Storage | Cannot be stored in variables | Storable in variables/objects | Storable in variables/objects |
| Syntax | { ... } or do...end | proc { ... } or Proc.new | lambda { ... } or -> { ... } |
5. Practical Use Cases for Blocks and Procs
Iteration and Collection Processing
Blocks are the backbone of Ruby’s iteration patterns. Methods like each, map, and select rely on blocks to define per-element logic. Procs extend this by allowing reusable iteration logic:
# Reusable proc for squaring numbers
square = proc { |x| x **2 }
[1, 2, 3].map(&square) # => [1, 4, 9]
(4..6).map(&square) # => [16, 25, 36]
Code Reusability and Abstraction
Blocks and procs help abstract repetitive code. For example, wrapping database transactions or logging logic:
# Abstract "measure execution time" logic into a proc
measure_time = proc do |&block|
start = Time.now
result = block.call
puts "Time taken: #{Time.now - start}s"
result
end
# Use the proc to measure different operations
measure_time.call { sleep 1 } # Output: "Time taken: ~1.0s"
measure_time.call { (1..1_000_000).sum } # Measures sum calculation time
Custom Control Structures
Blocks enable custom control flow. For example, a retry_until method that repeats a block until a condition is met:
def retry_until(max_attempts: 3, &block)
attempts = 0
loop do
attempts += 1
result = block.call
return result if result.success?
raise "Max attempts reached" if attempts >= max_attempts
sleep 1 # Wait before retrying
end
end
# Usage: Retry an API call until it succeeds
retry_until do
api_response = fetch_data_from_api
OpenStruct.new(success?: api_response.ok?) # Mock "success?" check
end
Callbacks and Event Handling
Procs/lambdas are ideal for callbacks. For example, in GUI frameworks or web apps, you might pass a proc to run when a button is clicked:
class Button
def initialize(&on_click)
@on_click = on_click # Store callback as a proc
end
def click
@on_click.call if @on_click # Trigger callback on click
end
end
# Create a button with a callback
submit_button = Button.new { puts "Form submitted!" }
submit_button.click # Output: "Form submitted!"
6. Advanced Topics
Passing Blocks to Methods Explicitly (&block)
By default, blocks are passed implicitly to methods. To treat a block as a proc (e.g., to store or pass it elsewhere), use the & operator to convert it to a proc parameter:
def capture_block(&block) # & converts block to a proc
@saved_block = block # Store the proc for later use
end
capture_block { puts "Hello from the saved block!" }
@saved_block.call # Output: "Hello from the saved block!"
Converting Blocks to Procs and Vice Versa
- Block → Proc: Use
&to convert a block to a proc (as in&blockabove). - Proc → Block: Use
&to convert a proc to a block when passing to a method:
my_proc = proc { |x| x * 3 }
[1, 2, 3].map(&my_proc) # => [3, 6, 9] (proc converted to block for map)
Closures in Ruby
Blocks and procs are closures, meaning they retain access to variables from their lexical scope (the context in which they were defined), even if executed outside that scope.
Example: Lexical scoping with a closure
def make_counter
count = 0
proc { count += 1 } # Closure captures "count"
end
counter = make_counter
puts counter.call # 1 (count = 0 + 1)
puts counter.call # 2 (count = 1 + 1)
puts counter.call # 3 (count = 2 + 1)
Here, the proc “remembers” the count variable from make_counter’s scope, even after make_counter has finished executing.
Proc Composition
Procs can be composed to build complex logic from simple functions, a staple of functional programming:
add = proc { |x| x + 2 }
multiply = proc { |x| x * 3 }
# Compose: multiply after add (i.e., multiply(add(x)))
add_then_multiply = proc { |x| multiply.call(add.call(x)) }
add_then_multiply.call(5) # (5 + 2) * 3 = 21
7. Conclusion
Blocks and procs are foundational to Ruby’s design philosophy of “developer happiness.” Blocks simplify iteration and code reuse, while procs (and lambdas) extend this by making code chunks first-class objects. By mastering these tools, you’ll write more idiomatic, flexible, and maintainable Ruby code—whether you’re building web apps, scripts, or libraries.
Remember: blocks are for short, inline logic; procs for reusable, object-like code chunks; and lambdas for function-like behavior with strict argument and return rules.
8. References
- Ruby Documentation:
Proc - Ruby Guides: Blocks, Procs, and Lambdas
- “Programming Ruby: The Pragmatic Programmer’s Guide” (Dave Thomas, Chad Fowler, Andy Hunt)
- Ruby Monk: Blocks and Procs
- Stack Overflow: What’s the difference between a block, a proc, and a lambda in Ruby?