CrackedRuby CrackedRuby

Overview

Function composition is a mathematical and programming concept where two or more functions combine to produce a new function. In mathematical notation, composing functions f and g creates a new function (f ∘ g)(x) = f(g(x)), where g executes first and its result feeds into f.

This technique originates from mathematical function theory but applies directly to software development. Rather than nesting function calls or storing intermediate results in variables, composition creates a pipeline where data flows through a series of transformations.

# Without composition - nested calls
result = uppercase(trim(input))

# With composition - creating a new function
process = uppercase ∘ trim
result = process.call(input)

Function composition supports building complex operations from simple, reusable components. Each function performs one specific transformation, and composition chains these transformations into complete workflows. This approach reduces coupling between components since each function operates independently without knowledge of the others in the chain.

The concept appears throughout software development: Unix pipes compose command-line tools, middleware stacks in web frameworks compose request handlers, and data processing pipelines compose transformation steps. Understanding composition provides a foundation for recognizing and applying these patterns across different contexts.

Key Principles

Function composition operates on the principle that functions are first-class values that can be passed, returned, and combined like any other data type. A function accepts input, produces output, and causes no side effects beyond returning its result. This property enables safe composition since each function's behavior remains predictable regardless of its position in a composition chain.

Mathematical Foundation

The composition of functions f and g, written as f ∘ g, creates a new function h where h(x) = f(g(x)). Composition is associative: (f ∘ g) ∘ h equals f ∘ (g ∘ h). This property means the grouping of compositions doesn't affect the result, allowing flexible construction of complex function chains.

# Demonstrating associativity
add_ten = ->(x) { x + 10 }
multiply_two = ->(x) { x * 2 }
square = ->(x) { x ** 2 }

# These produce identical results
result1 = add_ten.call(multiply_two.call(square.call(3)))
result2 = add_ten.call(multiply_two.call(9))
# => 28

Data Flow Direction

Composition establishes a data flow direction. In traditional mathematical notation (f ∘ g), execution proceeds right to left: g executes first, then f receives g's output. Some programming languages and APIs reverse this to left-to-right execution for improved readability, matching the natural reading order of most written languages.

# Right-to-left (mathematical)
composed = f ∘ g  # g executes, then f

# Left-to-right (pipeline style)  
composed = g >> f  # g executes, then f

Purity and Referential Transparency

Pure functions form ideal composition building blocks. A pure function always returns the same output for the same input and produces no observable side effects. This predictability means composed functions behave consistently and can be reasoned about locally without considering the entire system state.

# Pure function - composable
double = ->(x) { x * 2 }

# Impure function - problematic for composition
counter = 0
increment_and_double = ->(x) { counter += 1; x * 2 }  # Side effect

Type Compatibility

Successful composition requires type compatibility between functions. The output type of one function must match the input type of the next function in the chain. Type mismatches break composition and generate runtime errors in dynamically typed languages or compilation errors in statically typed languages.

Identity and Unit

The identity function serves as the composition identity element. Composing any function with the identity function returns the original function unchanged: f ∘ identity = identity ∘ f = f. This property provides a neutral element for composition chains, similar to how zero functions in addition or one in multiplication.

identity = ->(x) { x }
double = ->(x) { x * 2 }

# Composing with identity returns the original function
composed = identity >> double
composed.call(5)  # => 10 (same as double.call(5))

Ruby Implementation

Ruby provides multiple mechanisms for function composition through Proc, lambda, and Method objects. Each offers different characteristics and composition capabilities.

Proc and Lambda Composition

Ruby 2.6 introduced composition operators for Proc objects. The << operator composes right-to-left (f << g applies g then f), while >> composes left-to-right (f >> g applies f then g).

add_five = ->(x) { x + 5 }
multiply_three = ->(x) { x * 3 }
square = ->(x) { x ** 2 }

# Right-to-left composition (mathematical notation)
# Read: multiply_three after add_five
process_rtl = multiply_three << add_five
process_rtl.call(10)  # => (10 + 5) * 3 = 45

