Functional Programming with Java: It Can Be Done
Java is an object oriented programming (OOP) language, which is fundamentally different from functional programming (FP). So if you want to apply more functional programming with Java coding, you are going to run into obstacles. In this article, Java Developer Hilco Wijbenga describes one way of addressing those issues for your development team.
There is no universally accepted definition of FP. Still, a big part of it is immutability, pure functions (more on that in a bit), and limiting side effects.
And identifying patterns in code. Not so much “Gang of Four” patterns like Singleton and Decorator but more low-level patterns (think nested if statements, for example). FP also cares about composing things. You will see this is a recurring theme in this article.
When compared to OOP, the difference is in the approach. OOP uses smart data (objects) and relies heavily on mutation. FP uses stupid data (immutable values) and relies heavily on functions.
Mutation is a source of bugs and makes multithreading difficult. FP limits where mutation happens; FP prefers immutability but allows mutability where it is truly essential (which turns out to be surprisingly rare).
Using immutable data and side-effect free functions allows you to make guarantees about your code. While this is also possible in OOP programs, it is often harder. If anything can mutate your data, you can provide fewer guarantees about it. If your functions can have side effects, you can provide fewer guarantees about them.
Pure Functions
A pure function is very similar to a mathematical function: its output depends solely on its input. In other words, there are no side effects. There are plenty of examples in Java too, for example String::valueOf.
PRO TIP: No side effects? You could argue that every function has side effects: the CPU does work, heat is generated, memory is used, and many other effects. Those are not the side effects we are talking about. It’s not about physics but about changes in the environment that the program runs in: logging, database access, changes to global variables, network communication, et cetera.
This simplifies testing. It means your functions can be smaller as they are focussed on a single goal.
I will use Haskell as the language to show the FP concepts as it is elegant and concise. I will then translate those concepts to functional programming with Java.
Let us start with a simple function ‘f’ that takes a single input of type ‘‘x’‘ (in Java this would be the generic type parameter ‘X’) and produces a single output of type ‘r’ (in Java this would be the generic type parameter ‘R’).
PRO TIP: The code below does not have an implementation, it is just a function signature.
f :: x -> r
PRO TIP: In Java, this would match the signature of <X, R> R f(final X x) { … }.
We want to be able to compose different functions to create more powerful functions.
f :: x -> y g :: y -> z h :: x -> z h x_value = g (f x_value)
PRO TIP: The first three lines are again merely function signatures (for functions ‘f’, ‘g’, and ‘h’). The last line shows the implementation of function ‘h’. It takes a value (‘x_value’) of type ‘x’ and first applies function ‘f’ (which takes a value of type ‘x’ to yield a value of type ‘y’) to ‘x_value’ and then function ‘g’ (which takes a value of type ‘y’ to yield a value of type ‘z’) to its result, giving an end result of type ‘z’.
Function application in Haskell does not require parentheses so ‘f x_value’ is the same as ‘f(x_value)’ in Java. We do need the parentheses for ‘g’ because ‘g’ does not take 2 arguments but only 1: the result of applying ‘f’ to ‘x_value’.
It is very useful and common to compose functions so we have identified our first pattern. In Haskell, we can refer to a generic function from ‘x’ to ‘y’ (like ‘f’ above) as ‘(x -> y)’.
compose :: (y -> z) -> (x -> y) -> x -> z
The ‘compose’ function takes 3 parameters: a function from ‘y’ to ‘z’, a function from ‘x’ to ‘y’, and a value of type ‘x’. Given those 3 things, it will produce a value of type ‘z’.
:: (y -> z) -> (x -> y) -> x -> z compose g f x_value = g (f x_value)
This is exactly the same implementation as ‘h’ above. So we can now rewrite ‘h’ using ‘compose’.
f :: x -> y g :: y -> z h :: x -> z h x_value = compose g f x_value
You may remember ‘.’ from your algebra classes. Let us rename ‘compose’ to ‘.’.
(.) :: (y -> z) -> (x -> y) -> x -> z (.) g f x_value = g (f x_value)
PRO TIP: The parentheses around ‘.’ are required in Haskell but they are purely syntactical and have no additional meaning.
We can now make the implementation of ‘h’ a bit nicer.
f :: x -> y g :: y -> z h :: x -> z h x_value = (g . f) x_value
In fact, the explicit x_value’ is unnecessary. Applying function ‘h’ is the same as first applying ‘f’ and then applying ‘g’.
f :: x -> y g :: y -> z h :: x -> z h = g . f
So far, so good. This is easy in Java too.
final Function<X, Y> f = ...; final Function<Y, Z> g = ...; final Function<X, Z> h = g.compose(f);
(Where ‘Function’ is ‘java.util.function.Function’ and ‘X’, ‘Y’, and ‘Z’ have been defined previously.)
Java is a bit more verbose but it is certainly not bad.
A Step Further
FP is all about generalizing patterns. Some very common patterns for computations are: the possibility of no result, the possibility of failure, and the possibility of an indeterminate number of results.
A common source of problems and bugs in Java is ‘null’. ‘null’ has no type but satisfies all type requirements. Java treats ‘null’ as a legitimate value but it really represents the lack of a value. The presence of ‘null’ in Java makes it harder to provide guarantees about code.
String doSomething(final String s) { ... }
Function ‘doSomething’ requires a ‘String’ according to its signature but because ‘null’` satisfies all type requirements, the compiler will not complain if you call `doSomething` with ‘null’. Is this okay? It is not clear from the signature. Nor is there a way to make it clear in the signature whether ‘null’ is a legitimate argument.
The same goes for the return value. Will ‘doSomething’ ever return ‘null’? It is impossible to say just by looking at the signature.
There are some potential solutions like annotations and asserts. Unfortunately, there are no annotations that cause the compiler to reject calling ‘doSomething’ with ‘null’ or prevent it from returning ‘null’. And asserts only work at runtime.
So while we cannot make the problem go away completely, we may be able to improve the situation.
Let us define a data type that represents the potential lack of a value. You probably already know it as ‘java.util.Optional’. Note that its implementation still uses ‘null’. We want to be more principled and do better.
In haskell, the ‘Maybe’ type can be used to indicate the lack of a value.
data Maybe x = Just x | Nothing
PRO TIP: Note that Haskell has no concept of ‘null’.
So something of type ‘Maybe’ is _either_ a ‘Just’ (guaranteed with a value) or ‘Nothing’ (guaranteed without a value).
We can define a similar ‘Maybe’ type in Java, without using ‘null’.
public abstract class Maybe<X> { public static final <T> Maybe<T> just(final T value) { Objects.requireNonNull(value, "Missing 'value'."); return new Just<>(value); } public static final <T> Maybe<T> nothing() { @SuppressWarnings("unchecked") final Nothing<T> nothing = (Nothing<T>) NOTHING; return nothing; } // Prevent any further inheritance. private Maybe() { } private static final Nothing<?> NOTHING = new Nothing<>(); public static final class Nothing<X> extends Maybe<X> { private Nothing() { // Empty. } } public static final class Just<X> extends Maybe<X> { public final X value; private Just(final X value) { this.value = value; } } }
Nowhere near as elegant and concise as Haskell but it gets the job done. And without ‘null’. Annoyingly, we do have to check explicitly for ‘null’ arguments because there is no way in Java to opt out of ‘null’s.
You might argue that this does not help us much. Sure, we can now indicate in the signature of a function whether or not we handle the lack of a value but we still need to check.
String doSomething(final Maybe<String> maybeString) { if (maybeString instanceof Maybe.Just) { ... } else { ... } }
Like many FP languages, Haskell supports pattern matching.
f :: Maybe Text -> Text f maybeText = case maybeText of Just text -> text Nothing -> "nothing"
PRO TIP: If ‘maybeText’ is a ‘Just’ then the first clause matches and the text stored in the ‘Just’ (namely ‘text’) is returned. Otherwise, the second clause matches and a hard coded ‘”nothing”‘ is returned.
We can implement this in Java as a first step to making ‘Maybe’ more convenient to use.
public abstract class Maybe<X> { ... elided ... public abstract <Y> Y match(PatternJust<X, Y> just, PatternNothing<X, Y> nothing); ... elided ... public static final class Nothing<X> extends Maybe<X> { ... elided ... public <R> R match( final PatternJust<X, R> patternJust, final PatternNothing<X, R> patternNothing ) { Objects.requireNonNull(patternJust, "Missing 'patternJust'."); Objects.requireNonNull(patternNothing, "Missing 'patternNothing'."); final R result = patternNothing.match(); Objects.requireNonNull(result, "Result is NULL."); return result; } } public static final class Just<X> extends Maybe<X> { ... elided ... public <R> R match( final PatternJust<X, R> patternJust, final PatternNothing<X, R> patternNothing ) { Objects.requireNonNull(justPattern, "Missing 'justPattern'."); Objects.requireNonNull(nothingPattern, "Missing 'nothingPattern'."); final R result = patternJust.match(value); Objects.requireNonNull(result, "Result is NULL."); return result; } } public interface PatternJust<X, R> { R match(X value); } public interface PatternNothing<X, R> { R match(); } }
We can use this in ‘doSomething’.
String doSomething(final Maybe<String> maybeString) { return maybeString.match( s -> s, () -> "nothing" ); }
This is an improvement but it is common to want to use the value stored in a ‘Maybe, if there is one. We want to be able to map over a ‘Maybe.
public abstract class Maybe<X> { ... elided ... public abstract <Y> Maybe<Y> map(Function<X, Y> function); ... elided ... public static final class Nothing<X> extends Maybe<X> { ... elided ... public Maybe<Y> map(final Function<X, Y> function) { Objects.requireNonNull(function, "Missing 'function'."); @SuppressWarnings("unchecked") final Maybe<Y> result = (Maybe<Y>) this; return result; } } public static final class Just<X> extends Maybe<X> { ... elided ... public Maybe<Y> map(final Function<X, Y> function) { Objects.requireNonNull(function, "Missing 'function'."); return just(function.apply(value); } } }
We have just turned our ‘Maybe’ into what is known as a Functor, which you can learn more about at Wikipedia and Haskell. You can read “functor” as “mappable.” Essentially, something is a Functor if you can apply a function to its content. A good example is a list: you can apply a function to each of its elements in turn and thus generate a new list.
Now we can execute different functions (mappings) on a ‘Maybe’ without having to worry about ‘null’.
public Maybe<String> doSomething(final Maybe<String> value) { Objects.requireNonNull(value, "Missing 'value'."); return value .map(s -> s.toLowerCase()) .map(s -> s + s); }
It is important to note that calling ‘map’ never changes the structure of ‘Maybe’, i.e. a ‘Nothing’ stays a ‘Nothing’, and a ‘Just’ stays a ‘Just’.
PRO TIP: This isn’t true in Java. If you look at ‘java.util.Optional#map’, you’ll see that if the mapping returns ‘null’ then the ‘Optional’ changes from non-empty (i.e. a ‘Just’) to empty (i.e. a ‘Nothing’). While this is arguably more practical (at least in a language which suffers from the presence of ‘null’), it is clearly less principled.
We Want More
Let us envision a scenario where we have multiple methods that may not return a result. A ‘GroupId’ may not be associated with a ‘Group’, a ‘Group’ may not have a leader, a ‘Person’ may not have an ‘Address’, and an ‘Address’ may not have a ‘City’.
public class SomeClass { public static final Maybe<Group> getGroup(final GroupId groupId) { ... } public static final Maybe<Person> getLeader(final Group group) { ... } public static final Maybe<Address> getAddress(final Person person) { ... } public static final Maybe<City> getCity(final Address address) { ... } public static final String getCityName(final City city) { ... } }
We want to implement a method that returns the leader’s city given a ‘GroupID’.
public Maybe<String> getLeaderCityByGroupId(final GroupId groupId) { Objects.requireNonNull(groupId, "Missing 'groupId'."); return SomeClass.getGroup(groupId).match( group -> SomeClass.getLeader(group).match( leader -> SomeClass.getAddress(leader).match( address -> SomeClass.getCity(address), () -> Maybe.nothing() ), () -> Maybe.nothing() ), () -> Maybe.nothing() ).map(SomeClass::getCityName); }
That works but we can easily identify a pattern here so surely there is a better way? We can turn ‘Maybe’ into what is known as a Monad Learn more from Wikipedia and Haskell. Just like before, we will add another method to ‘Maybe’. We will use ‘bind’ but another common name is ‘flatMap’ (as it is clearly similar to ‘map’).
PRO TIP: In Java’s ‘java.util.Optional’ this is indeed called ‘flatMap’.
The ‘bind’ method allows us to chain computations. In the case of ‘Maybe’, each computation may fail to return a result. Unlike a Functor (and ‘map’), a Monad can change the structure of the data. In the case of ‘Maybe’, a ‘Just’ can return a ‘Nothing’ when using ‘bind’. (And ‘Nothing’ may transform into a ‘Just’ but that is uncommon.)
public abstract class Maybe<X> { ... elided ... public abstract <Y> Maybe<Y> bind(Function<X, Maybe<Y>> function); ... elided ... public static final class Nothing<X> extends Maybe<X> { ... elided ... public Maybe<Y> bind(final Function<X, Maybe<Y>> function) { Objects.requireNonNull(function, "Missing 'function'."); @SuppressWarnings("unchecked") final Maybe<Y> result = (Maybe<Y>) this; return result; } } public static final class Just<X> extends Maybe<X> { ... elided ... public Maybe<Y> bind(final Function<X, Maybe<Y>> function) { Objects.requireNonNull(function, "Missing 'function'."); final Maybe<Y> result = function.apply(value); Objects.requireNonNull(result, "Missing 'result'."); return result; } } }
Using our newfound powers, we can re-implement getLeaderCityByGroupId’ without all the repetition.
public Maybe<String> getLeaderCityByGroupId(final GroupId groupId) { Objects.requireNonNull(groupId, "Missing 'groupId'."); return Maybe.just(groupId) .bind(SomeClass::getGroup) .bind(SomeClass::getLeader) .bind(SomeClass::getAddress) .bind(SomeClass::getCity) .map(SomeClass::getCityName); }
Summary
As you can see, using just a couple of FP principles we can make our code more concise, easier to read/understand, and harder to break. You can also see that the plumbing needed to implement these principles in Java is not insignificant. Java really is a verbose language.
I hope you have gained some appreciation for FP principles. If you would like to know more, I recommend looking into FP languages like Elm, Haskell (and Eta (“Haskell for the JVM”)), Purescript, and OCaml. Languages like Kotlin (with Arrow), Scala (especially Scala 3, and especially with ScalaZ and ZIO), and Rust also have very nice FP features.
Resources
- https://en.wikipedia.org/wiki/Side_effect_(computer_science)
- https://en.wikipedia.org/wiki/Functor
- https://wiki.haskell.org/Functor
- https://en.wikipedia.org/wiki/Monad_(functional_programming)
- https://wiki.haskell.org/Monad
- https://elm-lang.org/
- https://www.haskell.org/
- https://eta-lang.org/
- http://www.purescript.org/
- http://ocaml.org/
- https://kotlinlang.org/
- https://arrow-kt.io/
- https://www.scala-lang.org/
- https://zio.dev/
- https://dotty.epfl.ch/
- https://www.rust-lang.org/
I hope this little tutorial was helpful. If you have something to add about functional programming with Java, please give us a shout on social media. You can also get your hands dirty by trying xMatters.