2018年1月10日 星期三

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

Preface 
This chapter covers 
* Holding information about errors with the Either type
* Easier error handling with the biased Result type
* Accessing the data inside a Result
* Applying effects to Result data
* Lifting functions to operate on Result

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): 
  1. Option<String> goofy = toons.get("Goofy").flatMap(Toon::getEmail);  
  2. ...  
  3. System.out.println(goofy.getOrElse(() -> "No data"));  
In this example, output of “No data” was obtained because the "Goofy" key was not in the map. This could be considered a normal case. But take a look at this one: 
  1. Option<String> toon = getName()  
  2.                          .flatMap(toons::get)  
  3.                          .flatMap(Toon::getEmail);  
  4.   
  5. System.out.println(toon.getOrElse(() -> "No data"));  
  6.   
  7. Option<String> getName() {  
  8.   String name = // retrieve the name from the user interface  
  9.   return name;  
  10. }  
If the user enters an empty string, what should you do? An obvious solution would be to validate the input and return an Option<String>. In the absence of a valid string, you could return None. But although you haven’t yet learned how to functionally let the user input a string, you can be sure that such an operation could throw an exception. The program would look like this
  1. Option<String> toon = getName()  
  2.                          .flatMap(Example::validate)  
  3.                          .flatMap(toons::get)  
  4.                          .flatMap(Toon::getEmail);  
  5.   
  6. System.out.println(toon.getOrElse(() -> "No data"));  
  7.   
  8. Option<String> getName() {  
  9.   try {  
  10.     String name = // retrieve the name from the user interface  
  11.     return Option.some(name);  
  12.   } catch (Exception e) {  
  13.     return Option.none();  
  14.   }  
  15. }  
  16.   
  17. Option<String> validate(String name) {  
  18.   return name.length() > 0 ? Option.some(name) : Option.none();  
  19. }  
Now think about what could happen: 
* Everything goes well, and you get an email printed to the console.
* An IOException is thrown, and you get “No data” printed to the console.
* The name entered by the user doesn’t validate, and you get “No data.
* The name validates but isn’t found in the map. You get “No data.
* The name is found in the map, but the corresponding toon has no email. You get “No data.

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 SomeT 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 
  1. package fp.utils;  
  2.   
  3. public abstract class Either<T, U> {  
  4.     private static class Left<T, U> extends Either<T, U> {  
  5.   
  6.         private final T value;  
  7.   
  8.         private Left(T value) {  
  9.             this.value = value;  
  10.         }  
  11.   
  12.         @Override  
  13.         public String toString() {  
  14.             return String.format("Left(%s)", value);  
  15.         }  
  16.     }  
  17.   
  18.     private static class Right<T, U> extends Either<T, U> {  
  19.   
  20.         private final U value;  
  21.   
  22.         private Right(U value) {  
  23.             this.value = value;  
  24.         }  
  25.   
  26.         @Override  
  27.         public String toString() {  
  28.             return String.format("Right(%s)", value);  
  29.         }  
  30.     }  
  31.   
  32.     public static <T, U> Either<T, U> left(T value) {  
  33.         return new Left<>(value);  
  34.     }  
  35.   
  36.     public static <T, U> Either<T, U> right(U value) {  
  37.         return new Right<>(value);  
  38.     }  
  39. }  
Now you can easily use Either instead of Option to represent values that could be absent due to errors. You have to parameterize Either with the type of your data and the type of the error. By convention, you’ll use the Right subclass to represent success (which is “right”and the Left to represent error. But you won’t call the subclass Wrong because the Either type may be used to represent data that can be represented by one type or another, both being valid. Of course, you have to choose what type will represent the error. You can choose String in order to carry an error message, or you can choose Exception. For example, the max function you defined in chapter 6 could be modified as follows: 
  1. <A extends Comparable<A>> Function<List<A>, Either<String, A>> max() {  
  2.   return xs -> xs.isEmpty()  
  3.       ? Either.left("max called on an empty list")  
  4.       : Either.right(xs.foldLeft(xs.head(), x -> y -> x.compareTo(y) < 0 ?  
  5.                                                                   x : y));  
  6. }  
Composing Either 
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): 
  1. public abstract <B> Either<E, B> map(Function<A, B> f);  
I’ve used type parameters E and A to make clear which side you should map, E standing for error. But it would be possible to define two map methods (call them mapLeft and mapRight) to map one or the other side of an Either instance. In other words, you’re developing a “biased” version of Either that will be mappable on one side only. 

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: 
  1. public <B> Either<E, B> map(Function<A, B> f) {  
  2.   return new Left<>(value);  
  3. }  
The Right implementation is exactly like the one in Some
  1. public <B> Either<E, B> map(Function<A, B> f) {  
  2.   return new Right<>(f.apply(value));  
  3. }  
Define a flatMap method to change an Either<E, A> into an Either<E, B>, given a function from A to Either<E, B>. The signature of the flatMap method is as follows (Exercise 7.2): 
  1. public abstract <B> Either<E, B> flatMap(Function<A, Either<E, B>> f);  
The Left implementation is exactly the same as for the map method: 
  1. public <B> Either<E, B> flatMap(Function<A, Either<E, B>> f) {  
  2.   return new Left<>(value);  
  3. }  
The Right implementation is the same as the Option.flatMap method: 
  1. public <B> Either<E, B> flatMap(Function<A, Either<E, B>> f) {  
  2.   return f.apply(value);  
  3. }  
Define methods getOrElse and orElse with the following signatures: 
  1. A getOrElse(Supplier<A> defaultValue)  
  2. Either<E, A> orElse(Supplier<Either<E, A>> defaultValue)  
For the Left subclass: 
  1. public Either<E, A> orElse(Supplier<Either<E, A>> defaultValue) {  
  2.     return defaultValue.get();  
  3. }  
  4.   
  5. public A getOrElse(Supplier<A> defaultValue) {  
  6.     return defaultValue.get();  
  7. }  

For Right subclass: 
  1. public Either<E, A> orElse(Supplier<Either<E, A>> defaultValue) {  
  2.     return this;  
  3. }  
  4.   
  5. public A getOrElse(Supplier<A> defaultValue) {  
  6.     return value;  
  7. }  
Those methods work, but it’s far from ideal. The problem is that you don’t know what has happened if no value was available. You simply get the default value, not even knowing if it’s the result of a computation or the result of an error. To handle error cases correctly, you’d need a biased version of Either, where the left type is known. Rather than using Either (which, by the way, has many other interesting uses), you can create a specialized version using a known fixed type for the Left class. 

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 
  1. package test.fp.utils;  
  2.   
  3. import java.io.Serializable;  
  4.   
  5. public abstract class Result<V> implements Serializable{    
  6.     private static final long serialVersionUID = 1L;  
  7.     private Result(){}  
  8.       
  9.     private static class Failure<V> extends Result<V>{  
  10.         private final RuntimeException exception;  
  11.           
  12.         private Failure(String message){  
  13.             super();  
  14.             this.exception = new IllegalStateException(message);  
  15.         }  
  16.           
  17.         private Failure(RuntimeException e){  
  18.             super();  
  19.             this.exception = e;  
  20.         }  
  21.           
  22.         private Failure(Exception e){  
  23.             super();  
  24.             this.exception = new IllegalStateException(e.getMessage(), e);  
  25.         }  
  26.           
  27.         @Override  
  28.         public String toString(){  
  29.             return String.format("Failure(%s)", exception.getMessage());  
  30.         }  
  31.     }  
  32.       
  33.     private static class Success<V> extends Result<V>{  
  34.         private final V value;  
  35.           
  36.         private Success(V value){  
  37.             super();  
  38.             this.value = value;  
  39.         }  
  40.           
  41.         @Override  
  42.         public String toString(){  
  43.             return String.format("Success(%s)", value.toString());  
  44.         }  
  45.     }  
  46.       
  47.     public static <V> Result<V> failure(String message){ return new Failure<>(message);}  
  48.     public static <V> Result<V> failure(Exception e){ return new Failure<>(e); }  
  49.     public static <V> Result<V> failure(RuntimeException e){ return new Failure<>(e); }  
  50.       
  51.     public static <V> Result<V> success(V value){ return new Success<>(value); }  
  52.       
  53. }  
This class is much like the Option class, with the additional stored exception. 

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): 
  1. public abstract V getOrElse(final V defaultValue);  
  2. public abstract V getOrElse(final Supplier<V> defaultValue);  
  3. public abstract <U> Result<U> map(Function<V, U> f);  
  4. public abstract <U> Result<U> flatMap(Function<V, Result<U>> f);  
  5. public Result<V> orElse(Supplier<Result<V>> defaultValue)  
The first version of getOrElse is useful when the default value is a literal because it’s already evaluated. In that case, you don’t need to use lazy evaluation. This time, you’ll have no problem with getOrElse, because you just have to throw the exception contained in a Failure. All other methods are very similar to those of the Either class. Here are the implementations for the Success class: 
  1. public V getOrElse(V defaultValue) {  
  2.     return value;  
  3. }  
  4.   
  5. public V getOrElse(Supplier<V> defaultValue) {  
  6.     return value;  
  7. }  
  8.   
  9. public <U> Result<U> map(Function<V, U> f) {  
  10.     try {  
  11.         return success(f.apply(value));  
  12.     } catch (Exception e) {  
  13.         return failure(e);  
  14.     }  
  15. }  
  16.   
  17. public <U> Result<U> flatMap(Function<V, Result<U>> f) {  
  18.     try {  
  19.         return f.apply(value);  
  20.     } catch (Exception e) {  
  21.         return failure(e);  
  22.     }  
  23. }  
And here are the implementations for the Failure class: 
  1. public V getOrElse(V defaultValue) {  
  2.     return defaultValue;  
  3. }  
  4.   
  5. public V getOrElse(Supplier<V> defaultValue) {  
  6.     return defaultValue.get();  
  7. }  
  8.   
  9. public <U> Result<U> map(Function<V, U> f) {  
  10.     return failure(exception);  
  11. }  
  12.   
  13. public <U> Result<U> flatMap(Function<V, Result<U>> f) {  
  14.     return failure(exception);  
  15. }  
As in Optionmap and flatMap can’t return this in the Failure class because the type would be invalid. Finally, you can define the orElse method in the parent class because the implementation is valid for both subclasses: 
  1. public Result<V> orElse(Supplier<Result<V>> defaultValue) {  
  2.     return map(x -> this).getOrElse(defaultValue);  
  3. }  
Result patterns 
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 
  1. package fp.utils;  
  2.   
  3. import java.util.concurrent.ConcurrentHashMap;  
  4. import java.util.concurrent.ConcurrentMap;  
  5.   
  6. public class Mapr <T, U> {  
  7.     private final ConcurrentMap<T, U> map = new ConcurrentHashMap<>();  
  8.       
  9.     public static <T, U> Mapr<T, U> empty(){ return new Mapr<>(); }  
  10.     public static <T, U> Mapr<T, U> add(Mapr<T, U> m, T t, U u)  
  11.     {  
  12.         m.map.put(t, u);   
  13.         return m;  
  14.     }  
  15.       
  16.     public Result<U> get(final T t)  
  17.     {  
  18.         return this.map.containsKey(t)  
  19.                 ? Result.success(this.map.get(t))  
  20.                 : Result.failure(String.format("Key=%s is not found in mapr", t));  
  21.     }  
  22.       
  23.     public Mapr<T, U> put(T t, U u)  
  24.     {  
  25.         return add(this, t, u);  
  26.     }  
  27.       
  28.     public Mapr<T, U> removeKey(T t)  
  29.     {  
  30.         this.map.remove(t);  
  31.         return this;  
  32.     }  
  33. }  
- Listing 7.4. The modified Toon class with the modified mail property 
  1. package fp.ch7;  
  2.   
  3. import fp.utils.Result;  
  4.   
  5. public class Toon {  
  6.     private final String firstName;  
  7.     private final String lastName;  
  8.     private final Result<String> email;  
  9.       
  10.     Toon(String firstName, String lastName)  
  11.     {  
  12.         this.firstName = firstName;  
  13.         this.lastName = lastName;  
  14.         this.email = Result.failure(String.format("%s %s has no email", firstName, lastName));  
  15.     }  
  16.       
  17.     Toon(String firstName, String lastName, String email)  
  18.     {  
  19.         this.firstName = firstName;  
  20.         this.lastName = lastName;  
  21.         this.email = Result.success(email);  
  22.     }  
  23.       
  24.     public Result<String> getEmail(){ return email; }  
  25. }  
