2018年1月16日 星期二

[ FP with Java ] Ch7 - Handling errors and exceptions - Part2

Advanced Result handling 
So far, you’ve seen a very limited use of ResultResult should never be used for directly accessing the wrapped value (if it exists). The way you used Result in the previous example corresponds to the simpler specific composition use case: get the result of one computation and use it for the input of the next computation. More specific use cases exist. You could choose to use the result only if it matches some predicate (which means some condition). You could also use the failure case, for which you’d need to map the failure to something else, or transform the failure into a success of exception (Success). You might also need to use several Results as the input for a single computation. You’d probably benefit from some helper methods that create Result from computations, in order to deal with legacy code. Finally, you’ll sometimes need to apply effects to Results. 

Applying predicates 
Applying a predicate to a Result is something that you’ll often have to do. This is something that can easily be abstracted, so that you can write it only once. Let's write a method filter taking a condition that’s represented by a function from Tto Boolean, and returning a Result, which will be a Success or a Failure depending on whether the condition holds for the wrapped value. The signature will be (Exercise 7.5): 
  1. filter(Function<T, Boolean> f);
Create a second method taking a condition as its first argument and a String as a second argument, and using the string argument for the potential Failure case. Although it’s possible to define abstract methods in the Result class and implement them in subclasses, try not to do so. Instead use one or more methods you’ve previously defined to create a single implementation in the Result class. 

You have to create a function that takes the wrapped value as a parameter, applies the function to it, and returns the same Result if the condition holds or Empty (or Failure) otherwise. Then all you have to do is flatMap this function: 
  1. public Result<V> filter(Function<V, Boolean> p) {  
  2.     return flatMap(x -> p.apply(x)  
  3.           ? this  
  4.           : failure("Condition not matched"));  
  5. }  
  6.   
  7. public Result<V> filter(Function<V, Boolean> p, String message) {  
  8.     return flatMap(x -> p.apply(x)  
  9.           ? this  
  10.           : failure(message));  
  11. }  
  12.  
To continue, let's define an exists method that takes a function from T to Boolean and returns true if the wrapped value matches the condition, or false otherwise. Here’s the method signature: 
  1. boolean exists(Function<T, Boolean> p);  
The solution is simply to map the function to Result, giving a Result, and then to use getOrElse with false as the default value. You don’t need to use a Supplier because the default value is a literal: 
  1. public boolean exists(Function<V, Boolean> p) {  
  2.     return map(p).getOrElse(false);  
  3. }
  4.   
Using exists as the name of this method may seem questionable. But it’s the same method that could be applied to a list, returning true if at least one element satisfies the condition, so it makes sense to use the same name. Some might argue that this implementation would also work for a forAll method that returns true if all elements in the list fulfill the condition. It’s up to you either to choose another name or to define a forAll method in the Result class with the same implementation. The important point is understanding what makes List and Result similar and what makes them different. 

Mapping failures 
It’s sometimes useful to change a Failure into a different one, as in the following example. 
- Listing 7.10. A memory monitor 


In multithreaded Java programs, an OutOfMemoryError (OOME) will often crash a thread but not the application, leaving it in an indeterminate state. To solve this problem, you have to catch the error and cleanly stop the application. Let's define a mapFailure method that takes a String as its argument and transforms a Failure into another Failure using the string as its error message. If the Result is Empty or Success, this method should do nothing. Here’s the abstract method in the parent class: 
  1. public abstract Result<T> mapFailure(String s);  
The Empty and Success implementations just return this: 
  1. public Result<T> mapFailure(String s) {  
  2.   return this;  
  3. }
  4.   
The Failure implementation wraps the existing exception into a new one created with the given message. It then creates a new Failure by calling the corresponding static factory method: 
  1. public Result<T> mapFailure(String s) {  
  2.   return failure(new IllegalStateException(s, exception));  
You could choose RuntimeException as the exception type, or a more specific custom subtype of RuntimeException. Note that some other methods of the same kind might be useful, such as these: 
  1. public abstract Result<T> mapFailure(String s, Exception e);  
  2. public abstract Result<T> mapFailure(Exception e);
Another useful method would be one that maps an Empty to a Failure, given a String message. 

Adding factory methods 
You’ve seen how Success and Failure can be created from a value. Some other use cases are so frequent that they deserve to be abstracted into supplemental static factory methods. To adapt legacy libraries, you’ll probably often create Result from a value that could possibly be null. To do this, you could use a static factory method with the following signatures: 
  1. public static <T> Result<T> of(T value)  
  2. public static <T> Result<T> of(T value, String message)
A method creating a Result from a function from T to Boolean and an instance of T might also be useful: 
  1. public static <T> Result<T> of(Function<T, Boolean> predicate, T value)  
  2. public static <T> Result<T> of(Function<T, Boolean> predicate,  
  3.                                            T value, String message)
  4.   
Let's define these static factory methods (Exercise 7.8). There are no difficulties in this exercise. Here are possible implementations, based on the choice to return Empty when no error message is used, and a Failure otherwise: 
  1. public static <T> Result<T> of(T value) {  
  2.     return value != null ? success(value) : Result.failure("Null value");  
  3. }  
  4.   
  5. public static <T> Result<T> of(T value, String message) {  
  6.     return value != null ? success(value) : failure(message);  
  7. }  
  8.   
  9. public static <T> Result<T> of(Function<T, Boolean> predicate, T value) {  
  10.     try {  
  11.         return predicate.apply(value) ? success(value) : empty();  
  12.     } catch (Exception e) {  
  13.         String errMessage = String.format("Exception while evaluating predicate: %s", value);  
  14.         return Result.failure(new IllegalStateException(errMessage, e));  
  15.     }  
  16. }  
  17.   
  18. public static <T> Result<T> of(Function<T, Boolean> predicate, T value, String message) {  
  19.     try {  
  20.         return predicate.apply(value) ? Result.success(value) : Result.failure(String.format(message, value));  
  21.     } catch (Exception e) {  
  22.         String errMessage = String.format("Exception while evaluating predicate: %s",  
  23.                 String.format(message, value));  
  24.         return Result.failure(new IllegalStateException(errMessage, e));  
  25.     }  
  26. }
  27.   
Note that you should handle the possibility that the message parameter may be null. Not doing so would throw an NPE, so a null message would be considered a bug. Instead, you could check the parameter and use a default value in the case of null. This is up to you. In any case, consistently checking parameters for null should be abstracted, as you’ll see in chapter 15

Applying effects 
So far, you haven’t applied any effects to values wrapped in Result, other than by getting these values (through getOrElse). This isn’t satisfying because it destroys the advantage of using Result. On the other hand, you haven’t yet learned the necessary techniques to apply effects functionally. Effects include anything that modifies something in the outside world, such as writing to the console, to a file, to a database, or to a field in a mutable component, or sending a message locally or over a network. 

The technique I’ll show you now isn’t functional, but it is an interesting abstraction that allows you to use Result without knowing the functional techniques involved. You can use the technique shown here until we look at the functional versions, or you may even find that this is powerful enough to be used on a regular basis. The technique discussed in this section is the approach taken by the functional constructs of Java 8, which isn’t surprising, because Java isn’t a functional programming language

To apply an effect, use the Effect interface you developed in chapter 3. This is a very simple functional interface: 
  1. public interface Effect<T> {  
  2.   void apply(T t);  
You could name this interface Consumer and define an accept method instead, as is the case in Java 8. I’ve already said that this name was very badly chosen, because a Consumer should have a consume method. But, in fact, a v doesn’t consume anything—after applying an effect to a value, the value is left unchanged and is still available for further computations or effects. 

Let's define a forEach method that takes an Effect as its parameter and applies it to the wrapped value. Here’s the abstract method declaration in Effect
  1. public abstract void forEach(Effect<T> ef)  
The Empty and Failure implementations do nothing. As a result, you only need to implement the method in Empty, because Failure extends this class: 
  1. public void forEach(Effect<T> ef) {  
  2.   // Empty. Do nothing.  
  3.   
The Success implementation is straightforward. You just have to apply the effect to the value: 
  1. public void forEach(Effect<T> ef) {  
  2.   ef.apply(value);  
  3. }
  4.   
This forEach method would be perfect for the Option class you created in chapter 6. But that’s not the case for Result. Generally, you want to take special actions on a failure. One simple way to handle failure is to throw the exception. 

Let's define the forEachOrThrow method to handle this use case. Here’s its signature in the Result class: 
  1. public abstract void forEachOrThrow(Effect<V> ef)  
The Success implementation is identical to that of the forEach method. The Failure implementation just throws the wrapped exception: 
  1. public void forEachOrThrow(Effect<V> ef) {  
  2.   throw exception;  
  3. }
  4.   
The Empty implementation is more of a problem. You can choose to do nothing, considering that Empty isn’t an error. Or you can decide that calling forEachOrThrow means that you want to convert the absence of data into an error. This is a tough decision to make. Empty is not an error by itself. And if you need to make it an error, you can use one of the mapFailure methods, so it’s probably better to implement forEachOrThrow in Empty as a do-nothing method. 

The more general use case when applying an effect to Result is applying the effect if it’s a Success, and handling the exception in some way if it’s a Failure. The forEachOrThrow method is fine for throwing, but sometimes you just want to log the error and continue. Rather than defining a method for logging, define a forEachOrException method that will apply an effect if a value is present and return a Result. This Result will be Empty if the original Result was a Success, or Empty and Success  if it was a Failure. (Exercise 7.11

The method is declared as abstract in the Result parent class: 
  1. public abstract Result<RuntimeException> forEachOrException(Effect<V> ef);  
The Empty implementation returns Empty
  1. @Override  
  2. public Result<RuntimeException> forEachOrException(Effect<V> ef) {  
  3.     return empty();  
  4. }
  5.   
The Success implementation applies the effect to the wrapped value and returns Empty
  1. public Result<RuntimeException> forEachOrException(Effect<V> ef) {  
  2.   ef.apply(value);  
  3.   return empty();  
  4. }
  5.   
The Failure implementation returns a Success holding the original exception, so that you can act on it: 
  1. public Result<RuntimeException> forEachOrException(Effect<V> ef) {  
  2.   return success(exception);  
  3. }
  4.   
The typical use case for this method is as follows (using a hypothetical Logger type with a log method): 
  1. Result<Integer> result = getComputation();  
  2.   
  3. result.forEachOrException(System.out::println).forEach(Logger::log);
  4.   
Remember that these methods aren’t functional, but they are a good and simple way to use Result. If you prefer to apply effects functionally, you’ll have to wait until chapter 13

Advanced result composition 
Use cases for Result are more or less the same as for Option. In the previous chapter, you defined a lift method for composing Options by transforming a function from A to B into a function from Option to Option. You can do the same for Result. Let's write a lift method for Result. This will be a static method in the Result class with the following signature: 
  1. static <A, B> Function<Result<A>, Result<B>> lift(final Function<A, B> f)
Here’s the very simple solution: 
  1. public static <A, B> Function<Result<A>, Result<B>> lift(final Function<A, B> f) {  
  2.     return x -> {  
  3.         try {  
  4.             return x.map(f);  
  5.         } catch (Exception e) {  
  6.             return failure(e);  
  7.         }  
  8.     };  
  9. }
  10.   
Let's define lift2 for lifting a function from A to B to C, and lift3 for functions from A to B to C to D, with the following signatures: 
  1. public static <A, B, C> Function<Result<A>, Function<Result<B>,  
  2.                         Result<C>>> lift2(Function<A, Function<B, C>> f)  
  3. public static <A, B, C, D> Function<Result<A>,  
  4.             Function<Result<B>, Function<Result<C>,  
  5.             Result<D>>>> lift3(Function<A, Function<B, Function<C, D>>> f)
Here are the solutions: 
  1. public static <A, B, C> Function<Result<A>, Function<Result<B>,  
  2.                         Result<C>>> lift2(Function<A, Function<B, C>> f) {  
  3.   return a -> b -> a.map(f).flatMap(b::map);  
  4. }  
  5.   
  6. public static <A, B, C, D> Function<Result<A>,  
  7.           Function<Result<B>, Function<Result<C>,  
  8.           Result<D>>>> lift3(Function<A, Function<B, Function<C, D>>> f) {  
  9.   return a -> b -> c -> a.map(f).flatMap(b::map).flatMap(c::map);  
  10. }  
  11.  
I guess you can see the pattern. You could define lift for any number of parameters that way. 

In chapter 6, you defined a map2 method, taking as its arguments an Option, an Option, and a function from A to B to C, and returning an Option. Define a map2 method for Result (Exercise 7.14). The solution defined for Option was: 
  1. <A, B, C> Option<C> map2(Option<A> a,  
  2.                          Option<B> b,  
  3.                          Function<A, Function<B, C>> f) {  
  4.   return a.flatMap(ax -> b.map(bx -> f.apply(ax).apply(bx)));  
  5. }
This is the same pattern you used for lift2. So the map2 method will look like this: 
  1. public static <A, B, C> Result<C> map2(Result<A> a,  
  2.                                        Result<B> b,  
  3.                                        Function<A, Function<B, C>> f) {  
  4.   return lift2(f).apply(a).apply(b);  
  5.  
A common use case for such functions is calling methods or constructors with arguments of type Result returned by other functions or methods. Take the previous ToonMail example. To populate the Toon map, you could construct toons by asking the user to input the first name, last name, and mail on the console, using the following methods: 
  1. static Result<String> getFirstName() {  
  2.   return success("Mickey");  
  3. }  
  4.   
  5. static Result<String> getLastName() {  
  6.   return success("Mickey");  
  7. }  
  8.   
  9. static Result<String> getMail() {  
  10.   return success("mickey@disney.com");  
  11. }  
  12.  
The real implementation will be different, but you still have to learn how to functionally get input from the console. For now, you’ll use these mock implementations. Using these implementations, you could create a Toon as follows: 
  1. Function<String, Function<String, Function<String, Toon>>> createPerson =  
  2.                                           x -> y -> z -> new Toon(x, y, z);  
  3. Result<Toon> toon2 = lift3(createPerson)  
  4.     .apply(getFirstName())  
  5.     .apply(getLastName())  
  6.     .apply(getMail());
  7.  
But you’re reaching the limits of abstraction. You might have to call methods or constructors with more than three arguments. In such a case, you could use the following pattern: 
  1. Result<Toon> toon = getFirstName()  
  2.           .flatMap(firstName -> getLastName()  
  3.               .flatMap(lastName -> getMail()  
  4.                   .map(mail -> new Toon(firstName, lastName, mail))));
This pattern has two advantages: 
* You can use any number of arguments.
* You don’t need to define a function.

Supplement 
FP with Java - Ch7 - Handling errors and exceptions - Part1 
FP with Java - Ch7 - Handling errors and exceptions - Part2

沒有留言:

張貼留言

[ Py DS ] Ch1 - IPython: Beyond Normal Python

Source From  Here   Keyboard Shortcuts in the IPython Shell   If you spend any amount of time on the computer, you’ve probably found a u...