September 3, 2019

Cleaner Code

Clean Code, by Robert C. Martin. A formative work used as gospel by some organisations, and for good reason: it’s a manifesto on what good code looks like, and removes contention over opinions on how things should be done. The book has a lot of great things to say, and I personally agree with the author and have learned some useful language for talking about code. However, in the context of strongly-typed functional programming (namely Haskell), there are some glaring inconsistencies between the book’s recommendations and the solutions it presents. In this series of posts I attempt to address these and make the book more useful in an FP context.

I’ll write as I’m reading, so I apologise in advance if I solve a problem which the book will address in a future chapter. I will cover each individual chapter in order, and will follow the book’s own (well chosen) section order. I’ll only address sections as needed, so anything I don’t mention, I agree with.

I start with chapter 2 because chapter 1 was merely introductory.

Use Intention-Revealing Names

The book presents this code:

public List<int[]> getThem() {
  List<int[]> list1 = new ArrayList<int[]>();
  for (int[] x : theList)
    if (x[0] == 4)
      list1.add(x);
  return list1;
}

…and then states that renaming the function and variables would have resolved the ‘implicitity’ of the code (the degree to which the context is not explicit in the code itself). Choosing names is important, but FP gives us another axis along which to convey meaning: function shape. What do I mean by function shape? Not the physical layout of the function in text, but the mental structure it evokes when you read it. Observe the same function in Haskell:

getThem = reader (mfilter ((== 4) . (! 0)))

This code contains no variable names, yet by its shape we gain a stronger understanding on what it does than was possible in Java. We can immediately tell that:

  • The function uses a value in its environment (courtesy of reader). Try to spot this in the Java code! You have to notice that theList is not defined locally to realise the method has external dependencies.
  • The function finds things matching a predicate (courtesy of mfilter). This is only evident in the Java code once you recognise the for-if-add pattern.

Of course, there are some obvious design flaws. What is the significance of the number 4? What is the significance of the first element of the array? What is the purpose of this function? To answer these questions, the book sensibly renames the function and adds a level of abstraction over the data. A similar refactor to our Haskell code looks like this:

getFlaggedCells = reader (mfilter isFlagged)

A three-word implementation that leaves little to the imagination. The function name together with the shape of its implementation strongly suggests the desired interpretation.

Method Names

This section uses the phrase static factory method. The Haskell equivalent of a static factory method is a function. I can find no nuance here worth expanding on. In Java we use a static factory method to abstract over a constructor, and the result is a means to produce one value given some other value (or tuple of values). This is exactly the definition of a function.

In Haskell, like in Java, an API becomes more meaningful when the low-level value constructors are hidden from the user and replaced with higher-level functions. Given:

data Complex = MakeComplex Double Double

…rather than exporting the low-level MakeComplex value constructor from the module, export this more meaningful function (or static factory method, if you prefer):

fromRealNumber :: Double -> Complex

Add Meaningful Context

This section breaks my brain. The book rewrites a short and convoluted method into a very long and convoluted class. Here is the starting point:

private void printGuessStatistics(char candidate, int count) {
  String number;
  String verb;
  String pluralModifier;
  if (count == 0) {
    number = "no";
    verb = "are";
    pluralModifier = "s";
  } else if (count == 1) {
    number = "1";
    verb = "is";
    pluralModifier = "";
  } else {
    number = Integer.toString(count);
    verb = "are";
    pluralModifier = "s";
  }
  String guessMessage = String.format(
    "There %s %s %s%s", verb, number, candidate, pluralModifier
  );
  print(guessMessage);
}

The book claims to improve clarity by adding a class and four extra methods with shared state, nearly doubling the LOC, and yet resolving none of the structural inaccuracies present in the original code. For brevity I will not reproduce the book’s solution here, but instead point out that the original code was already too abstract, and the book’s solution added even more abstraction. A much better solution is to remove the unneeded abstraction. With the help of the string-interpolate library:

guessStatisticsMessage :: Char -> Int -> String
guessStatisticsMessage candidate count
  | count == 0 = [i|There are no #{candidate}s]
  | count == 1 = [i|There is 1 #{candidate}]
  | otherwise  = [i|There are #{count} #{candidate}s]

Witness again the benefits of function shape! The code is stupidly easy to understand, and in a quarter of the original LOC. Compare with the 40 lines of ‘improved’ code in the book which gives no such clarity.

My hope is that the author knew there was a better solution but was focused on a particular demonstration; but the book gives no such disclaimer.

Powered by Hugo & Kiss.