CrackedRuby logo

CrackedRuby

Currying and Partial Application

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