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"
])
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!