Skip to content
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 10 commits into from
Sep 12, 2017
4 changes: 4 additions & 0 deletions docs/src/main/resources/microsite/data/menu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ options:
url: datatypes/optiont.html
menu_type: data

- title: EitherT
url: datatypes/eithert.html
menu_type: data

- title: State
url: datatypes/state.html
menu_type: data
Expand Down
160 changes: 160 additions & 0 deletions docs/src/main/tut/datatypes/eithert.md
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
import cats.syntax.either._
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We decide to use the uber import import cats.implicits._ in the documentation, see issue #1026.


def parseDouble(s: String): Either[String, Double] =
Try(s.toDouble).map(Right(_)).getOrElse(Left(s"$s is not a number"))
Copy link
Contributor

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 a Double. It might be best to leave it simple and just say "$s is not a valid double.


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)
Copy link
Collaborator

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.

Copy link
Contributor Author

@Technius Technius Aug 26, 2017

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.


parseResult match {
case Right((a, b)) => divideAsync(a, b)
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: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"))
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using tut:book, it shows

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, doesn't hurt to do an Await.result here.

```

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
```