Include argument labels in identifiers

Ever since SE-0111, argument labels have not been part of function types. While this makes sense, it leads to a dearth of labels that can be difficult to work with.

This prompts a question: where do argument labels live? They aren’t in the type, and they obviously aren’t in the body. That leaves one place: the name.

This is already acknowledged in documentation, such as the following excerpts from the Language Guide:

The print(_:separator:terminator:) function doesn’t have a label for its first argument, and its other arguments are optional because they have a default value.

Here’s a function called chooseStepFunction(backward:) , whose return type is (Int) -> Int . The chooseStepFunction(backward:) function returns the stepForward(_:) function or the stepBackward(_:) function based on a Boolean parameter called backward:

func chooseStepFunction(backward: Bool) -> (Int) -> Int {
    return backward ? stepBackward : stepForward
}

I propose making this an actual part of the language, such that argument labels can be specified in an identifier:

// identifier: “instance”; type: Int
let instance = 42

// identifier: “firstMethod(_:)”; type: (Int) -> Bool
let firstMethod = instance.isMultiple

// identifier: “firstResult”; type: Bool
let firstResult = firstMethod(8)

// identifier: “secondMethod(_:)”; type: (Int) -> Bool
let secondMethod(_:) = firstMethod

// identifier: “secondResult”; type: Bool
let secondResult = secondMethod(6)

// identifier: “possibleProduct(from:)”; type: (Int) -> Bool
let possibleProduct(from:) = secondMethod

// identifier: “thirdResult”; type: Bool
let thirdResult = possibleProduct(from: 7)

This syntax is currently prohibited by the compiler, so it wouldn’t cause any breaking changes. The behavior of existing identifiers would be completely unchanged, since they would implicitly continue to have no argument labels.

The nomenclature has already been tacitly accepted by the Swift community for years, so I think it would be an excellent addition to Swift.

9 Likes

It's not only tacitly accepted, it is explicitly the plan of record:

To reformat the text for this forum, the relevant part is as follows:

First, we extend declaration names for variables, properties, and parameters to allow parameter names as part of their declaration name. For example:

  var op(lhs:,rhs:) : (Int, Int) -> Int // variable or property.
  x = op(lhs: 1, rhs: 2) // use of the variable or property.

  // API name of parameter is “opToUse”, internal name is "op(lhs:,rhs:)”.
  func foo(opToUse op(lhs:,rhs:) : (Int, Int) -> Int) {
    x = op(lhs: 1, rhs: 2) // use of the parameter
  }
  foo(opToUse: +) // call of the function

This will restore the ability to express the idea of a closure parameter that carries labels as part of its declaration, without requiring parameter labels to be part of the type system (allowing, e.g. the operator + to be passed into something that requires parameter labels).

Second, extend the rules for function types to allow parameter API labels if and only if they are used as the type of a declaration that allows parameter labels, and interpret them as a sugar form for providing those labels on the underlying declaration. This means that the example above could be spelled as:

  var op : (lhs: Int, rhs: Int) -> Int // Nice declaration syntax
  x = op(lhs: 1, rhs: 2) // Same as above

  // API name of parameter is “opToUse”, internal name is "op(lhs:,rhs:)”.
  func foo(opToUse op : (lhs: Int, rhs: Int) -> Int) {
    x = op(lhs: 1, rhs: 2) // Same as above.
  }
  foo(opToUse: +) // Same as above.

These two steps will provide the simple and expressive design approach that we have now, without all of the problems that representing parameter labels in the type system introduces.

7 Likes

I knew it seemed too obvious. Still, it’s been years, and it would solve a few problems that are cropping up elsewhere.

1 Like

Sure, it would be great to continue this discussion to see if it is worth solving!

1 Like

Here is a related thread I created two years ago:

2 Likes

This is a no-brainer, it's clearly missing from the language.

4 Likes

For one thing, this would allow properties which are closures to witness protocol requirements which are functions, which is a feature I’ve wanted for a long time.

If we were to do that today, we’d have to drop the argument labels, since closure properties don’t support them.

4 Likes

Can you explain what you mean by that. Isn‘t this whole feature to add parameter labels back to the call side, even though it‘s sugar.

This isn’t syntactic sugar. This is critical for complying with protocols, conveying clarity at the point of use, and disambiguating between functions.

3 Likes

What do you mean? The call side of the compound properties is syntactic sugar.

