Allow key paths to reference unapplied instance methods

Allow key paths to reference unapplied instance methods

The original key path proposal expressly limited key paths to be able to reference only properties and subscripts:

\Person.name                  // KeyPath<Person, String>
\Person.pets[0]               // KeyPath<Person, Pet>

This proposal adds the ability for key paths to reference instance methods, optionally specifying argument names:

\Person.sing                  // KeyPath<Person, () -> Sound>
\Person.sing(melody:lyrics:)  // KeyPath<Person, (Melody, String) -> Sound>

Note that these key paths do not provide argument values; they reference unapplied methods, and the value they give is a function, not the the value that results from calling the method. (See Future Directions.)

Adding this capability not only removes an inconsistency in Swift, but also solves pratical problems involving map/filter operations, proxying with key path member lookup, and passing weak method references that do not retain their receiver.

Full proposal text: Allow key paths to reference unapplied instance methods

This document is still a work in progress, but is ready for public discussion.

I expect the most controversial aspect of this proposal will not be what it includes, but rather what it leaves out. Some important problems we expressly defer to future proposals. Please see the proposal text for the rationale behind what is in and out of scope.

Thanks to @filip-sakel and @Saklad5 for their contributions to the writing and example code, as well as the others who made helpful editorial suggestions.

57 Likes

This seems like the sort of change that makes the language more uniform and therefore easier to grok.

Thanks for the very well written pitch. I especially appreciate the attention to addressing future directions and anticipating problems that might arise.

2 Likes

Not solving problems from the future directions section was a good decision because itā€˜s still a huge step forward.

4 Likes

This pitch is exemplar. Thank you very much for the long and extensive exploration of the consequences of the proposal. This really helps the reader.

5 Likes

I want it, I want it nooow! :smiley:

2 Likes

The one unresolved TODO in this proposal is the ā€œEffect on ABI stabilityā€ section.

I only understand all things ABI only dimly at best. Do others in the forum have insight into the question?

Joe mentioned that this would "more of less be identical to how read-only subscripts are handled." So this would be nonbreaking to the ABI (or if in the rare case it needs to add to ABI, but I don't believe this requires that. I guess the changes made to the standard library to support this means you need a base Swift version to support this.)

Well, applied method key paths would be more or less identical to subscripts. However, if we're talking about unapplied methods, that's slightly different, since subscript key paths are currently fully applied. If we support unapplied method key paths, we'd presumably want to also support applied method key paths too, as well as function application keypaths to bind arguments for an unapplied key path. That makes things a bit more interesting.

Certainly, agreed ā€” at least in the long term. The proposal tries to make the case that unapplied key paths still add enough utility on their own to justify addition to the language, and Iā€™m leery of taking on the complexity of handling both unapplied and applied at the same time. But I do view this proposal as a stepping stone to handle applied methods as well.

Ah, I hadnā€™t considered that!

Speaking of which: it is even possible with current key paths to make a ā€œmid-chainā€ cut anywhere? For example:

\String.first?.isASCII                     // works
\String.first.appending(path: \?.isASCII)  // error

It seems to me that supporting only applied key paths would be simpler in the short term.

That should work if you spell it \.self?.isASCII.

Perhaps, but in my own experience and testing the waters with others, unapplied is clearly the more-desired feature. One developer remarked, ā€œIā€™d like both, but if I have to choose unboundā€ (meaning unapplied).

Aha! TIL. Thanks!

Edit: Turns out I needed parens too: (\String.first).appending(path: \.self?.isASCII)

2 Likes

I can only speak for myself, but the feature I really want is the ability to use key path syntax to get an unapplied mutating method. Specifically, I want this syntax to solve the problem SE-0042 was going to solve.

1 Like

Yes, see this section.

FWIW, you're the only person in my informal survey who brought this one up ā€” though Iā€™m sure many others will discover itā€™s missing when non-mutating is added. It is a notable gap in the language.

In that section you're asking questions about the KeyPath type. I'm only talking about using key path syntax to access unbound methods as functions. @Joe_Groff mentioned both use cases being important in one of the recent discussions on this topic. I'm just reiterating the use case that I have found to be most important.

I should point out that the need is not only in the case of mutating methods, that just happens to be the case where there is the largest gap. I would enjoy this syntactic sugar for class methods as well.

1 Like

That section does cover both, though perhaps so obliquely it was easy to miss. In fact it directly mentions your idea of adding an inout param for mutating methods:

Key paths for mutating methods might induce an inout param for self:

let appender: (inout Array<Int>, Int) -> Void = \Array.append
appender(primes, 7)

ā€¦and it draws exactly the contrast youā€™re talking about:

ā€¦but that idea only makes sense when using key path literals as a convenience to form closures; it quickly breaks down when using actual key path values as subscripts:

var mersennes = [0, 1, 3]
primes[keyPath: \.append](mersennes, 7)  // Which is mutated: mersennes or primes?

ā€¦arguing that weā€™re better off considering mutating methods as a separate design unit (which Iā€™m all in favor of doing, should this proposal pass):

This line of reasoning suggests that mutating methods are best left for a follow-up proposal:

  • If they are in fact limited to key path literals converted to closures, then they are both outside the scope of this proposal and not in conflict with it.
  • If we seek a broader solution, it would need to be consistent with unbound methods as well, and thus (as in the previous section) this proposal is only uniformly respecting an existing problem, not introducing a new one.

(And just to be crystal clear: I'm all for another proposal to meet the need you're talking about; it seems compelling.)

+1

Fills a need.

(Might want to fix the typos in the pitch though.)

Please flag any you find. (If I knew about them, they would of course already be gone!) You can leave comments directly on the text here.

Very well written proposal :+1:
One thing that I didn't see, even as maybe a future direction it is also support initializers. Have you consider this?
Something like

enum E: Int {
  case a
  case b
}

struct A {
  init() {}
}

\E.init(rawValue:)
\A.init 

I can't think in a pratical case where that would be used, but I thought pointing out maybe can be discussed :))

1 Like

That's a good question. I imagine they wouldn't be supported here, since the proposal doesn't cover other type methods (static and class) since they have a different structure that raises special questions. I've updated the text to reflect that (search for ā€œMethod key paths may not reference type methodsā€).

1 Like

Feedback to this proposal being largely positive, I'd love to take a crack at an implementationā€¦but Iā€™m barely keeping my head above water with remote teaching during a pandemic. Anybody want to have a go at implementation?

1 Like