diff --git a/proposals/p3848.md b/proposals/p3848.md index a8869477cc600..6707a9c5f7837 100644 --- a/proposals/p3848.md +++ b/proposals/p3848.md @@ -18,8 +18,8 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - [Positional Parameters](#positional-parameters) - [Positional Parameter Restrictions](#positional-parameter-restrictions) - [Function Captures](#function-captures) - - [Default Capture Mode](#default-capture-mode) - [Capture Modes](#capture-modes) + - [Default Capture Mode](#default-capture-mode) - [Function Fields](#function-fields) - [Copy Semantics](#copy-semantics) - [Self and Recursion](#self-and-recursion) @@ -29,12 +29,12 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception ## Abstract -To support migration from C++ to Carbon, there must be valid syntax to capture -the behavior of C++ lambdas. They are defined at their point of use and are -often anonymous, meaning replacing them solely with function declarations will -create an ergonomic burden compounded by the need for the migration tool to -select a name. This proposal proposes a path forward to add lambdas to Carbon -and augment function declarations accordingly. +This document proposes a path forward to add lambdas to Carbon. It further +proposes augmenting function declarations to create a more continuous syntax +between the two categories of functions. In short, both lambdas and function +declarations will be introduced with the `fn` keyword. The presence of a name +distinguishes a declaration from a lambda expression, and the rest of the syntax +applies to both kinds. See [Syntax Overview](#syntax-overview) for more. ## Syntax Overview @@ -79,8 +79,10 @@ let lambda: auto = fn { Print(T.Make()); }; fn FunctionDeclaration { Print(T.Make()); } ``` -Functions support [captures](#function-captures) and [fields](#function-fields) -in the square brackets. +Functions support [captures](#function-captures), [fields](#function-fields) and +deduced parameters in the square brackets. In addition, `self: Self` or +`addr self: Self*` can be added to the square brackets of function declarations +that exist inside class or interface definitions. ``` fn Foo(x: X) { @@ -103,7 +105,8 @@ fn Foo() { } ``` -This is of course in addition to supporting explicit parameters. +Of course, functions can also have named parameters, but a single function can't +have both named and positional parameters. ``` fn Foo() { @@ -123,6 +126,35 @@ fn Foo() { } ``` +

Succinctly

+ +Function definitions have one of the following syntactic forms (where items in +square brackets are optional and independent): + +`fn` \[_name_\] \[_implicit-parameters_\] \[_tuple-pattern_\] `=>` _expression_ +`;` + +`fn` \[_name_\] \[_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 `[]`. + +The presence of _name_ determines whether this is a function declaration or a +lambda expression. + +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. @@ -199,9 +231,11 @@ fn G[B, C](D) { E3; } fn G[B, C](D) -> F { E4; } ``` -**Alternative Considered**: As opposed to a continuous syntax between lambdas -and function declarations, alternatively, Carbon could adopt a few different -categories of functions. As was considered in a previous discussion doc +

Alternative Considered

+ +As opposed to a continuous syntax between lambdas and function declarations, +alternatively, Carbon could adopt a few different categories of functions. As +was considered in a previous discussion doc ([Lambdas Discussion 2](https://docs.google.com/document/d/14K_YLjChWyyNv3wv5Mn7uLFHa0JZTc21v_WP8RzC8M4/)), these categories would be terse lambdas, elaborated lambdas, and function declarations. Unfortunately, separating these categories out presented a @@ -265,11 +299,13 @@ fn FunctionDeclaration1 => T.Make(); fn FunctionDeclaration2[]() -> T { return T.Make(); } ``` -**Alternative Considered**: Introduce with a sigil, such as `$` or `@`. Since -introducer punctuation is such a scarce resource, and since there was no -consensus on what sigil would best represent a lambda, and since there was a -desire to create a more continuous syntax between lambdas and function -declarations, this alternative was decided against. +

Alternative Considered

+ +Introduce with a sigil, such as `$` or `@`. Since introducer punctuation is such +a scarce resource, and since there was no consensus on what sigil would best +represent a lambda, and since there was a desire to create a more continuous +syntax between lambdas and function declarations, this alternative was decided +against. ``` let lambda1: auto = @ => T.Make(); @@ -287,8 +323,8 @@ 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. Users are free to omit lower-numbered -parameters (ex: `fn { Print($10); }`). +arguments required by the function. The function body is free to omit +lower-numbered parameters (ex: `fn { Print($10); }`). ``` // A lambda that takes two positional parameters being used as a comparator @@ -336,10 +372,12 @@ fn Foo6() { } ``` -**Alternative Considered**: In addition to the proposed restrictions, an -additional restriction was considered. That being, visibility of functions with -positional parameters could be restricted to only non-public interfaces. **This -alternative will be put forth as a leads question before a decision is made.** +

Alternative Considered

+ +In addition to the proposed restrictions, an additional restriction was +considered. That being, visibility of functions with positional parameters could +be restricted to only non-public interfaces. **This alternative will be put +forth as a leads question before a decision is made.** ## Function Captures @@ -365,46 +403,10 @@ fn Foo() { } ``` -### Default Capture Mode - -**Proposal**: By default, there is no capturing in functions. 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 mirrors the syntax `[=]` and `[&]` capture modes from C++ (written -in Carbon as `[copy]` and `[ref]`) by being the first thing to appear in the -square brackets. - -``` -fn Foo() { - let handle: Handle = Handle.Get(); - fn MyThread[copy]() { - handle.Process(); // `handle` is copy captured due to the default capture - // mode specifier of `copy` - } - var thread: Thread = Thread.Make(MyThread); - thread.Join(); -} -``` - -**Alternative Considered**: Previously, it was proposed that the default capture -mode would come after all the explicit captures. In addition, it was proposed -that the lack of any square brackets opted the function into capturing by -default with an implicitly defined default capture mode. These behaviors do not -mirror lambdas in C++ and so were decided against. Primarily, it was recognized -that it's valuable to be able to intermix explicit captures with deduced -parameters and fields in any order that makes the most sense for the context. -Without a clear justification for a rule that says they can't intermix, the more -flexible behavior was favored. - ### Capture Modes -**Proposal**: `copy` and `ref` are the two primary forms of function captures. -They each have companion `const` versions. They are syntactically distinct from -`let` and `var`. `let` and `var` can appear in binding patterns, such as -[function fields](#function-fields), while `copy`, `ref`, `const copy` and -`const ref` can appear as function captures. The two sets of keywords cannot -appear in the same syntactic positions. +**Proposal**: `copy`, `ref`, `const copy` and `const ref` can appear as function +captures. They behave as specified in the following table: | Capture Mode Syntax | Corresponding Semantics | | :-----------------: | :--------------------------------------------------------------: | @@ -419,26 +421,20 @@ captures as shown in the example code below. ``` fn Example { - var by_copy: i32 = 0; - var by_const_copy: i32 = 0; - var by_reference: i32 = 0; - var by_const_reference: i32 = 0; + var a: i32 = 0; + var b: i32 = 0; + var c: i32 = 0; + var d: i32 = 0; - let lambda: auto = fn [copy by_copy, - const copy by_const_copy, - ref by_reference, - const ref by_const_reference] { - by_copy += 1; // ✅ Valid: by-copy captures are mutable BUT they only - // modify the instance attached to the lambda, not the - // variable declared in the outer scope. + let lambda: auto = fn [copy a, const copy b, ref c, const ref d] { + a += 1; // ✅ Valid: by-copy captures are mutable BUT they only modify the instance + // attached to the lambda, not the variable declared in the outer scope. - by_const_copy += 1; // ❌ Invalid: Cannot modify a const copy capture. + b += 1; // ❌ Invalid: Cannot modify a const copy capture. - by_reference += 1; // ✅ Valid: Modifies the variable from the - // outer scope + c += 1; // ✅ Valid: Modifies the variable from the outer scope - by_const_reference += 1; // ❌ Invalid: Cannot modify a const reference - // capture. + d += 1; // ❌ Invalid: Cannot modify a const reference capture. }; lambda(); @@ -458,10 +454,12 @@ fn Example { } ``` -**Alternative Considered**: Alternatively, the below-shown four capture modes -(by-value, by-object, by-copy \[immutable\], and by-reference \[mutable\]) could -be provided for function declarations and lambdas both as default capture modes -and as explicit capture specifiers. This was decided against because of +

Alternative Considered

+ +Alternatively, the below-shown four capture modes (by-value, by-object, by-copy +\[immutable\], and by-reference \[mutable\]) could be provided for function +declarations and lambdas both as default capture modes and as explicit capture +specifiers. This was decided against because of [Carbon's "One Way" Principle](https://github.com/carbon-language/carbon-lang/blob/trunk/docs/project/principles/one_way.md). By providing both by-object function fields and by-object captures, there would be duplicate behavior with an unclear syntactic choice forced on the user. Since @@ -516,6 +514,39 @@ fn AlternativeExample { } ``` +### Default Capture Mode + +**Proposal**: By default, there is no capturing in functions. 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 mirrors the syntax `[=]` and `[&]` capture modes from C++ (written +in Carbon as `[copy]` and `[ref]`) by being the first thing to appear in the +square brackets. + +``` +fn Foo() { + let handle: Handle = Handle.Get(); + fn MyThread[copy]() { + handle.Process(); // `handle` is copy captured due to the default capture + // mode specifier of `copy` + } + var thread: Thread = Thread.Make(MyThread); + thread.Join(); +} +``` + +

Alternative Considered

+ +Previously, it was proposed that the default capture mode would come after all +the explicit captures. In addition, it was proposed that the lack of any square +brackets opted the function into capturing by default with an implicitly defined +default capture mode. These behaviors do not mirror lambdas in C++ and so were +decided against. Primarily, it was recognized that it's valuable to be able to +intermix explicit captures with deduced parameters and fields in any order that +makes the most sense for the context. Without a clear justification for a rule +that says they can't intermix, the more flexible behavior was favored. + ## Function Fields **Proposal**: To mirror the behavior of init captures in C++, function fields @@ -535,10 +566,12 @@ fn Foo() { } ``` -**Alternative Considered**: Alternatively, by-value and by-object capturing -could serve the same purpose. This was decided against because capturing is not -as expressive as general purpose binding patterns. The lack of an initializing -expression would create an ergonomic burden. +

Alternative Considered

+ +Alternatively, by-value and by-object capturing could serve the same purpose. +This was decided against because capturing is not as expressive as general +purpose binding patterns. The lack of an initializing expression would create an +ergonomic burden. ## Copy Semantics @@ -565,12 +598,22 @@ For function declarations, it is only permitted when the function is a member of a class type, such that it refers to the class type and not to the function itself. -**Alternative Considered**: For use in recursion, `self: Self` could be -permitted on all functions and lambdas and refer to the function itself. This -was originally the plan. Unfortunately, it created a bit of a discontinuity -between class members and non-class members and was thus decided against. +

Alternative Considered

+ +For use in recursion, `self: Self` could be permitted on all functions and +lambdas and refer to the function itself. This was originally the plan. +Unfortunately, it created a bit of a discontinuity between class members and +non-class members and was thus decided against. ## Rationale -- [Code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write) -- [Interoperability with and migration from existing C++ code](/docs/project/goals.md#interoperability-with-and-migration-from-existing-c-code) +Lambdas in Carbon serve two purposes. The primary purpose is in support of the +["Code that is easy to read, understand, and write"](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write) +goal. It is because of this goal that we leverage syntactic features such as the +returned expression (indicated by `=>`) and positional parameters (indicated by +the lack of a tuple pattern of explicit parameters as well as the use of `$N` in +the body of such functions). In addition, Lambdas serve to support the +[Interoperability with and migration from existing C++ code](/docs/project/goals.md#interoperability-with-and-migration-from-existing-c-code) +goal. They are defined at their point of use and are often anonymous, meaning +replacing C++ lambdas solely with function declarations will create an ergonomic +burden compounded by the need for the migration tool to select a name.