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

Function declaration syntax for named parameters and default values #505

Open
jonmeow opened this issue Apr 29, 2021 · 22 comments
Open

Function declaration syntax for named parameters and default values #505

jonmeow opened this issue Apr 29, 2021 · 22 comments
Labels
leads question A question for the leads team long term Issues expected to take over 90 days to resolve.

Comments

@jonmeow
Copy link
Contributor

jonmeow commented Apr 29, 2021

Where #478 is focusing on the call syntax for named parameters, I'd like to focus on the declaration syntax here. I think there's not much dissent on providing named parameters, and #478 notes some advantages to providing them.

Questions to leads (marked below):

  • Should the declarer be able to rename parameters for callers?
  • Should the caller or declarer decide to specify arguments with names?
  • Is fn Foo(Int x) called using a named parameter?
  • Are named parameters positional?
  • Should non-defaulted parameters be allowed after defaulted parameters?
  • Overall, what does the named parameter approach look like?

For a TL;DR, just read the end -- "Question to leads: Overall, what does the named parameter approach look like?". This question encapsulates the choices elsewhere.

Context

For some context on declaration syntax:

  • Rosetta
    • Python2
      • The caller decides whether parameters are positional or called by name, not the declarer.
        • Python3 augments Python2 syntax by adding * for requiring named parameters. For example, in def Foo(x, *, y=3) callers must call y by name.
      • All parameters may be specified either positionally or by name: def Foo(x) is called with either Foo(3) or Foo(x=3)
      • Similar approaches are taken by C#, Kotlin
    • Swift
      • The declarer decides whether parameters are called by name or not, not the caller.
      • By default, all arguments require names: func Foo(x: Int) is called with Foo(x: 3).
        • Declarers may rename parameters for callers: func Foo(y x: Int) is called with Foo(y: 3).
        • Arguments are always positional, even if named. i.e., func Foo(x: Int, y: Int) must be called as Foo(x:1, y:2); Foo(y:2, x:1) is invalid.
      • Declarers may make arguments positional-only, disallowing names: func Foo(_ x: Int) is called with Foo(3).
    • Ruby calls these "keyword arguments".
      • The declarer decides whether parameters are positional or called by name, not the caller.
      • By default, arguments are positional: def Foo(x) is called with Foo(3)
      • "Keyword" arguments require names: def Foo(x: 1) is called with Foo() or Foo(x: 1)
      • "Keyword" arguments may be required if there's no default: def Foo(x:) is called with Foo(x: 1)
    • Rust: Not present, long discussed (example)
  • Wikipedia

Many languages can simulate named parameters with structs, such as Go allowing Foo(params{x: 3}). I think we're advocating for a more integrated solution.

Pattern matching

I'm not sure of all the implications, but we should keep in mind correspondence to pattern matching syntax.

Destructuring named tuples

@josh11b raises correspondence to named tuples. For example, consider this potential syntax:

// Assuming tuple declaration syntax like:
var (Int c, Int d) = (c: 1, d: 2);

// Destructuring of the return value for:
fn F(a Int, b Int) -> (Int c, Int d) { ... }

// Could look like:
var (c: Int e, d: Int f) = F(a: 3, b: 4);

Keeping correspondence for the locations of c and d in the final destructuring line may be desirable.

I will note here, I don't know how much destructuring named tuples has been considered. If we go for an overlapping syntax, it may make sense to resolve syntax for such on this issue. However, maybe we will end up with a function declaration syntax that doesn't risk overlap: with that hope, I will simply note this risk, and try to separate discussions.

Question to leads: Should the declarer be able to rename parameters for callers?

Swift provides a feature which explicit supports renaming parameters for callers. Where func Foo(x: Int) provides a "default name" of x, func Foo(y x: Int) renames to y.

