====== PHP RFC: Function composition operators ======
* Version: 3.0
* Date: 2024-11-09
* Author: Larry Garfield (larry@garfieldtech.com), Jordan LeDoux (jordan.ledoux@gmail.com)
* Status: Draft
* First Published at: http://wiki.php.net/rfc/function-composition
===== 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, [[rfc:pipe-operator-v3|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 [[rfc:partial_function_application|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. [[https://wiki.haskell.org/Function_composition|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 [[https://pipeline.thephpleague.com/|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 [[https://github.com/illuminate/pipeline|Illuminate/Pipeline]] package that has an [[https://agoalofalife.medium.com/pipeline-and-php-d9bb0a6370ca|even more cumbersome syntax]].
* The [[https://github.com/azjezz/psl|PHP Standard Library]] (PSL) library includes a [[https://github.com/azjezz/psl/blob/1.8.x/src/Psl/Fun/pipe.php|pipe function]], though it is more of a function concatenation operation.
* [[https://github.com/sebastiaanluca/php-pipe-operator|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.
* [[https://github.com/Toobo/PipePie|PipePie]] is another very similar implementation to the previous ones.
* [[https://github.com/dynamik-dev/zenpipe-php|ZenPipe]] is a new-comer that also uses a method named ''pipe()'' for what is actually a composition operation.
* [[https://github.com/Crell/fp|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" ([[https://medium.com/@aaronweatherall/the-pipeline-pattern-for-fun-and-profit-9b5f43a98130|for example]]), or more recently, [[https://refactorers-journal.ghost.io/creating-a-type-safe-pipe-in-php/|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
- the version(s) it was merged into
- a link to the git commit(s)
- a link to the PHP manual entry for the feature
- a link to the language specification section (if any)
===== References =====
Links to external references, discussions or RFCs
===== Rejected Features =====