Overview
First-class functions represent a programming language feature where functions are treated as first-class citizens, meaning they have the same rights and capabilities as other values in the language. Functions can be assigned to variables, passed as arguments to other functions, returned as values from functions, and stored in data structures like arrays and hashes.
This concept originates from lambda calculus and functional programming languages like Lisp and Scheme, but has been adopted by many modern languages including Ruby, JavaScript, Python, and others. First-class functions enable functional programming paradigms within multi-paradigm languages.
The significance of first-class functions extends beyond theoretical computer science. They enable higher-order functions, closures, callbacks, decorators, and many design patterns that reduce code duplication and increase abstraction. Without first-class functions, developers would need to rely on more verbose patterns like the Strategy pattern or Command pattern to achieve similar flexibility.
# Functions as values
greeting = lambda { |name| "Hello, #{name}!" }
puts greeting.call("World")
# => Hello, World!
# Functions passed as arguments
def apply_twice(value, &operation)
operation.call(operation.call(value))
end
result = apply_twice(3) { |x| x * 2 }
# => 12
Ruby implements first-class functions through three distinct mechanisms: blocks, procs, and lambdas. While blocks are syntactic constructs that cannot be stored in variables directly, procs and lambdas are true objects that can be manipulated like any other value. This distinction creates both flexibility and complexity in Ruby's approach to functional programming.
Key Principles
The foundation of first-class functions rests on treating functions as data. In languages supporting this feature, a function is not fundamentally different from an integer, string, or object. The function can be referenced, stored, and manipulated without being invoked immediately.
Assignment to Variables: Functions can be bound to variable names, allowing the same function to be referenced by different identifiers or stored for later use. This separates the definition of behavior from its invocation.
multiply = Proc.new { |a, b| a * b }
calculate = multiply # Same function, different name
calculate.call(4, 5)
# => 20
Function Parameters: Functions can accept other functions as arguments, creating higher-order functions. This pattern enables callbacks, event handlers, and customizable behavior without inheritance hierarchies.
def retry_operation(max_attempts, &operation)
attempts = 0
begin
attempts += 1
operation.call
rescue StandardError => e
retry if attempts < max_attempts
raise
end
end
retry_operation(3) { fetch_remote_data }
Return Values: Functions can return other functions, enabling function factories, partial application, and closures that capture surrounding state. The returned function maintains access to variables in its defining scope.
def multiplier(factor)
lambda { |n| n * factor }
end
times_three = multiplier(3)
times_three.call(10)
# => 30
Storage in Data Structures: Functions can be elements in arrays, values in hashes, or attributes of objects. This enables dispatch tables, command registries, and strategy collections.
operations = {
add: ->(a, b) { a + b },
subtract: ->(a, b) { a - b },
multiply: ->(a, b) { a * b }
}
operations[:add].call(10, 5)
# => 15
Lexical Closure: When a function references variables from its surrounding scope, it captures those variables, creating a closure. The closure maintains access to these variables even after the outer function has returned.
def counter(initial = 0)
count = initial
lambda { count += 1 }
end
counter_a = counter(0)
counter_b = counter(100)
counter_a.call # => 1
counter_a.call # => 2
counter_b.call # => 101
Each closure maintains its own binding to captured variables, creating independent state. This mechanism enables encapsulation without classes and object-oriented patterns using only functions.
Arity and Argument Handling: First-class functions have defined parameter lists (arity). Languages differ in how they handle mismatched arguments. Some enforce strict arity checking, while others allow flexible argument counts. This affects how functions can be composed and called dynamically.
The distinction between parameters (formal arguments in the definition) and arguments (actual values passed) becomes significant when functions are passed as values. A function's signature must be compatible with how it will be invoked, or the language must provide mechanisms for adapting arity.
Ruby Implementation
Ruby provides three mechanisms for first-class functions: blocks, procs, and lambdas. Each has distinct syntax, semantics, and use cases, reflecting Ruby's evolution and different programming needs.
Blocks: The most common Ruby construct, blocks are not true objects but syntactic elements. They cannot be assigned to variables without conversion but are passed implicitly to methods. Blocks use either do...end or { } syntax.
# Block passed to method
[1, 2, 3].each do |num|
puts num * 2
end
# Block with braces (same semantics)
[1, 2, 3].map { |num| num * 2 }
# => [2, 4, 6]
# Blocks cannot be assigned directly
# block = { puts "test" } # Syntax error
Methods receive blocks via the yield keyword or by declaring a block parameter with &block. The ampersand converts the block to a proc object.
def execute_with_timing(&block)
start_time = Time.now
result = block.call
elapsed = Time.now - start_time
puts "Execution took #{elapsed} seconds"
result
end
execute_with_timing { expensive_computation }
Procs: Created via Proc.new or the proc method, procs are objects that encapsulate blocks. They return from the enclosing method, not just from the proc itself, which can create unexpected control flow.
def test_proc_return
my_proc = Proc.new { return "from proc" }
my_proc.call
"after proc" # Never reached
end
test_proc_return
# => "from proc"
Procs have lenient arity checking. Extra arguments are ignored, missing arguments become nil.
flexible_proc = Proc.new { |a, b, c| [a, b, c] }
flexible_proc.call(1)
# => [1, nil, nil]
flexible_proc.call(1, 2, 3, 4, 5)
# => [1, 2, 3]
Lambdas: Created via lambda or the stabby lambda syntax ->, lambdas are procs with stricter semantics. They return from themselves only, matching function behavior in other languages, and enforce arity checking.
def test_lambda_return
my_lambda = lambda { return "from lambda" }
result = my_lambda.call
"after lambda: #{result}"
end
test_lambda_return
# => "after lambda: from lambda"
strict_lambda = ->(a, b) { a + b }
strict_lambda.call(1)
# => ArgumentError: wrong number of arguments (given 1, expected 2)
The stabby lambda syntax supports default arguments and explicit parameter types (though Ruby doesn't enforce types).
calculator = ->(x, y = 10) { x + y }
calculator.call(5)
# => 15
# Multi-line stabby lambda
processor = ->(data) do
validated = validate(data)
transformed = transform(validated)
transformed.result
end
Method Objects: Ruby methods can be converted to objects using method or public_method, creating another form of first-class function.
class Calculator
def add(a, b)
a + b
end
end
calc = Calculator.new
add_method = calc.method(:add)
add_method.call(5, 3)
# => 8
# Can be passed to higher-order functions
[1, 2, 3].map(&add_method.curry[10])
# => [11, 12, 13]
Symbol to Proc Conversion: Ruby's Symbol#to_proc enables concise higher-order function calls by converting symbols to procs that call the named method.
["apple", "banana", "cherry"].map(&:upcase)
# => ["APPLE", "BANANA", "CHERRY"]
# Equivalent to:
["apple", "banana", "cherry"].map { |s| s.upcase }
This works because &:symbol converts the symbol to a proc via to_proc, then the & operator converts the proc to a block argument.
Currying and Partial Application: Ruby procs and lambdas support currying, converting a function taking multiple arguments into a sequence of functions each taking a single argument.
multiply = ->(a, b, c) { a * b * c }
curried = multiply.curry
times_2 = curried.call(2)
times_2_and_3 = times_2.call(3)
times_2_and_3.call(4)
# => 24
# Partial application in one step
times_5 = multiply.curry.call(5)
times_5.call(2, 3)
# => 30
Common Patterns
Callback Pattern: Pass functions as arguments to be invoked at specific points in execution. This decouples the caller from the specific behavior that occurs at callback points.
class FileProcessor
def process(filename, &on_line)
File.foreach(filename) do |line|
on_line.call(line.chomp)
end
end
end
processor = FileProcessor.new
processor.process("data.txt") do |line|
puts "Processing: #{line}" if line.include?("error")
end
Callbacks enable inversion of control, where the framework calls application code rather than the application calling framework code. This pattern appears in event handlers, middleware chains, and plugin architectures.
Higher-Order Functions: Functions that accept other functions as parameters or return functions as results. These create abstractions over common patterns.
def compose(f, g)
lambda { |x| f.call(g.call(x)) }
end
add_one = ->(x) { x + 1 }
double = ->(x) { x * 2 }
add_then_double = compose(double, add_one)
add_then_double.call(5)
# => 12
Common higher-order functions include map, filter, reduce, and sort_by. These abstract iteration patterns, making code more declarative.
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
result = numbers
.select { |n| n.even? }
.map { |n| n * n }
.reduce(0) { |sum, n| sum + n }
# => 220
Function Factory: Return functions from functions, often creating closures that capture configuration or state.
def create_validator(min, max)
lambda do |value|
return false if value < min
return false if value > max
true
end
end
age_validator = create_validator(0, 120)
age_validator.call(25) # => true
age_validator.call(150) # => false
percentage_validator = create_validator(0, 100)
percentage_validator.call(50) # => true
This pattern creates specialized functions from general templates, reducing duplication when multiple similar functions are needed with different parameters.
Strategy Pattern: Use functions instead of strategy objects to select algorithms at runtime. This simplifies the classic object-oriented pattern.
class DataExporter
def initialize(format_strategy)
@format_strategy = format_strategy
end
def export(data)
@format_strategy.call(data)
end
end
json_format = ->(data) { data.to_json }
csv_format = ->(data) {
data.map { |row| row.join(',') }.join("\n")
}
exporter = DataExporter.new(json_format)
exporter.export([{name: "Alice", age: 30}])
Decorator Pattern: Wrap functions with additional behavior while maintaining the same interface.
def logged(function)
lambda do |*args|
puts "Calling with arguments: #{args.inspect}"
result = function.call(*args)
puts "Result: #{result}"
result
end
end
def timed(function)
lambda do |*args|
start = Time.now
result = function.call(*args)
elapsed = Time.now - start
puts "Execution time: #{elapsed}s"
result
end
end
expensive_operation = ->(n) { sleep(n); n * 2 }
decorated = logged(timed(expensive_operation))
decorated.call(1)
Decorators compose cleanly because they maintain the function signature, allowing multiple decorators to wrap the same function.
Lazy Evaluation: Return functions that compute values only when called, deferring expensive operations.
def lazy_load(expensive_operation)
cached_result = nil
lambda do
cached_result ||= expensive_operation.call
end
end
fetch_user = lazy_load(lambda { fetch_from_database })
# Database not yet queried
user = fetch_user.call # Now queries database
user_again = fetch_user.call # Uses cached result
Command Objects as Functions: Replace command objects with simple functions when no state beyond the command operation is needed.
# Traditional command pattern with classes
class Command
def execute; raise NotImplementedError; end
end
class SaveCommand < Command
def initialize(data); @data = data; end
def execute; save_to_database(@data); end
end
# Functional approach
save_command = ->(data) { save_to_database(data) }
delete_command = ->(id) { delete_from_database(id) }
commands = [
save_command.curry["user_data"],
delete_command.curry[old_id]
]
commands.each(&:call)
Practical Examples
Event System: Build an event dispatcher using first-class functions to decouple event producers from consumers.
class EventEmitter
def initialize
@listeners = Hash.new { |h, k| h[k] = [] }
end
def on(event_name, &handler)
@listeners[event_name] << handler
end
def emit(event_name, *args)
@listeners[event_name].each do |handler|
handler.call(*args)
end
end
def off(event_name, handler)
@listeners[event_name].delete(handler)
end
end
emitter = EventEmitter.new
# Register multiple handlers
emitter.on(:user_login) do |user|
puts "Logging access for #{user.name}"
end
emitter.on(:user_login) do |user|
send_welcome_email(user)
end
emitter.on(:user_login) do |user|
update_last_seen(user)
end
# Trigger event
emitter.emit(:user_login, current_user)
This pattern decouples event sources from handlers. New behaviors can be added by registering additional handlers without modifying the event emitter or existing handlers.
Pipeline Processing: Chain operations on data using function composition.
class Pipeline
def initialize(value)
@value = value
end
def pipe(&transformation)
@value = transformation.call(@value)
self
end
def value
@value
end
end
# Define transformation functions
normalize_whitespace = ->(text) { text.gsub(/\s+/, ' ').strip }
remove_punctuation = ->(text) { text.gsub(/[[:punct:]]/, '') }
downcase_text = ->(text) { text.downcase }
split_words = ->(text) { text.split(' ') }
count_words = ->(words) { words.size }
# Build and execute pipeline
text = " Hello, World! How are you? "
word_count = Pipeline.new(text)
.pipe(&normalize_whitespace)
.pipe(&remove_punctuation)
.pipe(&downcase_text)
.pipe(&split_words)
.pipe(&count_words)
.value
# => 5
Pipelines make data transformations explicit and composable. Each stage is independent and testable, and stages can be reordered or removed without affecting others.
Retry Logic with Configurable Strategies: Create reusable retry mechanisms where the retry strategy is a function.
def with_retry(max_attempts: 3, delay_strategy: nil, &operation)
delay_strategy ||= ->(attempt) { 2 ** attempt }
attempts = 0
begin
attempts += 1
operation.call
rescue StandardError => e
if attempts < max_attempts
delay = delay_strategy.call(attempts)
sleep(delay)
retry
else
raise
end
end
end
# Exponential backoff
with_retry(max_attempts: 5) do
fetch_from_unreliable_api
end
# Linear backoff
with_retry(
max_attempts: 3,
delay_strategy: ->(attempt) { attempt * 0.5 }
) do
write_to_database
end
# Constant delay
with_retry(
max_attempts: 4,
delay_strategy: ->(_) { 1 }
) do
send_notification
end
Memoization: Cache function results for expensive computations.
def memoize(function)
cache = {}
lambda do |*args|
cache[args] ||= function.call(*args)
end
end
fibonacci = lambda do |n|
return n if n <= 1
fibonacci.call(n - 1) + fibonacci.call(n - 2)
end
memoized_fibonacci = memoize(fibonacci)
# First call computes
memoized_fibonacci.call(35) # Takes time
# Second call uses cache
memoized_fibonacci.call(35) # Instant
The memoized function maintains a cache across invocations, trading memory for speed. This pattern works for pure functions where outputs depend only on inputs.
Builder Pattern with Fluent Interface: Use function chaining to construct complex objects.
class QueryBuilder
def initialize
@conditions = []
@limit_value = nil
@order_column = nil
end
def where(&condition)
@conditions << condition
self
end
def limit(n)
@limit_value = n
self
end
def order_by(column)
@order_column = column
self
end
def execute(collection)
result = collection.select do |item|
@conditions.all? { |condition| condition.call(item) }
end
result = result.sort_by { |item| item[@order_column] } if @order_column
result = result.take(@limit_value) if @limit_value
result
end
end
users = [
{name: "Alice", age: 30, role: "admin"},
{name: "Bob", age: 25, role: "user"},
{name: "Charlie", age: 35, role: "admin"}
]
admins_over_25 = QueryBuilder.new
.where { |user| user[:role] == "admin" }
.where { |user| user[:age] > 25 }
.order_by(:age)
.limit(10)
.execute(users)
# => [{name: "Alice", age: 30, role: "admin"}, {name: "Charlie", age: 35, role: "admin"}]
Design Considerations
When to Use First-Class Functions: Choose first-class functions when behavior varies independently of data structure. If different algorithms need to operate on the same data, passing functions avoids creating class hierarchies for each algorithm variant.
First-class functions excel for callbacks and event handlers where the timing of execution is determined by the framework but the specific behavior is application-defined. They reduce coupling between components by eliminating direct dependencies.
Use functions for simple behaviors that don't require state beyond their parameters. When behavior needs extensive configuration, initialization, or maintains state across invocations, objects may provide better encapsulation.
Functions vs Objects: Objects bundle data and behavior, maintaining state across method calls. Functions, especially pure functions, take inputs and produce outputs without side effects. Objects work well for entities with identity and lifecycle. Functions work well for transformations and operations.
# Object-oriented approach
class UserValidator
def initialize(min_age, banned_domains)
@min_age = min_age
@banned_domains = banned_domains
end
def valid?(user)
return false if user.age < @min_age
return false if @banned_domains.include?(user.email_domain)
true
end
end
validator = UserValidator.new(18, ["spam.com"])
validator.valid?(user)
# Functional approach
def create_user_validator(min_age, banned_domains)
lambda do |user|
return false if user.age < min_age
return false if banned_domains.include?(user.email_domain)
true
end
end
validator = create_user_validator(18, ["spam.com"])
validator.call(user)
Both approaches capture configuration state. Objects make state mutable and provide multiple related methods. Functions create immutable closures focused on single operations.
Performance Trade-offs: Creating closures has memory overhead. Each closure maintains references to captured variables, preventing garbage collection of that context. Thousands of closures can impact memory usage.
Function calls through proc/lambda have slight overhead compared to direct method calls. For tight loops with millions of iterations, this overhead may matter. Profile before optimizing.
# Memory consideration
def create_processors(count)
large_data = Array.new(1_000_000) { rand }
count.times.map do |i|
# Each lambda captures reference to large_data
lambda { |x| large_data[x] * i }
end
end
processors = create_processors(1000)
# large_data cannot be garbage collected while processors exist
Blocks vs Procs vs Lambdas: Use blocks for single-use iterations and callbacks that don't need to be stored. Use procs when lenient argument handling is needed or when the return behavior of jumping to the enclosing method is desired.
Use lambdas for functions that need to be passed around, stored, and called multiple times with strict argument checking. Lambdas behave most like functions in other languages.
# Block: Single use, not stored
collection.each { |item| process(item) }
# Proc: Stored, flexible arguments
processor = Proc.new { |a, b, c| [a, b, c].compact }
different_arg_counts.each { |args| processor.call(*args) }
# Lambda: Stored, strict arguments
validator = ->(email) { email.match?(/@/) }
emails.select(&validator)
Testing Implications: First-class functions simplify testing by isolating behavior. Test functions independently, then test higher-order functions with stub functions.
# Test the higher-order function
def test_with_retry
call_count = 0
operation = lambda do
call_count += 1
raise "Error" if call_count < 3
"success"
end
result = with_retry(max_attempts: 3, &operation)
assert_equal "success", result
assert_equal 3, call_count
end
Functions passed as parameters become test seams. Replace real implementations with test doubles without affecting the code under test.
Common Pitfalls
Proc vs Lambda Return Behavior: The most common mistake is misunderstanding how return behaves differently in procs versus lambdas.
def test_returns
proc_test = Proc.new { return "proc return" }
lambda_test = lambda { return "lambda return" }
proc_result = proc_test.call # Returns from test_returns
# This line never executes
lambda_result = lambda_test.call
"end of method"
end
test_returns
# => "proc return"
def test_lambda_only
lambda_test = lambda { return "lambda return" }
lambda_result = lambda_test.call
"end of method: #{lambda_result}"
end
test_lambda_only
# => "end of method: lambda return"
Procs return from the method where they were defined, not where they're called. This can cause unexpected control flow when procs are passed between methods. Lambdas return only from themselves, making their behavior more predictable.
Closure Variable Capture: Closures capture variables by reference, not by value. This leads to unexpected behavior when variables change after closure creation.
# Common mistake
functions = []
(1..3).each do |i|
functions << lambda { puts i }
end
functions.each(&:call)
# Prints: 3, 3, 3 (not 1, 2, 3)
# Correct approach: explicitly capture value
functions = []
(1..3).each do |i|
functions << lambda { |n| lambda { puts n } }.call(i)
end
functions.each(&:call)
# Prints: 1, 2, 3
The closure captures the variable i, not its value at closure creation time. When closures execute later, they see the final value of i after the loop completes.
Arity Mismatches: Forgetting that lambdas enforce arity while procs don't can cause runtime errors or silent bugs.
strict_lambda = ->(a, b) { a + b }
flexible_proc = Proc.new { |a, b| a + b }
strict_lambda.call(1) # ArgumentError
flexible_proc.call(1) # NoMethodError: undefined method + for nil
The proc accepts the call but creates nil values for missing parameters, which may cause errors later in execution. Lambdas fail immediately with clear error messages.
Memory Leaks Through Closures: Closures maintain references to all variables in their scope, even if they don't use them all. This can prevent garbage collection.
def create_handler
large_object = Array.new(1_000_000) { Object.new }
small_value = 42
# This closure captures both variables
lambda { puts small_value }
# large_object cannot be garbage collected
end
handler = create_handler
# large_object stays in memory while handler exists
Extract only needed values before creating closures:
def create_handler
large_object = Array.new(1_000_000) { Object.new }
small_value = large_object.size # Extract needed value
large_object = nil # Allow garbage collection
lambda { puts small_value }
end
Block vs Proc Confusion: Converting between blocks and procs requires the & operator. Forgetting this leads to syntax errors.
def takes_block(&block)
block.call
end
my_proc = proc { puts "Hello" }
takes_block(my_proc) # Wrong: passes proc as regular argument
takes_block(&my_proc) # Correct: converts proc to block
The & operator converts procs to blocks when calling methods and converts blocks to procs when receiving them as parameters.
Ignored Return Values: Procs and lambdas return the value of their last expression, but this return value is often ignored in callback scenarios.
result = [1, 2, 3].each { |n| n * 2 }
# result is [1, 2, 3], not [2, 4, 6]
# Correct use of map to transform
result = [1, 2, 3].map { |n| n * 2 }
# result is [2, 4, 6]
Understanding whether a method uses the block's return value (like map and select) versus just calling the block for side effects (like each) prevents confusion.
Nested Block Limitations: Ruby syntax doesn't cleanly support nested blocks with the do...end or {} syntax.
# Doesn't work as expected
collection.each do |item|
item.values.select do |value|
value > 10
end
end
# Use braces for inner block
collection.each do |item|
item.values.select { |value| value > 10 }
end
# Or assign to variable
collection.each do |item|
large_values = item.values.select { |value| value > 10 }
process(large_values)
end
Symbol#to_proc Limitations: While &:method_name is concise, it only works for methods that take no arguments.
strings = ["hello", "world"]
# Works: no arguments
strings.map(&:upcase)
# => ["HELLO", "WORLD"]
# Doesn't work: method needs argument
strings.map(&:include?("o")) # Syntax error
# Must use explicit block
strings.map { |s| s.include?("o") }
# => [true, true]
Reference
Block, Proc, and Lambda Comparison
| Feature | Block | Proc | Lambda |
|---|---|---|---|
| Object Status | Not an object | Object | Object (special proc) |
| Assignment | Cannot assign directly | Assignable | Assignable |
| Creation Syntax | do...end or {} | Proc.new or proc | lambda or -> |
| Return Behavior | Returns from enclosing method | Returns from enclosing method | Returns from lambda only |
| Arity Checking | Flexible | Flexible | Strict |
| Parameter Declaration | Pipes around params | Block parameters | Arrow parameters |
Common Higher-Order Functions
| Method | Purpose | Returns | Example |
|---|---|---|---|
| map | Transform elements | New array | [1, 2].map { abs } |
| select | Filter elements | New array | [1, 2].select { abs > 1 } |
| reject | Inverse filter | New array | [1, 2].reject { abs.zero? } |
| reduce | Accumulate value | Single value | [1, 2].reduce(0) { abs + abs } |
| each | Iterate for side effects | Original | [1, 2].each { puts abs } |
| sort_by | Sort by computed value | New array | users.sort_by { abs.age } |
| group_by | Group by computed key | Hash | users.group_by { abs.role } |
| find | First matching element | Element or nil | [1, 2].find { abs > 1 } |
| all? | Test all elements | Boolean | [1, 2].all? { abs > 0 } |
| any? | Test any element | Boolean | [1, 2].any? { abs > 1 } |
Function Conversion Syntax
| Operation | Syntax | Result |
|---|---|---|
| Block to Proc | &block | Proc object |
| Proc to Block | &proc | Block argument |
| Method to Proc | object.method(:name) | Method object |
| Symbol to Proc | &:symbol | Proc calling method |
| Lambda Creation | lambda {} or ->() {} | Lambda object |
| Proc Creation | Proc.new {} or proc {} | Proc object |
Arity Methods
| Method | Purpose | Returns |
|---|---|---|
| arity | Number of parameters | Integer |
| parameters | Parameter details | Array of [type, name] |
| lambda? | Check if strict lambda | Boolean |
| curry | Convert to curried form | Curried proc |
| call | Invoke function | Return value |
| === | Call (for case statements) | Return value |
Closure Behavior
| Aspect | Behavior | Implication |
|---|---|---|
| Variable Capture | By reference | Changes visible |
| Scope | Lexical | Sees defining context |
| Lifetime | With function object | Prevents GC |
| Mutability | Captured vars mutable | State can change |
| Sharing | Multiple closures share | Side effects visible |
Performance Characteristics
| Operation | Relative Cost | Notes |
|---|---|---|
| Block yield | Fastest | Direct method call |
| Lambda call | Fast | Small overhead |
| Proc call | Fast | Small overhead |
| Method object call | Slower | Additional indirection |
| Symbol to proc | Moderate | Conversion overhead |
| Curry | One-time cost | Per partial application |
Common Patterns
| Pattern | Use Case | Key Benefit |
|---|---|---|
| Callback | Async operations | Decoupling |
| Factory | Create configured functions | Reusability |
| Decorator | Add behavior | Composition |
| Strategy | Algorithm selection | Flexibility |
| Command | Encapsulate operations | Undo/redo support |
| Pipeline | Sequential transformations | Clarity |
| Memoization | Cache expensive results | Performance |
| Lazy evaluation | Defer computation | Efficiency |