I've seen scalaz as fairly intimidating • People always spoke about it being more "pure"/"haskelly"/"mathy" • I sort of suck at math • "What's wrong with what I have in standard Scala?"
was hard to stop • The constructs are powerful, and mostly useful • I am by no means an expert, however • This is not a math/category theory/ haskell talk
errors & validating input was a problem • Our API Server was part of a larger application: error passing was hard • 500s & generic exceptions, complicate frontend devs debugging
received bad/invalid data? (e.g. JSON failed to parse) • Database failed? • What if multiple errors occurred? • How do we communicate this effectively?
commonly used tool, allowing Left and Right Projections • By convention, Left indicates an error while Right indicates a success • Good concept, mediocre interaction
scala.util.Right[Nothing,String] = Right(Success!) scala> success.isRight res2: Boolean = true scala> success.isLeft res3: Boolean = false scala> for { | x <- success | } yield x <console>:10: error: value map is not a member of scala.util.Right[Nothing,String] x <- success ^ • Not a Monad. Pain in the ass to extract.
assumes we mostly want the right (success) value - aka "Right Bias" • Unpacks in for comprehensions / map / flatMap where the "positive" \/- value "continues", and "negative" -\/ aborts
scala> "Failure!".left res8: scalaz.\/[String,Nothing] = -\/(Failure!) Postfix Operators (.left & .right) allow us to convert an existing Scala value to a disjunction.
container, having a None and a Some subtype • Like \/ it also has a bias towards a value: Some • Comprehension over it has issues with "undiagnosed aborts"
address: Option[Address]) case class DBObject(id: Long, user: Option[User]) val brendan = Some(DBObject(1, Some(User("Brendan", "McAdams", None)))) val someOtherGuy = Some(DBObject(2, None))
user /* res13: Option[User] = Some(User(Brendan,McAdams,None)) */ for { dao <- someOtherGuy user <- dao.user } yield user /* res14: Option[User] = None */ • What went wrong?
leads to "silent failure" • Luckily, scalaz includes implicits to help convert an Option to a Disjunction • \/ right bias makes it easy to comprehend • On a left, we'll get potentially useful information instead of None
object found) */ None toRightDisjunction "No object found" /* res1: scalaz.\/[String,Nothing] = -\/(No object found) */ Some("My Hovercraft Is Full of Eels") \/> "No object found" /* res2: scalaz.\/[String, String] = \/-(My Hovercraft Is Full of Eels) */ Some("I Will Not Buy This Record It Is Scratched") .toRightDisjunction("No object found") /* res3: scalaz.\/[String, String] = \/-(I Will Not Buy This Record, It Is Scratched") */
ID" user <- dao.user \/> "Join failed: no user object" } yield user /* res0: scalaz.\/[String,User] = \/-(User(Brendan,McAdams,None)) */ for { dao <- someOtherGuy \/> "No user by that ID" user <- dao.user \/> "Join failed: no user object" } yield user /* res1: scalaz.\/[String,User] = -\/(Join failed: no user object) */ Suddenly we have much more useful failure information... but what if we want to do something beyond comprehensions?
convert between them) • Subtypes success and failure • Validation however is not a monad (despite some 6.x examples that show it as one...) • Validation is an applicative functor • If any failure in the chain, failure wins: All errors get mashed together
case Some(User(_, _, Some(address))) if postOfficeValid(address) => address.success case Some(User(_ , _, Some(address))) => "Invalid address: Not recognized by postal service".failure case Some(User(_, _, None)) => "User has no defined address".failure case None => "No such user".failure } }
mail carrier validAddress(cthulhu.user) /* Failure(Invalid address: Not recognized by postal service) */ validAddress(noSuchPerson.user) /* Failure(No such user) */ validAddress(jonPretty.user) /* Failure(User has no defined address) */
applicative operators to combine results • *> and <* are two of the ones you'll see first • *> takes the right hand value and discards the left • <* takes the left hand value and discards the right • Errors "win"
*> validAddress(cthulhu.user) /* res17: scalaz.Validation[String,Address] = Failure(Invalid address: Not recognized by postal service) */ validDBUser(jonPretty) *> validAddress(jonPretty.user) //res19: scalaz.Validation[String,Address] = Failure(User has no defined address) */ validDBUser(noSuchPerson) *> validAddress(noSuchPerson.user) /* res18: scalaz.Validation[String,Address] = Failure(DBObject DBObject(6,None) does not contain a user objectNo such user) */ • Wait. WTF happened to that last one?!?
*> validAddress(cthulhu.user) /* res17: scalaz.Validation[String,Address] = Failure(Invalid address: Not recognized by postal service) */ validDBUser(jonPretty) *> validAddress(jonPretty.user) //res19: scalaz.Validation[String,Address] = Failure(User has no defined address) */ validDBUser(noSuchPerson) *> validAddress(noSuchPerson.user) /* res18: scalaz.Validation[String,Address] = Failure(DBObject DBObject(6,None) does not contain a user objectNo such user) */ • The way *> is called on Validation, it appends all errors together... • We need another tool
to have at least one element • Commonly used with Validation to allow accrual of multiple error messages • So common, in fact, that there's a type alias for Validation[NonEmptyList[L], R] of ValidationNEL[L, R] • Append on an NEL will add each element separately.
case Some(user) => Success(user) case None => Failure(NonEmptyList(s"DBObject $dbObj does not contain a user object")) } } • We can be explicit, and construct a NonEmptyList (and declare it explicitly)
case Some(User(_, _, Some(address))) if postOfficeValid(address) => address.success case Some(User(_ , _, Some(address))) => "Invalid address: Not recognized by postal service".failureNel case Some(User(_, _, None)) => "User has no defined address".failureNel case None => "No such user".failureNel } } • Or we can use some helpful shortcuts and call .failureNel, and declare a ValidationNel return type.
is always a challenge • There are a few ways in the Scala world of avoiding the traditional try/catch, such as scala.util.Try • scalaz' \/ offers the Higher Order Function fromTryCatchThrowable, which catches any specified exception, and returns a Disjunction • You specify your return type, the type of exception to catch, and your function body...
-\/(java.lang.NumberFormatException: For input string: "foo") */ • Our passed exception type matters: if a thrown exception doesn't match, it will still be thrown.
-\/(java.lang.NumberFormatException: For input string: "foo") */ • There's also \/.tryCatchNonFatal which will catch anything classified as scala.util.control.NonFatal
common use of symbols gets... interesting • Agreeing on names, at least within your own team, is important • Although it is defined in Either.scala, calling \/ "Either" gets confusing vs. Scala's Either • Here's a few of the names I've heard used in the community for |@| (There's also a unicode alias of ⊛)