UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

How to use @dynamicCallable in Swift

Swift 5.0 introduces a new way to work with dynamic languages

Paul Hudson       @twostraws

SE-0216 adds a new @dynamicCallable attribute to Swift, which brings with it the ability to mark a type as being directly callable. It’s syntactic sugar rather than any sort of compiler magic, effectively transforming this code:

let result = random(numberOfZeroes: 3)

Into this:

let result = random.dynamicallyCall(withKeywordArguments: ["numberOfZeroes": 3])

Previously I wrote about a feature in Swift 4.2 called @dynamicMemberLookup. @dynamicCallable is the natural extension of @dynamicMemberLookup, and serves the same purpose: to make it easier for Swift code to work alongside dynamic languages such as Python and JavaScript.

To add this functionality to your own types, you need to add the @dynamicCallable attribute plus one or both of these methods:

func dynamicallyCall(withArguments args: [Int]) -> Double

func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double

The first of those is used when you call the type without parameter labels (e.g. a(b, c)), and the second is used when you do provide labels (e.g. a(b: cat, c: dog)).

@dynamicCallable is really flexible about which data types its methods accept and return, allowing you to benefit from all of Swift’s type safety while still having some wriggle room for advanced usage. So, for the first method (no parameter labels) you can use anything that conforms to ExpressibleByArrayLiteral such as arrays, array slices, and sets, and for the second method (with parameter labels) you can use anything that conforms to ExpressibleByDictionaryLiteral such as dictionaries and key value pairs.

  • Note: If you haven’t used KeyValuePairs before, now would be a good time to learn what they are because they are extremely useful with @dynamicCallable. Find out more here: What are KeyValuePairs?

As well as accepting a variety of inputs, you can also provide multiple overloads for a variety of outputs – one might return a string, one an integer, and so on. As long as Swift is able to resolve which one is used, you can mix and match all you want.

Let’s look at an example. First, here’s a RandomNumberGenerator struct that generates numbers between 0 and a certain maximum, depending on what input was passed in:

struct RandomNumberGenerator {
    func generate(numberOfZeroes: Int) -> Double {
        let maximum = pow(10, Double(numberOfZeroes))
        return Double.random(in: 0...maximum)
    }
}

To switch that over to @dynamicCallable we’d write something like this instead:

@dynamicCallable
struct RandomNumberGenerator {
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double {
        let numberOfZeroes = Double(args.first?.value ?? 0)
        let maximum = pow(10, numberOfZeroes)
        return Double.random(in: 0...maximum)
    }
}

That method can be called with any number of parameters, or perhaps zero, so we read the first value carefully and use nil coalescing to make sure there’s a sensible default.

We can now create an instance of RandomNumberGenerator and call it like a function:

let random = RandomNumberGenerator()
let result = random(numberOfZeroes: 0)

If you had used dynamicallyCall(withArguments:) instead – or at the same time, because you can have them both a single type – then you’d write this:

@dynamicCallable
struct RandomNumberGenerator {
    func dynamicallyCall(withArguments args: [Int]) -> Double {
        let numberOfZeroes = Double(args[0])
        let maximum = pow(10, numberOfZeroes)
        return Double.random(in: 0...maximum)
    }
}

let random = RandomNumberGenerator()
let result = random(0)

There are some important rules to be aware of when using @dynamicCallable:

  • You can apply it to structs, enums, classes, and protocols.
  • If you implement withKeywordArguments: and don’t implement withArguments:, your type can still be called without parameter labels – you’ll just get empty strings for the keys.
  • If your implementations of withKeywordArguments: or withArguments: are marked as throwing, calling the type will also be throwing.
  • You can’t add @dynamicCallable to an extension, only the primary definition of a type.
  • You can still add other methods and properties to your type, and use them as normal.

Perhaps more importantly, there is no support for method resolution, which means we must call the type directly (e.g. random(numberOfZeroes: 5)) rather than calling specific methods on the type (e.g. random.generate(numberOfZeroes: 5)). There is already some discussion on adding the latter using a method signature such as this:

func dynamicallyCallMethod(named: String, withKeywordArguments: KeyValuePairs<String, Int>)

If that became possible in future Swift versions it might open up some very interesting possibilities for test mocking.

In the meantime, @dynamicCallable is not likely to be widely popular, but it is hugely important for a small number of people who want interactivity with Python, JavaScript, and other languages.

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 3.8/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.