Callable Objects and callAsFunction() in Swift
In Swift, any object that has the callAsFunction
method can be called like a function. Such objects are named callable.
The function-call syntax forwards arguments to the corresponding callAsFunction
method [SE-0253]:
struct WannabeFunction {
func callAsFunction() {
print("Hi there")
}
}
let f = WannabeFunction()
f() // Hi there
There are three ways of invoking callAsFunction()
on f
:
let f = WannabeFunction()
f() // Callable sugar
f.callAsFunction() // Member function sugar
WannabeFunction.callAsFunction(f)() // No sugar
The callAsFunction()
method obeys all Swift function rules. It can be declared in an extension:
struct WannabeFunction {}
extension WannabeFunction {
func callAsFunction() { ... }
}
And overloaded:
struct WannabeFunction {
func callAsFunction() { print(#function) }
// Argument label overloading
func callAsFunction(x: Int) { print(#function, x) }
func callAsFunction(y: Int) { print(#function, y) }
// Argument type overloading
func callAsFunction(x: String) { print(#function, x) }
// Generic type constraint overloading
func callAsFunction<T>(value: T) where T: Numeric { print(#function, value) }
func callAsFunction<T>(value: T) where T: StringProtocol { print(#function, value) }
}
let f = WannabeFunction()
f() // callAsFunction()
f(x: 1) // callAsFunction(x:) 1
f(y: 2) // callAsFunction(y:) 2
f(value: 3) // callAsFunction(value:) 3
f(value: "str") // callAsFunction(value:) str
f([1, 2, 3]) // ❌ Error: Type of expression is ambiguous without more context
And passed around:
let foo = f.callAsFunction(y:)
foo(10) // callAsFunction(y:) 10
Does callAsFunction()
bring to Swift anything more than yet another syntactic sugar? Let’s find out by checking the use cases below.
Types representing functions
Types that represent functions, such as mathematical operations or machine learning computations, typically contain a single primary method, which can be conveniently sugared using callable syntax.
A prominent example is TensorFlow. The Layer
protocol, which represents a neural network layer, contains the callAsFunction(_:)
method requirement.
Another example is reducer from the Redux architecture. A motivating example can be found in the Composable Architecture.
Parsers, calculators, shell commands fall into this category.
Passing generic functions
Referencing and passing generic functions would not have been possible without callable syntax. A compiler error pops up if we try to reference a generic function:
func bar<T>(_ x: T) { print(x) }
let f = bar<Int>() // ❌ Error: Cannot explicitly specialize a generic function
However, we can achieve this using callAsFunction()
:
struct Bar<T> {
var f: (T) -> Void
func callAsFunction(_ x: T) { f(x) }
}
let f = Bar<Int> { print($0) }
f(1)
Although f
is factually a callable object, it can be considered a generic function at a point of use.
Function identity
Two functions are considered equal if for each input they produce the same output. Given that some categories are infinite, the only possible way to achieve this is by wrapping functions into a nominal type.
The next use case shows how we can add Identifiable
, Hashable
, and Equatable
conformance to a function by wrapping it into a nominal type. The method callAsFuntion()
allows us to invoke the wrapper using a function-like syntax:
struct Function<Input, Output>: Identifiable {
let id: UUID
let f: (Input) -> Output
init(id: UUID = UUID(), f: @escaping (Input) -> Output) {
self.id = id
self.f = f
}
func callAsFunction(_ input: Input) -> Output {
f(input)
}
}
extension Function: Equatable {
static func == (lhs: Function<Input, Output>, rhs: Function<Input, Output>) -> Bool {
lhs.id == rhs.id
}
}
extension Function: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
Consider Swift key paths as an alternative to the above technique as they already conform to
Hashable
andEquatable
.
Bound functions
Partial application and bound closures can be modeled using callable syntax.
Partial application is a functional programming technique that means binding some arguments to a function without fully evaluating it.
Here is a curried add()
function which is then partially applied:
func add(_ x: Int) -> (_ y: Int) -> Int {
{ y in return x + y }
}
let addTwo = add(2)
print(addTwo(3)) // 5
I am touching on the subject in Functions in Swift.
The same technique can be modeled using callable syntax:
// 1.
struct Sum {
var x: Int
func callAsFunction(_ y: Int) -> Int { x + y }
}
// 2.
let addTwo = Sum(x: 2)
print(addTwo(3)) // 5
Here are the key highlights:
- Declare type
Sum
, which reprents the arithmetic addition operation. - The argument
2
is stored in the struct instance, which essentially bounds2
to the methodcallAsFunction()
.
Delegation
The auto-weak delegate pattern, which I’ve learned from @olegdreyman, uses callAsFunction()
to improve readability at a call site:
class Delegate<Input, Output> {
init() {}
private var block: ((Input) -> Output?)?
func delegate<T: AnyObject>(on target: T, block: ((T, Input) -> Output)?) {
self.block = { [weak target] input in
guard let target = target else { return nil }
return block?(target, input)
}
}
func call(_ input: Input) -> Output? {
return block?(input)
}
func callAsFunction(_ input: Input) -> Output? {
return call(input)
}
}
Say, there is a DataLoader
which fetches data and notifies when it’s done:
class DataLoader {
let onLoad = Delegate<(), Void>()
func loadData() {
onLoad(())
}
}
Then, in the onLoad
callback, we receive weakified self
, and there is no need to write [weak self]
every time:
class ViewController: UIViewController {
let loader = DataLoader()
override func viewDidLoad() {
super.viewDidLoad()
loader.loadData()
loader.onLoad.delegate(on: self) { (self, _) in
self.setupLoadedUI()
}
}
func setupLoadedUI() {}
}
See the original Oleg’s post here for more details on the pattern.
Wrapping Up
Apart from the subjective improvements that callable syntactic sugar brings into the Swift language, callAsFunction()
has two real benefits which are not achievable by any other means. These are:
- passing generic function, and
- functions identity.
I also believe that the types-representing-functions, bound functions, and auto-weak delegation pattern will find their application in many modern codebases.
Thanks for reading!
If you enjoyed this post, be sure to follow me on Twitter to keep up with the new content. There I write daily on iOS development, programming, and Swift.