Skip to content

Commit c298cea

Browse files
committed
extract monad transformers to their own section
1 parent 55828c3 commit c298cea

File tree

17 files changed

+245
-231
lines changed

17 files changed

+245
-231
lines changed

docs/datatypes.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

docs/datatypes/directory.conf

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,18 @@ laika.title = Data Types
22
laika.navigationOrder = [
33
chain.md
44
const.md
5-
contt.md
65
either.md
7-
eithert.md
86
eval.md
97
freeapplicative.md
108
freemonad.md
119
functionk.md
1210
id.md
1311
ior.md
14-
iort.md
1512
kleisli.md
1613
nel.md
17-
nested.md
1814
oneand.md
19-
optiont.md
2015
state.md
2116
statet.md
2217
validated.md
2318
writer.md
24-
writert.md
2519
]

docs/directory.conf

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
laika.navigationOrder = [
22
index.md
3-
typeclasses.md
4-
datatypes.md
53
algebra.md
64
alleycats.md
75
motivations.md
@@ -17,4 +15,5 @@ laika.navigationOrder = [
1715
typelevelEcosystem.md
1816
typeclasses
1917
datatypes
18+
monadtransformers
2019
]

docs/faq.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ It's really common to have a `List` of values with types like `Option`, `Either`
6868

6969
## Where is ListT?
7070

71-
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:
71+
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:
7272

7373
```scala mdoc:reset:silent
7474
val l: Option[List[Int]] = Some(List(1, 2, 3, 4, 5))

docs/jump_start_guide.md

Lines changed: 0 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -216,205 +216,6 @@ def computeOverValue3: Future[Option[Int]] = valueOpt.flatTraverse(compute)
216216
```
217217
and that solves our problem for good.
218218

219-
220-
# Monad Transformers
221-
222-
## OptionT
223-
224-
```scala mdoc:silent
225-
import cats.data.OptionT
226-
```
227-
228-
An instance of [`OptionT[F, A]`](datatypes/optiont.md) can be thought of as a wrapper over `F[Option[A]]`
229-
which adds a couple of useful methods specific to nested types that aren't available in `F` or `Option` itself.
230-
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`).
231-
Wrappers such as `OptionT` are generally known as _monad transformers_.
232-
233-
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`.
234-
This can be done with rather verbose syntax like:
235-
```scala mdoc:silent
236-
lazy val resultFuture: Future[Option[Int]] = ???
237-
238-
def mappedResultFuture: Future[Option[String]] = resultFuture.map { maybeValue =>
239-
maybeValue.map { value =>
240-
// Do something with the value and return String
241-
???
242-
}
243-
}
244-
```
245-
246-
With the use of `OptionT`, this can be simplified as follows:
247-
248-
```scala mdoc:silent
249-
def mappedResultFuture2: OptionT[Future, String] = OptionT(resultFuture).map { value =>
250-
// Do something with the value and return String
251-
???
252-
}
253-
```
254-
255-
The above `map` will return a value of type `OptionT[Future, String]`.
256-
257-
To get the underlying `Future[Option[String]]` value, simply call `.value` on the `OptionT` instance.
258-
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.
259-
260-
There are several ways to construct an `OptionT` instance.
261-
The method headers in the table below are slightly simplified: the type parameters and type classes required by each method are skipped.
262-
263-
| Method | Takes | Returns |
264-
| :---: | :---: | :---: |
265-
| `OptionT.apply` or `OptionT(...)` | `F[Option[A]]` | `OptionT[F, A]` |
266-
| `OptionT.fromOption` | `Option[A]` | `OptionT[F, A]` |
267-
| `OptionT.liftF` | `F[A]` | `OptionT[F, A]` |
268-
| `OptionT.pure` | `A` | `OptionT[F, A]` |
269-
270-
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]`.
271-
The other methods, in turn, prove useful to set up `OptionT`-typed dummy values in unit tests.
272-
273-
We have already come across one of `OptionT`'s methods, namely `map`.
274-
There are several other methods available and they mostly differ by the signature of the function they accept as the parameter.
275-
As was the case with the previous table, the expected type classes are skipped.
276-
277-
| Method | Takes | Returns
278-
| :---: | :---: | :---: |
279-
| `map[B]` | `A => B` | `OptionT[F, B]` |
280-
| `subflatMap[B]` | `A => Option[B]` | `OptionT[F, B]` |
281-
| `semiflatMap[B]` | `A => F[B]` | `OptionT[F, B]` |
282-
| `flatMapF[B]` | `A => F[Option[B]]` | `OptionT[F, B]` |
283-
| `flatMap[B]` | `A => OptionT[F, B]` | `OptionT[F, B]` |
284-
285-
In practice, you're most likely to use `map` and `semiflatMap`.
286-
287-
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:
288-
289-
```scala mdoc:silent
290-
class Money { /* ... */ }
291-
292-
def findUserById(userId: Long): OptionT[Future, User] = { /* ... */ ??? }
293-
294-
def findAccountById(accountId: Long): OptionT[Future, Account] = { /* ... */ ??? }
295-
296-
def getReservedFundsForAccount(account: Account): OptionT[Future, Money] = { /* ... */ ??? }
297-
298-
def getReservedFundsForUser(userId: Long): OptionT[Future, Money] = for {
299-
user <- findUserById(userId)
300-
account <- findAccountById(user.accountId)
301-
funds <- getReservedFundsForAccount(account)
302-
} yield funds
303-
```
304-
305-
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`.
306-
Otherwise, if the result of all three calls contains `Some`, the final outcome will also contain `Some`.
307-
308-
309-
## EitherT
310-
311-
```scala mdoc:silent
312-
import cats.data.EitherT
313-
```
314-
315-
[`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.
316-
317-
Just as in the above section, I simplified the method headers, skipping type parameters or their context bounds and lower bounds.
318-
319-
Let's have a quick look at how to create an `EitherT` instance:
320-
321-
| Method | Takes | Returns |
322-
| :---: | :---: | :---: |
323-
| `EitherT.apply` or `EitherT(...)` | `F[Either[A, B]]` | `EitherT[F, A, B]` |
324-
| `EitherT.fromEither` | `Either[A, B]` | `EitherT[F, A, B]` (wraps the provided `Either` value into `F`) |
325-
| `EitherT.right` or `EitherT.liftF` | `F[B]` | `EitherT[F, A, B]` (wraps value inside `F[B]` into `Right`) |
326-
| `EitherT.left` | `F[A]` | `EitherT[F, A, B]` (wraps value inside `F[B]` into `Left`) |
327-
| `EitherT.pure` | `A` | `EitherT[F, A, B]` (wraps value into `Right` and then into `F`) |
328-
329-
Another useful way to construct an `EitherT` instance is to use `OptionT`'s methods `toLeft` and `toRight`:
330-
331-
```scala mdoc:silent
332-
abstract class BaseException(message: String) extends Exception(message)
333-
334-
case class UserNotFoundException(message: String) extends BaseException(message)
335-
336-
def getUserById(userId: Int): Future[Option[User]] = { /* ... */ ??? }
337-
338-
def ensureUserExists(userId: Int): EitherT[Future, BaseException, User] = {
339-
OptionT(getUserById(userId))
340-
.toRight(left = UserNotFoundException(s"user not found, userId=$userId"))
341-
}
342-
```
343-
344-
`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`.
345-
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`.
346-
To provide the `left` value within the monad, there is corresponding `toRightF` method.
347-
348-
`toLeft` is `toRight`'s counterpart which wraps the `Some` value into `Left` and transforms `None` into `Right` enclosing the provided `right` value.
349-
This is less commonly used in practice, but can serve e.g. for enforcing uniqueness checks in code.
350-
We return `Left` if the value has been found, and `Right` if it doesn't yet exist in the system.
351-
352-
The methods available in `EitherT` are pretty similar to those we've seen in `OptionT`, but there are some notable differences.
353-
354-
You might get into some confusion at first when it comes to e.g. `map`.
355-
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.
356-
This is slightly less obvious in case of `EitherT`: should it map over both `Left` and `Right` values, or only the `Right` value?
357-
358-
The answer is that `EitherT` is _right-biased_, therefore plain `map` actually deals with the `Right` value.
359-
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.
360-
361-
Having said that, let's take a quick look at the right-biased methods that `EitherT` offers:
362-
363-
| Method | Takes | Returns |
364-
| :---: | --- | --- |
365-
| `map[D]` | `B => D` | `EitherT[F, A, D]` |
366-
| `subflatMap[D]` | `B => Either[A, D]` | `EitherT[F, A, D]` |
367-
| `semiflatMap[D]` | `B => F[D]` | `EitherT[F, A, D]` |
368-
| `flatMapF[D]` | `B => F[Either[A, D]]` | `EitherT[F, A, D]` |
369-
| `flatMap[D]` | `B => EitherT[F, A, D]` | `EitherT[F, A, D]` |
370-
371-
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`.
372-
373-
`EitherT` is very useful for fail-fast chained verifications:
374-
375-
```scala mdoc:silent
376-
377-
case class Item(state: String)
378-
class ItemOrder { /* ... */ }
379-
380-
case class ItemNotFoundException(message: String) extends BaseException(message)
381-
case class InvalidItemStateException(message: String) extends BaseException(message)
382-
383-
def getItemById(itemId: Int): Future[Option[Item]] = { /* .. */ ??? }
384-
385-
def ensureItemExists(itemId: Int): EitherT[Future, BaseException, Item] = {
386-
OptionT(getItemById(itemId))
387-
.toRight(ItemNotFoundException(s"item not found, itemId = $itemId"))
388-
}
389-
390-
def ensureItemStateIs(actual: String, expected: String): EitherT[Future, BaseException, Unit] = {
391-
// Returns a Unit value wrapped into Right and then into Future if condition is true,
392-
// otherwise the provided exception wrapped into Left and then into Future.
393-
EitherT.cond(actual == expected, (), InvalidItemStateException(s"actual=$actual, expected=$expected"))
394-
}
395-
396-
def placeOrderForItem(userId: Int, itemId: Int, count: Int): Future[ItemOrder] = { /* ... */ ??? }
397-
398-
def buyItem(userId: Int, itemId: Int, count: Int): EitherT[Future, BaseException, ItemOrder] = {
399-
for {
400-
user <- ensureUserExists(userId)
401-
item <- ensureItemExists(itemId)
402-
_ <- ensureItemStateIs(item.state, "AVAILABLE_IN_STOCK")
403-
// EitherT.liftF is necessary to make EitherT[Future, BaseException, ItemOrder] out of Future[ItemOrder]
404-
placedOrder <- EitherT.liftF(placeOrderForItem(userId, itemId, count))
405-
} yield placedOrder
406-
}
407-
```
408-
409-
In the above example, we're running various checks against the item one by one.
410-
If any of the checks fails, the resulting `EitherT` will contain a `Left` value.
411-
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`.
412-
This is a fail-fast behavior: we're effectively stopping the `for` comprehension flow at the first `Left`-ish result.
413-
414-
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.
415-
416-
417-
418219
# Common issues
419220

420221
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._```.

docs/datatypes/contt.md renamed to docs/monadtransformers/contt.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def updateUser(persistToDatabase: User => Eval[UserUpdateResult])
3030
```
3131

3232
(Note: We will be using `Eval` throughout the examples on this page. If you are not
33-
familiar with `Eval`, it's worth reading [the Eval documentation](eval.md) first.)
33+
familiar with `Eval`, it's worth reading [the Eval documentation](../datatypes/eval.md) first.)
3434

3535
Our `updateUser` function takes in an existing user and some updates to perform.
3636
It sanitises the inputs and updates the user model, but it delegates the
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
laika.title = Monad Transformers
2+
laika.navigationOrder = [
3+
monadtransformers.md
4+
contt.md
5+
eithert.md
6+
iort.md
7+
nested.md
8+
optiont.md
9+
statet.md
10+
writert.md
11+
]

docs/datatypes/eithert.md renamed to docs/monadtransformers/eithert.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
API Documentation: @:api(cats.data.EitherT)
44

55
`Either` can be used for error handling in most situations. However, when
6-
`Either` is placed into effectful types such as `Option` or`Future`, a large
6+
`Either` is placed into effectful types such as `Option`, a large
77
amount of boilerplate is required to handle errors. For example, consider the
88
following program:
99

File renamed without changes.

0 commit comments

Comments
 (0)