From 98bca15dc4c433759d2612c1dfe0125646b94685 Mon Sep 17 00:00:00 2001 From: Bryan Tan Date: Fri, 25 Aug 2017 00:22:50 -0700 Subject: [PATCH 01/10] Add introduction to EitherT in docs --- .../main/resources/microsite/data/menu.yml | 4 + docs/src/main/tut/datatypes/eithert.md | 92 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 docs/src/main/tut/datatypes/eithert.md diff --git a/docs/src/main/resources/microsite/data/menu.yml b/docs/src/main/resources/microsite/data/menu.yml index 081dfffe2a..59b62aa7d5 100644 --- a/docs/src/main/resources/microsite/data/menu.yml +++ b/docs/src/main/resources/microsite/data/menu.yml @@ -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 diff --git a/docs/src/main/tut/datatypes/eithert.md b/docs/src/main/tut/datatypes/eithert.md new file mode 100644 index 0000000000..5fd45eed74 --- /dev/null +++ b/docs/src/main/tut/datatypes/eithert.md @@ -0,0 +1,92 @@ +--- +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 stringToDouble(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 <- stringToDouble(inputA) + b <- stringToDouble(inputB) + result <- divide(a, b) + } yield result + +divisionProgram("4", "2") +divisionProgram("a", "b") +``` + +Suppose `stringToDouble` 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. + +```tut:silent +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future + +def stringToDouble(s: String): Future[Either[String, Double]] = ??? +def divide(a: Double, b: Double): Future[Either[String, Double]] = ??? + +def divisionProgram(inputA: String, inputB: String): Future[Either[String, Double]] = + stringToDouble(inputA) flatMap { eitherA => + stringToDouble(inputB) flatMap { eitherB => + val parseResult = for { + a <- eitherA + b <- eitherB + } yield (a, b) + + parseResult match { + case Right((a, b)) => divide(a, b) + case l@Left(err) => Future.successful(Left(err)) + } + } + } +``` + +It is easy to see that as additional `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. Using `EitherT`, the asynchronous +division program can be rewritten as follows: + +```tut:silent +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future +import scala.util.Try +import cats.data.EitherT +import cats.implicits._ + +def stringToDouble(s: String): Future[Either[String, Double]] = ??? +def divide(a: Double, b: Double): Future[Either[String, Double]] = ??? + +def divisionProgram(inputA: String, inputB: String): EitherT[Future, String, Double] = + for { + a <- EitherT(stringToDouble(inputA)) + b <- EitherT(stringToDouble(inputB)) + result <- EitherT(divide(a, b)) + } yield result +``` From 1e38f84769a52b0f4abccf3e4213f3516e6f893b Mon Sep 17 00:00:00 2001 From: Bryan Tan Date: Fri, 25 Aug 2017 11:47:02 -0700 Subject: [PATCH 02/10] Finish writing EitherT docs --- docs/src/main/tut/datatypes/eithert.md | 130 +++++++++++++++++++------ 1 file changed, 99 insertions(+), 31 deletions(-) diff --git a/docs/src/main/tut/datatypes/eithert.md b/docs/src/main/tut/datatypes/eithert.md index 5fd45eed74..7c2f600a13 100644 --- a/docs/src/main/tut/datatypes/eithert.md +++ b/docs/src/main/tut/datatypes/eithert.md @@ -15,78 +15,146 @@ following program: ```tut:book import scala.util.Try -def stringToDouble(s: String): Either[String, Double] = +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") + Either.cond(b != 0, a / b, "Cannot divide by zero") def divisionProgram(inputA: String, inputB: String): Either[String, Double] = for { - a <- stringToDouble(inputA) - b <- stringToDouble(inputB) + a <- parseDouble(inputA) + b <- parseDouble(inputB) result <- divide(a, b) } yield result -divisionProgram("4", "2") -divisionProgram("a", "b") +divisionProgram("4", "2") // Right(2.0) +divisionProgram("a", "b") // Left("a is not a number") ``` -Suppose `stringToDouble` 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. +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 stringToDouble(s: String): Future[Either[String, Double]] = ??? -def divide(a: Double, b: Double): Future[Either[String, Double]] = ??? +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 divisionProgram(inputA: String, inputB: String): Future[Either[String, Double]] = - stringToDouble(inputA) flatMap { eitherA => - stringToDouble(inputB) flatMap { eitherB => +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)) => divide(a, b) + case Right((a, b)) => divideAsync(a, b) case l@Left(err) => Future.successful(Left(err)) } } } ``` -It is easy to see that as additional `Either`s and `Futures` are included, the -amount of boilerplate required to properly handle the errors will increase -dramatically. +Clearly, the updated code is less readible 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. Using `EitherT`, the asynchronous -division program can be rewritten as follows: +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 scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future -import scala.util.Try import cats.data.EitherT import cats.implicits._ -def stringToDouble(s: String): Future[Either[String, Double]] = ??? -def divide(a: Double, b: Double): Future[Either[String, Double]] = ??? - -def divisionProgram(inputA: String, inputB: String): EitherT[Future, String, Double] = +def divisionProgramAsync(inputA: String, inputB: String): EitherT[Future, String, Double] = for { - a <- EitherT(stringToDouble(inputA)) - b <- EitherT(stringToDouble(inputB)) - result <- EitherT(divide(a, b)) + 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")) +``` + +Note that since `EitherT` is a monad, monadic combinators such as `flatMap` can +be used to compose `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 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") + +val error: Option[Either[String, Int]] = errorT.value ``` From ca43a0d94545ad46b92b9ce2964f5fdb4bcf563c Mon Sep 17 00:00:00 2001 From: Bryan Tan Date: Fri, 25 Aug 2017 12:07:20 -0700 Subject: [PATCH 03/10] Oops, missed a sentence and an example --- docs/src/main/tut/datatypes/eithert.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/src/main/tut/datatypes/eithert.md b/docs/src/main/tut/datatypes/eithert.md index 7c2f600a13..3c188701c4 100644 --- a/docs/src/main/tut/datatypes/eithert.md +++ b/docs/src/main/tut/datatypes/eithert.md @@ -125,14 +125,16 @@ 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 +A `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]` From ed3ee975eb24e7f06d288e776c5232f62231bb48 Mon Sep 17 00:00:00 2001 From: Bryan Tan Date: Fri, 25 Aug 2017 12:09:12 -0700 Subject: [PATCH 04/10] Add missing comma --- docs/src/main/tut/datatypes/eithert.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/tut/datatypes/eithert.md b/docs/src/main/tut/datatypes/eithert.md index 3c188701c4..6acfd7fe56 100644 --- a/docs/src/main/tut/datatypes/eithert.md +++ b/docs/src/main/tut/datatypes/eithert.md @@ -108,7 +108,7 @@ 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]` +## From `F[A]` or `F[B]` to `EitherT[F, A, B]` 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 From 1282a74eb05f53b989387aa15a6a70191a2dd22d Mon Sep 17 00:00:00 2001 From: Bryan Tan Date: Fri, 25 Aug 2017 14:02:34 -0700 Subject: [PATCH 05/10] Fix crossbuild issues, a typo, and consistency --- docs/src/main/tut/datatypes/eithert.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/src/main/tut/datatypes/eithert.md b/docs/src/main/tut/datatypes/eithert.md index 6acfd7fe56..19d98338cd 100644 --- a/docs/src/main/tut/datatypes/eithert.md +++ b/docs/src/main/tut/datatypes/eithert.md @@ -14,12 +14,10 @@ following program: ```tut:book import scala.util.Try +import cats.syntax.either._ 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) - } + 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") @@ -66,7 +64,7 @@ def divisionProgramAsync(inputA: String, inputB: String): Future[Either[String, } ``` -Clearly, the updated code is less readible and more verbose: the details of the +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. @@ -94,8 +92,8 @@ divisionProgramAsync("4", "2").value // Future(Right(2.0)) divisionProgramAsync("a", "b").value // Future(Left("a is not a number")) ``` -Note that since `EitherT` is a monad, monadic combinators such as `flatMap` can -be used to compose `EitherT` values. +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]` @@ -110,7 +108,7 @@ 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 a `F[A]` or a `F[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`. @@ -125,7 +123,7 @@ 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 converted into `EitherT` using the `EitherT` constructor. +An `F[Either[A, B]]` can be converted into `EitherT` using the `EitherT` constructor. ```tut:silent val numberE: Either[String, Int] = Right(100) @@ -139,7 +137,7 @@ val numberFET: EitherT[List, String, Int] = EitherT(numberFE) ## 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 +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`. From f4893994044810b3692a87afd29b7b5424d2b247 Mon Sep 17 00:00:00 2001 From: Bryan Tan Date: Fri, 25 Aug 2017 14:04:41 -0700 Subject: [PATCH 06/10] Switch Option to Future in the extraction example --- docs/src/main/tut/datatypes/eithert.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/main/tut/datatypes/eithert.md b/docs/src/main/tut/datatypes/eithert.md index 19d98338cd..bdbdf4b9fb 100644 --- a/docs/src/main/tut/datatypes/eithert.md +++ b/docs/src/main/tut/datatypes/eithert.md @@ -154,7 +154,7 @@ val myOptionListET = EitherT.fromOptionF(myOptionList, "option not defined") 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") +val errorT: EitherT[Future, String, Int] = EitherT.leftT("foo") -val error: Option[Either[String, Int]] = errorT.value +val error: Option[Future[String, Int]] = errorT.value ``` From 1a30df7a8f7b243ad8831cb8d50bb73f04d4a391 Mon Sep 17 00:00:00 2001 From: Bryan Tan Date: Fri, 25 Aug 2017 14:06:20 -0700 Subject: [PATCH 07/10] Switch Option to Future for real this time --- docs/src/main/tut/datatypes/eithert.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/tut/datatypes/eithert.md b/docs/src/main/tut/datatypes/eithert.md index bdbdf4b9fb..6966a2e980 100644 --- a/docs/src/main/tut/datatypes/eithert.md +++ b/docs/src/main/tut/datatypes/eithert.md @@ -156,5 +156,5 @@ Use the `value` method defined on `EitherT` to retrieve the underlying `F[Either ```tut:book val errorT: EitherT[Future, String, Int] = EitherT.leftT("foo") -val error: Option[Future[String, Int]] = errorT.value +val error: Future[Either[String, Int]] = errorT.value ``` From 7c35e58cab5fdec85b3641dee467fdf58f4b95b1 Mon Sep 17 00:00:00 2001 From: Bryan Tan Date: Sat, 26 Aug 2017 11:16:42 -0700 Subject: [PATCH 08/10] Update based on feedback --- docs/src/main/tut/datatypes/eithert.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/docs/src/main/tut/datatypes/eithert.md b/docs/src/main/tut/datatypes/eithert.md index 6966a2e980..387bc3a251 100644 --- a/docs/src/main/tut/datatypes/eithert.md +++ b/docs/src/main/tut/datatypes/eithert.md @@ -14,7 +14,7 @@ following program: ```tut:book import scala.util.Try -import cats.syntax.either._ +import cats.implicits._ def parseDouble(s: String): Either[String, Double] = Try(s.toDouble).map(Right(_)).getOrElse(Left(s"$s is not a number")) @@ -51,14 +51,10 @@ def divideAsync(a: Double, b: Double): Future[Either[String, Double]] = 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)) + (eitherA, eitherB) match { + case (Right(a), Right(b)) => divideAsync(a, b) + case (l@Left(err), _) => Future.successful(Left(err)) + case (_, l@Left(err)) => Future.successful(Left(err)) } } } @@ -77,7 +73,7 @@ it easy to compose `Either`s and `F`s together. To use `EitherT`, values of resulting `EitherT` values are then composed using combinators. For example, the asynchronous division program can be rewritten as follows: -```tut:silent +```tut:book import cats.data.EitherT import cats.implicits._ @@ -88,8 +84,8 @@ def divisionProgramAsync(inputA: String, inputB: String): EitherT[Future, String 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")) +divisionProgramAsync("4", "2").value +divisionProgramAsync("a", "b").value ``` Note that when `F` is a monad, then `EitherT` will also form a monad, allowing From 29ab953d123477d421c79ec9908cfe7a1ea34355 Mon Sep 17 00:00:00 2001 From: Bryan Tan Date: Sat, 26 Aug 2017 11:26:47 -0700 Subject: [PATCH 09/10] Remove unused bindings in pattern match --- docs/src/main/tut/datatypes/eithert.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/main/tut/datatypes/eithert.md b/docs/src/main/tut/datatypes/eithert.md index 387bc3a251..8a83b39ed1 100644 --- a/docs/src/main/tut/datatypes/eithert.md +++ b/docs/src/main/tut/datatypes/eithert.md @@ -37,7 +37,7 @@ 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: +the proOh, you're right. I jper types are returned: ```tut:silent import scala.concurrent.ExecutionContext.Implicits.global @@ -53,8 +53,8 @@ def divisionProgramAsync(inputA: String, inputB: String): Future[Either[String, parseDoubleAsync(inputB) flatMap { eitherB => (eitherA, eitherB) match { case (Right(a), Right(b)) => divideAsync(a, b) - case (l@Left(err), _) => Future.successful(Left(err)) - case (_, l@Left(err)) => Future.successful(Left(err)) + case (Left(err), _) => Future.successful(Left(err)) + case (_, Left(err)) => Future.successful(Left(err)) } } } From 5e6c4084cc1e7e6a617027e3aff103555bcb15b0 Mon Sep 17 00:00:00 2001 From: Bryan Tan Date: Sat, 26 Aug 2017 11:31:53 -0700 Subject: [PATCH 10/10] My editor likes to randomly paste text --- docs/src/main/tut/datatypes/eithert.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/main/tut/datatypes/eithert.md b/docs/src/main/tut/datatypes/eithert.md index 8a83b39ed1..7d2af23966 100644 --- a/docs/src/main/tut/datatypes/eithert.md +++ b/docs/src/main/tut/datatypes/eithert.md @@ -37,7 +37,7 @@ 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 proOh, you're right. I jper types are returned: +the proper types are returned: ```tut:silent import scala.concurrent.ExecutionContext.Implicits.global