Skip to content

Commit

Permalink
Write documentation for ContT (#2677)
Browse files Browse the repository at this point in the history
  • Loading branch information
cb372 authored and Luka Jacobowitz committed Jan 22, 2019
1 parent 9e617d8 commit 9bed9b5
Showing 1 changed file with 186 additions and 0 deletions.
186 changes: 186 additions & 0 deletions docs/src/main/tut/datatypes/contt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
---
layout: docs
title: "ContT"
section: "data"
source: "core/src/main/scala/cats/data/ContT.scala"
scaladoc: "#cats.data.ContT"
---
# 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:

```tut:silent
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
```

```tut:book
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](eval.html) 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:

* The return type of our `updateUser` function (`Eval[UserUpdateResult]`) is the
same as the return type of the continuation function that was passed in.
* Our function does a bit of work to build an intermediate value, then passes
that value to the continuation, which takes care of the remainder of the
work.

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

```tut:book
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:

```tut:book
val existingUser = User(100, "Alice", 42)
val computation = updateUserCont(existingUser, "Bob", 200)
```

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

```tut:book
val eval = computation.run { user =>
Eval.later {
println(s"Persisting updated user to the DB: $user")
Succeeded(user.id)
}
}
```

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

```tut:book
eval.value
```

## 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`:

```tut:book
val anotherComputation = computation.map { user =>
Map(
"id" -> user.id.toString,
"name" -> user.name,
"age" -> user.age.toString
)
}
val anotherEval = anotherComputation.run { userFields =>
Eval.later {
println(s"Persisting these fields to the DB: $userFields")
Succeeded(userFields("id").toInt)
}
}
anotherEval.value
```

And we can use `flatMap` to chain multiple `ContT`s 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.

```tut:book
val updateUserModel: ContT[Eval, UserUpdateResult, User] =
updateUserCont(existingUser, "Bob", 200).map { updatedUser =>
println("Updated user model")
updatedUser
}
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))
}
}
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)
}
}
val chainOfContinuations =
updateUserModel flatMap persistToDb flatMap publishEvent
val eval = chainOfContinuations.run { finalResult =>
Eval.later {
println("Finished!")
finalResult
}
}
eval.value
```

## 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.

0 comments on commit 9bed9b5

Please sign in to comment.