ScalaZ Disjunctions
2016-07-06
Last time, while discussing the Scala ecosystem, I said that ScalaZ often provides great alternatives to the standard library classes. In this post, we will look at ScalaZ disjunctions, which offer an alternative to Either
and Try
.
Indicating possible dangers
Since Scala doesn't have the notion of checked exceptions, you don't have to use the throws
keyword to declare them. In Java terms, all exceptions in Scala are considered runtime and can be thrown from anywhere. Let's look at a concrete example. Here is a fictitious method (shamelessly stolen from my "Modern Web Development with Scala") that works roughly 60% of the time.
object DangerousService {
def queryNextNumber: Long = {
val source = Math.round(Math.random * 100)
if (source <= 60) source
else throw new Exception("The generated number is too big!")
}
}
The problem here is that the callers have no idea that an exception may occur and quite possible they will be using this method until something bad happens. A better approach is to use types to communicate about possible dangers and there are several candidates that can do the job:
scala.Option
scala.util.Either
scala.util.Try
scalaz.\/
Different choices bring different limitations, so let's discuss them in detail.
Standard library options explained
Using Option
s is trivial. Simply change the return type from Long
to Option[Long]
and instead of throwing an exception, return None
:
def queryNextNumber: Option[Long] = {
val source = Math.round(Math.random * 100)
if (source <= 60) Some(source)
else None
}
The problem here is also quite obvious. The None
value only says that there is no value, and therefore it's impossible to get to the cause.
The Either
type allows us to store the exception and therefore inspect it later if something goes wrong:
def queryNextNumber: Either[Exception, Long] = {
val source = Math.round(Math.random * 100)
if (source <= 60) Right(source)
else Left(new Exception("The generated number is too big!"))
}
Later, we can pattern match the value of type Either
to determine whether we have a successfully calculated value or an error. The problem here is that Either
doesn't really have any bias. In the example above, we used Right
as a value storage and Left
as an exception storage, but it is only a convention. The Either
itself doesn't have map
/flatMap
methods, so in order to use it in for comprehensions, we would need to switch to Either
projections and it is not as convenient as it should be. For details, check out Daniel Westheide's excellent post about The Either Type.
The Try
type, which was added in Scala 2.10, solves above mentioned problems, but also introduces one serious limitation. Unlike Either
, its left type is fixed as Throwable
. Therefore, you cannot create your own error type and use it as a method result in Try
.
Introducing ScalaZ disjunctions
Interestingly, ScalaZ offers an alternative to scala.util.Either
which is right-biased, works great in for comprehensions and comes with some additional utilities.
The usual way to start working with ScalaZ is to import all its definitions with the following:
import scalaz._, Scalaz._
Then, you can use ScalaZ disjunctions in a way similar to scala.util.Either
:
def queryNextNumber: Exception \/ Long = {
val source = Math.round(Math.random * 100)
if (source <= 60) \/.right(source)
else \/.left(new Exception("The generated number is too big!"))
}
Alternatively, you can use \/[Exception, Long]
instead of Exception \/ Long
. Also, \/.right
is the same as \/-
and \/.left
is the same as -\/
.
Unlike scala.util.Either
, Scalaz disjunctions are right biased, so you can use them easily in for comprehensions.
Replacing scala.util.Try
The Try
type has a convenient feature of safely absorbing thrown exceptions. Not surprisingly, something similar is possible with disjunctions:
def queryNextNumber: Throwable \/ Long = \/.fromTryCatchNonFatal {
val source = Math.round(Math.random * 100)
if (source <= 60) source
else throw new Exception("The generated number is too big!")
}
The fromTryCatchNonFatal
method will happily catch all non-fatal exceptions and put them into an instance of \/
. Note that here we changed our signature from Exception \/ Long
to Throwable \/ Long
and basically ended up with a more verbose version of Try
. In reality, however, disjunctions are more flexible than that.
Let's create our own Exception
subclass that will be able to store a generated number in addition to an error message:
class GenerationException(number: Long, message: String) extends Exception(message)
Instead of fromTryCatchNonFatal
, we need to use the fromTryCatchThrowable
method. It works in a similar way, but since it returns a custom Throwable
subtype, it also requires that a NonNothing
implicit value is defined in the scope:
implicit val geNotNothing = NotNothing.isNotNothing[GenerationException]
def queryNextNumber: GenerationException \/ Long = \/.fromTryCatchThrowable {
val source = Math.round(Math.random * 100)
if (source <= 60) source
else throw new GenerationException(source, "The generated number is too big!")
}
If you try invoking the queryNextNumber
method several times, you will see that it actually works as expected:
scala> DangerousService.queryNextNumber
res2: scalaz.\/[services.GenerationException,Long] = \/-(9)
scala> DangerousService.queryNextNumber
res3: scalaz.\/[services.GenerationException,Long] = \
-\/(services.GenerationException: The generated number is too big!)
Sequencing disjunctions
Sometimes you end up with a collection of disjunctions:
val lst = List(queryNextNumber, queryNextNumber, queryNextNumber)
If this is the case, you may want to get the disjunction of a collection. In order to do that, simply use the sequenceU
method, which was added to List
via the first import:
// import scalaz._, Scalaz._
val lstD = lst.sequenceU
If all numbers were generated successfully, you will get a \/-
containing a List[Long]
. If there was an exception, you will get a -\/
with a GenerationException
.
Conclusion
As I already stated in this post, the Scala ecosystem is extremely rich and diverse. As developers get more familiar and confident with the language, they tend to gravitate towards more powerful constructs. For example, when I was starting out, I initially settled on using Try
and Future
, but after some time ended up using ScalaZ disjunctions in most projects. If you're interested in advanced Scala topics or ScalaZ in particular, take a look at my book - "Mastering Advanced Scala".
Finally, if you are only starting with Scala and want to make the learning process very efficient, take a look at one of my previous posts about learning Scala by using frameworks.