diff --git a/docs/design/README.md b/docs/design/README.md index 26a0e3a7fa288..59a6f1048430f 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -3793,7 +3793,11 @@ the critical underpinnings of such abstractions. #### Lambdas -> **TODO:** +> **TODO:** References need to be evolved. Needs a detailed design and a high +> level summary provided inline. + +> References: [Lambdas](lambdas.md), +> [Proposal #3848: Lambdas](https://github.com/carbon-language/carbon-lang/pull/3848) #### Co-routines diff --git a/docs/design/lambdas.md b/docs/design/lambdas.md new file mode 100644 index 0000000000000..efaf51cbc4dcb --- /dev/null +++ b/docs/design/lambdas.md @@ -0,0 +1,487 @@ +# Lambdas + + + + + +## Table of contents + +- [Syntax Overview](#syntax-overview) + - [Return type](#return-type) + - [Return expression](#return-expression) + - [Explicit return type](#explicit-return-type) + - [No return](#no-return) + - [Implicit parameters in square brackets](#implicit-parameters-in-square-brackets) + - [Parameters](#parameters) + - [Syntax defined](#syntax-defined) +- [Positional parameters](#positional-parameters) + - [Positional parameter restrictions](#positional-parameter-restrictions) +- [Captures](#captures) + - [Capture modes](#capture-modes) + - [Default capture mode](#default-capture-mode) +- [Function fields in lambdas](#function-fields-in-lambdas) +- [Copy semantics](#copy-semantics) +- [Self and recursion](#self-and-recursion) +- [Alternatives considered](#alternatives-considered) + + + +## Syntax Overview + +One goal of Carbon's lambda syntax is to have continuity between lambdas and +function declarations. Below are some example declarations: + +Implicit return types: + +```carbon +// In a variable: +let lambda: auto = fn => T.Make(); +// Equivalent in C++23: +// const auto lambda = [] { return T::Make(); }; + +// As an argument to a function call: +Foo(10, 20, fn => T.Make()); +// Equivalent in C++23: +// Foo(10, 20, [] { return T::Make(); }); +``` + +Explicit return types: + +```carbon +// In a variable: +let lambda: auto = fn -> T { return T.Make(); }; +// Equivalent in C++23: +// const auto lambda = [] -> T { return T::Make(); }; + +// As an argument to a function call: +PushBack(my_list, fn -> T { return T.Make() }); +// Equivalent in C++23: +// PushBack(my_list, [] { return T::Make(); }); +``` + +### Return type + +There are three options for how a lambda expresses its return type, parallel to +[how function declarations express returns](functions.md#return-clause): using a +return expression, using an explicit return type, or having no return. + +#### Return expression + +A return expression is introduced with a double arrow (`=>`) followed by an +expression describing the function's return value. In this case, the return type +is determined by the type of the expression, as if the return type was `auto`. + +```carbon +// In a variable: +let lambda: auto = fn => T.Make(); +// Equivalent in C++23: +// const auto lambda = [] { return T::Make(); }; + +// As an argument to a function call: +Foo(fn => T.Make()); +// Equivalent in C++23: +// Foo([] { return T::Make(); }); +``` + +#### Explicit return type + +An explicit return type is introduced with a single arrow (`->`), followed by +the return type, and finally the body of the lambda with a sequence of +statements enclosed in curly braces (`{`...`}`). + +```carbon +// In a variable: +let lambda: auto = fn -> T { return T.Make(); }; +// Equivalent in C++23: +// const auto lambda = [] -> T { return T::Make(); }; + +// As an argument to a function call: +Foo(fn -> T { return T.Make(); }); +// Equivalent in C++23: +// Foo([] -> T { return T::Make(); }); +``` + +#### No return + +Lambdas that don't return anything end with a body of statements in curly braces +(`{`...`}`). + +```carbon +// In a variable: +let lambda: auto = fn { Print(T.Make()); }; +// Equivalent in C++23: +// const auto lambda = [] -> void { Print(T::Make()); }; + +// As an argument to a function call: +Foo(fn { Print(T.Make()); }); +// Equivalent in C++23: +// Foo([] -> void { Print(T::Make()); }); +``` + +### Implicit parameters in square brackets + +Lambdas support [captures](#captures), [fields](#function-fields-in-lambdas) and +deduced parameters in the square brackets. + +```carbon +fn Foo(x: i32) { + // In a variable: + let lambda: auto = fn [var x, var y: i32 = 0] { Print(++x, ++y); }; + // Equivalent in C++23: + // const auto lambda = [x, y = int32_t{0}] mutable -> void { Print(++x, ++y); }; + + // As an argument to a function call: + Foo(fn [var x, var y: i32 = 0] { Print(++x, ++y); }); + // Equivalent in C++23: + // Foo([x, y = int32_t{0}] mutable -> void { Print(++x, ++y); }); +} +``` + +### Parameters + +Lambdas also support so-called ["positional parameters"](#positional-parameters) +that are defined at their point of use using a dollar sign and a non-negative +integer. They are implicitly of type `auto`. + +```carbon +fn Foo() { + let lambda: auto = fn { Print($0); }; + // Equivalent in C++23: + // auto lambda = [](auto _0, auto...) -> void { Print(_0); }; + // Equivalent in Swift: + // let lambda = { Print($0) }; +} +``` + +Of course, lambdas can also have named parameters, but a single lambda can't +have both named and positional parameters. + +```carbon +fn Foo() { + // In a variable: + let lambda: auto = fn (v: auto) { Print(v); }; + // Equivalent in C++23: + // const auto lambda = [](v: auto) -> void { Print(v); }; + + // As an argument to a function call: + Foo(fn (v: auto) { Print(v); }); + // Equivalent in C++23: + // Foo([](v: auto) { Print(v); }); +} +``` + +And in additional the option between positional and named parameters, deduced +parameters are always permitted. + +```carbon +fn Foo() { + let lambda: auto = fn [T:! Printable](t: T) { Print(t); }; +} +``` + +### Syntax defined + +Lambda expressions have one of the following syntactic forms (where items in +square brackets are optional and independent): + +`fn`\[_implicit-parameters_\] \[_tuple-pattern_\] `=>` _expression_ + +`fn` \[_implicit-parameters_\] \[_tuple-pattern_\] \[`->` _return-type_\] `{` +_statements_ `}` + +The first form is a shorthand for the second: "`=>` _expression_" is equivalent +to "`-> auto { return` _expression_ `; }`". + +_implicit-parameters_ consists of square brackets enclosing a optional default +capture mode and any number of explicit captures, function fields, and deduced +parameters, all separated by commas. The default capture mode (if any) must come +first; the other items can appear in any order. If _implicit-parameters_ is +omitted, it is equivalent to `[]`. + +Function definitions are distinguished from lambdas by the presence of a name +after the `fn` keyword. + +The presence of _tuple-pattern_ determines whether the function body uses named +or positional parameters. + +The presence of "`->` _return-type_" determines whether the function body can +(and must) return a value. + +To understand how the syntax between lambdas and function declarations is +reasonably "continuous", refer to this table of syntactic positions and the +following code examples. + +| Syntactic Position | Syntax Allowed in Given Position (optional, unless otherwise stated) | +| :----------------: | :------------------------------------------------------------------------------------------------------------------: | +| A1 | Required Returned Expression ([positional parameters](#positional-parameters) allowed) | +| A2 | Required Returned Expression ([positional parameters](#positional-parameters) disallowed) | +| B | [Default capture mode](#default-capture-mode) | +| C | Explicit [Captures](#captures), [Function fields](#function-fields-in-lambdas) and Deduced Parameters (in any order) | +| D | Explicit Parameters | +| E1 | Body of Statements (no return value) ([positional parameters](#positional-parameters) allowed) | +| E2 | Body of Statements (with return value) ([positional parameters](#positional-parameters) allowed) | +| E3 | Body of Statements (no return value) ([positional parameters](#positional-parameters) disallowed) | +| E4 | Body of Statements (with return value) ([positional parameters](#positional-parameters) disallowed) | +| F | Required Return Type | + +```carbon +// Lambdas (all the following are in an expression context and are +// themselves expressions) + +fn => A1 + +fn [B, C] => A1 + +fn (D) => A2 + +fn [B, C](D) => A2 + +fn { E1; } + +fn -> F { E2; } + +fn [B, C] { E1; } + +fn [B, C] -> F { E2; } + +fn (D) { E3; } + +fn (D) -> F { E4; } + +fn [B, C](D) { E3; } + +fn [B, C](D) -> F { E4; } +``` + +## Positional parameters + +Positional parameters, denoted by a dollar sign followed by a non-negative +integer (for example, $3), are auto-typed parameters defined within the lambda's +body. + +```carbon +let lambda: auto = fn => $0 +``` + +They can be used in any lambda declaration that lacks an explicit parameter list +(parentheses). They are variadic by design, meaning an unbounded number of +arguments can be passed to any function that lacks an explicit parameter list. +Only the parameters that are named in the body will be read from, meaning the +highest named parameter denotes the minimum number of arguments required by the +function. The lambda body is free to omit lower-numbered parameters (ex: +`fn { Print($10); }`). + +This syntax was inpsired by Swift's +[Shorthand Argument Names](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/#Shorthand-Argument-Names). + +```carbon +// A lambda that takes two positional parameters being used as a comparator +Sort(my_list, fn => $0.val < $1.val); +// In Swift: { $0.val < $1.val } +``` + +### Positional parameter restrictions + +Lambdas with positional parameters have the restriction that they can only be +used in a context where there is exactly one enclosing function or lambda that +has no explicit parameter list. For example: + +```carbon +fn Foo1 { + // ❌ Invalid: Foo1 is already using positional parameters + let lambda: auto = fn => $0 < $1 +} + +fn Foo2 { + my_list.Sort( + // ❌ Invalid: Foo2 is already using positional parameters + fn => $0 < $1 + ); +} + +fn Foo3() { + my_list.Sort( + // ✅ Valid: Foo3 has explicit parameters + fn => $0 < $1 + ); +} + +fn Foo4() { + let lambda: auto = fn -> bool { + // ❌ Invalid: Outer lambda is already using positional parameters + return (fn => $0 < $1)($0, $1); + }; +} + +fn Foo5() { + let lambda: auto = fn (x: i32, y: i32) -> bool { + // ✅ Valid: Outer lambda has explicit parameters + return (fn => $0 < $1)(x, y); + }; +} +``` + +## Captures + +Captures in Carbon mirror the non-init captures of C++. A capture declaration +consists of a capture mode (for `var` captures) followed by the name of a +binding from the enclosing scope, and makes that identifier available in the +inner function body. The lifetime of a capture is the lifetime of the function +in which it exists. For example... + +```carbon +fn Foo() { + let handle: Handle = Handle.Get(); + var thread: Thread = Thread.Make(fn [var handle] { handle.Process(); }); + thread.Join(); +} +``` + +```carbon +fn Foo() { + let handle: Handle = Handle.Get(); + fn MyThread[handle]() { handle.Process(); } + var thread: Thread = Thread.Make(MyThread); + thread.Join(); +} +``` + +### Capture modes + +Lambdas can capture variables from their surrounding scope using `let` or `var`, +just like regular bindings. + +Capture modes can be used as +[default capture mode specifiers](#default-capture-mode) or for explicit +captures as shown in the example code below. + +```carbon +fn Example() { + var a: i32 = 0; + var b: i32 = 0; + +let lambda: auto = fn [a, var b] { + // ❌ Invalid: by-value captures are immutable (default `let`) + a += 1; + // ✅ Valid: `b` is a mutable copy (captured with `var`) + b += 1; +}; + + lambda(); +} +``` + +```carbon +fn Example { + fn Invalid() -> auto { + var s: String = "Hello world"; + return fn [s]() => s; + } + + // ❌ Invalid: returned lambda references `s` which is no longer alive + // when the lambda is invoked. + Print(Invalid()()); +} +``` + +Note: If a function object F has mutable state, either because it has a +by-object capture or because it has a by-object function field, then a call to F +should require the callee to be a reference expression rather than a value +expression. We need a mutable handle to the function in order to be able to +mutate its mutable state. + +### Default capture mode + +By default, there is no capturing in lambdas. The lack of any square brackets is +the same as an empty pair of square brackets. Users can opt into capturing +behavior. This is done either by way of individual explicit captures, or more +succinctly by way of a default capture mode. The default capture mode roughly +mirrors the syntax `[=]` and `[&]` capture modes from C++ by being the first +thing to appear in the square brackets. + +```carbon +fn Foo1() { + let handle: Handle = Handle.Get(); + fn MyThread[var]() { + // `handle` is captured by-object due to the default capture + // mode specifier of `var` + handle.Process(); + } + var thread: Thread = Thread.Make(MyThread); + thread.Join(); +} + +fn Foo2() { + let handle: Handle = Handle.Get(); + fn MyThread[let]() { + // `handle` is captured by-value due to the default capture + // mode specifier of `let` + handle.Process(); + } + var thread: Thread = Thread.Make(MyThread); + thread.Join(); +} +``` + +## Function fields in lambdas + +Function fields in lambdas mirror the behavior of init captures in C++. A +function field definition consists of an irrefutable pattern, `=`, and an +initializer. It matches the pattern with the initializer when the lambda +definition is evaluated. The bindings in the pattern have the same lifetime as +the function, and their scope extends to the end of the function body. + +```carbon +fn Foo() { + var h1: Handle = Handle.Get(); + var h2: Handle = Handle.Get(); + var thread: Thread = Thread.Make(fn [a: auto = h1, var b: auto = h2] { + a.Process(); + b.Process(); + }); + thread.Join(); +} +``` + +## Copy semantics + +To mirror the behavior of C++, lambdas will be as copyable as their contained +function fields and function captures. This means that, if a function holds a +by-object function field, if the type of the field is copyable, so too is the +function that contains it. This also applies to captures. + +The other case is by-value function fields. Since C++ const references, when +made into fields of a class, prevent the class from being copied assigned, so +too should by-value function fields prevent the function in which it is +contained from being copied assigned. + +## Self and recursion + +To mirror C++'s use of capturing `this`, `self` should always come from the +outer scope as a capture. `self: Self` is never permitted on lambdas. + +```carbon +// ❌ Not allowed +let lambda: auto = fn [self: Self] { self.F(); }; + +// ✅ Captures `self` from outer scope +let lambda: auto = fn [self] { self.F(); }; +``` + +Note: Following +[#3720](https://github.com/carbon-language/carbon-lang/pull/3720), an expression +of the form `x.(F)`, where `F` is a function with a `self` or `addr self` +parameter, produces a callable that holds the value of `x`, and does not hold +the value of `F`. As a consequence, we can't support combining captures and +function fields with a `self` parameter. + +## Alternatives considered + +- [Terse vs Elaborated](/proposals/p3848.md#alternative-considered-terse-vs-elaborated) +- [Sigil](/proposals/p3848.md#alternative-considered-sigil) +- [Additional Positional Parameter Restriction](/proposals/p3848.md#alternative-considered-additional-positional-parameter-restriction) +- [Recursive Self](/proposals/p3848.md#alternative-considered-recursive-self)