So far, you’ve seen a very limited use of Result. Result 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
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
- filter(Function<T, Boolean> f);
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:
- public Result<V> filter(Function<V, Boolean> p) {
- return flatMap(x -> p.apply(x)
- ? this
- : failure("Condition not matched"));
- }
- public Result<V> filter(Function<V, Boolean> p, String message) {
- return flatMap(x -> p.apply(x)
- ? this
- : failure(message));
- }
boolean exists(Function<T, Boolean> p);
- public boolean exists(Function<V, Boolean> p) {
- return map(p).getOrElse(false);
- }
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:
public abstract Result<T> mapFailure(String s);
- public Result<T> mapFailure(String s) {
- return this;
- }
- public Result<T> mapFailure(String s) {
- return failure(new IllegalStateException(s, exception));
- }
- public abstract Result<T> mapFailure(String s, Exception e);
- public abstract Result<T> mapFailure(Exception e);
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:
- public static <T> Result<T> of(T value)
- public static <T> Result<T> of(T value, String message)
- public static <T> Result<T> of(Function<T, Boolean> predicate, T value)
- public static <T> Result<T> of(Function<T, Boolean> predicate,
- T value, String message)
- public static <T> Result<T> of(T value) {
- return value != null ? success(value) : Result.failure("Null value");
- }
- public static <T> Result<T> of(T value, String message) {
- return value != null ? success(value) : failure(message);
- }
- public static <T> Result<T> of(Function<T, Boolean> predicate, T value) {
- try {
- return predicate.apply(value) ? success(value) : empty();
- } catch (Exception e) {
- String errMessage = String.format("Exception while evaluating predicate: %s", value);
- return Result.failure(new IllegalStateException(errMessage, e));
- }
- }
- public static <T> Result<T> of(Function<T, Boolean> predicate, T value, String message) {
- try {
- return predicate.apply(value) ? Result.success(value) : Result.failure(String.format(message, value));
- } catch (Exception e) {
- String errMessage = String.format("Exception while evaluating predicate: %s",
- String.format(message, value));
- return Result.failure(new IllegalStateException(errMessage, e));
- }
- }
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:
- public interface Effect<T> {
- void apply(T t);
- }
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:
public abstract void forEach(Effect<T> ef)
- public void forEach(Effect<T> ef) {
- // Empty. Do nothing.
- }
- public void forEach(Effect<T> ef) {
- ef.apply(value);
- }
Let's define the forEachOrThrow method to handle this use case. Here’s its signature in the Result class:
public abstract void forEachOrThrow(Effect<V> ef)
- public void forEachOrThrow(Effect<V> ef) {
- throw exception;
- }
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
The method is declared as abstract in the Result parent class:
public abstract Result<RuntimeException> forEachOrException(Effect<V> ef);
- @Override
- public Result<RuntimeException> forEachOrException(Effect<V> ef) {
- return empty();
- }
- public Result<RuntimeException> forEachOrException(Effect<V> ef) {
- ef.apply(value);
- return empty();
- }
- public Result<RuntimeException> forEachOrException(Effect<V> ef) {
- return success(exception);
- }
- Result<Integer> result = getComputation();
- result.forEachOrException(System.out::println).forEach(Logger::log);
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:
- static <A, B> Function<Result<A>, Result<B>> lift(final Function<A, B> f)
- public static <A, B> Function<Result<A>, Result<B>> lift(final Function<A, B> f) {
- return x -> {
- try {
- return x.map(f);
- } catch (Exception e) {
- return failure(e);
- }
- };
- }
- public static <A, B, C> Function<Result<A>, Function<Result<B>,
- Result<C>>> lift2(Function<A, Function<B, C>> f)
- public static <A, B, C, D> Function<Result<A>,
- Function<Result<B>, Function<Result<C>,
- Result<D>>>> lift3(Function<A, Function<B, Function<C, D>>> f)
- public static <A, B, C> Function<Result<A>, Function<Result<B>,
- Result<C>>> lift2(Function<A, Function<B, C>> f) {
- return a -> b -> a.map(f).flatMap(b::map);
- }
- public static <A, B, C, D> Function<Result<A>,
- Function<Result<B>, Function<Result<C>,
- Result<D>>>> lift3(Function<A, Function<B, Function<C, D>>> f) {
- return a -> b -> c -> a.map(f).flatMap(b::map).flatMap(c::map);
- }
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
- <A, B, C> Option<C> map2(Option<A> a,
- Option<B> b,
- Function<A, Function<B, C>> f) {
- return a.flatMap(ax -> b.map(bx -> f.apply(ax).apply(bx)));
- }
- public static <A, B, C> Result<C> map2(Result<A> a,
- Result<B> b,
- Function<A, Function<B, C>> f) {
- return lift2(f).apply(a).apply(b);
- }
- static Result<String> getFirstName() {
- return success("Mickey");
- }
- static Result<String> getLastName() {
- return success("Mickey");
- }
- static Result<String> getMail() {
- return success("mickey@disney.com");
- }
- Function<String, Function<String, Function<String, Toon>>> createPerson =
- x -> y -> z -> new Toon(x, y, z);
- Result<Toon> toon2 = lift3(createPerson)
- .apply(getFirstName())
- .apply(getLastName())
- .apply(getMail());
- Result<Toon> toon = getFirstName()
- .flatMap(firstName -> getLastName()
- .flatMap(lastName -> getMail()
- .map(mail -> new Toon(firstName, lastName, mail))));
Supplement
* FP with Java - Ch7 - Handling errors and exceptions - Part1
* FP with Java - Ch7 - Handling errors and exceptions - Part2
沒有留言:
張貼留言