In a world where paradigms and technology stacks change all the time, the struggle to remain competitive and improve both productivity and quality can sometimes prove to be quite the challenge.
In this article, I want to showcase first of all the advantages of going towards functional programming (FP) in general and spicing up your Java coding experience in particular. I will try to iterate a couple of the reasons I find most important when attempting a paradigm shift towards functional programming. Keep in mind that this is by no means a huge innovation, I believe FP has been around since the 70’s but only in the recent years has it gained traction and increased interest. Let’s see why!
Concurrency
With the advent of multi-core/multi-threaded processors, functional programming has started gaining some more attention . This is by no means a simple coincidence, as functional programming encourages the use of immutable objects, properties and variables (that is, data containers whose values cannot be changed). What does that have to do with it? Well imagine the following block:
private int aNumber;
public void setNumber(int numberParameter) {this.aNumber = numberParameter;}
Simple enough, right? You’ve probably seen it a gazillion times before. But what happens if two threads access the setNumber method in the same time? You could imagine that some kind of blocking might happen and, in the end, only the last thread accessing the method will have its final say regarding aNumber’s value. But this is not deterministic. It depends on various factors, therefore, we can say that the method setNumber is not referentially transparent (more on that later). This is a case where immutability helps reasoning about the code, because we know for sure that no matter how many threads access a section of it, its value will always be the same.
Referentially transparent functions and testability
Functional programming encourages the use of referentially transparent functions. What does that mean? Well it means that a function can always be replaced by its value and everything will stay the same. Imagine the following code block:
import java.util.Random;
public class RandomValueProvider {public int getSomeRandomValue() {Random rand = new Random();return rand.nextInt(50);}}
Is the getSomeRandomValue() method referentially transparent? Well try to replace it with its value. Does it always stay the same? Guess not. This is just an example, but random numbers are not easily handled in FP. Trying to use referentially transparent functions as often as possible might be a good habit though. Imagine that it’s much harder to test the getSomeRandomValue method above than testing the following method:
public int getSum(int a, int b) {return a + b;}
Small functions that have suggestive names are usually better than applying the expression that represents their return value wherever the function is used. A good idea might be to also ensure that (at least most of) the functions we write are deterministic. This will increase the ease of reasoning about the code, as well as testability.
Function composition
The fact that operations are now simpler and more deterministic when applying FP principles allows us to create more complex behavior by composing different functions together. Functions that receive other functions as parameters or return functions all together are called higher-order functions. Some examples here come from the Java 8 Stream API. A huge lot has been written on streams ever since they became part of the JDK in 2014 (and even before that). I just want to show a simple example here using the Consumer functional interface. Take the following snippet, simplified for brevity:
public void processListOfNumbers(List<Integer> listOfNumbers, Consumer<Integer> processor) {return listOfNumbers.stream().forEach(number -> processor.accept(number));}
And some client code:
List<Integer> numbers = Arrays.asList(5, 6, 7, 8);Consumer<Integer> numberPrinter = n -> System.out.println(n);
processListOfNumbers(numbers, numberPrinter);
The method processListOfNumbers is an example of function composition and you might hear it be named sometimes a higher-order function. In Java, functions (also suppliers, consumers) are objects. That means we can apply them, combine them and pass them around as parameters.
Apps written in FP style are more robust
It can be argued that when writing code in a functional style, the app itself is less error/bug-prone. That’s because when you have fewer moving parts apps tend to become more predictable, easier to reason about and more resilient towards adversity. What I mean by this is that, general usage of function composition and immutability will ensure that all those bugs that happen because state changes in different parts of the app will go away now by default. The app will be more robust to change and will provide shorter development -> testing -> debugging iteration loops.
Focus on the “what” and not on the “how”
Assuming that we have a getUserById method (in the same class) that takes care of fetching the corresponding User object from the database, take the following classic application of Java streams:
public List<User> getAdultUsers(List<Integer> listOfUserIds) {return listOfUserIds.stream().map(this::getUserById).filter(user -> user.getAge() >= 18).collect(Collectors.toList());}
Now let’s look at the same code in non-functional style:
public List<User> getAdultUsers(List<Integer> listOfUserIds) {List<User> adultUsers = new ArrayList<>();for(int id: listOfUserIds) {User user = getUserById(id);if (user.getAge() >= 18) {adultUsers.add(user);}}return adultUsers;}
Besides the fact that the second snippet is slightly longer, we can also notice that it takes its time to “explain” how every step of this operation is done: create a blank list, iterate over the ids, get each User, add some of the users to the blank list based on a conditional expression, finish up and return the collected users.
On the other hand, in the first snippet, the functional approach focuses more on the “what”. What is the code doing? It’s mapping some ids to some users, filtering them out and the collecting the rest in a list. One could argue that the same thing can be achieved by extracting small methods in the second case, but I believe that the streams and functions-as-data approach of the first snippet is still better. It puts our functions in the forefront of our business logic, having the same status as any other data that moves around in our application.
Better-looking method signatures
When our functions move from an imperative to a functional style, things start to clear out in the naming department as well. At a glance, the following method is difficult to read just by its signature:
public void executeProcess() {// executing some mysterious stuff!}
What does the code do? Why doesn’t it want anything from us and why doesn’t it want to give anything back? Can you test it? Can you read it? It’s not easy, at least. What if it looked like this?
public ExecutionStatus executeProcess(Process processToBeExecuted) {// execute "processToBeExecuted" and return some status}
Just by taking some FP concepts and applying them in this simple case, the code has become a lot more readable. It now speaks for itself just by looking at the signature (though the method name could probably still be improved). It takes a process and responds with some status. Signatures become more meaningful aside from offering better “documentation” directly in the code. What process is executed? We can just look into the Process object and see it at runtime. What happens after it does its mojo? We can now see for ourselves and possibly use the results later on in our flow.
Conclusion
Nowadays, whether we’re working on legacy code or fresh greenfield projects, there are some things that we could use to improve both quality and productivity of our day to day work. Functional programming approaches coding from a different perspective. It usually means more conciseness, but given the proper care, also improves on the readability. It also helps us with some common pains, like race conditions in concurrent programming, nasty object state bugs or difficult to follow code.