How to Implement Function Composability In Java

  • Post last modified:December 15, 2022
  • Reading time:5 mins read

Introduction

  • Composition is one of the attractive features that can be built using Lambdas in Java.
  • Often time we need to compose different functions in order to build higher-level functions.
  • In this article, we will deep dive to understand function composability and how we can achieve it in Java.

Use Case

  • Building Pipeline for File Processing Logic. 
    We will follow these steps Read -> Validate -> Compute -> Write
    and build Function composition.

Building Composition 

  • For each process, we will first create lambda expressions. For example, we have lambda expressions for reading files, validating logic, etc.
  • Once we have lambda expression we can use andThen to bind these operations one after another.

File Read Logic

  • As next, we will implement the logic for each lambda expression that we defined above.
  • Below is file read logic that takes the file path and returns a list of strings.
 private static List<String> FileRead(String FilePath){
        List<String> readLines = null;
        try {
            readLines = Files.readAllLines(Path.of(FilePath));
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("readLines : "+readLines);
        return readLines;
    }

Data Validation Logic

  • Data validation logic is next in pipeline, which is defined below.
private static List<String> validateRecords(List<String> content, Predicate<String> logic){
        return content.stream()
                .filter(logic.negate())
                .collect(Collectors.toList());
}

Compute Logic

  • Now we can compute logic , here we are computing frequency of each word.
  • The Use of peek is for debugging purpose, it doesn’t play any transformational role.
private static Map<String, Long> computeOccurence(List<String> content){
        return content
                .stream()
                .peek(System.out::println)
                .collect(Collectors
                        .groupingBy(Function.identity(), Collectors.counting())
                );
}

Writing to File

  • Now once we have computed data, all we have to do is to write to the output sink , which is the file in this case.
private static boolean write(Map<String, Long> a) {
        boolean result = false;
        try {
           result = writeToLocal(a);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }
  
  private static boolean writeToLocal(Map<String, Long> occurrenceMap) throws IOException {
        FileWriter fileWriter = new FileWriter(""); // replace with output file
        BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
        boolean isCompleteWrite = true;
        for(Map.Entry<String, Long> entry: occurrenceMap.entrySet()){
            try {
                bufferedWriter.write(entry.getKey()+","+entry.getValue());
                bufferedWriter.newLine();
            } catch (IOException e) {
                e.printStackTrace();
                isCompleteWrite = false;
            }
        }
        bufferedWriter.close();
        return isCompleteWrite;
    }

Client Code

  • Now since we have developed all the pieces, our overall code will look like below.
  • Basically, line 16 is where we have started building function composability.
  • Once we have the pipeline ready all we have to do is to run apply and pass the fille path to it.
  • For all the files we go through a defined path in the logic and provide output as an output file.
public static void main(String[] args) {

        String inputFile = ""; // replace input file name

        // read file
        Function<String, List<String>> fileRead = CompositionExample::FileRead;
        // validate records
        Predicate<String> isEmptyOrNull = (a) -> a == null && a.isEmpty();
        Function<List<String>,  List<String>> validate = (a) -> validateRecords(a, isEmptyOrNull);
        // compute metrics
        Function<List<String>, Map<String, Long>> computeOccurence = CompositionExample::computeOccurence;
        // write to local file
        Function<Map<String, Long>, Boolean> writeToLocal = CompositionExample::write;

        // setup pipeline
        Function<String, Boolean> pipeline = fileRead
                .andThen(validate)
                .andThen(computeOccurence)
                .andThen(writeToLocal);

        Boolean result = pipeline.apply(inputFile);
        System.out.println("does pipeline ran successfully ? : " + result);
}

Complete Code

  • Here is how complete code looks like.
package Composition;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class CompositionExample {


    public static void main(String[] args) {

        String inputFile = ""; // replace with full path of input file

        // read file
        Function<String, List<String>> fileRead = CompositionExample::FileRead;
        // validate records
        Predicate<String> isEmptyOrNull = (a) -> a == null && a.isEmpty();
        Function<List<String>,  List<String>> validate = (a) -> validateRecords(a, isEmptyOrNull);
        // compute metrics
        Function<List<String>, Map<String, Long>> computeOccurence = CompositionExample::computeOccurence;
        // write to local file
        Function<Map<String, Long>, Boolean> writeToLocal = CompositionExample::write;

        // setup pipeline
        Function<String, Boolean> pipeline = fileRead
                .andThen(validate)
                .andThen(computeOccurence)
                .andThen(writeToLocal);

        Boolean result = pipeline.apply(inputFile);
        System.out.println("does pipeline ran successfully ? : " + result);
    }

    private static boolean write(Map<String, Long> a) {
        boolean result = false;
        try {
           result = writeToLocal(a);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }

    private static List<String> FileRead(String FilePath){
        List<String> readLines = null;
        try {
            readLines = Files.readAllLines(Path.of(FilePath));
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("readLines : "+readLines);
        return readLines;
    }

    private static List<String> validateRecords(List<String> content, Predicate<String> logic){
        return content.stream()
                .filter(logic.negate())
                .collect(Collectors.toList());
    }

    private static Map<String, Long> computeOccurence(List<String> content){
        return content
                .stream()
                .peek(System.out::println)
                .collect(Collectors
                        .groupingBy(Function.identity(), Collectors.counting())
                );
    }

    private static boolean writeToLocal(Map<String, Long> occurrenceMap) throws IOException {
        FileWriter fileWriter = new FileWriter("");// replace with output file
        BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
        boolean isCompleteWrite = true;
        for(Map.Entry<String, Long> entry: occurrenceMap.entrySet()){
            try {
                bufferedWriter.write(entry.getKey()+","+entry.getValue());
                bufferedWriter.newLine();
            } catch (IOException e) {
                e.printStackTrace();
                isCompleteWrite = false;
            }
        }
        bufferedWriter.close();
        return isCompleteWrite;
    }
}

Conclusion

  • In this article, we discussed about Function Composability and how we can use Lambda / functional programming to compose higher-order functions from the chain of simple functions.

Bonus Tip

Leave a Reply