Difference between callAsFunction and @dynamicCallable in Swift

Jul 23, 2023#swift#comparison

The main difference between callAsFunction and @dynamicCallable is that the former is more static and type-safe, while the latter is more dynamic and flexible. The primary use case for @dynamicCallable is to interoperate with dynamic languages like Python and JavaScript, while callAsFunction can be used to create types that behave like functions or closures.

callAsFunction

Special method callAsFunction allows you to call an object like a function. For example, if you have a struct that defines a callAsFunction method, you can create an instance of that struct and call it with parentheses:

struct Adder {
    func callAsFunction(_ x: Int, _ y: Int) -> Int {
        return x + y
    }
}

let add = Adder()
let sum = add(3, 4) // 7

You can also pass arguments and return values to the callAsFunction method, just like a normal function. This can be useful for creating custom operators, wrappers, or DSLs.

A type can define multiple callAsFunction methods:

struct Transformer {
    func callAsFunction(_ input: String) -> String {
        return input.uppercased()
    }

    func callAsFunction(_ input: Int) -> Int {
        return input * 2
    }

    func callAsFunction(_ input: Bool) -> Bool {
        return !input
    }
}

let transformer = Transformer()
let result1 = transformer("hello") // equivalent to transformer.callAsFunction("hello")
print(result1) // HELLO
let result2 = transformer(10) // equivalent to transformer.callAsFunction(10)
print(result2) // 20
let result3 = transformer(true) // equivalent to transformer.callAsFunction(true)
print(result3) // false

Some of the environment values in SwiftUI are instances of types that define a callAsFunction method, which allows you to call them like functions using a simple syntactic sugar. For example, the OpenURLAction and DismissAction types have a callAsFunction method that opens a URL or dismisses a view, respectively.

struct ModalView: View {
    @Environment(\.dismiss) private var dismiss
    @Environment(\.openURL) private var openURL

    var body: some View {
        VStack {
            Button("Get Help") {
                if let url = URL(string: "https://www.example.com") {
                    openURL(url) // Implicitly calls openURL.callAsFunction(url)
                }
            }
            Button("Dismiss") {
                dismiss() // Implicitly calls dismiss.callAsFunction()
            }
        }
    }
}

@dynamicCallable

Attribute @dynamicCallable lets you call a type like a function, using a simple syntactic sugar. It is mainly used for dynamic language interoperability, such as calling Python or JavaScript functions from Swift.

You mark a type with @dynamicCallable attribute and implement one or both of these methods:

func dynamicallyCall(withArguments: <#Arguments#>) -> <#R1#>
func dynamicallyCall(withKeywordArguments: <#KeywordArguments#>) -> <#R2#>

These methods are called when the type is used as a function with positional or keyword arguments respectively.

A type that can be called with unnamed arguments:

@dynamicCallable
struct Sum {
    func dynamicallyCall(withArguments args: [Int]) -> Int {
      return args.reduce(0, +)
    }
}

let sum = Sum()
let result1 = sum(1, 2, 3, 4) 
// equivalent to sum.dynamicallyCall(withArguments: [1, 2, 3, 4])
print(result1) // 10

A type that can be called with named arguments:

@dynamicCallable
struct Greeting {
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, String>) -> String {
        var message = "Hello"
        for (name, value) in args {
            message += ", \(name): \(value)"
        }
        return message
    }
}

let greeting = Greeting()
let result2 = greeting(name: "Alice", age: "25") 
// equivalent to greeting.dynamicallyCall(withKeywordArguments: ["name": "Alice", "age": "25"])
print(result2) // Hello, name: Alice, age: 25

A type that can be called with either named or unnamed arguments:

@dynamicCallable
struct Calculator {
    func dynamicallyCall(withArguments args: [Double]) -> Double {
        return args[0] * args[1]
    }

    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Double>) -> Double {
        return args.first?.value ?? 0
    }
}

let calculator = Calculator()
let result3 = calculator(2.0, 3.0) 
// equivalent to calculator.dynamicallyCall(withArguments: [2.0, 3.0])
print(result3) // 6.0

let result4 = calculator(answer: 42.0) 
// equivalent to calculator.dynamicallyCall(withKeywordArguments: ["answer": 42.0])
print(result4) // 42.0