Developers, along with many Test Solution Developers, have to decide whether they want to jump on the functional programming bandwagon. I’m not here to extol the virtues of functional programming. What I am here to look at is something I hear particularly from many people, which is that functional programming tends to make programs less clear. This does have particularly relevance to me since I do like code with a high degree of clarity regarding its intention.
First, there’s a book I highly recommend called Functional Thinking by Neal Ford. It’s a fantastic book and it’s where I got my examples that I showcase in this book. These examples are also freely available in the GitHub repository for the book. If you want to use the code in this post (and from the book), you’ll need Java 8, Scala, Groovy, and Clojure installed.
My focus here is on determining if going with a functional approach sacrifices the clarity of code. However, I will also say this. Sometimes proponents of functional programming techniques do not do themselves any favors. In a somewhat related book to the one I just mentioned, Joshua Backfield in Becoming Functional says this:
Requirements and expectations today are difficult, so being able to closely mirror mathematical functions allows engineers to design strong algorithms in advance and rely on developers to implement those algorithms within the time frame required. The closer we bind ourselves to a mathematical underpinning, the better understood our algorithms will be. Functional programming also allows us to apply mathematics on those functions. Using concepts such as derivatives, limits, and integrals on functions can be useful when we are trying to identify where functions might fail.
Yeah, that’s great and all. Maybe it’s even true. But it does absolutely nothing for me in terms of deciding how much and to what extent I should care about functional programming based on what it can do for me. (That said, I actually do recommend Joshua’s book.) What I like to see are what trade offs I have to make when I switch from one language or one concept to another. One of the most important for me is clarity of expression in code. I had some concerns about this related to functional examples I’ve seen. So let’s delve into this a bit.
Word Frequency – Imperative vs Functional
In his book, Neal Ford attempts to contrast between the traditional programming style — imperative loops, essentially — and a functional approach. This is in service to an implementation of a word-frequency algorithm. So that everyone can play along, let’s first code up the imperative approach using Java 7. Create a file called Words.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
import java.util.HashSet; import java.util.Set; import java.util.Map; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; public class Words { @SuppressWarnings("serial") private Set<String> NON_WORDS = new HashSet<String>() {{ add("the"); add("and"); add("of"); add("to"); add("a"); add("i"); add("it"); add("in"); add("or"); add("is"); add("d"); add("s"); add("as"); add("so"); add("but"); add("be"); }}; public Map<String, Integer> wordFreq(String words) { TreeMap<String, Integer> wordMap = new TreeMap<String, Integer>(); Matcher m = Pattern.compile("\\w+").matcher(words); while (m.find()) { String word = m.group().toLowerCase(); if (! NON_WORDS.contains(word)) { if (wordMap.get(word) == null) { wordMap.put(word, 1); } else { wordMap.put(word, wordMap.get(word) + 1); } } } return wordMap; } } |
Then create a Main.java file to put that logic to the test:
1 2 3 4 5 6 7 8 9 10 |
import java.util.Map; public class Main { public static void main(String[] args) { String phrase = "I am going to the market for a few market things."; Words words = new Words(); Map<String, Integer> result = words.wordFreq(phrase); System.out.println(result); } } |
The output from this is:
{am=1, for=1, going=1, market=2, things=1}
The logic here is to determine the most frequently used words in a given sentence, printing out a sorted list of those words along with their frequencies, while also ignorning articles or connector words. The wordFreq() method has a Map that holds the key/value pairs. A regular expression is created that allows words to be pulled out of the phrase. The core logic of this method iterates over the words found, ensuring that a word is either added for the first time to the map or the occurence of the existing word is incremented for the frequency count.
As Neal says in the book:
This style of coding is quite common in languages that encourage you to work through collections (such as regular expression matches) piecemeal.
Now let’s change up the example to use the more functional aspects of Java 8. Change Words.java to look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import java.util.HashSet; import java.util.Set; import java.util.Map; import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.List; import java.util.ArrayList; public class Words { @SuppressWarnings("serial") private Set<String> NON_WORDS = new HashSet<String>() {{ add("the"); add("and"); add("of"); add("to"); add("a"); add("i"); add("it"); add("in"); add("or"); add("is"); add("d"); add("s"); add("as"); add("so"); add("but"); add("be"); }}; private List<String> regexToList(String words, String regex) { List<String> wordList = new ArrayList<>(); Matcher m = Pattern.compile("\\w+").matcher(words); while (m.find()) wordList.add(m.group()); return wordList; } public Map<String, Integer> wordFreq(String words) { TreeMap<String, Integer> wordMap = new TreeMap<String, Integer>(); regexToList(words, "\\w+").stream() .map(w -> w.toLowerCase()) .filter(w -> !NON_WORDS.contains(w)) .forEach(w -> wordMap.put(w, wordMap.getOrDefault(w, 0) + 1)); return wordMap; } } |
Here the results of a regular expression match are converted to a stream. (This uses the Stream API of Java 8.) Notice how this version of the code allows for performing discrete operations. A mapping is used to make all of the word entries lowercase. A filter is used to remove the articles and connector words. A forEach loop is used to count the frequencies of the words that are left after the filter operation. The find() method returns an iterator and that iterator is converted to a stream in the regexToList() method. That allows the discrete operations to be chained together. According to Neal, this is “the same way that [we] think about the problem.”
It was certainly possible to do all of this in the imperative version. You could have simply looped over the collection three times to perform the same discrete operations. So why not do that? Mainly because, from an execution perspective, that would be considered inefficient. So the core idea here is that the functional approach is giving you efficiency. What some would argue is that it’s sacrificing clarity. Put another way, while the imperative approach of the Java 7 example may be more clear, the functional approach of Java 8 is more efficient.
What do you think?
I know that some would argue that higher-order functions, like map() and filter(), actually do provide better clarity because they allow you, in Neal’s words, “elevate your level of abstraction.” But, again, what do you think? This is an important area to wrestle with as you explore the functional landscape. It’s the area where I’ve seen most people encounter cognitive friction.
Name Capitalizer – Java, Scala, Groovy
In the book Neal gives an example of a program that takes a list of names, remove any single-letter names, and then capitalizes the list of remaining names. Let’s recreate the example for ourselves. We’ll take a look at it in the context of the main JVM-hosted languages. As we go through this, consider that what this example shows, from a purely programmatic point of view, is three discrete operations that are taking place: filtering a list of names, transforming the list of names (so they are capitalized), and converting the list of names (into a single string).
Java 7
First create Names.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import java.util.List; public class Names { public String cleanNames(List<String> names) { StringBuilder result = new StringBuilder(); for (int i = 0; i < names.size(); i++) { if (names.get(i).length() > 1) { result.append(capitalize(names.get(i))).append(","); } } return result.substring(0, result.length() - 1).toString(); } public String capitalize(String s) { return s.substring(0, 1).toUpperCase() + s.substring(1, s.length()); } } |
There you have the quintessential example of an imperative loop. For each name that is provided as part of the list, a check is made see if the length of the name is at least greater than 1. The name is capitalized (by calling an entirely different method) and then that capitalized name is appended onto a result variable. Comma handling is done by making sure that each name, except the last one, gets a comma appended to it.
Create a Main.java that will let you test it out:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) { List<String> list = new ArrayList<String>(); list.add("j"); list.add("jeff"); list.add("emilia"); list.add("caity"); list.add("s"); Names names = new Names(); String result = names.cleanNames(list); System.out.println(result); } } |
The output of this will be:
Jeff,Emilia,Caity
Neal calls out a key point about this, which is that we are using the “same low-level mechanism (iteration over the list) for all three types of processing.”
Java 8
Let’s turn this into a Java 8 example. Modify Names.java so that it looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import java.util.List; import java.util.stream.Collectors; public class Names { public String cleanNames(List<String> names) { if (names == null) return ""; return names .stream() .filter(name -> name.length() > 1) .map(name -> capitalize(name)) .collect(Collectors.joining(",")); } public String capitalize(String s) { return s.substring(0, 1).toUpperCase() + s.substring(1, s.length()); } } |
That will run exactly the same as the previous imperative example but I would argue it does provide quite a bit of clarity. But what exactly is happening? Functional programming describes programs as transformations, which is a low-level activity. Filtering, collecting, mapping — those are all transformations. As you can see with the above Java 8 example, those transformations are represented as higher-order functions.
Now let’s consider how we can do the above in other JVM-hosted languages.
Groovy
Let’s try Groovy first since it tends to be the easiest for Java developers to move to given the similarity of language constructs. Create a file called Names.groovy with the following code:
1 2 3 4 5 6 7 8 |
class Names { public static String cleanNames(names) { names .findAll { it.length() > 1 } .collect { it.capitalize() } .join ',' } } |
If you want to run it, just create a main method like this:
1 2 3 4 5 |
static void main(args) { def list = ["j", "jeff", "emilia", "caity", "s"] def result = new Names(); println result.cleanNames(list) } |
The output will be exactly the same. What Groovy mainly does here is allow you to remove some of the boilerplate of Java. I would argue the clarity remains.
Scala
Now let’s do the same thing in Scala. Create a file called Names.scala and put the following in it:
1 2 3 4 5 6 7 8 |
val names = List("j", "jeff", "emilia", "caity", "s") val result = names .filter(_.length() > 1) .map(_.capitalize) .reduce(_ + "," + _) println(result) |
Again, same output. You could be forgiven for thinking that it looks like Scala is actually starting to remove some clarity.
But let’s stop a moment and consider what’s going on with both Groovy and Scala. The first thing to notice is that Scala, like Groovy, allows you to shortcut writing code blocks with single parameters. Groovy used “it”, Scala uses the underscore. Scala lets you use an underscore for the parameter if you don’t care what the actual name of the parameter is and Groovy lets you use “it”. If that reduces clarity — and it kind of does, I think — you can certainly use your own parameters.
Name Capitalizer – A Synthesis View
Let’s consider these different functional implementations, somewhat side-by-side, particularly since all of the code examples are doing the same thing. Here I’ll just focus on the core transformations that we are concerned about:
1 2 3 4 5 6 |
// Java 8 names .stream() .filter(name -> name.length() > 1) .map(name -> capitalize(name)) .collect(Collectors.joining(",")); |
1 2 3 4 5 |
// Groovy names .findAll { it.length() > 1 } .collect { it.capitalize() } .join ',' |
1 2 3 4 5 |
// Scala names .filter(_.length() > 1) .map(_.capitalize) .reduce(_ + "," + _) |
Do keep in mind that Java 8’s call to capitalize does require your own function, which is not the case with Groovy or Scala. It also requires use of the Stream API and the Collectors API.
In the case of Scala, a list of names is filtered and the output of that filter transformation is used as an argument to the map() function. The map() function executes the supplied code block on each element of the collection, returning the transformed collection. The collection is held by the underscore and so a capitalize operation is performed on each element in that collection. The output collection from the map transformation is then provided to the reduce() function. This function combines each element based on the rules supplied in the code block. So knowing that the underscore is holding the each element of the collection is key to understanding this functional example.
In the case of Groovy, a findAll() function is used on a collection. This is basically Groovy’s filter transformation. The collect() method executes the supplied code block on each element of the collection which, remember, is held by the implicit parameter “it”. The join() function is then called, which accepts a collection of strings.
The reduce() method being used in the Scala version is very much like the collect() method that was used in the Java 8 example earlier. In fact, collect() is really a special case of the reduce() function in Java 8. The collect() function used in the Groovy version is very much like Scala’s map() function.
I turn the question to you: has a functional approach maintained clarity when you remove a lot of the surrounding material? Does one language or another stand out as “better” or “worse”? I would argue that this is the kind of comparison that needs to be made for people who want to look at the essence of functional programming as compared to other styles.
Name Capitalizer — Getting Some Clojure
Since Clojure is usually stated as the functional language for the JVM, let’s consider a Clojure version of all this. Create a file called Names.clj and put the following in it:
1 2 3 |
(defn process [names] (reduce str (interpose "," (map s/capitalize (filter #(< 1 (count %)) names))))) |
If you’re not used to Lisp-like functional languages, the above is likely to scare you. Neal provides a good example of this in his book and describes it in detail. Basically, Clojure’s (filter) function accepts two parameters. The first is a function to use for filtering and the second is the collection to filtered. Here an anonymous function is passed that actually does the filtering and the collection is the names, of course.
The (map) function requires a transformation function as the first parameter. In this case, that’s a capitalization call. The second parameter must be the collection (names) but note that here the code is actually passing the result of the filter transformation, which is the filtered list of names. The result of the (map) transformation is passed as the parameter to the (reduce) function. The (interpose) function is like the collect() of Java 8, the join() of Groovy, or the reduce() of Scala.
So … Was Clarity Achieved?
Neal’s book takes you through further examples of how to make the Clojure version even more readable. He also shows you to make parallelizable versions of all of the examples. I don’t want to reproduce that here because (1) I’m still trying to understand it myself and (2) you really should buy his book, which I hope I’ve whetted your appetite for.
What I hope you got out of this post is that there is a good reason to look at functional programming concepts. It is an important area to become familiar with and as Neal says in the book:
Even if you don’t care about Scala or Clojure, and are happy coding in your current language for the rest of your career, your language will change underneath you, looking more functional all the time. Now is the time to learn functional paradigms, so that you can leverage them when (not if) they appear in your everyday language.
If I was going to build a new functional-based test solution framework right now and I wanted it to be in a JVM-hosted language, and assuming I didn’t want that to be something like JRuby or Jython, I would certainly avoid Java. (Then again, I avoid the Java language as much as I can.) I would likely pick Groovy, with Scala being a close second. The point here being that I need some basis to make that decision and the above provides me with some of that.
A personal goal of mine is to retain clarity of expression in my code at all times. Any language that does not allow me to do that is one I would tend to avoid. That, in fact, reinforces my view of avoiding Java and likely Clojure. I would still focus on Groovy and Scala. A known aspect of Scala is that Scala has some horrendous start up times — as opposed to operation execution time — when compared with other JVM-languages so if that aspect of performance mattered, it would lead me even more into Groovy.
Again, though, at least I would have a basis for making these decisions and that’s what matters to me. That’s certainly what I hope this post does for you: provides you a way for further exploration and thus making your own decisions.
Did you not get my comment or did you just not want to publish it? Not a problem, just wondering. I’m not the kind of person who just blindly agrees with anyone in the industry, regardless of what the current fads are. I don’t even remember what I wrote exactly now but perhaps it was a bit too snarky. 🙂
Hey Matt. I apologize; it looks like your original comment might have gotten lost in the ether. I definitely don’t mind snarky. 🙂
I’m with you about not just following the fads, however.
Weird, I’m not sure what happened. . . anyway, I thought you may have taken my comment the wrong way. 🙂
Or maybe I failed to post it. . . anyway, I posted a comment to the effect of yes and no and that functional programmers tend to be arrogant. 😉
Yeah, I’m somewhat with you on this one. Given the functional approach still making its way out of academia, I’ve definitely noticed a little aloofness on the part of many proponents.
For myself, I really struggle to see what, if any, benefit functional programming has for test solutions development in particular. I’m struggling to determine the relevance that functional programming has in a wider context so my initial follow up to this post was going to be a “simple” test framework done in both “traditional” OOP and somewhat-new-fangled functional.
But I haven’t found a way to do that yet that doesn’t make it look like the functional is purely contrived as an academic exercise as opposed to something demonstrably more useful than another approach.
Yeah, that’s how I feel basically. It’s not that I think functional programming is bad, it’s just that OO or better yet, multi-paradigm languages work well for most use cases.
There are puzzling contradictions in FP too. . . functional programmers are usually obsessed with minimizing state as much as possible yet they also LOVE closures. For me, a closure over a function is a stateful construct and therefore a contradiction of FP ideals.
But the part from you post that really struck a nerve in me was the quote “Now is the time to learn functional paradigms, so that you can leverage them when (not if) they appear in your everyday language.”.
Be alert peons! Sophisticated functional constructs are coming your way. . . don’t get lost in the shuffle!
Languages like Ruby and Python (more so with Ruby) have had functional interfaces for years. Yes, Java 8 is adding lambdas. . . I suppose that’s what he was referencing.
That attitude though. . .
You can call me crazy but I don’t think Haskell’s monads can solve all of the world’s problems.
🙂