var foo(a:): (Int) -> Void
// no sugar
foo(a:)(42)
// sugar
foo(a: 42)

Feel free to correct me if I‘m wrong. :thinking:

protocol FooProtocol {
  func doAThing(using: String)
}

struct ConformsUsingClosure: FooProtocol {
  var doAThing: (String)->Void = { print($0) }
}

let instance = ConformsUsingClosure()
instance.doAThing("where did my label go?")

let erasedInstance: FooProtocol = instance
erasedInstance.doAThing(using: "Okay, now we get a label?")

Since closures don't support argument labels, we'd have to drop the "using:" label when invoking the function in a concrete context. I asked about adding support for this before, and the consensus was that dropping the argument labels was not acceptable (so it's not a technical limitation - it's just something we don't want to support until we can preserve the labels).

If we could instead write this:

struct ConformsUsingClosure: FooProtocol {
  var doAThing: (using: String)->Void = { print($0) }
}

Then we get a nice, consistent calling syntax, and there shouldn't be any objection to allowing the closure to witness the function requirement.

5 Likes

One thing I still do not understand is why it should even work if you only implement a closure with a non compound name, this is name mismatch. I think only your latter form should witness the protocol requirement as it‘s pure sugar over the compound name.

var foo: (a: Int) -> Void
// is sugar for 
var foo(a:): (Int) -> Void
// and is different from 
var foo: (Int) -> Void

Last closure in the example cannot and should not witness func foo(a: Int) from a protocol requirement, but only the first two closures can.

1 Like

Ah, I think you misunderstand - I’m not saying that the first example should work. That’s just all we can do in the language today, since closures can’t have argument labels.

The latter form isn’t valid Swift - if it were (that is to say, if we had the feature this thread is about), then it would be valid Swift, and the closure could theoretically witness the protocol requirement.

So I think we’re on the same page.

1 Like

Indeed I misunderstood you at first, and yes I agree with that direction. I'm also keen to see if this will change something about optional requirement we can have for Obj-C types.

For example:

protocol Foo {
  optional func bar(a: String)
}

// could be pure sugar for
protocol Foo {
  var bar(a:): ((String) -> Void)?
}

extension Foo {
  var bar(a:): ((String) -> Void)? { .none }
}

And it would be cool if optional chaining would be adopted to support foo.bar?(a: "swift") calls.

This one's not going to work because @objc optional requirements have to be compatible with -respondsToSelector:. But on the plus side, the optional chaining syntax you demonstrate already works.

But does it have to be @objc only? I mean, could the above not be allowed as pure swift version of the optional keyword which would be translated into the compound closure?

By the end of the day, if the answer is still "no", then I still would be able to write the above boilerplate-ish code manually to achieve the same, but optional func bar(a: String) would be much cleaner and readable.

And since we love optional customization points that can be satisfied with non-optional implementation from the conforming type, we should not forget that closures with compound names as protocol requirements which should be satisfied by functions as well (even optional closures).

protocol Bar {
  var first(a:): (Int) -> Void { get }
  var second(b:) ((Int) -> Void)? { get }
}

struct S: Bar {
  func first(a: Int) {}
  func second(b: Int) {}
}

Another thought: Can we have get set compound closures as protocol requirements in the future? Maybe with generalized coroutines?

protocol P {
  var foo(a:): (Int) -> Int { get set }
}

struct T: P {
  func foo(a: Int) -> inout Int { ... }
}

Maybe I miss something here. :thinking:

Hm, that's an interesting point. I think they'd still be different, though, because the var version is allowed to change during an instance's lifetime, and the optional version probably wouldn't be.

Historically, the main reason to not bring optional requirements to pure Swift protocols has been because default implementations have generally been considered a better solution for default behavior, but it does make it harder to check for capabilities. There are probably older threads that explain this better than I've been.

2 Likes

Those two declarations wouldn't be compatible.
The second would mean that foo(a:) returns a[n inner] modifiable integer,
while the first would mean that the function foo(a:) itself is modifiable.

This would imply that a {get set} function property cannot be satisfied by a func declaration. The closest thing would be the [non-@objc] dynamic func prototypes I've seen in the Swift commit history.

1 Like

Ah, yeah, I had not enough :coffee: this morning. It probably will be var foo(a:): (Int) -> inout Int { get }. :upside_down_face: