Literate Functional Development

⚠️ This article assumes you have basic knowledge of functional development in Java.

You can write functional code that reads so clearly that non-technical people can understand it. I’m going to show you how using functional principles, Java’s syntactic sugar, and higher order functions, you can translate working code into readable code.

All examples will work with Java 8 and above

For our example, we’re going to read as List of Strings and count the occurrences of all words between 3 and 12 characters (exclusive).

    List<String> text; // Assume there's some content here.
    Map<String, Integer> counts = new HashMap<>;
    for (String line: text) {
        String[] words = text.split();
        for (String word: words) {
            word = word.replaceAll(",",""));
            word = word.replaceAll("\\.",""));
            word = word.toLowerCase();
            if (word.length() > 3 && word.length() < 12) {
                currentCount = counts.getOrDefault(word, 0);
                counts.put(word, currentCount + 1);
            }
        }
    }

This is verbose, includes a fair amount of implementation, and obscures the meaning of the code with the extra details.

We can do this functionally like this.

 Map<String, Integer>  wordCounts = text.stream()
                .flatMap(s -> Arrays.stream(s.split())
                .map(w -> w.replaceAll(",", ""))
                .map(w -> w.replaceAll("\\.", "")) // Escaped period 
                .map(s -> s.toLowerCase())
                .filter(s -> s.length() > 3)
                .filter(s -> s.length() < 12)
                .collect(Collectors.toMap(Function.identity(), s -> 1, (a, b) -> a + b));

This is fine, but we can do so much better. It’s not easy to read. While the lambda syntax is concise, it still reads like code. We’re going to move all of the lambda syntax out of the main logic, which will make the code much easier to read.

We’re going to use the following techniques:

  • Method references
  • Methods that return functional elements like functions or predicates.

Method References

First, we’re going to use method references to make

                .map(s -> s.toLowerCase())

Can become this. The compiler handles translating this reference to a function.

                .map(String::toLowerCase)

Higher Order Functions

A higher order function is one that accepts a function as a parameter and/or returns a function. In this case, we’re going to create methods that return functions so that our code reads clearly.

Mapping

Let’s start with this code. It’s all focused around extracting words and clearing out the punctuation.

                .flatMap(s -> Arrays.stream(s.split())
                .map(w -> w.replaceAll(",", ""))
                .map(w -> w.replaceAll("\\.", "")) // Escaped period 

We’ve got a number of functions related to words, so let’s create a class to encapsulate that behavior.

public class Words {

}

To that, we’ll add a method to handle mapping to remove the punctuation. The below accepts a set of Strings to erase and returns a function that accepts a String and returns the a String with those characters erased.

    private static Function<String, String> remove(String... toRemove) {
        return s -> {
            for (String remove : toRemove) {
                s = s.replaceAll(remove, "");
            }
            return s;
        };
    }

Now, we can use that method in our next method. This accepts a single String and splits it into a Stream of words with the punctuation removed.

    public static Function<String, Stream<String>> fromSentence() {
        return s -> Arrays.stream(s.split(" "))
            .map(remove(",", "\\."));
    }

The payoff is this. We have replaced a bunch of unimportant details with a clear description of what we’re doing. I named the class Words and made the method static, so it would read well.

                .flatMap(Words.fromSentence())

Filter

This code isn’t egregious, but I find that the lambda syntax adds just a little bit of friction that requires translation.

                .filter(s -> s.length() > 3)
                .filter(s -> s.length() < 12)

Going back to our Words class, we can add methods that handle figuring out whether a String is of a certain size. This will increase readability and provide some reusability, even if it’s a nominal amount of code.

    public static Predicate<String> longerThan(int size) {
        return s -> s.length() > size;
    }

    public static Predicate<String> shorterThan(int size) {
        return s -> s.length() < size;
    }

The payoff is code that reads just fine as English. My main complaint is Java’s word filter. Ruby provides select for items you’re keeping and offers reject to filter out items that match the predicate.

                .filter(Words.longerThan(3))
                .filter(Words.shorterThan(12))

Collect

This code requires a fair amount of knowledge to understand it.

.collect(Collectors.toMap(Function.identity(), s -> 1, (a, b) -> a + b));

The collector is going to create a map and takes 3 functions. The first maps the value to the key. We use Function.identity(), because the word is the key. The next function maps the value to the value in the map. Each word counts once. The last bifunction describes how to combine 2 values with the same key; we’re just going to add them.

We’ll just move this code wholesale to a method that returns the Collector.

private static <V> Collector<V, ?, Map<V, Integer>> countInstances() {
        return Collectors.toMap(Function.identity(), s -> 1, (a, b) -> a + b);
    }

The parameterized return type is ugly, but it won’t show up in our top level code. The order is input type, accumulator type (which isn’t exposed), and finally the return type. So, all of that noise just means this returns a Collector that accepts any type and returns a Map of that type and integer as the value.

We could put this in the Words class, though there’s actually nothing word specific in this code. I just left it as a method in the class we’re writing.

Final Result

In the end, we have this, which is extremely readable.

 Map<String, Integer>  wordCounts = text.stream()
                .flatMap(Words.fromSentence())
                .map(String::toLowerCase)
                .filter(Words.longerThan(3))
                .filter(Words.shorterThan(12))
                .collect(countInstances());

I am a big believer in pushing your code. See if you can make it just a little bit better. This is how we can all become better developers. In this case, I wanted to push to see how to make my code as readable as possible. I encourage you to try it, but also to try making your code as clean as possible. Explore what can be accomplished.

You can find additional examples and some slides here.

Seth Kraut
Seth Kraut

Obsessed with how the world works

Related