Shorthand syntax when variable name matches argument label?

I've noticed that I often end up calling functions -- especially memberwise initializers -- with variables that have the same name as the function's argument labels. Something like:

struct Person {
  var name: String
  var age: Int
}

// elsewhere...

let name = //...
let age = //...

let person = Person(name: name, age: age)

It kind of makes me wish there were a shorthand for this, where the compiler inferred the argument label based on the variable name.

I don't feel strongly about the actual syntax, but here's an example to illustrate:

let person = Person(: name, : age) // compiler infers `Person.init(name:age:)`
// or
let person = Person(_: name, _: age)

Another place this comes up is in initializers that convert between two similar types, like:

extension Person {
  init(_ src: API.Person) {
    self.init(name: src.name, age: src.age)
  }
}

I don't know if this would generalize well... But, for this specific case, it might be pleasantly concise to be able to write something like:

self.init(: src.name, : src.age) // compiler infers `init(name:age:)`

Would anyone else find this useful?

2 Likes

+1

I also wish there were something for this as I myself do the same thing quite often and I'm sure we're not alone.

1 Like

I understand the general idea but I'm against the pitched syntax, it just hurts the readability and becomes cryptic. Furthermore this pitch is about a sugar addition to the language which at this age of the language is inappropriate. I've been pitching the alignment of the return keyword for single expressions for the past two years and got rejected by the core team before it even could go through a review.

1 Like

Too cryptic for my liking - sorry.

I disagree with this. The pitch states that this would only occur when the variable name matches the parameter name. Therefore readability is not harmed and this would not become cryptic. It would however reduce code.

className.functionName(: names)

is not any more cryptic than

className.functionName(names: names)

In both instances it is obvious we are passing names to the function. It is less verbose, but that's not necessarily a bad thing. I would argue that this could reduce a lot of code across so many different projects without compromising clarity, and because an explicit (: variableName) type of syntax would require the developer to be aware that the variable name matches the parameter name. Therefore, to the developer who opts-in to use this feature, it would not be any less clear what they were doing.

I often find myself making all arguments unnamed (using the _ paramName: Type syntax) because I hate typing out each and every parameter name. Since the variables I'm passing to the function/initializer almost always match the name of the parameter anyways? Why not automatically allow me to reduce the amount of code I have to write? This is especially the case for structs that automatically receive an init with all named parameters. I find it very annoying to have to write my own initializer for unnamed parameters. This would make that unnecessary as I could still just as easily initialize a struct without having to write my own initializer.

You sure can, it does not mean I can have that opinion. We had the same issue with enums and we solved it without the :.

https://github.com/apple/swift-evolution/blob/master/proposals/0155-normalize-enum-case-representation.md#pattern-consistency

I don't say I'm against the general idea of this pitch, I'm just against the pitched syntax. The simplest solution is just to go a similar path that we already did in SE-0155 and accept ignoring the label if any expression is passed where it's last chained member is called the same as the label would be.


That said, the following example

className.functionName(: names)
className.functionName(: self.storage.names)

should be written as

className.functionName(names)
className.functionName(self.storage.names)

to shadow the labels for

className.functionName(names: names)
className.functionName(names: self.storage.names)
2 Likes

I agree that this simpler solution is cleaner, however, if there are any concerns about this being unintentionally abused or causing ambiguity, then the (: variableName) syntax would make this an opt-in feature that would not be accidentally used by unexperienced developers.

That being said, since [SE-0155] has been approved and is just waiting to be pulled into the main swift branch, then I don't see why we shouldn't just use the simplest syntax as you're suggesting.

I don't see how (: something) is different from (something) in terms of ambiguity, both can be ambiguous to the compiler for the same reasons.

I'm also not against such an addition at all. All I was saying is, we already solved it in a nice and clean way and we should go that path again, but we solved it for a different not purely sugar purpose. However this pitch is pure sugar and discussions from the past showed that the core team won't accept such proposals even for a review at this age of Swift.

Here is what @Chris_Lattner3 said back then.

I meant ambiguity in terms of the compiler searching for the matching parameter. I guess it shouldn't ever be ambiguous though considering you can't have a function/initializer with multiple parameters that have the same name. So please disregard my earlier incorrect statement :)

You keep saying this is purely sugar, which I don't disagree with, but I'm a little confused about how this is any less sugar than [SE-0155], which was accepted? When was the [SE-0155] proposed/accepted?

Also, the [SE-0155] implementation hasn't been pulled in to the main swift branch anyways, so I don't really see why this sugar couldn't also be accepted. Of course, that's a poor excuse that doesn't hold much weight.

That being said, if the core team decides this is too sugary and not enough benefit to justify a proposal, then that is their decision. I don't see any harm in pitching it and getting the opinions from the community until the core team decides on whether or not to accept or reject this pitch.

There are reasons pitches are "commonly rejected", not "absolutely rejected" for the very reason that there are always exceptions to the rules. Not saying that this should/will be one of those, but merely as a reason for why the pitch should still be made/discussed.

SE-0155 wasn't purely about sugar it switched completely the way how enum cases are represented. Associated values were tuples before but now you can write enums that look like this:

public enum Value {
  case double(Double)
  case string(String)
  ...
  case javaScript(String)
  case javaScript(String, scope: Document)
  ...
}

Tuple will no longer be used to represent the aggregate of associated values for an enum case.

Note that this part of SE-0155 was not accepted; see the core team’s review notes for details.

1 Like

Wait what? Okay I apologize for miscommunication and misleading information in this thread.

The proposal includes a rule inferring labels in case patterns from binding names. The core team feels that imparting local variable names with this kind of significance would be unprecedented, surprising, and rather "pushy". The goal of this rule is also largely achieved by the new rule allowing labels to be omitted regardless of binding. Accordingly, this rule is struck from the proposal. That said, it would be a good idea for the implementation to warn when a binding name matches the label for a different associated value.

i've been writing a lot of ruby recently after a long time not writing any, and the syntax they introduced for keyword-arguments w/ matching local variables it pretty nice. i was resistant at first, but now really appreciate the de-noising it affords. coming back to swift, i really miss it.

let name = "name"
let age = "age"

self.init(name:, age:)

in ruby, it works for for any values available in the current scope (variables, same-named getters), and after first exposure it's easy to understand. in swift, i personally wouldn't allow this for instance variables or getters, as i think that's more likely to produce surprising behavior, accidentally leak self-references, &c.

the syntax is a bit more natural than the above-proposed version, it's more limited in scope, requires less visual parsing to understand that the name it's going to pass down is the leaf of the value. do kinda like how that version doesn't require any code-scanning to know where the values come from, though.

2 Likes

In Swift code this would be easily mistakable with a function reference

func foo(bar: String, baz: String) -> String { "foo" }

let bar = "bar"
let baz = "baz"

let ref = foo(bar:baz:)  // ref is (String, String) -> String
let val = foo(bar:,baz:) // val is String
1 Like