KeyPath and methods

I'd love to have a type safe way to refer to a method, KeyPath seem like the sensible way to do this, but I'm open to suggestions.

A few general use cases:

  • A type safe way to define actions in target+action pairs, which will make UIKit more type safe
  • A nice way to list tests for XCTest on Linux
  • More type safety on DSLs can make it more obvious how they're meant to be used, and reduce error

KeyPath seems like a straightforward way to express what I want:

let myKeyPath: KeyPath<MyObject, (Arg)->Result> = \MyObject.myMethod

However this currently gives you an error:

error: Key path cannot refer to instance method 'myMethod'

Unfortunately even if it did compile it still has a few disadvantages:

  • (Arg)->Result doesn't unambiguously represent a method (it could be a closure property)
  • (Arg)->Result on a value type could require partial application of an inout (unsafe)

I think it could work well with a type like this:

public final class Method <Target, Arguments, Result> {
    public func call(_ target: Target, arguments: Arguments) -> Result
}

A type like this is less flexible than a closure, but it has a few advantages:

  • The type can be extended for convenience
  • There won't be unexpected implicit captures by a closure
  • KeyPath syntax is more concise and readable than closure syntax
  • The nominal type is easier to work out how to construct than the (A)->(B)->C equivalent.

Perhaps if you use this notation on a method:

\MyType.myProperty.myMethod(arg:)

Then you should get a method type instead of a compile time error, or KeyPath<MyType,(MyArg)->MyResult>:

Method<MyType, MyArg, MyResult>

This has a few caveats that I can think of:

  • You would need mutating and throws variants
  • It may not be able to conform to AnyKeyPath as you cannot append to it, or you could return nil from appending(path:)
  • I'm not sure if tuple splatting is a problem, but this is likely an implementation detail

What do you think?

2 Likes

I think you’re conflating KeyPaths to method results with unapplied function references.

We can already make KeyPaths to subscript results, and this comes with some constraints: for example, the KP capture all the arguments and the arguments must be Codable. Only then can you create a KeyPath which refers to the result of calling that subscript with those parameters.

It would be nice to have some kind of KeyPath-like object for referring to unapplied method references, but that would almost certainly require variadic generics.

2 Likes

Sorry, I might have missed the point here.

But isn't this the same thing as what we already have today?

class MyClass {
    func myMethod() {
        print("Hello!")
    }
}

let thatMethod = MyClass.myMethod

let instance = MyClass()
thatMethod(instance)() // Prints "Hello!"

Thanks Rafael, that's a good point that I forgot to clarify.

class MyClass {
    func myMethod(x: Int) -> String { ... }
    static var myStaticProperty: (MyClass) -> (Int) -> String = { ... }
}

let a = MyClass.myMethod
let b = MyClass.myStaticProperty
let c = { (_: MyClass) in { (x: Int) -> String in ... } }

Each of these variables have the same type (a, b, and c), however each has drastically different implications on API usage, capture, type safety, optimisation, etc.

I'd like to have a type I can use to distinguish the MyClass.myMethod case, so it informs how an API should be used, and so it constrains what might be captured in the calling scope.

If you changed the type for the MyClass.myMethod syntax you suggested then you can't do this (without modifying map as well):

[1, 2, 3].map(Int.negate)

The key path syntax is currently incompatible with methods, and I think it is an appropriate place to put what I am suggesting.

Thanks for your feedback!

From a type safety perspective what you're proposing with "unapplied function references" is equivalent of using (MyType) -> Int instead of KeyPath<MyType,Int>.

I want the equivalent type safety of KeyPath but for unapplied function references (perhaps I should have said this).

I don't believe this would require variadic generics, the implementation could benefit from it, but the public interface wouldn't require it.

This works:

func call<Args>(args: Args, block: (Args) -> Void) {
    block(args)
}
func sumAndPrint(a: Int, b: Int) {
    print(a + b)
}
call(args: (10, 20), block: sumAndPrint)

We do have unbound methods currently (the feature @rguerreiro mentioned), but I believe during the keypath proposal review we agreed that this feature should probably be replaced by something more keypath-like.

I think the simplest solution is to extend keypaths to support instance methods as well as properties. Trying to split Arguments and Result into separate generic parameters leaves lots of holes in our support: not only throws, but also inout and @escaping and so on. There's also no particular reason we wouldn't want to form a "method path" to a method on a property of the type, so that existing feature is not out of place.

This won't support mutating methods, but keypaths don't currently support properties with mutating getters either. I could imagine a future version of Swift adding new keypath types to support both.

I could also imagine future versions of Swift allowing methods with parameters to be specified in the keypath chain, like \Array<Person>.first(where: predicate).name. That's a different feature from this one, though.

I think it'd be really useful (if it's not already possible) to pass arguments to an unapplied function reference which would provide a native builder-like pattern.

let builder = \Array<Person>.init(firstName: "John", lastName: "Doe")
...
let person = builder()

I have just written an article on this subject, outlining one possible (but not pretty) solution that works with Swift 4.1

I would be interested to know if you think it is of any relevance

5 Likes