I wanted a macro, but needed recursion
Standard disclaimer: Clojure is a new hobby.
It's easier for me to start things than complete them. That seemed like bad news when I just couldn't figure out how to generalize a small bit of Clojure code. One long week went by as I repeatedly got very close but couldn't quite solve the problem. While eventually I figured it out, the process of getting there made me look forward to writing more Clojure.
My last exploration of Clojure taught me to avoid for loops while splitting a string into overlapping character pairs. This exploration into Clojure was going to generalize that code to emit any width of overlapping character chunks.
Here is the old, inflexible code:
(map
(fn [l r] (str l r))
(seq text)
(rest (seq text))))
A quick summary of that:
- Defines a function named split-bigrams
- It accepts one untyped parameter, here named
text - The method body contains one function call to
map, which takes three arguments: one anonymous function taking two arguments, and two sequences. mappulls one character from each sequence and passes them to the anonymous function, which turns them into a string
The trick? Two iterators over one string, with the second iterator starting at the second character. Then just iterate over the lists and mash the items together. Problem is, to get character triplets I would need a third iterator starting at the third character. That's no way to live! Just a big fat FALE WHALE.
I desperately hoped that macros could somehow magically generate all the lists for map. I like macros. Clojure likes macros! My poor little train of thought couldn't scale this mountain. I thought I could, I thought I could, but between tucking in kids and dog walks I was too tired/busy. So I gave up and looked for another option.
Howzabout recursion? I spent several days rewriting variants of this:
(when (nthnext seq (- n 1))
(take n seq)))
(defn nice-try [n str]
(when-let [chars (take-chunk n str)]
(prn chars)
(recur n (rest str))))
This prints the values I want... it just doesn't put them into a collection! I feebly tried incorporating map and reduce. Not sure how many infinite loops were created and cancelled...
Finally, I decided to read the source code of Clojure to see how they do things like this. That was a great decision. core.clj is clean, clear, and full of goodness. Somewhere in there I found recur being used how I needed it: do something to a collection, call itself with the output until the input was exhausted, and return the aggregated output:
(let [ngram (take-chunk n chars)]
(if ngram
(recur n (rest chars) (cons ngram ngrams))
ngrams)))
This is probably a basic recursion pattern. I feel a little silly taking so long to figure it out. Just a little silly though, as the coding I do for a living doesn't encourage recursion.
At this point I am almost convinced that Clojure is the optimal language for me to grow as a programmer. Lisp itself is a low-level high-level language; you have to really understand how things are going to work, but not worry about things like garbage collection. The "homegrown VM" and "no library" problems just don't exist in Clojure. The APIs are clean and clear. Best of all the dogma is minimal and pragmatic. HUZZAH!


