Give your simulator superpowers

RocketSim: An Essential Developer Tool
as recommended by Apple

@dynamicCallable in Swift explained with code examples

It’s all in the name: @dynamicCallable in Swift allows you to dynamically call methods using an alternative syntax. While it’s primarily syntactic sugar, it can be good to know why it exists and how it can be used.

We covered @dynamicMemberLookup earlier, allowing us to express member lookup rules in dynamic languages naturally. @dynamicCallable is another way to provide hooks into Swift and optimize the syntax written in other languages to communicate with Swift.

Why does Swift provide dynamic interpolation?

While most of us solely work in Swift code, other applications require communication between different languages. Swift naturally supports communicating with C and Objective-C APIs but couldn’t interpolate with other languages before @dynamicCallable and @dynamicMemberLookup were introduced.

Interoperability with other languages is essential for Swift to become more flexible. Server-side development and machine learning communities can benefit from Swift as a language by integrating dynamically.

What is @dynamicCallable used for?

You can use @dynamicCallable to provide dynamic access to your code from within Python, Javascript, or other languages.

For example, you could define a cache layer that can be used from within any languages by using the dynamic callable syntax:

let stored = cache.dynamicallyCall(withKeywordArguments: [
    "store": "Antoine"
])

Note that we’re using Swift to call into dynamic callable methods to demonstrate the purpose. You should imagine any language interpolating in the same way as described in my code examples.

The above code example can be replaced using a more readable variant:

let stored = cache(store: "Antoine")

As you can see, we’ve written self-explanatory code telling us that we’re storing the name “Antoine” inside the cache. You can see cache(store: "Antoine") as a syntactic alternative to cache.dynamicallyCall(withKeywordArguments: ["store": "Antoine"]).

You’ll understand this even better by looking at the following example:

/// The following line:
cache3(contains: "Antoine")

/// Is the same as:
cache3.dynamicallyCall(withKeywordArguments: [
    "contains": "Antoine"
])

Stay updated with the latest in Swift

The 2nd largest newsletter in the Apple development community with 18,598 developers. Don't miss out – Join today:


How to implement @dynamicCallable

Now that we know how @dynamicCallable looks from the implementation side, it’s time to look at the actual code to build this caching example.

Providing access using keyword arguments

We’ll use keyword arguments to have the most readable variant of dynamic callable methods:

@dynamicCallable
final class NamesCache {
    private var names: [String] = []

    @discardableResult
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, String>) -> Bool {
        for (key, value) in args {
            if key == "contains" {
                return names.contains(value)
            } else if key == "store" {
                names.append(value)
                return true
            }
        }
        return false
    }
}

let cache = NamesCache()
cache(contains: "Antoine") // Prints: false
cache(store: "Antoine") // Prints: true
cache(contains: "Antoine") // Prints: true

Our names cache becomes dynamically callable by adding the @dynamicCallable attribute. We can find the caller’s purpose by iterating over the key-value pairs and providing a boolean result accordingly. In this case, we return false if we can’t see the proper definition.

Using array arguments

Not all languages support keyword arguments, so Swift provides an alternative by using array arguments:

@dynamicCallable
final class NamesCache {
    private var names: [String] = []

    @discardableResult
    func dynamicallyCall(withArguments args: [String]) -> Bool {
        let pairs = stride(from: 0, to: args.endIndex, by: 2).map { argumentIndex in
            let lhsArgument = args[argumentIndex]
            let rhsArgument = argumentIndex < args.index(before: args.endIndex) ? args[argumentIndex.advanced(by: 1)] : nil
            return (lhsArgument, rhsArgument)
        }

        for (key, value) in pairs {
            guard let value else { continue }

            if key == "contains" {
                return names.contains(value)
            } else if key == "store" {
                names.append(value)
                return true
            }
        }
        return false
    }
}

let cache = NamesCache1()
cache("store", "Antoine") // Prints: true

We have to do a little more work to parse the arguments, but the final result equals what we’ve had before using key-value arguments.

Combining @dynamicCallable with @dynamicMemberLookup

Finally, I’d like to show you an example of combining dynamic callable and member lookup. If you’re new to @dynamicMemberLookup, I encourage you to read Dynamic Member Lookup combined with key paths in Swift.

In this case, we provide access to the underlying array that is used for storing the names:

@dynamicMemberLookup
@dynamicCallable
final class NamesCache {
    private var names: [String] = []

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

    @discardableResult
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, String>) -> Bool {
        for (key, value) in args {
            if key == "contains" {
                return names.contains(value)
            } else if key == "store" {
                names.append(value)
                return true
            }
        }
        return false
    }
}

By adding the subscript using the String array type we can now access underlying information about the stored names:

let cache = NamesCache()
cache(contains: "Maaike") // Prints: false
cache(store: "Maaike") // Prints: true
cache(contains: "Maaike") // Prints: true

cache.count // Prints: 1
cache.description // Prints: ["Maaike"]

Altogether, it provides you insights into providing access from other languages using both dynamic attributes.

Conclusion

Providing access to Swift code from other languages is essential for Swift to become more widely adopted. Both @dynamicCallable and @dynamicMemberLookup methods give us the tools to make our code accessible from languages like Python and Javascript. While many of us won’t need to implement this technique, it’s crucial for others.

If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.

Thanks!