The Landscape of Functional Programming in Java (Part I)
Functional programming (FP) has proven itself as a concise, secure, and effective programming paradigm. It has attracted considerable attention in recent years, and many new functional programming languages have emerged, as well as existing languages that have adopted several functional programming concepts.
In 2015, I discovered my liking for functional programming while learning Scala. Despite mainly using Java, I've been eager to find similar features in the Java ecosystem and have tried implementing some myself. While not a full-time functional programmer, I'm still learning and enthusiastic about FP.
In this article, I will investigate what we can do to write more functional code outside of Java’s features. My aim is to explain some functional programming concepts to Java developers and then introduce and explore open-source projects that offer those features.
Be Functional Yourself
Is this article for people who have never started functional programming?
I do not think so. Functional programming begins with a set of concepts that must be grasped before using features of a language or a library; e.g. immutability, first-class and higher-order functions, pure functions, referential transparency, ensuring type-safety, avoiding or encapsulating side-effects. If you are not familiar with all these, I suggest the Wikipedia page which is a good source to have a brief idea about the concepts, Noel Welsh’s very neat introduction, and Alvin Alexander’s more detailed post and post that mentions its benefits. As a utility, Scala’s glossary may help to search foreign concepts and words quickly.
Java 8 And Beyond
Java 8 was announced as the first Java version to enable functional programming. Functions were “first-class citizens” for the first time in Java; meaning that “they could be bound to names (including local identifiers), passed as arguments, and returned from other functions, just as any other data type could” (quote: Wikipedia).
On the other hand, it is still not possible to write fully functional code in Java, and never will be. The principles of functional programming are not enforced in Java, because that would need a shift in almost all Java libraries, which are all creations of an imperative and stateful mindset. Also, most of the Java developers would have to leave their current programming style. It is unreasonable to think these will happen, because Java is a highly popular and industrialized language, plus backward compatibility has been its main focus for years.
Nevertheless, the features of Java 8 have become a gateway to writing functional code and should not be missed; on behalf of FP, what will be done next will also be based on them. I am not going to introduce Java 8 features; there are already tons of resources for it, but in order to proceed, you should know what Consumer
, Supplier
, Predicate
is. You should make use of streams and lambdas and have a knowledge of common lambda operators like map
, flatMap
,filter
. You should favor immutability — using the final keyword and immutable APIs as much as possible. It would be best to eliminate illegal states, like nulls or exceptions, by replacing them with meaningful values. It is essential to know what Java offers first.
What is missing?
I remember the arrival of Java 8. As a developer who had met Scala before it, I was not quite satisfied with it.
First of all, it seemed very verbose. It has no syntactic sugar, which is not just about the “style” of code as it is thought but about increasing the expressiveness of the language. Even if you write functional code, you still get some of Java’s verbosity. The lack of FP features — like type inference, tuples, and case classes/algebraic data types- also makes the verbosity situation worse. You can not define functions anywhere, you have to use @FunctionalInterface
and it is a bit of a vibe-killer.
I am not a fan of stream()
and collect()
calls. Collectors are kind of a mess. I don’t know how many times I performed a Google search to properly use Collectors.toMap()
. I have never experienced this while writing Scala.
In functional programming (and in your Algorithms 101 class), writing recursive functions is encouraged; recursive calls are stateless, more simple, and more elegant than loops. But we make use of them rarely in real life because recursion is expensive. Scala dealt with this issue by utilizing tail recursion optimization. However, Java could not deliver this feature and there is no sign of it will arrive.
Java still does not have a proper immutable collection API. Collections are mutable, and in order to keep things backward compatible, they made the newcomer immutable collections that implement the old collection interfaces. This is absolutely wrong. As a result, an immutable List created by Arrays.asList()
call implements the mutable List
interface, which defines add(...)
method. Note that the add(...)
is not an immutable- append operation, but it is still old mutable add. And what happens when you call it? You get a RuntimeException
. It is dangerous, like a mousetrap for junior developers.
Optional<T>
emerged to represent nullable values and cleared the ugly null checks. But what about more, like containers for representing exceptional values? For that purpose, Scala has Try[T]
, but there is no Try<T>
in Java. It is necessary in an environment where the checked exceptions are still floating around.
I’m stopping the whining here. There’s a lot to say, but it’s hard to say anything original. So, which features could have helped us to write more functional code?
- Tuples
- Immutable collections with a proper immutable API
- More lambda operators: foldLeft, foldRight, zip, unzip, slice, join, …
- Lazy evaluation
- Algebraic data types, sealed classes
- Simple currying, lifting, composition, memoization
- More monadic containers: Try, Either, IO, …
- Pattern matching
Are these enough? It should be noted that the feature set of a functional programming language would overwhelm an OO developer; as for Scala, it has type inference, implicits, higher-kinded-types, mixins, traits, … well, actually a very extensible and a complex type system. But it would be unlikely to obtain these features with an external library. As Java developers, let’s settle with what we can find inside the Java ecosystem now.
Towards a Better Functional Programming
jOOλ
jOOλ or jOOL (L for lambda) is a library developed by the jOOQ team, a popular typesafe SQL-to-Java framework. The goal of jOOL is to complement what is missing in Java 8 regarding functional programming; therefore, it is not a complete FP library but rather a wrapper for Java 8 features.
jOOL provides tuples, and it defines functions with its Function
type. Both Function and Tuple are implemented by classes like FunctionN
, N being the argument count. It goes as far as Function16
so you can define tuples and functions having 16 parameters. You don’t need @FunctionalInterface
anymore!
In addition to these, jOOL has Seq
, an interface that extends Java’s Stream and provides missing operators on it.
A sequential, ordered Stream that adds all sorts of useful methods that work only because it is sequential and ordered(link).
And finally, Unchecked
is the jOOL’s way to hide checked exceptions from the compiler. It allows you to write intact lambdas, without try-catch, by wrapping the call that throws an exception.
jOOL is very lightweight and it fills some missing parts of functional Java with very little overhead. But if you are looking for a more comprehensive framework, the next one might suit you.
Vavr
Vavr (formerly called Javaslang) is a complete library for functional programming. I have been an enthusiast for Vavr in my recent projects, therefore I desired to explain its features in more detail, and show why they matter, especially for FP novices.
Functions & Tuples
For starters, Vavr defines Tuples with a very concise syntax. It even allows you to reach tuple elements with underscore notation, like _1.
Vavr allows you to define functions with 8 parameters at maximum — which is lower than jOOL’s limit of 16 for higher-order functions. Besides, Vavr has function composition, lifting, partial application, currying, and memoization. Starting with composition, you can compose functions with andThen
or compose
methods.
I would like to briefly explain what they are before exemplifying the next features:
- Partial Function: a function defined for specific values only. For example, division operation is not defined when the divider is zero.
- Lifting: Process of turning a partial function into a total function. For undefined values, it returns an Optional, or something similar.
- Partial Application: Converting a function into another function that takes a subset of its original parameters — by fixing the other parameters.
- Currying: Currying is a process that resembles the partial application, but the returned function takes only one parameter in each step. “The technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument.”(quote: Samuel Pouyt).
- Memoization: Caching the result of an expensive function call, when the function is called again, it returns the computed value. You probably remember memoization from the dynamic programming class.
Values
Values are Vavr’s implementation of monadic types. Monads are basically wrappers that apply effects to values or computations and share common behaviors like map
, flatMap
, peek
, orElse
, getOrElse
, andThen
etc. As an example, Java’s Optional
is a monad (or looks like a monad). If you are interested in the theory of monads, I have found the following sources very adequate and understandable: 0, 1, 2, 3, 4.
Option
Option
serves the same purpose as Optional
in Java 8. There is a slight difference, which is explained here. I will not go into detail to leave more space for the others.
Try
Vavr has Try<T>
. Thank God! For those unfamiliar, Try is a type that represents a computation that may outcome in a value or with an exception. Thanks to Try monad, all exception throws can be chained and processed functionally, preventing the program flow from breaking. No more ugly-nested try-catch blocks!
Try has two possible subtypes, Success
and Failure
. If it is a Success, it contains the result value; if it is a Failure, it contains an exception. For Failure scenarios, Try has recover(...)
and recoverWith(Try<T>...)
convenience methods, both can be defined for specific exception types. Try also supports the try-with-resources feature which was introduced in Java 7, if you call the withResources
factory method and provide the AutoCloseable
resource.
Either
Either is a value container for two possible types, Left
or Right
. According to the common convention; if Either is a Left it contains an error, if it is Right it contains a value. That makes Either right-biased; means that functions such as map
and flatMap
only apply to the Right — which contains the result of the successful operation.
Lazy
Lazy<T>
is a value container that represents a computation that will be computed when it is called, i.e. lazily. When it is computed, it always returns the same value anymore, because the value is memoized.
Future
Vavr’s Future<T>
is a wrapper for asynchronous computations and it is much like Scala’s Future. It can also be converted to Java’s CompletableFuture.
Future
is non-blocking by default, therefore it needs an ExecutorService
to execute functions. By default it employs an Executors.newCachedThreadPool()
but a different executor can be provided as an argument when the Future is created.
Future can provide common Value operations, like map
, flatMap
, andThen
as well as Future-specific methods. You can call isComplete()
to check if the Future has been completed, or call await()
to block the current thread until the Future is completed. When a Future is completed, it is represented by a Try
. The resulting Try may be a Success or a Failure, to handle these cases there are onSuccess
and onFailure
callbacks, as well as transform
method which takes the resulting Try itself.
Validation
Validation<T,Y>
is a structure that can be used for any data validation use case. It has two possible subtypes: Valid
and Invalid
, and takes two type parameters for valid and invalid situations, respectively.
Validation is different from other value containers; it does not short-circuit when it first encounters an error. When you flatMap
two Try
s, if one of them is a Failure
you get a Failure as the resulting Try
. However, validation accumulates the errors instead of doing this, which is very useful when you need to validate multiple fields, like a web form. Due to this feature, Validation is defined as an applicative functor instead of a monad.
In order to make use of Validation, you expose your validation logic as Valid
or Invalid
instances. Validation.combine(...)
method gathers these instances and creates a builder. ap(...)
method on the builder triggers the validation and also allows you to define what you are going to do if the operation is valid. In the end, you can convert the Validation to an Either, or call .isPresent() and .get() on it, or call .fold(…) to transform both error and data to a single value.
For-Comprehension
Let’s say, you have multiple Try
s that must be combined. What is the known way of doing that? You call flatMap on them, sequentially, like in the code below:
However, this looks a bit unreadable. How about, if you were able to iterate on Try’s, and accumulate the results, like combine in Validation? Well, there is a solution for that. For-comprehension of Vavr iterates over the Value
s and yields a final result.
The result from the yield is a lazily evaluated Iterator, therefore in order to process the values into their final form, forEach part must be called.
There is more …
In this article, I discussed what we can expect from a functional programming library for starters. Although I focused heavily on Vavr, the main goal was to establish the foundation and assess the libraries that meet our expectations. Other libraries that offer similar features and are worth mentioning include:
Cyclops
Cyclops is a comprehensive functional programming library that enhances Java’s capabilities by adding a wide range of functional programming constructs and utilities. Not only designed to work seamlessly with Java, Cyclops provides integrations to other functional programming libraries — like Vavr and FunctionalJava.
- Functional control structures —Optional, Try, Either, Validation
- Functional helpers. Extensions for Java’s Function, Predicate, and Consumer, also helpers for currying and memoization .
- Lazy Evaluation
- Wide range of immutable data structures
- Reactive collections and streams. They can be integrated to RxJava and Reactor.
- Pattern Matching
- Advanced functional data structures — control, arrow, free monad, type classes, etc. via cyclops-pure module
Atlassian Fugue
I learned about Fugue from Svend Vanderveken’s comment on the post. Fugue provides a subset of functional programming features, making it lighter and easier to integrate into existing Java projects. It offers Option
, Either
and Try
types help manage nullability and errors. Additional helper utilities are also available for Java’s native Function, Supplier, and Iterable operations. Fugue also has Monoid, SemiGroup, and Optics extensions for those interested in more advanced topics. Its maintenance by Atlassian is also an advantage because the lack of maintainers is often a problem in open-source projects.
Functional Java
Functional Java is possibly the oldest functional programming framework in existence. It enabled writing functional code in Java7 and, therefore, accomplished a lot. The library also has a huge set of features:
- Monads and functors: Includes
Option
,Either
,Validation
, andTry
for handling optional values, errors, and side effects. - Immutable collections: Provides immutable
List
,Set
,Map
,Array
,Tree
andTreeMap
structures for safer, side-effect-free programming. - Pattern Matching: Enables more expressive and concise code by simplifying the handling of complex data structures.
- Other functional abstractions: monoid, semigroup, natural, random number generator, reader, writer, state, IO, optics.
- Concurrency support:
Actor
andPromise
implementations.
However, I am unsure about the project’s current status. The website appears to be down, and it is unclear if an active maintainer exists. It is still a sound library in to study and learn the topics, but I suggest not getting involved until the project is back on track.
What’s next
I plan to explain more topics and explore various libraries in the upcoming articles. In addition to what I mentioned here, another topic I find important is functional data structures. The next post will be about this.
Until that, keep it stateless!