KeyPath Property Chaining

Hello! I have an idea that I have been pondering for a while. Swift KeyPaths have various appending methods, which allow us to chain them together. However, in my mind, the most natural way to do this is with a property access on the KeyPath which returns a new KeyPath with the property chained on, something like:

let path = \Double.description
let newPath = path.capitalized

This could be implemented by marking KeyPath with @dynamicMemberLookup and giving it the following subscript:

extension KeyPath {
    subscript<T>(dynamicMember dynamicMember: KeyPath<Value, T>) -> KeyPath<Root, T> {
        return appending(path: dynamicMember)
    }
}

This approach complements the existing KeyPath literal syntax, as substituting path in the example above in the last line results in let newPath = \Double.description.capitalized, which is exactly what it is.
What do you think? This may also work with the Partial and Any KeyPaths, but I haven't given it much thought.

3 Likes

This is a cool bit of syntactic sugar, but I'd be worried about conflicts between members of the value type and members of the KeyPath type itself, like we had with Optional.none. I also feel like the times I've needed to append key paths, the path I was appending was usually a variable, not a known key path. Can you share an example where this would make your existing code cleaner?

1 Like

Thanks for the response! The main idea that got me thinking about this was the composition of KeyPath-based APIs. For example, let's say I am making a MediaList type, which uses SwiftUI's List. I want to be able to have some common media metadata that I can use. Following the lead of List, it can be initialized with some protocol that is not relevant to the discussion, or with a KeyPath for the metadata:

struct MediaMetadata {...}
struct MediaList<Data>: View {
    var metadataPath: KeyPath<Data, MediaMetadata>
    var mediaItems: [Data]
    var body: some View { List(...) }
    init(_ items: [Data], metadata: KeyPath<Data, MediaMetadata>) {...}
}

If the items are not identifiable, then I will have to write this for the list:

List(mediaItems, id: metadataPath.appending(path: \.mediaID) {...}

However, with this new way of creating chained KeyPaths, I can write this:

List(mediaItems, id: metadataPath.mediaID) {...}

It is easier to understand what is happening (for me at least), and this applies generally whenever you are layering KeyPath APIs.

1 Like

I see. I'm not sure I personally think that is clearer, but I couldn't picture the use case before and now I understand, so thank you!