We are not using the "Zed" word.
We are not using the "Zed" word.
Ok, so I lied a little...
Helping Developers
Help Themselves
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.
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]
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
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?
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?
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) */
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?
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)*/
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
(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
"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