Preface
This chapter covers
You now have all the types of functions you’ll need. As you saw in the previous chapter, these functions don’t require any exceptions to the traditional Java coding rules. Using methods as pure functions (a.k.a. functional methods) is perfectly in line with most so-called Java best practices. You haven’t changed the rules or added any exotic constructs. You’ve just added some restrictions about what functional methods can do: they can return a value, and that’s all. They can’t mutate any objects or references in the enclosing scope, nor their arguments. In the first part of this chapter, you’ll learn how to apply the same principles to Java control structures.
You’ve also learned how to create objects representing functions, so that these functions can be passed as arguments to methods and other functions. But for such functions to be useful, you must create the methods or functions that can manipulate them. In the second part of this chapter, you’ll learn how to abstract collection operations and control structures to use the power of functions. The last part of the chapter presents techniques that will allow you to get the most out of the type system when handling business problems.
Making standard control structures functional
Control structures are the main building blocks of imperative programming. No imperative Java programmer would believe it’s possible to write programs without using if ... else, switch ... case, and for, while, and do loops. These structures are the essence of imperative programming. But in the following chapters, you’ll learn how to write functional programs with absolutely no control structures. In this section, you’ll be less adventurous—we’ll only look at using the traditional control structures in a more functional style.
One point you learned in chapter 2 is that purely functional methods can’t do anything but return a value. They can’t mutate an object or reference in the enclosing scope. The value returned by a method can depend only on its arguments, although the method can read data in the enclosing scope. In such a case, the data is considered to be implicit arguments. In imperative programming, control structures define a scope in which they generally do something, which means they have an effect. This effect might be visible only inside the scope of the control structure, or it might be visible in the enclosing scope. The control structures might also access the enclosing scope to read values. The following listing shows a basic example of email validation.
Listing 3.1. Simple email validation
In this example, the if ... else structure (1) accesses the emailPattern variable from the enclosing scope. From the Java syntax point of view, there’s no obligation for this variable to be final, but it’s necessary if you want to make the testMailmethod functional. Another solution would be to declare the pattern inside the method, but this would cause it to be compiled for each method call. If the pattern could change between calls, you should make it a second parameter of the method. If the condition is true, an effect (2) is applied to this email variable. This effect consists of sending a verification email, probably to check whether the email address, besides being well formed, is a valid one. In this example, the effect is simulated (4) by printing a message to standard output. If the condition is false, a different effect (3) is applied to the variable by including it in an error message. This message is logged (5), which once again is simulated by printing to standard error.
Abstracting control structures
The code in listing 3.1 is purely imperative. You’ll never find such code in functional programming. Although the testMail method seems to be a pure effect because it doesn’t return anything, it mixes data processing with effects. This is something you want to avoid, because it results in code that’s impossible to test. Let’s see how you can clean this up.
The first thing you may want to do is separate computation and effects so you can test the computation result. This could be done imperatively, but I prefer to use a function, as shown in the following listing.
Listing 3.2. Using a function to validate the email
Now you can test the data processing part of the program (validating the email string) because you’ve clearly separated it from the effects. But you still have many problems. One is that you handle only the case where the string doesn’t validate. But if the string received is null, a NullPointerException (NPE) is thrown. Consider the following example:
- testMail("john.doe@acme.com");
- testMail(null);
- testMail("paul.smith@acme.com");
- testMail("");
The double space (between “email” and “is”) indicates that the string was empty. A specific message would be better, such as this:
To handle these problems, you’ll first define a special component to handle the result of the computation.
- package fp.utils;
- public interface Result {
- public class Success implements Result{}
- public class Failure implements Result{
- private final String errorMsg;
- public Failure(String s)
- {
- this.errorMsg = s;
- }
- public String getMessage(){return errorMsg;}
- }
- }
Now you can write your new version of the program:
Listing 3.4. The program with better error handling
- package fp.compose.ch3;
- import java.util.regex.Pattern;
- import fp.utils.Function;
- import fp.utils.Result;
- public class EmailValidation {
- static Pattern emailPattern = Pattern.compile("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$");
- static Function
emailChecker = s -> { - if (s == null) {
- return new Result.Failure("email must not be null");
- } else if (s.length() == 0) {
- return new Result.Failure("email must not be empty");
- } else if (emailPattern.matcher(s).matches()) {
- return new Result.Success();
- } else {
- return new Result.Failure("email " + s + " is invalid.");
- }
- };
- public static void main(String... args) {
- validate("this.is@my.email");
- validate(null);
- validate("");
- validate("john.doe@acme.com");
- }
- private static void logError(String s) {
- System.err.println("Error message logged: " + s);
- }
- private static void sendVerificationMail(String s) {
- System.out.println("Mail sent to " + s);
- }
- static void validate(String s) {
- Result result = emailChecker.apply(s);
- if (result instanceof Result.Success) {
- sendVerificationMail(s);
- } else {
- logError(((Result.Failure) result).getMessage());
- }
- }
- }
But this still isn’t satisfactory. Using instanceof to determine whether the result is a success is ugly. And using a cast to access the failure message is even more so. But worse than this is the fact that you have some program logic in the validate method that can’t be tested. This is because the method is an effect, which means it doesn’t return a value but mutates the outside world.
Is there a way to fix this? Yes. Instead of sending an email or logging a message, you could return a small program that does the same thing. Instead of executing
- sendVerificationMail(s)
- logError(((Result.Failure) result).getMessage());
- public interface Executable {
- void exec();
- }
Listing 3.5. Returning executables
- package fp.compose.ch3;
- import java.util.regex.Pattern;
- import fp.utils.Function;
- import fp.utils.Result;
- public class EmailValidationV2 {
- static Pattern emailPattern = Pattern.compile("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$");
- static Function
emailChecker = s -> - s == null
- ? new Result.Failure("email must not be null")
- :s.length() == 0
- ? new Result.Failure("email must not be empty")
- : emailPattern.matcher(s).matches()
- ? new Result.Success()
- : new Result.Failure("email " + s + " is invalid.");
- // (1)
- public static void main(String... args) {
- validate("this.is@my.email");
- validate(null);
- validate("");
- validate("john.doe@acme.com");
- }
- private static void logError(String s) {
- System.err.println("Error message logged: " + s);
- }
- private static void sendVerificationMail(String s) {
- System.out.println("Mail sent to " + s);
- }
- // (2)
- static Executable validate(String s) {
- Result result = emailChecker.apply(s);
- return (result instanceof Result.Success)
- ? ()->sendVerificationMail(s)
- : ()->logError(((Result.Failure)result).getMessage());
- }
- }
You’ve also replaced the if ... else control structure with the ternary operator. This is a matter of preference. The ternary operator is functional because it returns a value and has no side effect. In contrast, the if ... else structure can be made functional by making it mutate only local variables, but it can also have side effects. If you see imperative programs with many embedded if ... else structures, ask yourself how easy it would be to replace them with the ternary operator. This is often a good indication of how close to functional the design is. Note, however, that it’s also possible to make the ternary operator nonfunctional by calling nonfunctional methods to get the resulting values.
Cleaning up the code
Your validate method is now functional, but it’s dirty. Using the instanceof operator is almost always an indication of bad code. Another problem is that reusability is low. When the validate method returns a value, you have no choice besides executing it or not. What if you want to reuse the validation part but produce a different effect?
The validate method shouldn’t have a dependency on sendVerificationMail or logError. It should only return a result expressing whether the email is valid, and you should be able to choose whatever effects you need for success or failure. Or you might prefer not to apply the effect but to compose the result with some other processing. Let's try to decouple the validation from the effects applied (Exercise 3.1).
Hint.
The first thing to do is create the interface representing an effect, such as the following:
- package fp.compose.ch3;
- public interface Effect
{ - void apply(T t);
- }
Figure 3.1. Changes to the Result interface
Take the example of Java interfaces. They’re supposed to be named either after what objects are (Comparable, Clonable, Serializable) or what they can do (Listener, Supplier, Consumer). Following this rule, a Function should be renamed Applicable and should have a method apply. A Supplier should define a method supply, and a Consumer should consume something and have a method named consume. But a Consumer defines an acceptmethod, and it doesn’t consume anything, because after having accepted an object, this object is still available.
Don’t trust names. Trust types. Types don’t lie. Types are your friends! The following listing shows the modified version of the Result class.
Listing 3.6. A Result that can handle Effects
- package fp.compose.ch3;
- public interface Result
{ - void bind(Effect
success, Effect failure); - public static
Result failure(String message) - {
- return new Failure<>(message);
- }
- public static
Result success(T value) - {
- return new Success<>(value);
- }
- public class Success
- {
- private final T value;
- private Success(T t){this.value = t;}
- @Override
- public void bind(Effect
success, Effect failure) - {
- success.apply(value);
- }
- }
- public class Failure
- {
- private final String errorMsg;
- private Failure(String s){
- this.errorMsg = s;
- }
- @Override
- public void bind(Effect
success, Effect failure) - {
- failure.apply((T)errorMsg);
- }
- }
- }
Listing 3.7. A cleaner version of the program
- package fp.compose.ch3;
- import java.util.regex.Pattern;
- import fp.utils.Function;
- public class EmailValidationV3 {
- static Pattern emailPattern = Pattern.compile("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$");
- // (3)
- static Effect
SuccessEft = s -> System.out.printf( "Mail sent to %s", s); - static Effect
FailureEft = s -> System.err.printf( "Error message logged: %s", s); - // (1)
- static Function
> emailChecker = s -> { - if(s==null)
- {
- return Result.failure("email must not be null");
- }
- else if(s.length() == 0)
- {
- return Result.failure("email must not be empty");
- }
- else if(emailPattern.matcher(s).matches())
- {
- return Result.success(s);
- }
- else
- {
- return Result.failure("email " + s + " is invalid.");
- }
- };
- // (2)
- public static void main(String args[])
- {
- emailChecker.apply("this.is@my.email").bind(SuccessEft, FailureEft);
- emailChecker.apply(null).bind(SuccessEft, FailureEft);
- emailChecker.apply("").bind(SuccessEft, FailureEft);
- emailChecker.apply("john.doe@acme.com").bind(SuccessEft, FailureEft);
- }
- }
An alternative to if ... else
You may wonder whether it’s possible to completely remove conditional structures or operators. Can you write a program without any of these constructs? This may seem impossible, because many programmers have learned that decision-making is the basic building block of programming. But decision-making is an imperative programming notion. It’s the notion of examining a value and deciding what to do next based on this observation. In functional programming, there’s no “what to do next” question, but only functions returning values. The most basic if structure may be seen as the implementation of a function:
- if (x > 0) {
- return x;
- } else {
- return -x;
- }
- Function
abs = x -> { - if (x > 0) {
- return x;
- } else {
- return -x;
- }
- }
- Function
square = x -> x * x;
(Exercise 3.2) Let's write a Case class representing a condition and corresponding result. The condition will be represented by a Supplier
- interface Supplier
{ - T get();
- }
- public static
Case mcase(Supplier condition, - Supplier
> value) - public static
DefaultCase mcase(Supplier > value) - public static
Result match(DefaultCase defaultCase, - Case
... matchers)
- private static class DefaultCase
- package fp.compose.ch3;
- import fp.utils.Tuple;
- public class Case
, Supplier >>{ - private Case(Supplier
booleanSupplier, - Supplier
> resultSupplier) { - super(booleanSupplier, resultSupplier);
- }
- }
- public static
Case mcase(Supplier condition, Supplier > value) { - return new Case<>(condition, value);
- }
- public static
DefaultCase mcase(Supplier > value) { - return new DefaultCase<>(() -> true, value);
- }
- private static class DefaultCase
{ - private DefaultCase(Supplier
booleanSupplier, Supplier > resultSupplier) { - super(booleanSupplier, resultSupplier);
- }
- }
- @SafeVarargs
- public static
Result match(DefaultCase defaultCase, Case ... matchers) { - for (Case
aCase : matchers) { - if (aCase._1.get())
- return aCase._2.get();
- }
- return defaultCase._2.get();
- }
Listing 3.8. Matching conditions with the Case class
- package fp.compose.ch3;
- import fp.utils.Tuple;
- public class Case
, Supplier >>{ - private static class DefaultCase
{ - private DefaultCase(Supplier
booleanSupplier, Supplier > resultSupplier) { - super(booleanSupplier, resultSupplier);
- }
- }
- private Case(Supplier
booleanSupplier, - Supplier
> resultSupplier) { - super(booleanSupplier, resultSupplier);
- }
- public static
Case mcase(Supplier condition, Supplier > value) { - return new Case<>(condition, value);
- }
- public static
DefaultCase mcase(Supplier > value) { - return new DefaultCase<>(() -> true, value);
- }
- @SafeVarargs
- public static
Result match(DefaultCase defaultCase, Case ... matchers) { - for (Case
aCase : matchers) { - if (aCase._1.get())
- return aCase._2.get();
- }
- return defaultCase._2.get();
- }
- }
Listing 3.9. The email validation application with no control structures
- package fp.compose.ch3;
- import java.util.regex.Pattern;
- import static fp.compose.ch3.Case.*;
- import static fp.compose.ch3.Result.*;
- import fp.utils.Function;
- public class EmailValidationV4 {
- static Pattern emailPattern = Pattern.compile("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$");
- static Effect
SuccessEft = s -> System.out.printf( "Mail sent to %s", s); - static Effect
FailureEft = s -> System.err.printf( "Error message logged: %s", s); - static Function
> emailChecker = s -> - match(
- mcase(()->success(s)),
- mcase(()->s==null,()->failure("email must not be null")),
- mcase(()->s.length()==0, ()->failure("email must not be empty")),
- mcase(()->emailPattern.matcher(s).matches(), ()->failure("email " + s + " is invalid.")));
- public static void main(String args[])
- {
- emailChecker.apply("this.is@my.email").bind(SuccessEft, FailureEft);
- emailChecker.apply(null).bind(SuccessEft, FailureEft);
- emailChecker.apply("").bind(SuccessEft, FailureEft);
- emailChecker.apply("john.doe@acme.com").bind(SuccessEft, FailureEft);
- }
- }
In this chapter, you’ll see how to generalize abstractions of all control structures. You’ve done this for conditional control structures such as embedded if..else statements (and switch..case is no different). Let’s see how to do the same with loops.
Supplement
* Java Annotation - 常用標準標註
沒有留言:
張貼留言