Returning functions
As a function may be considered as an object we can accept it as a method argument and return it as a value.
Let's consider the code below.
public static IntBinaryOperator sumF(IntUnaryOperator f) {
return (a, b) -> f.applyAsInt(a) + f.applyAsInt(b);
}
What can we do now?
Let's write a function sumOfSquares in terms of sumF and then apply values.
// build a new sumOfSquares operator
IntBinaryOperator sumOfSquares = sumF(x -> x * x);
// the sum is equal to 125
long sum = sumOfSquares.applyAsInt(5, 10);
Other examples:
// sum of two identities: 0 + 10 = 10
long sumOfIdentities = sumF(x -> x).applyAsInt(0, 10);
// sum with coefficients: 10 * 2 + 11 * 2 = 42
long sumWithCoefficient = sumF(x -> x * 2).applyAsInt(10, 11);
// sum of two squares: 3 * 3 * 3 + 8 * 8 * 8 = 539
long sumOfCubes = sumF(x -> x * x * x).applyAsInt(3, 8);
As you can see, the possibility of returning functions opens an easy way to build more complex and generalized functions.
Currying functions
Currying is a technique of translating the evaluation of a function that takes multiple parameters into evaluating a sequence of functions, each with a single parameter.
IntBinaryOperator notCurriedFun = (x, y) -> x + y; // not a curried function
IntFunction<IntUnaryOperator> curriedFun = x -> y -> x + y; // a curried function
We can define curried function with three arguments and then apply arguments one by one.
// curried function
IntFunction<IntFunction<IntFunction<Integer>>> fff = x -> y -> z -> x * y + z;
// fff returns a curried function y -> z -> 2 * y + z
IntFunction<IntFunction<Integer>> ff = fff.apply(2);
// ff returns a curried function z -> 2 * 3 + z
IntFunction<Integer> f = ff.apply(3);
// f returns 7
int result = f.apply(1);
A more short example.
// the another result is equal to 153
int anotherResult = fff.apply(10).apply(15).apply(3);
Let's rewrite the considered sumF method. Instead of this method returning a function we can write a curried function and then use it in the same way.
Function<IntUnaryOperator, IntBinaryOperator> sumF =
(f) -> (a, b) -> f.applyAsInt(a) + f.applyAsInt(b);
// build a new sumOfSquares operator in terms of sumF
IntBinaryOperator sumOfSquares = sumF.apply(x -> x * x);
// the sum is equal to 125 again
long sum = sumOfSquares.applyAsInt(5, 10);
As you see, returning functions and currying are very close concepts and both are based on closures.
An example of currying
Let's consider another example.
We would like to say "Hi" to our friends and "Hello" to our partners. We can create a function that has two arguments: what and whom. The function will apply what in dependency on the context.
Function<String, Consumer<String>> say = what -> whom -> System.out.println(what + ", " + whom);
The friends' context:
List<String> friends = Arrays.asList("John", "Neal", "Natasha");
Consumer<String> sayHi = say.apply("Hi");
// too many lines of a code...
friends.forEach(sayHi);
The partner's context:
List<String> partners = Arrays.asList("Randolph Singleton", "Jessie James");
Consumer<String> sayHello = say.apply("Hello");
// somewhere in another method
partners.forEach(sayHello);
The result:
Hi, John
Hi, Neal
Hi, Natasha
Hello, Randolph Singleton
Hello, Jessie James
Of course, we could get the person list from a database and a passing the consumer as a method parameter from another part of our program or do something more complex.