Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Scala Exchange 2014: A Skeptic's Look at scalaz...

Scala Exchange 2014: A Skeptic's Look at scalaz' "Gateway Drugs”: A Practical Exploration

We've all seen them on the corner of our local software development neighborhoods: FP purists, shamelessly peddling scalaz to unsuspecting developers. Lured in by promises of Free Monoids, Semigroups, and Endofunctors these developers soon seem lost in throes of ecstatic coding. " To the skeptical and stubbornly practical among us, the above might ring a little true – especially if read in Rod Serling's voice. Images of gibbering horrors lurking in the depths of mathematical perfection swim before our eyes.

But what if there is true value in the world of scalaz? What if it is possible to use these tools for good (and a little bit of evil – it's fun to use learning for evil!) and profit... Without getting hopelessly lost in the opium dens of FP?

In this talk we will look at some of the "gateway drugs" of scalaz: Validation, NonEmptyList, \/, Monad Transformers, and more. How do they work from a practical standpoint? What is their value for real world applications? Can we use them without an advanced Maths PhD? And just how fun is it to really code with these tools?

Brendan McAdams

December 09, 2014
Tweet

More Decks by Brendan McAdams

Other Decks in Technology

Transcript

  1. How I Used to See scalaz » In the past,

    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?"
  2. The Road to scalaz » Once I got started, it

    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
  3. The Road to scalaz » I want you to learn:

    » "Hey, this stuff may be useful!" » I don't want you to learn (from me): » "A monad is a monoid in the category of endofunctors, what's the problem?"
  4. The Road to scalaz problems to solve » Providing clearer

    errors & validating input was a problem » Our API Server is part of a larger application: erroring is hard » 500s & generic exceptions, complicate frontend devs debugging
  5. Helping developers help themselves » An Error Occurred » API

    received bad/invalid data? » Database failed? » What if multiple errors occurred? » How do we communicate this effectively?
  6. Scala's Either: The limitations » Scala's builtin Either is a

    commonly used tool, allowing Left and Right Projections » By convention, Left indicates an error while Right indicates a success » Good concept, mediocre interaction
  7. The Problem with Either scala> val success = Right("Success!") success:

    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.
  8. Disjunctions \/ as an Alternative » scalaz \/ (aka Disjunction)

    assumes we mostly want the right (success) value » "Right Bias" » Unpacks in for comprehensions / map / flatMap where the "positive" (\/-) value "continues", and "negative" (-\/) aborts » Best Practice: When declaring types, prefer infix notation – Error \/ Success) – vs. standard notation – \/[Error, Success]`
  9. import scalaz._ import Scalaz._ scala> "Success!".right res7: scalaz.\/[Nothing,String] = \/-(Success!)

    scala> "Failure!".left res8: scalaz.\/[String,Nothing] = -\/(Failure!) Postfix Operators (.left & .right) allow us to convert an existing Scala value to a disjunction.
  10. import scalaz._ import Scalaz._ scala> \/.left("Failure!") res10: scalaz.\/[String,Nothing] = -\/(Failure!)

    scala> \/.right("Success!") res12: scalaz.\/[Nothing,String] = \/-(Success!) Be Explicit: Invoke the left and right disjunction constructors.
  11. import scalaz._ import Scalaz._ scala> -\/("Failure!") res9: scalaz.-\/[String] = -\/(Failure!)

    scala> \/-("Success!") res11: scalaz.\/-[String] = \/-(Success!) Or go fully symbolic with -\/ for left and \/- for right
  12. Digression: Scala Option » Scala Option is a commonly used

    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"
  13. case class Address(city: String) case class User(first: String, last: String,

    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))
  14. for { dao <- brendan user <- dao.user } yield

    user /* res13: Option[User] = Some(User(Brendan,McAdams,None)) */ for { dao <- someOtherGuy user <- dao.user } yield user /* res14: Option[User] = None */ » What went wrong?
  15. \/ To the Rescue » Comprehending over groups of options

    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
  16. None \/> "No object found" /* res0: scalaz.\/[String,Nothing] = -\/(No

    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") */
  17. for { dao <- brendan \/> "No user by that

    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?
  18. Validation » Validation looks similar to \/ (and you can

    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
  19. val brendanCA = DBObject(4, Some(User("Brendan", "McAdams", Some(Address("Sunnyvale")))) ) val cthulhu

    = DBObject(5, Some(User("Cthulhu", "Old One", Some(Address("R'lyeh")))) ) val noSuchPerson = DBObject(6, None) val jonPretty = DBObject(7, Some(User("Jon", "Pretty", None)) )
  20. def validDBUser(dbObj: DBObject): Validation[String, User] = { dbObj.user match {

    case Some(user) => Success(user) case None => Failure(s"DBObject $dbObj does not contain a user object") } }
  21. def validAddress(user: Option[User]): Validation[String, Address] = { user match {

    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 } }
  22. validDBUser(brendanCA) /* Success[User] */ validDBUser(cthulhu) /* Success[User] */ validDBUser(noSuchPerson) /*

    Failure("... does not contain a user object") */ validDBUser(jonPretty) /* Success[User] */
  23. validAddress(brendanCA.user) /* Success(Address(Sunnyvale)) */ // let's assume R'Lyeh has no

    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) */
  24. Sticking it all together » scalaz has a number of

    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"
  25. 1.some *> 2.some /* res10: Option[Int] = Some(2) */ 1.some

    <* 2.some /* res11: Option[Int] = Some(1) */ 1.some <* None /* res13: Option[Int] = None */ None *> 2.some /* res14: Option[Int] = None */ » BUT: With Validation it will chain together all errors that occur instead of short circuiting
  26. validDBUser(brendanCA) *> validAddress(brendanCA.user) /* res16: scalaz.Validation[String,Address] = Success(Address(Sunnyvale)) */ validDBUser(cthulhu)

    *> 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?!?
  27. validDBUser(brendanCA) *> validAddress(brendanCA.user) /* res16: scalaz.Validation[String,Address] = Success(Address(Sunnyvale)) */ validDBUser(cthulhu)

    *> 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?!? » The way *> is called on Validation, it appends all errors together... we need another tool
  28. NonEmptyList » NonEmptyList is a scalaz List which is guaranteed

    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.
  29. def validDBUserNel(dbObj: DBObject): Validation[NonEmptyList[String], User] = { dbObj.user match {

    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)
  30. def validAddressNel(user: Option[User]): ValidationNel[String, Address] = { user match {

    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.
  31. One Last Operator » scalaz provides the applicative operator |@|,

    for when we want to combine all of the failure and success conditions » To handle the successes, we provide a PartialFunction
  32. (validDBUserNel(brendanCA) |@| validAddressNel(brendanCA.user)) { case (user, address) => s"User ${user.first}

    ${user.last} lives in ${address.city}" } // "User Brendan McAdams lives in Sunnyvale" » The other users will return NEL of Errors like with *>
  33. Final Thought: On Naming » From the skeptical side, the

    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 ⊛)