Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Write documentation for ContT #2677

Merged
merged 1 commit into from
Jan 22, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.