Enlightenment in action
One of the ideas that Clojure embraces is this Perlism:
It is better to have 100 functions operate on one data structure than 10 functions on 10 data structure
This makes a lot of sense. But it also is clear that the language, as a set of functions and features, is large. Of course, at a low level, the language is very small; but the layers that have been built to operate on data structures have a depth that I haven't yet mastered.
It's not a case of the layers or functions being too complicated ... rather, I just haven't discovered everything that's possible yet. And when I haven't, I am resorting to mechanical solutions. I suppose this is simply a part of the journey, and while building a mechanical solution to a problem is irksome, it's educational, especially when you are shown something so much more succinct.
An example
Here's one example, a solution to 4Clojure problem 63 "Group a Sequence". A fairly straightforward challenge, but one that I couldn't see an obviously neat way of solving. (Note that the rules prevented the use of the group-by
function, with which it would have been a cinch to solve, of course!).
A clean but mechanical approach
Here's what I ended up with:
(fn p63 [f xs]
(loop [elements xs
result {}]
(if (empty? elements)
result
(let [element (first elements)
value (f element)
values (or (result value) [])]
(recur (rest elements)
(assoc result value (conj values element)))))))
In one way, I'm happy, because it's using the loop/recur
construction (tail recursion idiom), with the "first/rest" pattern, and it's not mutating any state. And I typed this in directly and it solved the puzzle first time :-)
But there's a mechanical nature to it. Here's what it does, generally:
- starts a
loop
with the elements given, and an empty result map - if there are no elements, it just returns whatever's in the result map at that time
- otherwise it takes the first element, calculates the result of applying the function to that element, and retrieves the current values for that result key (defaulting to an empty vector if there aren't any yet)
- then in the body of the
let
binding it simplyrecur
s with therest
of the elements (all but thefirst
), setting the value for theresult
var to be that plus the addition of the calculated value in the right place in the map
A neater approach
Here's the solution from another 4Clojure user that I'm following (and I am learning a great deal from them, whoever they are!):
(fn [f s]
(apply merge-with concat (map #(hash-map (f %1) [%1]) s)))
Wow. The power of this solution, and the secret of its brevity, is the merge-with
function, which is documented thus:
Returns a map that consists of the rest of the maps conj-ed onto the first. If a key occurs in more than one map, the mapping(s) from the latter (left-to-right) will be combined with the mapping in the result by calling (f val-in-result val-in-latter).
This was exactly the right thing. The (map #(hash-map (f %1) [%1]) s)
form simply returned a flat list of hash-maps with the keys being the result of applying the given function to the element, and the values being the elements themselves. Beautifully simple, in the philosophy of focusing on performing just one task.
And then the myriad hash-maps were gathered together with merge-with
using the concat
function to resolve same-key clashes (in other words, "just group them together").
Taking the first of the puzzle's unit tests as an example, here's what stage one (pre merge-with
) looks like. This:
((fn [f s] (map #(hash-map (f %1) [%1]) s)) #(> % 5) [1 3 6 8])
produces this:
({false [1]} {false [3]} {true [6]} {true [8]})
Then applying the merge-with concat
we get the result:
{false (1 3) true (6 8)}
Lovely. I'm still on my journey to enlightenment, and am enjoying learning about functions such as merge-with
on the way.