Make Your Code More Readable With Functional Programming

Woman reading a book while seated on a black leather couch
Photo by Seven Shooter on Unsplash

I had heard about functional programming since Java made it available in 2014 with the release of version 1.8 of the JDK. I had been to several user group meetings where the speakers spoke glowingly of pure functional languages such as Scala and Haskell. It sounded promising, but I didn’t give it much thought until around 2018. That’s when I started seeing Java Streams being used more often in code examples. Recently, I was writing a simple integration solution, and the resulting code looked horrible. That’s when I gave functional programming an honest try. To my amazement, it indeed made my code cleaner. I’ll share my experiences with you and show you how you can make your code more readable with functional programming.

What Is Functional Programming?

Functional programming is a style of writing programs that uses a series of mathematical functions to perform some sort of computation. It uses a declarative approach, in that you specify what has to happen instead of how to make it happen. Each function has one input and one output, and has a single method. A function’s output only depends on its input, and nothing else. You string two or more functions together to accomplish what you want.

This differs from the imperative approach that most of us have been using for years. With imperative programming, you specify how to accomplish the computation. You use a series of step-by-step instructions to change the state of whatever you are working on until you get the desired result. Procedural programming and object-oriented programming are two types of imperative programming.

Wrapping your head around functional programming is not a trivial effort when you’ve spent a career in object-oriented programming. Personally, I’m finding it as big a leap as going from structured to object-oriented programming. But with enough determination, the “ah-hah” moments accumulate, and you eventually get it.

Types of Functions in Java

In Java, we typically use Lambda expressions with functions. There are four types of functions: Supplier, Function, Predicate, and Consumer. Each of these are interfaces in the java.util.function package. They are typed, based on what they are dealing with.

Supplier<T>

A suppler is usually the start of a functional program. It has one no-argument method – get() – that returns a typed object. for example:

Supplier<LocalDate> dateSupplier = () -> LocalDate.now();

Function<I, O>

Its apply method takes an object of type I and returns an object of type O. The letters don’t matter; think of them and <Input, Output>. You can chain several functions together, the output type of one is the input type of the next one.

For example, this takes a LocalDate and performs some logic to retrieve timesheets and returns them in a TimesheetEntries object::

Function<LocalDate, TimesheetEntries> timesheetFetcher = timesheetDate -> {
    return fetchTimesheets(timesheetDate); // Some method that returns a TimesheetEntries object.
}

Predicate<T>

The test method takes an argument of type T and returns a boolean:

Predicate<TimesheetEntries> filter = timesheets -> {
    return !timesheets.getEntries.isEmpty();
}

Consumer<T>

The opposite of a Supplier, a Consumer is usually the end of a functional program. Its accept method takes a typed object and returns void:

Consumer<QbdTimesheetEntries> consumer = timesheets -> {
apiAdapter.save(timesheets);
};

An Example of Imperative Code

I have been working on a use case where I need to fetch timesheet entries from TSheets, transform them, and make a call to QBD API to persist them. How far back I go depends on an AWS System Manager parameter known as “timesheet last fetched date”. I fetch all timesheets from the day after the last fetched date until yesterday. In the event the last fetched date was indeed yesterday, I log a message and do nothing. If no timesheet entries come back from the fetch operation (because I didn’t work on the weekend), then update the last fetched date to yesterday, and do nothing else. Oh, and if this is a dry run, go through the same motions without updating anything.

With that out of the way, here is what the imperative code looks like:

