Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lambdas #3848

Open
wants to merge 4 commits into
base: trunk
Choose a base branch
from
Open

Conversation

CJ-Johnson
Copy link
Contributor

@CJ-Johnson CJ-Johnson commented Apr 3, 2024

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 PR proposes a path forward to add lambdas to Carbon and augment function declarations accordingly.

Associated discussion docs:

@CJ-Johnson CJ-Johnson added the proposal rfc Proposal with request-for-comment sent out label Apr 3, 2024
@github-actions github-actions bot added the proposal A proposal label Apr 3, 2024
@CJ-Johnson CJ-Johnson added proposal draft Proposal in draft, not ready for review proposal rfc Proposal with request-for-comment sent out and removed proposal rfc Proposal with request-for-comment sent out proposal draft Proposal in draft, not ready for review labels Apr 4, 2024
@CJ-Johnson CJ-Johnson marked this pull request as ready for review April 4, 2024 17:56
@github-actions github-actions bot requested a review from chandlerc April 4, 2024 17:56

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider presenting this information more like this, instead:

Function definitions have one of the following syntactic forms (where items in square brackets are optional and independent):

fn [name] [ implicit-params ] [tuple-pattern] => expression ;
fn [name] [ implicit-params ] [tuple-pattern] [-> return-type] { statements }

The first form is a shorthand for the second: "=> expression ;" is equivalent to "-> auto { return expression ; }".

implicit-params consists of square brackets enclosing an 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-params 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.

That's more abstract, but at least for me, it would make it much easier to see how this design is (and isn't) continuous.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! I added this content right above the part that you highlighted. My reading of it is that these two blocks of text are not mutually exclusive. Do you disagree?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree they're not mutually exclusive. Personally I don't find the table and examples helpful, but they may work better for other people.

proposals/p3848.md Outdated Show resolved Hide resolved
proposals/p3848.md Outdated Show resolved Hide resolved
proposals/p3848.md Outdated Show resolved Hide resolved
proposals/p3848.md Outdated Show resolved Hide resolved
proposals/p3848.md Outdated Show resolved Hide resolved
proposals/p3848.md Outdated Show resolved Hide resolved
proposals/p3848.md Outdated Show resolved Hide resolved
proposals/p3848.md Outdated Show resolved Hide resolved
proposals/p3848.md Outdated Show resolved Hide resolved
proposals/p3848.md Outdated Show resolved Hide resolved

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree they're not mutually exclusive. Personally I don't find the table and examples helpful, but they may work better for other people.

proposals/p3848.md Outdated Show resolved Hide resolved
proposals/p3848.md Outdated Show resolved Hide resolved
proposals/p3848.md Outdated Show resolved Hide resolved
Comment on lines 521 to 525
**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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should specify this slightly differently:

Suggested change
**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.

This is more general than what we've discussed so far, because it allows things like fn [(a: auto, b: auto) = Foo()] {...}, but that generalization seems desirable.

Comment on lines +139 to +158
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're missing a description of the behavior of the trailing ; here. How about something like:

Suggested change
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.
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_ `}`
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 trailing `;` in the first form is required for a function declaration, but is not part of the syntax of a lambda expression.

Comment on lines +424 to +425
| `ref` | Capture "by-reference" behaving as a C++ reference |
| `const ref` | Capture "by-const-reference" behaving as a C++ const reference |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suppose I want to write this:

let a: String = "long string I'd rather not make a copy of";
DoThingWithLazyGetter(fn [???] => a);

Can a ref capture be used to capture a let binding? If so, what happens -- does that create a temporary and capture a reference to it, or does that capture a value as if by [a: auto = a]?

If not, I think the outcome is that there isn't a way to capture a let binding without renaming it. You can use a function field, but our name shadowing rules would suggest that you must use a new name for the function field. I wonder if it'd be worth adding syntax for capturing a value as a value, rather than as an object.

Comment on lines +607 to +610
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we distinguish between copy construction and copy assignment here? Const reference fields in C++ only prevent copy assignment, not copy construction.

Comment on lines +599 to +601
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to check my understanding:

// Returns the number of times it's been called.
fn Counter[var n: i32 = 0]() { ++n; return n; }

fn Run() {
  Print(Counter());
  Print(Counter());
  var my_counter: auto = Counter;
  Print(my_counter());
  Print(my_counter());
  Print(Counter());
}

... would print 1 2 3 4 3.

be restricted to only non-public interfaces. **This alternative will be put
forth as a leads question before a decision is made.**

## Function Captures
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something it might be good to mention here:

If a function object F has mutable state, either because it has a non-const copy capture or because it has a var 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.

by-value function fields prevent the function in which it is contained from
being copied.

## Self and Recursion
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the direction in #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, I think we can't support combining captures and function fields with a self parameter under that model.

Would that be a reasonable restriction to impose here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal rfc Proposal with request-for-comment sent out proposal A proposal
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants