Skip to content

Instantly share code, notes, and snippets.

@friedbrice
Last active September 28, 2017 20:36
Show Gist options
  • Save friedbrice/7b18bf50573ea27f27969b5b625db952 to your computer and use it in GitHub Desktop.
Save friedbrice/7b18bf50573ea27f27969b5b625db952 to your computer and use it in GitHub Desktop.
Scala Error Handling with Either
import scala.collection.generic.CanBuildFrom
import scala.collection.{TraversableLike, mutable}
import scala.reflect.ClassTag
object EitherMonad {
trait Error[E] extends ClassTag[E] {
def getDefault: E = fromMessage("")
def fromMessage(msg: String): E
def fromThrowable(err: Throwable): E = fromMessage(err.toString)
def toThrowable(e: E): Throwable = new Exception(e.toString)
final override def runtimeClass: java.lang.Class[_] = getDefault.getClass
}
object Error {
def getDefault[E: Error]: E =
implicitly[Error[E]].getDefault
def fromMessage[E: Error](msg: String): E =
implicitly[Error[E]].fromMessage(msg)
def fromThrowable[E: Error](err: Throwable): E =
implicitly[Error[E]].fromThrowable(err)
def toThrowable[E: Error](e: E): Throwable =
implicitly[Error[E]].toThrowable(e)
object Instances {
implicit lazy val errorString: Error[String] = new Error[String] {
def fromMessage(msg: String): String = msg
}
implicit lazy val errorThrowable: Error[Throwable] = new Error[Throwable] {
def fromMessage(msg: String): Throwable = new Throwable(msg)
override def getDefault: Throwable = new Throwable
override def fromThrowable(err: Throwable): Throwable = err
override def toThrowable(e: Throwable): Throwable = e
}
implicit lazy val errorException: Error[Exception] = new Error[Exception] {
def fromMessage(msg: String): Exception = new Exception(msg)
override def getDefault: Exception = new Exception
override def toThrowable(e: Exception): Throwable = e
@throws[Throwable]("Will rethrow any non-Exception Throwable.")
override def fromThrowable(err: Throwable): Exception =
err match { case e: Exception => e; case _ => throw err }
}
}
}
def safely[E: Error, A](a: => A, alt: => Any): Either[E, A] =
safelyPrimitive(a).left.map(_ => coerceAlt(alt))
def safely[E: Error, A](a: => A): Either[E, A] =
safelyPrimitive(a).left.map(e => Error.fromThrowable(e))
def ensure[E: Error](p: Boolean, alt: => Any): Either[E, Unit] =
ensurePrimitive(p).left.map(_ => coerceAlt(alt))
def ensure[E: Error](p: Boolean): Either[E, Unit] =
ensurePrimitive(p).left.map(_ => Error.getDefault)
def failure[E: Error, A](alt: Any): Either[E, A] =
Left(coerceAlt(alt))
def failure[E: Error, A]: Either[E, A] =
Left(Error.getDefault)
implicit class EitherMonadInstance[E, A](self: Either[E, A]) {
def get: Option[A] =
self.fold(_ => None, Option(_))
def getError: Option[E] =
self.fold(Option(_), _ => None)
def getOrElse(a: A): A =
self.fold(_ => a, identity)
@throws[Throwable]("Throws if instance is a Left Either.")
def getOrThrow(implicit ev: Error[E]): A =
self.fold(e => throw Error.toThrowable(e), identity)
def foreach(f: A => Unit): Unit =
self.fold(_ => {}, a => f(a))
def map[B](f: A => B): Either[E, B] =
self.fold(e => Left(e), a => Right(f(a)))
def flatMap[B](k: A => Either[E, B]): Either[E, B] =
self.fold(e => Left(e), a => k(a))
def filter(p: A => Boolean)(implicit ev: Error[E]): Either[E, A] =
flatMap(a => if (p(a)) Right(a) else failure)
def withFilter(p: A => Boolean)(implicit ev: Error[E]): Either[E, A] =
filter(p)
def ap[B](ef: Either[E, A => B]): Either[E, B] =
for {f <- ef; a <- self} yield f(a)
def flatten[B](implicit ev: A <:< Either[E, B]): Either[E, B] =
flatMap(ev)
def and[B](eb: => Either[E, B]): Either[E, B] =
flatMap(_ => eb)
def or[B >: A](eb: => Either[E, B]): Either[E, B] =
recover(_ => eb)
def recover[B >: A](f: E => Either[E, B]): Either[E, B] =
self.fold(err => f(err), Right(_))
def mapLeft[F](f: E => F): Either[F, A] =
self.fold(e => Left(f(e)), Right(_))
}
def safe[E: Error, A, X](f: A => X): A => Either[E, X] =
a => safely(f(a))
def safe2[E: Error, A, B, X](f: (A, B) => X):
(A, B) => Either[E, X] =
(a, b) => safely(f(a, b))
def safe3[E: Error, A, B, C, X](f: (A, B, C) => X):
(A, B, C) => Either[E, X] =
(a, b, c) => safely(f(a, b, c))
def safe4[E: Error, A, B, C, D, X](f: (A, B, C, D) => X):
(A, B, C, D) => Either[E, X] =
(a, b, c, d) => safely(f(a, b, c, d))
def lift[E, A, X](f: A => X): Either[E, A] => Either[E, X] =
_.map(f)
def lift2[E, A, B, X](f: (A, B) => X):
(Either[E, A], Either[E, B]) => Either[E, X] =
(ea, eb) =>
for { a <- ea; b <- eb }
yield f(a, b)
def lift3[E, A, B, C, X](f: (A, B, C) => X):
(Either[E, A], Either[E, B], Either[E, C]) => Either[E, X] =
(ea, eb, ec) =>
for { a <- ea; b <- eb; c <- ec }
yield f(a, b, c)
def lift4[E, A, B, C, D, X](f: (A, B, C, D) => X):
(Either[E, A], Either[E, B], Either[E, C], Either[E, D]) => Either[E, X] =
(ea, eb, ec, ed) =>
for { a <- ea; b <- eb; c <- ec; d <- ed }
yield f(a, b, c, d)
def bind[E, A, X](k: A => Either[E, X]): Either[E, A] => Either[E, X] =
_.flatMap(k)
def bind2[E, A, B, X](k: (A, B) => Either[E, X]):
(Either[E, A], Either[E, B]) => Either[E, X] =
(ea, eb) =>
for { a <- ea; b <- eb; res <- k(a,b) }
yield res
def bind3[E, A, B, C, X](k: (A, B, C) => Either[E, X]):
(Either[E, A], Either[E, B], Either[E, C]) => Either[E, X] =
(ea, eb, ec) =>
for { a <- ea; b <- eb; c <- ec; res <- k(a, b, c) }
yield res
def bind4[E, A, B, C, D, X](k: (A, B, C, D) => Either[E, X]):
(Either[E, A], Either[E, B], Either[E, C], Either[E, D]) => Either[E, X] =
(ea, eb, ec, ed) =>
for { a <- ea; b <- eb; c <- ec; d <- ed; res <- k(a, b, c, d) }
yield res
def traverse[E, A, TA, B, TB](ta: TraversableLike[A, TA])
(k: A => Either[E, B])
(implicit bf: CanBuildFrom[TA, B, TB]):
Either[E, TB] = {
type Bldr = mutable.Builder[B, TB]
val init: Either[E, Bldr] =
{ val bldr = bf(ta.asInstanceOf[TA]); bldr.sizeHint(ta); Right(bldr) }
def step(acc: Either[E, Bldr], next: A): Either[E, Bldr] =
for { bldr <- acc; b <- k(next) } yield bldr += b
ta.foldLeft(init)(step).map(_.result)
}
def sequence[E, A, TEA, TA](tea: TraversableLike[Either[E, A], TEA])
(implicit bf: CanBuildFrom[TEA, A, TA]):
Either[E, TA] =
traverse(tea)(identity)
def successes[E, A, TEA, TA](tea: TraversableLike[Either[E, A], TEA])
(implicit bf: CanBuildFrom[TEA, A, TA]):
TA = {
type Bldr = mutable.Builder[A, TA]
val init: Bldr = bf(tea.asInstanceOf[TEA])
def step(bldr: Bldr, next: Either[E, A]): Bldr =
next.fold(_ => bldr, a => bldr += a)
tea.foldLeft(init)(step).result()
}
private val nullValueMsg: String = "EitherMonad.safely: value was null"
private def safelyPrimitive[A](a: => A): Either[Throwable, A] =
try {
val maybeNull = a // evaluating `a' might throw or might return `null'
if (maybeNull != null) Right(maybeNull)
else throw new NoSuchElementException(nullValueMsg)
} catch {
case scala.util.control.NonFatal(e) => Left(e)
}
private def ensurePrimitive[A](p: Boolean): Either[Unit, Unit] =
if (p) Right(()) else Left(())
private def coerceAlt[E: Error](alt: Any): E =
alt match {
case e: E => e
case msg: String => Error.fromMessage(msg)
case err: Throwable => Error.fromThrowable(err)
case _ => Error.fromMessage(alt.toString)
}
}
import org.scalatest.{FlatSpec, Matchers}
class EitherMonadTest extends FlatSpec with Matchers {
import EitherMonad._
// The EitherMonad interface provides automatic marshalling/unmarshalling
// combinators for functions and values that have the possibility of failure.
// Use whatever type for your errors.
case class Err(msg: String, code: Int)
// Define an implicit Error instance so that `safely', `ensure', and `failure' work.
implicit val errorErr: Error[Err] = new Error[Err] {
def fromMessage(msg: String): Err = Err(msg, msg.length)
}
// Alternatively, you can import (exactly) one of the default instances,
// `import Error.Instances.errorString' or
// `import Error.Instances.errorException' or
// `import Error.Instances.errorThrowable'. Warning: defining (or importing)
// more than one implicit Error instance will wreck type inference! In any
// given scope, you should only ever have one implicit Error instance visible.
// Here are some types for our business logic
class Foo
class Bar extends Foo
class Baz
// In what follows, I'm going to abuse the FlatSpec machinery when convenient.
"`failure'" should "be used to construct Left values" in {
// call `failure' without an argument
failure shouldBe Left(Error.getDefault)
// or call with a value of your chosen Error type
failure(Err("Date is prior to beginning of time.", 19691231)) shouldBe
Left(Err("Date is prior to beginning of time.", 19691231))
// or call with a custom message
failure("This is not an Err") shouldBe Left(Error.fromMessage("This is not an Err"))
// or call with a throwable
val exc = new RuntimeException("This is not an Err")
failure(exc) shouldBe Left(Error.fromThrowable(exc))
}
"`safely'" should "be used to fence code blocks that can fail" in {
// example success
safely { "5".toInt } shouldBe Right(5)
// example failure
safely { "five".toInt } shouldBe Left(Error.fromMessage(
"java.lang.NumberFormatException: For input string: \"five\""
))
// optionally provide a custom message or a throwable
safely("five".toInt, "Well, shucks.") shouldBe
Left(Error.fromMessage("Well, shucks."))
}
"`ensure'" should "convert a boolean expression to an Either value" in {
// example success
ensure(1 == 1, "They were not the same!") shouldBe Right(())
// example failure
ensure(0 == 1, "They were not the same!") shouldBe
Left(Error.fromMessage("They were not the same!"))
// the message is optional
ensure { 0 == 1 } shouldBe Left(Error.getDefault)
}
"`fold'" should "be used to destructure and branch" in {
// given
val yourErr = failure
val yourFoo = Right(new Foo)
def handleErr(err: Err): Int = 404
def handleFoo(foo: Foo): Int = 200
// then
def yourConsumer(ef: Either[Err, Foo]): Int =
ef.fold(
err => handleErr(err),
foo => handleFoo(foo)
)
// then
yourConsumer(yourErr) shouldBe 404
yourConsumer(yourFoo) shouldBe 200
}
"pattern matching" should "also work" in {
// given
val yourErr = failure
val yourFoo = Right(new Foo)
def handleErr(err: Err): Int = 404
def handleFoo(foo: Foo): Int = 200
// then
def yourConsumer(ef: Either[Err, Foo]): Int =
ef match {
case Left(err) => handleErr(err)
case Right(foo) => handleFoo(foo)
}
// then
yourConsumer(yourErr) shouldBe 404
yourConsumer(yourFoo) shouldBe 200
}
"but you" should "prefer `fold' because it enforces covering both branch" in {
// given
val yourErr = failure
def handleFoo(foo: Foo): Int = 200
// when
def yourConsumer(ef: Either[Err,Foo]): Int =
ef match {
// It's a trap!
case Right(foo) => handleFoo(foo)
}
// then
withClue("`yourConsumer' throws because the Left branch is undefined") {
an[Exception] should be thrownBy yourConsumer(yourErr)
}
}
"`get', `getError', `getOrElse', and `getOrThrow'" should "be more accessors" in {
// when
val inner = new Foo
val foo = Right(inner)
val err = failure
val fallback = new Bar
// then
err.get shouldBe 'isEmpty
err.getError shouldBe 'nonEmpty
err.getOrElse(fallback) shouldBe fallback
an[Exception] should be thrownBy err.getOrThrow
// and
foo.get shouldBe 'nonEmpty
foo.getError shouldBe 'isEmpty
foo.getOrElse(fallback) shouldBe inner
foo.getOrThrow shouldBe inner
}
"`foreach'" should "be used to perform arbitrary side effects if " +
"and only if the provided Either holds a Right value inside" in {
// given
val foo = Right(new Foo)
val err = failure
var `took off every zig` = false
var `launched missiles` = false
def takeOffEveryZig(): Unit = `took off every zig` = true
def launchMissiles(): Unit = `launched missiles` = true
// when
foo.foreach { (f: Foo) => takeOffEveryZig() }
err.foreach { (f: Foo) => launchMissiles() }
// then
withClue("The action occurs if the Either was a success:") {
`took off every zig` shouldBe true
}
withClue("The action occurs only if the Either was a success:") {
`launched missiles` shouldBe false
}
}
"you" should "be able to manipulate Either values like you'd manipulate booleans" in {
// give
val err1 = failure
val err2 = failure("This is not e2.")
val foo1 = Right(new Bar)
val foo2 = Right(new Foo)
// then
withClue("`or' returns the first success (or last failure if no successes)") {
(err1 or foo1 or err2 or foo2) shouldBe foo1
(err1 or err2) shouldBe err2
}
withClue("`and' returns the first failure (or last success if no failures)") {
(foo1 and err1 and foo2 and err2) shouldBe err1
(foo1 and foo2) shouldBe foo2
}
}
"`and'" should "be useful for gating actions behind preconditions" in {
// given
var `reported success` = false
def reportSuccess(): Baz = {
`reported success` = true; new Baz
}
def flakeyIOAction(): Either[Err, Unit] = failure("Ran out of internet.")
// when
val result = flakeyIOAction() and Right(reportSuccess())
// then
withClue("Since `flakeyIOAction' failed, we didn't report success: ") {
`reported success` shouldBe false
}
}
"`or'" should "be useful for failure recovery" in {
// given
val flakeyValue = failure
val recovery = new Foo
// when
val result = flakeyValue or Right(recovery)
// then
result shouldBe Right(recovery)
}
"`recover'" should "be a more general version of `or' that allows you to " +
"branch on the contents of the error value" in {
// given
val notFound = failure(Err("Not Found", 404))
val notAllowed = failure(Err("Not Allowed", 403))
val ok = Right("This is a normal response body.")
// when
def handle404(err: Err): Either[Err, String] = err match {
case Err(_,404) => Right("This is a pretty 404 page.")
case _ => Left(err) // pass along errors we're not handling
}
// then
notFound.recover(handle404) shouldBe Right("This is a pretty 404 page.")
notAllowed.recover(handle404) shouldBe Left(Err("Not Allowed", 403))
ok.recover(handle404) shouldBe Right("This is a normal response body.")
}
// The below function fails in an expected way.
@throws[NumberFormatException]
def flakeyFunction(str: String): Int = str.toInt
// Why are we using exceptions for _expected_ failures, anyway?
// Exceptions should be exit paths for our program, not exit paths for a
// method. The purpose of the EitherMonad idiom is to have a sane, informative
// return type for functions that can fail in expected, non-fatal ways.
def safeFunction(str: String): Either[Err, Int] =
try Right(str.toInt) catch {
case scala.util.control.NonFatal(e) => failure(e)
}
// The above pattern is so common that we abstract it and give it a name
"`safely'" should "turn an `A' into an `Either[Err, A]'" in {
// when
def safeFunction2(str: String): Either[Err, Int] =
safely { flakeyFunction(str) }
// then
safeFunction2("12") shouldBe Right(12)
safeFunction2("twelve") shouldBe Left(Error.fromMessage(
"java.lang.NumberFormatException: For input string: \"twelve\""
))
}
"`safe'" should "turn an `A => B' into an `A => Either[Err, B]'" in {
// when
def safeFunction3: String => Either[Err, Int] = safe(flakeyFunction)
// then
safeFunction3("12") shouldBe Right(12)
safeFunction3("twelve") shouldBe Left(Error.fromMessage(
"java.lang.NumberFormatException: For input string: \"twelve\""
))
}
// Besides exception handling, some functions fail because preconditions
// are not met. EitherMonad has some tools for those situations.
"you" should "be able to use the EitherMonad idiom to enforce preconditions" in {
// when
def withdrawCash(balance: Long, amount: Long): Either[Err, Long] =
if (amount <= balance) Right(balance - amount)
else failure("Insufficient Funds!")
// then
withdrawCash(1000, 100) shouldBe Right(900)
withdrawCash(100, 1000) shouldBe
Left(Error.fromMessage("Insufficient Funds!"))
}
"`ensure'" should "be able to accomplish the same thing in a for comprehension" in {
// when
def withdrawCash(balance: Long, amount: Long): Either[Err, Long] =
for {
// `ensure' is used to gate the computation
// even though its result is ignored
_ <- ensure(amount <= balance, "Insufficient Funds!")
} yield balance - amount
// then
withdrawCash(1000, 100) shouldBe Right(900)
withdrawCash(100, 1000) shouldBe
Left(Error.fromMessage("Insufficient Funds!"))
}
"`ensure'" should "work with `and' outside of for comprehension too" in {
// when
def withdrawCash(balance: Long, amount: Long): Either[Err, Long] =
ensure(amount <= balance, "Insufficient Funds!") and Right(balance - amount)
// then
withdrawCash(1000, 100) shouldBe Right(900)
withdrawCash(100, 1000) shouldBe
Left(Error.fromMessage("Insufficient Funds!"))
}
"Either-y computations and Either-y results" should "be chainable" in {
// given
def readInput(str: String): Either[Err, Long] = safely { str.toInt }
def withdrawCash(balance: Long, amount: Long): Either[Err, Long] =
ensure(amount <= balance, "Insufficient Funds!") and Right(balance - amount)
// when
def atmTxn(input: String, startingBalance: Long): Either[Err, Long] =
readInput(input).flatMap { withdrawCash(startingBalance, _) }
// then
atmTxn("12", 100) shouldBe Right(88)
atmTxn("twelve", 100) shouldBe Left(Error.fromMessage(
"java.lang.NumberFormatException: For input string: \"twelve\""
))
atmTxn("12", 10) shouldBe Left(Error.fromMessage("Insufficient Funds!"))
}
"`ensure'" should "be usable in arbitrary for comprehensions" in {
// when
def doIfDistinct(f: (Int, Int) => Int)
(ea1: Either[Err, Int], ea2: Either[Err, Int]):
Either[Err, Int] = for {
a1 <- ea1 // stops here if ea1 is a failure
a2 <- ea2 // stops here if ea2 is a failure
_ <- ensure(a1 != a2, "Values not distinct!") // stops here if a1 == a2
} yield f(a1, a2)
// then
doIfDistinct(_+_)(Right(12), failure) shouldBe Left(Error.getDefault)
doIfDistinct(_*_)(Right(12), Right(12)) shouldBe
Left(Error.fromMessage("Values not distinct!"))
doIfDistinct(_/_)(Right(12), Right(4)) shouldBe Right(3)
}
// The above function take Either values as arguments. This is something of
// an antipattern, since functions that accept pure arguments can always
// operate on Either values through `map', `flatMap' and for comprehension.
// This is part of what we mean by "automatic marshalling/unmarshalling."
"`doIfDistinct'" should "be refactored so that it doesn't take Either args" in {
// given
val `12` = Right[Err,Int](12)
val `4` = Right[Err,Int](4)
// when
def doIfDistinct(f: (Int, Int) => Int)(x: Int, y: Int): Either[Err, Int] =
ensure(x != y, "Values not distinct!") and Right(f(x, y))
// then
withClue("You can use nested `flatMap's [img: small_brain.jpg]: ") {
val res1 = `12`.flatMap { x =>
`4`.flatMap { y =>
doIfDistinct(_/_)(x, y)
}
}
res1 shouldBe Right(3)
}
withClue("Or you can use for comprehension [img: glowing_brain.jpg]: ") {
val res2 =
for { x <- `12`; y <- `4`; quot <- doIfDistinct(_/_)(x, y) }
yield quot
res2 shouldBe Right(3)
}
withClue("Or you can use `bind' [img: genius_brain.jpg]: ") {
val res3 = bind2(doIfDistinct(_/_))(`12`, `4`)
res3 shouldBe Right(3)
}
}
// `bind' is basically flatMap, but for higher-arity functions
"`bind'" should "be used to pass Either values into a function that " +
"expects pure arguments and returns an Either value (see signature below)" in {
// given
val `12` = Right(12)
val `3` = Right(3)
val sayPlz = failure("You didn't say the magic word!")
val `0` = Right(0)
val div: (Int, Int) => Either[Err, Int] = (x, y) =>
ensure(y != 0, "Division by zero!") and Right(x / y)
// then
// bind: (A => Either[*,B]) => (Either[*,A] => Either[*,B])
// bindN: ((A1,...,AN) => Either[*,B]) => ((Either[*,A1],...,Either[*,AN]) => Either[*,B])
bind2(div)(`12`, `3`) shouldBe Right(12 / 3)
bind2(div)(sayPlz, `3`) shouldBe
Left(Error.fromMessage("You didn't say the magic word!"))
bind2(div)(`12`, `0`) shouldBe
Left(Error.fromMessage("Division by zero!"))
}
// `lift' is basically `map', but for higher-arity functions
"`lift'" should "be used to apply a pure function to Either values" in {
// given
val `12` = Right(12)
val `10` = Right(10)
val err = failure
val plus2 = (_: Int) + 2
val sumTwo = (_: Int) + (_: Int)
val sumThree = (_: Int) + (_: Int) + (_: Int)
// then
// lift: (A => B) => (Either[*,A] => Either[*,B])
// liftN: ((A1,...,AN) => B) => ((Either[*,A1],...,Either[*,AN]) => Either[*,B])
lift(plus2)(`12`) shouldBe Right(plus2(12))
lift2(sumTwo)(`12`, `10`) shouldBe Right(sumTwo(12, 10))
lift3(sumThree)(`12`, `10`, err) shouldBe Left(Error.getDefault)
}
"`safe'`" should "be used to convert flakey functions into Either-y functions" in {
// given
val `10` = "10"
val `12` = "12"
val twelve = "twelve"
// when
// safe: (A => B) => (A => Either[*,B])
// safeN: ((A1,...,AN) => B) => ((A1,...,AN) => Either[*,B])
val readAndAdd = safe2 {
(s1: String, s2: String) => s1.toInt + s2.toInt
}
// then
readAndAdd(`10`, `12`) shouldBe Right(22)
readAndAdd(`10`, twelve) shouldBe Left(Error.fromMessage(
"java.lang.NumberFormatException: For input string: \"twelve\""
))
}
"`safely'" should "accomplish the same thing at the value level" in {
// given
val `10` = "10"
val `12` = "12"
val twelve = "twelve"
// when
// safely: A => Either[*,A]
def readAndAdd(s1: String, s2: String) = safely { s1.toInt + s2.toInt }
// then
readAndAdd(`10`, `12`) shouldBe Right(22)
readAndAdd(`10`, twelve) shouldBe Left(Error.fromMessage(
"java.lang.NumberFormatException: For input string: \"twelve\""
))
}
"you" should "use `safely' and `ensure' judiciously in for comprehension" in {
// given
val `12` = "12"
val `10` = "10"
val twelve = "twelve"
// when
val result = for {
(x, y) <- safely { (`12`.toInt, `10`.toInt) }
_ <- ensure(x >= y, "Subtracting a larger from a smaller!")
diff = x - y
z <- safely { twelve.toInt }
_ <- ensure(z != 0, "Dividing by zero!")
quot = diff / z
} yield quot
// then
result shouldBe Left(Error.fromMessage(
"java.lang.NumberFormatException: For input string: \"twelve\""
))
}
// In summary:
// (1) Write pure methods (e.g., `A => B') whenever possible;
// (2) If your method might fail, accept pure values and return Either
// values (e.g, `A => Either[*,B]') to reflect failure in the signature;
// (3) `safely' is easy-mode exception/null handling;
// (4) `ensure' is easy-mode precondition enforcing;
// (5) Don't unwrap your Either values until the very end of your program,
// since Either values can be used wherever pure values are expected
// (using `map'/`lift' and `flatMap'/`bind').
// Now, for `traverse', `sequence', and `successes'.
// `traverse' and `sequence' have funky type signatures, but that's only
// because they are written to be as general as possible. They are easier to
// understand when you specialize them to lists.
// sequence: List[Either[*,A]] => Either[*,List[A]]
"`sequence'" should "be used to factor Either-ness out of a List" in {
// given
val eithers: List[Either[Err, Int]] =
List(Right(1), Right(2), Right(3))
// then
// sequence: List[Either[*,A]] => Either[*,List[A]]
withClue("Succeeds if each list elem is a success: ") {
sequence(eithers) shouldBe Right(List(1, 2, 3))
}
withClue("Succeeds only if each list elem is a success: ") {
sequence(eithers :+ failure("oops")) shouldBe
Left(Error.fromMessage("oops"))
}
}
// traverse: List[A] => (A => Either[*,B]) => Either[*,List[B]]
"`traverse'" should "be used to apply an Either-y function to each " +
"element of a list while collecting the Either-ness outside the list" in {
// given
val ints = List("1", "2", "3")
val readInt = safe { (n: String) => n.toInt }
// then
// traverse: List[A] => (A => Either[*,B]) => Either[*,List[B]]
withClue("Succeeds if each application succeeds: ") {
traverse(ints)(readInt) shouldBe Right(List(1, 2, 3))
}
withClue("Succeeds only if each application succeeds: ") {
traverse(ints :+ "twelve")(readInt) shouldBe Left(Error.fromMessage(
"java.lang.NumberFormatException: For input string: \"twelve\""
))
}
}
// successes: List[Either[*,A]] => List[A]
"`successes'" should "be use with the built-in `map' method for your " +
"collection if you want to keep all the successes and discard any failures" in {
// given
val ints = List("10", "11", "twelve", "13")
// then
successes(ints.map(safe(_.toInt))) shouldBe List(10, 11, 13)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment