Exploring The Benefits Of Function Currying In Java - Understanding The Concept And Advantages

Introduction

Currying is a technique used in Functional Programming to break down a function that takes multiple arguments into a sequence of functions that each take a single argument. This may sound unfamiliar if you're used to working in an imperative programming environment. In this article, we'll simplify what Currying is, explore its benefits, and highlight how it can help address certain limitations.

How Currying Enhances Functional Programming: Demystifying the Concept and Its Advantages

Currying is a technique used in Functional Programming that involves breaking down a function that takes multiple arguments into a series of functions, each of which takes only one argument. This can make it easier to compose functions and reason about their behavior and can lead to more modular and reusable code.

Currying solves the limitation of BiFunction and TriFunction

In the Java util package, there are two Functional Interfaces called "Function<A, B>" and "BiFunction<A, B, C>". The "Function" interface accepts one input and returns an output, while the "BiFunction" interface accepts two inputs and returns an output. For example:

Function<String, String> toUpper = (s) -> s.toUpperCase();
BiFunction<String, String, String> concat = (s1, s2) -> s1 + s2 ;

The initial example demonstrates the use of the "Function" interface by accepting a String input and returning its uppercase version. The second example utilizes the "BiFunction" interface, taking two strings as input and concatenating them.

This represents a clear limitation as we only have two functions available, with one accepting only one input and the other accepting two inputs. Therefore, if we require a function that accepts three inputs, we must create our own custom "Function". Let's create a Function that takes 3 inputs and call it as TriFunction, and implementation of it looks like this,

@FunctionalInterface
public interface TriFunction<A, B, C, R> {
    R apply(A a, B b, C c);

    default <R> TriFunction<A, B, C, R> andThen(TriFunction<A, B, C, R> after) {
        return (A a, B b, C c) -> after.apply(a,b,c);
    }
}
//usage
TriFunction<String, String, String, String> triFunction = (s1, s2, s3) -> s1 + s2 + s3;

However, if we find ourselves in a scenario requiring more parameters than the available functions can accept, Currying offers a solution to this limitation. Let's rewrite the above function using Currying, reiterating a Currying definition "Currying is a technique used in Functional Programming that involves breaking down a function that takes multiple arguments into a series of functions, each of which takes only one argument."

Function<String, Function<String, Function<String, String>>> triFunctionConcat = (s1)
-> (s2)
-> (s3) -> s1 + s2 + s3;

The code 'Function<String, Function<String, Function<String, String>>> triFunctionConcat' defines a functional interface that represents a curried function with three String parameters and one String return value.

Breaking it down from right to left:

  • The innermost 'Function<String, String>' takes a String as input and returns a String. This is the output type of the entire function.
  • The next 'Function<String, Function<String, String>>' takes a String as input and returns a new function that takes another String as input and returns a String. This is a curried function that has two String inputs.
  • The outermost 'Function<String, Function<String, Function<String, String>>>' takes a String as input and returns a new function that takes two more String inputs and returns a String. This is a curried function that has three String inputs.

In simpler terms, this functional interface represents a function that takes three String inputs and returns a String output. It is curried, meaning it can be broken down into a sequence of functions that each take a single input, allowing for more modular and composable code. This can make it easier to create more complex functions by composing existing functions together.

The execution of "triFunctionConcat" looks like,

String result = triFunctionConcat.apply("Java").apply("Programming").apply("Language");
System.out.println(result); //JavaProgrammingLanguage

Perfect, now I am assuming Currying is making sense here.

Creating more modular and reusable code.

Suppose we have a function filter that takes a list of integers and returns a new list containing only the even numbers:

List<Integer> filter(List<Integer> list) {
    List<Integer> result = new ArrayList<>();
    for (Integer i : list) {
        if (i % 2 == 0) {
            result.add(i);
        }
    }
    return result;
}

Suppose we want to use this function to filter a list of strings where we only want to keep the strings with even lengths. With Function Currying, we can transform the 'filter' function into a sequence of functions, each of which takes only one argument:

Function<Predicate<Integer>, Function<List<String>, List<String>>> filter =
    p -> list -> list.stream().filter(s -> p.test(s.length())).collect(Collectors.toList());

List<String> result = filter.apply(i -> i % 2 == 0).apply(Arrays.asList("AB", "CDE", "ABCD"));

That results in [AB, ABCD]

Conclusion

In addition to simplifying function parameter limitations, Currying enables simpler function composition, which is further explained in my other article. I highly recommend focusing on the first advantage of Currying. If you've found this information helpful, please check out my GitHub repository (https://github.com/sameershukla/JavaFPLearning) for pragmatic explanations of various FP concepts and consider giving it a star.