Dynamic Member Lookup combined with key paths in Swift

Dynamic member lookup has been introduced in Swift 4.2 with SE-195 and already allowed some pretty nice solutions to for example implement JSON parsing. Just like the @propertyWrapper keyword as discussed in last weeks blog post, it can be used with a @ keyword: @dynamicMemberLookup.

As quoted from the proposal, dynamic member lookup has been added for the following reason:

The driving motivation for this feature is to improve interoperability with inherently dynamic languages like Python, Javascript, Ruby and others.

Although this might not directly feel like a feature handy to use in your projects, it might become more handy with the support of key paths. This new feature has been introduced in Swift 5.1 with SE-252.

Adding dynamic member lookup support using @dynamicMemberLookup

@dynamicMemberLookup can be used with both classes and structs. As soon as you add the key above your type definition you will see the following error:

@dynamicMemberLookup attribute requires ‘NotificationWrapper’ to have a ‘subscript(dynamicMember:)’ method that accepts either ‘ExpressibleByStringLiteral’ or a keypath

Since Swift 5.1 this error message now also includes “or a keypath”.

We can solve this error easily by implementing the subscript as suggested. In the following example, we have a `NotificationWrapper` to use for accessing the user info of a notification.

@dynamicMemberLookup
struct NotificationWrapper {
    let notification: Notification

    subscript(dynamicMember string: String) -> String {
        return notification.userInfo?[string] as? String ?? ""
    }
}

The dynamic member lookup will allow us to access any String values from the user info as following:

let notificationWrapper = NotificationWrapper(notification: Notification(name: Notification.Name(rawValue: "user_did_login"), object: nil, userInfo: ["name": "Antoine van der Lee", "age": 28]))
print(notificationWrapper.name)

However, if we want to access the age integer using notificationWrapper.age, we would end up with an empty string. Although you might think that we can easily solve this using an extra subscript taking an integer type, it’s instead ending up with the following error:

error: ambiguous use of ‘subscript(dynamicMember:)’

This is where key paths come to the rescue.

Combining key paths with @dynamicMemberLookup

Swift 5.1 introducing key paths suddenly makes dynamic member lookup support a lot more interesting. It allows us to easily access values from an instance while keeping our models structured and small. The following example visualizes this using a blogger with a blog instance.

struct Blog {
    let title: String
    let url: URL
}

@dynamicMemberLookup
struct Blogger {
    let name: String
    let blog: Blog

    subscript<T>(dynamicMember keyPath: KeyPath<Blog, T>) -> T {
        return blog[keyPath: keyPath]
    }
}

We use a generic subscript which takes a key path for the type Blog. The key path member lookup support in Swift automatically allows us now to access properties from the blog instance directly on a blogger instance.

let blog = Blog(title: "SwiftLee", url: URL(string: "www.avanderlee.com")!)
let blogger = Blogger(name: "Antoine van der Lee", blog: blog)

print(blogger.title) // Prints: SwiftLee

Keeping structured code while having quick access to properties

This implementation exposes all the properties from the blog instance on accessing the blogger instance. This makes those properties easier to discover.

Autocompletion on a type supported dynamic member lookup.

Autocompletion on a type supported dynamic member lookup.

Should we now start implementing this throughout our projects?

Although the above example does make properties easy accessible, it doesn’t change a lot. We could easily access the same property using blogger.blog.title which you could even take as more readable. On top of that, you might expect the blogger instance to have a title property although it actually only has a name and blog property. As this all could even lead to a less readable codebase it might be better to only use it in very specific cases.

Conclusion

Although it’s good to know that it exists and how it works, at this moment I don’t think it will be used quite often. As the first quote points out, this was likely mainly added for interoperability with inherently dynamic languages and not really for general usage throughout Swift project.

Obviously, if you think different here, let me know!

Thanks!