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

Documentation refresh for monad transformers #4724

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
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
5 changes: 0 additions & 5 deletions docs/datatypes.md

This file was deleted.

6 changes: 0 additions & 6 deletions docs/datatypes/directory.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,18 @@ laika.title = Data Types
laika.navigationOrder = [
chain.md
const.md
contt.md
either.md
eithert.md
eval.md
freeapplicative.md
freemonad.md
functionk.md
id.md
ior.md
iort.md
kleisli.md
nel.md
nested.md
oneand.md
optiont.md
state.md
statet.md
validated.md
writer.md
writert.md
]
4 changes: 2 additions & 2 deletions docs/datatypes/writer.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ type Writer[L, V] = WriterT[Id, L, V]

So, all the [Operations](#operations) defined in the previous section
are actually coming from the [WriterT
datatype](writert.md)
datatype](../monadtransformers/writert.md)

Most of the [`WriterT`](writert.md) functions require a
Most of the [`WriterT`](../monadtransformers/writert.md) functions require a
[`Functor[F]`](../typeclasses/functor.md) or
[`Monad[F]`](../typeclasses/monad.md)
instance. However, Cats provides all the necessary instances for the
Expand Down
3 changes: 1 addition & 2 deletions docs/directory.conf
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
laika.navigationOrder = [
index.md
typeclasses.md
datatypes.md
algebra.md
alleycats.md
motivations.md
Expand All @@ -17,4 +15,5 @@ laika.navigationOrder = [
typelevelEcosystem.md
typeclasses
datatypes
monadtransformers
]
2 changes: 1 addition & 1 deletion docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ It's really common to have a `List` of values with types like `Option`, `Either`

## Where is ListT?

There are monad transformers for various types, such as [OptionT](datatypes/optiont.md), so people often wonder why there isn't a `ListT`. For example, in the following example, people might reach for `ListT` to simplify making nested `map` and `exists` calls:
There are monad transformers for various types, such as [OptionT](monadtransformers/optiont.md), so people often wonder why there isn't a `ListT`. For example, in the following example, people might reach for `ListT` to simplify making nested `map` and `exists` calls:

```scala mdoc:reset:silent
val l: Option[List[Int]] = Some(List(1, 2, 3, 4, 5))
Expand Down
2 changes: 1 addition & 1 deletion docs/guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ abstract class KleisliInstance1 {
We can introduce new type classes for the sake of adding laws that don't apply to the parent type class, e.g. `CommutativeSemigroup` and
`CommutativeArrow`.

### Applicative instances for monad transformers</a>
### Applicative instances for monad transformers

We explicitly don't provide an instance of `Applicative` for e.g. `EitherT[F, String, *]` given an `Applicative[F]`.
An attempt to construct one without a proper `Monad[F]` instance would be inconsistent in `ap` with the provided `Monad` instance
Expand Down
2 changes: 1 addition & 1 deletion docs/imports.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import cats.data._
import cats.syntax.all._
```

The `cats._` import brings in quite a few [type classes](typeclasses.md) (similar to interfaces) such as [Monad](typeclasses/monad.md), [Semigroup](typeclasses/semigroup.md), and [Foldable](typeclasses/foldable.md). Instead of the entire `cats` package, you can import only the types that you need, for example:
Copy link
Member

@lenguyenthanh lenguyenthanh Mar 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer to use cats.* as it is back-ported to scala 2, and underscore import is deprecated in scala 3. Similar in other places.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is one existing usage of a .* import in the docs. If we change it, I would much rather see it done as a separate PR doing the replacement everywhere.

The `cats._` import brings in quite a few [type classes](typeclasses/index.md) (similar to interfaces) such as [Monad](typeclasses/monad.md), [Semigroup](typeclasses/semigroup.md), and [Foldable](typeclasses/foldable.md). Instead of the entire `cats` package, you can import only the types that you need, for example:

```scala mdoc:reset:silent
import cats.Monad
Expand Down
10 changes: 5 additions & 5 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ We thankfully accept <a href="https://opencollective.com/typelevel/donate">one-t

<script src="/cats/js/sponsors.js"></script>

### Getting Started
## Getting Started

Cats is available for [Scala.js](http://www.scala-js.org/) and [Scala Native](https://www.scala-native.org/), as well as the standard JVM runtime.

Expand Down Expand Up @@ -130,7 +130,7 @@ into larger problems.
If you are being harassed, please contact one of [us](#maintainers)
immediately so that we can support you.

### Binary compatibility and versioning
## Binary Compatibility and Versioning

After `1.0.0` release, we [decided](https://github.com/typelevel/cats/issues/1233)
to use *MAJOR.MINOR.PATCH* [Semantic Versioning 2.0.0](http://semver.org/)
Expand All @@ -157,11 +157,11 @@ Any binary breaking changes will require a *MAJOR* version bump, which we will b
cautious about. We will also consider using `organization` and package name for major
versioning in the future. But that decision is yet to be made.

### Adopters
## Adopters

A (non-exhaustive) list of companies that use Cats in production is featured on the [Adopters page]. Don't see yours? [You can add it in a PR!](https://github.com/typelevel/cats/edit/main/ADOPTERS.md) And if you can, consider [supporting us](https://opencollective.com/typelevel).

### Maintainers
## Maintainers

The current maintainers (people who can merge pull requests) are:

Expand Down Expand Up @@ -194,7 +194,7 @@ wait for more). For typos, documentation improvements or minor build fix we
relax this to a single sign-off. More detail in the [process document](https://github.com/typelevel/cats/blob/main/PROCESS.md).


### Copyright and License
## Copyright and License

All code is available to you under the MIT license, available at
http://opensource.org/licenses/mit-license.php and also in the
Expand Down
199 changes: 0 additions & 199 deletions docs/jump_start_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,205 +216,6 @@ def computeOverValue3: Future[Option[Int]] = valueOpt.flatTraverse(compute)
```
and that solves our problem for good.


# Monad Transformers

## OptionT

```scala mdoc:silent
import cats.data.OptionT
```

An instance of [`OptionT[F, A]`](datatypes/optiont.md) can be thought of as a wrapper over `F[Option[A]]`
which adds a couple of useful methods specific to nested types that aren't available in `F` or `Option` itself.
Most typically, your `F` will be `Future` (or sometimes slick's `DBIO`, but this requires having an implementation of Cats type classes like `Functor` or `Monad` for `DBIO`).
Wrappers such as `OptionT` are generally known as _monad transformers_.

A quite common pattern is mapping the inner value stored inside an instance of `F[Option[A]]` to an instance of `F[Option[B]]` with a function of type `A => B`.
This can be done with rather verbose syntax like:
```scala mdoc:silent
lazy val resultFuture: Future[Option[Int]] = ???

def mappedResultFuture: Future[Option[String]] = resultFuture.map { maybeValue =>
maybeValue.map { value =>
// Do something with the value and return String
???
}
}
```

With the use of `OptionT`, this can be simplified as follows:

```scala mdoc:silent
def mappedResultFuture2: OptionT[Future, String] = OptionT(resultFuture).map { value =>
// Do something with the value and return String
???
}
```

The above `map` will return a value of type `OptionT[Future, String]`.

To get the underlying `Future[Option[String]]` value, simply call `.value` on the `OptionT` instance.
It's also a viable solution to fully switch to `OptionT[Future, A]` in method parameter/return types and completely (or almost completely) ditch `Future[Option[A]]` in type declarations.

There are several ways to construct an `OptionT` instance.
The method headers in the table below are slightly simplified: the type parameters and type classes required by each method are skipped.

| Method | Takes | Returns |
| :---: | :---: | :---: |
| `OptionT.apply` or `OptionT(...)` | `F[Option[A]]` | `OptionT[F, A]` |
| `OptionT.fromOption` | `Option[A]` | `OptionT[F, A]` |
| `OptionT.liftF` | `F[A]` | `OptionT[F, A]` |
| `OptionT.pure` | `A` | `OptionT[F, A]` |

In production code you'll most commonly use the `OptionT(...)` syntax in order to wrap an instance of `Future[Option[A]]` into `OptionT[F, A]`.
The other methods, in turn, prove useful to set up `OptionT`-typed dummy values in unit tests.

We have already come across one of `OptionT`'s methods, namely `map`.
There are several other methods available and they mostly differ by the signature of the function they accept as the parameter.
As was the case with the previous table, the expected type classes are skipped.

| Method | Takes | Returns
| :---: | :---: | :---: |
| `map[B]` | `A => B` | `OptionT[F, B]` |
| `subflatMap[B]` | `A => Option[B]` | `OptionT[F, B]` |
| `semiflatMap[B]` | `A => F[B]` | `OptionT[F, B]` |
| `flatMapF[B]` | `A => F[Option[B]]` | `OptionT[F, B]` |
| `flatMap[B]` | `A => OptionT[F, B]` | `OptionT[F, B]` |

In practice, you're most likely to use `map` and `semiflatMap`.

As is always the case with `flatMap` and `map`, you can use it not only explicitly, but also under the hood in `for` comprehensions, as in the example below:

```scala mdoc:silent
class Money { /* ... */ }

def findUserById(userId: Long): OptionT[Future, User] = { /* ... */ ??? }

def findAccountById(accountId: Long): OptionT[Future, Account] = { /* ... */ ??? }

def getReservedFundsForAccount(account: Account): OptionT[Future, Money] = { /* ... */ ??? }

def getReservedFundsForUser(userId: Long): OptionT[Future, Money] = for {
user <- findUserById(userId)
account <- findAccountById(user.accountId)
funds <- getReservedFundsForAccount(account)
} yield funds
```

The `OptionT[Future, Money]` instance returned by `getReservedFundsForUser` will enclose a `None` value if any of the three composed methods returns an `OptionT` corresponding to `None`.
Otherwise, if the result of all three calls contains `Some`, the final outcome will also contain `Some`.


## EitherT

```scala mdoc:silent
import cats.data.EitherT
```

[`EitherT[F, A, B]`](datatypes/eithert.md) is the monad transformer for `Either` — you can think of it as a wrapper over a `F[Either[A, B]]` value.

Just as in the above section, I simplified the method headers, skipping type parameters or their context bounds and lower bounds.

Let's have a quick look at how to create an `EitherT` instance:

| Method | Takes | Returns |
| :---: | :---: | :---: |
| `EitherT.apply` or `EitherT(...)` | `F[Either[A, B]]` | `EitherT[F, A, B]` |
| `EitherT.fromEither` | `Either[A, B]` | `EitherT[F, A, B]` (wraps the provided `Either` value into `F`) |
| `EitherT.right` or `EitherT.liftF` | `F[B]` | `EitherT[F, A, B]` (wraps value inside `F[B]` into `Right`) |
| `EitherT.left` | `F[A]` | `EitherT[F, A, B]` (wraps value inside `F[B]` into `Left`) |
| `EitherT.pure` | `A` | `EitherT[F, A, B]` (wraps value into `Right` and then into `F`) |

Another useful way to construct an `EitherT` instance is to use `OptionT`'s methods `toLeft` and `toRight`:

```scala mdoc:silent
abstract class BaseException(message: String) extends Exception(message)

case class UserNotFoundException(message: String) extends BaseException(message)

def getUserById(userId: Int): Future[Option[User]] = { /* ... */ ??? }

def ensureUserExists(userId: Int): EitherT[Future, BaseException, User] = {
OptionT(getUserById(userId))
.toRight(left = UserNotFoundException(s"user not found, userId=$userId"))
}
```

`toRight` is pretty analogous to the method `Either.fromOption` mentioned before: just as `fromOption` built an `Either` from an `Option`, `toRight` creates an `EitherT` from an `OptionT`.
If the original `OptionT` stores `Some` value, it will be wrapped into `Right`; otherwise the value provided as the `left` parameter will be wrapped into a `Left`.
To provide the `left` value within the monad, there is corresponding `toRightF` method.

`toLeft` is `toRight`'s counterpart which wraps the `Some` value into `Left` and transforms `None` into `Right` enclosing the provided `right` value.
This is less commonly used in practice, but can serve e.g. for enforcing uniqueness checks in code.
We return `Left` if the value has been found, and `Right` if it doesn't yet exist in the system.

The methods available in `EitherT` are pretty similar to those we've seen in `OptionT`, but there are some notable differences.

You might get into some confusion at first when it comes to e.g. `map`.
In the case of `OptionT`, it was pretty obvious what should be done: `map` should go over the `Option` enclosed within `Future`, and then map the enclosed `Option` itself.
This is slightly less obvious in case of `EitherT`: should it map over both `Left` and `Right` values, or only the `Right` value?

The answer is that `EitherT` is _right-biased_, therefore plain `map` actually deals with the `Right` value.
This is unlike `Either` in the Scala standard library up to 2.11, which is in turn _unbiased_: there's no `map` available in `Either`, only for its left and right projections.

Having said that, let's take a quick look at the right-biased methods that `EitherT` offers:

| Method | Takes | Returns |
| :---: | --- | --- |
| `map[D]` | `B => D` | `EitherT[F, A, D]` |
| `subflatMap[D]` | `B => Either[A, D]` | `EitherT[F, A, D]` |
| `semiflatMap[D]` | `B => F[D]` | `EitherT[F, A, D]` |
| `flatMapF[D]` | `B => F[Either[A, D]]` | `EitherT[F, A, D]` |
| `flatMap[D]` | `B => EitherT[F, A, D]` | `EitherT[F, A, D]` |

As a side note, there are also certain methods in `EitherT` (that you're likely to need at some point) which map over the `Left` value, like `leftMap`, or over both `Left` and `Right` values, like `fold` or `bimap`.

`EitherT` is very useful for fail-fast chained verifications:

```scala mdoc:silent

case class Item(state: String)
class ItemOrder { /* ... */ }

case class ItemNotFoundException(message: String) extends BaseException(message)
case class InvalidItemStateException(message: String) extends BaseException(message)

def getItemById(itemId: Int): Future[Option[Item]] = { /* .. */ ??? }

def ensureItemExists(itemId: Int): EitherT[Future, BaseException, Item] = {
OptionT(getItemById(itemId))
.toRight(ItemNotFoundException(s"item not found, itemId = $itemId"))
}

def ensureItemStateIs(actual: String, expected: String): EitherT[Future, BaseException, Unit] = {
// Returns a Unit value wrapped into Right and then into Future if condition is true,
// otherwise the provided exception wrapped into Left and then into Future.
EitherT.cond(actual == expected, (), InvalidItemStateException(s"actual=$actual, expected=$expected"))
}

def placeOrderForItem(userId: Int, itemId: Int, count: Int): Future[ItemOrder] = { /* ... */ ??? }

def buyItem(userId: Int, itemId: Int, count: Int): EitherT[Future, BaseException, ItemOrder] = {
for {
user <- ensureUserExists(userId)
item <- ensureItemExists(itemId)
_ <- ensureItemStateIs(item.state, "AVAILABLE_IN_STOCK")
// EitherT.liftF is necessary to make EitherT[Future, BaseException, ItemOrder] out of Future[ItemOrder]
placedOrder <- EitherT.liftF(placeOrderForItem(userId, itemId, count))
} yield placedOrder
}
```

In the above example, we're running various checks against the item one by one.
If any of the checks fails, the resulting `EitherT` will contain a `Left` value.
Otherwise, if all of the checks yield a `Right` (of course we mean a `Right` wrapped into an `EitherT`), then the final outcome will also contain `Right`.
This is a fail-fast behavior: we're effectively stopping the `for` comprehension flow at the first `Left`-ish result.

If you're instead looking for validation that accumulates the errors (e.g. when dealing with user-provided form data), `cats.data.Validated` may be a good choice.



# Common issues

The Cats type class instances for standard library types are available in implicit scope and hence no longer have to be imported. If anything doesn't compile as expected, first make sure all the required Cats syntax implicits are in the scope — try importing ```cats.syntax.all._``` and see if the problem persists. The only exception is here is Cat's own ```Order``` and ```PartialOrder``` type classes which are available by importing ```cats.implicits._```.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def updateUser(persistToDatabase: User => Eval[UserUpdateResult])
```

(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.md) first.)
familiar with `Eval`, it's worth reading [the Eval documentation](../datatypes/eval.md) 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
Expand Down
11 changes: 11 additions & 0 deletions docs/monadtransformers/directory.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
laika.title = Monad Transformers
laika.navigationOrder = [
index.md
contt.md
eithert.md
iort.md
nested.md
optiont.md
statet.md
writert.md
]
11 changes: 8 additions & 3 deletions docs/datatypes/eithert.md → docs/monadtransformers/eithert.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@

API Documentation: @:api(cats.data.EitherT)

@:callout(warning)

`EitherT` can interact poorly with more powerful effect types that provide concurrent computation, such as Cats Effect's `IO` or fs2's `Stream`. Because `IO` already provides its own error channel, `EitherT[IO, Throwable]` can lead to confusing behavior; prefer `IO`'s own error channel instead of carrying a second throwable error channel.

@:@

`Either` can be used for error handling in most situations. However, when
`Either` is placed into effectful types such as `Option` or`Future`, a large
`Either` is placed into effectful types such as `Option`, a large
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's still an example using Future below this, but I think if we change that it should happen in another PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya. Examples are awkward since one of the big motivations is IO, but we can't depend on CE here.

amount of boilerplate is required to handle errors. For example, consider the
following program:

Expand Down Expand Up @@ -65,8 +71,7 @@ handle the errors will increase dramatically.
`EitherT[F[_], A, B]` is a lightweight wrapper for `F[Either[A, B]]` that makes
it easy to compose `Either`s and `F`s together. To use `EitherT`, values of
`Either`, `F`, `A`, and `B` are first converted into `EitherT`, and the
resulting `EitherT` values are then composed using combinators. For example, the
asynchronous division program can be rewritten as follows:
resulting `EitherT` values are then composed using combinators. For example, the asynchronous division program can be rewritten as follows:

```scala mdoc:nest
import cats.data.EitherT
Expand Down
Loading