Skip to content

Commit

Permalink
Address review comments from @geoffromer
Browse files Browse the repository at this point in the history
  • Loading branch information
CJ-Johnson committed Apr 16, 2024
1 parent 26afb34 commit e119bcb
Showing 1 changed file with 135 additions and 92 deletions.
227 changes: 135 additions & 92 deletions proposals/p3848.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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) {
Expand All @@ -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() {
Expand All @@ -123,6 +126,35 @@ fn Foo() {
}
```

<h3 id="syntax-overview-succinctly">Succinctly</h3>

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.
Expand Down Expand Up @@ -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
<h3 id="syntax-overview-alternative-considered">Alternative Considered</h3>

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
Expand Down Expand Up @@ -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.
<h3 id="introducer-alternative-considered">Alternative Considered</h3>

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();
Expand All @@ -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
Expand Down Expand Up @@ -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.**
<h4 id="positional-parameter-restrictions-alternative-considered">Alternative Considered</h4>

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

Expand All @@ -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 |
| :-----------------: | :--------------------------------------------------------------: |
Expand All @@ -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();
Expand All @@ -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
<h4 id="capture-modes-alternative-considered">Alternative Considered</h4>

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
Expand Down Expand Up @@ -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();
}
```

<h4 id="default-capture-mode-alternative-considered">Alternative Considered</h4>

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
Expand All @@ -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.
<h3 id="function-fields-alternative-considered">Alternative Considered</h3>

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

Expand All @@ -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.
<h3 id="self-and-recursion-alternative-considered">Alternative Considered</h3>

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.

0 comments on commit e119bcb

Please sign in to comment.