# Left-to-right composition (pipeline notation)  
# Read: add_five then multiply_three
process_ltr = add_five >> multiply_three
process_ltr.call(10)  # => (10 + 5) * 3 = 45

# Chaining multiple functions
complex = add_five >> multiply_three >> square
complex.call(2)  # => ((2 + 5) * 3) ** 2 = 441

Method Object Composition

Method objects obtained through the method method support composition operators after conversion to Proc using to_proc:

class Calculator
  def add_ten(x)
    x + 10
  end
  
  def double(x)
    x * 2
  end
  
  def format(x)
    "Result: #{x}"
  end
end

calc = Calculator.new
composed = calc.method(:add_ten).to_proc >> 
           calc.method(:double).to_proc >> 
           calc.method(:format).to_proc

composed.call(5)  # => "Result: 30"

Building a Composition Helper

Ruby doesn't provide built-in composition for methods without conversion to Proc. A helper method can simplify composition of multiple functions:

def compose(*functions)
  functions.reduce do |f, g|
    ->(x) { f.call(g.call(x)) }
  end
end

add_one = ->(x) { x + 1 }
double = ->(x) { x * 2 }
negate = ->(x) { -x }

composed = compose(negate, double, add_one)
composed.call(5)  # => -((5 + 1) * 2) = -12

Partial Application with Composition

Combining partial application with composition creates configurable transformation pipelines:

multiply_by = ->(factor) { ->(x) { x * factor } }
add_value = ->(value) { ->(x) { x + value } }
clamp = ->(min, max) { ->(x) { [[x, min].max, max].min } }

# Create specialized transformations
double = multiply_by.call(2)
add_ten = add_value.call(10)
limit = clamp.call(0, 100)

# Compose into a processing pipeline
normalize = add_ten >> double >> limit
normalize.call(40)  # => 100 (clamped)
normalize.call(-5)  # => 10

Handling Multiple Arguments

Standard composition works with single-argument functions. Multi-argument functions require currying or parameter binding:

# Using curry for multi-argument functions
power = ->(base, exp) { base ** exp }
curried_power = power.curry

square = curried_power.call(2)  # Partially applied with base=2
cube = curried_power.call(3)

# Compose with curried functions
add_five = ->(x) { x + 5 }
composed = add_five >> square
composed.call(3)  # => 64 (8 ** 2)

Composition with Symbol#to_proc

Ruby's Symbol#to_proc enables method chaining that resembles composition:

words = ["hello", "world", "ruby"]

# Using Symbol#to_proc in a pipeline style
result = words
  .map(&:upcase)
  .map(&:reverse)
  .map { |w| w + "!" }
# => ["OLLEH!", "DLROW!", "YBUR!"]

# Creating reusable transformations
upcase = :upcase.to_proc
reverse = :reverse.to_proc

process_word = ->(w) { reverse.call(upcase.call(w)) + "!" }
words.map { |w| process_word.call(w) }

Common Patterns

Pipeline Pattern

The pipeline pattern chains transformations that each modify data and pass results forward. This pattern appears frequently in data processing and ETL operations:

# Data processing pipeline
parse_csv = ->(text) { text.split("\n").map { |line| line.split(",") } }
filter_valid = ->(rows) { rows.reject { |row| row.any?(&:empty?) } }
extract_numbers = ->(rows) { rows.map { |row| row.map(&:to_i) } }
sum_rows = ->(rows) { rows.map { |row| row.sum } }
calculate_total = ->(sums) { sums.sum }

pipeline = parse_csv >> filter_valid >> extract_numbers >> 
           sum_rows >> calculate_total

data = "1,2,3\n4,5,6\n7,,9\n10,11,12"
pipeline.call(data)  # => 69 (sum of valid rows)

Validation Chain

Composing validation functions creates comprehensive validation logic where each validator checks specific criteria:

# Individual validators return the value or raise an error
not_empty = ->(val) { val.empty? ? (raise "Empty value") : val }
min_length = ->(min) { ->(val) { val.length >= min ? val : (raise "Too short") } }
max_length = ->(max) { ->(val) { val.length <= max ? val : (raise "Too long") } }
alphanumeric = ->(val) { val.match?(/\A[a-zA-Z0-9]+\z/) ? val : (raise "Invalid chars") }

# Compose validators
validate_username = not_empty >> 
                    min_length.call(3) >> 
                    max_length.call(20) >> 
                    alphanumeric

begin
  validate_username.call("user123")  # => "user123" (valid)
  validate_username.call("ab")  # Raises "Too short"
rescue => e
  puts e.message
end

Transformation Stack

Building stacks of transformations that apply in sequence, commonly used in text processing, image manipulation, or request/response handling:

# Text transformation stack
remove_whitespace = ->(text) { text.gsub(/\s+/, ' ').strip }
normalize_case = ->(text) { text.downcase }
remove_punctuation = ->(text) { text.gsub(/[[:punct:]]/, '') }
remove_articles = ->(text) { text.gsub(/\b(a|an|the)\b/i, '').gsub(/\s+/, ' ').strip }
split_words = ->(text) { text.split(' ') }

text_processor = remove_whitespace >> 
                 normalize_case >> 
                 remove_punctuation >> 
                 remove_articles >>
                 split_words

input = "  The Quick, Brown FOX! "
text_processor.call(input)  # => ["quick", "brown", "fox"]

Strategy Selection Pattern

Composing functions based on runtime conditions creates flexible processing strategies:

# Different processing strategies
aggressive_clean = ->(text) { text.gsub(/[^a-z0-9]/i, '').downcase }
gentle_clean = ->(text) { text.gsub(/[^\w\s-]/, '').strip.downcase }
format_snake = ->(text) { text.gsub(/\s+/, '_') }
format_kebab = ->(text) { text.gsub(/\s+/, '-') }

def build_processor(cleaning: :gentle, format: :snake)
  cleaner = cleaning == :aggressive ? aggressive_clean : gentle_clean
  formatter = format == :kebab ? format_kebab : format_snake
  cleaner >> formatter
end

# Create different processors for different contexts
slug_processor = build_processor(cleaning: :aggressive, format: :kebab)
tag_processor = build_processor(cleaning: :gentle, format: :snake)

slug_processor.call("Hello, World!")  # => "hello-world"
tag_processor.call("Ruby on Rails")   # => "ruby_on_rails"

Decorator Pattern with Composition

Composing decorator functions adds behavior to core functionality without modifying the original function:

# Core operation
fetch_data = ->(id) { "data_#{id}" }

# Decorators add behavior
add_logging = ->(f) do
  ->(x) do
    puts "Calling with: #{x}"
    result = f.call(x)
    puts "Result: #{result}"
    result
  end
end

add_caching = ->(f) do
  cache = {}
  ->(x) do
    cache[x] ||= f.call(x)
  end
end

add_error_handling = ->(f) do
  ->(x) do
    begin
      f.call(x)
    rescue => e
      puts "Error: #{e.message}"
      nil
    end
  end
end

# Compose decorators
enhanced_fetch = add_error_handling.call(
  add_caching.call(
    add_logging.call(fetch_data)
  )
)

enhanced_fetch.call(123)  # Logs, caches, and handles errors

Practical Examples

Building a Data Processing Pipeline

Real-world data processing often requires multiple transformation steps. Composition organizes these steps into clear, testable units:

# Define individual transformation steps
parse_json = ->(json_string) do
  require 'json'
  JSON.parse(json_string)
end

extract_users = ->(data) { data['users'] || [] }

filter_active = ->(users) do
  users.select { |u| u['status'] == 'active' }
end

enrich_with_metadata = ->(users) do
  users.map do |u|
    u.merge({
      'processed_at' => Time.now.iso8601,
      'full_name' => "#{u['first_name']} #{u['last_name']}"
    })
  end
end

sort_by_name = ->(users) do
  users.sort_by { |u| u['full_name'] }
