rfc:function-composition

PHP RFC: Function composition operators

Introduction

In object-oriented code, “composition” generally means “one object having a reference to another.” In functional programming, “composition” generally means “sticking two functions together end-to-end to make a new function.” Both are valid and useful techniques, especially in a multi-paradigm language like PHP. However, the latter is currently not directly supported by the language. This RFC aims to correct that gap, with a new function composition operator for closures.

This RFC cleanly enables “point-free style,” an approach to programming that limits the use of unnecessary intermediary variables. Point-free style has been gaining popularity in JavaScript circles, so will be familiar to JavaScript developers using that style.

This RFC is a natural follow-on to its sister RFC, the Pipe operator

Proposal

This RFC proposes a new operator

closure + closure;

The compose operator (+) is very similar conceptually to pipe, but does not invoke immediately. When applied to two single-parameter closures, evaluates to a new Closure object that contains the two referenced closures. When invoked with a single argument, it will pass that argument to the first closure, then the result of that will be passed to the second closure, and return the result. If either closure is already such a composed Closure, it will be “flattened” into just being a longer array of closures.

Rather, it creates a new callable (Closure) that composes two or more other callables. That allows a new operation to be defined simply and easily and then saved for later in a variable. Because it is “just” an operator, it is compatible with all other language features. That means, for example, conditionally building up a pipeline is just a matter of throwing if statements around as appropriate.

Additionally, += will work as well, as it is just a shorthand for adding two values and assigning the result to the first variable.

To reuse the example from pipes:

$processor = htmlentities(...)
    + str_split(...)
    + fn($x) => array_map(strtoupper(...), $x);
 
if ($some_flag) {
    $processor += fn($x) => array_filter($x, fn($v) => $v != 'O');
}
 
$result1 = $processor('some string');
$result2 = $processor('other string');

This is far more flexible than the method chaining and embedded-conditional methods often used in user-space implementations today (see below).

As compose is an expression, it may be used anywhere an expression is valid.

Allowed arguments

The left and right sides of the compose operator may be any unary PHP closure object, including a first-class-callable (foo(...)). It may also be an invokable object (that is, an object with an __invoke() method).

The reason to disallow non-closures is largely for simplicity. PHP has many other callable syntaxes that are already very hard to distinguish from a random string or array, and both strings and arrays already have behavior associated with the + operator. As creating Closures these days is pretty easy (either first-class-callables or a short-lambda), this is not a significant limitation.

Should further improvements be made to closure-generation in the future, such as a revised Partial Function Application RFC, it would be supported naturally.

Closures with more than one required parameter are not allowed and will fail as if the function were called normally with insufficient arguments. If the right-hand side does not evaluate to a supported value it will throw an Error.

Logic description

