Java Currying
Since functions can be considered as objects, they can be returned as results from other functions. It allows us to use a special style of programming where we convey arguments to a function one by one and obtain functions as intermediate results. This is a fairly advanced technique, so you won't have to use it every time you can, but it is useful to be aware of such an option.
Returning functions
When we consider a function as an object we can accept it as an argument and return it as a value.
Here's an example:
public static IntBinaryOperator sumF(IntUnaryOperator f) {
return (a, b) -> f.applyAsInt(a) + f.applyAsInt(b);
}
The sumF
method accepts an operator f
with an integer argument and returns another operator with two integer arguments. In the method body, an anonymous function that takes two arguments is constructed and returned. This function applies f
to each of its arguments and summarizes the results.
What can we do now? Let's just return the function from the method, save it to the sumOfSquares
variable and apply some values to it:
// build a new sumOfSquares operator
IntBinaryOperator sumOfSquares = sumF(x -> x * x);
// the sum is equal to 125 (5 * 5 + 10 * 10)
long sum = sumOfSquares.applyAsInt(5, 10);
Here is a code block with some more 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 cubes: 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 provides an easy way to build complex and generalized functions.
Currying functions
Currying is a technique for translating the evaluation of a function that takes multiple parameters into evaluating a sequence of functions, each with a single argument. The technique is named after mathematician Haskell Curry who originally developed it. Let's see how to use it in Java.
First, let's compare a regular function and a curried function in this code snippet:
IntBinaryOperator notCurriedFun = (x, y) -> x + y; // not a curried function
IntFunction<IntUnaryOperator> curriedFun = x -> y -> x + y; // a curried function
We can define a 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 shorter example:
// here the result is equal to 153
int anotherResult = fff.apply(10).apply(15).apply(3);
Let's rewrite the sumF
method from the earlier example. Instead of returning a function from it, 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
Suppose we would like to say "Hi"
to our friends and "Hello"
to our business partners. We can create a function that has two arguments: what
and who
. The function will apply what
depending on the context.
Function<String, Consumer<String>> say = what -> who -> System.out.println(what + ", " + who);
The friends' context:
List<String> friends = Arrays.asList("John", "Neal", "Natasha");
Consumer<String> sayHi = say.apply("Hi");
// many lines of code...
friends.forEach(sayHi);
The partner's context:
List<String> partners = Arrays.asList("Randolph Singleton", "Jessie James");
Consumer<String> sayHello = say.apply("Hello");
// many lines of code...
partners.forEach(sayHello);
The result:
Hi, John
Hi, Neal
Hi, Natasha
Hello, Randolph Singleton
Hello, Jessie James
In the real situation, we can get the list of persons from a database and pass the consumer as an argument from another part of our program.
Conclusion
Returning functions and currying are powerful techniques in functional programming that enable more flexible, modularand expressive code by allowing functions to be passed as arguments and returned as values. While not always necessary, these approaches are valuable tools to consider for building complex and reusable function logic in Java.