rfc:short-functions

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
rfc:short-functions [2020/10/19 20:20] crellrfc:short-functions [2021/06/15 22:29] (current) – Fix RFC status ilutov
Line 1: Line 1:
 ====== PHP RFC: Short Functions ====== ====== PHP RFC: Short Functions ======
   * Version: 1.0   * Version: 1.0
-  * Date: 2020-09-26+  * Date: 2020-10-20
   * Author: Larry Garfield (larry@garfieldtech.com)   * Author: Larry Garfield (larry@garfieldtech.com)
-  * Status: Draft +  * Status: Declined 
-  * First Published at: http://wiki.php.net/rfc/short-functions+  *  First Published at: http://wiki.php.net/rfc/short-functions
  
 ===== Introduction ===== ===== Introduction =====
Line 19: Line 19:
 function add(int $a, int $b): int  function add(int $a, int $b): int 
 { {
-    return $a + b;+    return $a + $b;
 } }
 </code> </code>
Line 48: Line 48:
 Functions are simpler than lambdas, as there is no need for closing over variables contextually.  Therefore this patch is implemented 100% in the lexer, and thus should have no performance impact whatsoever. Functions are simpler than lambdas, as there is no need for closing over variables contextually.  Therefore this patch is implemented 100% in the lexer, and thus should have no performance impact whatsoever.
  
 +==== Consistency with closure syntax ====
 +
 +This RFC is designed to complement the [[rfc:auto-capture-closure|Auto-capturing multi-statement closures]] RFC.  Both stand on their own and offer independent benefits.  However, they have been designed such that their syntax is complementary and consistent.  Specifically:
 +
 +  * The ''=>'' sigil always means "evaluates to the expression on the right," in all circumstances.  (Named functions, anonymous functions, arrays, and ''match()''.)
 +  * ''{ ... }'' denotes a statement list, potentially ending in a ''return''.
 +  * The ''function'' keyword indicates a function that has no auto-capture.
 +  * The ''fn'' keyword indicates a function that will auto-capture variables, by value.
 +  * A function with a name is declared globally at compile time.  A function without a name is declared locally as a closure at runtime.
 +
 +These rules are easily recognizable and learnable by developers.
 +
 +(Further discussion of the possible permutations, and which are not useful to begin with, is in the Auto-capturing Closures RFC.)
  
 ==== Reasoning ==== ==== Reasoning ====
  
-Many functions and methods are, in practice, simple expressions.  They take input and return some simple output that can be computed in a single expression (of arbitrary complexity).  When anonymous, short lambdas offer a compact way to express that function as a literal expression.  Named functions, however, currently still require writing them as if they would be a long block of statements.  That provides extra visual clutter (especially when there are several such methods in one class).  It also forces you to code in "statement headspace" rather than "expression headspace".  Allowing functions to be written in a more expression-y way helps with conceptualizing a program as evaluating expressions, not statement steps.+Many functions and methods are, in practice, simple expressions.  They take input and return some simple output that can be computed in a single expression (of arbitrary complexity).  When anonymous, short lambdas offer a compact way to express that function as a literal expression.  Named functions, however, currently still require writing them as if they would be a long block of statements.  That provides extra visual clutter (especially when there are several such methods in one class).  It also forces you to code in "statement headspace" rather than "expression headspace. Allowing functions to be written in a more expression-y way helps with conceptualizing a program as evaluating expressions, not statement steps.
  
-Expressions are becoming increasingly capable, too.  match() expressions and throw expressions in PHP 8.0, plus proposals such as [[rfc:pipe-operator-v2|PHP RFC: Pipe Operator v2]], are collectively making it easier to write expressive expressions.  This improvement is a part of that larger trend.+Expressions are becoming increasingly capable, too.  match() expressions and throw expressions in PHP 8.0, plus proposals such as [[rfc:pipe-operator-v2|PHP RFC: Pipe Operator v2]], are collectively making it easier to write expressive expressions.  Enumeration methods are likely to consist primarily of a single match() statement.  This improvement is a part of that larger trend. 
 + 
 +The trend in PHP in recent years has been toward more compact but still readable syntax that eliminates redundancy.  Property promotion, arrow functions, the nullsafe operator, and similar recent well-received additions demonstrate this trend.  This RFC seeks to continue that trend to make PHP more pleasant to write while still being just as clear to read. 
 + 
 +==== Pure functions ==== 
 + 
 +Expression functions are also more likely to be pure, and thus avoid mutable state.  That cannot be guaranteed as it is possible to write single-expression functions that "leak" via global variables, like so: 
 + 
 +<code php> 
 + 
 +$called = 0; 
 + 
 +function fullName(string $first, string $last): string 
 +  => sprintf('%s %s %d', $first, $last, $GLOBALS['called']++); 
 +</code>
  
-Expression functions are also more likely to avoid mutable state.  While expression-only functions cannot guarantee that the function is pure (expressions like $i++ are a thing), it does tend to encourage more pure code, which is generally less error prone.+However, such code would be readily apparent as using global mutable state, and is easily avoided.  That makes short-functions more reliably "safe" and thus less prone to stateful errors.
  
-==== Examples ====+===== Examples =====
  
 Below are some examples of "long form" current code and what the short function equivalent would be.  This RFC asserts that the shorter version is more concise and readable.  All code is using standard PSR-12 formatting. Below are some examples of "long form" current code and what the short function equivalent would be.  This RFC asserts that the shorter version is more concise and readable.  All code is using standard PSR-12 formatting.
  
-=== Match functions ===+==== Match functions ====
  
 A function that encapsulates a match() expression. A function that encapsulates a match() expression.
Line 90: Line 117:
 </code> </code>
  
-=== Getter methods ===+==== Enum methods ====
  
-Many classes consist primarily or almost entirely out of methods that either return a property, or some computation off of a property.  With short-functions, that becomes considerably more concise.+In practice, most enum methods are likely to contain only a match expression, the evaluated value of which should be returned.  That makes them a good candidate for short functions and eliminating visual clutter. 
 + 
 +<code php> 
 +enum Suit  
 +
 + 
 +    case Hearts; 
 +    case Diamonds; 
 +    case Clubs; 
 +    case Spades; 
 +     
 +    public function color(): string => match($this) { 
 +        static::Hearts, static::Diamonds => 'Red', 
 +        static::Clubs, static::Spades => 'Black', 
 +    }; 
 +     
 +    vs: 
 +     
 +    public function color(): string 
 +    { 
 +        return match($this) { 
 +            static::Hearts, static::Diamonds => 'Red', 
 +            static::Clubs, static::Spades => 'Black', 
 +        }; 
 +    } 
 +
 + 
 +</code> 
 + 
 +==== Getter methods ==== 
 + 
 +Many classes consist primarily or almost entirely of methods that either return a property, or some computation off of a property.  With short-functions, that becomes considerably more concise.
  
 <code php> <code php>
Line 137: Line 195:
 </code> </code>
  
-=== Functional code ===+==== Functional code ====
  
 <code php> <code php>
Line 155: Line 213:
 More complex lines of short lambdas can be wrapped to a new line like this already, and it works just as well for short-functions. More complex lines of short lambdas can be wrapped to a new line like this already, and it works just as well for short-functions.
  
-=== Piped functions ===+It's also useful for basic API operations that can be expressed in terms of other basic API operations:
  
