Compound variable names

Ah. So my subtyping claim is incorrect. My bad, I should've checked. But the question about it being permissible for types with callAsFunction still remains. Should the following compile?

struct S {
  func callAsFunction(x: Int) { print(x) }
}
func foo(f f(x:): S) {
  f(x: 10)
}
1 Like

I lean towards "no" since we don't have more general equivalence between "callable" types and function types with matching parameter/return types. Further, these callable types don't suffer from the same problem as closures generally:

struct S {
  func callAsFunction(x: Int) { print(x) }
}

func foo(f: S) {
  f(x: 10) // This already works!
}

Also, there are more difficult questions that would have to be answered, such as what to do with a type that defines multiple callAsFunction methods:

struct S {
    func callAsFunction(x: Int) { print(x) }
    func callAsFunction(y: String) { print(y) }
    func callAsFunction(a: String, b: Int) { print(a); print(b) }
}

func foo(f(x:): S) {
    f(x: 10)
    f(x: "") // ?
    f(x: "", b: 0) // ??
}

Overall, I think this question is better left to a time when (if?) we want some sort of conversion/equivalence for these types.

3 Likes

Nice advice, but not necessary to current syntax system; more harder to write but less benefits to get for developers.
-0.5 for now

I love it. Argument labels are one of the coolest features of swift, and I was annoyed that you lose them when you pass functions around. Your solution is the ideal one in my opinion.

I'm neutral about if Xcode should offer a fixit for removing/adding labels

Can we make it so internal labels are used to guide the autocompletion, so that you don't have to guess which argument of a callback is which?

