-
-
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
Add EitherT docs #1854
Changes from 2 commits
98bca15
1e38f84
ca43a0d
ed3ee97
1282a74
f489399
1a30df7
7c35e58
29ab953
5e6c408
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
--- | ||
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 | ||
|
||
def parseDouble(s: String): Either[String, Double] = | ||
Try(s.toDouble).toEither match { | ||
case Left(_) => Left(s"$s is not a number") | ||
case Right(n) => Right(n) | ||
} | ||
|
||
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 => | ||
val parseResult = for { | ||
a <- eitherA | ||
b <- eitherB | ||
} yield (a, b) | ||
|
||
parseResult match { | ||
case Right((a, b)) => divideAsync(a, b) | ||
case l@Left(err) => Future.successful(Left(err)) | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Clearly, the updated code is less readible and more verbose: the details of the | ||
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. "readable" |
||
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:silent | ||
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 // Future(Right(2.0)) | ||
divisionProgramAsync("a", "b").value // Future(Left("a is not a number")) | ||
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. If you want to show the output, we should move these comments to a "non silent tut block", that way nobody can forget to update the comments if the code is changed. 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. Using divisionProgramAsync("4", "2").value
// res4: scala.concurrent.Future[Either[String,Double]] = Future(<not completed>)
divisionProgramAsync("a", "b").value
// res5: scala.concurrent.Future[Either[String,Double]] = Future(<not completed>) which is why I opted to use the comments. 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. IMO, doesn't hurt to do an |
||
``` | ||
|
||
Note that since `EitherT` is a monad, monadic combinators such as `flatMap` can | ||
be used to compose `EitherT` values. | ||
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. Maybe clarify here, that |
||
|
||
## 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]` | ||
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. Missing a comma in |
||
|
||
Similary, use `EitherT.left` and `EitherT.right` to convert a `F[A]` or a `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]`. | ||
A `F[Either[A, B]]` can be | ||
|
||
```tut:silent | ||
val numberE: Either[String, Int] = Right(100) | ||
val errorE: Either[String, Int] = Left("Not a number") | ||
|
||
val numberET: EitherT[List, String, Int] = EitherT.fromEither(numberE) | ||
val errorET: EitherT[List, String, Int] = EitherT.fromEither(errorE) | ||
``` | ||
|
||
## From `Option[B]` or `F[Option[B]]` to `EitherT[F, A, B]` | ||
|
||
An `Option[B]` or a `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[Option, String, Int] = EitherT.leftT("foo") | ||
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. I know it's just an example, but an 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. Yeah, that's much better actually. |
||
|
||
val error: Option[Either[String, Int]] = errorT.value | ||
``` |
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.
You could write
(eitherA, eitherB).tupled
.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.
If I were a new user, the applicative syntax wouldn't be well known to me, so I'll just replace the
parseResult
snippet with a pattern match on the eithers.