-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Comments
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, |
Thank you for pointing this out! I hadn't noticed that. I've tried to clarify this, and added positionality as a question. |
Clarification on Python:
So Python is actually in the third "hybrid" category for "Should the caller or declarer decide to specify arguments with names?" |
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:
Re: Is 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+. |
Added "Should non-defaulted parameters be allowed after defaulted parameters?" (separately, had this in flight while josh11b was commenting) |
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.
Yes, fixed.
See "Is
|
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 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
|
I feel like a separator option should be under consideration. In particular, if we used the
where |
@josh11b Do you have a specific corresponding call-site syntax in mind? |
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:
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:
My personal opinion about these use cases:
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). |
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. |
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:
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.... |
Updated to strikethrough use case (1) as #509 was resolved "no" (for now). |
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:
Renaming example from that page:
|
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
This would be equivalent to:
|
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 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 "
That is, named parameters have a keyword (
The main downside here is that the |
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 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. |
Use-case Echoing Python, we could make a 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 Note Ruby-like syntax could restore position defaults on the latter, such as |
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. |
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):
fn Foo(Int x)
called using a named parameter?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:
*
for requiring named parameters. For example, indef Foo(x, *, y=3)
callers must cally
by name.def Foo(x)
is called with eitherFoo(3)
orFoo(x=3)
func Foo(x: Int)
is called withFoo(x: 3)
.func Foo(y x: Int)
is called withFoo(y: 3)
.func Foo(x: Int, y: Int)
must be called asFoo(x:1, y:2)
;Foo(y:2, x:1)
is invalid.func Foo(_ x: Int)
is called withFoo(3)
.def Foo(x)
is called withFoo(3)
def Foo(x: 1)
is called withFoo()
orFoo(x: 1)
def Foo(x:)
is called withFoo(x: 1)
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:
Keeping correspondence for the locations of
c
andd
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" ofx
,func Foo(y x: Int)
renames toy
.Two use-cases have been suggested for this:
in
is a keyword in Swift,func Print(in data: String)
allows callers to writePrint(in="message")
.func greet(person: String, from hometown: String) -> String
allows the function definition to writehometown
while the caller writesgreet(person: "Bill", from: "Cupertino")
.In Carbon, regarding (1):
r#keyword
syntax. If adopted andin
is a keyword, perhaps callers could still writePrint(in="message")
even if the declarer writesfn Print(String r#in)
.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:
def Foo(x)
, bothFoo(1)
andFoo(x=1)
are valid calls.func Foo(x: Int)
,Foo(x: 1)
is valid, andFoo(1)
is invalid.func Foo(_ x: Int)
,Foo(x: 1)
is invalid, andFoo(1)
is valid.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 withFoo(x: 1)
.func Foo(_ x: Int)
makes the caller be less verbose withFoo(1)
.We could invert this trade-off by changing syntax from
_
, for example with anamed
keyword:func Foo(named x: Int)
makes the caller more verbose, withFoo(x: 1)
func Foo(x: Int)
makes the caller less verbose, withFoo(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 eitherFoo(x=1, y=2)
orFoo(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 withFoo(x: 1, y: 2)
; it is invalid to writeFoo(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 callFoo(3)
setsy
to3
becausex
requires a name and is positional. Therefore,3
corresponds to they
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?
fn Foo(Int x)
called using a named parameter? Caller decides.def Foo(x)
->fn Foo(Int x)
, called withFoo(1)
def Foo(x = 3)
->fn Foo(Int x = 3)
, called withFoo()
,Foo(1)
, orFoo(x: 3)
def Foo(x, y)
->fn Foo(Int x, Int y)
, called withFoo(1, 2)
,Foo(1, y=2)
,Foo(x: 1, y: 2)
, orFoo(y: 2, x: 1)
._
in_ x: Int = 1
simply because_
is likely to be used nearby for unused arguments per Optional argument names (unused arguments) #476.fn Foo(Int x)
called using a named parameter? Yes.func Foo(x: Int)
->fn Foo(Int x)
, called withFoo(x: 1)
func Foo(x: Int = 1)
->fn Foo(Int x = 1)
, called withFoo()
orFoo(x: 1)
func Foo(y x: Int)
->fn Foo(y: Int x)
, called withFoo(y: 1)
func Foo(y x: Int = 1)
->fn Foo(y: Int x = 1)
, called withFoo()
orFoo(y: 1)
func Foo(_ x: Int)
->fn Foo(_ Int x)
, called withFoo(1)
func Foo(_ x: Int = 1)
->fn Foo(_ Int x = 1)
, called withFoo()
orFoo(1)
func Foo(x: Int, y: Int)
->fn Foo(Int x, Int y)
, called withFoo(x: 1, y: 2)
(no other variations).fn Foo(Int x)
called using a named parameter? No.def Foo(x:)
->fn Foo(Int x:)
, called withFoo(x: 1)
def Foo(x: 1)
->fn Foo(Int x: 1)
, called withFoo()
orFoo(x: 1)
def Foo(x)
->fn Foo(Int x)
, called withFoo(1)
def Foo(x = 1)
->fn Foo(Int x = 1)
, called withFoo()
orFoo(1)
def Foo(x:, y:)
->fn Foo(Int x, Int y)
, called withFoo(x: 1, y: 2)
orFoo(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.
The text was updated successfully, but these errors were encountered: