Sometimes Coding in Clojure Feels like Cheating

(maniacal laughing in background)

October 8, 2021

Sometimes when I'm writing Clojure code I feel like I'm cheating. It can be so elegant and concise, but still intelligible (if you 'speak the lingo', that is). Take problem 23 from Project Euler. This problem requires that we identify so-called 'abundant' numbers:

(defn is-abundant? [n]
  (as-> n $
        (math/proper-factors-of $)
        (apply + $)
        (> $ n)))

We start with a number (n), calculate its factors, sum them, and report whether that sum is greater than the starting number.

Given a range of numbers we must gather up all the abundant numbers within that space. Then, we calculate all possible sums of every combination of two abundant numbers from the above collection (but which do not exceed the max of the range/space in play). A list comprehension does this elegantly (it looks like a for-loop, but it's actually a much more powerful tool for generating collections):

(defn sums [space max]
  (set (for [x space
             y space
             :let [sum (+ x y)]
             :while (< sum max)] sum)))

Finally, we derive all numbers within the space that are NOT the sum of two abundant numbers and total them up. Here's the entire high-level algorithm, which makes use of the two functions above:

(defn euler-23 [max]
  (let [space         (range 1 max)
        abundant      (filter is-abundant? space)
        abundant-sums (sums abundant max)
        deficient     (remove abundant-sums space)]
    (apply + deficient)))

It's usually easier to compose algorithms like the one above with a step-by-step let statement, as you see above, and only then refactor to a threading operation, if desired. In this case, the as-> ("thread-as") macro seems like the best choice:

(defn euler-23 [max]
  (let [space (range 1 max)]
    (as-> (filter is-abundant? space) $
          (remove (sums $ max) space)
          (apply + $))))

Each little chunk of code above is expressive and to-the-point. There are no cumbersome for-loops required for factoring, filtering, mapping, removing, or summing, which seem to be the hallmark of most procedural languages. Every time you write a for loop, it's purpose can only be ascertained upon closer inspection. In this regard, coding in Clojure feels like cheating, or like a backdoor or shortcut that very few people seem to know about.

Here's the whole code snippet, in all its concise glory:

(defn is-abundant? [n]
  (as-> n $
        (math/proper-factors-of $)
        (apply + $)
        (> $ n)))

(defn sums [space max]
  (set (for [x space
             y space
             :let [sum (+ x y)]
             :while (< sum max)] sum)))

(defn euler-23 [max]
  (let [space (range 1 max)]
    (as-> (filter is-abundant? space) $
          (remove (sums $ max) space)
          (apply + $))))

-Michael Whatcott