Now you can modify the ToonMail program as follows. 
- Listing 7.5. The modified program, using Result 
  1. package fp.ch7;  
  2.   
  3. import fp.utils.Mapr;  
  4. import fp.utils.Result;  
  5.   
  6. public class ToonMail {  
  7.     public static void main(String[] args)  
  8.     {  
  9.         Mapr<String, Toon> toons = new Mapr<String,Toon>()  
  10.                 .put("Mickey"new Toon("Mickey""Mouse""mickey@diseny.com"))  
  11.                 .put("Minnie"new Toon("Minnie""Mouse"))  
  12.                 .put("Donald"new Toon("Donald""Duck""donald@disney.com"));  
  13.           
  14.         Result<String> result = getName("Mickey").flatMap(toons::get).flatMap(Toon::getEmail);  
  15.         System.out.printf("Result=%s\n", result);  
  16.     }  
  17.       
  18.     public static Result<String> getName(String name)  
  19.     {  
  20.         return Result.success(name);  
  21.     }  
  22. }  
The program in listing 7.5 uses the getName method to simulate an input operation that may throw an exception. To represent an exception being thrown, you just have to return a Failure wrapping the exception. Note how the various operations returning a Result are composed. You don’t need to access the value contained in the Result (which may be an exception). The flatMap method is used for such composition. 

Try to run this program with various input argument of the getName method, such as these: 
  1. System.out.printf("%s\n", getName("Mickey").flatMap(toons::get).flatMap(Toon::getEmail));  
  2. System.out.printf("%s\n", getName(new IOException("Input error")).flatMap(toons::get).flatMap(Toon::getEmail));  
  3. System.out.printf("%s\n", getName("Minnie").flatMap(toons::get).flatMap(Toon::getEmail));  
  4. System.out.printf("%s\n", getName("Goofy").flatMap(toons::get).flatMap(Toon::getEmail));  
Here’s what the program prints in each case: 
Success(mickey@diseny.com)
Failure(Input error)
Failure(Minnie Mouse has no email)
Failure(Key=Goofy is not found in mapr)

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: 
  1. getName().flatMap(toons::get).flatMap(Toon::getEmail);  
This was only possible because getNameMapr.get, and Toon.getEmail all return a Result. If Mapr.get and Toon.getMail were to return Options, they’d no longer compose with getName. It would still be possible to convert a Result to and from an Option. For example, you could add a toOption method in Result
  1. public abstract Option<V> toOption()  
The Success implementation would be 
  1. public Option<V> toOption() {  
  2.   return Option.some(value);  
  3. }  
The Failure implementation would be 
  1. public Option<V> toOption() {  
  2.   return Option.none();  
  3. }  
You could then use it as follows: 
  1. Option<String> result =  
  2.      getName().toOption().flatMap(toons::get).flatMap(Toon::getEmail);  
Of course, this would require you to use the version of Map you defined in chapter 6 (listing 6.2) and a specific version of the Toon class: 
  1. public class Toon {  
  2.   private final String firstName;  
  3.   private final String lastName;  
  4.   private final Option<String> email;  
  5.   
  6.   Toon(String firstName, String lastName) {  
  7.     this.firstName = firstName;  
  8.     this.lastName = lastName;  
  9.     this.email = Option.none();  
  10.   }  
  11.   
  12.   Toon(String firstName, String lastName, String email) {  
  13.     this.firstName = firstName;  
  14.     this.lastName = lastName;  
  15.     this.email = Option.some(email);  
  16.   }  
  17.   
  18.   public Option<String> getEmail() {  
  19.     return email;  
  20.   }  
  21. }  
But you would have lost all the benefit of using Result! Now if an exception is thrown inside the getName method, it’s still wrapped in a Failure, but the exception is lost in the toOption method, and the program simply prints 
none

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 
  1. public abstract class Result<V> implements Serializable{    
  2.     private static final long serialVersionUID = 1L;  
  3.         @SuppressWarnings("rawtypes")  
  4.     private static Result empty = new Empty();  
  5.         ...  
  6.             private static class Empty<V> extends Result<V>{  
  7.         public Empty(){super();}  
  8.           
  9.         @Override  
  10.         public V getOrElse(final V defaultValue){ return defaultValue; }  
  11.           
  12.         @Override  
  13.         public <U> Result<U> map(Function<V, U> f){ return empty; }  
  14.           
  15.         @Override  
  16.         public <U> Result<U> flatMap(Function<V, Result<U>> f){ return empty; }  
  17.           
  18.         @Override  
  19.         public String toString(){ return "Empty()"; }  
  20.           
  21.         @Override  
  22.         public V getOrElse(Supplier<V> defaultValue){ return defaultValue.get(); }  
  23.     }  
  24.         ...  
  25.         @SuppressWarnings("unchecked")  
  26.     public static <V> Result<V> empty(){ return empty; }  
  27. }  
