In Clean Code, Uncle Bob proposes two rules for good functions: “The first rule of
functions is that they should be small. The second rule of functions
is that they should be smaller than that.” Useful rules, but Clojure
requires one more: they’re still not small enough.
languages and immutable data make reasoning easy by making functions
simpler. Functions take input, transform it, and return new output. Data passes
through functions, flowing rather than mutating. Complicated functions
hard, and they
can be dangerously easy to write.
Hyperbole aside, there really are two simple rules for functions: they
should be small, and do one thing. In his presentation on
functions, Uncle Bob describes a simple
algorithm for cleaning up crufty functions:
Pick a function.
Extract functions until it does one thing.
Recur on extracted functions.
Instead of a contrived wombat example, I’ll use one of my own
disgusting old 4Clojure solutions as an example of atrocious code. (But cut me some slack! I was
young and naive.)
Here’s the problem:
“Write a function which takes a collection of integers as an argument.
Return the count of how many elements are smaller than the sum of
their squared component digits. For example: 10 is larger than 1
squared plus 0 squared; whereas 15 is smaller than 1 squared plus 5
Like nested blocks in other languages, code that sprawls rightward
indicates a problem—and it can happen fast in Clojure.
To start, we’ll extract lt-sqd-components from the let binding.
(This is a common, awful 4Clojure hack for defining a named function
inside an anonymous one, though the discerning 4Clojurist uses letfn).
The original function is almost readable, but we can do better. It
looks like I didn’t understand filter when I wrote this: the extra
map is redundant since lt-sqd-components is already a predicate function that
returns true or false.
One more function to extract: the let binding should be its own
function. One might argue that this function does one thing—all it
does is check whether a number is less than the sum of its squared components!
But it’s operating on several different levels of abstraction: digits,
a sequence of digits, and their sum. A helpful guideline is limiting
functions to one level of abstraction. In this case, the function
should only know about the sum.
And there it is—clean, readable functions at all levels of
abstraction, minimal nesting, and nothing longer than three lines.
Starting from the top, low-level functions build into bigger
abstractions through combination and composition. Each step is easy to
read and comprehend.
As Uncle Bob puts it in Clean Code:
Master programmers think of systems as stories to be told rather than programs to be written. They use the facilities of their chosen programming language to construct a much richer and more expressive language that can be used to tell that story. Part of that domain-specific language is the hierarchy of functions that describe all the actions that take place within that system. In an artful act of recursion, those actions are written to use the very domain-specific language they define to tell their own small part of the story.
Extract. Simplify. Recur. Take the time to consider each line, and clean code comes naturally.