Overview
Currying and partial application are functional programming techniques that transform how functions handle multiple arguments. In Ruby, these concepts are implemented primarily through the Proc
and Method
classes, allowing developers to create more flexible and reusable code.
Currying transforms a function that takes multiple arguments into a series of functions that each take a single argument. For example, a function f(a, b, c)
becomes f(a)(b)(c)
.
Partial application creates a new function by fixing some arguments of an existing function, leaving the remaining arguments to be provided later.
Ruby provides built-in support for currying through Proc#curry
and partial application through Proc#call
with fewer arguments than expected. These techniques enable powerful functional programming patterns and can lead to more modular, composable code.
# Basic currying example
add = proc { |x, y| x + y }
curried_add = add.curry
increment = curried_add.call(1)
result = increment.call(5) # => 6
# Partial application example
multiply = proc { |x, y, z| x * y * z }
double = multiply.curry.call(2)
double_by_three = double.call(3)
result = double_by_three.call(4) # => 24
Basic Usage
Creating Curried Functions
Ruby's Proc#curry
method transforms a multi-argument proc into a curried version. The curried proc returns another proc when called with fewer arguments than expected, and executes when all arguments are provided.
# Define a proc that adds three numbers
add_three = proc { |a, b, c| a + b + c }
# Convert to curried version
curried_add = add_three.curry
# Call with one argument - returns a proc
step_one = curried_add.call(10)
step_two = step_one.call(20)
result = step_two.call(30) # => 60
# Can also chain calls
result = curried_add.call(10).call(20).call(30) # => 60
# Or provide multiple arguments at once
result = curried_add.call(10, 20).call(30) # => 60
Working with Method Objects
Ruby methods can be converted to procs and then curried, enabling the same functionality for instance and class methods.
class Calculator
def multiply(a, b, c)
a * b * c
end
end
calc = Calculator.new
multiply_method = calc.method(:multiply)
curried_multiply = multiply_method.to_proc.curry
# Create specialized multiplication functions
times_two = curried_multiply.call(2)
times_two_three = times_two.call(3)
result = times_two_three.call(4) # => 24
Partial Application Patterns
Partial application creates specialized versions of functions by fixing some arguments. This is particularly useful for creating domain-specific functions from general-purpose ones.
# General string formatting function
format_string = proc { |template, value1, value2|
template % [value1, value2]
}
# Create specialized formatters
error_formatter = format_string.curry.call("ERROR: %s - %s")
info_formatter = format_string.curry.call("INFO: %s - %s")
# Use specialized formatters
error_msg = error_formatter.call("Database", "Connection failed")
info_msg = info_formatter.call("Server", "Started successfully")
Advanced Usage
Currying with Variable Arguments
Ruby's currying mechanism can handle procs with variable argument counts by specifying the arity when calling curry
.
# Proc with variable arguments
sum_all = proc { |*args| args.sum }
# Curry with specific arity
curried_sum = sum_all.curry(3)
result = curried_sum.call(1).call(2).call(3) # => 6
# Different arity creates different behavior
curried_sum_5 = sum_all.curry(5)
partial = curried_sum_5.call(1, 2, 3)
result = partial.call(4, 5) # => 15
Function Composition with Curried Functions
Curried functions compose naturally, enabling powerful functional programming patterns.
# Define basic curried operations
add = proc { |x, y| x + y }.curry
multiply = proc { |x, y| x * y }.curry
power = proc { |base, exp| base ** exp }.curry
# Create composed operations
add_ten = add.call(10)
double = multiply.call(2)
square = power.call(2)
# Compose functions using method chaining or explicit composition
transform = proc { |x| square.call(double.call(add_ten.call(x))) }
result = transform.call(5) # => ((5 + 10) * 2) ** 2 = 900
# Alternative composition using array and reduce
operations = [add_ten, double, square]
result = operations.reduce(5) { |value, op| op.call(value) } # => 900
Building DSLs with Curried Functions
Curried functions excel at creating domain-specific languages and fluent interfaces.
# HTML builder using curried functions
tag = proc { |name, attrs, content|
attr_string = attrs.map { |k, v| "#{k}='#{v}'" }.join(' ')
attr_part = attr_string.empty? ? '' : " #{attr_string}"
"<#{name}#{attr_part}>#{content}</#{name}>"
}.curry
# Create specialized tag builders
div = tag.call('div')
span = tag.call('span')
p = tag.call('p')
# Create styled components
red_div = div.call({ class: 'red', style: 'color: red' })
bold_span = span.call({ style: 'font-weight: bold' })
# Generate HTML
html = red_div.call(
bold_span.call('Important: ') +
'This is a message'
)
# => "<div class='red' style='color: red'><span style='font-weight: bold'>Important: </span>This is a message</div>"
Currying for Configuration Objects
Curried functions provide an elegant way to handle complex configuration scenarios.
# Database connection builder
connect = proc { |adapter, host, port, database, username, password|
{
adapter: adapter,
host: host,
port: port,
database: database,
username: username,
password: password
}
}.curry
# Create environment-specific builders
production_mysql = connect.call('mysql', 'prod-server', 3306)
development_postgres = connect.call('postgres', 'localhost', 5432)
# Create database-specific builders
prod_users_db = production_mysql.call('users_db')
dev_test_db = development_postgres.call('test_db')
# Generate final configurations
prod_config = prod_users_db.call('app_user', 'secret_password')
dev_config = dev_test_db.call('dev_user', 'dev_password')
Common Pitfalls
Arity Mismatches and Debugging
Curried functions can be confusing when argument counts don't match expectations. Ruby's arity checking provides clues but requires careful attention.
add_three = proc { |a, b, c| a + b + c }.curry
# This creates a partial application, not an error
partial = add_three.call(1, 2, 3, 4) # Extra argument ignored
result = partial # This is still a proc, not 6!
# Correct usage requires matching arity
result = add_three.call(1, 2, 3) # => 6
# Check if a curried proc is fully applied
def fully_applied?(curried_proc)
curried_proc.arity == 0
end
partial = add_three.call(1, 2)
puts fully_applied?(partial) # => false
complete = partial.call(3)
puts fully_applied?(complete) # => true (but this is now the result, not a proc)
Performance Implications
Currying creates additional proc objects and call overhead, which can impact performance in tight loops or frequently called code.
require 'benchmark'
# Direct function call
add_direct = proc { |a, b, c| a + b + c }
# Curried version
add_curried = add_direct.curry
n = 100_000
Benchmark.bm do |bm|
bm.report("direct: ") do
n.times { add_direct.call(1, 2, 3) }
end
bm.report("curried: ") do
n.times { add_curried.call(1).call(2).call(3) }
end
bm.report("partial: ") do
partial = add_curried.call(1, 2)
n.times { partial.call(3) }
end
end
# Results show curried calls are slower due to additional proc creation
Memory Leaks with Closures
Curried functions that capture variables in their closure can create memory leaks if those variables reference large objects.
class DataProcessor
def initialize(large_dataset)
@data = large_dataset # Large array or hash
end
def create_processor
# This closure captures self, keeping @data in memory
proc { |filter, transform, output|
filtered = @data.select(&filter)
transformed = filtered.map(&transform)
output.call(transformed)
}.curry
end
end
# Each curried processor keeps the entire dataset in memory
processor = DataProcessor.new(Array.new(1_000_000) { rand })
filter_proc = processor.create_processor
# Better: extract only needed data
class DataProcessor
def initialize(large_dataset)
@data = large_dataset
end
def create_processor(sample_size = 1000)
sample_data = @data.sample(sample_size) # Extract smaller subset
proc { |filter, transform, output|
filtered = sample_data.select(&filter)
transformed = filtered.map(&transform)
output.call(transformed)
}.curry
end
end
Unexpected Evaluation Timing
Curried functions defer execution until all arguments are provided, which can cause unexpected behavior with side effects or time-sensitive operations.
logger = proc { |level, message|
puts "[#{Time.now}] #{level}: #{message}"
}.curry
# Time is captured when the proc is defined, not when called
error_logger = logger.call('ERROR')
sleep(2)
error_logger.call('Something went wrong') # Uses original timestamp!
# Solution: Move time-sensitive operations inside the final execution
better_logger = proc { |level, message|
puts "[#{Time.now}] #{level}: #{message}" # Time captured here
}.curry
error_logger = better_logger.call('ERROR')
sleep(2)
error_logger.call('Something went wrong') # Correct timestamp
Reference
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
Proc#curry |
arity=nil |
Proc |
Returns curried version of proc |
Proc#curry(n) |
n (Integer) |
Proc |
Returns curried proc expecting n arguments |
Method#curry |
arity=nil |
Proc |
Converts method to curried proc |
Proc#arity |
none | Integer |
Returns number of required arguments |
Proc#call(*args) |
*args |
varies | Calls proc with arguments |
Arity Behavior
Original Arity | Curried Behavior | Example |
---|---|---|
Fixed (2) | Requires exactly 2 arguments | proc { |a, b| a + b }.curry |
Variable (*args) | Requires explicit arity | proc { |*args| args.sum }.curry(3) |
Optional (a, b=nil) | Based on required arguments | proc { |a, b=10| a + b }.curry |
Mixed (a, *rest, b) | Complex arity calculation | proc { |a, *r, b| [a, r, b] }.curry |
Currying Patterns
Pattern | Use Case | Example |
---|---|---|
Sequential Application | Building specialized functions | add.curry.call(10).call(20) |
Partial Application | Configuration builders | connect.curry.call('mysql') |
Function Composition | Data transformation pipelines | transform.curry.call(filter).call(mapper) |
DSL Construction | Fluent interfaces | html.curry.call('div').call(attrs) |
Performance Characteristics
Operation | Time Complexity | Space Complexity | Notes |
---|---|---|---|
proc.curry |
O(1) | O(1) | Creates wrapper proc |
Partial application | O(1) | O(k) | Stores k captured arguments |
Full application | O(n) | O(1) | Where n is original proc complexity |
Arity checking | O(1) | O(1) | Built into Ruby VM |
Common Error Types
Error | Cause | Solution |
---|---|---|
ArgumentError |
Wrong argument count to original proc | Check proc arity before currying |
NoMethodError |
Calling curry on non-proc | Convert method to proc first |
TypeError |
Passing non-callable to curried proc | Validate argument types |
Performance degradation | Excessive currying in hot paths | Profile and optimize critical sections |