Now you can again modify your ToonMail application, as shown in listings 7.7 through 7.9. 
- Listing 7.7. The Mapr class using the new Result.Empty class for optional data 
  1. package fp.utils;  
  2.   
  3. import java.util.concurrent.ConcurrentHashMap;  
  4. import java.util.concurrent.ConcurrentMap;  
  5.   
  6. public class Mapr <T, U> {  
  7.     private final ConcurrentMap<T, U> map = new ConcurrentHashMap<>();  
  8.       
  9.     public static <T, U> Mapr<T, U> empty(){ return new Mapr<>(); }  
  10.     public static <T, U> Mapr<T, U> add(Mapr<T, U> m, T t, U u)  
  11.     {  
  12.         m.map.put(t, u);   
  13.         return m;  
  14.     }  
  15.       
  16.     public Result<U> get(final T t)  
  17.     {  
  18.         return this.map.containsKey(t)  
  19.                 ? Result.success(this.map.get(t))  
  20.                 : Result.empty();  // The get method now returns Result.emtpy() if key isn't found  
  21.     }  
  22.       
  23.     public Mapr<T, U> put(T t, U u)  
  24.     {  
  25.         return add(this, t, u);  
  26.     }  
  27.       
  28.     public Mapr<T, U> removeKey(T t)  
  29.     {  
  30.         this.map.remove(t);  
  31.         return this;  
  32.     }  
  33. }  
- Listing 7.8. The Toon class using Result.Empty for optional data 
  1. package fp.ch7;  
  2.   
  3. import fp.utils.Result;  
  4.   
  5. public class Toon {  
  6.     private final String firstName;  
  7.     private final String lastName;  
  8.     private final Result<String> email;  
  9.       
  10.     Toon(String firstName, String lastName)  
  11.     {  
  12.         this.firstName = firstName;  
  13.         this.lastName = lastName;  
  14.         this.email = Result.empty();  // If you construct the instance without an email...  
  15.     }  
  16.       
  17.     Toon(String firstName, String lastName, String email)  
  18.     {  
  19.         this.firstName = firstName;  
  20.         this.lastName = lastName;  
  21.         this.email = Result.success(email);  
  22.     }  
  23.       
  24.     public Result<String> getEmail(){ return email; }  
  25. }  
- Listing 7.9. The ToonMail application handling optional data correctly 
  1. package fp.ch7;  
  2.   
  3. import java.io.IOException;  
  4.   
  5. import fp.utils.Mapr;  
  6. import fp.utils.Result;  
  7.   
  8. public class ToonMail {  
  9.     public static void main(String[] args)  
  10.     {  
  11.         Mapr<String, Toon> toons = new Mapr<String,Toon>()  
  12.                 .put("Mickey"new Toon("Mickey""Mouse""mickey@diseny.com"))  
  13.                 .put("Minnie"new Toon("Minnie""Mouse"))  
  14.                 .put("Donald"new Toon("Donald""Duck""donald@disney.com"));  
  15.                   
  16.         System.out.printf("%s\n", getName("Mickey").flatMap(toons::get).flatMap(Toon::getEmail));  
  17.         System.out.printf("%s\n", getName(new IOException("Input error")).flatMap(toons::get).flatMap(Toon::getEmail));  
  18.         System.out.printf("%s\n", getName("Minnie").flatMap(toons::get).flatMap(Toon::getEmail));  
  19.         System.out.printf("%s\n", getName("Goofy").flatMap(toons::get).flatMap(Toon::getEmail));  
  20.     }  
  21.       
  22.     public static Result<String> getName(String name)  
  23.     {  
  24.         return Result.success(name);  
  25.     }  
  26.       
  27.     public static Result<String> getName(Exception e)  
  28.     {  
  29.         return Result.failure(e);  
  30.     }  
  31. }  
The execution output: 
Success(mickey@diseny.com)
Failure(Input error)
Empty()
Empty()

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! 

沒有留言:

張貼留言

[Git 常見問題] error: The following untracked working tree files would be overwritten by merge

  Source From  Here 方案1: // x -----删除忽略文件已经对 git 来说不识别的文件 // d -----删除未被添加到 git 的路径中的文件 // f -----强制运行 #   git clean -d -fx 方案2: 今天在服务器上  gi...