ContT

API Documentation: ContT

A pattern that appears sometimes in functional programming is that of a function first computing some kind of intermediate result and then passing that result to another function which was passed in as an argument, in order to delegate the computation of the final result.

For example:

case class User(id: Int, name: String, age: Int)
sealed abstract class UserUpdateResult
case class Succeeded(updatedUserId: Int) extends UserUpdateResult
case object Failed extends UserUpdateResult
import cats.Eval

def updateUser(persistToDatabase: User => Eval[UserUpdateResult])
              (existingUser: User, newName: String, newAge: Int): Eval[UserUpdateResult] = {
  val trimmedName = newName.trim
  val cappedAge = newAge max 150
  val updatedUser = existingUser.copy(name = trimmedName, age = cappedAge)

  persistToDatabase(updatedUser)
}

(Note: We will be using Eval throughout the examples on this page. If you are not familiar with Eval, it's worth reading the Eval documentation first.)

Our updateUser function takes in an existing user and some updates to perform. It sanitises the inputs and updates the user model, but it delegates the database update to another function which is passed in as an argument.

This pattern is known as "continuation passing style" or CPS, and the function passed in (persistToDatabase) is known as a "continuation".

Note the following characteristics:

In Cats we can encode this pattern using the ContT data type:

import cats.data.ContT

def updateUserCont(existingUser: User,
                   newName: String,
                   newAge: Int): ContT[Eval, UserUpdateResult, User] =
  ContT.apply[Eval, UserUpdateResult, User] { next =>
    val trimmedName = newName.trim
    val cappedAge = newAge min 150
    val updatedUser = existingUser.copy(name = trimmedName, age = cappedAge)

    next(updatedUser)
  }

We can construct a computation as follows:

val existingUser = User(100, "Alice", 42)
// existingUser: User = User(id = 100, name = "Alice", age = 42)

val computation = updateUserCont(existingUser, "Bob", 200)
// computation: ContT[Eval, UserUpdateResult, User] = FromFn(
//   runAndThen = Single(f = <function1>, index = 0)
// )

And then call run on it, passing in a function of type User => Eval[UserUpdateResult] as the continuation:

val eval = computation.run { user =>
  Eval.later {
    println(s"Persisting updated user to the DB: $user")
    Succeeded(user.id)
  }
}
// eval: Eval[UserUpdateResult] = cats.Later@58931553

Finally we can run the resulting Eval to actually execute the computation:

eval.value
// Persisting updated user to the DB: User(100,Bob,150)
// res0: UserUpdateResult = Succeeded(updatedUserId = 100)

Composition

You might be wondering what the point of all this was, as the function that uses ContT seems to achieve the same thing as the original function, just encoded in a slightly different way.

The point is that ContT is a monad, so by rewriting our function into a ContT we gain composibility for free.

For example we can map over a ContT:

val anotherComputation = computation.map { user =>
  Map(
    "id" -> user.id.toString,
    "name" -> user.name,
    "age" -> user.age.toString
  )
}
// anotherComputation: ContT[Eval, UserUpdateResult, Map[String, String]] = FromFn(
//   runAndThen = Single(
//     f = cats.data.ContT$$Lambda$11619/0x00007f89baf735b8@6526eb27,
//     index = 0
//   )
// )

val anotherEval = anotherComputation.run { userFields =>
  Eval.later {
    println(s"Persisting these fields to the DB: $userFields")
    Succeeded(userFields("id").toInt)
  }
}
// anotherEval: Eval[UserUpdateResult] = cats.Eval$$anon$5@689e39e0

anotherEval.value
// Persisting these fields to the DB: Map(id -> 100, name -> Bob, age -> 150)
// res1: UserUpdateResult = Succeeded(updatedUserId = 100)

And we can use flatMap to chain multiple ContTs together.

The following example builds 3 computations: one to sanitise the inputs and update the user model, one to persist the updated user to the database, and one to publish a message saying the user was updated. It then chains them together in continuation-passing style using flatMap and runs the whole computation.

val updateUserModel: ContT[Eval, UserUpdateResult, User] =
  updateUserCont(existingUser, "Bob", 200).map { updatedUser =>
    println("Updated user model")
    updatedUser
  }
// updateUserModel: ContT[Eval, UserUpdateResult, User] = FromFn(
//   runAndThen = Single(
//     f = cats.data.ContT$$Lambda$11619/0x00007f89baf735b8@1c78b9f0,
//     index = 0
//   )
// )

val persistToDb: User => ContT[Eval, UserUpdateResult, UserUpdateResult] = {
  user =>
    ContT.apply[Eval, UserUpdateResult, UserUpdateResult] { next =>
      println(s"Persisting updated user to the DB: $user")

      next(Succeeded(user.id))
    }
}
// persistToDb: User => ContT[Eval, UserUpdateResult, UserUpdateResult] = <function1>

val publishEvent: UserUpdateResult => ContT[Eval, UserUpdateResult, UserUpdateResult] = {
  userUpdateResult =>
    ContT.apply[Eval, UserUpdateResult, UserUpdateResult] { next =>
      userUpdateResult match {
        case Succeeded(userId) =>
          println(s"Publishing 'user updated' event for user ID $userId")
        case Failed =>
          println("Not publishing 'user updated' event because update failed")
      }

      next(userUpdateResult)
    }
}
// publishEvent: UserUpdateResult => ContT[Eval, UserUpdateResult, UserUpdateResult] = <function1>

val chainOfContinuations =
  updateUserModel flatMap persistToDb flatMap publishEvent
// chainOfContinuations: ContT[Eval, UserUpdateResult, UserUpdateResult] = FromFn(
//   runAndThen = Single(
//     f = cats.data.ContT$$Lambda$11623/0x00007f89baf76000@16d7d169,
//     index = 0
//   )
// )

val eval = chainOfContinuations.run { finalResult =>
  Eval.later {
    println("Finished!")
    finalResult
  }
}
// eval: Eval[UserUpdateResult] = cats.Eval$$anon$5@51e672e1

eval.value
// Updated user model
// Persisting updated user to the DB: User(100,Bob,150)
// Publishing 'user updated' event for user ID 100
// Finished!
// res2: UserUpdateResult = Succeeded(updatedUserId = 100)

Why Eval?

If you're wondering why we used Eval in our examples above, it's because the Monad instance for ContT[M[_], A, B] requires an instance of cats.Defer for M[_]. This is an implementation detail - it's needed in order to preserve stack safety.

In a real-world application, you're more likely to be using something like cats-effect IO, which has a Defer instance.