Two use-cases have been suggested for this:

  1. Allowing callers to use keywords for named parameters.
  • This use-case is presented in SE-0001
  • For example, where in is a keyword in Swift, func Print(in data: String) allows callers to write Print(in="message").
  1. Allow function definitions and callers to refer to the same variable with names that are locally better.
  • This use-case is presented in Swift docs
  • For example, func greet(person: String, from hometown: String) -> String allows the function definition to write hometown while the caller writes greet(person: "Bill", from: "Cupertino").

In Carbon, regarding (1):

  • Raw identifiers were mentioned in Carbon: Lexical conventions #17 as r#keyword syntax. If adopted and in is a keyword, perhaps callers could still write Print(in="message") even if the declarer writes fn Print(String r#in).
  • Or, perhaps we are okay with discouraging use of keywords for parameters.

I think (2) is more subjective about the value. geoffromer has commented on #478 about this.

If we choose not to support renaming, that should simplify syntax, and the implied effects for developers are likely manageable.

Question to leads: Should the caller or declarer decide to specify arguments with names?

This is a question of whether a caller should specify the name when specifying a given parameter. There are 3 options:

  1. Imitate Python; the caller controls. Given a declaration of def Foo(x), both Foo(1) and Foo(x=1) are valid calls.
  2. Imitate Swift; the declarer controls.
  • Given a declaration of func Foo(x: Int), Foo(x: 1) is valid, and Foo(1) is invalid.
  • Given a declaration of func Foo(_ x: Int), Foo(x: 1) is invalid, and Foo(1) is valid.
  1. Allow both; the declarer may hand over control to the caller, a hybrid of options (1) and (2).

Option (2), without caller controler, creates a uniformity of callers. This serves to both improve understandability of code due to reducing variations added by callers, and avoids accidental cases where a developer may leave the choice to callers and later run into issues renaming their parameter later. Overall, simplification of syntax suggests towards (2).

An exception may be in refactoring, particularly switching from positional and named arguments. Allowing callers control would allow for switches to occur cleanly. However, this should also be achievable with overloads (one doing positional, one doing named arguments). Consequently, unless caller control is desired, it's likely not necessary to provide through language syntax.

Question to leads: Is fn Foo(Int x) called using a named parameter?

The behavior of fn Foo(Int x) should reflect what we might want developers to make idiomatic in APIs. Is that requiring named parameters, or providing positional parameters? Is the syntax for requiring named parameters on calls more or less verbose?

Note this is a mooted question if the caller decides whether to use names in calls.

For example, in Swift, the less verbose syntax is that which requires the caller specify a label. Do we want to match that? Adding the _ is more verbose for the declarer, but less verbose for the caller:

  • func Foo(x: Int) makes the caller be more verbose with Foo(x: 1).
  • func Foo(_ x: Int) makes the caller be less verbose with Foo(1).

We could invert this trade-off by changing syntax from _, for example with a named keyword:

  • func Foo(named x: Int) makes the caller more verbose, with Foo(x: 1)
  • func Foo(x: Int) makes the caller less verbose, with Foo(1)

I expect this decision will be based on intuition about what developers would prefer by default. Either way, we should be able to tinker with our options to make the preferred approach easier to write.

Question to leads: Are named parameters positional?

Named parameters could either be positional, or provided by the caller in any order. What do we prefer to do?

For example, Python allows named parameters to be reordered: def Foo(x, y) may be called with either Foo(x=1, y=2) or Foo(y=2, x=1).

In the other direction, Swift does not allow named parameters to be reordered: func Foo(x: Int, y: Int) must be called with Foo(x: 1, y: 2); it is invalid to write Foo(y: 2, x: 1).

An advantage of requiring ordering is that it makes call sites more consistent. A disadvantage is that it further restricts refactoring of calls.

Question to leads: Should non-defaulted parameters be allowed after defaulted parameters?

If named parameters are positional, it opens the option for non-defaulted parameters to come after defaulted parameters.

For example, consider func Foo(x: Int = 1, _ y: Int) -> Int in Swift. The call Foo(3) sets y to 3 because x requires a name and is positional. Therefore, 3 corresponds to the y parameter.

Is this syntax desired? The advantage is that it allows greater flexibility in parameter ordering. The disadvantage is that it may confuse engineers that named parameters may be inserted before non-named parameters.

Question to leads: Overall, what does the named parameter approach look like?

I think that Python, Swift, and Ruby offer a reasonable catalogue of options that we could choose from, stemming from the above. Would one of these language approaches be acceptable (with variation for Carbon's syntax), some small variation, or is there another approach that would be better?

  • Python:
    • Answers to questions:
      • Should the declarer be able to rename parameters for callers? No.
      • Should the caller or declarer decide to specify arguments with names? Caller decides.
      • Is fn Foo(Int x) called using a named parameter? Caller decides.
      • Are named parameters positional? No.
      • Should non-defaulted parameters be allowed after defaulted parameters? No.
    • Carbonizing:
      • def Foo(x) -> fn Foo(Int x), called with Foo(1)
      • def Foo(x = 3) -> fn Foo(Int x = 3), called with Foo(), Foo(1), or Foo(x: 3)
      • def Foo(x, y) -> fn Foo(Int x, Int y), called with Foo(1, 2), Foo(1, y=2), Foo(x: 1, y: 2), or Foo(y: 2, x: 1).
  • Swift:
    • Answers to questions:
      • Should the declarer be able to rename parameters for callers? Yes.
        • It would be trivial to remove renaming while maintaining similar syntax, but we may want to then reconsider the use of _ in _ x: Int = 1 simply because _ is likely to be used nearby for unused arguments per Optional argument names (unused arguments) #476.
      • Should the caller or declarer decide to specify arguments with names? Declarer decides.
      • Is fn Foo(Int x) called using a named parameter? Yes.
      • Are named parameters positional? Yes.
      • Should non-defaulted parameters be allowed after defaulted parameters? Yes.
    • Carbonizing:
      • func Foo(x: Int) -> fn Foo(Int x), called with Foo(x: 1)
      • func Foo(x: Int = 1) -> fn Foo(Int x = 1), called with Foo() or Foo(x: 1)
      • func Foo(y x: Int) -> fn Foo(y: Int x), called with Foo(y: 1)
      • func Foo(y x: Int = 1) -> fn Foo(y: Int x = 1), called with Foo() or Foo(y: 1)
      • func Foo(_ x: Int) -> fn Foo(_ Int x), called with Foo(1)
      • func Foo(_ x: Int = 1) -> fn Foo(_ Int x = 1), called with Foo() or Foo(1)
      • func Foo(x: Int, y: Int) -> fn Foo(Int x, Int y), called with Foo(x: 1, y: 2) (no other variations).
  • Ruby:
    • Answers to questions:
      • Should the declarer be able to rename parameters for callers? No.
      • Should the caller or declarer decide to specify arguments with names? Declarer decides.
      • Is fn Foo(Int x) called using a named parameter? No.
      • Are named parameters positional? No.
      • Should non-defaulted parameters be allowed after defaulted parameters? No.
    • Carbonizing:
      • def Foo(x:) -> fn Foo(Int x:), called with Foo(x: 1)
      • def Foo(x: 1) -> fn Foo(Int x: 1), called with Foo() or Foo(x: 1)
      • def Foo(x) -> fn Foo(Int x), called with Foo(1)
      • def Foo(x = 1) -> fn Foo(Int x = 1), called with Foo() or Foo(1)
      • def Foo(x:, y:) -> fn Foo(Int x, Int y), called with Foo(x: 1, y: 2) or Foo(y: 2, x: 1) (no other variations).

Note Carbonized syntax probably shouldn't be considered final, I'm just trying to give examples loosely reshaped.

@gribozavr
Copy link
Contributor

gribozavr commented Apr 30, 2021

Swift
The declarer decides whether parameters are positional or called by name, not the caller.

A small clarification: in Swift, even the named parameters (the term Swift uses is "labels") are positional. In other words, the caller can't reorder parameters regardless of whether they have a name/label or not. The rationale for this behavior is to support fluently readable callsites, where reordering parameters could make the call more difficult to read, or change the meaning of the sentence formed by argument labels.

For example, func greet(person: String, from hometown: String) -> String is not callable as greet(from: "Cupertino", person: "Bill").

@jonmeow
Copy link
Contributor Author

jonmeow commented Apr 30, 2021

Thank you for pointing this out! I hadn't noticed that. I've tried to clarify this, and added positionality as a question.

@josh11b
Copy link
Contributor

josh11b commented Apr 30, 2021

Clarification on Python:

So Python is actually in the third "hybrid" category for "Should the caller or declarer decide to specify arguments with names?"

@josh11b
Copy link
Contributor

josh11b commented Apr 30, 2021

Re: Should the declarer be able to rename parameters for callers?

I would be fine without any syntax for a function declaration to rename parameters, except I think we need the corresponding ability when trying to destructure a tuple with names.

Re: Should the caller or declarer decide to specify arguments with names?

I prefer option 2, the declarer controls:

  • My experience with TensorFlow suggests this supports API evolution better.
  • It also seems more in line with Carbon's preference for uniformity and strictness. This makes call sites of a function look more consistent, making it easier to refactor them.
  • It opens the door for using parameter labels to distinguish overloads that would otherwise have the same signature.

Re: Is fn Foo(Int x) called using a named parameter?

I think saying this instead would more clearly communicate what you are trying to ask: "Should named or positional parameters be more convenient to declare?".

I don't have a strong feeling on this question, but I lean toward making positional more convenient just because that matches what C++ developers would expect.

Re: Are named parameters positional?

I would prefer them to not be positional, but unfortunately that adds some complexity in the form of introducing questions of destructor execution order.

Another possibility to consider

We could also put some sort of delimiter in the parameter list. Everything before the delimiter would be positional, everything after it would have a required label. This is the approach used by Python3+.

@jonmeow
Copy link
Contributor Author

jonmeow commented Apr 30, 2021

Added "Should non-defaulted parameters be allowed after defaulted parameters?" (separately, had this in flight while josh11b was commenting)

@jonmeow
Copy link
Contributor Author

jonmeow commented Apr 30, 2021

Re: Should the declarer be able to rename parameters for callers?

I would be fine without any syntax for a function declaration to rename parameters, except I think we need the corresponding ability when trying to destructure a tuple with names.

Re: Should the caller or declarer decide to specify arguments with names?

I prefer option 2, the declarer controls:

  • My experience with TensorFlow suggests this supports API evolution better.
  • It also seems more in line with Carbon's preference for uniformity and strictness. This makes call sites of a function look more consistent, making it easier to refactor them.
  • It opens the door for using parameter labels to distinguish overloads that would otherwise have the same signature.

Re: Is fn Foo(Int x) requiring a named parameter?

The phrasing of this question is very similar to the phrasing of the last question. I think saying this instead would more clearly communicate what you are trying to ask: "Should named or positional parameters be more convenient to declare?".

I've rephrased as "Is fn Foo(Int x) called using a named parameter?" and added "Note this is a mooted question if the caller decides whether to use names in calls."

The choice not to use the word "more convenient" is deliberate: I'm trying to avoid bias in my question.

I believe you have a typo:

makes the caller less verbose, with Foo(x: 1)

should be:

makes the caller less verbose, with Foo(1)

Yes, fixed.

I don't have a strong feeling on this question, but I lean toward making positional more convenient just because that matches what C++ developers would expect.

Re: Are named parameters positional?

I would prefer them to not be positional, but unfortunately that adds some complexity in the form of introducing questions of destructor execution order.

Re: Does fn Foo(Int x) require a named parameter?

I'm not sure what this question is asking. Could you perhaps rephrase it in terms of the options we would be choosing between?

See "Is fn Foo(Int x) require a named parameter?" -- "Does" versus "Is" was an unintentional difference.

Another possibility to consider

We could also put some sort of delimiter in the parameter list. Everything before the delimiter would be positional, everything after it would have a required label. This is the approach used by Python3+.

@zygoloid
Copy link
Contributor

zygoloid commented Apr 30, 2021

My initial thoughts (not to be construed as a lead decision; I think we need more discussion first):

Should the declarer be able to rename parameters for callers?

Yes. The label for the parameter as used by the callee and the name that makes most sense in the implementation of the function may diverge, so allowing two different names seems reasonable. Eg, the comparator passed to a sort function might be called sort_by, but that's likely not a good name for the comparator in the implementation (sort_by(a, b) doesn't read as "compare a and b").

