Overview
Ruby's code block system provides a mechanism for passing executable code to methods. The system consists of blocks (anonymous functions attached to method calls), procs (objects that encapsulate blocks), lambdas (strict procs with argument checking), and the yield keyword for executing passed blocks. Ruby implements blocks as closures that capture their lexical environment, maintaining access to variables from their defining scope.
The Proc
class serves as the foundation for Ruby's callable objects. Every block can be converted to a Proc object, while lambdas represent a special subset of procs with stricter semantics. The yield
keyword provides the primary mechanism for executing blocks within methods, while block_given?
checks for block presence.
def greet
yield "Hello" if block_given?
end
greet { |message| puts "#{message}, World!" }
# => "Hello, World!"
proc_object = Proc.new { |x| x * 2 }
lambda_object = lambda { |x| x * 2 }
result = proc_object.call(5)
# => 10
Ruby's block system enables powerful patterns including iterators, callbacks, and domain-specific languages. Blocks capture their lexical scope, creating closures that maintain references to local variables, instance variables, and constants defined in their creation context.
Basic Usage
Method definitions accept blocks implicitly without declaring them in the parameter list. The yield
keyword executes the passed block with optional arguments. Methods should check block_given?
before yielding to avoid LocalJumpError exceptions when no block is provided.
def each_with_index(array)
index = 0
array.each do |element|
yield element, index if block_given?
index += 1
end
end
numbers = [10, 20, 30]
each_with_index(numbers) do |value, idx|
puts "Index #{idx}: #{value}"
end
# Index 0: 10
# Index 1: 20
# Index 2: 30
The Proc.new
constructor creates proc objects from blocks. Procs maintain their creation context and can be stored, passed around, and called multiple times. The call
method executes procs with arguments, while square bracket notation provides syntactic sugar.
multiplier = Proc.new { |x, factor| x * factor }
doubled = multiplier.call(5, 2)
tripled = multiplier[5, 3]
# doubled => 10, tripled => 15
# Procs capture lexical scope
outer_var = "captured"
my_proc = Proc.new { puts outer_var }
my_proc.call
# => "captured"
Lambda creation uses the lambda
keyword or stabby lambda syntax ->
. Lambdas enforce argument count strictly and use method-like return semantics. They raise ArgumentError for incorrect argument counts, unlike regular procs which ignore extra arguments or assign nil to missing ones.
strict_lambda = lambda { |x, y| x + y }
arrow_lambda = ->(x, y) { x + y }
begin
strict_lambda.call(1) # Missing argument
rescue ArgumentError => e
puts "Lambda requires exact arguments: #{e.message}"
end
flexible_proc = Proc.new { |x, y| (x || 0) + (y || 0) }
result = flexible_proc.call(1) # y becomes nil
# => 1
Block syntax uses either curly braces for single-line blocks or do...end
for multi-line blocks. Ruby convention favors braces for functional-style operations and do...end
for procedural operations, though both forms are functionally equivalent.
# Functional style with braces
numbers.select { |n| n.even? }
# Procedural style with do...end
numbers.each do |number|
processed = number * 2
puts "Result: #{processed}"
end
Advanced Usage
Methods can capture blocks explicitly using the &
parameter prefix, converting blocks to proc objects. This technique enables storing blocks for later execution, passing blocks to other methods, or manipulating blocks as first-class objects.
class EventHandler
def initialize(&block)
@callback = block
end
def trigger(event_data)
@callback.call(event_data) if @callback
end
def add_filter(&filter_block)
original_callback = @callback
@callback = lambda do |data|
filtered_data = filter_block.call(data)
original_callback.call(filtered_data) if original_callback
end
end
end
handler = EventHandler.new { |data| puts "Processing: #{data}" }
handler.add_filter { |data| data.upcase }
handler.trigger("hello world")
# => "Processing: HELLO WORLD"
The define_method
class method creates methods dynamically using blocks, enabling metaprogramming patterns and runtime method generation. These dynamically defined methods capture their defining context, creating powerful abstraction mechanisms.
class AttributeAccessor
def self.attr_reader_with_prefix(prefix, *names)
names.each do |name|
define_method("#{prefix}_#{name}") do
instance_variable_get("@#{name}")
end
end
end
def self.attr_writer_with_validation(*names, &validator)
names.each do |name|
define_method("#{name}=") do |value|
if validator.call(value)
instance_variable_set("@#{name}", value)
else
raise ArgumentError, "Invalid value for #{name}: #{value}"
end
end
end
end
end
class User < AttributeAccessor
attr_reader_with_prefix :user, :name, :email
attr_writer_with_validation :age do |value|
value.is_a?(Integer) && value > 0
end
def initialize(name, email, age)
@name, @email = name, email
self.age = age
end
end
Block binding captures the complete lexical environment including local variables, constants, and method definitions. The binding
method returns a Binding object representing the current scope, while instance_eval
and class_eval
execute blocks in different contexts.
class ScopeExplorer
def initialize(value)
@instance_var = value
end
def explore_scopes(&block)
local_var = "method local"
# Execute in current context
puts "Current context:"
yield local_var, @instance_var
# Execute in instance context
puts "Instance context:"
instance_eval(&block)
# Create new binding
new_binding = binding
puts "Captured binding has access to local_var: #{eval('local_var', new_binding)}"
end
end
explorer = ScopeExplorer.new("instance value")
explorer.explore_scopes do |local, instance|
puts "Block sees: local=#{local}, instance=#{instance || @instance_var}"
end
Proc composition enables functional programming patterns by combining multiple procs into processing pipelines. The >>
and <<
operators provide composition methods for chaining transformations.
class ProcPipeline
def initialize(*procs)
@procs = procs
end
def call(input)
@procs.reduce(input) { |result, proc| proc.call(result) }
end
def >>(other_proc)
ProcPipeline.new(*@procs, other_proc)
end
end
# Create transformation pipeline
upcase_proc = ->(str) { str.upcase }
reverse_proc = ->(str) { str.reverse }
exclaim_proc = ->(str) { "#{str}!" }
pipeline = ProcPipeline.new(upcase_proc, reverse_proc, exclaim_proc)
result = pipeline.call("hello")
# => "OLLEH!"
# Chain with >> operator
extended_pipeline = pipeline >> ->(str) { "[#{str}]" }
final_result = extended_pipeline.call("world")
# => "[DLROW!]"
Common Pitfalls
Return behavior differs significantly between blocks, procs, and lambdas. Blocks and procs return from their defining method, potentially causing unexpected control flow, while lambdas return to their immediate caller like regular methods.
def test_returns
puts "Method start"
# Block return - returns from test_returns
[1, 2, 3].each do |n|
return "early exit" if n == 2 # Returns from test_returns!
end
puts "This won't execute"
"method end"
end
def safe_test_returns
puts "Method start"
# Lambda return - returns only from lambda
processor = lambda do |n|
return "lambda exit" if n == 2 # Returns only from lambda
end
[1, 2, 3].each(&processor)
puts "This executes"
"method end"
end
result1 = test_returns # => "early exit"
result2 = safe_test_returns # => "method end"
Variable scope in blocks creates closures that can lead to unexpected behavior when blocks outlive their defining scope or when loop variables are captured incorrectly.
# Problematic: All blocks reference same variable
handlers = []
(1..3).each do |i|
handlers << Proc.new { puts "Handler #{i}" }
end
handlers.each(&:call)
# Handler 3
# Handler 3
# Handler 3
# Solution: Create new scope for each iteration
corrected_handlers = []
(1..3).each do |i|
corrected_handlers << (lambda do |captured_i|
Proc.new { puts "Handler #{captured_i}" }
end).call(i)
end
corrected_handlers.each(&:call)
# Handler 1
# Handler 2
# Handler 3
Argument handling between procs and lambdas creates subtle bugs when transitioning between the two. Procs silently handle argument mismatches while lambdas raise exceptions, leading to inconsistent behavior.
def demonstrate_argument_handling
flexible_proc = Proc.new { |a, b, c| [a, b, c] }
strict_lambda = lambda { |a, b, c| [a, b, c] }
# Proc handles missing arguments gracefully
proc_result = flexible_proc.call(1, 2) # c becomes nil
puts "Proc result: #{proc_result.inspect}" # [1, 2, nil]
# Lambda raises ArgumentError
begin
lambda_result = strict_lambda.call(1, 2)
rescue ArgumentError => e
puts "Lambda error: #{e.message}"
end
# Proc ignores extra arguments
proc_extra = flexible_proc.call(1, 2, 3, 4, 5)
puts "Proc with extras: #{proc_extra.inspect}" # [1, 2, 3]
end
Block_given? checks can introduce race conditions in multi-threaded environments or when blocks are conditionally passed. Always structure code to handle both block and non-block scenarios consistently.
class SafeIterator
def initialize(collection)
@collection = collection
end
# Problematic: Inconsistent behavior
def each_bad
if block_given?
@collection.each { |item| yield item }
else
@collection.each { |item| puts item } # Different behavior!
end
end
# Better: Consistent interface
def each_good(&block)
default_block = block || ->(item) { puts item }
@collection.each(&default_block)
end
# Best: Return enumerator when no block
def each_best
return enum_for(:each_best) unless block_given?
@collection.each { |item| yield item }
end
end
iterator = SafeIterator.new([1, 2, 3])
iterator.each_best.map(&:to_s) # => ["1", "2", "3"]
Reference
Block and Proc Creation Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Proc.new { } |
Block | Proc |
Creates proc from block |
proc { } |
Block | Proc |
Alias for Proc.new |
lambda { } |
Block | Proc |
Creates lambda (strict proc) |
->() { } |
Block | Proc |
Stabby lambda syntax |
&:symbol |
Symbol | Proc |
Symbol to proc conversion |
Proc Instance Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#call(*args) |
Arguments | Object | Executes proc with arguments |
#[](*args) |
Arguments | Object | Alias for call |
#===(*args) |
Arguments | Object | Alias for call (case statement) |
#arity |
None | Integer |
Number of required arguments |
#lambda? |
None | Boolean |
True if proc is lambda |
#source_location |
None | Array |
File and line number where defined |
#binding |
None | Binding |
Binding object of proc's context |
#parameters |
None | Array |
Parameter information |
#curry |
Integer (optional) | Proc |
Returns curried proc |
Yield and Block Methods
Method | Parameters | Returns | Description |
---|---|---|---|
yield(*args) |
Arguments | Object | Executes attached block |
block_given? |
None | Boolean |
True if block was passed |
Kernel#proc |
Block | Proc |
Global proc creation method |
Method Definition with Blocks
Method | Parameters | Returns | Description |
---|---|---|---|
define_method(name, &block) |
Symbol/String, Block | Symbol |
Defines method using block |
define_singleton_method(name, &block) |
Symbol/String, Block | Symbol |
Defines singleton method |
Binding and Evaluation Methods
Method | Parameters | Returns | Description |
---|---|---|---|
binding |
None | Binding |
Current binding object |
eval(string, binding) |
String, Binding | Object | Evaluates string in binding |
instance_eval(&block) |
Block | Object | Evaluates block in receiver context |
class_eval(&block) |
Block | Object | Evaluates block in class context |
module_eval(&block) |
Block | Object | Alias for class_eval |
Argument Handling Comparison
Feature | Proc | Lambda |
---|---|---|
Argument count checking | Flexible | Strict |
Missing arguments | Assigned nil |
Raises ArgumentError |
Extra arguments | Ignored | Raises ArgumentError |
Return behavior | Returns from defining method | Returns to immediate caller |
Created with | Proc.new , proc |
lambda , -> |
Block Syntax Patterns
| Pattern | Syntax | Use Case |
| ------------------- | ------------------ | ---------------- | --------- | ----------------------- |
| Single line | { | x | x \* 2 }
| Functional operations |
| Multi-line | do | x | ...end
| Procedural operations |
| No parameters | { puts "hello" }
| Simple callbacks |
| Multiple parameters | { | a, b, c | ... }
| Complex transformations |
| Splat parameters | { | \*args | ... }
| Variable argument lists |
| Block parameter | { | &block | ... }
| Capturing nested blocks |
Common Block Patterns
| Pattern | Example | Description |
| --------- | ------------------ | ----------- | ------------------ | -------------------- |
| Iterator | array.each { | item | ... }
| Process each element |
| Filter | array.select { | item | condition }
| Filter elements |
| Transform | array.map { | item | transform(item) }
| Transform elements |
| Reduce | array.reduce(0) { | sum, n | sum + n }
| Aggregate values |
| Callback | on_complete { | result | handle(result) }
| Event handling |
| Builder | build_object { | builder | ... }
| Object construction |
Error Types
Error | Condition | Description |
---|---|---|
LocalJumpError |
yield without block |
No block provided to yield |
ArgumentError |
Wrong argument count (lambda) | Lambda called with incorrect arguments |
TypeError |
Non-callable object | Attempting to call non-proc object |
Performance Characteristics
Operation | Performance | Memory |
---|---|---|
Block creation | Very fast | Minimal |
Proc creation | Fast | Small object |
Lambda creation | Fast | Small object |
Block call via yield | Fastest | No allocation |
Proc#call | Fast | Minimal allocation |
Method definition | Moderate | Method object creation |