Why can’t key paths refer to instance methods?

In most parts of Swift, instance methods are treated like properties. However, if you try to reference an instance method using a key path, you get a compiler error. Why is this the case? I assume there is a reason, but I cannot find it. It would be extremely useful in all the same places that other key paths are.

Note that I am not asking about calling instance methods inside a key path. I want to know why this works:

let instance = 100
let method = instance.isMultiple

method(10) // true
method(11) // false

while this doesn’t:

let instance = 100
let method = instance[keyPath: \.isMultiple] // Key path cannot refer to instance method 'isMultiple(of:)'

method(10)
method(11)
21 Likes

I was wondering about that too. I firmly believe that key paths referring to instance methods would be a really powerful feature and seems only natural to be added to the language. I thought that the following would work normally:

struct Dog: Animal {
    var age: Int
    
    func greet() { /*...*/ }
    
    func bark() { /*...*/ }
}

@dynamicMemberLookup
struct AnimalWrapper<Wrapped: Animal> {
    var wrapped: Wrapped
    
    subscript<Value>(dynamicMember keyPath: KeyPath<Wrapped, Value>) -> Value {
        /* ... */
    }
}

let dog = Dog(age: 13)
let animal = AnimalWrapper(wrapped: dog)

animal.age // Int
animal.greet // () -> ()

This doesn't work because keyPaths can't reference instance methods, but getting the bark method as a closure is allowed:

let bark = Dog(age: 5).bark // () -> ()
4 Likes

That’s precisely the sort of thing I want to do. Dynamic member lookup with key paths would be perfect for wrapper types, were it not for this limitation.

3 Likes

If you assign an instance method to a property, that property can be referenced through key paths. I know that a function’s type does not include parameter labels: maybe it’s that sort of label erasure that requires special support?

It‘s simple, no one has implemented it yet. ;)

5 Likes

I'm not at all familiar with compiler architecture and the process of contribution. Do you happen to know what would need to be done in order to implement this feature?

I'm not a compiler developer either. Therefore I have no real clue.

I guess you could start in this file and try to understand the important pieces. swift/KeyPath.swift at main · apple/swift · GitHub

I found the precise limitations.

This snippet explains the limitation of dynamic member lookup, not KeyPath. This is not an explanation of why KeyPaths don't supports method, but a consequence.

They are the limitations, though. I’m not saying they’re the source of them.

There's no fundamental reason KeyPaths can't support instance methods. It would be a reasonable thing to add to the language. The implementation would be more or less identical to how read-only subscripts are handled.

14 Likes

What about static members? Is that just because of ambiguity?

I haven’t contributed to Swift before, but I might look into doing so for methods. With a formal pitch, of course.

2 Likes

Static members could be made to work too. I think @beccadax had taken a stab at it a while back, but I don't recall what limits he ran into.

4 Likes

This appears to be the stab in question: GitHub - beccadax/swift at i-am-the-keymaster

Judging by the last commit on the branch, his implementation wasn’t compatible with Swift 5.0 (the contemporary version). It worked on the Swift 5.1 beta, though.

2 Likes

This would be quite fantastic to get going, and we've been dreaming up some use-cases for this a while ago.

What I would be most interested in is ensuring that it is also possible to capture arguments in a KeyPath (slowly leading towards not only wrapping properties, but any function really via a kind of "function wrapper" which would kind of look the same -- get a KeyPath representing the invocation)...

1 Like

Assuming you mean argument labels, that seems way out of scope for this. I agree that it would be helpful, but that isn’t actually part of a function’s type right now.

Key paths to methods would handle arguments the same way subscript key paths handle indexes, by capturing them into the KeyPath object.

4 Likes

I still hope that we could somehow workaround the requirement of every parameter being hashable for key-path methods.

2 Likes

This one is on my wish list, because Siesta has a compelling use case for it. A common mistake when using Siesta is to do this:

resource.addObserver(owner: self, observer: self.fooChanged)

This innocent code creates a retain cycle under Siesta’s memory rules. No need to understand the context or fiddly details here; the fundamental problem is that self.fooChanged retains self, and the way Siesta works, we don’t want that.

Folks have suggested weak closures and/or weak method references (e.g. here, here), but another alternative that requires no new syntax and little new discussion — just the proposal at hand here — would be to alter Siesta’s observer API to use a keypath:

resource.addObserver(owner: self, observer: \.fooChanged)  // keypath doesn’t retain self!

Since the idea being pitched in this thread sounds feasible, I’d be happy to help write it up as a formal proposal.

4 Likes

Did you see @Joe_Groff’s comment:

If methods in key paths work like read-only subscripts, all arguments would need to be bound. Only self Is not bound. I looked at the siesta docs and it looks like the callback accepts two arguments that must be provided by the caller and neither appear to be the observer’s self which is what you would expect to pass to a key path.

It would be great if we could extend the read only subscript model somehow to support unbound methods where parameters are unbound in addition to self, or perhaps even allowing arbitrary “holes” in the path (including subscript parameters). This works with key path syntax. It might be possible to make it work with the KeyPath type as well by making Root a tuple, but I’m not sure that’s a good idea.