-The pipe operator |> is still pending in an RFC, but the feedback on it before was generally positive.  Short functions would allow a function to be easily defined as the composition of several other functions.+<code php> 
 +function str_contains(string $haystack, string $needle): bool => strpos($haystack, $needle) !== false; 
 +</code> 
 + 
 +==== Conditional methods ==== 
 + 
 +A common refactoring technique is to take a complex conditional in an if statement and move it to its own method, so it can be given a self-descriptive name.  Such methods are naturally single-expression.  Thus, well-factored code is likely to have a large percentage of its functions and methods be single-expression, and thus candidates for short functions. 
 + 
 +<code php> 
 +if ($this->isAdmin() || ($this->hasPermission('foo') && $this->hasPermission('bar'))) { 
 +    // ... 
 +
 +</code> 
 + 
 +Gets factored out to: 
 + 
 +<code php> 
 +if ($this->isGroupModerator()) { 
 +    // ... 
 +
 + 
 +// ... 
 + 
 +protected function isGroupModerator(): bool 
 +    => $this->isAdmin() || ($this->hasPermission('foo') && $this->hasPermission('bar')); 
 +</code> 
 + 
 +Which is more simple and compact than a full function body. 
 + 
 +==== Decorating functions in live code ==== 
 + 
 +Often times, methods exist that are just delegating to some other method, either in the same object or a composed object.  These are also good candidates for a more compact syntax.  For example, here's some code pulled from the Drupal database layer's Select query builder.  (These are all real methods; I've just stripped out the comments and converted them to PSR-12 style.) 
 + 
 +<code php> 
 +class Select extends Query implements SelectInterface 
 +
 +  public function hasTag($tag)  
 +  { 
 +    return isset($this->alterTags[$tag]); 
 +  } 
 + 
 +  public function hasAllTags()  
 +  { 
 +    return !(boolean) array_diff(func_get_args(), array_keys($this->alterTags)); 
 +  } 
 + 
 +  public function hasAnyTag()  
 +  { 
 +    return (boolean) array_intersect(func_get_args(), array_keys($this->alterTags)); 
 +  } 
 + 
 +  public function getMetaData($key)  
 +  { 
 +    return isset($this->alterMetaData[$key]) ? $this->alterMetaData[$key] : NULL; 
 +  } 
 +   
 +  public function &havingConditions()  
 +  { 
 +    return $this->having->conditions(); 
 +  } 
 + 
 +  public function havingArguments() 
 +  { 
 +    return $this->having->arguments(); 
 +  } 
 + 
 +  public function havingCompile(Connection $connection)  
 +  { 
 +    $this->having->compile($connection, $this); 
 +  } 
 + 
 +  public function &getFields() 
 +   { 
 +    return $this->fields; 
 +  } 
 + 
 +  public function &getExpressions()  
 +  { 
 +    return $this->expressions; 
 +  } 
 + 
 +  public function &getOrderBy()  
 +  { 
 +    return $this->order; 
 +  } 
 + 
 +  public function &getGroupBy()  
 +  { 
 +    return $this->group; 
 +  } 
 + 
 +  public function &getTables() 
 +   { 
 +    return $this->tables; 
 +  } 
 + 
 +  public function &getUnion()  
 +  { 
 +    return $this->union; 
 +  } 
 + 
 +  public function escapeLike($string) 
 +   { 
 +    return $this->connection->escapeLike($string); 
 +  } 
 + 
 +  public function escapeField($string)  
 +  { 
 +    return $this->connection->escapeField($string); 
 +  } 
 + 
 +  public function isPrepared()  
 +  { 
 +    return $this->prepared; 
 +  } 
 +   
 +  public function join($table, $alias = NULL, $condition = NULL, $arguments = [])  
 +  { 
 +    return $this->addJoin('INNER', $table, $alias, $condition, $arguments); 
 +  } 
 + 
 +  public function innerJoin($table, $alias = NULL, $condition = NULL, $arguments = [])  
 +  { 
 +    return $this->addJoin('INNER', $table, $alias, $condition, $arguments); 
 +  } 
 + 
 +  public function leftJoin($table, $alias = NULL, $condition = NULL, $arguments = []) 
 +  { 
 +    return $this->addJoin('LEFT OUTER', $table, $alias, $condition, $arguments); 
 +  } 
 + 
 +  // ... And lots of other multi-line methods we don't care about for now. 
 + 
 +
 +</code> 
 + 
 +That can collapse to this (a bit reordered): 
 + 
 +<code php> 
 +class Select extends Query implements SelectInterface 
 +
 +  public function hasTag($tag) => isset($this->alterTags[$tag]); 
 + 
 +  public function hasAllTags()  
 +    => !(boolean) array_diff(func_get_args(), array_keys($this->alterTags)); 
 + 
 +  public function hasAnyTag()  
 +    => (boolean) array_intersect(func_get_args(), array_keys($this->alterTags)); 
 + 
 +  public function getMetaData($key)  
 +    => isset($this->alterMetaData[$key]) ? $this->alterMetaData[$key] : NULL; 
 +   
 +  public function &havingConditions() => $this->having->conditions(); 
 + 
 +  public function havingArguments() => $this->having->arguments(); 
 + 
 +  public function havingCompile(Connection $connection)  
 +    => $this->having->compile($connection, $this); 
 + 
 +  public function &getFields() => $this->fields; 
 + 
 +  public function &getExpressions() => $this->expressions; 
 + 
 +  public function &getOrderBy() => $this->order; 
 + 
 +  public function &getGroupBy() => $this->group; 
 + 
 +  public function &getTables() => $this->tables; 
 + 
 +  public function &getUnion() => $this->union; 
 + 
 +  public function escapeLike($string) => $this->connection->escapeLike($string); 
 + 
 +  public function escapeField($string) => $this->connection->escapeField($string); 
 + 
 +  public function isPrepared() => $this->prepared; 
 +   
 +  public function join($table, $alias = NULL, $condition = NULL, $arguments = [])  
 +    => $this->addJoin('INNER', $table, $alias, $condition, $arguments); 
 + 
 +  public function innerJoin($table, $alias = NULL, $condition = NULL, $arguments = []) 
 +    => $this->addJoin('INNER', $table, $alias, $condition, $arguments); 
 + 
 +  public function leftJoin($table, $alias = NULL, $condition = NULL, $arguments = []) 
 +    => $this->addJoin('LEFT OUTER', $table, $alias, $condition, $arguments); 
 + 
 +  // ... And lots of other multi-line methods we don't care about for now. 
 + 
 +
 +</code> 
 + 
 +Which is much more compact, still quite readable, and makes the delegation more obvious. 
 + 
 +==== Syntax decisions ==== 
 + 
 +The => operator has de facto become the "maps to this expression" operator: Short lambdas use it, match() uses it, array literals use it...  It seemed the natural choice.  Anything else would have been more confusing. 
 + 
 +The use of the ''fn'' keyword was also considered, but rejected.  In context, ''fn'' currently indicates that auto-capture will happen for variables from the lexical scope.  (See the discussion above.)  However, a named function has no meaningful values to capture, making ''function'' more appropriate. 
 + 
 +===== Related RFCs ===== 
 + 
 +A number of other RFCs in active consideration would complement short functions.  They may or may not pass, but if they did then they would benefit from short functions without any further effort. 
 + 
 +==== Piped functions ==== 
 + 
 +The [[rfc:pipe-operator-v2|pipe operator]] |> is still pending in an RFC, but the feedback on it before was generally positive.  Short functions would allow a function to be easily defined as the composition of several other functions.
  
 <code php> <code php>
Line 177: Line 440:
 </code> </code>
  
-Which is a really nice way to build up a pipeline through composition.  (Modulo PHP's clumsy way of referencing functions by name, which is a separate matter being addressed elsewhere.)+Which is a really nice way to build up a pipeline through composition.  Modulo PHP's clumsy way of referencing functions by name, which is a separate matter that would be addressed by the [[rfc:partial_function_application|Partial Function Application]] RFC The three RFCs together would allow for this:
  
-==== Syntax decisions ====+<code php> 
 +function doAThing(User $u) => $u 
 +    |> step1(?)  
 +    |> step2(?) 
 +    |> step3($val, ?)  
 +    |> step4(?, $var) 
 +
 +</code>
  
-The => operator has de facto become the "maps to this expression" operator: Short lambdas use it, match() uses it, array literals use it...  It seemed the natural choice.  Anything else would have been more confusing. 
  
-I opted to not change the "function" keywordmainly because it was unnecessary The only thing it could have changed to would be "fn"but that would have been confusing with short lambdas.  It would also require more in-depth changes to the lexer rules for methods, as they are defined in a different way to functions so changing function to fn there would involve more invasive changes.+==== clone-with ==== 
 + 
 +Although no formal RFC has been proposedMáté had discussed a ''clone with'' syntax ([[https://github.com/php/php-src/pull/6538|code]][[https://externals.io/message/112624|thread]]) that would create a useful, single-expression clone-with-modification operation.  That is an excellent candidate for short-function "withX" methods
 + 
 +<code php> 
 +class Point 
 +
 +    public function __construct(private int $xprivate int $y) {} 
 + 
 +    public function getX(): int => $this->x; 
 +    public function getY(): int => $this->y; 
 +     
 +    public function withX($x): static => clone($this) with {x: $x}; 
 +    public function withY($y): static => clone($this) with {y: $y}; 
 +
 +</code> 
 + 
 +Thus making many cases of wither methods just as trivial to write as getter methods.
  
 ===== Backward Incompatible Changes ===== ===== Backward Incompatible Changes =====
Line 192: Line 478:
  
 PHP 8.1. PHP 8.1.
- 
  
 ===== Open Issues ===== ===== Open Issues =====
Line 201: Line 486:
  
 This is a simple up-or-down vote, requiring 2/3 approval to pass. This is a simple up-or-down vote, requiring 2/3 approval to pass.
 +
 +Voting started 2021-05-31 and closes 2021-06-14.
 +
 +<doodle title="Include short-function syntax in PHP" auth="crell" voteType="single" closed="true">
 +   * Yes
 +   * No
 +</doodle>
  
 ===== Patches and Tests ===== ===== Patches and Tests =====
Line 206: Line 498:
 [[https://github.com/php/php-src/pull/6221|Pull request with the code.]] [[https://github.com/php/php-src/pull/6221|Pull request with the code.]]
  
-Pull request for the spec still to come. 
- 
-===== 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 ===== 
-Keep this updated with features that were discussed on the mail lists. 
rfc/short-functions.1603138833.txt.gz · Last modified: 2020/10/19 20:20 by crell