Programming Techniques

Higher-Order Functions

Functions that take other functions as arguments or return them, enabling powerful abstraction.

Overview

A higher-order function either takes one or more functions as arguments, returns a function, or both. They are the backbone of functional composition, enabling behaviour to be parameterised and combined without subclassing or inheritance.

Origin

Higher-order functions are a direct consequence of the lambda calculus (Church, 1930s). Languages like LISP, APL, and ML embraced them early. They entered mainstream programming through JavaScript, Ruby, and Python, and are now present in virtually every modern language.

Examples

Map, filter, and reduce as higher-order functions

const orders = [
  { id: 1, total: 120, status: 'completed' },
  { id: 2, total: 45,  status: 'pending' },
  { id: 3, total: 300, status: 'completed' },
]

const totalRevenue = orders
  .filter(o => o.status === 'completed')
  .map(o => o.total)
  .reduce((sum, total) => sum + total, 0)

// => 420

Function composition and pipelines in Ruby

double  = ->(x) { x * 2 }
add_ten = ->(x) { x + 10 }
square  = ->(x) { x * x }

# Ruby 2.6+ compose operator
pipeline = double >> add_ten >> square

pipeline.call(3)   # => ((3 * 2) + 10)^2 = 256

# Building a configurable validator
def build_validator(*rules)
  ->(value) { rules.all? { |rule| rule.call(value) } }
end

not_blank    = ->(v) { !v.nil? && !v.empty? }
min_length   = ->(n) { ->(v) { v.length >= n } }
no_spaces    = ->(v) { !v.include?(' ') }

valid_username = build_validator(not_blank, min_length.call(3), no_spaces)
valid_username.call('jarred')  # => true
valid_username.call('j r')     # => false

Use Cases

  • 01Data transformation pipelines (ETL, report generation)
  • 02Middleware and plugin systems that wrap behaviour without modifying originals
  • 03Event callbacks and promise chaining
  • 04Building configurable validators, formatters, or strategies without subclassing
  • 05Lazy sequences and generators that apply transformations on demand

When Not to Use

  • //When chaining many anonymous functions makes the call stack opaque in error traces, named functions improve debugging
  • //When the team is primarily procedural and the indirection adds confusion rather than clarity
  • //For simple one-off transformations where a direct loop is more readable

Technical Notes

  • Prefer named functions over anonymous arrow functions in long pipelines, stack traces show the function name
  • In Ruby, Symbol#to_proc (&:method) is shorthand for a common pattern: array.map(&:upcase) vs array.map { |s| s.upcase }
  • Functions returned from other functions capture the enclosing scope as closures, be aware of what they hold onto
  • Higher-order functions enable the Strategy, Decorator, and Command design patterns without class hierarchies