end

format_output = ->(users) do
  users.map do |u|
    "#{u['full_name']} (#{u['email']}) - #{u['status']}"
  end.join("\n")
end

# Compose the complete pipeline
user_processor = parse_json >> 
                 extract_users >> 
                 filter_active >> 
                 enrich_with_metadata >> 
                 sort_by_name >> 
                 format_output

json_data = '{"users": [
  {"first_name": "John", "last_name": "Doe", "email": "john@example.com", "status": "active"},
  {"first_name": "Jane", "last_name": "Smith", "email": "jane@example.com", "status": "inactive"},
  {"first_name": "Bob", "last_name": "Johnson", "email": "bob@example.com", "status": "active"}
]}'

puts user_processor.call(json_data)
# => Bob Johnson (bob@example.com) - active
#    John Doe (john@example.com) - active

Request Processing Middleware

Web frameworks use composition to build middleware stacks. Each middleware function handles one aspect of request processing:

# Middleware functions that wrap the next handler
add_request_id = ->(handler) do
  ->(request) do
    request[:id] = SecureRandom.uuid
    handler.call(request)
  end
end

log_request = ->(handler) do
  ->(request) do
    puts "[#{request[:id]}] #{request[:method]} #{request[:path]}"
    result = handler.call(request)
    puts "[#{request[:id]}] Status: #{result[:status]}"
    result
  end
end

authenticate = ->(handler) do
  ->(request) do
    if request[:headers]['Authorization']
      request[:user] = "authenticated_user"
      handler.call(request)
    else
      { status: 401, body: "Unauthorized" }
    end
  end
end

parse_body = ->(handler) do
  ->(request) do
    if request[:body]
      request[:parsed_body] = JSON.parse(request[:body])
    end
    handler.call(request)
  end
end

# Base handler
app = ->(request) do
  { 
    status: 200, 
    body: "Hello #{request[:user]}",
    request_id: request[:id]
  }
end

# Compose middleware stack
middleware_stack = add_request_id.call(
  log_request.call(
    authenticate.call(
      parse_body.call(app)
    )
  )
)

request = {
  method: 'GET',
  path: '/api/users',
  headers: { 'Authorization' => 'Bearer token123' },
  body: '{"key": "value"}'
}

response = middleware_stack.call(request)

Financial Calculation Pipeline

Financial calculations benefit from composition where each function performs one calculation step with clear semantics:

# Define calculation functions
apply_discount = ->(percentage) do
  ->(amount) { amount * (1 - percentage / 100.0) }
end

add_tax = ->(rate) do
  ->(amount) { amount * (1 + rate / 100.0) }
end

apply_shipping = ->(method) do
  shipping_costs = { standard: 10, express: 25, overnight: 50 }
  ->(amount) { amount + shipping_costs[method] }
end

round_to_cents = ->(amount) do
  (amount * 100).round / 100.0
end

format_currency = ->(amount) do
  "$#{'%.2f' % amount}"
end

# Build different pricing calculators
standard_checkout = apply_discount.call(10) >> 
                   add_tax.call(8.5) >> 
                   apply_shipping.call(:standard) >> 
                   round_to_cents >> 
                   format_currency

premium_checkout = apply_discount.call(15) >> 
                  add_tax.call(8.5) >> 
                  apply_shipping.call(:express) >> 
                  round_to_cents >> 
                  format_currency

standard_checkout.call(100)  # => "$107.65"
premium_checkout.call(100)   # => "$117.22"

Content Sanitization and Normalization

Content processing requires multiple sanitization and normalization steps. Composition chains these operations cleanly:

# HTML processing functions
strip_html_tags = ->(html) do
  html.gsub(/<[^>]+>/, '')
end

decode_entities = ->(text) do
  require 'cgi'
  CGI.unescapeHTML(text)
end

normalize_whitespace = ->(text) do
  text.gsub(/\s+/, ' ').strip
end

