We are not using the "Zed" word.

We are not using the "Zed" word.

Ok, so I lied a little...

How I've Traditionally Seen scalaz

  • In the past, I've seen scalaz as fairly intimidating
     
  • People always spoke about it being more "pure"/"haskelly"/"mathy"
     
  • I'll be the first to admit: I don't have a CS degree and sort of suck at math
     
  • "What's wrong with what I have in standard Scala?!?"

The Reality about scalaz?

IT'S MAGIC

The Road to scalaz

  • Once I got started, it was hard to stop
     
  • The constructs are powerful and useful
     
  • I am by no means an expert: just an excited amateur
     
  • This is not a category theory or haskell talk: Let's be practical

The Road to scalaz

  • I want you to learn:
    • "Hey! This stuff may be useful!"
       
  • I am not going to teach you:
    • "A monad is a monoid in the category of endofunctors, what's the problem?"

The Road to scalaz

Problems to solve...

  • Our API server was part of a larger Angular.js application: error passing was hard
    • Providing clear errors & validating input was a problem
       
    • 500s & generic exceptions complicate and frustrate frontend devs' debugging

Helping Developers
      Help Themselves

  • An error occurred
    • API Received bad/invalid data? (e.g. JSON Failed to parse)
       
    • Database failed?
       
    • Hovercraft filled up with eels?
       
  • What if multiple errors occurred?
     
  • How do we communicate all of this effectively?

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, but there are some limitations in interaction

Scala's Either: The Limitations

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.

Disjunctions as an Alternative

  • scalaz' \/ (aka "Disjunction") is similar to "Either" 
     
  • By convention, the right is success and the left failure
    • The symbol -\/ is "left" 
    • The symbol \/- is "right"

Disjunctions as an Alternative

  • Disjunctions assume we prefer success (the right)
     
  • This is also known as "Right Bias"
     
  • for comprehensions, map, and flatMap statements unpack where "success" \/- continues, and "failure" -\/ aborts
def query(arg: String): Error \/ Success

Best Practice

When declaring types, prefer infix notation, i.e.

over "standard" notation such as

def query(arg: String): \/[Error, Success]

Disjunctions as an Alternative

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 wrap an existing Scala value to a disjunction

import scalaz._
import Scalaz._

scala> \/.left("Failure!")
res10: scalaz.\/[String,Nothing] = -\/(Failure!)


scala> \/.right("Success!")
res12: scalaz.\/[Nothing,String] = \/-(Success!)

We can also invoke .left .right methods on the Disjunction singleton for the same effect...

import scalaz._
import Scalaz._

scala> -\/("Failure!")
res9: scalaz.-\/[String] = -\/(Failure!)

scala> \/-("Success!")
res11: scalaz.\/-[String] = \/-(Success!)

... or go fully symbolic with specific constructors: 

-\/ for left 

\/- for right

Digression: Scala Option

  • Scala Option is a commonly used container, having  a None and a Some subtype
     
  • Like \/ it also has a bias towards "success": Some
     
  • Comprehension over it has issues with "undiagnosed aborts"
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))
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?

\/ to the Rescue

  • Comprehending over groups of Option leads to "silent failure"
     
  • Luckily, scalaz includes implicits to help convert a Option to a Disjunction
     
  • \/ right bias makes it easy to comprehend
     
  • On a left, we'll get  potentially useful information instead of None

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") */
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?

Validation

  • Validation looks similar to \/ at first glance
    • (And you can convert between them)
    • Subtypes are Success and Failure
  • Validation is not  a monad
     
  • Validation is an applicative functor,  and many can be chained together
     
  • If any failure in the chain, failure wins: All errors get appended together

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 wanderingJoe = 
  DBObject(7, 
    Some(User("Wandering", "Joe", None))
  )
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")

  }
}
validDBUser(brendanCA)
/* Success[User] */

validDBUser(cthulhu)
/* Success[User] */

validDBUser(noSuchPerson)
/* Failure("... does not contain a user object") */

validDBUser(wanderingJoe)
/* Success[User] */
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

  }
}

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(wanderingJoe.user)
/* Failure(User has no defined address) */

Sticking it all together

  • scalaz  has a number of applicative operators to combine Validation results
     
  • *> and <* are two of the ones you'll run into first
    • *> takes the right hand value and discards the left
       
    • <* takes the left hand value and discards the right
       
    • Errors "win"
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 

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(wanderingJoe) *> validAddress(wanderingJoe.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'll need another tool if we want this to make sense      
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(wanderingJoe) *> validAddress(wanderingJoe.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)*/

  • NonEmptyList is a scalaz List that is guaranteed to have at least one element
     
  • Commonly used
    with Validation to allow accrual of multiple error messages
     
  • There's a type alias for Validation[NonEmptyList[L], R] of ​ValidationNEL[L, R]
     
  • Like a list, append allows elements to be added to the end

NonEmptyList

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 by hand

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 helpers, calling .failureNel, and declaring a ValidationNel return type.




validDBUserNel(noSuchPerson) *> validAddressNel(noSuchPerson.user)             
/* res20: scalaz.Validation[scalaz.NonEmptyList[String],Address] = 
Failure(NonEmptyList(
  DBObject(6,None) does not contain a user object,
  No such user
)) 
*/

Now, we get a list of errors - instead of a globbed string

One Last Operator

  • scalaz provides another useful applicative operator for us
     
  • |@| combines all of the Failure 
    and Success conditions
     
  • To handle Successes we provide  a PartialFunction

(validDBUserNel(brendanCA) |@| validAddressNel(brendanCA.user)) {
  case (user, address) =>
    s"User ${user.first} ${user.last} lives in ${address.city}"
}

// "User Brendan McAdams lives in Sunnyvale"

Our other users will return an NEL of errors, like with *>


(validDBUserNel(noSuchPerson) |@| validAddressNel(noSuchPerson.user)) {
  case (user, address) =>
    s"User ${user.first} ${user.last} lives in ${address.city}"
}

// Failure(
//   NonEmptyList(DBObject DBObject(6,None) does not contain a user object, 
//                No such user))

noSuchPerson gets a combined list

One last function: Error Handling

  • Dealing sanely with errors is always a challenge
     
  • There are a few ways in the Scala world to avoid try/catch, such as scala.util.Try
     
  • scalaz' \/ offers the Higher Order Function fromTryCatchThrowable, which catches any specific exception, and returns a Disjunction
     
  • You specify your return type, the type of exception to catch, and your function body...



"foo".toInt

/* java.lang.NumberFormatException: For input string: "foo"
	  at java.lang.NumberFormatException.forInputString ...
	  at java.lang.Integer.parseInt(Integer.java:492)
	  at java.lang.Integer.parseInt(Integer.java:527) */

Here's a great function to wrap...




\/.fromTryCatchThrowable[Int, NumberFormatException] { 
  "foo".toInt 
}

/* res9: scalaz.\/[NumberFormatException,Int] = 
    -\/(java.lang.NumberFormatException: 
        for input string: "foo") */

Note the reversed order of arguments: Right type, then Left ​type




\/.fromTryCatchThrowable[Int, Exception] { 
  "foo".toInt 
}

/* res9: scalaz.\/[NumberFormatException,Int] = 
    -\/(java.lang.NumberFormatException: 
        for input string: "foo") */

We can also be "less specific" in our exception type to catch more




\/.fromTryCatchThrowable[Int, java.sql.SQLException] { 
  "foo".toInt 
}

/*
java.lang.NumberFormatException: For input string: "foo"
  at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
  at java.lang.Integer.parseInt(Integer.java:580)
  at java.lang.Integer.parseInt(Integer.java:615)
...
*/

Our exception type matters: if an Exception doesn't match it will still be thrown



\/.fromTryCatchNonFatal[Int] { 
  "foo".toInt 
}
/* res14: scalaz.\/[Throwable,Int] = 
    -\/(java.lang.NumberFormatException:
        For input string: "foo") */

There is also \/.tryCatchNonFatal which will catch anything classified as scala.util.control.NonFatal

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 the file "Either.scala", calling \/ "Either" gets confusing vs. Scala's builtin Either
     
  • Here's a few of the names I've heard used in the community for |@| (There's also a unicode alias of ⊛)

Oink

Cinnabon/Cinnamon Bun

Chelsea Bun / Pain aux Raisins

Tie Fighter

Princess Leia

Admiral Ackbar

Scream

Scream 2?

Home Alone

Pinkie Pie

Some Resources...

  • Eugene Yokota's free website, "Learning Scalaz"
    • http://eed3si9n.com/learning-scalaz/
       
  • Learn some Haskell! I really like "Learn You A Haskell For Great Good" by Miran Lipovača
    • http://learnyouahaskell.com

Questions?

A SKEPTIC'S LOOK AT SCALAZ' "GATEWAY DRUGS": A PRACTICAL EXPLORATION

By Brendan McAdams

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?

  • 2,181
Loading comments...

More from Brendan McAdams