The Scala Try
class was introduced as a monadically-composable mechanism for exception handling
in Scala 2.10
and back-ported to Scala 2.9.3. The
ways that this class interacts with the Monad Laws has
been a subject of some discussion in the Scala community.
In response to some discussions
on scala-debate, this gist examines two ways in
which Try
has been described as violating the monad laws in detail, and argues that in fact,
the design of this class goes to some lengths to both be a useful exception handling mechanism and
to satisfy these laws.
The reason that Try
fails the 1st and 3rd monad laws above when f
throws
an exception appears to be simply because the Java exception instances do not
compare as equal, even though the type and message may be equal, as you
can see below:
scala> f(v)
res4: scala.util.Try[Int] = Failure(java.lang.NumberFormatException: For input string: "bad")
scala> unit(v).flatMap(f)
res5: scala.util.Try[Int] = Failure(java.lang.NumberFormatException: For input string: "bad")
scala> m.flatMap(f).flatMap(g)
res6: scala.util.Try[Int] = Failure(java.lang.NumberFormatException: For input string: "bad")
scala> m.flatMap(n => f(n).flatMap(g))
res7: scala.util.Try[Int] = Failure(java.lang.NumberFormatException: For input string: "bad")
If you use the two lines under (a) in the above example, defining f
to
throw a single constant exception, then Try
fully satisfies the monad
laws.
Therefore it seems clear that, independent of the non-referentially
transparent aspect of throwing exceptions (as each throw records
information about the stack), Try
does absolutely satisfy the monad laws.
It is only in the presence of the aspect of throwing exceptions which
violates referential transparency that Try
cannot satisfy the laws,
which seems consistent and clear.
The reason that Try
fails the 1st (identity) monad law when f
throws
an uncaught exception in the Success
constructor, is that Try#flatMap
is catching uncaught exceptions and converting to Failure, whereas
Success
allows an uncaught exception to propagate.
If you use the line under (b) in the above example, defining f
to
have a bug generating an uncaught exception, then the naked evaluation
of f
will throw the exception, but the evaluation inside the #flatMap
will catch the uncaught exception and convert to Failure.
However, if you use the line under (c) in the above example, which
fixes the uncaught exception in f
by using Try
instead of Success
to wrap, then Try
can satisfy the monad laws.
Therefore, it seems clear that, independent of functions which throw
uncaught exceptions, Try
satisfies the laws, and its bias in design
is to extend an extra safety net via #flatMap
, that subsequent
chained function evaluations will have exceptions caught and transformed
even if that function does throw uncaught exceptions.
In other words, it's better to characterize the case where f: X => Try[Y]
throws an uncaught exception as an input which is outside of the monad laws,
and for which Try
extends one extra safety net. For acceptable inputs, it
does satisfy the laws.
thanks man!!!