Skip to content

Instantly share code, notes, and snippets.

@ms-tg
Last active May 28, 2022 03:48
Show Gist options
  • Save ms-tg/6222775 to your computer and use it in GitHub Desktop.
Save ms-tg/6222775 to your computer and use it in GitHub Desktop.
Scala Try: monad axioms
import scala.util.{Try, Success, Failure}
def f(s: String): Try[Int] = Try { s.toInt }
def g(i: Int): Try[Int] = Try { i * 2 }
def unit[T](v: T): Try[T] = Success(v)
//val v = "1"
val v = "bad"
val m = Success(v)
// Monad Laws. At first glance, does `Try` pass laws if `v` == "bad"? No.
assert(f(v) == unit(v).flatMap(f))
assert(m == m.flatMap(unit))
assert(m.flatMap(f).flatMap(g) == m.flatMap(n => f(n).flatMap(g)))
// (a) Does `Try` pass laws if `f` throws a constant exception? Yes.
val ce = new RuntimeException("always the same exception instance")
def f(s: String): Try[Int] = Try { throw ce }
// (b) Does `Try` pass laws if `f` wraps an uncaught exception in Success? No.
def f(s: String): Try[Int] = if (false) Failure(ce) else Success(throw ce)
// (c) Does `Try` pass laws if `f` wraps an uncaught exception in Try? Yes.
def f(s: String): Try[Int] = if (false) Failure(ce) else Try(throw ce)

Try & the Monad Laws

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.

(a) Failures due to non-matching Exception instances

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.

(b) Failures due to #flatMap catching, but Success() not

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.

@jesinity
Copy link

thanks man!!!

@vpatryshev
Copy link

Wow; I would never think... Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment