Alternative abbreviated lambdas
Introduction
This paper provides an alternative abbreviated lambda expression syntax, as well as an alternative syntax for transparent functions, compared to [@P0573R2].
Motivation and scope
[@P0573R2] introduces a notion of transparent functions, which aim to behave as close as possible to as if their body was directly inserted into the caller (except that their arguments are only computed a single time).
As described in the mentioned proposal, current function declarations and lambda-expressions have multiple problems with transparent functions:
- They take arguments and return by value by default (
auto
), while we usually want to accept forwarding references and retain ref-qualifier for the return type (decltype(auto)
). Copies should be explicit - They are not
noexcept
-correct, i.e. they are not automaticallynoexcept
if all the expressions within their body are non-throwing - They aren't SFINAE-friendly by default
There are additional problems not mentioned in [@P0573R2]:
- Function declarations are not
constexpr
-correct, i.e. they aren't automaticallyconstexpr
if they satisfy the requirements for a constexpr function - Another problem is discussed in the following subsection
Boilerplate
Lambda expressions are meant to reduce boilerplate, but today's lambdas, especially short ones, are cumbersome to use. Compare to other programming languages:
- A lambda returning 42:
- In Haskell:
\() -> 42
(ignoring the nuances) - In Java:
() -> 42
- In Kotlin:
{ 42 }
- In Swift:
{ 42 }
- In C#:
() => 42
- In Rust:
|| 42
- In C++:
[] { return 42; }
- In Haskell:
- A lambda multiplying its argument by 2:
- In Haskell:
\x -> x * 2
- In Java:
x -> x * 2
- In Kotlin:
{ x -> x * 2 }
- In Swift:
{ x in x * 2 }
- In C#:
x => x * 2
- In Rust:
|x| x * 2
- In C++:
[](auto&& x) { return x * 2; }
- In Haskell:
- A lambda that adds its arguments:
- In Haskell:
\x y -> x + y
- In Java:
(x, y) -> x + y
- In Kotlin:
{ x, y -> x + y }
- In Swift:
{ x, y in x + y }
- In C#:
(x, y) => x + y
- In Rust:
|x, y| x + y
- In C++:
[](auto&& x, auto&& y) { return x + y; }
- In Haskell:
Other programming languages:
- Do not require an explicit capture clause, assuming capture by reference
- Do not require explicit parameter types, inferring them from context or assuming most general types
- Do not require an explicit
return
, assuming it - Some languages have special short forms for zero and single-parameter lambdas
[@P0927R2] discusses how their "implicit lambdas" could be replaced with a lambda-based approach, but the syntax for a zero-parameter lambda expression would need to be as terse as possible.
Proposed solution
The primary proposed syntax is |param1, param2, param3| expr
, as in Rust.
Such a lambda:
- Assumes capture by reference
[&]
, except when the lambda is non-local, then assumes no capture[]
- Assumes
auto&&
declarators for all the parameters (attributes are allowed on the parameters) - Is equivalent to a normal lambda with a single
return
statement, except when the return type isvoid
, then it is equivalent to a normal lambda with an expression-statement - Avoids copies by deducing the return type using
decltype((expr))
. Users will have to make copies explicitly where appropriate, that could be done usingauto
operator proposed by [@P0849R2] - Is SFINAE-friendly
- Is
noexcept
-correct: marked asnoexcept
unless its expression is potentially-throwing
Capture customization is possible using |param1, param2, param3| [captures] expr
syntax.
Optional extension: abbreviated lambdas with multiple statements
The syntax in this case is:
|params| [optional-captures] { statement… optional-expr }
Such a lambda:
- Has all the traits of a single-expression abbreviated lambda, except that…
- Is not SFINAE-friendly
- Implicitly
return
s the tailing (semicolon-less) expression, unless there is none such - Deduces the return type using the first
return
statement or, if there is none such, the trailing expression
Optional extension: transparent function declarations
The syntax in this case is:
auto f(auto&& x, auto&& y) transparent { statement… optional-expr }
Such a function:
- Is SFINAE-friendly iff the body only consists of the
expr
- Is
noexcept
-correct - Is
constexpr
-correct - Implicitly
return
s the trailing (semicolon-less) expression, unless there is none such - Avoids copies by deducing the return type using
decltype((expr-in-first-return))
. Users will have to make copies explicitly where appropriate, that could be done usingauto
operator proposed by [@P0849R2]
Discussion
Syntax choice
For the purposes of integration with SFINAE-friendliness and noexcept
-correctness of [@P0573R2], we will only discuss single-expression lambdas.
[@P0927R2] and some other applications require that the lambda syntax is as brief as possible.
Any abbreviated lambda expression syntax must have a list of parameters (which may consist of zero or one parameter) and an expression, which is its body. It will therefore have a general form of:
… param1, param2, param3 … expr …
Because param1
would otherwise create an ambiguity (consider usage of lambda expression as a higher-order function argument), some separator is required before param1
. For clarity when reading, some separator should be required before expr
. A separator after expr
is not required; expr
is then defined to be an assignment-expression. The choice then boils down to choosing the appropriate "framing" of parameters. Several choices have been reviewed:
(x, y) f(x)
is ambiguous with a C-style cast (single-parameter case) or with a comma-expression (multiple-parameter case)[x, y] f(x)
is ambiguous with normal lambda expressions{x, y} f(x)
is ambiguous with initializer-lists<x, y> f(x)
visually conflicts with<…>
usually meaning templates in C++|x, y| f(x)
is potentially ambiguous with|
and||
operators, but not actually, because those are invalid where a lambda-expression can start