Skip to content

Instantly share code, notes, and snippets.

@shajra
Last active July 16, 2017 18:19
Show Gist options
  • Save shajra/6290214 to your computer and use it in GitHub Desktop.
Save shajra/6290214 to your computer and use it in GitHub Desktop.
my thoughts on scala.util.Try, scalaz.concurrent.Task, and referential transparency

When scala.util.Try first came out, there was a lot of discussions about the validity of it's implementation. Some discussions got distracted with issues beyond the facts. Ultimately, the issue came down to the following:

  1. Is Try a monad?

  2. Does referential transparency (RT) apply in the presence of possible non-terminating or errant functions -- those returning ⊥?

Not requiring Try to be a monad is an easy solution to the whole problem. If it's not a monad, there's no laws restricting it's implementation. It does what it does, and we hope it's useful.

However, if it's a monad, it needs to obey certain laws. One law that breaks is the left identity monad law:

return a >>= f ≡ f a

but it only breaks when f throws an exception or fails to terminate (returns ⊥):

scala> def f[A, B](a: A): Try[B] = throw new RuntimeException("fail")
f: [A, B](a: A)scala.util.Try[B]

scala> f(1)
java.lang.RuntimeException: fail

scala> Try(1) flatMap f
res0: scala.util.Try[Nothing] = Failure(java.lang.RuntimeException: fail)

Though the two calls should be the same, the first one throws returns ⊥ (by throwing an exception), but the second call returns an instance of Failure, which is a value and not ⊥.

We know we want RT for total functions. But in the Turing complete languages we use, it's too difficult to exclude the introduction of partial functions. The benefits of equational reasoning that we derive from RT are so great we'd like not to sacrifice it all, just because we occaisionally get an exceptional condition.

Typesafe has shown strong resistance to changing the implementation of Try, but this problem surfaces in another monad implementation -- ScalaZ's Task, which is a variant of their Future implementation that catches exceptions.

Here's what ScalaZ's Task does in version 7.0.2 of the library:

scala> def f[A, B](a: A): Task[B] = throw new RuntimeException("fail")
f: [A, B](a: A)scalaz.concurrent.Task[B]

scala> f(1) attemptRun
java.lang.RuntimeException: fail

scala> Task(1) flatMap f attemptRun
res1: scalaz.\/[Throwable,Nothing] = -\/(java.lang.RuntimeException: fail)

This is the same problem as scala.util.Try. The first call throws an exception (⊥), while the other returns an instance of Scala's / type (not ⊥).

This problem was addressed for ScalaZ's Task in versions 7.0.3 and 7.0.1-M2:

scala> def f[A, B](a: A): Task[B] = throw new RuntimeException("fail")
f: [A, B](a: A)scalaz.concurrent.Task[B]

scala> f(1) attemptRun
java.lang.RuntimeException: fail

scala> Task(1) flatMap f attemptRun
^C

Now, rather than returning /, the thread hangs (because the exception has been thrown unhandled in a dependent thread that the first is blocking on). Operationally, this is a different failure, but logically, the non-terminating computation is the same ⊥ type as the exception. Thus we have referential transparency, but the possible added inconvenience of non-termination.

However, is this really a fix? I've suffixed attemptRun in this expression for one particular illustration of the monad law, but the expressions should be the same even without this ancillary call. But this is not the case:

scala> f(1)
java.lang.RuntimeException: fail

scala> Task(1) flatMap f
res1: scalaz.concurrent.Task[Nothing] = scalaz.concurrent.Task@3248b991

As we'd expect the first call returns ⊥ again. But the second one still doesn't return ⊥, returning an instance of a Task instead.

In a language like Scala's this expression could be easily computed, but not used. Because Scala is strictly evaluated, this first version would make the whole program return ⊥. But the second version would allow the program to continue on unaffected.

In this regard, the fix for Task seems:

  1. incomplete, and therefore not much better from a correctness perspective

  2. now more inconvenient to use operationally.

Does this mean that Task is better off the way it was? I'm not sure I feel that way. But it does not feel right the way it is. The dilemma feels in our face. f(1) is going to throw an exception regardless of the implementation of Task. This seems to imply that Task(1) flatMap f must return ⊥ as well. But how can we do that without evaluating the Task? This wouldn't be a problem if the Task had no side-effects, but ScalaZ's Task and Future implementations were designed with side-effects in mind. They are supposed to be asynchronous effect tracking types.

Copy link

ghost commented Oct 10, 2014

I would not be surprised that both Try's satisfy the applicative functor laws.
So they fit somewhere in a theoretical model.
The applicative functor application applies a Try[A => B] to a Try[A] in order to result in a Try[B].
So we do not have to bother about normal functions throwing exceptions.
The applicative functor application can be implemented to be consistent with the monad application
[ for { a <- tryA ; a2b <- tryA2B } yield (a2b(a)) ], in other words, fail fast, but does not need to
(it could fail slow and accumulate errors like ScalaZ's validation).

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