public void run() {
if (verbose) {
log.info("Begin fetching timesheets.");
}
if (dryRun) {
log.info("This is a dry run.");
}
tsheetsPort.checkAvailability();
qbdApiPort.checkAvailability();
LocalDate timesheetLastFetchedDate = LocalDate.parse(configPort.getTimesheetLastFetchedDate(), ISO_DATE);
LocalDate today = LocalDate.now();
LocalDate yesterday = today.minusDays(1);
if (timesheetLastFetchedDate.isBefore(yesterday)) {
LocalDate fromDate = timesheetLastFetchedDate.plusDays(1);
String jsonString = tsheetsPort.retrieveTimesheets(null, fromDate.format(ISO_DATE),
yesterday.format(ISO_DATE));
Map<String, Object> jsonMap = xformToJsonMap(jsonString);
if (hasTimesheets(jsonMap)) {
QbdTimesheetEntries qbdApiBody = transformer.transform(jsonString);
if (dryRun) {
log.info("This would be the call to QBD API:{}{}", System.getProperty("line.separator"),
formatQbdTimesheetEntries(qbdApiBody));
} else {
qbdApiPort.enterTimesheets(qbdApiBody);
configPort.updateTimesheetLastFetchedDate(yesterday);
}
} else {
log.info("No timesheet entries retrieved between {} and {}", fromDate.format(simpleDisplayFormatter),
yesterday.format(simpleDisplayFormatter));
if (!dryRun) {
configPort.updateTimesheetLastFetchedDate(yesterday);
}
}
} else {
log.debug("Today is {} and timesheet last fetched date is {}", today.format(ISO_DATE),
timesheetLastFetchedDate.format(ISO_DATE));
log.info("Timesheets have already been fetched. Try again tomorrow.");
}
}

With all those nested “if” statements (I counted three levels deep), your cyclomatic complexity bone should be throbbing. When I went to figure out how I could write automated unit tests to assert the correct behaviour, I knew there had to be a better way. I wrote those tests anyway so I could assert my next idea would work. Let’s see how to make your code more readable with functional programming.

Functional Programming

Here is the same code using a Java Stream to feed the fetched timesheets into a series of filters, maps, and a forEach terminator. These are the java.util.Stream equivalents of Predicates, Functions, and Consumers.

public void run2() {
if (verbose) {
log.info("Begin fetching timesheets.");
}
if (dryRun) {
log.info("This is a dry run.");
}
boolean wetRun = !dryRun;
tsheetsPort.checkAvailability();
qbdApiPort.checkAvailability();
LocalDate today = LocalDate.now();
LocalDate yesterday = today.minusDays(1);
Stream.of(LocalDate.parse(configPort.getTimesheetLastFetchedDate(), ISO_DATE))
.filter(timesheetLastFetchedDate -> {
if (timesheetLastFetchedDate.isBefore(yesterday)) {
return true;
} else {
log.debug("Today is {} and timesheet last fetched date is {}", today.format(ISO_DATE),
timesheetLastFetchedDate.format(ISO_DATE));
log.info("Timesheets have already been fetched. Try again tomorrow.");
return false;
}
})
.map(timesheetLastFetchedDate -> {
LocalDate fromDate = timesheetLastFetchedDate.plusDays(1);
return xformToJsonMap(tsheetsPort.retrieveTimesheets(null, fromDate.format(ISO_DATE),
yesterday.format(ISO_DATE)));
})
.map(jsonMap -> {
if (dryRun) {
return jsonMap;
}
configPort.updateTimesheetLastFetchedDate(yesterday);
return jsonMap;
})
.filter(this::hasTimesheets)
.map(this::xformToQbdTimesheetEntries)
.forEach(qbdTimesheetEntries -> {
if (dryRun) {
return;
}
qbdApiPort.enterTimesheets(qbdTimesheetEntries);
});
}

Notice how much flatter this code is. It’s much easier to figure out what I’m doing. This is a rather simple use case; imagine if it was a more complex computation? All the unit tests passed – hurray for Test-Driven Development!

You can see this code on GitHub. Look for it as part of commit f715dff since it will have changed by the time you read this.

To be clear, this is not pure functional programming in the truest sense of the term. It does rely on some external helper methods. In fact, Java is a general-purpose programming language, meaning it is an imperative as well as a declarative language. Languages such as Haskell, Scala and Clojure are examples of pure functional languages, among others.

Final Thoughts

If you’ve been undecided s to when to take the plunge into functional programming, or are not sure when it makes sense, give it a try. Begin with simple use cases like Java Streams. Read up on the fundamentals of functional programming. Eventually you’ll discover how useful it can be in certain contexts. You can make your code more readable with functional programming

Share this: