Higher-order functions — functions that return functions
Concept
Higher-order functions (HOFs) are functions that either accept other functions as arguments, return functions as their result, or both. PHP supports HOFs natively because the Closure type is a first-class value — closures can be stored in variables, passed to functions, returned from functions, and stored in arrays or object properties.
The most primitive HOFs are PHP's built-in array operations: array_map applies a transformation to each element, array_filter selects elements matching a predicate, and array_reduce folds a collection into a single value using an accumulator. These three combinators can express any data transformation pipeline without mutation or loops.
Returning a function from a function is called a factory or a curried function. Partial application — fixing some arguments of a function and returning a new function that accepts the rest — is a specific pattern that emerges naturally. PHP does not have built-in currying, but you can implement it with closures. This is the foundation of configurability: a function that takes a configuration parameter and returns a configured transformation.
In large codebases, HOFs enable composition — building complex behaviour from simple building blocks. Laravel's Collection class is a HOF gallery: map, filter, reject, reduce, flatMap, groupBy all accept callables. The Pipeline class composes an ordered list of Closure or middleware objects using the functional reduce pattern: each pipe wraps the next in a new closure, and calling the outermost closure cascades through the chain.
The distinction between HOFs and callbacks: a callback is any callable passed as an argument. A HOF has the additional property that the function itself is its primary abstraction — it exists to transform or compose other functions, not just to call one callback.
Code Example
<?php
declare(strict_types=1);
// Partial application — fix the first argument
function partial(callable $fn, mixed ...$partialArgs): Closure
{
return function () use ($fn, $partialArgs): mixed {
$remainingArgs = func_get_args();
return $fn(...$partialArgs, ...$remainingArgs);
};
}
$multiply = fn(int $a, int $b): int => $a * $b;
$double = partial($multiply, 2);
$triple = partial($multiply, 3);
echo $double(5) . PHP_EOL; // 10
echo $triple(5) . PHP_EOL; // 15
// Function composition — compose(f, g)(x) = f(g(x))
function compose(callable ...$fns): Closure
{
return function (mixed $value) use ($fns): mixed {
return array_reduce(
array_reverse($fns),
fn(mixed $carry, callable $fn): mixed => $fn($carry),
$value
);
};
}
$process = compose(
fn(string $s): string => strtoupper($s),
fn(string $s): string => trim($s),
fn(string $s): string => str_replace('-', ' ', $s),
);
echo $process(' hello-world ') . PHP_EOL; // HELLO WORLD
// Memoizing HOF — wraps any pure function
function memoize(callable $fn): Closure
{
$cache = [];
return function () use ($fn, &$cache): mixed {
$key = serialize(func_get_args());
if (!array_key_exists($key, $cache)) {
$cache[$key] = $fn(...func_get_args());
}
return $cache[$key];
};
}
$factorial = memoize(function (int $n) use (&$factorial): int {
return $n <= 1 ? 1 : $n * $factorial($n - 1);
});
echo $factorial(10) . PHP_EOL; // 3628800
// Pipeline pattern (simplified Laravel-style)
function pipeline(mixed $payload, callable ...$pipes): mixed
{
return array_reduce(
$pipes,
fn(mixed $carry, callable $pipe): mixed => $pipe($carry),
$payload
);
}
$result = pipeline(
' raw user input ',
trim(...),
strtolower(...),
fn(string $s): string => htmlspecialchars($s, ENT_QUOTES),
);
echo $result . PHP_EOL; // raw user inputInterview Q&A
Q: Explain the difference between partial application and currying. Does PHP support either natively?
Partial application fixes one or more arguments of a function, returning a new function that accepts the remaining arguments in a single call. Currying is a stricter transformation: a curried function accepts exactly one argument and returns a new function that accepts the next, continuing until all arguments are supplied. add(1)(2)(3) is curried; add(1, 2)(3) is partially applied. PHP has neither natively. Partial application is trivially implemented with closures as shown above. True currying requires wrapping in nested single-argument closures, which is verbose in PHP but perfectly valid. Most real PHP code uses partial application because it maps more naturally to functions with multiple parameters that are provided in groups, not one at a time.
Q: How does Laravel's Pipeline implement function composition under the hood?
Illuminate\Pipeline\Pipeline::thenReturn() calls then(fn($p) => $p). Inside then(), the pipes are collected into an array and passed to array_reduce(). The reducer builds nested closures: each iteration wraps the previous "stack" in a new closure that calls the current pipe and passes $next (the inner closure). The final result of array_reduce is a single closure representing the entire chain. Calling that outermost closure with the initial payload threads it through every pipe in sequence. This is the classic function composition pattern implemented with array_reduce, exactly as in a functional language. The pipes can be closure instances or class names — when class names are passed, the pipeline resolves them through the container, calling handle($passable, $next) using call_user_func.
Q: What are the performance implications of using HOFs like array_map versus a plain foreach loop in PHP?
array_map and array_filter allocate new arrays and invoke a user-space callable for each element, which incurs call overhead per element. A foreach loop avoids the callable dispatch overhead and modifies in place (or appends to a pre-allocated array). In microbenchmarks, foreach is consistently faster for large collections by 20–50% depending on the work done per element. However, the functional style is preferred for code clarity, immutability, and composability — the performance difference is irrelevant in typical web request handling where database I/O dominates. Where it matters is in tight loops processing tens of thousands of records entirely in memory — there, foreach or generator-based processing is the right tool.