rfc:short-functions

PHP RFC: Short Functions

Introduction

Short lambdas / arrow functions offer a convenient, compact way to write simple closures as a single expression. This RFC offers the same convenience for named functions and methods that are simple return expressions.

Proposal

This RFC provides an alternate, abbreviated syntax for functions and methods, designed to mimic the syntax of short lambdas. Specifically, the first function below is semantically identical to the second:

function add(int $a, int $b): int => $a + $b;
 
function add(int $a, int $b): int 
{
    return $a + b;
}

The same abbreviated form is also available for methods. Both of the following methods are semantically identical.

class Adder
{
    public function __construct(private int $val) {}
 
    public function add(int $in): int => $in + $this->val;
 
    public function add(int $in): int {
        return $in + $this->val;
    }
}

More precisely, the form of a short function/method is:

function ($params): returnType => expression;

Where “expression” is any valid PHP expression, the evaluated value of which will be returned by the function. That is the same semantic behavior as for the body of a short-lambda.

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.

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.

Expressions are becoming increasingly capable, too. match() expressions and throw expressions in PHP 8.0, plus proposals such as PHP RFC: Pipe Operator v2, are collectively making it easier to write expressive expressions. This improvement is a part of that larger trend.

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.

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.

Match functions

A function that encapsulates a match() expression.

function pick_one(int $a) 
{
    return match($a) {
        1 => 'One',
        2 => 'Two',
        3 => 'Three',
        default => 'More',
    };
}

vs.

function pick_one(int $a) => match($a) {
    1 => 'One',
    2 => 'Two',
    3 => 'Three',
    default => 'More',
};
 
print pick_one(1) . PHP_EOL;

Getter 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.

class Person
{
    public function __construct(
        private string $firstName, 
        private string $lastName,
    ) {}
 
    public function getFirstName(): string
    {
        return $this->firstName;
    }
 
    public function getLastName(): string
    {
        return $this->lastName;
    }
 
    public function getFullName(): string
    {
        return $this->firstName . ' ' . $this->lastName;
    }
}

vs.

class Person
{
    public function __construct(
        private string $firstName, 
        private string $lastName,
    ) {}
 
    public function getFirstName(): string => $this->firstName;
 
    public function getLastName(): string => $this->lastName;
 
    public function getFullName(): string => $this->firstName . ' ' . $this->lastName;
}

Functional code

function addUp(array $vals) 
{
    return array_reduce($vals, fn($x, $col) => $coll + $x, 0);
}

vs.

function addUp(array $vals) 
    => array_reduce($vals, fn($x, $col) => $coll + $x, 0);

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.

It's also useful for basic API operations that can be expressed in terms of other basic API operations:

function str_contains(string $haystack, string $needle): bool => strpos($haystack, $needle) !== false;

Piped functions

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.

function doAThing(User $u)
{
    return $u |> 'step1' |> 'step2' |> 'step3' |> 'step4';
}

vs.

function doAThing(User $u) => $u
    |> 'step1' 
    |> 'step2' 
    |> 'step3' 
    |> 'step4'
;

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.)

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.

if ($this->isAdmin() || ($this->hasPermission('foo') && $this->hasPermission('bar'))) {
    // ...
}

Gets factored out to:

if ($this->isGroupModerator()) {
    // ...
}
 
// ...
 
protected function isGroupModerator(): bool
    => $this->isAdmin() || ($this->hasPermission('foo') && $this->hasPermission('bar'));

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.)

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.
 
}

That can collapse to this (a bit reordered):

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.
 
}

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 author favors using “function” as a keyword for all named functions rather than “fn”, on the grounds that it would introduce additional visual clutter in classes that make use of both short and long functions, and increase the work when a short function needs to be modified into a long function. However, if the consensus is to use “fn” instead that is also possible. An alternate PR that does so, courtesy of Sara Golemon, is also available below. It also confines all its changes to the lexer so all the same arguments in its favor apply.

As of this time, this RFC is to use “function” for short named functions.

Backward Incompatible Changes

None. This would have been a syntax error in the past.

Proposed PHP Version(s)

PHP 8.1.

Open Issues

None?

Proposed Voting Choices

This is a simple up-or-down vote, requiring 2/3 approval 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

Keep this updated with features that were discussed on the mail lists.

rfc/short-functions.txt · Last modified: 2020/10/23 21:48 by crell