Function calls in key paths?

Today a key path may only reference properties and subscripts:

\Array<String>.[0].count

But cannot reference method calls:

\Array<String>.[0].lowercased().count
//                 ^
//                 error: Invalid component of Swift key path

Since Swift already supports subscripts, how hard can it be to implement support for method calls in key paths? Sure, subscripts are different from regular methods, but are they different enough to be an implementation problem?

I imagine it could open a lot of new possibilities. The first thing that comes to mind is allowing a type expose methods of other type. Let me clarify.

Today we can do this:

@dynamicMemberLookup
struct Transparent<Source> {

    let source: Source

    subscript<Destination>(dynamicMember keyPath: KeyPath<Source, Destination>) -> Destination {
        return source[keyPath: keyPath]
    }
}

let transparent = Transparent<String>(source: "hello")
print(transparent.capitalized) // prints "HELLO"

This serves the same purpose as the Deref trait in Rust. However, the Deref trait would enable calling the methods of type Source on the Transparent instance, which we can't do right now.

Now, some of you remember discussions about passing methods of classes in point-free style and the problems that it may hide: [Pitch] Introduction of `weak/unowned` closures

tl;dr: If we don't want to strongly capture self when passing a method to a function that takes another function, we can't do it in point-free style, like so:

import Dispatch

class C {

    func doStuffAsynchronously() {
        // doStuf is passed in point-free style here
        DispatchQueue.main.async(execute: doStuff) // self is captured by strong reference here!
    }

    func doStuff() {
        // ...
    }
}

Instead, we have to do this:

DispatchQueue.main.async { [weak self] in self?.doStuff() }

This is a lot less concise and I'm sure seems uglier to many of us.

There were proposals that tried to address this at the language level. Interestingly, allowing function calls in key paths could solve this at the library level in quite a general way. Imagine this little helper:

@dynamicMemberLookup
struct WeaklyCapture<T: AnyObject> {
    private weak var object: T?

    init(_ object: T) {
        self.object = object
    }

    subscript<Destination>(dynamicMember keyPath: KeyPath<T, Destination>) -> Destination? {
        return object?[keyPath: keyPath]
    }

    subscript(dynamicMember keyPath: KeyPath<T, Void>) -> Void {
        object?[keyPath: keyPath]
    }
}

Now we can use point-free style to pass methods around, and it reads nice:

DispatchQueue.main.async(execute: WeaklyCapture(self).doStuff)

The same could be done for unowned. What's good about this is that we're not limited to capturing only self, we can capture any variable with this solution.

I remember someone from the core team mentioning that function calls in key paths is something that we want to eventually support. What implementation challenges can one encounter should they try to implement this in the compiler and the standard library?

11 Likes

It should be straightforward to add support for functions, since in most respects they're isomorphic to get-only subscripts. You should be able to reuse most of the same implementation paths currently used for subscript key paths to capture the function arguments, and synthesize an identifier to use for function equality.

10 Likes