Try class is broken.
Not “broken” in the sense that it doesn’t fulfill its purpose: its purpose is to sandbox
Throwables in an algebraic sum construct and provide a monadic interface over the sandbox.
It does that just fine.
Try is broken in the sense that one of its methods,
_.toOption can return malformed
Option values, violating our trust in the semantics of the
Option class and leading to potential instability down the call stack.
Purpose of Try
If you are already familiar with Scala’s
Try class, you might skip ahead.
scala.util.Try class exists as a way to get a type-level representation for the result of operations that might throw.
Take, for example, integer division, which throws when attempting to divide by zero.
/ might throw, we have wrapped the division inside the constructor for
More subtly, notice the type signature of
We don’t return an
Int, we return a
Try itself is a sealed abstract class, with exactly two subclasses,
A value of type
Try[T] can either be a
Success[T], which is simply a case class wrapping a
T, or it can be a
Failure[T], which doesn’t actually contain a
T, but instead contains a
In general, we, as the caller of a procedure that might throw an exception, have a number of ways we can deal with the possibility of a thrown exception:
- Let the program halt.
- Handle the exception.
- Pass the exception up the call stack.
Letting the program halt is okay if we’re at the top level, but not if we’re writing a library or utility functions that will be used frequently.
Handling the error requires we provide a fallback value or return a
null, which means we need to know something about what’s going to consume that value.
Again, if you’re at the top level of your program you probably know what’s going to consume the value, but not if you’re writing a function that’s going to be used in all kinds of places.
So, for maximal code reuse, we simply want to pass the exception up the call stack and let the caller decide what to do with it (this is exactly the convention in, for example,
node.js, where the first argument of every function is a placeholder for a possible exception).
Once we have one, how do we consume a value of
node.js, every function begins with an explicit null check on the first argument.
The analog of that in Scala would be case matching on the
In addition to explicit pattern matching,
Try provides a monadic interface, with methods such as
flatMap and with
StdIn.readLine(...).toInt can fail and throw, so we wrap it in a
Try and if successful bind the successful values to
Then we consume the
x / y, which can also fail and throw, so we wrap that as well and yield it to the caller, wrapped in
Here’s the important part: If anything inside a
Try constructor throws, then instead of the program halting, the
Try will resolve to
e is the thrown exception as a value of the class
Thus, any thrown exceptions get passed up to the caller, as was desired.
How Try is Broken and How To Use It Safely
Try exists to create a type-level abstraction for the results of operations that might throw,
Option exists to create a type-level abstraction for values that might be
Option is an abstract class with two implementors,
A value of type
Option[T] is either a
Some[T] which wraps a
T or the singleton
Option provides a monadic interface that I’m sure everyone in the world is familiar with by now, and it’s not all all much different from
Try in that respect.
What we’re interested in at the moment is the constructor for
Option, found in its companion object.
The constructor codifies the semantics we expect of
Option values, namely that the
Some(x) cannot be
If we have an
Option[T] in our hands, and if it is not
None, then we’re safe to extract the
T and use it without the possibility of having a null
Option both deal with failure scenarios, but a value of
Try contains strictly more information than a value of
Failure wraps a value whereas
None doesn’t wrap anything.
Because of this, there should be an obvious conversion from
Option[T], and this is exactly what
Try gives us in its
Here’s the problem:
Try makes no guarantee that the
Success(v) is not
And it shouldn’t make such a guarantee.
Try has one job, to pass thrown exceptions up the call stack, not to guard against null pointers, and
Try does its one job well.
The problem is that
_.toOption method calls
Some directly instead of calling
Options’s default constructor.
This can result in the creation of
Some(v) values where the
null, violating our expectations of
The preferred implementation of
_.toOption should replace the call to
Some with a call to
I can understand why the Scala developers would pick the implementation they did.
The implementation of
_.toOption they chose will only convert
Failure values to
None, giving a completely lossless translation of
Success values to
But information fidelity should not be the goal of such a method, especially when it violates our expectations of the
The goal of the
_.toOption method should be to return a value that behaves the way the user expects
Option values to behave, and that means no null
If we want to be able to reuse our code, we need to be able to count on the semantics of types like
Try, for exception passing, and
Option, for null guarding.
The solution to the problem presented by
_.toOption method is to either avoid its use or to flatMap over the unsafe
Option value with the default constructor, both illustrated below:
Either of those approaches will produce an
Option value that conforms to our expectations.