Should the caller or declarer decide to specify arguments with names?

The choice to use names, or not, must be left to the declarer. Permission to use a parameter name (or not) at a call site is part of the API contract -- the function author gets to choose how the function may be called. This seems necessary for both our evolution goal (the function author needs to know what they have promised so they know what they can change) and for the C++ interoperability goal (existing C++ functions do not include parameter names as part of their API contracts).

Is fn Foo(Int x) called using a named parameter?

No, for similar reasons to the previous question. In particular, fn Foo(Int x) should be the correct Carbon equivalent of void Foo(int x) in C++, which means the parameter name should not be part of the interface contract for this syntax. If we made a different choice for idiomatic Carbon function declarations, we could not smoothly interoperate with C++ code, because we would use different call syntax for calls to idiomatic C++ functions and calls to idiomatic Carbon functions.

Are named parameters positional?

I'm slightly leaning no: requiring an exact parameter ordering seems to require programmers to make an arbitrary decision in the case where there is no meaningful order, and that arbitrary decision must be reflected at all call sites. (I acknowledge that this is already the case for positional parameters, but removing the need to remember this arbitrary ordering seems like one of the benefits I'd be looking for in a named parameter system). That said, having a single canonical way to write any given function call does seem useful, and if we choose to have parameter destruction order be guaranteed to be reverse construction order, there may be other reasons we want to force the source syntax to have a consistent ordering.

