An exercise

Sample 4clojure exercise solutions, my solution and then other developers’ solutions.

Introduction

I was looking for a new Clojure-related pet project to train my Clojure programming skills, but finding a new pet project idea turned out to be a rather difficult task. Meanwhile, I thought I could start doing 4Clojure exercises. I remember beginning those exercises during the old site. Now I decided to do the exercises in the difficulty order: elementary, easy, medium, and hard. I have now finished all elementary and easy tasks, and I thought this might be a perfect moment to reflect on what I have learned developing solutions for these exercises.

You can find my solutions in my4clojure repository.

The Repository Structure

When I started implementing the solutions for the 4Clojure exercises, I didn’t plan to publish my solutions. Later on, I thought it might be an interesting idea to write a blog post regarding these exercises. Therefore, I published the project directory as a GitHub repository. There is some Finnish text here and there: had I known that I would publish the project in Github, I would have written all comments in English. But basically, you don’t lose that much. If you really want to know the meaning of some Finnish comment - just use Google Translate.

Some explanation for the project structure and the tooling might be beneficial if some Clojure-newbie wants to start practicing the same 4Clojure exercises and use the same setup I use in my repository.

There are two source directories. In the src directory you can find clj subdirectory for the Clojure on JVM, and the cljs subdirectory for the Clojurescript on Node. Since both Clojure and Clojurescript are essentially the same languages, just running on different hosts (except the host interop, of course), you can run your Clojure REPL in whatever host you are more familiar with - I implemented most of the exercises on the Clojure/JVM side. Still, I occasionally wanted to test some exercises also on the Clojurescript/Node side. The reason for having both Clojure on JVM and Clojurescript on Node setups is that the 4ever-clojure site runs using Clojurescript on the browser (transpiling to Javascript) - therefore, you might want to use Clojurescript/Javascript interop in some of the exercises (and you cannot use Clojure/Java interop, of course).

The second source directory is scratch, which I always have in my projects. This is a scratch area for experimental and throw-away REPL stuff.

Development Instructions

There is a Justfile:

λ> just
Available recipes:
    backend     # Start backend repl.
    clean       # Clean .cpcache and .shadow-cljs directories, run npm install.
    init        # Init node packages.
    lint        # Lint.
    list        # List Just options.
    outdated    # Update dependencies.
    run-node    # Start node process (2/2).
    shadow-node # Node compilation (1/2).

Open three terminals. In the first terminal run just backend. In the second terminal run just init, then just shadow-node, and wait until the build is ready. In the third terminal run just run-node. Now you are ready to connect your IDE’s REPL integration both to the Clojure on JVM process and to the Clojurescript on Node process. After connecting the Clojurescript REPL you need to give command (shadow.cljs.devtools.api/repl :app) in the REPL. Now you have a working IDE REPL integration to both processes, and you can send Clojure/script forms from your editor directly to be evaluated to those REPL processes. I use IntelliJ/Cursive, and these instructions work at least in that environment - I haven’t tested other environments, but I see no reason why they wouldn’t work e.g., using Emacs/Cider.

Elementary Tasks

The elementary tasks were, well, elementary. If you have been learning Clojure, these tasks should be pretty straightforward - you may want to skip them.

Let’s have an example of one of the most interesting elementary tasks, my solution first, and then other developers’ best solutions:

Problem 156, Map Defaults

; P156
(def P156 (fn [v keys] (into {} (map (fn [k] {k v}) keys))))
; Other developers' solutions:
; HYVÄ!
(def P156 #(reduce into (for [k %2] {k %})))
(def P156 #(zipmap %2 (repeat %1)))

Easy Tasks

Most of the easy tasks were easy, but some turned out to be a bit challenging for me. And some were really interesting and provided good learning opportunities. Let’s have a couple of examples.

Problem 19, Last Element

; P19
(def P19 (fn [lst] (let [c (count lst)] (first (drop (- c 1) lst)))))
; Other developers' solutions:
(def P19 #(first (reverse %)))
; HYVÄ!
(def P19 (comp first reverse))

My solution is a bit awkward - count the sequence size, drop all but last and then take the first element. Why didn’t I figure out that you can just reverse the sequence and take the first element?

Problem 135, Infix Calculator

(def P135 (fn [a & xs] (:acc (reduce (fn [acc x]
                                       (if (#{+ - / *} x)
                                         (assoc acc :oper x)
                                         (assoc acc :acc ((:oper acc) (:acc acc) x))))
                                     {:acc a :oper nil} xs))))
; Other developers' solutions
(def P135 (fn c [x f y & r]
            ((if r
               (fn [z] (apply c z r))
               +)
             (f x y))))

My solution is self-explanatory. But the solution below my solution is really ingenious. I.e., you first calculate the (f x y), and if there are more elements, you apply function c to that new calculated element z and the rest of the elements r. See also the next chapter in which I explain how to understand this solution using hashp tool.

Using Hashp to Study Other Developers’ Solutions

I often studied other developers’ solutions in more detail using the hashp library. I strongly recommend hashp tool while learning Clojure.

Let’s use the Problem 135, Infix Calculator task as an example. This solution might be a bit hard to understand, but the hashp printouts provide good clues to what is happening in the function.

(def P135 (fn c [x f y & r]
            (let [_ #p x
                  _ #p f
                  _ #p y
                  _ #p r])
            ((if r
               (fn [z] (let [_ #p z] (apply c z r)))
               +)
             (f x y))))
=> #'easy-solutions/P135
(P135 10 / 2 - 1 * 2)
#p[easy-solutions/c:2] x => 10
#p[easy-solutions/c:2] f => #<Fn@1f494d4 clojure.core/_SLASH_>
#p[easy-solutions/c:2] y => 2
#p[easy-solutions/c:2] r => (#<Fn@36a25998 clojure.core/_> 1 #<Fn@1467e3a9 clojure.core/_STAR_> 2)
#p[easy-solutions/c:7] z => 5
#p[easy-solutions/c:2] x => 5
#p[easy-solutions/c:2] f => #<Fn@36a25998 clojure.core/_>
#p[easy-solutions/c:2] y => 1
#p[easy-solutions/c:2] r => (#<Fn@1467e3a9 clojure.core/_STAR_> 2)
#p[easy-solutions/c:7] z => 4
#p[easy-solutions/c:2] x => 4
#p[easy-solutions/c:2] f => #<Fn@1467e3a9 clojure.core/_STAR_>
#p[easy-solutions/c:2] y => 2
#p[easy-solutions/c:2] r => nil
=> 8

Lessons Learned This Far

While doing these exercises, I realized one essential insight. After implementing your solution for the exercise, it is really important to study other developers’ solutions. While studying other developers’ solutions, I often noticed a one-liner solution that just applies one or two Clojure standard library functions. So, studying other developers’ solutions provides two great learning purposes:

  1. You get to understand how other developers think while constructing their own solutions. Other developers have a different mental model regarding high-level functional programming, and often those solutions reveal quite a lot regarding their thinking and problem-solving process.
  2. You get to have good examples applying Clojure standard library functions - applying one or more functions with map, filter, and reduce often provides a very concise and beautiful solution.

So, if you are reading my solutions in this repository, remember to examine other developers’ solutions at 4ever-clojure. You can find other developers’ solutions at the end of each exercise page, e.g.:

Problem 135, Infix Calculator => “Want to see how others have solved this? View problem #135 solutions archive No cheating please! :)”

Final Words

Implementing 4Clojure exercises has been quite fun. I have occasionally streamed good music and spent some time implementing one or two exercises. I didn’t rush. After implementing my solution, I always studied the best other developers’ solutions. Especially those moments were really delightful and rewarding - it was really satisfying to read some of the best solutions, how elegantly they solved the task just by applying the basic functional primitives with one or two Clojure standard library functions.

The writer is working at Metosin using Clojure in cloud projects. If you are interested in starting a cloud or Clojure project in Finland, you can contact me by sending an email to my Metosin email address or contact me via LinkedIn.

Kari Marttila