Martin Odersky in the Reactive Programming course mentions that Try types are not true monads as they violate the Left Unit rule. But if you try (no pun intended) it, Try does indeed appear to obey the rules for monads.
Just to recap, here are the monad rules expressed in Scala:
def testMonadicPropertiesOfTry[T, U](f: T => Try[U], g: U => Try[U], m: Try[T], x: T, unit: T => Try[T]): Boolean = {
// Associativity: (m flatMap f) flatMap g == m flatMap (x => f(x) flatMap g)
def associativity: Boolean = {
val associativityLhs = (m flatMap f) flatMap g
val associativityRhs = m flatMap (x => f(x) flatMap g)
assertEqual(associativityLhs, associativityRhs)
}
val associativityResult = Try(associativity)
// Left unit: unit(x) flatMap f == f(x)
def leftUnit: Boolean = {
val leftUnitLhs = unit(x) flatMap f
val leftUnitRhs = f(x)
assertEqual(leftUnitLhs, leftUnitRhs)
}
val leftUnitResult = Try(leftUnit)
// Right unit: m flatMap unit == m
def rightUnit: Boolean = {
val rightUnitLhs = m flatMap unit
assertEqual(rightUnitLhs, m)
}
val rightUnitResult = Try(rightUnit)
(associativityResult, leftUnitResult, rightUnitResult) match {
case (Success(_), Success(_), Success(_)) => true
case _ => false
}
}
def assertEqual[T](try1: Try[T], try2: Try[T]): Boolean = {
try1 match {
case Success(v1) => try2 match {
case Success(v2) => v1 == v2
case _ => false
}
case Failure(x1) => try2 match {
case Failure(x2) => x1.toString == x2.toString
case _ => false
}
}
}
Now, if we run the code below (borrowed liberally from here) where we're deliberately trying to cause a java.lang.NumberFormatException as the code attempts to convert our 'a' into a numeric:
def factory[T](x: => T): Try[T] = Try(x)
def unit[T](x: T): Try[T] = factory(x)
def f(x: String): Try[Int] = factory(x.toInt)
def g(x: Int): Try[Int] = factory(x + 1)
val x = "a"
val m = factory(x)
val isMonadic = testMonadicPropertiesOfTry(f, g, m, x, unit[String])
println("is monadic? " + isMonadic)
Mauricio Linhares says: "there is some debate as to if Try[U] is a full monad or not. The problem is that if you think unit(x) is Success(x), then exceptions would be raised when you try to execute the left unit law since flatMap will correctly wrap an exception but the f(x) might not be able to do it. Still, if you assume that the correct unit is Try.apply then this would not be an issue."
So, let's take the first line of the last code snippet and make it thus:
def factory[T](x: => T): Try[T] = Success(x)
val leftUnitLhs = unit(x) flatMap f
works fine but:
val leftUnitRhs = f(x)
blows up. The left hand side does not equal the right.
The reason for this is that Success.flatMap catches any NonFatal exceptions just like the constructor of Try. But the constructor of Success does not. And it's this asymmetry that means Try acts like a monad and Success does not.
Further reading
An interesting debate about monads and exceptions here.
No comments:
Post a Comment