callAsFunction vs @dynamicCallable


Greetings, traveler!

There are many hidden handy features in the Swift language. Today, we will talk about two of them: callAsFunction and @dynamicCallable. They offer pretty much the same functionality but have fundamental differences. Both of them allow us to mark a type as being directly callable.

@dynamicCallable

There are two types of languages: statically typed and dynamically typed. In dynamically typed languages like Python and JavaScript, type-checking occurs at runtime or execution time. Swift is statically typed since its type checking takes place at compile time. So, if you want to bridge two languages together, there can be challenges when one is statically typed, and the other is dynamically typed. And here is when @dynamicCallable comes to the rescue.

By the way, you can also check out another Swift tool — the @dynamicMemberLookup attribute. Here is the article about it.

You can mark a type with the @dynamicCallable attribute and implement these methods:

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

Consider this example. We can create a Calculator. It will store an operation constant and use it with its functions.

enum OperationKind: String {
    case add, subtract, multiply, divide

    func perform(arguments: (Double, Double)) -> Double {
        switch self {
        case .add:
            return arguments.0 + arguments.1
        case .subtract:
            return arguments.0 - arguments.1
        case .multiply:
            return arguments.0 * arguments.1
        case .divide:
            guard arguments.1 != .zero else {
                return .zero
            }
            return arguments.0 / arguments.1
        }
    }
}

struct Calculator {

    private let operation: OperationKind

    init(operation: OperationKind) {
        self.operation = operation
    }

}

Now, let’s mark this type with the @dynamicCallable attribute.

@dynamicCallable
struct Calculator {
    
    private let operation: OperationKind

    init(operation: OperationKind) {
        self.operation = operation
    }

    func dynamicallyCall(withArguments arguments: (Double, Double)) -> Double {
        operation.perform(arguments: arguments)
    }

    func dynamicallyCall(withKeywordArguments pairs: KeyValuePairs<String, (Double, Double)>) {
        for (key, value) in pairs {
            print("\(key): \(operation.perform(arguments: value))")
        }
    }

}

And you can use it like this.

let calc = Calculator(operation: .add)

calc((10.0, 5.0)) // "15"
calc(result: (10.0, 5.0)) // "result: 15"

Also, you can return values with this methods.

func dynamicallyCall(withArguments arguments: (Double, Double)) -> Double {
    operation.perform(arguments: arguments)
}

callAsFunction

This is a more swifty approach for calling an object like a function. You can create a function with that naming. This method can apply custom arguments and return a value. Consider this example.

struct Calculator {
    
    private let operation: OperationKind
    
    init(operation: OperationKind) {
        self.operation = operation
    }
    
}

You can add a callAsFunction method to its body.


struct Calculator {
    
    private let operation: OperationKind
    
    init(operation: OperationKind) {
        self.operation = operation
    }
    
    func callAsFunction(_ arguments: (Double, Double)) {
        print(operation.perform(arguments: arguments))
    }
    
    func callAsFunction(arguments: (Double, Double)) -> Double {
        operation.perform(arguments: arguments)
    }
    
}

let calc = Calculator(operation: .add)

calc((10.0, 5.0))
calc(arguments: (1, 4))

Sure thing, you can use it with generics or even with Any.

struct Calculator {
    
    private let operation: OperationKind
    
    init(operation: OperationKind) {
        self.operation = operation
    }
    
    func callAsFunction<T>(_ arguments: T) {
        guard let arguments = arguments as? (Double, Double) else { return }
        print(operation.perform(arguments: arguments))
    }
    
    func callAsFunction(_ arguments: Any) {
        guard let arguments = arguments as? (Double, Double) else { return }
        print(operation.perform(arguments: arguments))
    }
    
    func callAsFunction(arguments: (Double, Double)) -> Double {
        operation.perform(arguments: arguments)
    }
    
}

SwiftUI

This tool is used in SwiftUI. The OpenURLAction and DismissAction types have a callAsFunction method.

struct CustomView: View {
    @Environment(\.dismiss) private var dismiss
    @Environment(\.openURL) private var openURL
    
    var body: some View {
        VStack {
            Button("Swift blog") {
                openURL(URL(string: "https://www.livsycode.com")!)
            }
            
            Button("Close") {
                dismiss()
            }
        }
    }
}

Conclusion

We considered two interesting tools in Swift, which can be used as syntax sugar to improve the look of your code.