truncate = ->(max_length) do
  ->(text) do
    if text.length > max_length
      text[0...max_length].rstrip + '...'
    else
      text
    end
  end
end

capitalize_sentences = ->(text) do
  text.gsub(/([.!?]\s+)(\w)/) { |m| "#{$1}#{$2.upcase}" }
       .sub(/\A(\w)/) { |m| m.upcase }
end

# Compose sanitization pipeline
sanitize_html = strip_html_tags >> 
                decode_entities >> 
                normalize_whitespace >> 
                truncate.call(200) >> 
                capitalize_sentences

html_input = "<p>Hello &amp; welcome to our site!   <strong>Enjoy</strong> your stay.</p>"
sanitize_html.call(html_input)
# => "Hello & welcome to our site! Enjoy your stay."

Design Considerations

When to Use Function Composition

Function composition fits scenarios where data flows through a series of transformations. ETL pipelines, request/response processing, data validation, and formatting operations benefit from composition's clarity. The pattern works best when each transformation step has a single responsibility and functions remain pure or have minimal side effects.

Consider composition when building reusable transformation chains. If the same sequence of operations applies in multiple contexts, composing them into a named function reduces duplication. Composition also improves testability since each component function tests independently.

# Without composition - repeated logic
def process_user_name_for_display(name)
  name.strip.downcase.capitalize
end

def process_user_name_for_search(name)
  name.strip.downcase
end

# With composition - reusable components
strip = :strip.to_proc
downcase = :downcase.to_proc  
capitalize = :capitalize.to_proc

for_display = strip >> downcase >> capitalize
for_search = strip >> downcase

for_display.call("  JOHN  ")  # => "John"
for_search.call("  JOHN  ")   # => "john"

Trade-offs

Composition introduces abstraction that can obscure program flow. Deeply nested compositions or long chains make debugging difficult since stack traces show multiple composed functions rather than clear execution paths. Each composition level adds function call overhead, though this cost remains negligible for most applications.

Type safety decreases in dynamically typed languages like Ruby. Composition connects functions based on developer intent, but the runtime doesn't verify that function outputs match subsequent function inputs until execution. Type mismatches surface as runtime errors rather than early feedback.

# Type mismatch caught only at runtime
to_integer = ->(x) { x.to_i }
double = ->(x) { x * 2 }
upcase = ->(x) { x.upcase }  # Expects string

# This composes successfully but fails at runtime
bad_composition = to_integer >> double >> upcase
bad_composition.call("5")  # NoMethodError: undefined method 'upcase' for 10

Composing functions with side effects creates unpredictable behavior. If composed functions modify external state or depend on mutable state, the composition's behavior varies based on execution context and ordering. Pure functions eliminate these issues but aren't always practical for real-world applications.

Alternatives to Composition

Method chaining provides similar benefits with more conventional Ruby style. Rather than composing separate functions, method chaining calls methods sequentially on intermediate results. This approach feels more natural in Ruby and provides better IDE support and error messages.

# Function composition
trim = :strip.to_proc
downcase = :downcase.to_proc
reverse = :reverse.to_proc

process = trim >> downcase >> reverse
process.call("  HELLO  ")  # => "olleh"

# Method chaining
"  HELLO  ".strip.downcase.reverse  # => "olleh"

Explicit intermediate variables offer maximum clarity at the cost of verbosity. Assigning each transformation result to a named variable documents the process explicitly and simplifies debugging since each step can be inspected independently.

# Explicit steps
input = get_data()
validated = validate(input)
normalized = normalize(validated)
enriched = enrich(normalized)
result = format(enriched)

Object-oriented design with command objects encapsulates transformation logic within classes. Each command class implements a common interface, and a coordinator chains commands together. This approach provides better structure for complex transformations requiring state management.

Composition vs Method Chaining

Method chaining reads left-to-right and integrates naturally with Ruby's object-oriented design. Each method call returns an object that supports subsequent method calls. This pattern works well with Ruby's standard library and aligns with developer expectations.

