CrackedRuby logo

CrackedRuby

Block to Proc Conversion

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 {} or do..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

The & Operator

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

Custom to_proc Implementations

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

Common to_proc Implementations

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?