Extending declaration names for closures

SE-0111 was accepted with a small temporary regression of loosing parameter labels in closures. In an extra commentary to this regression the core team pitched a solution that would be purely additive to the language without being a burden for the type system like pre SE-0111.

The solution has to come in two steps:

  1. We have to extend the declaration names to allow labels in parentheses for variables, properties and parameters. (The following code snippet does not use , in between labels because in my honest opinion this is unnecessary and even inconsistent to how we already reference to functions foo(first:second) today.)

    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 
    
  2. We extend the rules for function types to allow API labels to simplify the syntax from above. (This step would be pure sugar over the first step and won't replace it.)

    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 
    

I personally think while the second step would be nice, it is complementary to the core functionality introduced by the first step. Thus we should keep our focus on the main functionality.

However, I'm very open minded in that regard. That said, if someone can tackle the implementation of both steps for Swift 5 that would be great.

17 Likes

Furthermore I think we also should allow multi-line declaration names when they contain parentheses similar to how we can break down a closure type into multiple line when necessary (for instance in a 80 character line width code base):

var operate(
  first:
  second:
  third:
  fourth:
  fifth:
): (
  SomeType,
  OtherType,
  SomeVeryLongNameForAType,
  YetAnotherType,
  LastType
) -> Void = ...
3 Likes

I have been looking forward to this for a long time, and I’m glad you brought it up.

While you are technically correct that the second step would be entirely syntax sugar, your last example shows how important it can be to keep the labels adjacent to the types:

It is not obvious at a glance which label corresponds to which type. It would be much easier to read and understand if written as:

var operate : (
  first : SomeType,
  second: OtherType,
  third : SomeVeryLongNameForAType, 
  fourth: YetAnotherType,
  fifth : LastType
) -> Void = ...
5 Likes

Yeah sorry that was a typo, I added -> Void now.

You are correct that it is not obvious but this should be supported anyways even if step 2 will refine the syntax from the readability standpoint.

Thank you Adrian for resurrecting this topic.
I remember that thread was quite heated as more people became aware of the changes and the regression and maybe there were a bit over the top emotions sometimes, but the issue was concerning enough and the core team re-discussed and agreed a fix was needed and that the situation was temporary. I am afraid that source and ABI stability goals may kill this for good which I think is a huge wasted opportunity for the language.

It would be good to hear core maintainers chime on this again please, this is not an easy community task, but it something the language needs and we lost a big push for clarity at call site and we should gain it back.

6 Likes

Future direction that is orthogonal to this pitch:

Introduction of optional function to pure Swift

The extended declaration names and the sugar from the second step together with an automatic extension synthesizing could allow us to introduce optional function to Swift protocols. (Possibly it will allow us to drop @objc attribute for optional functions as well.)

protocol P {
   optional func foo(first: Int, second: String) -> SomeReturnType
}

// The compiler would synthesize the following extension:
extension P {
   // If we only had (1) from the original pitch
   var foo(first:second:): ((Int, String) -> SomeReturnType)? {
     return nil
   }

  // If we had both (1) and (2)
  var foo: ((first: Int, second: String) -> SomeReturnType)? {
    return nil
  }
}
1 Like

IIRC the optional attribute of protocol requirements was discussed a couple of times already and dismissed as not fitting with Swift.
It makes more sense imho in ObjC where the nature of objects is very dynamic anyway.

1 Like

Everything in this particular reply is orthogonal to the original pitch, but is a possible consequence from it. We should not debated about that intensively now because it is subject of a future discussion assuming the original pitch made into the language. However I just wanted to mention it here, so that everyone is aware of it.


The first part is only partly correct. Indeed it was discussed before but in a different context, which would align it with the behaviour of Objective-C. However I showed a different approach which would be mostly pure sugar like the second step is from the original post.

A protocol requirement marked as optional will tell the compiler to synthesize an extension with a default implementation that returns nil for the whole closure. Nothing really special here. The only extra thing that requires support is the infix ? between the function name and the parameter list foo?(first: 42, second: "Swift"). That should work because it has also been discussed whether or not protocol requirement should be satisfied with closures that uses this extended declaration names, and the idea was supported.

protocol Q {
   func bar(label: Int)
}

struct S : Q {
   var bar(label:): (Int) -> Void

   init(_ closure: (Int) -> Void) {
     self.bar(label:) = closure
   }
}

In fact this should also work the other way around, as long as only a getter is required:

protocol R {
   var bar(label:): (Int) -> Void { get }
}

struct T : R {
   func bar(label x: Int) { ... }
}

The only question is what will we do about optional closures? Are those allowed to use the extended names?

var something(label:): ((Int) -> Void)? = ... // Is this allowed?

If this is valid then making optional as pure sugar is only natural.

However if it's not there is still a different way to synthesize the extension.

protocol P {
  // non-optional return type
  optional func foo(first: Int) -> Result
  // optional return type
  optional func bar(first: Int) -> Result?
}

extension P {
   // return type becomes optional
   var foo(first:): (Int) -> Result? {
     return { _ in nil }
   }
   
   // return is again wrapped into an optional (consistent behavior)
   var bar(first:): (Int) -> Result?? {
     return { _ in nil }
   }
}

// usage of optional functions requires an infix `?` or `!`
let some1: Result? = p.foo?(first: 1)
let some2: Result = p.foo!(first: 1)

let some3: Result?? = p.bar?(first: 2)
let some4: Result? = p.bar!(first: 2)
let some4: Result = p.bar!(first: 2)!
1 Like

I support extending declaration names, but I think you're getting ahead of yourself by bringing up optional protocol methods. The former is a widely desired feature with approval from the core team; the latter a controversial subject, requiring a feature which is not yet implemented. The general response from past discussion seems negative (with some going so far as to suggest eliminating optional methods entirely), so you're probably not doing yourself any favours by mentioning it as a future direction.

5 Likes

That said, from the same topic we have your suggestion from Joe Groff:

We may get the feature, but I still wouldn't bring it up this far in advance.

1 Like

Thank you for finding that post, interestingly enough non of the points Douglas Gregor made remain true post SE-0111, they were true before SE-0111 but are no longer correct. He describes the workarounds as "hacks", but those workarounds won't be any hacks anymore if the original pitch from the fist post will be part of the language.
In that regard I don't think it does hurt anyone if I shared my ideas about optional requirements in this context. ;)

1 Like

This closing statement sums up the negativity around this topic:

The main thing that can be brought to the discussion at this point isn't how it would work, but rather what the use-case is. At some point it may be a simple sugar feature, or even a natural consequence of other improvements, but it just doesn't seem to carry it's weight as an independent feature.

I have to disagree with that statement from Douglas because the API describes difference semantics. Sometimes it's impossible to have a default return value for the default implementation and then the whole requirement must become optional, either by moving it to a different protocol and using existentials when required, or by making the non-optional return type optional and provide a default implementation that returns nil. However the latter breaks the optional requirement itself, because it's not the return type that should be optional but rather the whole function. The user that implements the requirement then definitely will wonder why the return type is made optional where it really shouldn't be. Yes it is fact that an optional requirement affects the return type but it shouldn't be seen as if a functions return type must become optional in a regular protocol requirement.

protocol Proto {
   optional func foo()
   func bar() -> Void?

   optional baz() -> Int
   func mad() -> Int?
}

These functions foo and bar, baz and mad has different semantics even if the returning type in every case is the same Void? or Int?.

Anyways, this is orthogonal and we should focus on the main issue. :nerd_face:

1 Like

Thanks for bringing this up! I definitely want to see this happen soon. I don't think implementing #2 would be that much more work than just #1, and would greatly improve the ergonomics of closures with compound names in their most common use cases.

15 Likes

+1, it would be great if argument labels return to closures

1 Like

Any suggestions on how we can drive this topic forward?

1 Like

Swift 5 getting closer and closer but we still have no argument labels in closures.

It would be great to hear about any closure argument label plans in context of Swift 5 release. Lack of arguments labels in closures confuses a bit and I think it is very important changes in syntax to be included in Swift 5.

9 Likes

Bumping this thread so maybe someone can pick up the topic again! :slight_smile:

5 Likes

I posted a more comprehensive pitch here last year to try to move this forward, and have a (now outdated) PR up for some initial refactoring work here that never got reviewed and so never moved forward. AFAIK this is the latest activity on this topic.

I'm still interested in picking this back up as soon as I have the time, though that shouldn't discourage anyone else from jumping on it in the meantime! :slight_smile:

15 Likes

I sometimes use a struct with a callAsFunction(...) method instead of using a closure.
You gain argument labels at the call site, but it requires more code than simply passing closures around.
Improved clarity but less brevity.

3 Likes