Should non-defaulted parameters be allowed after defaulted parameters?

Initial reaction: I'd prefer all positional parameters to be specified before all named parameters. That seems to lose no interesting expressive power and should be simple and unsurprising.

Overall, what does the named parameter approach look like?

I'll try to present a complete and specific approach that is in line with the above answers:

An argument list in a function call is ( followed by a sequence of positional arguments followed by a sequence of named arguments followed by ), with , separators between arguments. A positional argument is an expression. A named argument is an identifier, :, then an expression:

f(1, 2, 3, foo: 1, bar: 2)

A parameter list in a function declaration is similar, with the expressions replaced by patterns:

fn f(Int a, Int b, Int c, foo: Int d, bar: Int e);

I think we should initially have no syntax for specifying a single identifier that is both the binding name and the parameter label, and see how often they are the same in practice. If they are very frequently the same and this appears to be an ergonomic issue, we could consider permitting something like _: Int e to indicate the label is the (unique) binding name in the parameter pattern.

When matching a call to a function declaration, we match the positional arguments to the positional parameters, and match the named arguments to the named parameters. We never match a positional argument to a named parameter. If we support default arguments, then a positional parameter can only have a default argument if all subsequent positional parameters do.

This has some properties that are nice for evolution, that not all the other choices have:

  • Adding a new positional parameter with a default argument is always a non-breaking change, even if there are named arguments.
  • Adding a new named parameter with a default argument is always a non-breaking change.
  • Removing the final positional parameter will always break the build of all callers who were passing a corresponding argument.
  • Removing a named parameter that is not explicitly named in any call is always a non-breaking change.
  • Renaming a parameter binding to improve the implementation of a function doesn't affect its callers.

I would suggest we use the same function call and function declaration syntax for modeling tuples-with-named-fields:

var (Int a, Int b, n: Int c) = (1, 2, n: 3);

@josh11b
Copy link
Contributor