Function composition separates data transformation from data structure. Composed functions work with any compatible data type, while method chaining ties operations to specific classes. Composition provides more flexibility but requires more explicit plumbing.

# Method chaining - tied to String class
text = "hello world"
result = text.upcase.reverse.chars.sort.join
# => "WROLDLEH"

# Function composition - works with any type
upcase = ->(x) { x.upcase }
reverse = ->(x) { x.reverse }  
to_chars = ->(x) { x.chars }
sort = ->(x) { x.sort }
join = ->(x) { x.join }

processor = upcase >> reverse >> to_chars >> sort >> join
processor.call("hello world")  # => "WROLDLEH"

Performance Implications

Each composition level adds function call overhead. For simple transformations on small datasets, this overhead remains negligible. Processing large datasets or performance-critical code paths may see measurable impact from deeply nested compositions.

Composition prevents certain optimizations. Fusing multiple transformation steps into a single pass improves performance by eliminating intermediate allocations and reducing iteration count. Composed functions execute sequentially, creating intermediate results between each step.

# Composed functions - multiple passes
add_ten = ->(arr) { arr.map { |x| x + 10 } }
double = ->(arr) { arr.map { |x| x * 2 } }
filter_even = ->(arr) { arr.select(&:even?) }

composed = add_ten >> double >> filter_even
composed.call([1, 2, 3, 4, 5])  # Three passes over the data

# Fused operation - single pass
def process(arr)
  arr.map { |x| (x + 10) * 2 }.select(&:even?)
end
process([1, 2, 3, 4, 5])  # One pass over the data

Reference

Composition Operators

Operator Direction Execution Order Example
<< Right-to-left g executes first, then f f << g
>> Left-to-right f executes first, then g f >> g

Function Types for Composition

Type Description Composition Suitability
Pure Function No side effects, deterministic output Ideal for composition
Lambda Ruby proc with strict argument checking Full composition support
Proc Ruby proc with flexible arguments Full composition support
Method Object Bound method reference Requires to_proc conversion
Symbol to_proc Method name as callable Works with map, select, etc

Common Composition Patterns

Pattern Structure Use Case
Pipeline f >> g >> h Sequential data transformation
Validation Chain v1 >> v2 >> v3 Multi-step validation
Decorator Stack d1(d2(d3(f))) Adding behavior to functions
Strategy Composition condition ? (a >> b) : (c >> d) Conditional processing paths
Transformation Chain parse >> filter >> transform >> format ETL operations

Type Compatibility Requirements

Situation Requirement Result if Violated
f >> g Output type of f must match input type of g Runtime type error
Curried functions Arity matches at each composition point Argument error
Multi-argument functions Must be curried or partially applied Wrong number of arguments error
Side effects Functions should be pure or controlled Unpredictable behavior

Creating Composed Functions

Method Code Notes
Direct operator f >> g Available for Proc and Lambda
Compose helper compose(f, g, h) Custom implementation needed
Method conversion method(:name).to_proc >> other Converts Method to Proc
Reduce pattern fns.reduce { pipe } Dynamic composition

Performance Characteristics

Aspect Impact Mitigation
Function call overhead Each composition adds call Profile before optimizing
Intermediate allocations Each step may create objects Consider fused operations for hot paths
Stack depth Deep composition increases stack Limit composition depth
Lazy evaluation Ruby eager by default Use Enumerator for lazy chains

Common Gotchas

Issue Cause Solution
Type mismatch at runtime Output type doesn't match next input Document expected types
Side effects causing bugs Composed functions modify state Prefer pure functions
Confusing execution order << vs >> semantics Use >> for left-to-right readability
Debugging difficulty Stack traces show composition Use intermediate variables during debug
Argument count mismatch Multi-arg function in composition Use curry or partial application

Testing Composed Functions

Approach Method Example
Unit test components Test each function independently assert add_ten.call(5) == 15
Integration test composition Test the composed function assert pipeline.call(input) == expected
Property-based testing Verify composition laws test associativity holds
Mock dependencies Stub functions in composition replace fetch with mock