Overview
Ruby's block to proc conversion is the mechanism by which Ruby transforms blocks (anonymous code snippets) into proc objects that can be stored, passed around, and called like regular objects. This conversion happens automatically in certain contexts and can be explicitly triggered using the &
operator.
Ruby provides several related concepts that work together:
- Blocks: Anonymous code snippets passed to methods using
{}
ordo..end
- Procs: Objects that encapsulate blocks and can be stored in variables
- Method objects: Objects that represent existing methods and can be converted to procs
- The
&
operator: Ruby's mechanism for explicit block/proc conversion
# Block passed to method
[1, 2, 3].map { |x| x * 2 }
# Explicit conversion to proc
double = proc { |x| x * 2 }
[1, 2, 3].map(&double)
# Method to proc conversion
numbers = [1, 2, 3]
strings = numbers.map(&:to_s)
The conversion process allows Ruby to bridge the gap between blocks (which cannot be stored or manipulated as objects) and procs (which are first-class objects). This enables powerful metaprogramming patterns and functional programming techniques.
Basic Usage
&
Operator
The The &
operator is Ruby's primary mechanism for block to proc conversion. When used as a method parameter prefix, it converts the argument to a proc and makes it available as a block to the method:
def call_block(&block)
block.call("Hello")
end
# Convert proc to block
my_proc = proc { |msg| puts msg.upcase }
call_block(&my_proc)
# => "HELLO"
When used in method calls, &
converts objects to blocks by calling their to_proc
method:
numbers = [1, 2, 3, 4, 5]
strings = numbers.map(&:to_s)
# Equivalent to: numbers.map { |n| n.to_s }
# Works with any object that responds to to_proc
class Doubler
def to_proc
proc { |x| x * 2 }
end
end
doubler = Doubler.new
result = numbers.map(&doubler)
# => [2, 4, 6, 8, 10]
Symbol to Proc Conversion
Ruby's Symbol#to_proc
is one of the most commonly used conversions:
words = ["hello", "world", "ruby"]
# These are equivalent:
words.map(&:upcase)
words.map { |word| word.upcase }
# Symbol to_proc also works with method arguments
numbers = ["1", "2", "3"]
integers = numbers.map(&:to_i)
# => [1, 2, 3]
# And with multiple objects
people = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 }
]
names = people.map(&:name) # Won't work - symbols don't call hash methods
Proc Creation and Conversion
Ruby provides several ways to create procs that participate in the conversion system:
# Using proc keyword
square = proc { |x| x ** 2 }
[1, 2, 3].map(&square)
# Using Proc.new
cube = Proc.new { |x| x ** 3 }
[1, 2, 3].map(&cube)
# Using lambda
add_one = lambda { |x| x + 1 }
[1, 2, 3].map(&add_one)
# Method to proc conversion
def multiply_by_ten(x)
x * 10
end
multiplier = method(:multiply_by_ten)
[1, 2, 3].map(&multiplier)
# => [10, 20, 30]
Advanced Usage
to_proc
Implementations
Custom Creating custom classes that respond to to_proc
enables sophisticated block conversion patterns:
class PropertyExtractor
def initialize(property)
@property = property
end
def to_proc
proc { |obj| obj.send(@property) }
end
end
class Person
attr_reader :name, :age
def initialize(name, age)
@name, @age = name, age
end
end
people = [Person.new("Alice", 30), Person.new("Bob", 25)]
names = people.map(&PropertyExtractor.new(:name))
# => ["Alice", "Bob"]
More complex custom converters can implement sophisticated logic:
class ConditionalProcessor
def initialize(condition, true_action, false_action)
@condition = condition
@true_action = true_action
@false_action = false_action
end
def to_proc
proc do |item|
if @condition.call(item)
@true_action.call(item)
else
@false_action.call(item)
end
end
end
end
numbers = [1, 2, 3, 4, 5, 6]
processor = ConditionalProcessor.new(
proc { |n| n.even? },
proc { |n| n * 2 },
proc { |n| n + 1 }
)
result = numbers.map(&processor)
# => [2, 4, 4, 8, 6, 12] (odds get +1, evens get *2)
Method Object Manipulation
Ruby's Method
objects provide powerful conversion capabilities:
class Calculator
def add(a, b)
a + b
end
def multiply(a, b)
a * b
end
end
calc = Calculator.new
add_method = calc.method(:add)
multiply_method = calc.method(:multiply)
# Convert to procs and use with curry
add_5 = add_method.to_proc.curry[5]
pairs = [[1, 2], [3, 4], [5, 6]]
sums = pairs.map { |pair| add_5.call(pair.sum) }
# Combine method objects
operations = [add_method, multiply_method]
operations_as_procs = operations.map(&:to_proc)
# Use in functional programming patterns
def apply_operation(operation, a, b)
operation.call(a, b)
end
result = apply_operation(add_method.to_proc, 10, 20)
# => 30
Block Parameter Conversion Patterns
Advanced patterns emerge when combining block parameter conversion with metaprogramming:
class FlexibleMapper
def self.create_mapper(&converter)
proc do |collection|
collection.map(&converter)
end
end
end
# Create specialized mappers
string_mapper = FlexibleMapper.create_mapper(&:to_s)
upcase_mapper = FlexibleMapper.create_mapper(&:upcase)
numbers = [1, 2, 3]
words = ["hello", "world"]
string_results = string_mapper.call(numbers)
# => ["1", "2", "3"]
upcase_results = upcase_mapper.call(words)
# => ["HELLO", "WORLD"]
Common Pitfalls
Symbol to Proc Limitations
Symbol to proc conversion has several limitations that catch developers:
# Won't work - symbols can't call methods with arguments
numbers = ["1", "2", "3"]
integers = numbers.map(&:to_i(10)) # SyntaxError
# Must use explicit block
integers = numbers.map { |n| n.to_i(10) }
# Won't work with hash access
data = [{ a: 1 }, { a: 2 }]
values = data.map(&:a) # NoMethodError
# Must use explicit hash access
values = data.map { |h| h[:a] }
# Or create a custom method
class Hash
def a
self[:a]
end
end
# Now data.map(&:a) would work, but modifies core classes
Block vs Proc Argument Handling
Blocks and procs handle arguments differently, causing subtle bugs:
def test_block
[1, 2, 3].each { |x, y| puts "#{x}-#{y}" }
end
def test_proc
my_proc = proc { |x, y| puts "#{x}-#{y}" }
[1, 2, 3].each(&my_proc)
end
test_block
# => "1-"
# => "2-"
# => "3-"
test_proc
# => "1-" (proc ignores missing second argument)
# => "2-"
# => "3-"
# Lambda is stricter
strict_proc = lambda { |x, y| puts "#{x}-#{y}" }
[1, 2, 3].each(&strict_proc)
# => ArgumentError: wrong number of arguments (given 1, expected 2)
Conversion Context Confusion
The &
operator behaves differently in different contexts:
# As method parameter - converts argument to proc
def accepts_block(&block)
block.call
end
my_proc = proc { "Hello" }
accepts_block(&my_proc) # Converts proc to block
# In method call - converts object to block via to_proc
[1, 2, 3].map(&:to_s) # Calls Symbol#to_proc
# This distinction matters for debugging
class MyClass
def to_proc
puts "Converting to proc"
proc { |x| x }
end
end
obj = MyClass.new
# This calls to_proc
[1, 2, 3].map(&obj)
# => "Converting to proc"
# This doesn't call to_proc (obj is already an object, not convertible to block)
def test(&block)
block.call(1)
end
# test(&obj) # This would call to_proc and convert result to block
Performance and Memory Implications
Block to proc conversion has performance costs:
# Efficient - no conversion
numbers = (1..1000000).to_a
result1 = numbers.map { |x| x.to_s }
# Less efficient - creates proc object on each iteration
result2 = numbers.map(&:to_s) # Symbol#to_proc called once, but still object creation
# Most efficient way when reusing
to_string_proc = :to_s.to_proc
result3 = numbers.map(&to_string_proc)
# Memory allocation comparison shows the cost
# Creating many short-lived proc objects increases GC pressure
def benchmark_conversion(iterations)
numbers = [1, 2, 3]
# Direct block
iterations.times do
numbers.map { |x| x.to_s }
end
# Symbol conversion
iterations.times do
numbers.map(&:to_s)
end
# Pre-converted proc
converter = :to_s.to_proc
iterations.times do
numbers.map(&converter)
end
end
Reference
Block to Proc Conversion Methods
Method | Usage | Purpose | Returns |
---|---|---|---|
& (parameter) |
def method(&block) |
Converts passed block to proc parameter | Proc object |
& (argument) |
array.map(&obj) |
Calls obj.to_proc and converts result to block |
Calls to_proc |
proc { } |
proc { |x| x * 2 } |
Creates new proc object from block | Proc object |
Proc.new |
Proc.new { |x| x * 2 } |
Creates new proc object from block | Proc object |
lambda { } |
lambda { |x| x * 2 } |
Creates lambda (strict proc) from block | Proc object (lambda) |
Method#to_proc |
method(:name).to_proc |
Converts method object to proc | Proc object |
Symbol#to_proc |
:method_name.to_proc |
Creates proc that calls method on argument | Proc object |
to_proc
Implementations
Common Class | Implementation | Example Usage |
---|---|---|
Symbol |
Calls method on argument | [1,2,3].map(&:to_s) |
Method |
Calls the method with arguments | [1,2,3].map(&method(:puts)) |
Proc |
Returns self | proc.to_proc |
Custom classes | User-defined conversion | collection.map(&custom_converter) |
Argument Handling Differences
Type | Arity Checking | Missing Arguments | Extra Arguments |
---|---|---|---|
Block | Lenient | Assigns nil |
Ignores extras |
Proc | Lenient | Assigns nil |
Ignores extras |
Lambda | Strict | Raises ArgumentError |
Raises ArgumentError |
Method | Strict | Raises ArgumentError |
Raises ArgumentError |
Conversion Decision Matrix
Context | Input Type | Conversion Process | Result |
---|---|---|---|
Method parameter &block |
Block | Direct capture | Proc object |
Method parameter &block |
Proc | Proc converted to block | Proc object |
Method call &obj |
Object with to_proc |
Calls to_proc , result becomes block |
Block execution |
Method call &proc |
Proc object | Proc converted to block | Block execution |
Method call &method |
Method object | Method's to_proc called |
Block execution |
Error Conditions
Scenario | Error Type | Solution |
---|---|---|
&obj where obj lacks to_proc |
NoMethodError |
Implement to_proc method |
Lambda with wrong argument count | ArgumentError |
Match expected argument count |
Converting non-callable object | TypeError |
Ensure object responds to call |
Block expected but none given | LocalJumpError |
Provide block or handle with block_given? |