josh11b commented May 3, 2021

I feel like a separator option should be under consideration. In particular, if we used the <id>: <type> syntax for variable declarations, then we might do something like:

fn f(a: Int, b: Int, named c: Int, d e: Int);

where a and b are positional parameters referred to by those names in the body of the function but not in the caller and c and d are labeled/named parameters, and where the last parameter is renamed. We would have to decide whether d is used by the caller and e in the function body, or the other way around.

@zygoloid
Copy link
Contributor

zygoloid commented May 3, 2021

@josh11b Do you have a specific corresponding call-site syntax in mind?

@josh11b
Copy link
Contributor

josh11b commented May 4, 2021

@josh11b Do you have a specific corresponding call-site syntax in mind?

Well the call syntax was supposed to be the subject of #478, but I'm guessing something like

f(1, 2, c: 3, e: 4);

@chandlerc
Copy link
Contributor

chandlerc commented May 7, 2021

I'm still thinking about this, but I'd also like to suggest re-framing the questions a little bit. Specifically, I'd like to first answer which use cases we want to address in this space. Sometimes these map directly, other times they don't, but I think its really useful to think in terms of use cases as the initial framing.

Here are the use cases I see:

  1. Placing part of the function's name next to a parameter, both at declaration and call site. This is fundamentally the Swift use case as I understand it.
  2. Labelling a positional parameter with a name in the call that clarifies the semantics of that parameter.
  3. Providing a subset of parameters identified by name rather than position. (And these might naturally be optional parameters.)
  4. (orthogonally relevant to each of the above) Enable call argument labels to differ from declared parameter names.

I understand there is some small overlap in these use cases, and likely overlap in any feature we would use to address them. But I'm trying to identify the important driving use cases we want to consider. Are there other high-level uses cases I'm missing?

Brief observations and thoughts about these first:

  • (1) is a pretty significant divergence from C++ today. An optional (2) is something we nearly have in C++ today with clang-tidy and specially formatted comments. And (3) is almost doable in C++ with some really awkward usage of designated initializers.
  • (2) could be either optional or mandatory in theory. If mandatory, it seems close to (1) but I think remains a usefully distinct use case. For example, even if mandatory, it might not influence the actual function name. So (2) I wouldn't expect to allow two overloads of a function with the same parameter type but different labels. I would expect that to be enabled by (1).
  • (3) is especially interesting because it allows arbitrary subsets of optional parameters rather than requiring a prefix as we have in C++.

My personal opinion about these use cases:

  • I'm not convinced we should pursue (1). I don't think it is bad... I actually somewhat like it in the abstract. But I think it is somewhat of a divergence from C++ and hard to justify at this stage.
  • I think at some form of (2) is clearly needed given the investment in tooling to achieve it today in C++. I'm more interested in allowing an optional (but checked to match) label, as that is what we have approximated in C++ today. Requiring the label in some cases is interesting, but I'm hesitant if it forces calls to repeat themselves when the argument is already completely clear. So I lean towards enabling (2) as an optional feature, and then we can consider if ethere are sufficiently reliably reasons to require it.
  • There is pretty clear demand for (3) in some form as well IMO, and it seems worth ensuring we have a good story here. While I'm not opposed to re-using syntax with (2), I think it is somewhat important to separate them semantically. Even if we insist that arguments using (3) use a consistent order, they certainly don't have fixed positions. As a consequence, the name seems like it must be mandatory here at a minimum, while in (2) some amount of optionalness seems important. I also find the arguments to allow different orders convincing, and that further differentiates these use cases. Despite the differences, I do think we should harmonize any syntax used for (2) and (3) if not outright match them.
  • I only see (4) as an important facility to provide for (1). And since I'm not convinced we should pursue (1), I don't see much reason to enable (4).

Given all of this, what do folks think about these use cases? I'd be interested if there are strong reasons why we need to add (1) especially, as it seems like the most divergent. Similarly I'd like to confirm that everyone actually agrees about supporting both (2) and (3), and understand whether there is consensus around optional vs. mandatory labelling in those respective cases. And last but not least, whether anyone sees important reasons to support (4) for use cases (2) and (3).

@geoffromer
Copy link
Contributor

I broadly agree with @chandlerc's take, but if we continue to follow the model where function parameter lists are tuple patterns, it might be difficult for us to avoid supporting (4), and that in turn might get us close enough to supporting (1) that people start using that way. If so, it might be better to bite the bullet and support those use cases intentionally.

@chandlerc
Copy link
Contributor

I have a few clarifications after chatting about this with @josh11b -- thanks for the chat, it helped! Will write them up as separate comments as I think through them.

First, apologies both to @josh11b and @jonmeow -- I had really missed an interesting use case that also motivates (4) here: how labels apply to destructuring variable declarations. I think @geoffromer this is also what you're referencing.

I think there are really two things here from a use-case perspective. Let's call these:

  1. Labelling a positional tuple pattern to ensure it destructures the intended positional field (whether from a tuple or struct).
  2. Destructuring non-positional values from a tuple (or struct) necessarily identified by name.

Both of these directly correspond to use cases with passing arguments to functions, but somewhat in reverse -- they seem likely to be used for the returns from a function call. (5) corresponds to (2), and (6) to (3).

The key observation here is that if we want (5) and (6), then without (4) local variable names would have to follow exactly the field or other names, which seems likely untenable.

Second clarification is around (3) -- my comment said "optional", but whether or not non-positional named parameters are optional or required isn't essential to the use case. I think C++'s default arguments provide a good reason to at least support optional parameters, but I suspect we should also support required but still non-positional and named arguments for (3).

I think there was a third clarification, but I need to think more as my brain got interrupted mid-discussion....

@chandlerc
Copy link
Contributor

Updated to strikethrough use case (1) as #509 was resolved "no" (for now).

@josh11b
Copy link
Contributor

josh11b commented May 13, 2021

As a datapoint, Javascript supports destructuring by name (case 6), including allowing renaming (case 4), though the syntax is very different with renaming. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#object_destructuring

No renaming example from that page:

const user = {
    id: 42,
    is_verified: true
};

const {id, is_verified} = user;

console.log(id); // 42
console.log(is_verified); // true

Renaming example from that page:

const o = {p: 42, q: true};
const {p: foo, q: bar} = o;

console.log(foo); // 42
console.log(bar); // true

@josh11b
Copy link
Contributor

josh11b commented May 13, 2021

One option to consider: I believe we can handle case 4 using a separate operation. This would be something, which I am going to call extract but am open to other names, that would take a list of fields and a value and return an unnamed tuple with the value for those fields in order. You might use it like so:

var (String k, Int v) = extract((.key, .value), F());

This would be equivalent to:

var auto TEMP = F();
var (String k, Int v) = (TEMP.key, TEMP.value);

@chandlerc
Copy link
Contributor

Chatting a bit with @zygoloid - we're both pretty happy with at least saying "no" to use case (4) entirely based on the suggestion from @josh11b for how to handle that. Remaining use cases are 2, 3, 5, and 6.

@josh11b
Copy link
Contributor

josh11b commented May 19, 2021

I have been trying to write out choices in this Google doc. I'd like to propose we go with something simple:

Should the declarer be able to rename parameters for callers? No
Should the caller or declarer decide to specify arguments with names? Declarer
Is fn Foo(Int x) called using a named parameter? No
Are named parameters positional? No

Should non-defaulted parameters be allowed after defaulted parameters? Labeled/non-positional parameters are always after unlabeled/positional parameters in both declaration and caller. The tail of the positional parameters may have defaults, and any subset of the labeled/non-positional parameters may have defaults.

Overall, what does the named parameter approach look like?

Using our current "<type> <name>" declaration syntax:

fn F(Int x, Int y = 0, named Int w, named Int z = 0);

