Advanced function features
You’ve seen how to create apply and compose functions. You’ve also learned that functions can be represented by methods or by objects. But you haven’t answered a fundamental question: why do you need function objects? Couldn’t you simply use methods? Before answering this question, you have to consider the problem of the functional representation of multiargument methods.
What about functions of several arguments?
In section 2.1.1, I said that there are no functions of several arguments. There are only functions of one tuple of arguments. The cardinality of a tuple may be whatever you need, and there are specific names for tuples with a few arguments: pair, triplet, quartet, and so on. Other possible names exist, and some prefer to call them tuple2, tuple3, tuple4, and so forth. But I also said that arguments can be applied one by one, each application of one argument returning a new function, except for the last one. Let’s try to define a function for adding two integers. You’ll apply a function to the first argument, and this will return a function. The type will be as follows:
- Function
>
- Integer -> Integer -> Integer
- Integer -> (Integer -> Integer)
>
- import java.util.function.Function;
- public interface BinaryOperator extends Function
>{} - BinaryOperator add = x -> y -> x + y;
- BinaryOperator mult = x -> y -> x * y;
- System.out.printf("%d\n", add.apply(1).apply(2)); // 1 + 2 = 3
- System.out.printf("%d\n", mult.apply(2).apply(3)); // 2 * 3 = 6
Applying curried functions
You’ve seen how to write curried function types and how to implement them. But how do you apply them? Well, just like any function. You apply the function to the first argument, and then apply the result to the next argument, and so on until the last one. For example, you can apply the add function to 3 and 5:
- System.out.println(add.apply(3).apply(5)); // 3 + 5 = 8
- add(3)(5)
- add 3 5
Higher-order functions
In section 2.14, you wrote a method to compose functions. That method was a functional one, taking as its argument a tuple of two functions and returning a function. But instead of using a method, you could use a function! This special kind of function, taking functions as its arguments and returning functions, is called a higher-order function (HOF).
Let's write a function to compose the two functions square and triple (exercise 2.4). The first thing to do is to write the type. This function will work on two arguments, so it’ll be a curried function. The two arguments and the return type will be functions from Integer to Integer:
- Function
- Function
>
- Function
, - Function
, - Function
>>
- x -> y -> z -> x.apply(y.apply(z));
- Function
, - Function
, - Function
>> compose = - x -> y -> z -> x.apply(y.apply(z));
- Function
triple = x -> x * 3; - Function
square = x -> x * x; - Function
f = compose.apply(square).apply(triple);
- System.out.println(f.apply(2)); // (2*3)^2 = 36
Polymorphic higher-order functions
Our compose function is fine, but it can compose only functions from Integer to Integer. It would be much more interesting if you could compose any types of functions, such as String to Double or Boolean to Long. But that’s only the beginning. A fully polymorphic compose function would allow you to compose Function
Now let's write a polymorphic version of the compose function (Exercise 2.5). The first step seems to be to “generify” the example of exercise 2.4:
Function , - Function
, - Function
>> higherCompose = - f -> g -> x -> f.apply(g.apply(x));
Variance
Variance describes how parameterized types behave in relation to subtyping. Covariance means that Matcher<Red> is considered a subtype of Matcher<Color> if Red is a subtype of Color. In such case, Matcher<T> is said to be covariant on T. If, on the contrary, Matcher<Color> is considered a subtype of Matcher<Red>, then Matcher<T> is said to be contravariant on T. In Java, although an Integer is a subtype of Object, a List<Integer> is not a subtype of List<Object>. You may find this strange, but a List<Integer> is an Object, but it is not a List<Object>. And a Function
In Java, all parameterized types are said to be invariant on their parameter.
- static
Function , Function , Function >> higherCompose() { - return f -> g -> x -> f.apply(g.apply(x));
- }
Figure 2.3. Pay attention to the order of type parameters.
You could give the lambda parameters more-meaningful names, such as uvFunction and tuFunction, or more simply uv and tu, but you should refrain from doing so. Names aren’t reliable. They show the intention (of the programmer) and nothing else. You could easily switch the names without noticing any change:
- static
Function , Function , Function >> higherCompose() { - return tuFunc -> uvFunc -> t -> tuFunc.apply(uvFunc.apply(t));
- }
- static <T, U, V&tl; Function<Function<U, V>,
- Function<Function<T, U>,
- Function<T, V>>> higherCompose() {
- return (Function f) -> (Function
g) -> (T x) - -> f.apply(g.apply(x));
- }
- Function
triple = x -> x * 3; - Function
square = x -> x * x; - Integer x = higherCompose().apply(square).apply(triple).apply(2);
The compiler is saying that it couldn’t infer the real types for the T, U, and V type parameters, so it used Object for all three. But the square and triple functions have types Function<Integer, Integer>. If you think that this is enough information to infer the T, U, and V types, then you’re smarter than Java! Java tried to go the other way around, casting a Function<Integer, Integer> into a Function<Object, Object>. And although an Integer is an Object, a Function<Integer, Integer> isn’t a Function <Object, Object&tl;. These two types aren’t related because types are invariant in Java. For the cast to work, the types should have been covariant, but Java doesn’t know about variance. The solution is to revert to the original problem and help the compiler by telling it what real types T, U, and V are. This can be done by inserting the type information between the dot and the method name:
- Integer x = Function.
higherCompose().apply(....)
- import static com.fpinjava. ... .Function.*;
- ...
- Integer x =
higherCompose().apply(...;
Let's write the higherAndThen function that composes the functions the other way around, which means that higherCompose(f, g) is equivalent to higherAndThen(g, f) (Exercise 2.6).
- public static
Function , Function , Function >> higherAndThen() { - return f -> g -> x -> g.apply(f.apply(x));
- }
If you have any doubt concerning the order of the parameters, you should test these higher-order functions with functions of different types. Testing with functions from Integer to Integer will be ambiguous, because you’ll be able to compose the functions in both orders, so an error will be difficult to detect. Here’s a test using functions of different types:
- package fp.compose;
- import java.util.function.Function;
- import static org.junit.Assert.*;
- import org.junit.Test;
- public class Ex2_6 {
- public static
Function , Function , Function >> higherAndThen() { - return f -> g -> x -> g.apply(f.apply(x));
- }
- static
Function , Function , Function >> higherCompose() { - return tvFunc -> vuFunc -> t -> tvFunc.apply(vuFunc.apply(t));
- }
- @Test
- public void testHigherCompose() {
- Function
f = a -> (int) (a * 3); - Function
g = a -> a + 2.0; - assertEquals(Integer.valueOf(9), f.apply((g.apply(1L))));
- assertEquals(Integer.valueOf(9),
- Ex2_6.
higherCompose().apply(f).apply(g).apply(1L)); - }
- }
Using anonymous functions
Until now, you’ve been using named functions. These functions were implemented as anonymous classes, but the instances you created were named and had explicit types. Often you won’t define names for functions, and you’ll use them as anonymous instances. Let’s look at an example. Instead of writing:
- Function
f = x -> Math.PI / 2 - x; - Function
sin = Math::sin; - Double cos = f.compose(sin).apply(2.0);
- Double cos = JFun.
Compose(x -> Math.PI / 2 - x, Math::sin).apply(2.0);
- package fp.compose;
- import java.util.function.Function;
- public class JFun {
- public static
Function Compose(Function f1, Function f2) { - return arg -> f1.apply(f2.apply(arg));
- }
- public static
Function , Function , Function >> HigherCompose() { - return tvFunc -> vuFunc -> t -> tvFunc.apply(vuFunc.apply(t));
- }
- }
- Double cos1 = JFun.
Compose(x -> Math.PI / 2 - x, Math::sin).apply(2.0); - Double cos2 = JFun.
HigherCompose().apply(x -> Math.PI / 2 - x).apply(Math::sin).apply(2.0); - assert(cos1==cos2);
Beside lambdas, Java 8 also brings method references, which is a syntax that can be used to replace a lambda when the lambda implementation consists of a method call with a single argument. For example,
- Function
sin = Math::sin;
- Function
sin = x -> Math.sin(x);
- Function
sin = this.sin(x);
When to use anonymous and when to use named functions
Apart from special cases when anonymous functions can’t be used, it’s up to you to choose between anonymous and named functions. As a general rule, functions that are used only once are defined as anonymous instances. But used once means that you write the function once. It doesn’t mean that it’s instantiated only once. In the following example, you define a method to compute the cosine of a Double value. The method implementation uses two anonymous functions because you’re using a lambda expression and a method reference:
- Double cos(Double arg) {
- return JFun.Compose(z -> Math.PI / 2 - z, Math::sin).apply(arg);
- }
Type inference
Type inference can also be an issue with anonymous functions. In the previous example, the types of the two anonymous functions can be inferred by the compiler because it knows that the compose methods take two functions as arguments:
- static
Function Compose(Function f, Function g)
But this won’t always work. If you replace the second argument with a lambda instead of a method reference,
- Double cos2(Double arg) {
- return JFun.Compose(z -> Math.PI / 2 - z, a -> Math.sin(a)).apply(arg);
- }
The compiler is so confused that it even finds a nonexistent error in column 44! But the error in column 63 is real. As strange as it may seem, Java is unable to guess the type of the second argument. To make this code compile, you have to add type annotations:
- Double cos2(Double arg) {
- return JFun.Compose(z -> Math.PI / 2 - z, (Function
)a -> Math.sin(a)).apply(arg); - }
Local functions
You just saw that you can define functions locally in methods, but you can’t define methods within methods. On the other hand, functions can be defined inside functions without any problem through lambdas. The most frequent case you’ll encounter is embedded lambdas, shown here:
- public
Result ifElse(List conditions, List ifTrue) { - return conditions.zip(ifTrue)
- .flatMap(x -> x.first(y -> y._1))
- .map(x -> x._2);
- }
Local functions aren’t always anonymous. They’re generally named when used as helper functions. In traditional Java, using helper methods is common practice. These methods allow you to simplify the code by abstracting portions of it. The same technique is used with functions, although you may not notice it because it’s made implicit when using anonymous lambdas. But using explicitly declared local functions is always possible, as in the following example, which is nearly equivalent to the previous one:
- public
Result ifElse_(List conditions, List ifTrue) { - Function
, Boolean> f1 = y -> y._1; - Function
- >, Result
>> f2 = - x -> x.first(f1);
- Function
, T> f3 = x -> x._2; - return conditions.zip(ifTrue)
- .flatMap(f2)
- .map(f3);
- }
Closures
You’ve seen that pure functions must not depend on anything other than their arguments to evaluate their return values. Java methods often access class members, either to read or even write them. Methods may even access static members of other classes. I’ve said that functional methods are methods that respect referential transparency, which means they have no observable effects besides returning a value. The same is true for functions. Functions are pure if they don’t have observable side effects.
But what about functions (and methods) with return values depending not only on their arguments, but on elements belonging to the enclosing scope? You’ve already seen this case, and these elements of the enclosing scope could be considered implicit parameters of the functions or methods using them. Lambdas carry an additional requirement: a lambda can access a local variable only if it’s final. This requirement isn’t new to lambdas. It was already a requirement for anonymous classes prior to Java 8, and lambdas must respect the same condition, although it has been made a little less strict. Starting with Java 8, elements accessed from anonymous classes or lambdas can be implicitly final; they don’t need to be declared final, provided they aren’t modified. Let’s look at an example:
- public void aMethod() {
- double taxRate = 0.09;
- Function
addTax = price -> price + price * taxRate; - ...
- }
- public void aMethod() {
- double taxRate = 0.09;
- Function
addTax = price -> price + price * taxRate; - ...
- taxRate = 0.13;
- ...
- }
- double taxRate = 0.09;
- public void aMethod() {
- Function
addTax = price -> price + price * taxRate; - taxRate = 0.13;
- ...
- }
Closures are compatible with pure functions if you consider them as additional implicit arguments. They can, however, cause problems when refactoring the code, and also when functions are passed as parameters to other functions. This can result in programs that are difficult to read and maintain. One way to make programs more modular is to use functions of tuples of arguments:
- double taxRate = 0.09;
- Function
, Double> addTax - = tuple -> tuple._2 + tuple._2 * tuple._1;
- System.out.println(addTax.apply(new Tuple<>(taxRate, 12.0)));
- interface Function2
{ - V apply(T t, U u);
- }
- Function2
addTax = (taxRate, price) -> price + price * taxRate; - double priceIncludingTax = addTax.apply(0.09, 12.0);
You could also use the class BiFunction defined in Java 8, which simulates a function of a tuple of two arguments, or even BinaryOperator, which corresponds to a function of a tuple of two arguments of the same type, or even DoubleBinaryOperator, which is a function of a tuple of two double primitives. All these possibilities are fine, but what if you need three arguments or more? You could define Function3, Function4, and so on. But currying is a much better solution. That’s why it’s absolutely necessary to learn to use currying, which, as you already saw, is extremely simple:
- double tax = 0.09;
- Function
> addTax - = taxRate -> price -> price + price * taxRate;
- System.out.println(addTax.apply(tax).apply(12.00));
The closure and curried versions in the previous example give the same results and may be seen as equivalent. In fact, they are “semantically” different. As I’ve already said, the two parameters play totally different roles. The tax rate isn’t supposed to change often, whereas the price is supposed to be different on each invocation. This appears clearly in the closure version. The function closes over a parameter that doesn’t change (because it’s final). In the curried version, both arguments may change on each invocation, although the tax rate won’t change more often than in the closure version.
It’s common to need a changing tax rate, such as when you have several tax rates for different categories of products or for different shipping destinations. In traditional Java, this could be accommodated by turning the class into a parameterized “tax computer”:
- public class TaxComputer {
- private final double rate;
- public TaxComputer(double rate) {
- this.rate = rate;
- }
- public double compute(double price) {
- return price * rate + price;
- }
- }
- TaxComputer tc9 = new TaxComputer(0.09);
- double price = tc9.compute(12);
- BinaryOperator
addTax = rate -> price -> price * rate + price; - Function
tc9 = addTax.apply(0.09); - System.out.printf("%.02f", tc9.apply(12.0)); // 13.08 = 12*0.09 + 12
Write a functional method to partially apply a curried function of two arguments to its first argument (Exercise 2.7):
- public static Function PartialA(A a, Function> f)
- {
- return f.apply(a);
- }
- public static Function partialB(B b, Function> f) {
- return a -> f.apply(a).apply(b);
- }
Now let's convert the following method into a curried function (Exercise 2.9):
- String func(A a, B b, C c, D d) {
- return String.format("%s, %s, %s, %s", a, b, c, d);
- }
Switching arguments of partially applied functions
If you have a function of two arguments, you might want to apply only the first argument to get a partially applied function. Let’s say you have the following function:
- Function
> addTax = x -> y -> y + y / 100 * x;
- Function
add9percentTax = addTax.apply( 9.0);
- Double priceIncludingTax = add9percentTax.apply(price);
- Function
> addTax = x -> y -> x + x / 100 * y;
Let write a method to swap the arguments of a curried function (Exercise 2.11). The following method returns a curried function with the arguments in reverse order. It could be generalized to any number of arguments and to any arrangement of them:
- public static
Function> reverseArgs(Function - Function> f) {
- return u -> t -> f.apply(t).apply(u);
- }
- Function
> payment = amount -> rate -> ...
You can very easily create a function of one argument to compute the payment for a fixed amount and a varying rate, or a function computing the payment for a fixed rate and a varying amount.
Recursive functions
Recursive functions are a ubiquitous feature in most functional programming languages, although recursion and functional programming aren’t connected. Some functional programmers even say that recursion is the goto feature of functional programming, and thus should be avoided as much as possible. Nevertheless, as functional programmers, you must master recursion, even if eventually you decide to avoid it.
As you may know, Java is limited in terms of recursion. Methods can call themselves recursively, but this implies that the state of the computation is pushed on the stack for each recursive call, until a terminal condition is reached, at which time all preceding states of the computation are popped out of the stack, one after the other, and evaluated. The size of the stack can be configured, but all threads will use the same size. The default size varies according to the implementation of Java, from 320 KB for a 32-bit version to 1,064 KB for a 64-bit implementation, both of which are very small compared to the size of the heap, where objects are stored. The end result is that the number of recursive steps is limited.
Determining how many recursive steps Java can handle is difficult, because it depends on the size of the data that’s pushed on the stack, and also on the state of the stack when the recursive process starts. In general, Java can handle about 5,000 to 6,000 steps. Pushing this limit artificially is possible because Java uses memoization internally. This technique consists of storing the results of functions or methods in memory to speed up future access. Instead of reevaluating a result, Java can retrieve it from memory if it has previously been stored. Besides speeding access, this can allow you to partly avoid recursion by finding a terminal state much quicker. We’ll come back to this subject in chapter 4, where you’ll learn how to create heap-based recursion in Java. For the rest of this section, you’ll pretend Java’s standard recursion isn’t broken.
A recursive method is simple to define. The method factorial(int n) can be defined as returning 1 if its argument is 0, and n * factorial(n – 1) otherwise:
- public int factorial(int n) {
- return n == 0 ? 1 : n * factorial(n - 1);
- }
Let's write a recursive factorial function (Exercise 2.12).
Hint.
Put aside this chicken-and-egg problem for the moment. Converting a single argument method into a function is straightforward. The type is Function
- Function
factorial = n -> n <= 1 ? n : n * factorial.apply(n – 1);
- int x = x + 1;
- int x;
- {
- x = x + 1;
- }
- public Function
factorial; - {
- factorial = n -> n <= 1 ? n : n * factorial.apply(n - 1);
- }
- public static Function
factorial; - static {
- factorial = n -> n <= 1 ? n : n * factorial.apply(n - 1);
- }
- public final Function
factorial = - n -> n <= 1 ? n : n * this.factorial.apply(n - 1);
By adding this. before the variable name, it’s possible to self-reference it while making it final. For the static implementation, you just have to replace this with the name of the including class:
- public static final Function
factorial = - n -> n <= 1 ? n : n * FunctionExamples.factorial.apply(n - 1);
You’ve seen that in functional programming, functions are treated as data. They can be passed as arguments to other functions, can be returned by functions, and can be used in operations, exactly like integers or doubles. In future programs, you’ll apply operations to functions, and you’ll need a neutral element, or identity element, for these operations. A neutral element will act as the 0 for addition, or 1 for multiplication, or the empty string for string concatenation.
The identity function can be added to the definition of our Function class in the form of a method named identity, returning the identity function:
- static
Function identity() { - return t -> t;
- }
Listing 2.2. The complete Function interface
- package fp.utils;
- public interface Function
{ - U apply(T x);
- default
Function compose(Function f) { - return x -> this.apply(f.apply(x));
- }
- default
Function andThen(Function f) { - return x -> f.apply(apply(x));
- }
- static
Function Identity() { - return t -> t;
- }
- static
Function Compose(Function f, Function g) { - return x -> f.apply(g.apply(x));
- }
- static
Function andThen(Function f, Function g) { - return x -> g.apply(f.apply(x));
- }
- static
Function , Function , Function >> compose() { - return x -> y -> y.compose(x);
- }
- static
Function , Function , Function >> andThen() { - return x -> y -> y.andThen(x);
- }
- static
Function , Function , Function >> higherAndThen() { - return x -> y -> z -> y.apply(x.apply(z));
- }
- static
Function , Function , Function >> higherCompose() { - return (Function x) -> (Function
y) -> (T z) -> x.apply(y.apply(z)); - }
- }
Lambdas are used in places where a specific interface is expected. This is how Java can determine which method to call. Java doesn’t impose any constraints on naming, as may be the case in other languages. The only constraint is that the interface used must not be ambiguous, which generally means it should have only one abstract method. (In reality, it’s a bit more complex, because some methods don’t count.) Such interfaces are said to be SAM type, for single abstract method, and are called functional interfaces.
Note that lambdas aren’t used only for functions. In standard Java 8, many functional interfaces are available, although they aren’t all related to functions. The most important ones are listed here:
Java defines many other functional interfaces (43 in the java.util.function package) that are mostly useless for functional programming. Many of them deal with primitives and others with functions of two arguments, and there are special versions for operations (functions of two arguments of the same type).
In this book, I don’t talk much about standard Java 8 functions. This is intentional. This isn’t a book about Java 8. It’s a book about functional programming, and it happens to use Java for the examples. You’re learning how to construct things rather than to use provided components. After you master the concepts, it’ll be up to you to choose between your own functions or the standard Java 8 ones. Our Function is similar to the Java 8 Function. It doesn’t use a wildcard for its argument in order to simplify the code shown in the book. On the other hand, the Java 8 Function doesn’t define compose and andThen as higher-order functions, but only as methods. Other than these differences, these Function implementations are interchangeable.
Debugging with lambdas
Using lambdas promotes a new style of code writing. Code that was once written in several short lines is often replaced with one-liners such as this:
- public
T ifElse(List conditions, List ifTrue, T ifFalse) { - return conditions.zip(ifTrue).flatMap(x -> x.first(y -> y._1))
- .map(x -> x._2).getOrElse(ifFalse);
- }
In Java 5 to 7, this code would be written without using lambdas, as shown in the following listing.
Listing 2.3. A one-liner lambda-based method converted to previous Java versions
- public
T ifElse(List conditions, List ifTrue, T ifFalse) { - Function
, Boolean> f1 = - new Function
, Boolean>() { - public Boolean apply(Tuple
y) { - return y._1;
- }
- };
- Function
- >, Result
>> f2 = - new Function
- >, Result
>>() { - public Result
> apply(List > x) { - return x.first(f1);
- }
- };
- Function
, T> f3 = - new Function
, T>() { - public T apply(Tuple
x) { - return x._2;
- }
- };
- Result
- >> temp1 = conditions.zip(ifTrue);
- Result
> temp2 = temp1.flatMap(f2); - Result
temp3 = temp2.map(f3); - T result = temp3.getOrElse(ifFalse);
- return result;
- }
- public
T ifElse(List conditions, List ifTrue, T ifFalse) { - return conditions.zip(ifTrue)
- .flatMap(x -> x.first(y -> y._1))
- .map(x -> x._2)
- .getOrElse(ifFalse);
- }
To make this problem less crucial, it’s important to extensively unit test each component, which means each method and each function passed as an argument to each method. Here, it’s easy. The methods used are (in order of appearance) List.zip, Option.flatMap, List.first, Option.map, and Option.getOrElse. Whatever these methods are doing, they can be extensively tested. You don’t know about them yet, but you’ll build the Option and List components in the next chapters, and also write the implementations of the map, flatMap, first, zip, and getOrElse methods (as well as many others). As you’ll see, these methods are purely functional. They can’t throw any exceptions and they always return the intended result without doing anything else. So, after they’re fully tested, nothing bad can happen.
Regarding the functions, the preceding example uses three of them:
The first one can’t throw any exceptions because x can’t be null (you’ll see why in chapter 5), and method first can’t throw an exception either. The second and third functions can’t throw a NullPointerException because you’ve ensured that a Tuple couldn’t be constructed with null arguments. (See chapter 1 for the code of the Tuple class.) Figure 2.4 shows these functions in their anonymous form.
Figure 2.4. Functions in their anonymous form
This is one area where functional programming shines: if no components can break, the whole program can’t either. In imperative programming, components might work fine in tests but break in production because of some nondeterministic behavior. If the behavior of a component depends on external conditions, you have no way to fully test it. And even if no component has any problem as a unit, the composition of several components could create conditions for the program to be ill-behaved. This can’t happen with functional programming. If the components have a deterministic behavior, the whole composition will be deterministic too.
Many spots remain open for errors. The program might not do what is expected, because the components may be composed the wrong way. But implementation errors can’t cause an unwanted crash. If this program crashes, it will be, for example, because a null reference has been passed to the Tuple constructor. You don’t need a debugger to catch this kind of error.
So, yes, debugging functional programs that use lambdas extensively is somewhat more difficult than debugging imperative programs, but debugging is much less necessary, provided all the components have been validated. Keep in mind that this is true only if a thrown exception crashes the program. We’ll come back to this in chapter 6. But for now, remember that by default, an exception or an error thrown will only crash the thread in which it happened, and not the whole application. Even an OutOfMemoryError might not crash the application, so you, as the programmer, have to handle this.
Supplement
* Wiki - Covariance and contravariance (computer science)
* Ch2 - Using functions in Java : Part1
* Ch2 - Using functions in Java : Part2
初学程序员的java编程实例
回覆刪除java代码从命令行隐藏密码