The result of adding two closures is an object approximately equivalent to (though this isn't quite the implementation):

readonly class ComposedClosure
{
    private array $callables = [];
 
    // This is called when the left-side variable is not a ComposedClosure yet.
    public static function make(callable $left, callable $right)
    {
        $this->callables[] = $left;
        $this->callables[] = $right;
    }
 
    public function __invoke(mixed $arg): mixed
    {
        foreach ($this->callables as $fn) {
            $arg = $fn($arg);
        }
        return $arg;
    }
 
    // This is an operator.
    public function add(callable $next): self
    {
        $new = new self();
        $new->callables = [...$this->callables, $next];
        return $new;
    }
}

Syntax choice

The use of |> for pipe is obvious, as discussed in that RFC.

For composition, there is a bit less standardization. Haskell uses ., but it reads “backwards” from what is proposed here. F# uses >>, as does Ruby.

>> is a bit-wise shift operator, which is currently useless except on numeric values. It provides no behavior objects (eg, Closures), making it a safe option. However, it has no natural “append” behavior to update a ComposedClosure in place. The natural first choice would be >>=, but that is well-known as the “bind” operator in Haskell, which does something more advanced. We're hesitant to claim that operator for a different behavior, especially when it may be useful for a bind operator in the future.

Both . and + are available, and currently have no meaning on objects, making them safe options. Both also have a natural append operation (.= and +').

The main reason this RFC favors + is to avoid confusion with Haskell, which as noted also has a . operator but it would read the opposite way from PHP's behavior. (That is, in Haskell you'd write foo . bar . baz, but the equivalent in PHP is $baz . $bar . $foo.) This RFC's position is that Haskell's ordering is poor DX, but using the same syntax as Haskell in a different way would be needlessly confusing and attract too many “OMG you did it wrong” trolls, of which PHP has enough as is. + would have no such confusion.

Existing implementations

(This section is the same as in the Pipes RFC, as the arguments, and existing examples, are effectively identical.)

Multiple user-space libraries exist in PHP that attempt to replicate pipe-like or compose-like behavior. All are clunky and complex by necessity compared to a native solution. There is clear demand for this functionality, but user-space's ability to provide it is currently limited. This list has only grown since the Pipes v2 RFC, indicating an even stronger benefit to the PHP ecosystem with a solid built-in composition syntax.

  • The PHP League has a Pipeline library that encourages wrapping all functions into classes with an __invoke() method to allow them to be referenced, and using a ->pipe() call for each step.
  • Laravel includes a Illuminate/Pipeline package that has an even more cumbersome syntax.
  • The PHP Standard Library (PSL) library includes a pipe function, though it is more of a function concatenation operation.
  • Sebastiaan Luca has a pipe library that works through abuse of the __call method. It only works for named functions, I believe, not for arbitrary callables.
  • PipePie is another very similar implementation to the previous ones.
  • ZenPipe is a new-comer that also uses a method named pipe() for what is actually a composition operation.
  • Crell/fp provides pipe() and compose() functions that take an array of callables. While the lightest-weight option on this list, that makes dynamically-built pipelines or compositions more cumbersome than the syntax proposed here.
  • Various blogs speak of “the Pipeline Pattern” (for example), or more recently, Creating a type-safe pipe() in PHP

Those libraries would be mostly obsoleted by this RFC, with a more compact, more universal, better-performing syntax.

Why in the engine?

The biggest limitation of any user-space implementation is performance. Even the most minimal implementation (Crell/fp) requires adding 2-3 function calls to every operation, which is relatively expensive in PHP. A native implementation would not have that additional overhead. Crell/fp also results in somewhat awkward function nesting, like this:

$fn = compose(
    htmlentities(...),
    str_split(...),
    fn($x) => array_map(strtoupper(...), $x),
    fn($x) => array_filter($x, fn($v) => $v != 'O'),
);
 
// (Or worse if you need conditional stages.)

More elaborate implementations tend to involve magic methods (which are substantially slower than normal function/method calls) or multi-layer middlewares, which are severe overkill for sticking two functions together.

A native implementation eliminates all of those challenges.

Additionally, a native operator would make it much easier for static analysis tools to ensure compatible types without several layers of obfuscated user-space function calls in the way.

Future Scope

There are a number of potential improvements to this feature that have been left for later, as their implementation would be notably more involved than this RFC. The authors believe they would be of a benefit in their own RFCs.

* Generic partial function application. While the prior RFC was declined due to its perceived use cases being insufficient to justify its complexity, there was clear interest in it, and it would vastly improve the usability of function composition. If a less complex implementation can be found, it would most likely pass and complement this RFC well.

* A __bind method or similar on objects, possibly with a dedicated operator of its own (such as >>=). If implemented by an object on the left-hand side, the right-hand side would be passed to that method to invoke as it sees fit. Such a feature would be sufficient to support arbitrary monadic behavior in PHP in a type-friendly way.

Backward Incompatible Changes

None

Proposed PHP Version(s)

8.5

Open Issues

Still deciding between + and >> for composition.

Future Scope

Proposed Voting Choices

Yes or no vote. 2/3 required to pass.

Patches and Tests

Implementation

After the project is implemented, this section should contain

  1. the version(s) it was merged into
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature
  4. a link to the language specification section (if any)

References

Links to external references, discussions or RFCs

Rejected Features

rfc/function-composition.txt · Last modified: 2025/02/19 00:03 by crell