How and when to use callAsFunction in Swift 5.2

Published on: February 17, 2020

A new Swift 5.2 feature is the ability to call instances of types as functions. Or, as the Swift Evolution proposal calls it "Callable values of user-defined nominal types". The very short description of this feature is that it allows you to call instances of any type that has a callAsFunction method implemented as if it's a function:

struct InvestmentsCalculator {
  let input: Double
  let averageGrowthPerYear = 0.07

  func callAsFunction(years: Int) -> Double {
    return (0..<years).reduce(input, { value, _ in
      return value * (1 + averageGrowthPerYear)
    })
  }
}

let calculator = InvestmentsCalculator(input: 1000)
let newValue = calculator(years: 10)

While this is pretty cool, you might wonder when a feature like this is useful in the real world, and how you can apply it in your code. Luckily, that's exactly what I hope to help you discover in today's post, and what I've been trying to find out myself.

Understanding the background of callAsFunction

Swift isn't the only language to allow its users to call instances of certain types as functions. A language that I have used a lot that allows this kind of behavior is Python. The ability to invoke instances as functions is very interesting in certain applications where you want to represent a stateful calculator, parser or computing object. This is very common in complex math operations and machine learning where certain objects might hold on to some state and only implement a single method.

An application that I am more familiar with myself, is to pass a renderer object to a function in Python. I won't bother you with Python code in this post, but I do want to show you what I mean by my previous statement. So let's look at a Swift example:

protocol Route {
  associatedtype Output
}

func registerHandler<R: Route>(_ route: R, _ handler: (R) -> R.Output) {
  return renderer(route)
}

Upon first glance, this shouldn't look too crazy. It's a function that accepts an object that conforms to Route, and a closure that takes this Route as its argument, and returns R.Output, whatever that may be. If you were to call this function, you might write the following:

registerHandler(homeRoute) { route in
  /* Do a bunch of work to create output */

  return output
}

This might work fine for very simple renderers. However, in the context of Python I mentioned before, this kind of code would be running on a web server. And a route would be the equivalent of a URL or path to a webpage. The closure passed to registerHandler is called whenever the associated route is requested by the user. In many cases, a simple closure wouldn't do. The object that ends up handling the route would need to have a database connection, a concept of caching, authenticating and possibly a lot of other functionality. Capturing all of that in a closure just doesn't seem like a great idea. Instead, you'd want some kind of complex object to handle this route. And that's exactly the kind of freedom we get with callAsFunction:

struct HomeHandler {
  let database: Database
  let authenticator: Authenticator

  // etc...

  func callAsFunction<R: Route>(_ route: R) -> R.Output {
    /* Do a bunch of work to create output */

    return output
  }
}

let homeHander = HomeHandler(database: database, authenticator: authenticator)

registerHandler(for: homeRoute, homeHandler)

Patterns like this are quite common in Python, and in my opinion, it's pretty cool to be able to transparently pass entire objects to functions take closures is really neat.

If you know a thing or two about closures, you might be wondering if there's any difference between the previous code, and the following:

registerHandler(for: homeRoute, homeHandler.callAsFunction)

And there really isn't much of a difference other than the amount of typing you need to do. Functions can still be passed to functions that take closures just fine. It just reads a little bit nicer to not have to specify what function on an object needs to be called exactly.

Understanding how to use callAsFunction

Using callAsFunction in Swift is relatively straightforward. Any object that defines a callAsFunction method can be treated as a function. Your callAsFunction can take arguments and return values as shown in the Swift Evolution proposal with the following example:

struct Adder {
  let base: Int

  func callAsFunction(_ x: Int) -> Int {
    return base + x
  }
}

let add3 = Adder(base: 3)
add3(10) // 13

Or it can take no arguments at all:

struct Randomizer<T> {
  let elements: [T]

  func callAsFunction() -> T? {
    return elements.randomElement()
  }
}

let randomizer = Randomizer(elements: [1, 2, 3])
randomizer() // a random element

And you can even have multiple overloads on a single object:

struct Randomizer<T> {
  let elements: [T]

  func callAsFunction() -> T? {
    return elements.randomElement()
  }

  func callAsFunction(resultCount: Int) -> [T] {
    return (0..<resultCount).reduce(into: [T]()) { result, _ in
      if let element = elements.randomElement() {
        result.append(element)
      }
    }
  }
}

let randomizer = Randomizer(elements: [1, 2, 3])
randomizer() // a random element
randomizer(resultCount: 2) // an array with two random elements

The ability to decide whether you want your callAsFunction implementation to take arguments and what its return type is, makes it a pretty powerful feature. You can really customize this feature to your heart's content and because you can add multiple overloads of callAsFunction to your objects, you can use a single object as a function in many contexts.

In Summary

The ability to make your types callable might not be something that you're likely to use a lot. It's a niche feature that has a couple of very specific, yet convenient, applications. In today's post, you saw an example of how Python uses callables in a framework that I've worked with a couple of years ago to build websites. I roughly translated the ability to register route handlers using objects to Swift to give you an idea of what a practical application could look like and give you a little bit of background from a new perspective.

I then proceeded to show you how callAsFunction can be used on your own types. You saw that it's totally up to you to decide whether you want your implementation to take arguments and that you get to pick the return type of callAsFunction yourself. You also learned that it's possible to specify your overloads for callAsFunction which means that you can have multiple flavors of callAsFunction on a single type.

If you have any questions about this post, or if you have feedback for me, don't hesitate to reach out to me on Twitter.

Categories

Swift

Subscribe to my newsletter