Java Functional Programming: Second, Higher-Order Functions, Closures, Function Composition, and Currying

To undertake the above: Java functional programming: 1. Functional interfaces, lambda expressions and method references
This time, let's talk about some other important concepts and technologies in functional programming, so that we can master functional programming in Java more deeply.
This blog mainly discusses the following issues

  • Higher order functions
  • Closure concept
  • Use of function composition handlers
  • Currying and partial evaluation
    Start with:

1. Higher order functions

The higher-order function does not refer to the one in mathematics. It is mainly understood from the concept of dimension. Originally, the function generates a value, that is to say, the function is higher-dimensional than the value, then if we have a function that can generate a function or function as a parameter, then it is obviously higher dimensional than a normal value-generating function, because I can generate you.

Definition: A higher-order function is a function that can accept a function as a parameter or can return a function as a value.

public interface Function{
    String str(String s);
}

public class Procudure{
    // The following is a standard higher-order function
    public Function(String s){
        return s -> s.upperCase();
    }
}

There are two points here:

  • We can create aliases for specialized interfaces by inheriting the interfaces in java.util.function or by customizing a functional interface
  • With lambda expressions, it is clear that we can easily create and return a function

But that's just the basics, remember what functional programming is all about? The key here is that, sometimes, we can make a higher-order function generate a new function based on the accepted function.

public class test {
    public static Function<String, String> transform(Function<String, String> f){
        return f.andThen(String::toUpperCase);
    }

    public static void main(String[] args) {
        Function<String, String> transform = test2.transform(str -> {
            return str.substring(0, 2);
        });

        String s = transform.apply("abcdefg");
        System.out.println(s);
    }
}

It can be seen that here we connect the two methods before and after through the general method of the Function interface andThen(), and make the string converted to all uppercase no matter what we enter, and then we enter a interception of the first two character as a method of returning a value, but obviously, there are more choices here, and we can actually refer to some defined functions through method references, which is very flexible.

2. Closure

What is a closure?

Consider a lambda expression that uses a variable outside its function scope. What happens when the function is returned? That is, what happens when we refer to these external variables by calling the anonymous method generated by the lambda expression?

If a language can solve this problem, we can say that the language supports closures, or we can say that it supports lexical scoping.

There is also a term involved here: variable capture

It doesn't matter if the above sounds unclear, let's give an example:

public class Example{
    IntSupplier plus(int x){
        int y = 1;
        return () -> x + y;
    }
}

Consider this class and the method plus(int x) in it, and you'll see that there are some problems.

Because our plus(int x) method returns a function, it is assumed here that the returned function is f(int x), that is to say, when f(int x) returns, plus(int x) has been executed, so where The variable int y = 1; has gone out of scope, so when we get the object of f(int x) and then call the f(int x) method, what should we do with this y?

You will find that the above method can be compiled and executed successfully, but the following one cannot:

public class Example{
    IntSupplier plus(int x){
        int y = 1;
        return () -> x + (++y);
    }
}

why? Compiler Hint: Variables used in lambda expressions should be final or effectively final

This sentence is very clear. For the first example, although our y does not have the final keyword, it is a de facto final variable. Once assigned here, it will not be changed. For the second method, the Saying it is equivalent to assigning a new value to y.

Here if we use references, such as the following example

public class Example{
    IntSupplier plus(int x){
        Queue<Integer> y = new LinkedList<>();
        y.offer(1);
        return () -> x + y.poll();
    }
}

Note that it can be compiled here, because in fact, we only need to ensure that the object pointed to by this reference is not modified, so that when the returned function is called later, it is suddenly found that the corresponding object cannot be found.

Therefore, the condition of the closure provided by Java is that we must be able to guarantee that the captured variable is final.

However, it should be noted that if it is a multi-threaded situation here, thread safety cannot be guaranteed.

3. Function composition

We mentioned the andThen() method before. These methods are provided in each functional interface in the Java.util.function package. In general, there are several types:

  • andThen(arg)
    • Execute the original operation first, then the parameter operation
  • compose(arg)
    • Execute the parameter operation first, then the original operation
  • and(arg)
    • logical AND operation on primitive and parameter predicates
  • or(arg)
    • Logical OR of primitive and parameter predicates
  • negate()
    • The resulting predicate is the negation of the original predicate

In the stream processing chapters that follow, you will appreciate the power of composition of these functions.

4. Currying and partial evaluation

The so-called currying refers to converting a function that accepts multiple parameters into a series of functions that accept only one parameter. The purpose of doing this in functional-oriented programming is the same as we need to abstract interfaces and interfaces in object-oriented programming. The abstract class is the same, the purpose is that we can reuse the code through partial evaluation.

public class Currying{
    // Uncurried function
    static String unCurried(String a,String b){
        return a + b;
    }
    
    // Curried functions
    static Function<String, Function<String, String>> Curried(){
        return a -> b -> a + b;
    }
    
    // example
    public static void main(String[] args) {
        Function<String, Function<String, String>> curried = test2.Curried();

        System.out.println(unCurried("hello ", "World"));

        Function<String, String> firstWord = curried.apply("hello ");

        System.out.println(firstWord.apply("World"));
        System.out.println(firstWord.apply("My friend"));
        System.out.println(firstWord.apply("My love"));
    }
    
    /** 
    	output
        hello World
        hello World
        hello My friend
        hello My love
    **/
}

Simply put, each layer returns the function of the next layer until it finally returns the value we need.

Posted by DimeDropper on Wed, 02 Nov 2022 21:37:50 +0530