From 6d4a0e04fd3b045ccfc40324b54904b067928238 Mon Sep 17 00:00:00 2001 From: Justin Reardon Date: Mon, 3 Mar 2025 20:16:03 -0500 Subject: [PATCH 1/8] extract monad transformers to their own section --- docs/datatypes.md | 5 - docs/datatypes/directory.conf | 6 - docs/directory.conf | 3 +- docs/faq.md | 2 +- docs/jump_start_guide.md | 199 ---------------- .../{datatypes => monadtransformers}/contt.md | 2 +- docs/monadtransformers/directory.conf | 11 + .../eithert.md | 2 +- docs/{datatypes => monadtransformers}/iort.md | 0 docs/monadtransformers/monadtransformers.md | 213 ++++++++++++++++++ .../nested.md | 0 .../optiont.md | 0 .../statet.md | 0 .../writert.md | 26 +-- docs/typeclasses/directory.conf | 1 + docs/typeclasses/monad.md | 2 +- docs/{ => typeclasses}/typeclasses.md | 4 +- 17 files changed, 245 insertions(+), 231 deletions(-) delete mode 100644 docs/datatypes.md rename docs/{datatypes => monadtransformers}/contt.md (99%) create mode 100644 docs/monadtransformers/directory.conf rename docs/{datatypes => monadtransformers}/eithert.md (98%) rename docs/{datatypes => monadtransformers}/iort.md (100%) create mode 100644 docs/monadtransformers/monadtransformers.md rename docs/{datatypes => monadtransformers}/nested.md (100%) rename docs/{datatypes => monadtransformers}/optiont.md (100%) rename docs/{datatypes => monadtransformers}/statet.md (100%) rename docs/{datatypes => monadtransformers}/writert.md (91%) rename docs/{ => typeclasses}/typeclasses.md (99%) diff --git a/docs/datatypes.md b/docs/datatypes.md deleted file mode 100644 index b151b8ebe5..0000000000 --- a/docs/datatypes.md +++ /dev/null @@ -1,5 +0,0 @@ -{% laika.title = "Data Types" %} - -@:navigationTree { - entries = [ { target = "datatypes", depth = 2 } ] -} diff --git a/docs/datatypes/directory.conf b/docs/datatypes/directory.conf index b44c76219c..b7b01bd51c 100644 --- a/docs/datatypes/directory.conf +++ b/docs/datatypes/directory.conf @@ -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 ] \ No newline at end of file diff --git a/docs/directory.conf b/docs/directory.conf index 7632d07504..da69be8691 100644 --- a/docs/directory.conf +++ b/docs/directory.conf @@ -1,7 +1,5 @@ laika.navigationOrder = [ index.md - typeclasses.md - datatypes.md algebra.md alleycats.md motivations.md @@ -17,4 +15,5 @@ laika.navigationOrder = [ typelevelEcosystem.md typeclasses datatypes + monadtransformers ] \ No newline at end of file diff --git a/docs/faq.md b/docs/faq.md index 6a16af2904..d593e14196 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -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)) diff --git a/docs/jump_start_guide.md b/docs/jump_start_guide.md index 06da8d27b3..045934fe0c 100644 --- a/docs/jump_start_guide.md +++ b/docs/jump_start_guide.md @@ -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._```. diff --git a/docs/datatypes/contt.md b/docs/monadtransformers/contt.md similarity index 99% rename from docs/datatypes/contt.md rename to docs/monadtransformers/contt.md index 503bd31f78..df6b24269c 100644 --- a/docs/datatypes/contt.md +++ b/docs/monadtransformers/contt.md @@ -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 diff --git a/docs/monadtransformers/directory.conf b/docs/monadtransformers/directory.conf new file mode 100644 index 0000000000..582293e094 --- /dev/null +++ b/docs/monadtransformers/directory.conf @@ -0,0 +1,11 @@ +laika.title = Monad Transformers +laika.navigationOrder = [ + monadtransformers.md + contt.md + eithert.md + iort.md + nested.md + optiont.md + statet.md + writert.md +] \ No newline at end of file diff --git a/docs/datatypes/eithert.md b/docs/monadtransformers/eithert.md similarity index 98% rename from docs/datatypes/eithert.md rename to docs/monadtransformers/eithert.md index e724d693f0..f6b14b8cfe 100644 --- a/docs/datatypes/eithert.md +++ b/docs/monadtransformers/eithert.md @@ -3,7 +3,7 @@ API Documentation: @:api(cats.data.EitherT) `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 amount of boilerplate is required to handle errors. For example, consider the following program: diff --git a/docs/datatypes/iort.md b/docs/monadtransformers/iort.md similarity index 100% rename from docs/datatypes/iort.md rename to docs/monadtransformers/iort.md diff --git a/docs/monadtransformers/monadtransformers.md b/docs/monadtransformers/monadtransformers.md new file mode 100644 index 0000000000..824f856ae1 --- /dev/null +++ b/docs/monadtransformers/monadtransformers.md @@ -0,0 +1,213 @@ +# Overview + +Monad transformers are data types used to simplify the composition of monads. For example: + +## OptionT + +```scala mdoc:silent +import cats.data.OptionT +``` + +An instance of [`OptionT[F, A]`](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 `IO` (or something like [doobie](https://typelevel.org/doobie/index.html)'s `ConnectionIO`). +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 +import scala.util.Try + +lazy val resultTry: Try[Option[Int]] = ??? + +def mappedResultTry: Try[Option[String]] = resultTry.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 +import cats.data.OptionT + +def mappedResultTry2: OptionT[Try, String] = OptionT(resultTry).map { value => + // Do something with the value and return String + ??? +} +``` + +The above `map` will return a value of type `OptionT[Try, String]`. + +To get the underlying `Try[Option[String]]` value, simply call `.value` on the `OptionT` instance. +In practice, you should not include monad transformers in function signatures, and only use them as locally-scoped utilities to reduce boilerplate so that calling code is not required to care about the composition. + +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 `Try[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 + +case class User(accountId: Long) { /* ... */ } +class Account { /* ... */ } +class Money { /* ... */ } + +def findUserById(userId: Long): Try[Option[User]] = { /* ... */ ??? } + +def findAccountById(accountId: Long): Try[Option[Account]] = { /* ... */ ??? } + +def getReservedFundsForAccount(account: Account): Try[Option[Money]] = { /* ... */ ??? } + +def getReservedFundsForUser(userId: Long): Try[Option[Money]] = ( + for { + user <- OptionT(findUserById(userId)) + account <- OptionT(findAccountById(user.accountId)) + funds <- OptionT(getReservedFundsForAccount(account)) + } yield funds +).value +``` + +The `OptionT[Try, 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]`](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): Try[Option[User]] = { /* ... */ ??? } + +def ensureUserExists(userId: Int): EitherT[Try, 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 `Try`, 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. + +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): Try[Option[Item]] = { /* .. */ ??? } + +def ensureItemExists(itemId: Int): EitherT[Try, BaseException, Item] = { + OptionT(getItemById(itemId)) + .toRight(ItemNotFoundException(s"item not found, itemId = $itemId")) +} + +def ensureItemStateIs(actual: String, expected: String): EitherT[Try, BaseException, Unit] = { + // Returns a Unit value wrapped into Right and then into Try if condition is true, + // otherwise the provided exception wrapped into Left and then into Try. + EitherT.cond(actual == expected, (), InvalidItemStateException(s"actual=$actual, expected=$expected")) +} + +def placeOrderForItem(userId: Int, itemId: Int, count: Int): Try[ItemOrder] = { /* ... */ ??? } + +def buyItem(userId: Int, itemId: Int, count: Int): EitherT[Try, BaseException, ItemOrder] = { + for { + user <- ensureUserExists(userId) + item <- ensureItemExists(itemId) + _ <- ensureItemStateIs(item.state, "AVAILABLE_IN_STOCK") + // EitherT.liftF is necessary to make EitherT[Try, BaseException, ItemOrder] out of Try[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), [Validated](datatypes/validated.md) or the [Parallel](typeclasses/parallel.md) applicative functor for `Either` may be a good choice. + +## Pitfalls + +In principle, monad transformers can be stacked to create data types with complex behavior. +While powerful, this can lead to unwieldy type signatures and poor type inference. +The [Cats MTL](https://typelevel.org/cats-mtl/) project provides a capability based approach to extend effect types without requiring as much boilerplate. + +Some monad transformers can also interact poorly with more powerful effect types that provide concurrent computation, such as Cats Effect's `IO` or fs2's `Stream`. You should avoid `StateT` and `WriterT` and use the concurrent data types provided by those libraries instead. Because `IO` already provides its own error channel, `EitherT[IO, Throwable]` can also lead to confusing behavior; prefer `IO`'s own error channel instead. \ No newline at end of file diff --git a/docs/datatypes/nested.md b/docs/monadtransformers/nested.md similarity index 100% rename from docs/datatypes/nested.md rename to docs/monadtransformers/nested.md diff --git a/docs/datatypes/optiont.md b/docs/monadtransformers/optiont.md similarity index 100% rename from docs/datatypes/optiont.md rename to docs/monadtransformers/optiont.md diff --git a/docs/datatypes/statet.md b/docs/monadtransformers/statet.md similarity index 100% rename from docs/datatypes/statet.md rename to docs/monadtransformers/statet.md diff --git a/docs/datatypes/writert.md b/docs/monadtransformers/writert.md similarity index 91% rename from docs/datatypes/writert.md rename to docs/monadtransformers/writert.md index d793d2cea9..fe3e8cd8d2 100644 --- a/docs/datatypes/writert.md +++ b/docs/monadtransformers/writert.md @@ -3,7 +3,7 @@ API Documentation: @:api(cats.data.WriterT) `WriterT[F[_], L, V]` is a type wrapper on an `F[(L, -V)]`. Speaking technically, it is a monad transformer for [`Writer`](writer.md), +V)]`. Speaking technically, it is a monad transformer for [`Writer`](../datatypes/writer.md), but you don't need to know what that means for it to be useful. @@ -11,8 +11,8 @@ useful. `WriterT` can be more convenient to work with than using `F[Writer[L, V]]` directly, because it exposes operations that allow -you to work with the values of the inner [`Writer`](writer.md) (`L` and -`V`) abstracting both the `F` and [`Writer`](writer.md). +you to work with the values of the inner [`Writer`](../datatypes/writer.md) (`L` and +`V`) abstracting both the `F` and [`Writer`](../datatypes/writer.md). For example, `map` allow you to transform the inner `V` value, getting back a `WriterT` that wraps around it. @@ -25,13 +25,13 @@ WriterT[Option, String, Int](Some(("value", 10))).map(x => x * x) Plus, when composing multiple `WriterT` computations, those will be composed following the same behaviour of a -[`Writer`](writer.md) and the +[`Writer`](../datatypes/writer.md) and the generic `F`. Let's see two examples with `Option` and `Either`: if one of the computations has a `None` or a `Left`, the whole computation will return a `None` or a `Left` since the way the two types compose typically behaves that way. Moreover, when the computation succeed, the logging side of the -[`Writer`](writer.md)s will be +[`Writer`](../datatypes/writer.md)s will be combined. ```scala mdoc:silent @@ -75,17 +75,17 @@ for { Just for completeness, we can have a look at the same example, but with -[`Validated`](validated.md) +[`Validated`](../datatypes/validated.md) since it as a slightly different behaviour than `Either`. Instead of short-circuiting when the first error is encountered, -[`Validated`](validated.md) +[`Validated`](../datatypes/validated.md) will accumulate all the errors. In the following example, you can see how this behaviour is respected when -[`Validated`](validated.md) is +[`Validated`](../datatypes/validated.md) is wrapped as the `F` type of a `WriterT`. In addition, notice how `flatMap` and for comprehension can't be used in this case, since -[`Validated`](validated.md) +[`Validated`](../datatypes/validated.md) only extends [`Applicative`](../typeclasses/applicative.md), but not [`Monad`](../typeclasses/monad.md). ```scala mdoc:silent @@ -147,7 +147,7 @@ to fill the `L` value not specified in the input. `put[F[_], L, V](v: V)(l: L)(implicit applicativeF: Applicative[F]): WriterT[F, L, V]` : As soon as there is an [`Applicative`](../typeclasses/applicative.md) instance of `F`, this function -creates the datatype starting from the inner [`Writer`](writer.md)'s values. +creates the datatype starting from the inner [`Writer`](../datatypes/writer.md)'s values. ```scala mdoc:nest WriterT.put[Option, String, Int](123)("initial value") @@ -163,14 +163,14 @@ creates the datatype starting from the inner [`Writer`](writer.md)'s values. ## Operations In the [Writer -definition](writer.md#definition) +definition](../datatypes/writer.md#definition) section, we showed how it is actually a `WriterT`. Therefore, all the operations described into [Writer -operations](writer.md#operations) +operations](../datatypes/writer.md#operations) are valid for `WriterT` as well. The only aspect we want to remark here is the following sentence from -[`Writer`](writer.md)'s page: +[`Writer`](../datatypes/writer.md)'s page: > Most of the `WriterT` functions require a `Functor[F]` or > `Monad[F]` instance. However, Cats provides all the necessary diff --git a/docs/typeclasses/directory.conf b/docs/typeclasses/directory.conf index 08270eb62b..243f731979 100644 --- a/docs/typeclasses/directory.conf +++ b/docs/typeclasses/directory.conf @@ -1,5 +1,6 @@ laika.title = Type Classes laika.navigationOrder = [ + typeclasses.md alternative.md applicative.md applicativemonaderror.md diff --git a/docs/typeclasses/monad.md b/docs/typeclasses/monad.md index 080f0a3986..b675a3e798 100644 --- a/docs/typeclasses/monad.md +++ b/docs/typeclasses/monad.md @@ -158,7 +158,7 @@ implicit def optionTMonad[F[_]](implicit F: Monad[F]): Monad[OptionT[F, *]] = { This sort of construction is called a monad transformer. -Cats has an [`OptionT`](../datatypes/optiont.md) monad transformer, which adds a lot of useful functions to the simple implementation above. +Cats has an [`OptionT`](../monadtransformers/optiont.md) monad transformer, which adds a lot of useful functions to the simple implementation above. ## FlatMap - a weakened Monad A closely related type class is `FlatMap` which is identical to `Monad`, minus the `pure` diff --git a/docs/typeclasses.md b/docs/typeclasses/typeclasses.md similarity index 99% rename from docs/typeclasses.md rename to docs/typeclasses/typeclasses.md index 85a4d32f69..d792bba2b9 100644 --- a/docs/typeclasses.md +++ b/docs/typeclasses/typeclasses.md @@ -1,4 +1,4 @@ -# Type Classes +# Overview Type classes are a powerful tool used in functional programming to enable ad-hoc polymorphism, more commonly known as overloading. Where many object-oriented languages leverage subtyping for polymorphic code, functional programming tends towards a combination of parametric polymorphism (think type parameters, like Java generics) @@ -223,7 +223,7 @@ val result = Monoid[Int].combine(sumLeft, sumRight) Cats provides laws for type classes via the `kernel-laws` and `laws` modules which makes law checking type class instances easy. -You can find out more about law testing [here](typeclasses/lawtesting.md). +You can find out more about law testing [here](lawtesting.md). ## Type classes in Cats From a60331092ed8f1f2ccef48a231b91377f16543b8 Mon Sep 17 00:00:00 2001 From: Justin Reardon Date: Mon, 3 Mar 2025 20:31:29 -0500 Subject: [PATCH 2/8] Clean up index headers --- docs/datatypes/writer.md | 4 ++-- docs/guidelines.md | 2 +- docs/imports.md | 2 +- docs/index.md | 10 +++++----- docs/monadtransformers/directory.conf | 2 +- .../{monadtransformers.md => index.md} | 2 +- docs/motivations.md | 2 +- docs/typeclasses/directory.conf | 2 +- docs/typeclasses/{typeclasses.md => index.md} | 0 docs/typeclasses/lawtesting.md | 2 +- 10 files changed, 14 insertions(+), 14 deletions(-) rename docs/monadtransformers/{monadtransformers.md => index.md} (98%) rename docs/typeclasses/{typeclasses.md => index.md} (100%) diff --git a/docs/datatypes/writer.md b/docs/datatypes/writer.md index 7d1cb39f4d..c6b3bdf73b 100644 --- a/docs/datatypes/writer.md +++ b/docs/datatypes/writer.md @@ -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 diff --git a/docs/guidelines.md b/docs/guidelines.md index 0a731cfc76..cf7291eae1 100644 --- a/docs/guidelines.md +++ b/docs/guidelines.md @@ -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 +### 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 diff --git a/docs/imports.md b/docs/imports.md index 866a9986e1..c712c25c9a 100644 --- a/docs/imports.md +++ b/docs/imports.md @@ -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: +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 diff --git a/docs/index.md b/docs/index.md index a6f5e443d0..f2841ac585 100644 --- a/docs/index.md +++ b/docs/index.md @@ -65,7 +65,7 @@ We thankfully accept one-t -### 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. @@ -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/) @@ -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: @@ -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 diff --git a/docs/monadtransformers/directory.conf b/docs/monadtransformers/directory.conf index 582293e094..a4691c09f0 100644 --- a/docs/monadtransformers/directory.conf +++ b/docs/monadtransformers/directory.conf @@ -1,6 +1,6 @@ laika.title = Monad Transformers laika.navigationOrder = [ - monadtransformers.md + index.md contt.md eithert.md iort.md diff --git a/docs/monadtransformers/monadtransformers.md b/docs/monadtransformers/index.md similarity index 98% rename from docs/monadtransformers/monadtransformers.md rename to docs/monadtransformers/index.md index 824f856ae1..3c84e2d02f 100644 --- a/docs/monadtransformers/monadtransformers.md +++ b/docs/monadtransformers/index.md @@ -202,7 +202,7 @@ 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), [Validated](datatypes/validated.md) or the [Parallel](typeclasses/parallel.md) applicative functor for `Either` may be a good choice. +If you're instead looking for validation that accumulates the errors (e.g. when dealing with user-provided form data), [Validated](../datatypes/validated.md) or the [Parallel](../typeclasses/parallel.md) applicative functor for `Either` may be a good choice. ## Pitfalls diff --git a/docs/motivations.md b/docs/motivations.md index bab6174237..81b088710b 100644 --- a/docs/motivations.md +++ b/docs/motivations.md @@ -15,7 +15,7 @@ page](CONTRIBUTING.md) to find out ways to give us feedback. #### Modularity We are trying to make the library modular. It will have a tight -core which will contain only the [type classes](typeclasses.md), +core which will contain only the [type classes](typeclasses/index.md), the bare minimum of data structures that are needed to support them, and type class instances for those data structures and standard library types. diff --git a/docs/typeclasses/directory.conf b/docs/typeclasses/directory.conf index 243f731979..6fd578269a 100644 --- a/docs/typeclasses/directory.conf +++ b/docs/typeclasses/directory.conf @@ -1,6 +1,6 @@ laika.title = Type Classes laika.navigationOrder = [ - typeclasses.md + index.md alternative.md applicative.md applicativemonaderror.md diff --git a/docs/typeclasses/typeclasses.md b/docs/typeclasses/index.md similarity index 100% rename from docs/typeclasses/typeclasses.md rename to docs/typeclasses/index.md diff --git a/docs/typeclasses/lawtesting.md b/docs/typeclasses/lawtesting.md index f4a8ab68ab..c9a6fa37c5 100644 --- a/docs/typeclasses/lawtesting.md +++ b/docs/typeclasses/lawtesting.md @@ -1,6 +1,6 @@ # Law testing -[Laws](../typeclasses.md#laws) are an important part of cats. +[Laws](../typeclasses/index.md#laws) are an important part of cats. Cats uses [discipline](https://github.com/typelevel/discipline) to define type class laws and the [ScalaCheck](https://github.com/rickynils/scalacheck) tests based on them. From 56fa9de767918bf39ffbf78db72e8e5c7497144b Mon Sep 17 00:00:00 2001 From: Justin Reardon Date: Tue, 4 Mar 2025 22:10:52 -0500 Subject: [PATCH 3/8] Address review comments - add warnings to specific transformer pages - call out recommended usage more specifically - add CE link --- docs/monadtransformers/eithert.md | 9 ++- docs/monadtransformers/index.md | 121 +++--------------------------- docs/monadtransformers/statet.md | 6 ++ docs/monadtransformers/writert.md | 6 ++ 4 files changed, 28 insertions(+), 114 deletions(-) diff --git a/docs/monadtransformers/eithert.md b/docs/monadtransformers/eithert.md index f6b14b8cfe..8a9140dd7e 100644 --- a/docs/monadtransformers/eithert.md +++ b/docs/monadtransformers/eithert.md @@ -2,6 +2,12 @@ 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`, a large amount of boilerplate is required to handle errors. For example, consider the @@ -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 diff --git a/docs/monadtransformers/index.md b/docs/monadtransformers/index.md index 3c84e2d02f..d2420f6a89 100644 --- a/docs/monadtransformers/index.md +++ b/docs/monadtransformers/index.md @@ -10,7 +10,7 @@ import cats.data.OptionT An instance of [`OptionT[F, A]`](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 `IO` (or something like [doobie](https://typelevel.org/doobie/index.html)'s `ConnectionIO`). +Most typically, your `F` will be `IO` from [Cats Effect](https://typelevel.org/cats-effect), or something like [doobie](https://typelevel.org/doobie/index.html)'s `ConnectionIO`). 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`. @@ -40,9 +40,7 @@ def mappedResultTry2: OptionT[Try, String] = OptionT(resultTry).map { value => ``` The above `map` will return a value of type `OptionT[Try, String]`. - To get the underlying `Try[Option[String]]` value, simply call `.value` on the `OptionT` instance. -In practice, you should not include monad transformers in function signatures, and only use them as locally-scoped utilities to reduce boilerplate so that calling code is not required to care about the composition. 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. @@ -54,7 +52,7 @@ The method headers in the table below are slightly simplified: the type paramete | `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 `Try[Option[A]]` into `OptionT[F, A]`. +In production code you'll most commonly use the `OptionT(...)` syntax in order to wrap an instance of `F[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`. @@ -97,117 +95,16 @@ def getReservedFundsForUser(userId: Long): Try[Option[Money]] = ( The `OptionT[Try, 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]`](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): Try[Option[User]] = { /* ... */ ??? } - -def ensureUserExists(userId: Int): EitherT[Try, 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 `Try`, 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. - -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): Try[Option[Item]] = { /* .. */ ??? } - -def ensureItemExists(itemId: Int): EitherT[Try, BaseException, Item] = { - OptionT(getItemById(itemId)) - .toRight(ItemNotFoundException(s"item not found, itemId = $itemId")) -} - -def ensureItemStateIs(actual: String, expected: String): EitherT[Try, BaseException, Unit] = { - // Returns a Unit value wrapped into Right and then into Try if condition is true, - // otherwise the provided exception wrapped into Left and then into Try. - EitherT.cond(actual == expected, (), InvalidItemStateException(s"actual=$actual, expected=$expected")) -} - -def placeOrderForItem(userId: Int, itemId: Int, count: Int): Try[ItemOrder] = { /* ... */ ??? } - -def buyItem(userId: Int, itemId: Int, count: Int): EitherT[Try, BaseException, ItemOrder] = { - for { - user <- ensureUserExists(userId) - item <- ensureItemExists(itemId) - _ <- ensureItemStateIs(item.state, "AVAILABLE_IN_STOCK") - // EitherT.liftF is necessary to make EitherT[Try, BaseException, ItemOrder] out of Try[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), [Validated](../datatypes/validated.md) or the [Parallel](../typeclasses/parallel.md) applicative functor for `Either` may be a good choice. - ## Pitfalls In principle, monad transformers can be stacked to create data types with complex behavior. While powerful, this can lead to unwieldy type signatures and poor type inference. The [Cats MTL](https://typelevel.org/cats-mtl/) project provides a capability based approach to extend effect types without requiring as much boilerplate. -Some monad transformers can also interact poorly with more powerful effect types that provide concurrent computation, such as Cats Effect's `IO` or fs2's `Stream`. You should avoid `StateT` and `WriterT` and use the concurrent data types provided by those libraries instead. Because `IO` already provides its own error channel, `EitherT[IO, Throwable]` can also lead to confusing behavior; prefer `IO`'s own error channel instead. \ No newline at end of file +Some monad transformers can also interact poorly with more powerful effect types that provide concurrent computation, such as Cats Effect's `IO` or fs2's `Stream`. You should avoid `StateT` and `WriterT` and use the concurrent data types provided by those libraries instead. Because `IO` already provides its own error channel, `EitherT[IO, Throwable]` can also lead to confusing behavior; prefer `IO`'s own error channel instead. + +## Recommended Usage + +To minimize these pitfalls, monad transformers should only be used as locally-scoped utilities to reduce boilerplate. +Do not include monad transformers in public method signatures. +Calling code can always use monad transformers themselves. \ No newline at end of file diff --git a/docs/monadtransformers/statet.md b/docs/monadtransformers/statet.md index b275a463f9..9a08fc9327 100644 --- a/docs/monadtransformers/statet.md +++ b/docs/monadtransformers/statet.md @@ -22,6 +22,12 @@ state to an existing computation in the context of `F`. This definition could be confusing, but it will become clear after learning the `State` monad and by the example below. +@:callout(warning) + +`StateT` interacts poorly with more powerful effect types that provide concurrent computation, such as Cats Effect's `IO` or fs2's `Stream`. You should instead use the concurrent data types provided by those libraries. + +@:@ + ## StateT and State Relationship `StateT` is a monad transformer for diff --git a/docs/monadtransformers/writert.md b/docs/monadtransformers/writert.md index fe3e8cd8d2..024832f547 100644 --- a/docs/monadtransformers/writert.md +++ b/docs/monadtransformers/writert.md @@ -7,6 +7,12 @@ V)]`. Speaking technically, it is a monad transformer for [`Writer`](../datatype but you don't need to know what that means for it to be useful. +@:callout(warning) + +`WriterT` interacts poorly with more powerful effect types that provide concurrent computation, such as Cats Effect's `IO` or fs2's `Stream`. You should instead use the concurrent data types provided by those libraries. + +@:@ + ## Composition `WriterT` can be more convenient to work with than using From 344d6ed38a4ff8f420faadd786b7268d87a8505a Mon Sep 17 00:00:00 2001 From: Justin Reardon Date: Mon, 26 Oct 2026 08:34:14 -0400 Subject: [PATCH 4/8] Fix up monad transform doc changes - fix broken links - add a small overview page for data types - fix number of type parameters for EitherT --- CONTRIBUTING.md | 33 +++++++++++++++------ README.md | 4 +-- core/src/main/scala/cats/data/OptionT.scala | 2 +- docs/datatypes/directory.conf | 4 +-- docs/datatypes/index.md | 3 ++ docs/monadtransformers/eithert.md | 2 +- docs/monadtransformers/index.md | 8 ++--- docs/typeclasses/lawtesting.md | 2 +- 8 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 docs/datatypes/index.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c5eb7fff3..c135b5dcde 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,14 +22,29 @@ follow up with any polishing that may need to be done. If you want to touch up some documentation or fix typos, feel free to skip these steps and jump straight to submitting a pull request. - 1. [Find something that belongs in cats](#find-something-that-belongs-in-cats) - 2. [Let us know you are working on it](#let-us-know-you-are-working-on-it) - 3. [Build the project](#build-the-project) - 4. [Implement your contribution](#write-code) - 5. [Write tests](#write-tests) - 6. [Write documentation](#contributing-documentation) - 7. [Write examples](#write-examples) - 8. [Submit pull request](#submit-a-pull-request) +- [Contributor Guide](#contributor-guide) + - [About this document](#about-this-document) + - [How can I help?](#how-can-i-help) + - [Find something that belongs in Cats](#find-something-that-belongs-in-cats) + - [Cats subprojects](#cats-subprojects) + - [Let us know you are working on it](#let-us-know-you-are-working-on-it) + - [Build the project](#build-the-project) + - [Scala and Scala.js](#scala-and-scalajs) + - [Editor Setup Tips](#editor-setup-tips) + - [Write code](#write-code) + - [Attributions](#attributions) + - [Write tests](#write-tests) + - [Binary compatibility](#binary-compatibility) + - [Contributing documentation](#contributing-documentation) + - [source for the documentation](#source-for-the-documentation) + - [Generating the Site](#generating-the-site) + - [Previewing the site](#previewing-the-site) + - [Compiler verified documentation](#compiler-verified-documentation) + - [Write examples](#write-examples) + - [Submit a pull request](#submit-a-pull-request) + - [How did we do?](#how-did-we-do) + - [Getting in touch](#getting-in-touch) + - [Nix Cats Development Environment](#nix-cats-development-environment) ### Find something that belongs in Cats @@ -50,7 +65,7 @@ pull request. The preferred ways to do that are to either: Things that belong in Cats generally have the following characteristics: - * Their behavior is governed by well-defined [laws](https://typelevel.org/cats/typeclasses.html#laws). + * Their behavior is governed by well-defined [laws](https://typelevel.org/cats/typeclasses/index.html#laws). * They provide general abstractions. Laws help keep types consistent, and remove ambiguity or sensitivity diff --git a/README.md b/README.md index a1dacb4452..d113a8dcbd 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,8 @@ Links: 1. Website: [typelevel.org/cats/](https://typelevel.org/cats/) 2. ScalaDoc: [typelevel.org/cats/api/](https://typelevel.org/cats/api/) -3. Type classes: [typelevel.org/cats/typeclasses.html](https://typelevel.org/cats/typeclasses.html) -4. Data types: [typelevel.org/cats/datatypes.html](https://typelevel.org/cats/datatypes.html) +3. Type classes: [typelevel.org/cats/typeclasses/](https://typelevel.org/cats/typeclasses/) +4. Data types: [typelevel.org/cats/datatypes/](https://typelevel.org/cats/datatypes/) 5. Algebra overview: [typelevel.org/cats/algebra.html](https://typelevel.org/cats/algebra.html) 6. Glossary: [typelevel.org/cats/nomenclature.html](https://typelevel.org/cats/nomenclature.html) 7. Resources for Learners: [typelevel.org/cats/resources_for_learners.html](https://typelevel.org/cats/resources_for_learners.html) diff --git a/core/src/main/scala/cats/data/OptionT.scala b/core/src/main/scala/cats/data/OptionT.scala index 53c9d4f05c..ca5f4900ff 100644 --- a/core/src/main/scala/cats/data/OptionT.scala +++ b/core/src/main/scala/cats/data/OptionT.scala @@ -28,7 +28,7 @@ package data * * It may also be said that `OptionT` is a monad transformer for `Option`. * - * For more information, see the [[http://typelevel.org/cats/datatypes/optiont.html documentation]]. + * For more information, see the [[http://typelevel.org/cats/monadtransformers/optiont.html documentation]]. */ final case class OptionT[F[_], A](value: F[Option[A]]) { diff --git a/docs/datatypes/directory.conf b/docs/datatypes/directory.conf index b7b01bd51c..8d55bbd5c6 100644 --- a/docs/datatypes/directory.conf +++ b/docs/datatypes/directory.conf @@ -1,5 +1,6 @@ laika.title = Data Types laika.navigationOrder = [ + index.md chain.md const.md either.md @@ -13,7 +14,6 @@ laika.navigationOrder = [ nel.md oneand.md state.md - statet.md validated.md writer.md -] \ No newline at end of file +] diff --git a/docs/datatypes/index.md b/docs/datatypes/index.md new file mode 100644 index 0000000000..3541038ecd --- /dev/null +++ b/docs/datatypes/index.md @@ -0,0 +1,3 @@ +# Overview + +Cats includes a number of utility data types. There are several general collection data structures that provide functionality not found in the Scala standard library: [Chain](chain.md), [Ior](ior.md), [NonEmptyList](nel.md), [OneAnd](oneand.md) and [Validated](validated.md). The other data types provide various utilities for working with computations. diff --git a/docs/monadtransformers/eithert.md b/docs/monadtransformers/eithert.md index 8a9140dd7e..569f69eb7a 100644 --- a/docs/monadtransformers/eithert.md +++ b/docs/monadtransformers/eithert.md @@ -4,7 +4,7 @@ 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. +`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, A]` can lead to confusing behavior; prefer `IO`'s own error channel instead of carrying a second throwable error channel. @:@ diff --git a/docs/monadtransformers/index.md b/docs/monadtransformers/index.md index d2420f6a89..e1452b91cf 100644 --- a/docs/monadtransformers/index.md +++ b/docs/monadtransformers/index.md @@ -92,8 +92,8 @@ def getReservedFundsForUser(userId: Long): Try[Option[Money]] = ( ).value ``` -The `OptionT[Try, 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`. +The `OptionT[Try, Money]` instance created by the for comprehension in `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`. Calling `value` at the end removes the monad transformer. ## Pitfalls @@ -101,10 +101,10 @@ In principle, monad transformers can be stacked to create data types with comple While powerful, this can lead to unwieldy type signatures and poor type inference. The [Cats MTL](https://typelevel.org/cats-mtl/) project provides a capability based approach to extend effect types without requiring as much boilerplate. -Some monad transformers can also interact poorly with more powerful effect types that provide concurrent computation, such as Cats Effect's `IO` or fs2's `Stream`. You should avoid `StateT` and `WriterT` and use the concurrent data types provided by those libraries instead. Because `IO` already provides its own error channel, `EitherT[IO, Throwable]` can also lead to confusing behavior; prefer `IO`'s own error channel instead. +Some monad transformers can also interact poorly with more powerful effect types that provide concurrent computation, such as Cats Effect's `IO` or fs2's `Stream`. You should avoid `StateT` and `WriterT` and use the concurrent data types provided by those libraries instead. Because `IO` already provides its own error channel, `EitherT[IO, Throwable, A]` can also lead to confusing behavior; prefer `IO`'s own error channel instead. ## Recommended Usage To minimize these pitfalls, monad transformers should only be used as locally-scoped utilities to reduce boilerplate. Do not include monad transformers in public method signatures. -Calling code can always use monad transformers themselves. \ No newline at end of file +Calling code can always use monad transformers themselves. diff --git a/docs/typeclasses/lawtesting.md b/docs/typeclasses/lawtesting.md index c9a6fa37c5..1c5f0e302b 100644 --- a/docs/typeclasses/lawtesting.md +++ b/docs/typeclasses/lawtesting.md @@ -1,6 +1,6 @@ # Law testing -[Laws](../typeclasses/index.md#laws) are an important part of cats. +[Laws](index.md#laws) are an important part of cats. Cats uses [discipline](https://github.com/typelevel/discipline) to define type class laws and the [ScalaCheck](https://github.com/rickynils/scalacheck) tests based on them. From ef8b6e9876ae45a7fb4330a9523b7b2bab8346c3 Mon Sep 17 00:00:00 2001 From: Justin Reardon Date: Mon, 26 Oct 2026 17:43:37 -0400 Subject: [PATCH 5/8] Fix CONTRIBUTING.md --- CONTRIBUTING.md | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c135b5dcde..4e7ade7bcb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,29 +22,14 @@ follow up with any polishing that may need to be done. If you want to touch up some documentation or fix typos, feel free to skip these steps and jump straight to submitting a pull request. -- [Contributor Guide](#contributor-guide) - - [About this document](#about-this-document) - - [How can I help?](#how-can-i-help) - - [Find something that belongs in Cats](#find-something-that-belongs-in-cats) - - [Cats subprojects](#cats-subprojects) - - [Let us know you are working on it](#let-us-know-you-are-working-on-it) - - [Build the project](#build-the-project) - - [Scala and Scala.js](#scala-and-scalajs) - - [Editor Setup Tips](#editor-setup-tips) - - [Write code](#write-code) - - [Attributions](#attributions) - - [Write tests](#write-tests) - - [Binary compatibility](#binary-compatibility) - - [Contributing documentation](#contributing-documentation) - - [source for the documentation](#source-for-the-documentation) - - [Generating the Site](#generating-the-site) - - [Previewing the site](#previewing-the-site) - - [Compiler verified documentation](#compiler-verified-documentation) - - [Write examples](#write-examples) - - [Submit a pull request](#submit-a-pull-request) - - [How did we do?](#how-did-we-do) - - [Getting in touch](#getting-in-touch) - - [Nix Cats Development Environment](#nix-cats-development-environment) + 1. [Find something that belongs in cats](#find-something-that-belongs-in-cats) + 2. [Let us know you are working on it](#let-us-know-you-are-working-on-it) + 3. [Build the project](#build-the-project) + 4. [Implement your contribution](#write-code) + 5. [Write tests](#write-tests) + 6. [Write documentation](#contributing-documentation) + 7. [Write examples](#write-examples) + 8. [Submit pull request](#submit-a-pull-request) ### Find something that belongs in Cats From 3622567b3e69ebc79c479b19c21e695124d331c7 Mon Sep 17 00:00:00 2001 From: Justin Reardon Date: Tue, 27 Oct 2026 08:09:16 -0400 Subject: [PATCH 6/8] docs: Fix dangling paren --- docs/monadtransformers/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/monadtransformers/index.md b/docs/monadtransformers/index.md index e1452b91cf..a1cc5af22f 100644 --- a/docs/monadtransformers/index.md +++ b/docs/monadtransformers/index.md @@ -10,7 +10,7 @@ import cats.data.OptionT An instance of [`OptionT[F, A]`](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 `IO` from [Cats Effect](https://typelevel.org/cats-effect), or something like [doobie](https://typelevel.org/doobie/index.html)'s `ConnectionIO`). +Most typically, your `F` will be `IO` from [Cats Effect](https://typelevel.org/cats-effect), or something like [doobie](https://typelevel.org/doobie/index.html)'s `ConnectionIO`. 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`. From 0a2f3f259fbbd102bc1894ea38d460ba3bd86f39 Mon Sep 17 00:00:00 2001 From: Justin Reardon Date: Tue, 27 Oct 2026 08:09:33 -0400 Subject: [PATCH 7/8] Move some old jumpstart guide content to EitherT doc - Include part of the section on right bias - Include section on OptionT -> EitherT - move .value section up higher --- docs/monadtransformers/eithert.md | 49 ++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/docs/monadtransformers/eithert.md b/docs/monadtransformers/eithert.md index 569f69eb7a..09b479a70e 100644 --- a/docs/monadtransformers/eithert.md +++ b/docs/monadtransformers/eithert.md @@ -91,6 +91,20 @@ divisionProgramAsync("a", "b").value Note that when `F` is a monad, then `EitherT` will also form a monad, allowing monadic combinators such as `flatMap` to be used in composing `EitherT` values. +## Extracting an `F[Either[A, B]]` from an `EitherT[F, A, B]` + +Use the `value` method defined on `EitherT` to retrieve the underlying `F[Either[A, B]]`: + +```scala mdoc:nest +val errorT: EitherT[Future, String, Int] = EitherT.leftT("foo") + +val error: Future[Either[String, Int]] = errorT.value +``` + +## Right Bias + +Like `Either`, `EitherT` is _right-biased_, therefore plain `map` actually deals with the `Right` value. 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`. + ## From `A` or `B` to `EitherT[F, A, B]` To obtain a left version or a right version of `EitherT` when given an `A` or a @@ -146,6 +160,31 @@ val myOptionListET = EitherT.fromOptionF(myOptionList, "option not defined") val myOptionListETM = EitherT.fromOptionM(myOptionList, List("option not defined")) ``` +## From `OptionT[F, A] to `EitherT[F, A, B]` + +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): Future[Either[BaseException, User]] = + OptionT(getUserById(userId)) + .toRight(left = UserNotFoundException(s"user not found, userId=$userId")) + .value +``` + +`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. + ## From `ApplicativeError[F, E]` to `EitherT[F, E, A]` An `ApplicativeError[F, E]` or a `MonadError[F, E]`, can be converted into `EitherT[F, E, A]` @@ -158,13 +197,3 @@ val myFuture: Future[String] = Future.failed(new Exception()) val myTryET: EitherT[Try, Throwable, Int] = myTry.attemptT val myFutureET: EitherT[Future, Throwable, String] = myFuture.attemptT ``` - -## Extracting an `F[Either[A, B]]` from an `EitherT[F, A, B]` - -Use the `value` method defined on `EitherT` to retrieve the underlying `F[Either[A, B]]`: - -```scala mdoc:nest -val errorT: EitherT[Future, String, Int] = EitherT.leftT("foo") - -val error: Future[Either[String, Int]] = errorT.value -``` From 94de1ce58096b516b0da1d162e9df3330c568640 Mon Sep 17 00:00:00 2001 From: Justin Reardon Date: Tue, 27 Oct 2026 08:21:46 -0400 Subject: [PATCH 8/8] Fix EitherT docs - fix mdoc errors - group tranforming sections to simplify titles --- docs/monadtransformers/eithert.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/monadtransformers/eithert.md b/docs/monadtransformers/eithert.md index 09b479a70e..d7eb6d0ad5 100644 --- a/docs/monadtransformers/eithert.md +++ b/docs/monadtransformers/eithert.md @@ -105,7 +105,9 @@ val error: Future[Either[String, Int]] = errorT.value Like `Either`, `EitherT` is _right-biased_, therefore plain `map` actually deals with the `Right` value. 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`. -## From `A` or `B` to `EitherT[F, A, B]` +## Transforming to `EitherT[F, A, B]` + +### From `A` or `B` To obtain a left version or a right version of `EitherT` when given an `A` or a `B`, use `EitherT.leftT` and `EitherT.rightT` (which is an alias for @@ -113,10 +115,10 @@ To obtain a left version or a right version of `EitherT` when given an `A` or a ```scala mdoc:silent val number: EitherT[Option, String, Int] = EitherT.rightT(5) -val error: EitherT[Option, String, Int] = EitherT.leftT("Not a number") +val numberError: EitherT[Option, String, Int] = EitherT.leftT("Not a number") ``` -## From `F[A]` or `F[B]` to `EitherT[F, A, B]` +### From `F[A]` or `F[B]` Similarly, use `EitherT.left` and `EitherT.right` to convert an `F[A]` or an `F[B]` into an `EitherT`. It is also possible to use `EitherT.liftF` as an alias for @@ -130,7 +132,7 @@ val number: EitherT[Option, String, Int] = EitherT.right(numberO) val error: EitherT[Option, String, Int] = EitherT.left(errorO) ``` -## From `Either[A, B]` or `F[Either[A, B]]` to `EitherT[F, A, B]` +### From `Either[A, B]` or `F[Either[A, B]]` Use `EitherT.fromEither` to lift a value of `Either[A, B]` into `EitherT[F, A, B]`. An `F[Either[A, B]]` can be converted into `EitherT` using the `EitherT` constructor. @@ -145,7 +147,7 @@ val errorET: EitherT[List, String, Int] = EitherT.fromEither(errorE) val numberFET: EitherT[List, String, Int] = EitherT(numberFE) ``` -## From `Option[B]` or `F[Option[B]]` to `EitherT[F, A, B]` +### From `Option[B]` or `F[Option[B]]` An `Option[B]` or an `F[Option[B]]`, along with a default value, can be passed to `EitherT.fromOption` and `EitherT.fromOptionF`, respectively, to produce an @@ -160,18 +162,20 @@ val myOptionListET = EitherT.fromOptionF(myOptionList, "option not defined") val myOptionListETM = EitherT.fromOptionM(myOptionList, List("option not defined")) ``` -## From `OptionT[F, A] to `EitherT[F, A, B]` +### From `OptionT[F, A]` Another useful way to construct an `EitherT` instance is to use `OptionT`'s methods `toLeft` and `toRight`: ```scala mdoc:silent +import cats.data.OptionT + abstract class BaseException(message: String) extends Exception(message) case class UserNotFoundException(message: String) extends BaseException(message) -def getUserById(userId: Int): Future[Option[User]] = { /* ... */ ??? } +def getUserById(userId: Int): Future[Option[String]] = { /* ... */ ??? } -def ensureUserExists(userId: Int): Future[Either[BaseException, User]] = +def ensureUserExists(userId: Int): Future[Either[BaseException, String]] = OptionT(getUserById(userId)) .toRight(left = UserNotFoundException(s"user not found, userId=$userId")) .value @@ -185,7 +189,7 @@ To provide the `left` value within the monad, there is corresponding `toRightF` 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. -## From `ApplicativeError[F, E]` to `EitherT[F, E, A]` +### From `ApplicativeError[F, E]` An `ApplicativeError[F, E]` or a `MonadError[F, E]`, can be converted into `EitherT[F, E, A]` using the `attemptT` method on them.