POSTED ON 09 JUL 2020
READING TIME: 15 MINUTES
The functional paradigm in programming is over 62 years old, first presented in the Lisp language in 1958 1. In the context of the Java language, it began to be widely spoken relatively recently from the perspective of these years because from version 1.8 or 6 years ago. So what is functional programming, can we program functionally in Java, can we reconcile this paradigm with OOP and how can Vavr library help us in all this?
Before we get into the library itself, let's briefly recall what functional programming is and what its assumptions are.
The most important assumption of functional programming (FP) is the lack of the so-called side effects where there are many approaches, solutions or language features that bring us closer to this goal, and they include:
Explanation what each of these mean is beyond the scope of this article, but for those interested in learning more, I’ve listed some good reading material in the references.
As for the FP pillar, lack of side effects, it is a situation where a given function call does not cause a side effect in the system and retains the so-called referential transparency. This means, no less, no more that we should be able to replace the call of our function for given input parameters with its value. In other words, the function should be deterministic for the arguments given to it and should always return the same result without causing any change in the state of the system except the function scope.
However this does not mean that we must completely abandon mutation operations, after all, it would be impossible to write a system based on business requirements without a database, cache or even http requests. The main thing is that actions that change some state should be extracted as far as possible, should be explicitly marked and ideally be performed at one point in the codebase, ideally in the main class or some dedicated module.
A common complaint against Java is its simple syntax and the lack of so-called syntactic sugar. The amount of code needed to write a given functionality, compared to the languages that put the functional paradigm in the first place in their foundations2, is definitely larger. It is also true that these languages have a higher entry threshold than Java, it is definitely easier to find a Java programmer than Clojure or Scala, which often has an impact on the main language in which the project will be written. In addition, Java is a huge percentage of the current market, many projects have been written or are in the process of writing in Java, and it would often be impossible to rewrite it to a new language with better language constructions to enjoy the benefits of using the FP paradigm. Simply put - sometimes we have no way of changing the language, and yet we would like to try working with a new paradigm. That is why, starting from Java version 8 and thanks to constructions implemented in the Vavr library3, this desire becomes feasible.
Vavr, formerly Java Slang, is a library made available under Apache 2.0 license, which provides us with tools that facilitate functional programming using Java. It is worth noting that Vavr does not contain any dependencies on external libraries, and is based solely on the API offered by Java. Thanks to lambda expressions4, functional interfaces5, var keyword6 along with the use of components from the Vavr library, functional programming in Java becomes possible. The library itself consists of logically separated modules such as: gwt, match, test, jackson, gson, render, which have been implemented around the main core module and we will look at the selected elements of this module.
To better understand the concepts implemented in the Vavr library we will need another definition, the definition of Monad. This sinister-sounding name, which has its roots in Category Theory7, for the purposes of this article and in simplification, it can be broken down to simple to understand description - a Monad is a container for data, which allows us to operate declaratively on the data, using operations that are common to other Monads. There is a high probability that if you have not come across this term before, you have unwittingly used the Monads themselves, e.g. using the Monad introduced in JDK 1.8 - Optional8 class. In the context of the description presented earlier, the Optional class is a container for data or lack thereof, and provides a set of API enabling declarative operations on this data, including among others operations:
map - enabling a function to be performed on a given data flatmap - enabling a function to be performed on data that is already a monad, in other words, flattening data filter - filtering-out data that is not of interest for us stream9 - converting from Optional Monad to Stream10 Monad
It is worth noting that, as already mentioned, the counterparts of these operations can also be found in other Monads, so when we have to work with the Monad supplied from a given library, we should be able to call functions on data, flatten data, filter data or convert to another Monad. These are, of course, only some of the operations available on Monads, and the available API strongly depends on the library provider or own creative invention of the author.
Getting to the point, the Vavr library provides us with 5 basic monads, and these are:
Either represents the result in which we can return one of two values - left or right. Used when the call to our method can have two waveforms, for example in a situation where the result of a given operation may fail - assigning it the value left, and in the case of success the value right.
Monad used in a situation where we explicitly want to express the intention in which calling our method may return the result or lack of result. The main advantage as opposed to using the @Nullable annotation11 or Javadoc describing a possible null result is that we described the situation using an object that is part of the method signature, and thus handling the situation where the result would be not present is required and checked by the compiler. The most important difference between Option and Optional is that the mapping function that returns null will throw an exception. In addition, this Monad provides a richer set of APIs, including the fold method, which allows us to perform a value mapping operation in the absence or existence of it. Examples of using the Option:
public interface BookModuleOperations {
Option<BookDetails> findBookDetails(ISBN isbn);
}
In the presented implementation, the Book module provides the operation of finding information about a book based on ISBN using the findBookDetails method, which uses the Option Monad in its signature. The API user is obliged to handle the situation in which the data about the book is not available through the type contained in the signature, otherwise he will receive a compilation error.
An example implementation of operations based on the Option API along with Either API looks following:
final class BookService implements BookModuleOperations {
private final BookStoreClient bookStoreClient;
BookService(final BookStoreClient bookStoreClient) {
this.bookStoreClient = bookStoreClient;
}
@Override
public Option<BookDetails> findBookDetails(final ISBN isbn) {
return Option.of(isbn)
.flatMap(bookNumber -> Option.of(bookNumber.value))
.map(bookStoreClient::callForBookData)
.fold(Option::none, this::handleResult);
}
private Option<BookDetails> handleResult(final Either<BookStoreRestException, BookData> callResult) {
return callResult.toOption()
.map(BookData::toDto);
}
}
Where: Lines 11-12 handle potential null values from the ISBN object passed from the user of our API, to further on line 13 perform http request which will return the result wrapped in Either Monad. The whole is handled in line 14, where in the absence of values from previous calls we return an empty result, and in the case of having data and invoking the request, we handle the result by mapping Either Monad to the Option Monad and calling the toDto method from the BookData object.
Monad Try is similar to Option Monad, except that in contrast to missing values it stores an exception that may have been thrown when calling the function. It is worth considering using this monad instead of using checked-exception or instead of throwing runtime exceptions and catching them with the help of framework mechanisms. Instead, we can treat our exceptions as standard Objects and handle errors on a regular basis or postpone their handling to a selected place. Examples of using Try Monad with the Either Monad:
final class BookStoreClient {
private static final Logger logger = LoggerFactory.getLogger(BookStoreClient.class);
private final ThirdPartyBookStoreLibrary thirdPartyBookStoreLibrary;
private final Function<Throwable, BookStoreRestException> exceptionMapper;
BookStoreClient(final ThirdPartyBookStoreLibrary thirdPartyBookStoreLibrary,
final Function<Throwable, BookStoreRestException> exceptionMapper) {
this.thirdPartyBookStoreLibrary = thirdPartyBookStoreLibrary;
this.exceptionMapper = exceptionMapper;
}
public Either<BookStoreRestException, BookData> callForBookData(final String isbn) {
return Try.of(() -> thirdPartyBookStoreLibrary.getBookData(isbn))
.onFailure(throwable -> logger.warn("Failed to get book data for isbn {}", isbn, throwable))
.toEither()
.mapLeft(exceptionMapper);
}
}
Where: Line 16 starts using Try by wrapping the getBookData method call from the ThirdPartyBookStoreLibrary class with the checked-exception signature. Thanks to the Vavr library, we can move on and give up the imperative style - the definition of the try catch block, to the declarative style, while handling an exception. On line 17, in the case of an exception thrown by the getBookData method, we log the situation, then on line 18 we convert to Either Monad by calling mapLeft in line 19, in which we translate the exception from the library into a comprehensible one inside our system.
In addition, on line 19 the property of functional interfaces was used - use of a qualifier, so we can skip the name of the called method.
Monad Lazy represents the value obtained not at the stage of initialization, but at the moment of referring to the value itself, in this context we refer to this as lazy initialization. Its operation can be compared to the known from Java 8 interface Supplier
final class Library {
private final Lazy<AvailableBooks> availableBooks;
Library(final Lazy<AvailableBooks> availableBooks) {
this.availableBooks = availableBooks;
}
public AvailableBooks getAvailableBooks() {
return availableBooks.get();
}
}
Where: On line 5 we passed a dependency returning a list of available books, the operation of fetching available books was defined outside the Library class and is expensive because it requires invoking multiple HTTP requests. By using Lazy Monad, we postpone the invocation of expensive calls until the first invocation of the getAvailableBooks method, and this result is saved thanks to the previously mentioned memoization functionality.
Last of the Monads provided in the Vavr library - Future Monad represents value, the result of which will be made available somewhere in time. Operations on this monad are non-blocking. In its behaviour, it resembles the Future / CompletableFuture classes known from JDK. An example of using Future Monad:
final class AvailableBooks {
private final BookService bookService;
private final Executor executor;
AvailableBooks(final BookService bookService, final Executor executor) {
this.bookService = bookService;
this.executor = executor;
}
Future<AvailableBooksResult> listAvailableBooks(final Set<ISBN> bookNumbers) {
return bookNumbers.toStream()
.map(this::findBookDetails)
.collect(collectingAndThen(toList(), Future::sequence))
.map(this::extractAvailableBooks)
.map(AvailableBooksResult::new);
}
private Seq<BookDetails> extractAvailableBooks(final Seq<Option<BookDetails>> foundBooks) {
return foundBooks.filter(Option::isDefined)
.map(Option::get);
}
private Future<Option<BookDetails>> findBookDetails(final ISBN isbn) {
return Future.of(executor, () -> bookService.findBookDetails(isbn));
}
}
Where: Line 13 transforms the passed Set collection to Stream Monad Line 14 calls the findBookDetails method that wraps the call of the BookService method in the Future.of method call with the use of the injected Executor dependency Line 15 collects the result, transforms it to a list and then the result is passed to the sequence method from the Future class Last Lines 16 and 17 are responsible for collecting the result and creating a new AvailableBooksResult object and returning it in the Future monad. In this way we have achieved functionality in which we can, without blocking, download information about available books.
When it comes to the modification of state mentioned many times, you might have the irresistible impression that we have a problem in the Java world. This problem is the lack of the immutable collections. They all modify the internal state, and when it comes to unmodifiable view methods from the Collections class, they only make our collection become read only, which in many cases may not be enough. In the case that we would like our collection to be fully immutable, Vavr library provides us with a solution in the form of newly defined "Functional Data Structures", which includes Seq, Set and Map interfaces, all implementing a common interface - Iterable. The review of implementations available in the Vavr library goes far beyond this article, after all, we have available at the time of writing this article 15 implementations, but I strongly encourage you to read their documentation about it.
Another element provided in the Vavr library, and often a part of many functional languages12 is Pattern Matching. Simply put, Pattern Matching allows you to declaratively define control based on a given condition so-called "Match predicate". An example of using Pattern Matching from the Vavr library looks like this:
Number addNumber(Number value) {
return Match(value).of(
Case($(instanceOf(BigDecimal.class)), bigDecimal -> bigDecimal.add(BigDecimal.ONE)),
Case($(instanceOf(BigInteger.class)), bigInteger -> bigInteger.add(BigInteger.ONE)),
Case($(instanceOf(Integer.class)), i -> i + 1),
Case($(instanceOf(Float.class)), f -> f + 1.0f));
}
Where: Line 2 Starts Pattern Matching consisting of predicates from lines 2 to 5 and when: Instance of Number object is BigDecimal type, it calls and returns the result of add method from BigDecimal Instance of Number object is of the BigInteger type, it calls and returns the result of the add method from the BigInteger class. Instance of the Number object is of the Integer type, sums the value and returns the result. Instance of the Number object is of the Float type, sums the value and returns the result.
In addition to the predicates provided by the Vavr library, it is possible to define your own predicates and use them with the mechanism presented above by utilizing the vavr-match module.
It is worth adding that Type Pattern Matching is slowly being implemented in Java. Switch expressions have been added the JDK 14 13 which makes it possible to use the switch statement also as expressions moreover in the JDK 14 version, authors introduced Pattern Matching for the instance of instruction in the preview version, which has been further transferred to the JDK 15 version in the second preview version 14.
Despite being 62 years old, functional programming is doing very well and has not been forgotten but is a paradigm that is used on a daily basis. Languages are more and more boldly adapting the functional paradigm, and programmers are noticing the benefits behind its use. The world of Java and OOP does not necessarily exclude the use of FP in existing or new projects, Java syntax has been improved over the years, which is why together with the use of libraries supporting functional programming and concepts implemented there, nowadays we can be tempted to introduce a new paradigm to our codebase. This will allow us to better abstract side effects, help us create code resistant to multithreading problems, code that is more predictable and thus better maintained over time. In the article I have included basic concepts regarding FP and briefly presented the use of selected elements of the Vavr library, which are only a brief introduction to the whole range of solutions and approaches that can be implemented. If you first came across the approaches described in the article I strongly encourage you to explore the subject further because this knowledge will definitely pay off in the future and can give a different point of view on certain problems.