// today
func takesCallback(callback: (Int, Int, Int) -> Void) { }
takesCallback { (<#Int#>, <#Int#>, <#Int#>) in
    <#code#>
}
// future?
func takesCallback(callback callback(a:b:c:): (Int, Int, Int) -> Void) { }
takesCallback { (a, b, c) in
    <#code#>
}
7 Likes

I really like that. I’m not actually clear on how much of code completion falls under Swift Evolution, but if that’s in-scope then I’m all for it!

2 Likes

This uses different syntax which does not involve key paths. It wouldn’t make sense to access an unbound method from an instance like this. The syntax that has been discussed has always been using keypath syntax.

Here are a couple of threads that have included discussions of this feature:

1 Like

I understand the feature you're talking about, and I wasn't trying to propose an alternate key path syntax for unbound methods—my point was about the syntax used today for normal instance-member access of bound methods.

I took your objection to be something like, "unbound method key paths already are expected to use the \.f(x:) syntax, so this proposal would potentially introduce a conflict." My response was meant to illustrate that, in a future world with both compound-name members and unbound method key paths, I see no issue with using the same syntax to refer to both, since under this proposal we would already use the same syntax to refer to both bound methods and compound-name members on instances.

To be more concrete, I see no problem with the following:

struct S {
  var f(x:): (Int) -> Void = {}
  func g(x: Int) {}
}

let kpf: KeyPath<S, (Int) -> Void> = \.f(x:)
let kpg: KeyPath<S, (Int) -> Void> = \.g(x:)

In fact, I would insist that we should use the same syntax for both precisely because it mirrors the syntax of a normal instance-member access. Additionally, if we adopt @Dante-Broggi's suggestion of disallowing both var f(x:): (Int) -> Void and func f(x: Int) {} in the same type, then we would never even have to worry about conflicting key paths between unbound methods and compound-name members.

If the above is not an acceptable future state of affairs, then IMO neither is

let f = S().f(x:)
let g = S().g(x:)

and so the Referencing compound names section of this proposal would need to be rethought.

If I've misunderstood your objection, would you mind elaborating a bit?

@cukr I've thought about this a bit more and have a couple of questions. Mainly, I'm concerned that this code completion behavior would place external argument labels as the internal names for the closure, which may not be the right behavior. E.g.,

// Public interface
func getDownloadURL(then initiateRequest(with:): (URL) -> Void)

// Code completion
getDownloadURL { with in
  <#code#>
}

Given that the argument labels in compound names are meant to be used at the call site, its not clear to me that this would result in helpful (internal) parameter names for the closure itself.

Since we don't have any data currently on how this might be used (unless we look back at pre-Swift-3 code...?), maybe it would be better to wait to see how this feature gets used before we start messing with code completion?

This is not correct. The signature of the unbound, uncurried form of g is (S, Int) -> Void. It would not typically be used in a context expecting a key path, but if it were, it would need to be a key path to a static member, i.e. KeyPath<S.Type, (S, Int) -> Void>.

I think you do misunderstand my comments. I do not necessarily have an objection, just something for consideration. I want to make sure we don’t introduce a feature that would make it more difficult to eventually provide access to unbound, uncurried methods (including mutating methods). This has been a frustrating gap in the language for a long time.

@Joe_Groff do you have any thought on whether this pitch would conflict with the eventual use of key path syntax for accessing unbound, uncurried methods?

2 Likes

Good point. Let's not do that yet.

Ah! Thank you for making that clear. IMO, if type context is sufficient to differentiate between the bound method key path \.f(x:) and the unbound, uncurried method key path \.f(x:) then I still don't see much of an issue with overloading the syntax—a user could always differentiate as \S.f(x:) and \S.Type.f(x:) if necessary.

OTOH, if overloading the syntax in this manner turns out to be an issue in practice, I personally would much rather see \.f(x:) be used to refer to instance members and bound methods. Given that we already have the S().f(x:) syntax, it would be very disappointing if \.f(x:) did not refer to a key path to the same method.

If unbound, uncurried key path syntax fits into that story, then great! But if not, I would rather extend the existing syntax to bound methods in the most natural way, and introduce a new "flatten" operator or something that could be used on S.f(x:) to access the unbound, uncurried method.

It is not possible to access a mutating method in curried form so an uncurry operator is not a viable solution. We need new syntax for accessing an unbound, uncurried method.

Sure, I’m not trying to propose actionable concrete alternatives here—just expressing a preference for us to use the key path syntax under discussion for compound-name members and bound methods if it turns out to be in conflict with the unbound/uncurried method usage.

And I’m expressing a preference for having a clear path forward for access to unbound, uncurried methods regardless of what happens with this proposal. I think the proposal will be stronger if it addresses this point.

1 Like

I'm a little confused as to what you're looking for in this "clear path forward." As far as I'm aware, the design space for the syntax these unbound, uncurried method references is basically wide open—the key-path-based '\.f(x:)' syntax is one suggested option, but there's no reason that it couldn't be, say, #unboundUncurried(S.f(x:)), or an unbounded number of alternatives.

It seems out-of-scope for this proposal to explore the design space of a largely unrelated feature that simply happens to share a tentative syntax for one aspect. Especially so when, IMO, the only reasonable way to form a key path to a compound name is as \.f(x:). I don't see the alternatives of "you can't form a key path to a member with a compound name" and "key paths which refer to a compound name use a different syntax than 'normal' members" as tenable options.

I'd be happy to add a paragraph to the KeyPath and compound names section that mentions this potential overlap/conflict, but if you could provide some more concrete suggestions about how you'd like to see this consideration addressed in the proposal I'm all ears!

1 Like

A more broad thought that I'm interested in the community's feedback on: is there any reason why changing a declaration from

func foo(x: Int) {}

to

let foo(x:): (Int) -> Void = {}

should not be an API-compatible change? I can't think of any off the top of my head. Should such a change be ABI-compatible?

"may" or "must"?

This seems to conflict with the later statement "Compound names are not permitted to appear in any position which would result in them being the external label to a function parameter."

I don't believe it is either of these in general, nor can be. The simplest demonstration I can think of is:

struct S {
    let foo: (Int) -> Void
}

let offset = MemoryLayout<S>.offset(of: \S.foo)  // Okay

versus

struct S {
    func foo(_ : Int) {}
}

let offset = MemoryLayout<S>.offset(of: \S.foo)  // Nonsense

I think the same would be true of a class type.

I think "may" is appropriate, since you could also call the variable by referencing the name and immediately applying it, as you can do with methods/functions today:

f(x1:...xn:)(arg1, ..., argn)

Good catch! I had thought that the parameter production was defined as

parameter → external-parameter-name local-parameter-name_opt type-annotation

but turns out it's

parameter → external-parameter-name_opt local-parameter-name type-annotation

so we can exclude the compound name from the external name from the external name in the grammar. Nice!

Even simpler, this proposal promises key paths to members with compound names which don't exist at all for methods (yet...). I think we could (?) promise that the method -> compound member transformation is API-compatible, though I don't know if its worth it to make that a guarantee (even if it ends up being the case in practice).

I wonder—does any promise like that exist today for turning func foo() {} into var foo: () -> Void?

I would like to have an understanding of whether this feature would in fact conflict with use of this syntax for unbound, uncurried methods or not. Hopefully someone from the core team can chime in and the answer can be included in the proposal text for reviewers to consider when evaluating this proposal.

2 Likes