Search code examples
scalascalacheck

Scalacheck Try: Monadic Associativity law passes with generated functions


When i run the following property it passes:

import org.scalacheck.Prop.forAll

import scala.util.Try

forAll { (m: Try[String], f: String => Try[Int], g: Int => Try[Double]) =>
    m.flatMap(f).flatMap(g) == m.flatMap(x => f(x).flatMap(g))

}.check // + OK, passed 100 tests.

However it should fail as Try is not monadic (same applies to "left identity")

When not generating the functions it works as expected and the property fails:

val f2 = (s: String) => Try { s.toInt }
val g2 = (i: Int) => Try { i / 2d }

forAll { (m: Try[String]) =>
    m.flatMap(f2).flatMap(g2) == m.flatMap(x => f2(x).flatMap(g2))
}.check  // ! Falsified after 0 passed tests.

Why does it not work with the generated functions?


Solution

  • First of all, monadic associativity law is supposed to pass here. Try only violates the left identity law:

    unit(x).flatMap(f) == f(x)
    

    because Try will never throw an exception that happens inside of it (this was by design; left identity was willingly traded for more safety). So if f throws an exception, left side will be a failed Try, and right side will simply blow up.

    But associativity law:

    m.flatMap(f).flatMap(g) == m.flatMap(x ⇒ f(x).flatMap(g))
    

    should hold. Both sides should either succeed or fail every time, but it's impossible to blow up one side and not the other one, since both f and g are inside a flatMap on both sides.

    What happens here is that your second snippet, the one where you define the functions yourself, throws different kinds of exceptions. If you simply print out what's going on:

    ...
      {
        val fst = m.flatMap(f).flatMap(g)
        val snd = m.flatMap(x => f(x).flatMap(g))
        println(fst)
        println(snd)
        fst == snd
      }
    }.check
    ...
    

    you can see it for yourself. Here's the first case, with undefined functions:

    // ...
    // Failure(java.lang.Error)
    // Failure(java.lang.Error)
    // Success(5.16373771232299E267)
    // Success(5.16373771232299E267)
    // Failure(java.lang.Exception)
    // Failure(java.lang.Exception)
    // Failure(java.lang.Error)
    // Failure(java.lang.Error)
    // ...etc...
    

    And now the second case, with defined functions:

    // ...
    // Failure(java.lang.Error)
    // Failure(java.lang.Error)
    // Failure(java.lang.Exception)
    // Failure(java.lang.Exception)
    // Failure(java.lang.NumberFormatException: For input string: "걡圤")
    // Failure(java.lang.NumberFormatException: For input string: "걡圤")
    

    This is the thing. Second one at some point provides a weird, Chinese-character-filled string to your f2; this case never happens in the first scenario. This has to do with the way Scalacheck generates test cases. Given a function String => Try[Int], it will either come up with a valid integer or make up some generic exception in order to create a failure case. It will not use concrete functions defined on String (such as toInt, which you used).

    So, second scenario causes a number format exception. Why do exceptions in the first scenario yield true every time they are compared, and number format exception in the second scenario yields false when compared? I will leave this to Java gurus. I think it has to do with the fact that == relies on Java's equals, which compares references, but null == null yields true, so I guess at some point internal fields of exceptions are compared, first scenario yielding nulls all around the place (remember, these exceptions are general since they are made up by Scalacheck), and second scenario having actual exceptions (more specifically, java.lang.NumberFormatException) with actual objects inside them, which causes seemingly identical exceptions to yield false upon equality comparison. Try changing the test condition from fst == snd to fst.toString == snd.toString and you will see that both scenarios will be passing.

    Sorry about not providing a 100% complete answer, but I would need to invest a lot of time into debugging silly Java handling of objects and references and how equality is implemented on various levels and classes of exceptions, not to mention the whole "null == null is true" philosophical predicament. If you're not that interested in Java quirks (like me), then I guess this answers your question.