Abstract: calls to
reduce are difficult to understand. An example
reduce in a code sample is analyzed and discussed to explore the reasons. An alternative version without the
reduce is offered and compared.
I’m really keen on legible code. Apart from correctness, I think legibility is the most important thing for software engineering, where the main cost is in communicating things between people. The trick is that “legible” is neither objective nor quantifiable, but that’s actually how it should be: we’re dealing with humans, here.
In my quest for legibility, I do code reviews. I recently saw the following code (in Clojure). I think it’s not quite as legible as it could be.
1 2 3 4 5 6 7 8 9
What is this doing? It’s taking a list of possible environment variable names (like
"ASSET_ROOT"), and, if the variable has a non-empty value, converting the name to a Clojure-friendly keyword (like
:asset-root) and adding the keyword and the corresponding value to a map. In other words, it goes from:
1 2 3
How legible is this code? Well, it’s not too bad. We have:
||Standard Clojure and LISP-y fare. Somewhat involved relative to a plain old function, but one of the first things any LISP-er learns.||1|
||A user-defined function. Well-named, conveys meaning well.||1|
||Simple function that almost all Clojurians have in their vocabulary.||1|
|| Not a widely-known core function, and a little complex to understand. (It takes a while to grok that the binding is available only in the consequent expression, not the alternate expression. And I still don’t know
||Basic Clojure stuff.||1|
Why do I give
reduce a score of 9? I know
reduce well. Heck, we first met 12 years ago in college. Even so, I have to slow down when I see a
reduce. What a given call to
reduce does doesn’t spring into my mind like
I think the reason is that, even though I can often understand the reducing function without too much effort, it is still difficult to understand the overall effect of the reduction. In other words, I have a (hopefully simple) function, but I have to understand the emergent effect of the function as it is used by
reduce. I have to hold the function in my mind—including all its conditional branches—as I walk through the operation of
reduce, until I eventually see how the complete call to
reduce shapes the output. Holding both the function and
reduce in my mind can be challenging.
Another reason why
reduce tends to be less legible is that it entangles multiple transformations into a single function. Clojurians would say that it “complects” multiple concerns. Isolating and disentangling the individual transformation steps can result in more legible code, as the example below will show.
The total weight in my totally subjective complexity score is 18. Can we do better?
Thankfully, with a good understanding of Clojure core functions, we have several good alternatives. Here’s one that I consider pretty legible:
1 2 3 4 5 6
How legible is the alternative?
||Same as above.||1|
||A threading macro. I think threading macros take a little work to understand.||2|
||A simple and widely understood Clojure and LISP tool.||1|
||This higher-order function takes a little work to understand, and it isn’t super common.||3|
||Same as above.||1|
|| A function literal. Basic Clojure stuff. (It’s a shame we can’t just use
|| The dual of the ubiquitous
||You have to be fairly comfortable with functions to be comfortable with this. One tricky bit is that the order in which the given functions are called is the reverse of the order in the parameter list.||5|
||Pretty basic Clojure function, and easy to understand, though not one of the first things people usually learn. Although this is made a little bit trickier because you have to know that you put pairs of values into a map.||3|
The total complexity of my alternative: 18.
It’s a wash! Both alternatives are 18. So maybe
reduce is just as good in this case. Nevertheless, I think the exercise of analyzing legibility is worthwhile, and I still consider the
reduce-less alternative simpler for those comfortable with higher-order functions.
Agree? Disagree? We’d like to know! Email us your comments at firstname.lastname@example.org.
Addendum: Our very own Brad Dollard prefers the
reduce version. He says that he traces the execution of the seed and the first element through the reducing function, and at that point it’s pretty easy for him to see the pattern. In any case, I think all would agree that this is very much a matter of taste and style. Nevertheless, I think it’s a worthwhile discussion to have, so long as nobody’s feelings get hurt. :-)
Edit 2015-07-09 at 3:55pm EDT: @gtrakGT suggests that a
for-based version, like the one below, might be even more legible. I’m inclined to agree!
1 2 3 4 5 6