-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Add EitherT docs #1854
Merged
Merged
Add EitherT docs #1854
Changes from 8 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
98bca15
Add introduction to EitherT in docs
Technius 1e38f84
Finish writing EitherT docs
Technius ca43a0d
Oops, missed a sentence and an example
Technius ed3ee97
Add missing comma
Technius 1282a74
Fix crossbuild issues, a typo, and consistency
Technius f489399
Switch Option to Future in the extraction example
Technius 1a30df7
Switch Option to Future for real this time
Technius 7c35e58
Update based on feedback
Technius 29ab953
Remove unused bindings in pattern match
Technius 5e6c408
My editor likes to randomly paste text
Technius File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
--- | ||
layout: docs | ||
title: "EitherT" | ||
section: "data" | ||
source: "core/src/main/scala/cats/data/EitherT.scala" | ||
scaladoc: "#cats.data.EitherT" | ||
--- | ||
# 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 | ||
amount of boilerplate is required to handle errors. For example, consider the | ||
following program: | ||
|
||
```tut:book | ||
import scala.util.Try | ||
import cats.implicits._ | ||
|
||
def parseDouble(s: String): Either[String, Double] = | ||
Try(s.toDouble).map(Right(_)).getOrElse(Left(s"$s is not a number")) | ||
|
||
def divide(a: Double, b: Double): Either[String, Double] = | ||
Either.cond(b != 0, a / b, "Cannot divide by zero") | ||
|
||
def divisionProgram(inputA: String, inputB: String): Either[String, Double] = | ||
for { | ||
a <- parseDouble(inputA) | ||
b <- parseDouble(inputB) | ||
result <- divide(a, b) | ||
} yield result | ||
|
||
divisionProgram("4", "2") // Right(2.0) | ||
divisionProgram("a", "b") // Left("a is not a number") | ||
``` | ||
|
||
Suppose `parseDouble` and `divide` are rewritten to be asynchronous and return | ||
`Future[Either[String, Double]]` instead. The for-comprehension can no longer be | ||
used since `divisionProgram` must now compose `Future` and `Either` together, | ||
which means that the error handling must be performed explicitly to ensure that | ||
the proper types are returned: | ||
|
||
```tut:silent | ||
import scala.concurrent.ExecutionContext.Implicits.global | ||
import scala.concurrent.Future | ||
|
||
def parseDoubleAsync(s: String): Future[Either[String, Double]] = | ||
Future.successful(parseDouble(s)) | ||
def divideAsync(a: Double, b: Double): Future[Either[String, Double]] = | ||
Future.successful(divide(a, b)) | ||
|
||
def divisionProgramAsync(inputA: String, inputB: String): Future[Either[String, Double]] = | ||
parseDoubleAsync(inputA) flatMap { eitherA => | ||
parseDoubleAsync(inputB) flatMap { eitherB => | ||
(eitherA, eitherB) match { | ||
case (Right(a), Right(b)) => divideAsync(a, b) | ||
case (l@Left(err), _) => Future.successful(Left(err)) | ||
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.
|
||
case (_, l@Left(err)) => Future.successful(Left(err)) | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Clearly, the updated code is less readable and more verbose: the details of the | ||
program are now mixed with the error handling. In addition, as more `Either`s | ||
and `Futures` are included, the amount of boilerplate required to properly | ||
handle the errors will increase dramatically. | ||
|
||
## EitherT | ||
|
||
`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: | ||
|
||
```tut:book | ||
import cats.data.EitherT | ||
import cats.implicits._ | ||
|
||
def divisionProgramAsync(inputA: String, inputB: String): EitherT[Future, String, Double] = | ||
for { | ||
a <- EitherT(parseDoubleAsync(inputA)) | ||
b <- EitherT(parseDoubleAsync(inputB)) | ||
result <- EitherT(divideAsync(a, b)) | ||
} yield result | ||
|
||
divisionProgramAsync("4", "2").value | ||
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. | ||
|
||
## 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 | ||
`B`, use `EitherT.leftT` and `EitherT.rightT` (which is an alias for | ||
`EitherT.pure`), respectively. | ||
|
||
```tut:silent | ||
val number: EitherT[Option, String, Int] = EitherT.rightT(5) | ||
val error: EitherT[Option, String, Int] = EitherT.leftT("Not a number") | ||
``` | ||
|
||
## From `F[A]` or `F[B]` to `EitherT[F, A, B]` | ||
|
||
Similary, 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.liftT` as an alias for | ||
`EitherT.right`. | ||
|
||
```tut:silent | ||
val numberO: Option[Int] = Some(5) | ||
val errorO: Option[String] = Some("Not a number") | ||
|
||
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]` | ||
|
||
Use `EitherT.fromEither` to a 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. | ||
|
||
```tut:silent | ||
val numberE: Either[String, Int] = Right(100) | ||
val errorE: Either[String, Int] = Left("Not a number") | ||
val numberFE: List[Either[String, Int]] = List(Right(250)) | ||
|
||
val numberET: EitherT[List, String, Int] = EitherT.fromEither(numberE) | ||
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]` | ||
|
||
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 | ||
`EitherT`. | ||
|
||
```tut:book | ||
val myOption: Option[Int] = None | ||
val myOptionList: List[Option[Int]] = List(None, Some(2), Some(3), None, Some(5)) | ||
|
||
val myOptionET = EitherT.fromOption[Future](myOption, "option not defined") | ||
val myOptionListET = EitherT.fromOptionF(myOptionList, "option not defined") | ||
``` | ||
|
||
## 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]]`: | ||
|
||
```tut:book | ||
val errorT: EitherT[Future, String, Int] = EitherT.leftT("foo") | ||
|
||
val error: Future[Either[String, Int]] = errorT.value | ||
``` |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super nitpicky and unreasonable comment:
s
could still technically be a number that can't be represented as aDouble
. It might be best to leave it simple and just say "$s is not a valid double
.