Introduction
It has been expressed both on and off the mailing list that arrow functions are a highly desired feature. It seems that we mostly do not agree on how this feature should work. The purpose of this document and its corresponding vote is to help us come to consensus.
The result of this vote is non-binding, meaning that voting here is not accepting something into the language. This is a way to express your preference of the two proposed syntaxes in voting form. The result of this vote will be used to draft a final RFC targeting PHP 7.2.
Concerns
Having reviewed the past discussions about arrow functions I have determined that there are a few main points that people disagree on. The ordering here has no significance; they are numbered only so it is easier to refer to a given point later.
- How variables/values from the outer scope are bound.
- How concise it should be / Readability.
- Using a keyword or not.
- Will it ever be able to support statement blocks instead of only a single expression?
- Syntax even aside from the above concerns.
Binding variables from the outer scope
The current proposal binds variables used from the outer scope by value. To address the concern from the community I propose that by default we bind by value and allow opting into binding by reference. Being by-value as the default and opting into by-reference is prudent for two reasons:
- It mimics how we deal with parameters and the return value. By default they are by-value and are changed to pass-by-reference and return-by-reference when the respective ampersands are present. The amended proposal makes this consistent.
- Binding by-reference has a higher performance cost.
The details of how to opt-in are detailed in syntax.
Syntax
I am unsure if the other points listed in Concerns are truly reconcilable. Instead I suggest two syntaxes for us to discuss and choose between. Each has different strengths and weaknesses with regards to points 2-5.
Both options support type declarations for parameters and the return type, as well as parameter references and return by-reference.
Option 1: fn-prefixed
This is what current RFC outlines except it has the optional leading ampersand to indicate that bound variables from the outer scope should be done by-reference instead of by-value. The simplified grammar looks like this:
T_FN '(' parameter_list ')' T_DOUBLE_ARROW expr
This requires a new keyword fn
and token T_FN
. There are a few usages in .phpt
tests and I expect it will similarly disrupt test suites of other projects as well.
Option 2: curly-brace enclosed
The simplified grammar looks like this:
'{' '(' parameter_list ')' T_DOUBLE_ARROW expr '}'
You may omit the parenthesis around the parameter list when there is only a single variable and neither references nor type information is provided:
{$x => $x + 2}
To support this syntax the grammar is modified to allow these closures wherever expressions are allowed except as a statement because we allow freestanding blocks. This is an acceptable trade-off because such a closure would not have any positive observable effect.
This syntax would restrict the ability to add object literals a-la JavaScript. Specifically it would prevent the ability to use arbitrary expressions as object literal keys.
Examples
Here are some snippets from real code-bases converted to use the syntaxes:
$s1 = fn($c) => $callable($factory($c), $c); $s2 = {$c => $callable($factory($c), $c)};
$this->existingSchemaPaths = array_filter($paths, fn($v) => in_array($v, $names)); $this->existingSchemaPaths = array_filter($paths, {$v => in_array($v, $names)});
function complement_s1(callable $f) { return fn(...$args) => !$f(...$args); } function complement_s2(callable $f) { return {(...$args) => !$f(...$args)}; }
function reject_s1($collection, callable $function) { return filter($collection, fn($value, $key) => !$function($value, $key)); } function reject_s2($collection, callable $function) { return filter($collection, {($value, $key) => !$function($value, $key)}); }
// not real code but typical of such $result = Collection::from([1, 2]) ->map(fn($v) => $v * 2) ->reduce(fn($tmp, $v) => $tmp + $v, 0); $result = Collection::from([1, 2]) ->map({$v => $v * 2}) ->reduce({($tmp, $v) => $tmp + $v}, 0);
I'll point out only one thing and leave the rest to the you to analyze: the closing curly brace }
makes it a bit easier to distinguish the end of the expression when the closure is directly passed as a parameter.
Of references
As an example of all the places references can be used:
&fn &(&$x) => function_that_takes_ref_and_returns_ref($x, $y) ^ ^ ^ | | \ Parameter is passed by reference | \ Function returns by reference \ Variables bound from the outer-scope are bound by reference
Here is the same closure written in the brace-enclosed style:
&{ &(&$x) => function_that_takes_ref_and_returns_ref($x, $y) } ^ ^ ^ | | \ Parameter is passed by reference | \ Function returns by reference \ Variables bound from the outer-scope are bound by reference
Wes Stark suggests that we move the return-by-reference sigil to behind the parameters where we put return type information:
{($x): & => function_that_returns_ref($x, $y) } ^ \ Function returns by reference
This cleans up the potential density if you close by reference and return by reference but means that we have a break in convention.
Of Nested Closures
Nikita Popov requested that I show an example of nested closures:
$add2 = fn($x) => fn($y) => $x + $y; $add2 = {$x => {$y => $x + $y}};