That is, named parameters have a keyword (named here, but I'm open to other suggestions) marking they are named. This works equally well if we switch to "<name>: <type>" declaration syntax:

fn F(x: Int, y: Int = 0, named w: Int, named z: Int = 0);

The main downside here is that the named keyword is too big to make a distinction where the consequences are very mild if the reader misses that distinction. If you are reading an API, you only care about the names of the parameters to understand their function. The named distinction is only relevant when calling the function, and making a mistake will generally be diagnosable by the compiler.

@chandlerc
Copy link
Contributor

I have been trying to write out choices in this Google doc. I'd like to propose we go with something simple:

I think I agree with your suggestion, especially given the current variable syntax.

I'm not a big fan of the keyword approach for basically the reason you outline. I'm also not a fan of the aesthetics. But I agree with all of the positives you list -- it is simple, direct and unambiguous.

That said, I think the syntax considerations here really do suggest revisiting this, and I've filed #542 to do so. I'm curious whether the other leads would rather just used named as suggested for now while sorting out #542 (my mild preference) and then potentially suggest improved syntax, or if they'd rather resolve #542 first and then consider the syntaxes here.

I'd also like to suggest that I'm increasingly convinced that regardless of the syntax used, the answers are the ones we should use. I think important aspects of this is that I think considering named positional parameters adds significantly more complexity than it is worth. Having a sharp separation between positional (and never-named) and non-positional named parameters seems like a really good direction semantically.

@jonmeow
Copy link
Contributor Author

jonmeow commented May 20, 2021

Use-case #3 seems to be the main non-positional use-case. I'll comment I'm focusing on that as I want to offer up a few different syntaxes versus the repeated named keyword.

Echoing Python, we could make a named keyword be a separator between positional and named parameters, such as fn F(Int x, Int y = 0, named: Int w, Int z = 0);. This is similar to Python's *, but using a Josh's suggested named as a keyword to improve readability. As a reserved keyword, I would hope it could also work even in fn F(named: w: Int) syntax forms, although a different syntax may help (maybe a semicolon before named: with positional params)?

Alternately, I'll note that providing a default for a positional parameter seems to be getting assumed to be an important use-case (I don't think this is explicit in the aforementioned 6 use-cases). If that use-case could be discarded, and we could go with an approach that requires all named parameters to be non-positional, and all defaulted parameters to be named. An example would be syntax like fn F(Int x, Int y, Int w = _, Int z = 0);, using _ as a keyword for a non-defaulted, named parameter. The works trivially in fn F(w: Int = _) syntax forms.

Note Ruby-like syntax could restore position defaults on the latter, such as fn F(Int x, Int y = 0, Int w: _, Int z: 0). This works less well if : is in the parameter type/name though.

@chandlerc
Copy link
Contributor

Just to leave a note here that the leads are explicitly deferring this question and #476. We're not opposed to named parameters and arguments, but the initial motivation for prioritizing this right away seems better addressed separately, and it seems valuable to more fully understand the expected syntax for things like struct literals and pattern matching generally if possible to better inform any decision.

Leaving the question open to make it clear that this is something we can and should expect to revisit in the future.

@github-actions

This comment was marked as outdated.

@github-actions github-actions bot added the inactive Issues and PRs which have been inactive for at least 90 days. label Sep 16, 2021
geoffromer added a commit that referenced this issue Oct 15, 2021
Rationale: Based on the status of #478 and #505, Carbon won't have this feature for a while, and it will be simpler not to support it on spec in the meantime.
chandlerc pushed a commit that referenced this issue Jun 28, 2022
Rationale: Based on the status of #478 and #505, Carbon won't have this feature for a while, and it will be simpler not to support it on spec in the meantime.
@jonmeow jonmeow added leads question A question for the leads team long term Issues expected to take over 90 days to resolve. and removed inactive Issues and PRs which have been inactive for at least 90 days. labels Aug 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
leads question A question for the leads team long term Issues expected to take over 90 days to resolve.
Projects
None yet
Development

No branches or pull requests

6 participants