Skip to content

Instantly share code, notes, and snippets.

@Anton3
Last active January 14, 2020 11:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Anton3/3926dc16b9815d022475cb4b680a9744 to your computer and use it in GitHub Desktop.
Save Anton3/3926dc16b9815d022475cb4b680a9744 to your computer and use it in GitHub Desktop.

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 automatically noexcept 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 automatically constexpr 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; }
  • 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; }
  • 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; }

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 is void, 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 using auto operator proposed by [@P0849R2]
  • Is SFINAE-friendly
  • Is noexcept-correct: marked as noexcept 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 returns 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 returns 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 using auto 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment