From e06337e5eb258fb6b294ce1cc27f8f8eb5d06276 Mon Sep 17 00:00:00 2001 From: CJ Johnson Date: Wed, 3 Apr 2024 12:48:07 -0400 Subject: [PATCH 01/12] Creating new proposal: Lambdas --- proposals/new-proposal.tmp.md | 87 +++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 proposals/new-proposal.tmp.md diff --git a/proposals/new-proposal.tmp.md b/proposals/new-proposal.tmp.md new file mode 100644 index 0000000000000..0e1ef1d8f6b1f --- /dev/null +++ b/proposals/new-proposal.tmp.md @@ -0,0 +1,87 @@ +# TODO + + + +[Pull request](https://github.com/carbon-language/carbon-lang/pull/####) + + + +## Table of contents + +- [TODO: Initial proposal setup](#todo-initial-proposal-setup) +- [Abstract](#abstract) +- [Problem](#problem) +- [Background](#background) +- [Proposal](#proposal) +- [Details](#details) +- [Rationale](#rationale) +- [Alternatives considered](#alternatives-considered) + + + +## TODO: Initial proposal setup + +> TIP: Run `./new_proposal.py "TITLE"` to do new proposal setup. + +1. Copy this template to `new.md`, and create a commit. +2. Create a GitHub pull request, to get a pull request number. + - Add the `proposal draft` label to the pull request. +3. Rename `new.md` to `/proposals/p####.md`, where `####` should be the pull + request number. +4. Update the title of the proposal (the `TODO` on line 1). +5. Update the link to the pull request (the `####` on line 11). +6. Delete this section. + +TODOs indicate where content should be updated for a proposal. See +[Carbon Governance and Evolution](/docs/project/evolution.md) for more details. + +## Abstract + +TODO: Describe, in a succinct paragraph, the gist of this document. This +paragraph should be reproduced verbatim in the PR summary. + +## Problem + +TODO: What problem are you trying to solve? How important is that problem? Who +is impacted by it? + +## Background + +TODO: Is there any background that readers should consider to fully understand +this problem and your approach to solving it? + +## Proposal + +TODO: Briefly and at a high level, how do you propose to solve the problem? Why +will that in fact solve it? + +## Details + +TODO: Fully explain the details of the proposed solution. + +## Rationale + +TODO: How does this proposal effectively advance Carbon's goals? Rather than +re-stating the full motivation, this should connect that motivation back to +Carbon's stated goals and principles. This may evolve during review. Use links +to appropriate sections of [`/docs/project/goals.md`](/docs/project/goals.md), +and/or to documents in [`/docs/project/principles`](/docs/project/principles). +For example: + +- [Community and culture](/docs/project/goals.md#community-and-culture) +- [Language tools and ecosystem](/docs/project/goals.md#language-tools-and-ecosystem) +- [Performance-critical software](/docs/project/goals.md#performance-critical-software) +- [Software and language evolution](/docs/project/goals.md#software-and-language-evolution) +- [Code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write) +- [Practical safety and testing mechanisms](/docs/project/goals.md#practical-safety-and-testing-mechanisms) +- [Fast and scalable development](/docs/project/goals.md#fast-and-scalable-development) +- [Modern OS platforms, hardware architectures, and environments](/docs/project/goals.md#modern-os-platforms-hardware-architectures-and-environments) +- [Interoperability with and migration from existing C++ code](/docs/project/goals.md#interoperability-with-and-migration-from-existing-c-code) + +## Alternatives considered + +TODO: What alternative solutions have you considered? From f4638751a1433cec2c657203e2bc9aec1884f281 Mon Sep 17 00:00:00 2001 From: CJ Johnson Date: Thu, 4 Apr 2024 13:54:41 -0400 Subject: [PATCH 02/12] Add content to p3848.md --- proposals/new-proposal.tmp.md | 87 ----- proposals/p3848.md | 576 ++++++++++++++++++++++++++++++++++ 2 files changed, 576 insertions(+), 87 deletions(-) delete mode 100644 proposals/new-proposal.tmp.md create mode 100644 proposals/p3848.md diff --git a/proposals/new-proposal.tmp.md b/proposals/new-proposal.tmp.md deleted file mode 100644 index 0e1ef1d8f6b1f..0000000000000 --- a/proposals/new-proposal.tmp.md +++ /dev/null @@ -1,87 +0,0 @@ -# TODO - - - -[Pull request](https://github.com/carbon-language/carbon-lang/pull/####) - - - -## Table of contents - -- [TODO: Initial proposal setup](#todo-initial-proposal-setup) -- [Abstract](#abstract) -- [Problem](#problem) -- [Background](#background) -- [Proposal](#proposal) -- [Details](#details) -- [Rationale](#rationale) -- [Alternatives considered](#alternatives-considered) - - - -## TODO: Initial proposal setup - -> TIP: Run `./new_proposal.py "TITLE"` to do new proposal setup. - -1. Copy this template to `new.md`, and create a commit. -2. Create a GitHub pull request, to get a pull request number. - - Add the `proposal draft` label to the pull request. -3. Rename `new.md` to `/proposals/p####.md`, where `####` should be the pull - request number. -4. Update the title of the proposal (the `TODO` on line 1). -5. Update the link to the pull request (the `####` on line 11). -6. Delete this section. - -TODOs indicate where content should be updated for a proposal. See -[Carbon Governance and Evolution](/docs/project/evolution.md) for more details. - -## Abstract - -TODO: Describe, in a succinct paragraph, the gist of this document. This -paragraph should be reproduced verbatim in the PR summary. - -## Problem - -TODO: What problem are you trying to solve? How important is that problem? Who -is impacted by it? - -## Background - -TODO: Is there any background that readers should consider to fully understand -this problem and your approach to solving it? - -## Proposal - -TODO: Briefly and at a high level, how do you propose to solve the problem? Why -will that in fact solve it? - -## Details - -TODO: Fully explain the details of the proposed solution. - -## Rationale - -TODO: How does this proposal effectively advance Carbon's goals? Rather than -re-stating the full motivation, this should connect that motivation back to -Carbon's stated goals and principles. This may evolve during review. Use links -to appropriate sections of [`/docs/project/goals.md`](/docs/project/goals.md), -and/or to documents in [`/docs/project/principles`](/docs/project/principles). -For example: - -- [Community and culture](/docs/project/goals.md#community-and-culture) -- [Language tools and ecosystem](/docs/project/goals.md#language-tools-and-ecosystem) -- [Performance-critical software](/docs/project/goals.md#performance-critical-software) -- [Software and language evolution](/docs/project/goals.md#software-and-language-evolution) -- [Code that is easy to read, understand, and write](/docs/project/goals.md#code-that-is-easy-to-read-understand-and-write) -- [Practical safety and testing mechanisms](/docs/project/goals.md#practical-safety-and-testing-mechanisms) -- [Fast and scalable development](/docs/project/goals.md#fast-and-scalable-development) -- [Modern OS platforms, hardware architectures, and environments](/docs/project/goals.md#modern-os-platforms-hardware-architectures-and-environments) -- [Interoperability with and migration from existing C++ code](/docs/project/goals.md#interoperability-with-and-migration-from-existing-c-code) - -## Alternatives considered - -TODO: What alternative solutions have you considered? diff --git a/proposals/p3848.md b/proposals/p3848.md new file mode 100644 index 0000000000000..a8869477cc600 --- /dev/null +++ b/proposals/p3848.md @@ -0,0 +1,576 @@ +# Lambdas + + + +[Pull request](https://github.com/carbon-language/carbon-lang/pull/3848) + + + +## Table of contents + +- [Abstract](#abstract) +- [Syntax Overview](#syntax-overview) +- [Introducer](#introducer) +- [Positional Parameters](#positional-parameters) + - [Positional Parameter Restrictions](#positional-parameter-restrictions) +- [Function Captures](#function-captures) + - [Default Capture Mode](#default-capture-mode) + - [Capture Modes](#capture-modes) +- [Function Fields](#function-fields) +- [Copy Semantics](#copy-semantics) +- [Self and Recursion](#self-and-recursion) +- [Rationale](#rationale) + + + +## 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. + +## Syntax Overview + +**Proposal**: A largely continuous syntax between lambdas and function +declarations. + +At a high level, lambdas and function declarations will look like the following. + +``` +let lambda1: auto = fn => T.Make(); + +let lambda2: auto = fn []() -> T { return T.Make(); }; + +fn FunctionDeclaration1 => T.Make(); + +fn FunctionDeclaration2[]() -> T { return T.Make(); } +``` + +There are functions which return an expression, such that the return type is +`auto`. + +``` +let lambda: auto = fn => T.Make(); + +fn FunctionDeclaration => T.Make(); +``` + +And there are functions with an explicit return type that provide a body of +statements. + +``` +let lambda: auto = fn -> T { return T.Make(); }; + +fn FunctionDeclaration -> T { return T.Make(); } +``` + +There are even functions that provide a body of statements but no return value. + +``` +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. + +``` +fn Foo(x: X) { + let lambda: auto = fn [const copy x, var y: i32 = 0] { Print(x, ++y); }; + + fn FunctionDeclaration[const copy x, var y: i32 = 0] { Print(x, ++y); } +} +``` + +Functions 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`. + +``` +fn Foo() { + let lambda: auto = fn { Print($0); }; + + fn FunctionDeclaration { Print($0); } +} +``` + +This is of course in addition to supporting explicit parameters. + +``` +fn Foo() { + let lambda: auto = fn(v: auto) { Print(v); }; + + fn FunctionDeclaration(v: auto) { Print(v); } +} +``` + +And deduced parameters. + +``` +fn Foo() { + let lambda: auto = fn[T:! Printable](t: T) { Print(t); }; + + fn FunctionDeclaration[T:! Printable](t: T) { Print(t); } +} +``` + +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](#function-captures), [Function Fields](#function-fields) 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 | +| G | Function Declaration Name | + +``` +// 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; } +``` + +``` +// Function Declarations (all the following are allowed as statements in a +// function body or as declarations in other scopes) + +fn G => A1; + +fn G[B, C] => A1; + +fn G(D) => A2; + +fn G[B, C](D) => A2; + +fn G { E1; } + +fn G -> F { E2; } + +fn G[B, C] { E1; } + +fn G[B, C] -> F { E2; } + +fn G(D) { E3; } + +fn G(D) -> F { E4; } + +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 +([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 +syntactic challenge in the form of cliffs, explained below. + +Terse lambdas were slated to be the most compact form of a lambda. Combined with +a [sigil introducer](#introducer), they would be syntactically minimal. One way +in which syntax was minimized was the granting of an **implicit** default +[capture](#function-captures) mode. If no square brackets were present, by-value +captures would be allowed. This, combined with the lack of an arrow to signify a +return value, created syntax of the following form (being passed into the filter +function). + +``` +let zero: i32 = 0; +let list_all: List(i32) = GetAllValues(); +let list_positive: List(i32) = list_all.Filter( + @ $0 > zero +); +``` + +To give users more control over the feature set in a lambda, the next step up +was an elaborated lambda. This provided the ability to add both square brackets +and explicit parameters to lambdas at the cost of more syntax. Unfortunately, +this also meant there was a bit of a syntactic cliff and a stumbling block. It +was considered desirable for empty square brackets to mean capturing is +disabled. But since the no-square-brackets form needed to support capturing for +terse lambdas, elaborated lambdas needed to both add the square brackets and +also add an explicit default capture mode at the same time just to maintain the +existing capturing behavior. The net result was code that looked like the +following (being passed into the filter function again). + +``` +let zero: i32 = 0; +let list_all: List(i32) = GetAllValues(); +let list_positive: List(i32) = list_all.Filter( + @[let](x: auto) x > zero +); +``` + +Finally, if a user wanted to upgrade a lambda to a function declaration, this +created another cliff where they needed to switch from the sigil to the `fn` +keyword, on top of adding a name. Ultimately these downsides suggested that a +continuous syntax was the better path forward, even though the minimal lambda +syntax would be a bit less terse. + +## Introducer + +**Proposal**: Introduce with the `fn` keyword to mirror function declarations. +If a statement or declaration begins with `fn`, a name is required and it +becomes a function declaration. Otherwise, if in an expression context, `fn` +introduces a lambda. + +``` +let lambda1: auto = fn => T.Make(); + +let lambda2: auto = fn []() -> T { return T.Make(); }; + +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. + +``` +let lambda1: auto = @ => T.Make(); + +let lambda2: auto = @[]() -> T { return T.Make(); }; +``` + +## Positional Parameters + +**Proposal**: Positional parameters, introduced in the body of a function by way +of the dollar sign and a corresponding non-negative parameter position integer +(ex: `$3`), are `auto` parameters to the function in which they are defined. +They can be used in any lambda or function 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. Users are free to omit lower-numbered +parameters (ex: `fn { Print($10); }`). + +``` +// A lambda that takes two positional parameters being used as a comparator +Sort(my_list, fn => $0 < $1); +``` + +### Positional Parameter Restrictions + +**Proposal**: There are two restrictions applied to functions with positional +parameters. The first restriction is that the definitions of function +declarations must be attached to the declarations. The second restriction is +that positional parameters can only be used in a context where there is exactly +one enclosing function without an explicit parameter list. For example... + +``` +fn Foo1 { + fn Bar1 {} // ❌ Invalid: Foo1 is already using positional parameters +} + +fn Foo2 { + Print($0); + fn Bar2 {} // ❌ Invalid: Foo2 is already using positional parameters +} + +fn Foo3 { + fn Bar3 { + Print($0); // ❌ Invalid: Foo3 is already using positional parameters + } +} + +fn Foo4() { + fn Bar4 { + Print($0); // ✅ Valid: Foo4 has explicit parameters + } +} + +fn Foo5 { + fn Bar5() {} // ✅ Valid: Bar5 has explicit parameters +} + +fn Foo6() { + my_list.Sort( + fn => $0 < $1 // ✅ Valid: Foo6 has explicit parameters + ); +} +``` + +**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 + +**Proposal**: Function captures in Carbon mirror the non-init captures of C++. +Bindings defined in a scope visible to a function can be captured in the +function instance, be it a function declaration or a lambda. The lifetime of a +capture is the lifetime of the function in which it exists. For example... + +``` +fn Foo() { + let handle: Handle = Handle.Get(); + var thread: Thread = Thread.Make(fn [copy handle] { handle.Process(); }); + thread.Join(); +} +``` + +``` +fn Foo() { + let handle: Handle = Handle.Get(); + fn MyThread[copy]() { handle.Process(); } + var thread: Thread = Thread.Make(MyThread); + thread.Join(); +} +``` + +### 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. + +| Capture Mode Syntax | Corresponding Semantics | +| :-----------------: | :--------------------------------------------------------------: | +| `copy` | Capture "by-copy" where the resulting capture is mutable | +| `const copy` | Capture "by-const-copy" where the resulting capture is immutable | +| `ref` | Capture "by-reference" behaving as a C++ reference | +| `const ref` | Capture "by-const-reference" behaving as a C++ const reference | + +Capture modes can be used as +[default capture mode specifiers](#default-capture-mode) or for explicit +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; + + 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. + + by_const_copy += 1; // ❌ Invalid: Cannot modify a const copy capture. + + by_reference += 1; // ✅ Valid: Modifies the variable from the + // outer scope + + by_const_reference += 1; // ❌ Invalid: Cannot modify a const reference + // capture. + }; + + lambda(); +} +``` + +``` +fn Example { + fn Invalid() -> auto { + var s: String = "Hello world"; + return fn[const ref s]() => s; + } + + // ❌ Invalid: returned lambda references `s` which is no longer alive + // when the lambda is invoked. + Print(Invalid()()); +} +``` + +**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 +we know we want to support function fields, which have the full expressive power +of every other form of binding pattern, favoring functions fields as the syntax +alone that provides that functionality seems appropriate. + +| Capture Mode Syntax | Corresponding Semantics | +| :-----------------: | :----------------------------------------------------------------: | +| `let` \* | Capture "by-value" using Carbon's notion of "by-value" | +| `var` | Capture "by-object" using Carbon's notion of "by-object" | +| `copy` | Capture "by-copy" where the resulting capture is immutable | +| `ref` | Capture "by-reference" where the capture acts like a C++ reference | + +\* let is the default behavior for captures with no mode specifier (ex: +`fn[foo] {}`). + +``` +fn AlternativeExample { + var by_value: i32 = 0; + var by_object: i32 = 0; + var by_copy: i32 = 0; + var by_reference: i32 = 0; + + let lambda: auto = fn [ref, by_value, var by_object, copy by_copy] { + by_value += 1; // ❌ Invalid: Cannot modify a value binding. + + by_object += 1; // ✅ Valid: `var`/object bindings are mutable BUT they only + // modify the instance attached to the lambda, not the + // variable declared in the outer scope. + + by_copy += 1; // ❌ Invalid: Cannot modify a copy capture. + + by_reference += 1; // ✅ Valid: Modifies the variable from the outer scope + }; + + lambda(); +} +``` + +``` +fn AlternativeExample { + fn Invalid() -> auto { + var s: String = "Hello world"; + let x: i32 = 0; + return fn[ref s, x]() => (s, x); + } + + // ❌ Invalid: returned lambda references `s` which is no longer alive when the lambda + // is invoked. It also captures `x` by value which is similarly no longer alive. + Print(Invalid()()); +} +``` + +## Function Fields + +**Proposal**: To mirror the behavior of init captures in C++, function fields +will support nothing-implies-`let` and `var` binding patterns. These will be +annotated with a type and initialized with the right-hand-side of an equals +sign. The lifetime of a function field is the same as the lifetime of the +function declaration or lambda in which it exists. + +``` +fn Foo() { + var h: Handle = Handle.Get(); + var thread: Thread = Thread.Make(fn [a: auto = h; var b: auto = h] { + a.Process(); // ❌ Invalid: Cannot call mutating methods on value bindings + b.Process(); // ✅ Valid: Can mutate object bindings + }); + thread.Join(); +} +``` + +**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 + +**Proposal**: To mirror the behavior of C++, function declarations and 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 by-copy and by-const-copy captures. + +By-reference and by-const-reference captures are a bit different, because even +if the underlying type is not copyable, the reference to that object is always +copyable. + +The final case is by-value function fields. Since C++ const references, when +made into fields of a class, prevent the class from being copied, so too should +by-value function fields prevent the function in which it is contained from +being copied. + +## Self and Recursion + +**Proposal**: 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. +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. + +## 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) From f7d461bb0f8c0ad398e913d7f14310b6055ec467 Mon Sep 17 00:00:00 2001 From: CJ Johnson Date: Tue, 16 Apr 2024 12:56:19 -0400 Subject: [PATCH 03/12] Address review comments from @geoffromer --- proposals/p3848.md | 227 +++++++++++++++++++++++++++------------------ 1 file changed, 135 insertions(+), 92 deletions(-) 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. From 4b408a233fef65dc088719a970f144e1baa73281 Mon Sep 17 00:00:00 2001 From: CJ Johnson Date: Tue, 16 Apr 2024 15:11:10 -0400 Subject: [PATCH 04/12] Additional reviewer comments addressed --- proposals/p3848.md | 57 ++++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/proposals/p3848.md b/proposals/p3848.md index 6707a9c5f7837..f64123ceb1393 100644 --- a/proposals/p3848.md +++ b/proposals/p3848.md @@ -14,15 +14,23 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - [Abstract](#abstract) - [Syntax Overview](#syntax-overview) + - [Succinctly](#succinctly) + - [Alternative Considered: Terse vs Elaborated](#alternative-considered-terse-vs-elaborated) - [Introducer](#introducer) + - [Alternative Considered: Sigil](#alternative-considered-sigil) - [Positional Parameters](#positional-parameters) - [Positional Parameter Restrictions](#positional-parameter-restrictions) + - [Alternative Considered: Additional Restriction](#alternative-considered-additional-restriction) - [Function Captures](#function-captures) - [Capture Modes](#capture-modes) + - [Alternative Considered: By-Value Captures](#alternative-considered-by-value-captures) - [Default Capture Mode](#default-capture-mode) + - [Alternative Considered: Middle vs Beginning](#alternative-considered-middle-vs-beginning) - [Function Fields](#function-fields) + - [Alternative Considered: No Function Fields](#alternative-considered-no-function-fields) - [Copy Semantics](#copy-semantics) - [Self and Recursion](#self-and-recursion) + - [Alternative Considered: Recursive Self](#alternative-considered-recursive-self) - [Rationale](#rationale) @@ -126,7 +134,7 @@ fn Foo() { } ``` -

Succinctly

+### Succinctly Function definitions have one of the following syntactic forms (where items in square brackets are optional and independent): @@ -149,8 +157,8 @@ 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 _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. @@ -231,7 +239,7 @@ fn G[B, C](D) { E3; } fn G[B, C](D) -> F { E4; } ``` -

Alternative Considered

+### Alternative Considered: Terse vs Elaborated As opposed to a continuous syntax between lambdas and function declarations, alternatively, Carbon could adopt a few different categories of functions. As @@ -299,7 +307,7 @@ fn FunctionDeclaration1 => T.Make(); fn FunctionDeclaration2[]() -> T { return T.Make(); } ``` -

Alternative Considered

+### Alternative Considered: Sigil 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 @@ -372,7 +380,7 @@ fn Foo6() { } ``` -

Alternative Considered

+#### Alternative Considered: Additional Restriction In addition to the proposed restrictions, an additional restriction was considered. That being, visibility of functions with positional parameters could @@ -381,10 +389,11 @@ forth as a leads question before a decision is made.** ## Function Captures -**Proposal**: Function captures in Carbon mirror the non-init captures of C++. -Bindings defined in a scope visible to a function can be captured in the -function instance, be it a function declaration or a lambda. The lifetime of a -capture is the lifetime of the function in which it exists. For example... +**Proposal**: Function captures in Carbon mirror the non-init captures of C++. A +function capture declaration consists of a capture mode 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... ``` fn Foo() { @@ -415,6 +424,11 @@ captures. They behave as specified in the following table: | `ref` | Capture "by-reference" behaving as a C++ reference | | `const ref` | Capture "by-const-reference" behaving as a C++ const reference | +To prevent ambiguities, captures can only exist on functions where the +definition is attached to the declaration. This means they are supported on +lambdas and they are supported on function declarations that are immediately +defined, but they are not supported on forward-declared functions. + Capture modes can be used as [default capture mode specifiers](#default-capture-mode) or for explicit captures as shown in the example code below. @@ -454,7 +468,7 @@ fn Example { } ``` -

Alternative Considered

+#### Alternative Considered: By-Value Captures Alternatively, the below-shown four capture modes (by-value, by-object, by-copy \[immutable\], and by-reference \[mutable\]) could be provided for function @@ -536,7 +550,7 @@ fn Foo() { } ``` -

Alternative Considered

+#### Alternative Considered: Middle vs Beginning 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 @@ -549,11 +563,16 @@ 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 -will support nothing-implies-`let` and `var` binding patterns. These will be -annotated with a type and initialized with the right-hand-side of an equals -sign. The lifetime of a function field is the same as the lifetime of the -function declaration or lambda in which it exists. +**Proposal**: Function fields 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 function +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. + +To prevent ambiguities, function fields can only exist on functions where the +definition is attached to the declaration. This means they are supported on +lambdas and they are supported on function declarations that are immediately +defined, but they are not supported on forward-declared functions. ``` fn Foo() { @@ -566,7 +585,7 @@ fn Foo() { } ``` -

Alternative Considered

+### Alternative Considered: No Function Fields Alternatively, by-value and by-object capturing could serve the same purpose. This was decided against because capturing is not as expressive as general @@ -598,7 +617,7 @@ 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

+### Alternative Considered: Recursive Self 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. From 2460c5fd604f5a3d76870e36454116aabc7913e8 Mon Sep 17 00:00:00 2001 From: CJ Johnson Date: Thu, 23 May 2024 10:56:55 -0400 Subject: [PATCH 05/12] Address feedback from Richard --- proposals/p3848.md | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/proposals/p3848.md b/proposals/p3848.md index f64123ceb1393..b721c919086e2 100644 --- a/proposals/p3848.md +++ b/proposals/p3848.md @@ -136,11 +136,11 @@ fn Foo() { ### Succinctly -Function definitions have one of the following syntactic forms (where items in -square brackets are optional and independent): +Function definitions and lambda expressions 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_ `}` @@ -155,7 +155,8 @@ 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. +lambda expression. The trailing `;` in the first form is required for a function +declaration, but is not part of the syntax of a lambda expression. The presence of _tuple-pattern_ determines whether the function body uses named or positional parameters. @@ -468,6 +469,12 @@ fn Example { } ``` +Note: If a function object F has mutable state, either because it has a +non-const copy 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. + #### Alternative Considered: By-Value Captures Alternatively, the below-shown four capture modes (by-value, by-object, by-copy @@ -605,9 +612,9 @@ if the underlying type is not copyable, the reference to that object is always copyable. The final case is by-value function fields. Since C++ const references, when -made into fields of a class, prevent the class from being copied, so too should -by-value function fields prevent the function in which it is contained from -being copied. +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 @@ -617,6 +624,13 @@ 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. +Note: Given the direction in +[#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. + ### Alternative Considered: Recursive Self For use in recursion, `self: Self` could be permitted on all functions and From c9014605ebb3483f52e1ac92756a6debdaaa3036 Mon Sep 17 00:00:00 2001 From: CJ Johnson Date: Tue, 16 Apr 2024 15:11:10 -0400 Subject: [PATCH 06/12] Additional reviewer comments addressed From dddf0e17cea741765f4a8f9f909296516733ff15 Mon Sep 17 00:00:00 2001 From: CJ Johnson Date: Thu, 23 May 2024 10:56:55 -0400 Subject: [PATCH 07/12] Address feedback from Richard From b3330530bd23d8e134b672f1bc97628895564d46 Mon Sep 17 00:00:00 2001 From: CJ Johnson Date: Wed, 24 Jul 2024 11:24:21 -0400 Subject: [PATCH 08/12] Wrap up the design for landing --- proposals/p3848.md | 179 +++++++++++++++------------------------------ 1 file changed, 57 insertions(+), 122 deletions(-) diff --git a/proposals/p3848.md b/proposals/p3848.md index b721c919086e2..ca61fe08c57f5 100644 --- a/proposals/p3848.md +++ b/proposals/p3848.md @@ -23,11 +23,10 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - [Alternative Considered: Additional Restriction](#alternative-considered-additional-restriction) - [Function Captures](#function-captures) - [Capture Modes](#capture-modes) - - [Alternative Considered: By-Value Captures](#alternative-considered-by-value-captures) + - [Future Work: Reference Captures](#future-work-reference-captures) - [Default Capture Mode](#default-capture-mode) - [Alternative Considered: Middle vs Beginning](#alternative-considered-middle-vs-beginning) - [Function Fields](#function-fields) - - [Alternative Considered: No Function Fields](#alternative-considered-no-function-fields) - [Copy Semantics](#copy-semantics) - [Self and Recursion](#self-and-recursion) - [Alternative Considered: Recursive Self](#alternative-considered-recursive-self) @@ -41,8 +40,9 @@ 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. +distinguishes a function declaration from a lambda expression, and the rest of +the syntax applies to both kinds. See [Syntax Overview](#syntax-overview) for +more. ## Syntax Overview @@ -94,9 +94,9 @@ that exist inside class or interface definitions. ``` fn Foo(x: X) { - let lambda: auto = fn [const copy x, var y: i32 = 0] { Print(x, ++y); }; + let lambda: auto = fn [var x, var y: i32 = 0] { Print(x, y); }; - fn FunctionDeclaration[const copy x, var y: i32 = 0] { Print(x, ++y); } + fn FunctionDeclaration[var x, var y: i32 = 0] { Print(x, y); } } ``` @@ -256,7 +256,7 @@ in which syntax was minimized was the granting of an **implicit** default [capture](#function-captures) mode. If no square brackets were present, by-value captures would be allowed. This, combined with the lack of an arrow to signify a return value, created syntax of the following form (being passed into the filter -function). +function below). ``` let zero: i32 = 0; @@ -337,7 +337,7 @@ lower-numbered parameters (ex: `fn { Print($10); }`). ``` // A lambda that takes two positional parameters being used as a comparator -Sort(my_list, fn => $0 < $1); +Sort(my_list, fn => $0.val < $1.val); ``` ### Positional Parameter Restrictions @@ -385,21 +385,23 @@ fn Foo6() { 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.** +be restricted to only non-public interfaces. This alternative was considered by +way of a leads question +([#3860](https://github.com/carbon-language/carbon-lang/issues/3860)) and was +decided against. ## Function Captures **Proposal**: Function captures in Carbon mirror the non-init captures of C++. A -function capture declaration consists of a capture mode 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... +function 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... ``` fn Foo() { let handle: Handle = Handle.Get(); - var thread: Thread = Thread.Make(fn [copy handle] { handle.Process(); }); + var thread: Thread = Thread.Make(fn [var handle] { handle.Process(); }); thread.Join(); } ``` @@ -407,7 +409,7 @@ fn Foo() { ``` fn Foo() { let handle: Handle = Handle.Get(); - fn MyThread[copy]() { handle.Process(); } + fn MyThread[handle]() { handle.Process(); } var thread: Thread = Thread.Make(MyThread); thread.Join(); } @@ -415,15 +417,8 @@ fn Foo() { ### Capture Modes -**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 | -| :-----------------: | :--------------------------------------------------------------: | -| `copy` | Capture "by-copy" where the resulting capture is mutable | -| `const copy` | Capture "by-const-copy" where the resulting capture is immutable | -| `ref` | Capture "by-reference" behaving as a C++ reference | -| `const ref` | Capture "by-const-reference" behaving as a C++ const reference | +**Proposal**: `let` and `var` can appear as function captures. They behave as +they would in regular bindings. To prevent ambiguities, captures can only exist on functions where the definition is attached to the declaration. This means they are supported on @@ -438,18 +433,11 @@ captures as shown in the example code below. fn Example { var a: i32 = 0; var b: i32 = 0; - var c: i32 = 0; - var d: i32 = 0; - - 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. - - b += 1; // ❌ Invalid: Cannot modify a const copy capture. - c += 1; // ✅ Valid: Modifies the variable from the outer scope + let lambda: auto = fn [a, var b] { + a += 1; // ❌ Invalid: by-value captures are immutable - d += 1; // ❌ Invalid: Cannot modify a const reference capture. + b += 1; // ✅ Valid: Modifies the captured copy of the by-object capture }; lambda(); @@ -460,7 +448,7 @@ fn Example { fn Example { fn Invalid() -> auto { var s: String = "Hello world"; - return fn[const ref s]() => s; + return fn[s]() => s; } // ❌ Invalid: returned lambda references `s` which is no longer alive @@ -470,70 +458,18 @@ fn Example { ``` Note: If a function object F has mutable state, either because it has a -non-const copy 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 +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. -#### Alternative Considered: By-Value Captures - -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 -we know we want to support function fields, which have the full expressive power -of every other form of binding pattern, favoring functions fields as the syntax -alone that provides that functionality seems appropriate. - -| Capture Mode Syntax | Corresponding Semantics | -| :-----------------: | :----------------------------------------------------------------: | -| `let` \* | Capture "by-value" using Carbon's notion of "by-value" | -| `var` | Capture "by-object" using Carbon's notion of "by-object" | -| `copy` | Capture "by-copy" where the resulting capture is immutable | -| `ref` | Capture "by-reference" where the capture acts like a C++ reference | - -\* let is the default behavior for captures with no mode specifier (ex: -`fn[foo] {}`). - -``` -fn AlternativeExample { - var by_value: i32 = 0; - var by_object: i32 = 0; - var by_copy: i32 = 0; - var by_reference: i32 = 0; - - let lambda: auto = fn [ref, by_value, var by_object, copy by_copy] { - by_value += 1; // ❌ Invalid: Cannot modify a value binding. +#### Future Work: Reference Captures - by_object += 1; // ✅ Valid: `var`/object bindings are mutable BUT they only - // modify the instance attached to the lambda, not the - // variable declared in the outer scope. - - by_copy += 1; // ❌ Invalid: Cannot modify a copy capture. - - by_reference += 1; // ✅ Valid: Modifies the variable from the outer scope - }; - - lambda(); -} -``` - -``` -fn AlternativeExample { - fn Invalid() -> auto { - var s: String = "Hello world"; - let x: i32 = 0; - return fn[ref s, x]() => (s, x); - } - - // ❌ Invalid: returned lambda references `s` which is no longer alive when the lambda - // is invoked. It also captures `x` by value which is similarly no longer alive. - Print(Invalid()()); -} -``` +Much discussion has been had so far about the implications of capturing by +reference. For now, such behavior is supported not through captures but instead +through function fields formed from the address of an object in the outer scope. +It is imperative that more work be done in this area to address the erganomic +concerns of the current solution. ### Default Capture Mode @@ -541,16 +477,25 @@ fn AlternativeExample { 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. +capture mode roughly mirrors the syntax `[=]` and `[&]` capture modes from C++ +by being the first thing to appear in the square brackets. ``` -fn Foo() { +fn Foo1() { let handle: Handle = Handle.Get(); - fn MyThread[copy]() { - handle.Process(); // `handle` is copy captured due to the default capture - // mode specifier of `copy` + fn MyThread[var]() { + handle.Process(); // `handle` is captured by-object due to the default capture + // mode specifier of `var` + } + var thread: Thread = Thread.Make(MyThread); + thread.Join(); +} + +fn Foo2() { + let handle: Handle = Handle.Get(); + fn MyThread[let]() { + handle.Process(); // `handle` is captured by-value due to the default capture + // mode specifier of `let` } var thread: Thread = Thread.Make(MyThread); thread.Join(); @@ -583,35 +528,25 @@ defined, but they are not supported on forward-declared functions. ``` fn Foo() { - var h: Handle = Handle.Get(); - var thread: Thread = Thread.Make(fn [a: auto = h; var b: auto = h] { - a.Process(); // ❌ Invalid: Cannot call mutating methods on value bindings - b.Process(); // ✅ Valid: Can mutate object bindings + 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(); } ``` -### Alternative Considered: No Function Fields - -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 **Proposal**: To mirror the behavior of C++, function declarations and 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 by-copy and by-const-copy captures. - -By-reference and by-const-reference captures are a bit different, because even -if the underlying type is not copyable, the reference to that object is always -copyable. +applies to captures. -The final case is by-value function fields. Since C++ const references, when +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. @@ -621,8 +556,8 @@ contained from being copied assigned. **Proposal**: 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. 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. +a class type or an interface, such that it refers to the class/interface and not +to the function itself. Note: Given the direction in [#3720](https://github.com/carbon-language/carbon-lang/pull/3720), an expression From aa6e888e82596983308f8e9e25e17082df54ef13 Mon Sep 17 00:00:00 2001 From: CJ Johnson Date: Wed, 7 Aug 2024 14:52:54 -0400 Subject: [PATCH 09/12] Spelling mistake --- proposals/p3848.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/p3848.md b/proposals/p3848.md index ca61fe08c57f5..cc07f7f60ade9 100644 --- a/proposals/p3848.md +++ b/proposals/p3848.md @@ -468,7 +468,7 @@ mutate its mutable state. Much discussion has been had so far about the implications of capturing by reference. For now, such behavior is supported not through captures but instead through function fields formed from the address of an object in the outer scope. -It is imperative that more work be done in this area to address the erganomic +It is imperative that more work be done in this area to address the ergonomic concerns of the current solution. ### Default Capture Mode From 063c2cd13f85e9f25cc7840c03d05a4bf13b40ae Mon Sep 17 00:00:00 2001 From: CJ Johnson Date: Mon, 26 Aug 2024 14:32:45 -0400 Subject: [PATCH 10/12] Address feedback about formatting and content --- proposals/p3848.md | 399 +++++++++++++++++++++++++++++---------------- 1 file changed, 261 insertions(+), 138 deletions(-) diff --git a/proposals/p3848.md b/proposals/p3848.md index cc07f7f60ade9..c0ee2ed86dd01 100644 --- a/proposals/p3848.md +++ b/proposals/p3848.md @@ -14,23 +14,24 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - [Abstract](#abstract) - [Syntax Overview](#syntax-overview) - - [Succinctly](#succinctly) - - [Alternative Considered: Terse vs Elaborated](#alternative-considered-terse-vs-elaborated) + - [Syntax Defined](#syntax-defined) - [Introducer](#introducer) - - [Alternative Considered: Sigil](#alternative-considered-sigil) - [Positional Parameters](#positional-parameters) - [Positional Parameter Restrictions](#positional-parameter-restrictions) - - [Alternative Considered: Additional Restriction](#alternative-considered-additional-restriction) - [Function Captures](#function-captures) - [Capture Modes](#capture-modes) - - [Future Work: Reference Captures](#future-work-reference-captures) - [Default Capture Mode](#default-capture-mode) - - [Alternative Considered: Middle vs Beginning](#alternative-considered-middle-vs-beginning) - [Function Fields](#function-fields) - [Copy Semantics](#copy-semantics) - [Self and Recursion](#self-and-recursion) - - [Alternative Considered: Recursive Self](#alternative-considered-recursive-self) - [Rationale](#rationale) +- [Alternatives Considered](#alternatives-considered) + - [Alternative Considered: Terse vs Elaborated](#alternative-considered-terse-vs-elaborated) + - [Alternative Considered: Sigil](#alternative-considered-sigil) + - [Alternative Considered: Additional Positional Parameter Restriction](#alternative-considered-additional-positional-parameter-restriction) + - [Alternative Considered: Recursive Self](#alternative-considered-recursive-self) +- [Future Work](#future-work) + - [Future Work: Reference Captures](#future-work-reference-captures) @@ -41,8 +42,28 @@ 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 function declaration from a lambda expression, and the rest of -the syntax applies to both kinds. See [Syntax Overview](#syntax-overview) for -more. +the syntax applies to both kinds. By providing a valid lambda syntax in Carbon, +migration from from C++ to Carbon will be made easier and more idiomatic. In +C++, lambdas are defined at their point of use and are often anonymous, meaning +replacing them solely with function declarations would create an ergonomic +burden compounded by the need for the migration tool to select a name. + +Associated discussion docs: + +- [Lambdas Discussion 1](https://docs.google.com/document/d/1rZ9SXL4Voa3z20EQz4UgBMOZg8xc8xzKqA1ufPQdTao/) +- [Lambdas Discussion 2](https://docs.google.com/document/d/14K_YLjChWyyNv3wv5Mn7uLFHa0JZTc21v_WP8RzC8M4/) +- [Lambdas Discussion 3](https://docs.google.com/document/d/1VVOlRuPGt8GQpjsygMwH2B7Wd0mBsS3Qif8Ve2yhX_A/) +- [Lambdas Discussion 4](https://docs.google.com/document/d/1Sevhvjo06Bc6wTigNL1pK-mlF3IXvzmU1lI2X1W9OYA/) + +# Background + +Refer to the following documentation about lambdas in other languages. What +separates these three and makes them more analegous to Carbon's direction is the +use of "captures" such that the lambda has state, a lifetime, etc. + +- [Lambdas in C++](https://en.cppreference.com/w/cpp/language/lambda) +- [Closures in Swift](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/) +- [Closures in Rust](https://doc.rust-lang.org/rust-by-example/fn/closures.html) ## Syntax Overview @@ -52,39 +73,101 @@ declarations. At a high level, lambdas and function declarations will look like the following. ``` -let lambda1: auto = fn => T.Make(); +// In a variable: +let lambda: auto = fn => T.Make(); +// Equivalent in C++23: +// const auto lambda = [] { return T::Make(); }; + +// In a function call: +Foo(10, 20, fn => T.Make()); +// Equivalent in C++23: +// Foo(10, 20, [] { return T::Make(); }); +``` -let lambda2: auto = fn []() -> T { return T.Make(); }; +``` +// In a variable: +let lambda: auto = fn -> T { return T.Make(); }; +// Equivalent in C++23: +// const auto lambda = [] -> T { return T::Make(); }; -fn FunctionDeclaration1 => T.Make(); +// In a function call: +PushBack(my_list, fn => T.Make()); +// Equivalent in C++23: +// PushBack(my_list, [] { return T::Make(); }); +``` -fn FunctionDeclaration2[]() -> T { return T.Make(); } +``` +fn FunctionDeclaration => T.Make(); +// Equivalent in C++23: +// auto FunctionDeclaration() { return T.Make(); } +``` + +``` +fn FunctionDeclaration -> T { return T.Make(); } +// Equivalent in C++23: +// auto FunctionDeclaration() -> T { return T::Make(); } ``` There are functions which return an expression, such that the return type is `auto`. ``` +// In a variable: let lambda: auto = fn => T.Make(); +// Equivalent in C++23: +// const auto lambda = [] { return T::Make(); }; + +// In a function call: +Foo(fn => T.Make()); +// Equivalent in C++23: +// Foo([] { return T::Make(); }); +``` +``` fn FunctionDeclaration => T.Make(); +// Equivalent in C++23: +// auto FunctionDeclaration() { return T::Make(); } ``` And there are functions with an explicit return type that provide a body of statements. ``` +// In a variable: let lambda: auto = fn -> T { return T.Make(); }; +// Equivalent in C++23: +// const auto lambda = [] -> T { return T::Make(); }; + +// In a function call: +Foo(fn -> T { return T.Make(); }) +// Equivalent in C++23: +// Foo([] -> T { return T::Make(); }); +``` +``` fn FunctionDeclaration -> T { return T.Make(); } +// Equivalent in C++23: +// auto FunctionDeclaration() -> T { return T::Make(); } ``` There are even functions that provide a body of statements but no return value. ``` +// In a variable: let lambda: auto = fn { Print(T.Make()); }; +// Equivalent in C++23: +// const auto lambda = [] -> void { Print(T::Make()); }; +// In a function call: +Foo(fn { Print(T.Make()); }); +// Equivalent in C++23: +// Foo([] -> void { Print(T::Make()); }); +``` + +``` fn FunctionDeclaration { Print(T.Make()); } +// Equivalent in C++23: +// auto FunctionDeclaration() -> void { Print(T::Make()); } ``` Functions support [captures](#function-captures), [fields](#function-fields) and @@ -93,10 +176,20 @@ deduced parameters in the square brackets. In addition, `self: Self` or that exist inside class or interface definitions. ``` -fn Foo(x: X) { - let lambda: auto = fn [var x, var y: i32 = 0] { Print(x, y); }; +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); }; - fn FunctionDeclaration[var x, var y: i32 = 0] { Print(x, y); } + // In 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); }); + + fn FunctionDeclaration[var x, var y: i32 = 0] { Print(++x, ++y); } + // Equivalent in C++23: + // Such a function declaration is not possible in C++. } ``` @@ -108,8 +201,16 @@ of type `auto`. ``` fn Foo() { let lambda: auto = fn { Print($0); }; + // Equivalent in C++23: + // Such a lambda is not possible in C++. + // Equivalent in Swift: + // let lambda = { Print($0) }; fn FunctionDeclaration { Print($0); } + // Equivalent in C++23: + // Such a function declaration is not possible in C++. + // Equivalent in Swift: + // Such a function declaration is not possible in Swift. } ``` @@ -118,23 +219,34 @@ have both named and positional parameters. ``` fn Foo() { - let lambda: auto = fn(v: auto) { Print(v); }; + // In a variable: + let lambda: auto = fn (v: auto) { Print(v); }; + // Equivalent in C++23: + // const auto lambda = [](v: auto) -> void { Print(v); }; + + // In a function call: + Foo(fn (v: auto) { Print(v); }); + // Equivalent in C++23: + // Foo([](v: auto) { Print(v); }); fn FunctionDeclaration(v: auto) { Print(v); } + // Equivalent in C++23: + // auto FunctionDeclaration(v: auto) -> void { Print(v); } } ``` -And deduced parameters. +And in additional the option between positional and named parameters, deduced +parameters are always permitted. ``` fn Foo() { - let lambda: auto = fn[T:! Printable](t: T) { Print(t); }; + let lambda: auto = fn [T:! Printable](t: T) { Print(t); }; fn FunctionDeclaration[T:! Printable](t: T) { Print(t); } } ``` -### Succinctly +### Syntax Defined Function definitions and lambda expressions have one of the following syntactic forms (where items in square brackets are optional and independent): @@ -154,9 +266,9 @@ 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 +The presence of _name_ determines whether this is a function definition or a lambda expression. The trailing `;` in the first form is required for a function -declaration, but is not part of the syntax of a lambda expression. +definition, but is not part of the syntax of a lambda expression. The presence of _tuple-pattern_ determines whether the function body uses named or positional parameters. @@ -188,27 +300,27 @@ following code examples. fn => A1 -fn[B, C] => A1 +fn [B, C] => A1 -fn(D) => A2 +fn (D) => A2 -fn[B, C](D) => A2 +fn [B, C](D) => A2 fn { E1; } fn -> F { E2; } -fn[B, C] { E1; } +fn [B, C] { E1; } -fn[B, C] -> F { E2; } +fn [B, C] -> F { E2; } -fn(D) { E3; } +fn (D) { E3; } -fn(D) -> F { E4; } +fn (D) -> F { E4; } -fn[B, C](D) { E3; } +fn [B, C](D) { E3; } -fn[B, C](D) -> F { E4; } +fn [B, C](D) -> F { E4; } ``` ``` @@ -240,57 +352,6 @@ fn G[B, C](D) { E3; } fn G[B, C](D) -> F { E4; } ``` -### Alternative Considered: Terse vs Elaborated - -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 -syntactic challenge in the form of cliffs, explained below. - -Terse lambdas were slated to be the most compact form of a lambda. Combined with -a [sigil introducer](#introducer), they would be syntactically minimal. One way -in which syntax was minimized was the granting of an **implicit** default -[capture](#function-captures) mode. If no square brackets were present, by-value -captures would be allowed. This, combined with the lack of an arrow to signify a -return value, created syntax of the following form (being passed into the filter -function below). - -``` -let zero: i32 = 0; -let list_all: List(i32) = GetAllValues(); -let list_positive: List(i32) = list_all.Filter( - @ $0 > zero -); -``` - -To give users more control over the feature set in a lambda, the next step up -was an elaborated lambda. This provided the ability to add both square brackets -and explicit parameters to lambdas at the cost of more syntax. Unfortunately, -this also meant there was a bit of a syntactic cliff and a stumbling block. It -was considered desirable for empty square brackets to mean capturing is -disabled. But since the no-square-brackets form needed to support capturing for -terse lambdas, elaborated lambdas needed to both add the square brackets and -also add an explicit default capture mode at the same time just to maintain the -existing capturing behavior. The net result was code that looked like the -following (being passed into the filter function again). - -``` -let zero: i32 = 0; -let list_all: List(i32) = GetAllValues(); -let list_positive: List(i32) = list_all.Filter( - @[let](x: auto) x > zero -); -``` - -Finally, if a user wanted to upgrade a lambda to a function declaration, this -created another cliff where they needed to switch from the sigil to the `fn` -keyword, on top of adding a name. Ultimately these downsides suggested that a -continuous syntax was the better path forward, even though the minimal lambda -syntax would be a bit less terse. - ## Introducer **Proposal**: Introduce with the `fn` keyword to mirror function declarations. @@ -301,25 +362,11 @@ introduces a lambda. ``` let lambda1: auto = fn => T.Make(); -let lambda2: auto = fn []() -> T { return T.Make(); }; +let lambda2: auto = fn -> T { return T.Make(); }; fn FunctionDeclaration1 => T.Make(); -fn FunctionDeclaration2[]() -> T { return T.Make(); } -``` - -### Alternative Considered: Sigil - -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(); - -let lambda2: auto = @[]() -> T { return T.Make(); }; +fn FunctionDeclaration2 -> T { return T.Make(); } ``` ## Positional Parameters @@ -335,9 +382,13 @@ from, meaning the highest named parameter denotes the minimum number of arguments required by the function. The function 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). + ``` // 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 @@ -381,15 +432,6 @@ fn Foo6() { } ``` -#### Alternative Considered: Additional Restriction - -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 was considered by -way of a leads question -([#3860](https://github.com/carbon-language/carbon-lang/issues/3860)) and was -decided against. - ## Function Captures **Proposal**: Function captures in Carbon mirror the non-init captures of C++. A @@ -422,8 +464,11 @@ they would in regular bindings. To prevent ambiguities, captures can only exist on functions where the definition is attached to the declaration. This means they are supported on -lambdas and they are supported on function declarations that are immediately -defined, but they are not supported on forward-declared functions. +lambdas (which always exist in an expression context) and they are supported on +function declarations that are immediately defined inside the body of another +function (which is in a statement context), but they are not supported on +forward-declared functions nor are they supported as class members where +`self: Self` is permitted. Capture modes can be used as [default capture mode specifiers](#default-capture-mode) or for explicit @@ -448,7 +493,7 @@ fn Example { fn Example { fn Invalid() -> auto { var s: String = "Hello world"; - return fn[s]() => s; + return fn [s]() => s; } // ❌ Invalid: returned lambda references `s` which is no longer alive @@ -463,14 +508,6 @@ 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. -#### Future Work: Reference Captures - -Much discussion has been had so far about the implications of capturing by -reference. For now, such behavior is supported not through captures but instead -through function fields formed from the address of an object in the outer scope. -It is imperative that more work be done in this area to address the ergonomic -concerns of the current solution. - ### Default Capture Mode **Proposal**: By default, there is no capturing in functions. The lack of any @@ -502,17 +539,6 @@ fn Foo2() { } ``` -#### Alternative Considered: Middle vs Beginning - -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**: Function fields mirror the behavior of init captures in C++. A @@ -523,8 +549,11 @@ the function, and their scope extends to the end of the function body. To prevent ambiguities, function fields can only exist on functions where the definition is attached to the declaration. This means they are supported on -lambdas and they are supported on function declarations that are immediately -defined, but they are not supported on forward-declared functions. +lambdas (which always exist in an expression context) and they are supported on +function declarations that are immediately defined inside the body of another +function (which is in a statement context), but they are not supported on +forward-declared functions nor are they supported as class members where +`self: Self` is permitted. ``` fn Foo() { @@ -566,13 +595,6 @@ 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. -### Alternative Considered: Recursive Self - -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 Lambdas in Carbon serve two purposes. The primary purpose is in support of the @@ -585,3 +607,104 @@ the body of such functions). In addition, Lambdas serve to support the 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. + +## Alternatives Considered + +### Alternative Considered: Terse vs Elaborated + +Proposed above is 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 +syntactic challenge in the form of cliffs, explained below. As a result, they +were decided against. + +Terse lambdas were slated to be the most compact form of a lambda. Combined with +a [sigil introducer](#alternative-considered-sigil), they would be syntactically +minimal. One way in which syntax was minimized was the granting of an +**implicit** default [capture](#function-captures) mode. If no square brackets +were present, by-value captures would be allowed. This, combined with the lack +of an arrow to signify a return value, created syntax of the following form +(being passed into the filter function below). + +``` +let zero: i32 = 0; +let list_all: List(i32) = GetAllValues(); +let list_positive: List(i32) = list_all.Filter( + @ $0 > zero +); +``` + +To give users more control over the feature set in a lambda, the next step up +was an elaborated lambda. This provided the ability to add both square brackets +and explicit parameters to lambdas at the cost of more syntax. Unfortunately, +this also meant there was a bit of a _syntactic cliff_ and a stumbling block. It +was considered desirable for empty square brackets to mean capturing is +disabled. But since the no-square-brackets form needed to support capturing for +terse lambdas, elaborated lambdas needed to both add the square brackets and +also add an explicit default capture mode at the same time just to maintain the +existing capturing behavior. The net result was code that looked like the +following (being passed into the filter function again). + +``` +let zero: i32 = 0; +let list_all: List(i32) = GetAllValues(); +let list_positive: List(i32) = list_all.Filter( + @[let](x: auto) x > zero +); +``` + +Finally, if a user wanted to upgrade a lambda to a function declaration, this +created another cliff where they needed to switch from the sigil to the `fn` +keyword, on top of adding a name. Ultimately these downsides suggested that a +continuous syntax was the better path forward, despite the face that the +shortest spellable lambda would be a bit less terse than the alternative +considered. + +### Alternative Considered: Sigil + +Proposed above is the use of `fn` as the [introducer](#introducer) for all +functions/lambdas. An alternative considered was to tntroduce 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. It would have +looked like the following: + +``` +let lambda1: auto = @ => T.Make(); + +let lambda2: auto = @[]() -> T { return T.Make(); }; +``` + +### Alternative Considered: Additional Positional Parameter Restriction + +In addition to +[the above proposed restrictions](#positional-parameter-restrictions) to +positional parameters, an additional restriction was considered. That being, +visibility of functions with positional parameters could be restricted to only +non-public interfaces. This alternative was considered by way of a leads +question ([#3860](https://github.com/carbon-language/carbon-lang/issues/3860)) +and was decided against, with the speculation that such a restriction may be +enforced by way of an HOA rule as opposed to a compiler error. + +### Alternative Considered: Recursive Self + +Proposed above is a deliniation between function declarations that can provide a +`self` parameter and functions declarations (plus lambdas) which cannot. An +alternative was considered such that, for use in recursion, `self: Self` could +be permitted on all functions and lambdas and refer to the function itself. +Unfortunately, it created a bit of a discontinuity between class members and +non-class members and was thus decided against. + +## Future Work + +### Future Work: Reference Captures + +Much discussion has been had so far about the implications of capturing by +reference. For now, such behavior is supported not through captures but instead +through function fields formed from the address of an object in the outer scope. +It is **imperative** that more work be done in this area to address the +ergonomic concerns of the current solution. From 718e52510953de5dda9ef30127858d67b8b166c7 Mon Sep 17 00:00:00 2001 From: Chandler Carruth Date: Thu, 19 Sep 2024 19:02:39 -0700 Subject: [PATCH 11/12] Apply suggestions from code review Co-authored-by: Richard Smith --- proposals/p3848.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/proposals/p3848.md b/proposals/p3848.md index c0ee2ed86dd01..a78bc8c11cca4 100644 --- a/proposals/p3848.md +++ b/proposals/p3848.md @@ -202,15 +202,15 @@ of type `auto`. fn Foo() { let lambda: auto = fn { Print($0); }; // Equivalent in C++23: - // Such a lambda is not possible in C++. + // auto lambda = [](auto _0, auto...) -> void { Print(_0); }; // Equivalent in Swift: // let lambda = { Print($0) }; fn FunctionDeclaration { Print($0); } // Equivalent in C++23: - // Such a function declaration is not possible in C++. + // auto FunctionDeclaration = [](auto _0, auto...) -> void { Print(_0); }; // Equivalent in Swift: - // Such a function declaration is not possible in Swift. + // let FunctionDeclaration = { Print($0) }; } ``` From eb056f1e2fa3432650c696bfe8de1217824b7cf5 Mon Sep 17 00:00:00 2001 From: Chandler Carruth Date: Thu, 19 Sep 2024 19:02:59 -0700 Subject: [PATCH 12/12] Apply suggestions from code review Co-authored-by: Richard Smith --- proposals/p3848.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proposals/p3848.md b/proposals/p3848.md index a78bc8c11cca4..1083f5a03246c 100644 --- a/proposals/p3848.md +++ b/proposals/p3848.md @@ -189,7 +189,7 @@ fn Foo(x: i32) { fn FunctionDeclaration[var x, var y: i32 = 0] { Print(++x, ++y); } // Equivalent in C++23: - // Such a function declaration is not possible in C++. + // auto FunctionDeclaration = [x, y = int32_t{0}] mutable -> void { Print(++x, ++y); }; } ```