Repository navigation
Documentation refresh for monad transformers #4724
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
Changes from 5 commits
6d4a0e0
a603310
56fa9de
344d6ed
ef8b6e9
3622567
0a2f3f2
94de1ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,25 +1,19 @@ | ||
| laika.title = Data Types | ||
| laika.navigationOrder = [ | ||
| index.md | ||
| 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 | ||
| ] | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @reardonj , it looks like while the Is that correct? Was it intentional or just occasionally missed out?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At this point, I can't say I recall what my intent was. I suspect it was that Looking over that old content again, I see the EitherT doc is missing some sections (right bias, OptionT transforms). I've pulled that in. |
||
|
|
||
| ```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._```. | ||
|
|
||
| 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 | ||
| ] |
Uh oh!
There was an error while loading. Please reload this page.