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):
- Option<String> goofy = toons.get("Goofy").flatMap(Toon::getEmail);
- ...
- System.out.println(goofy.getOrElse(() -> "No data"));
- Option<String> toon = getName()
- .flatMap(toons::get)
- .flatMap(Toon::getEmail);
- System.out.println(toon.getOrElse(() -> "No data"));
- Option<String> getName() {
- String name = // retrieve the name from the user interface
- return name;
- }
- Option<String> toon = getName()
- .flatMap(Example::validate)
- .flatMap(toons::get)
- .flatMap(Toon::getEmail);
- System.out.println(toon.getOrElse(() -> "No data"));
- Option<String> getName() {
- try {
- String name = // retrieve the name from the user interface
- return Option.some(name);
- } catch (Exception e) {
- return Option.none();
- }
- }
- Option<String> validate(String name) {
- return name.length() > 0 ? Option.some(name) : Option.none();
- }
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
- package fp.utils;
- public abstract class Either<T, U> {
- private static class Left<T, U> extends Either<T, U> {
- private final T value;
- private Left(T value) {
- this.value = value;
- }
- @Override
- public String toString() {
- return String.format("Left(%s)", value);
- }
- }
- private static class Right<T, U> extends Either<T, U> {
- private final U value;
- private Right(U value) {
- this.value = value;
- }
- @Override
- public String toString() {
- return String.format("Right(%s)", value);
- }
- }
- public static <T, U> Either<T, U> left(T value) {
- return new Left<>(value);
- }
- public static <T, U> Either<T, U> right(U value) {
- return new Right<>(value);
- }
- }
- <A extends Comparable<A>> Function<List<A>, Either<String, A>> max() {
- return xs -> xs.isEmpty()
- ? Either.left("max called on an empty list")
- : Either.right(xs.foldLeft(xs.head(), x -> y -> x.compareTo(y) < 0 ?
- x : y));
- }
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):
- public abstract <B> Either<E, B> map(Function<A, B> f);
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:
- public <B> Either<E, B> map(Function<A, B> f) {
- return new Left<>(value);
- }
- public <B> Either<E, B> map(Function<A, B> f) {
- return new Right<>(f.apply(value));
- }
- public abstract <B> Either<E, B> flatMap(Function<A, Either<E, B>> f);
- public <B> Either<E, B> flatMap(Function<A, Either<E, B>> f) {
- return new Left<>(value);
- }
- public <B> Either<E, B> flatMap(Function<A, Either<E, B>> f) {
- return f.apply(value);
- }
- A getOrElse(Supplier<A> defaultValue)
- Either<E, A> orElse(Supplier<Either<E, A>> defaultValue)
- public Either<E, A> orElse(Supplier<Either<E, A>> defaultValue) {
- return defaultValue.get();
- }
- public A getOrElse(Supplier<A> defaultValue) {
- return defaultValue.get();
- }
For Right subclass:
- public Either<E, A> orElse(Supplier<Either<E, A>> defaultValue) {
- return this;
- }
- public A getOrElse(Supplier<A> defaultValue) {
- return value;
- }
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
- package test.fp.utils;
- import java.io.Serializable;
- public abstract class Result<V> implements Serializable{
- private static final long serialVersionUID = 1L;
- private Result(){}
- private static class Failure<V> extends Result<V>{
- private final RuntimeException exception;
- private Failure(String message){
- super();
- this.exception = new IllegalStateException(message);
- }
- private Failure(RuntimeException e){
- super();
- this.exception = e;
- }
- private Failure(Exception e){
- super();
- this.exception = new IllegalStateException(e.getMessage(), e);
- }
- @Override
- public String toString(){
- return String.format("Failure(%s)", exception.getMessage());
- }
- }
- private static class Success<V> extends Result<V>{
- private final V value;
- private Success(V value){
- super();
- this.value = value;
- }
- @Override
- public String toString(){
- return String.format("Success(%s)", value.toString());
- }
- }
- public static <V> Result<V> failure(String message){ return new Failure<>(message);}
- public static <V> Result<V> failure(Exception e){ return new Failure<>(e); }
- public static <V> Result<V> failure(RuntimeException e){ return new Failure<>(e); }
- public static <V> Result<V> success(V value){ return new Success<>(value); }
- }
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):
- public abstract V getOrElse(final V defaultValue);
- public abstract V getOrElse(final Supplier<V> defaultValue);
- public abstract <U> Result<U> map(Function<V, U> f);
- public abstract <U> Result<U> flatMap(Function<V, Result<U>> f);
- public Result<V> orElse(Supplier<Result<V>> defaultValue)
- public V getOrElse(V defaultValue) {
- return value;
- }
- public V getOrElse(Supplier<V> defaultValue) {
- return value;
- }
- public <U> Result<U> map(Function<V, U> f) {
- try {
- return success(f.apply(value));
- } catch (Exception e) {
- return failure(e);
- }
- }
- public <U> Result<U> flatMap(Function<V, Result<U>> f) {
- try {
- return f.apply(value);
- } catch (Exception e) {
- return failure(e);
- }
- }
- public V getOrElse(V defaultValue) {
- return defaultValue;
- }
- public V getOrElse(Supplier<V> defaultValue) {
- return defaultValue.get();
- }
- public <U> Result<U> map(Function<V, U> f) {
- return failure(exception);
- }
- public <U> Result<U> flatMap(Function<V, Result<U>> f) {
- return failure(exception);
- }
- public Result<V> orElse(Supplier<Result<V>> defaultValue) {
- return map(x -> this).getOrElse(defaultValue);
- }
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
- package fp.utils;
- import java.util.concurrent.ConcurrentHashMap;
- import java.util.concurrent.ConcurrentMap;
- public class Mapr <T, U> {
- private final ConcurrentMap<T, U> map = new ConcurrentHashMap<>();
- public static <T, U> Mapr<T, U> empty(){ return new Mapr<>(); }
- public static <T, U> Mapr<T, U> add(Mapr<T, U> m, T t, U u)
- {
- m.map.put(t, u);
- return m;
- }
- public Result<U> get(final T t)
- {
- return this.map.containsKey(t)
- ? Result.success(this.map.get(t))
- : Result.failure(String.format("Key=%s is not found in mapr", t));
- }
- public Mapr<T, U> put(T t, U u)
- {
- return add(this, t, u);
- }
- public Mapr<T, U> removeKey(T t)
- {
- this.map.remove(t);
- return this;
- }
- }
- package fp.ch7;
- import fp.utils.Result;
- public class Toon {
- private final String firstName;
- private final String lastName;
- private final Result<String> email;
- Toon(String firstName, String lastName)
- {
- this.firstName = firstName;
- this.lastName = lastName;
- this.email = Result.failure(String.format("%s %s has no email", firstName, lastName));
- }
- Toon(String firstName, String lastName, String email)
- {
- this.firstName = firstName;
- this.lastName = lastName;
- this.email = Result.success(email);
- }
- public Result<String> getEmail(){ return email; }
- }
- Listing 7.5. The modified program, using Result
- package fp.ch7;
- import fp.utils.Mapr;
- import fp.utils.Result;
- public class ToonMail {
- public static void main(String[] args)
- {
- Mapr<String, Toon> toons = new Mapr<String,Toon>()
- .put("Mickey", new Toon("Mickey", "Mouse", "mickey@diseny.com"))
- .put("Minnie", new Toon("Minnie", "Mouse"))
- .put("Donald", new Toon("Donald", "Duck", "donald@disney.com"));
- Result<String> result = getName("Mickey").flatMap(toons::get).flatMap(Toon::getEmail);
- System.out.printf("Result=%s\n", result);
- }
- public static Result<String> getName(String name)
- {
- return Result.success(name);
- }
- }
Try to run this program with various input argument of the getName method, such as these:
- System.out.printf("%s\n", getName("Mickey").flatMap(toons::get).flatMap(Toon::getEmail));
- System.out.printf("%s\n", getName(new IOException("Input error")).flatMap(toons::get).flatMap(Toon::getEmail));
- System.out.printf("%s\n", getName("Minnie").flatMap(toons::get).flatMap(Toon::getEmail));
- System.out.printf("%s\n", getName("Goofy").flatMap(toons::get).flatMap(Toon::getEmail));
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:
- getName().flatMap(toons::get).flatMap(Toon::getEmail);
- public abstract Option<V> toOption()
- public Option<V> toOption() {
- return Option.some(value);
- }
- public Option<V> toOption() {
- return Option.none();
- }
- Option<String> result =
- getName().toOption().flatMap(toons::get).flatMap(Toon::getEmail);
- public class Toon {
- private final String firstName;
- private final String lastName;
- private final Option<String> email;
- Toon(String firstName, String lastName) {
- this.firstName = firstName;
- this.lastName = lastName;
- this.email = Option.none();
- }
- Toon(String firstName, String lastName, String email) {
- this.firstName = firstName;
- this.lastName = lastName;
- this.email = Option.some(email);
- }
- public Option<String> getEmail() {
- return email;
- }
- }
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
- public abstract class Result<V> implements Serializable{
- private static final long serialVersionUID = 1L;
- @SuppressWarnings("rawtypes")
- private static Result empty = new Empty();
- ...
- private static class Empty<V> extends Result<V>{
- public Empty(){super();}
- @Override
- public V getOrElse(final V defaultValue){ return defaultValue; }
- @Override
- public <U> Result<U> map(Function<V, U> f){ return empty; }
- @Override
- public <U> Result<U> flatMap(Function<V, Result<U>> f){ return empty; }
- @Override
- public String toString(){ return "Empty()"; }
- @Override
- public V getOrElse(Supplier<V> defaultValue){ return defaultValue.get(); }
- }
- ...
- @SuppressWarnings("unchecked")
- public static <V> Result<V> empty(){ return empty; }
- }
- Listing 7.7. The Mapr class using the new Result.Empty class for optional data
- package fp.utils;
- import java.util.concurrent.ConcurrentHashMap;
- import java.util.concurrent.ConcurrentMap;
- public class Mapr <T, U> {
- private final ConcurrentMap<T, U> map = new ConcurrentHashMap<>();
- public static <T, U> Mapr<T, U> empty(){ return new Mapr<>(); }
- public static <T, U> Mapr<T, U> add(Mapr<T, U> m, T t, U u)
- {
- m.map.put(t, u);
- return m;
- }
- public Result<U> get(final T t)
- {
- return this.map.containsKey(t)
- ? Result.success(this.map.get(t))
- : Result.empty(); // The get method now returns Result.emtpy() if key isn't found
- }
- public Mapr<T, U> put(T t, U u)
- {
- return add(this, t, u);
- }
- public Mapr<T, U> removeKey(T t)
- {
- this.map.remove(t);
- return this;
- }
- }
- package fp.ch7;
- import fp.utils.Result;
- public class Toon {
- private final String firstName;
- private final String lastName;
- private final Result<String> email;
- Toon(String firstName, String lastName)
- {
- this.firstName = firstName;
- this.lastName = lastName;
- this.email = Result.empty(); // If you construct the instance without an email...
- }
- Toon(String firstName, String lastName, String email)
- {
- this.firstName = firstName;
- this.lastName = lastName;
- this.email = Result.success(email);
- }
- public Result<String> getEmail(){ return email; }
- }
- package fp.ch7;
- import java.io.IOException;
- import fp.utils.Mapr;
- import fp.utils.Result;
- public class ToonMail {
- public static void main(String[] args)
- {
- Mapr<String, Toon> toons = new Mapr<String,Toon>()
- .put("Mickey", new Toon("Mickey", "Mouse", "mickey@diseny.com"))
- .put("Minnie", new Toon("Minnie", "Mouse"))
- .put("Donald", new Toon("Donald", "Duck", "donald@disney.com"));
- System.out.printf("%s\n", getName("Mickey").flatMap(toons::get).flatMap(Toon::getEmail));
- System.out.printf("%s\n", getName(new IOException("Input error")).flatMap(toons::get).flatMap(Toon::getEmail));
- System.out.printf("%s\n", getName("Minnie").flatMap(toons::get).flatMap(Toon::getEmail));
- System.out.printf("%s\n", getName("Goofy").flatMap(toons::get).flatMap(Toon::getEmail));
- }
- public static Result<String> getName(String name)
- {
- return Result.success(name);
- }
- public static Result<String> getName(Exception e)
- {
- return Result.failure(e);
- }
- }
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!
沒有留言:
張貼留言