This chapter covers
In chapter 6, you learned how to deal with optional data without having to manipulate null references by using the Option data type. As you saw, this data type is perfect for dealing with the absence of data when this isn’t the result of an error. But it’s not an efficient way to handle errors, because, although it allows you to cleanly report the absence of data, it swallows the cause of this absence. All missing data is thus treated the same way, and it’s up to the caller to try to figure out what happened, which is generally impossible.
The problems to be solved
Most of the time, the absence of data is the result of an error, either in the input data or in the computation. These are two very different cases, but they end with the same result: data is absent, and it was meant to be present. In classical imperative programming, when a function or a method takes an object parameter, most programmers know that they should test this parameter for null. What they should do if the parameter is null is often undefined. Remember the example from listing 6.3 in chapter 6 (listing 6.3):
What you need is different messages printed to the console to indicate what’s happening in each case.
If you wanted to use the types you already know, you could use a Tuple<Option<T>, Option<String>> as the return type of each method, but this is a bit complicated. Tuple is a product type, which means that the number of elements that can be represented by a Tuple<T, U> is the number of possible T multiplied by the number of possible U. You don’t need that because every time you have a value for T, you’ll have None for U. In the same way, each time U is Some, T will be None. What you need is a sum type, which means a type E<T, U> that will hold either a T or a U, but not a T and a U.
The Either type
Designing a type that can hold either a T or a U is easy. You just have to slightly modify the Option type by changing the None type to make it hold a value. You’ll also change the names. The two private subclasses of the Either type will be called Left and Right.
- Listing 7.1. The Either type
To compose methods or functions returning Either, you need to define the same methods you defined on the Option class.
Define a map method to change an Either<E, A> into an Either<E, B>, given a function from A to B. The signature of the map method is as follows (Exercise 7.1):
The Left implementation is a bit more complex than the None implementation for Option because you have to construct a new Either holding the same (error) value as the original:
For Right subclass:
The first question you might ask is, “What type should I use?” Obviously, two different types come to mind: String and RuntimeException. A string can hold an error message, as an exception does, but many error situations will produce an exception. Using a String as the type carried by the Left value will force you to ignore the relevant information in the exception and use only the included message. It’s thus better to use RuntimeException as the Left value. That way, if you only have a message, you can wrap it into an exception.
The Result type
Because the new type will generally represent the result of a computation that might have failed, you’ll call it Result. It’s very similar to the Option type, with the difference that the subclasses are named Success and Failure, as shown in the following listing.
- Listing 7.2. The Result class
Adding methods to the Result class
You’ll need the same methods in the Result class that you defined in the Option and Either classes, with small differences. Define map, flatMap, getOrElse, and orElse for the Result class. For getOrElse, you can define two methods: one taking a value as its argument, and one taking a Supplier. Here are the signatures (Exercise 7.4):
The Result class can now be used in a functional way, which means through composing methods representing computations that may succeed or fail. This is important because Result and similar types are often described as containers that may or may not contain a value. This description is partly wrong. Result is a computational context for a value that may or may not be present. The way to use it is not by retrieving the value, but by composing instances of Result using its specific methods.
You can, for example, modify the previous ToonMail example to use this class. First you have to modify the Map and Toon classes as shown in listings 7.3 and 7.4.
- Listing 7.3. The modified Mapr class with the get method returning a Result
- Listing 7.5. The modified program, using Result
Try to run this program with various input argument of the getName method, such as these:
This result may seem good, but it’s not. The problem is that Minnie, having no email, and Goofy, not being in the map, are reported as failures. They might be failures, but they might alternatively be normal cases.After all, if having no email was a failure, you wouldn’t have allowed a Toon instance to be created without one. Obviously this is not a failure, but only optional data. The same is true for the map. It might be an error if a key isn’t in the map (assuming it was supposed to be there), but from the map point of view, it’s just optional data.
You might think this isn’t a problem because you already have a type for this: the Option type you developed in chapter 6. But look at the way you’ve composed your functions:
You may think you should go the other way and convert an Option into a Result. This would work (although, in your example, you should call the new toResult method on both Option instances returned by Map.get and Toon.getMail), but it would be tedious, and because you’ll usually have to convert Option to Result, a much better way would be to cast this conversion into the Result class. All you have to do is create a new subclass corresponding to the None case, because the Some case doesn’t need conversion, apart from changing its name for Success. Listing 7.6 shows the new Result class with the new subclass called Empty.
- Listing 7.6. The new Result class handling errors and optional data
- Listing 7.7. The Mapr class using the new Result.Empty class for optional data
You may think that something is missing because you can’t distinguish between the two different empty cases, but this isn’t the case. Error messages aren’t needed for optional data, so if you think you need a message, the data isn’t optional. The success result is optional, but in that case a message is mandatory, so you should be using a Failure. This will create an exception, but nothing forces you to throw it!