Functional programming in Java - Functions

First article: https://zomor.hashnode.dev/functional-programming-in-java

Second article: https://zomor.hashnode.dev/functional-programming-in-java-concepts

As we have elaborated earlier, the functional paradigm has been introduced to Java since Java 8. In this article, we will discuss further how Java is implementing functions

Function as a first-class

As we mentioned in the first article, to be treated as a first-class, function needs to be (defined - passed as an arg - returned). We will see now how this is implemented starting from Java 8

I- Defining function

Function is an interface, that takes two generic args, and to use it, we run the (apply) method

Also, we have lots of Function alternatives as we will see now:

Function<T, R> myFunction = t -> r; //Equiv. to R myFunction(T t)
BiFunction<T, U, R> myBiFunction = (t, u) -> r; //Equiv. to R myBiFunction(T t, U u)
Supplier<R> mySupplier = () -> r; //Equiv. to R mySupplier()
Consumer<T> myConsumer = t -> System.out.println(t); //Equiv. to void myConsumer(T t)
Predicate<T> myPredicate = t -> true; //Equiv. to boolean myPredicate(T t)
UnaryOperator<T> myUnary = t -> t; //Equiv. to T myUnary (T t)
BinaryOperator<T> myBinary = (t1, t2) -> t; //Equiv. to T myUnary (T t1, T t2)

// To use any function from above, we use the following:
// function.apply() And pass the args needed
// Except for the following:
// supplier.get()
// consumer.accept()
// predicate.test()

// To create a new custom function
@FunctionalInterface
interface TriFunction<T, U, V, R> {
    R apply(T t, U u, V v);
}
// Then we can use it as following
TriFunction<Integer, Integer, Integer, String> sumThree = (t, u, v) -> "The summation of " + t + " + " + u + " + " + v " is: " + (t + u + v);
System.out.println(sumThree.apply(2, 3, 10)); //Prints 15

II- Passing function as an arg

To create a function that receives another function, we shall add the function as an arg in the function

BiFunction<Integer, Integer> sumTwo = (x, y) -> x + y;
BiFunction<Integer, Integer> subtractTwo = (x, y) -> x - y;
TriFunction<Integer, Integer, BiFunction<Integer, Integer>> useFunction = (x, y, func) -> func.apply(x, y);

System.out.println(useFunction(5, 3, sumTwo)); //prints 8
System.out.println(useFunction(5, 3, subtractTwo)); //prints 2

III- Returning function

Now, we are creating a function that returns another function, which makes much more flexibility in the code, and also reusability, as we can use the same method, with different inputs, to generate new methods dynamically

Function<Integer, Function<Integer, Integer>> multiply = x -> y -> x * y;
Function<Integer, Integer> multiplyByFive = multiply.apply(5);
Function<Integer, Integer> multiplyBySix = multiply.apply(6);

System.out.println(multiplyByFive.apply(10)) // prints 50
System.out.println(multiplyBySix.apply(10))  // prints 60

Higher Order Function

Mainly, higher-order functions are used to add validations inside the function to achieve the concept of single responsibility for each function and also to keep each check in a single block to be easy to maintain and test

// We need to make a function to divide (a / b) if a is bigger than b AND b is not 0

// Divide function
BiFunction<Float, Float, Float> divide = (x, y) -> x / y ;

// Test 0 Function
Function<BiFunction<Float, Float, Float>, BiFunction<Float, Float, Float>> TestZero = func -> (x, y) -> {
    if ( y == 0 ) {
        System.out.println("Can not divide by zero");
        return 0f;
    }
    return func.apply(x, y);
} 

// Test x > y Function
Function<BiFunction<Float, Float, Float>, BiFunction<Float, Float, Float>> TestXGreaterThanY = func -> (x, y) {
    if ( x < y ) {
        System.out.println("Can not divide number less than 
            the other");
        return 0f;
    }
    return func(x, y);
}

// Merge the two tests together (From the last to the first check)
BiFunction<Float, Float, Float> divideWithTestXGreaterThanY = TestXGreaterThanY.apply(divide)
BiFunction<Float, Float, Float> divideWithTestXGreaterThanYAndZeroSafe = TestZero.apply(divideWithTestXGreaterThanY)

System.out.println(divideWithTestXGreaterThanYAndZeroSafe.apply(10.0, 5.0) // Prints 2
System.out.println(divideWithTestXGreaterThanYAndZeroSafe.apply(10.0, 0) // Prints Can not divide by zero -> 0.0
System.out.println(divideWithTestXGreaterThanYAndZeroSafe.apply(10.0, 50.0) // Prints Can not divide number less than the other -> 0.0

Partial Function

If we have a generic function, and we want to keep a static arg to be used in different places while keeping the original function

Function<Integer, Function<Integer, Integer>> multiply = x -> y -> x * y;
Function<Integer, Integer> multiplyByFive = multiply.apply(5);

System.out.println(multiplyByFive.apply(10)) // prints 50
System.out.println(multiply.apply(10, 10))  // prints 100

Conclusion

As we have just seen, you can use functions as variables, define them, pass them as an arg or even return them.. Also, you can customize a new function as per your usage.

In the upcoming article, we will talk about one of the most important things introduced in Java 8 -> Streams

You can find more code examples on this repo

github.com/elZomor/functional_programming_w..