It’s often the case that the result of a function is not immediately available, but instead the result will be produced after some delay. To prevent our user interface from pausing and being unresponsive, we typically execute such functions off of the main thread. These functions are written to return the type void, and we get their result by passing in a function to which the result will be passed when it’s ready.

For example:

func getUser(id: UUID, callback: (User) -> ()) {
  // ...
}

The function getUser returns void, but to get its result, we provide the callback function that will be passed the resulting User when it’s ready.

The above function is equivalent to its curried version:

func getUser(id: UUID) -> ((User) -> ()) -> () {
  return { callback in
    // ...
  }
}

Here we change the function from one that requires two arguments to one that takes the first argument and returns a fuction expecting the second. We’d call this second version as follows:

let uuid: UUID = // ...
getUser(id: uuid)({ user in
  print(user)
})

We can go further and create a data structure that wraps the second function:

struct UserContinuation {
  let operation: (@escaping (User) ->()) -> ()
}

Which can then be generalized for a callback for any type which we’ll call Wrapped here:

struct Continuation<Wrapped> {
  let operation: (@escaping (Wrapped) ->()) -> ()
}

We can then refactor the getUser function to use this type:

func getUser(id: UUID) -> Continuation<User> {
  return Continuation { callback in
    // ...
  }
}

Now getUser is written in a way that looks just like synchronous code. We call it, and it returns a User, except that the User is wrapped up in a Continuation. It’s still a User more or less, just like if it produced an optional User instead. When we want the actual User, we must call the operation function and provide a callback that will get passed the actual User.

Enabling Code Reuse

We’ve converted code that requires a callback to synchronous code, but what good is a Continuation<User> to us? We presumably have functions that expect a User but not a Continuation<User> and as a result, we can’t use that code with our Continuation<User>. We need a way to call a function that expects a User, but somehow pass it a Continuation<User>. The map function does just that.

extension Continuation {
  func map<Mapped>(_ transform: @escaping (Wrapped) -> Mapped) -> Continuation<Mapped> {
    return Continuation<Mapped> { callback in
      self.operation { value in
        let transformed = transform(value)
        callback(transformed)
      }
    }
  }
}

This enables us to call any function expecting a User with a Continuation<User>:

func getUsername(from: User) -> String {
  // ...
}

let user: Continuation<User> = // ...
let username: Continuation<String> = user.map(getUsername)

This solves our problem of code reuse but with one major limitation; it only works with functions with single arguments.

Reusing Multi-Argument Functions

What if we had a function that expects two instances of User? We can still reuse such functions, but we must use a different strategy to do so.

First, we must define some custom operators:

infix operator <^>: MultiplicationPrecedence
infix operator <*>: AdditionPrecedence

Next, we’ll use these operators to define some functions.

First, we’ll use <^> to just be the map method with the order reversed so the function is the first argument:

extension Continuation {
  static func <^> <Mapped>(transform: @escaping (Wrapped) -> Mapped, continuation: Continuation) -> Continuation<Mapped> {
    return continuation.map(transform)
  }
}

So user.map(getUsername) is the same as getUsername <^> user. This gets us closer to the regular function calling syntax.

Next we’ll use <*> to define the following function:

extension Continuation {
  static func <*> <Mapped>(transform: Continuation<(Wrapped) -> Mapped>, continuation: Continuation) -> Continuation<Mapped> {
    return Continuation<Mapped> { callback in
      transform.operation { fn in
        continuation.operation { value in
          let transformed = fn(value)
          callback(transformed)
        }
      }
    }
  }
}

Note how this function has almost the exact same signature as <^> except its first parameter is a function that’s also wrapped in a Continuation.

These functions get used together as follows:

func getUsernames(_ user1: User, _ user2: User) -> (String, String) {
  // ...
}

let user1: Continuation<User> = // ...
let user2: Continuation<User> = // ...

let names: Continuation<(String, String)> = curried(getUsername) <^> user1 <*> user2

For this to work, we must have a curried version of getUsername. We can write a function that will do this conversion for us:

func curried<A, B, C>(_ fn: @escaping (A, B) -> C) -> (A) -> (B) -> C {
  return { a in { b in fn(a, b) } }
}

Sometimes, we’d like to pass in regular values that aren’t wrapped in continuations mixed with wrapped values. To do this we need a way to wrap a value for free. The function pure does this for us:

extension Continuation {
  static func pure(_ value: Wrapped) -> Continuation {
    return Continuation { callback in callback(value) }
  }
}

So, now we can reuse our code regardless of the arity of our functions with our Continuation type, but one problem still remains. How do we combine multiple functions that return Continuation instances?

No More Nested Callbacks

Chaining multiple function that produce our Continuation type currently is pretty awkward.

func getUser(id: UUID) -> Continuation<User> {
  // ...
}

func hasActiveAccount(user: User) -> Continuation<Bool> {
  // ...
}

getUser(id: uuid).operation { user in
  hasActiveAccount(user) { isActive in
    // ...
  }
}

We might be tempted to use the map method to solve this:

let isActive = getUser(id: uuid).map(hasActiveAccount)

The problem here is that the type of isActive ends up being Continuation<Continuation<Bool>> where we’d really like it to be just Continuation<Bool>. We need a function like map, but one that also flattens.

extension Continuation {
  func flatMap<Mapped>(_ transform: @escaping (Wrapped) -> Continuation<Mapped>) -> Continuation<Mapped> {
    return Continuation<Mapped> { callback in
      self.operation { value in
        let transformed = transform(value)
        transformed.operation(callback)
      }
    }
  }
}

Now we can write the following code:

let isActive: Continuation<Bool> = getUser(id: uuid).flatMap(hasActiveAccount)

And the type of isActive is Continuation<Bool>. No nested callbacks needed. The Continuation type now handles combining callbacks for us, and we only need to provide a single callback when we want the final result.

A Final Word

These three techniques we’ve discussed here are not my invention. They are based on abstractions from category theory. The map function corresponds to Functors, <*> to Applicative Functors, and flatMap to Monads. The pure function is part of both Applicatives and Monads. There’s more to know about these. There are rules that proper implementations of these abstractions must follow, relationships between them, and limitations on what types can use these techniques. I leave further exploration of these topics to the reader.