With Java 8 and the introduction of lambda expressions and method handles, the Java programming language got the essential feature to implement algorithms in a functional way.
At the LV 1871 coding dojo, we already coded different implementations of Conway’s game of life using various approaches.
To train lambda expressions and try out the possibilities of Java 8 the exercise should be solved in a functional way.
If we remember the base algorithm we find that the game of life can easily be represented by a list of fields. A game iteration is a transformation algorithm transforming the state of each field. To use Java 8 Streams API to implement this transformation seems to be an obvious thought.
Finally, the gameboard should be represented as a list of immutable field objects containing the coordinates and the life or dead flag.
During past sessions general readability of lambda expressions was a point of interest and the common consense was not to use them too excessive.
One implementation of the game of life ended up with a totally unreadable chain of arrow- and inline-functions. To write the functionality this way and refactor it was not as easy as refactoring conventional code.
Using the streams API with inline delegates is not the right way to keep a program readable and to prevent the duplication of essential code. The Java 8 interfaces that can be found in the java.util.function package can be used with ease to define functional elements as reusable types.
When keeping all the functions small and pure, a toolset created this way can be reused in different scenarios without building complicated and obscure inheritance structures. By delegating domain-specific algorithms into structural code defined as higher-order functions we can even avoid duplicating essential structural functionality.
The first-class goal of functional programming is the abstraction of structural code to reusable units without constructing complicated inheritance structures.
This example visualizes that lambda expressions can be used as types. They can be used as parameters, return values or variable values. We can reuse them, parameterize them and work with them in nearly every manner. After all, the usage of Java 8 method handles (Type::method) even eases the utilization of functional structures in java programs.
In this way, we are able to abstract structural and recurrent code. A set of tested and well named pure functions can even provide a technical or domain-specific toolset.
It just becomes easy to create some kind of custom DSL with the use of declared expressions.
But how to find out on how to solve a complex algorithm in this way?
In the case of the game of life, we decided to write down the functionality in some kind of humanly readable DSL and implement it later. The result of this approach looks promising.
The main goal, to write expressive readable code, is fulfilled in that way.
It became reality, to express the rules in a human-readable way through functional code.
For sure it is necessary to practice the use of expressions and streams API to write algorithms, which really can be implemented, in that style. But this article will show, how to think to find such a solution.
Let’s start at the beginning. The method toDeadField, should transform a living field to a dead field if some condition is fulfilled. A short version of the rule is: “A field dies if …”.
This means we need a method returning a function which maps a field to a dead field if some kind of condition evaluates in the correct way.
The method finally takes a predicate representing the condition as a parameter. Within the function, the method returns, we now can apply this predicate to test if we have to kill the field. The function itself gets a field as a parameter and returns the original field or a new dead field depending on the result of the given condition.
By the (not obvious) use of the Java 8 optional and it’s filter-method we can even optimize this function in a functional manner. After creation of an optional, we can filter those values not fulfilling the condition and return a new dead field if the condition is fulfilled.
So let’s take a look inside. ToDeadField requires us to provide a condition deciding whether to kill a field or not. This means which has to return a predicate.
If we read the parameters, we can translate them into their functional representations. The parameters have to be:
- A predicate (isAlive)
- A function combining two predicates to one predicate (and)
- A predicate (which; hasLessThanThree; etc)
Finally, we have to implement a Method returning a predicate and taking two predicates and a BiFunction as its parameters.
The algorithm implemented within “which” is really easy. (lambda experts know that this is just implemented to train. With the “and” method of predicates the implementation of a “which” can be unnecessary)
The first predicate should provide a condition expressing if the given field is alive. The condition can be expressed in many ways. Even many readable ways. In real-world code, we would just use the method handle of the getter. In this case, the usage of the method handle was abstracted to express the condition in a more human-readable way.
A predicate is a function which returns a boolean. The getter of the alive flag returns a boolean, so the getter itself fulfils every requirement to be a predicate.
The function providing a predicate testing if a field is dead, is just the negation of the function testing if a field is alive, after all.
Finally, let’s take a look at the BiFunction combining two predicates. A BiFunction expects two parameters and returns one. The function itself can do whatever required. The given case requires us to combine two predicates into one predicate. In the case of an and, we can just use the predicate’s own “and” method to combine the two predicates.
The rest of the code is just a repetition of the principles learned above. The “which” returns a predicate which can be used as a parameter of a “which”.
With the use of higher order functions expecting functions as parameters and returning functions, we can implement generic DSL units, composable to algorithms in many ways.
Feel free to have a look at the full code repository at GitHub: