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 10 commits into
base: trunk
Choose a base branch
from
Open

Lambdas #3848

wants to merge 10 commits into from

Conversation

CJ-Johnson
Copy link
Contributor

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

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 function declaration from a lambda expression, and the rest of 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:

@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.

proposals/p3848.md Outdated Show resolved Hide resolved
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.

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 still need an answer here -- I think wanting to capture a local let binding will be a common case, and requiring it to be renamed seems unsatisfying from an ergonomic perspective.

proposals/p3848.md Outdated Show resolved Hide resolved
proposals/p3848.md Outdated Show resolved Hide resolved
proposals/p3848.md Show resolved Hide resolved
proposals/p3848.md Show resolved Hide resolved
github-merge-queue bot pushed a commit that referenced this pull request May 29, 2024
Parse the name of a declaration as a sequence of `NameQualifier`s --
which have a name, possibly parameters, and a trailing period --
followed by a name and possibly parameters. This prepares us for parsing
declarations of members of generic classes and similar cases, but
actually supporting such member redeclarations is left to a future
change.

We previously required functions to have parameters, but no longer do,
following the direction of #3848. Cases like namespaces that can't
actually have parameters are now diagnosed in check instead of in parse.

---------

Co-authored-by: Jon Ross-Perkins <jperkins@google.com>
Comment on lines 386 to 389
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.**
Copy link
Contributor

Choose a reason for hiding this comment

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

This leads question has now been answered: #3860

Copy link
Contributor

@zygoloid zygoloid left a comment

Choose a reason for hiding this comment

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

Looks good to me, thanks, and sorry for the slow review cycle!

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

Choose a reason for hiding this comment

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

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! Fixed :)

Copy link
Contributor

@chandlerc chandlerc left a comment

Choose a reason for hiding this comment

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

Very happy with the direction here. A bunch of comments, but they're all focused on the proposal write up, not the specific proposed feature.

Comment on lines 38 to 45
## Abstract

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

Choose a reason for hiding this comment

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

We have a convention of keeping the abstract in the document and the proposal summary th e same -- I feel like there is a bit of content in each, could you merge them?

Comment on lines 37 to 45
## Abstract

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 function declaration from a lambda expression, and the rest of
the syntax applies to both kinds. See [Syntax Overview](#syntax-overview) for
more.
Copy link
Contributor

Choose a reason for hiding this comment

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

We should probably merge the content here and the PR description.

```
let lambda1: auto = fn => T.Make();

let lambda2: auto = fn []() -> T { return T.Make(); };
Copy link
Contributor

Choose a reason for hiding this comment

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

I understand this and the below are just illustrating where the []s and ()s go, but I suspect we'll want to not accept empty []s at least... Would it make sense to use some pseudo syntax to indicate that there's something inside? Or to have a more examples?

}
```

### Succinctly
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure what this section heading means....

(I fear it may be ... too succinct? I'll see myself out.)

Comment on lines 157 to 159
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.
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 this is just trying to speak to definition syntaxes.

Suggested change
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.
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
definition, but is not part of the syntax of a lambda expression.

Comment on lines 191 to 211
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; }
Copy link
Contributor

Choose a reason for hiding this comment

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

Adding some whitespace based on some bikeshed-y discussion in the syntax channel...

Suggested change
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; }
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; }

fn G[B, C](D) -> F { E4; }
```

### Alternative Considered: Terse vs Elaborated
Copy link
Contributor

Choose a reason for hiding this comment

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

The alternatives considered are spread throughout the proposal rather than grouped at the bottom which I think makes it a little bit harder to get a sense of what the actual proposal is.

But maybe more importantly, it would be good to try to summarize the pros, cons, and what tipped the scales to not pursue the alternative for each one. That's an important part of the structure we typically want in proposals.

proposals/p3848.md Show resolved Hide resolved
let lambda2: auto = @[]() -> T { return T.Make(); };
```

## Positional Parameters
Copy link
Contributor

Choose a reason for hiding this comment

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

As I think Swift is one of the few other languages that has a similar feature, it would be good to a) mention that and b) indicate what if any things really differ between them.

Comment on lines 423 to 426
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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this also mention only being supported inside the body of another function?

Or, alternatively, in an expression (lambda) or statement (function) context?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Made this change to both captures and function fields sections :)

@chandlerc
Copy link
Contributor

Another suggestion for the proposal -- I think it'd be nice to add some comparison for how the Carbon lambdas would look in C++.

This doesn't need to get into features that C++ lambdas have and Carbon's don't, just showing how analogous C++ lambdas would look so that folks can compare.

@KateGregory
Copy link
Contributor

I'm really liking this. A few small comments. More calling examples, ideally right after the first syntax example. "This would be invoked like this:" kind of thing. I see just one (though I may have missed others) example of a lambda not being put into a variable but being used right away, the comparator in the sort example. This is terrific but may be missed by someone less interested in positional parameters, so some other examples of unnamed lambdas just being used would be great. I would hate for a reader to think that the #1 use of lambdas is to put them into a named variable or value.

@CJ-Johnson
Copy link
Contributor Author

I'm really liking this. A few small comments. More calling examples, ideally right after the first syntax example. "This would be invoked like this:" kind of thing. I see just one (though I may have missed others) example of a lambda not being put into a variable but being used right away, the comparator in the sort example. This is terrific but may be missed by someone less interested in positional parameters, so some other examples of unnamed lambdas just being used would be great. I would hate for a reader to think that the #1 use of lambdas is to put them into a named variable or value.

Thanks for the feedback, Kate! I tried to address it by adding both // In a variable: and // In a function call: variants to the top level examples. Hopefully this will clarify things :)

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

6 participants