The Hyperpessimist

The grandest failure.

Seven Languages, Week 6: Clojure

We’re near the end and this week (cough, the last 2 days) I did CLojure. I used Clojure some years before, I even got “Programming Clojure” standing in my shelf, although I with it were “The Joy of Clojure” instead. Nevermind.

Clojure is powerful. Actually, Clojure is immensely powerful and super-concise. My experience is whenever I ask for code review on the Clojure IRC channel or mailing list, they reduce my code to 1/3 of it’s length. Form the book, it was obvious that Bruce Tate liked Clojure as well, unlike e.g. Scala, so in the Clojure chapter he also described what was not covered in depth.

Personally, I could have gone without defrecord and defprotocol, as the introduction was kinda weak and did not make much sense. The usage of protocols felt incredibly forced and the homework where one should design and implement a protocol was difficult, because I had no idea what to implement at all. So I made a protocol for ranting.

But this time, the homework was at least not that boring, in general. For example, one was to implement unless, so I actually went the extra mile and made it work just like if which means that it can take an optional else clause but does not have to.

1
2
3
(defmacro unless
  ([test iffalse] (list 'unless test iffalse 'nil))
  ([test iffalse iftrue] (list 'if (list 'not test) iffalse iftrue)))

So the first version expands into a recursive macro call with the second argument set to nil, in a similar way like if works. Coming from Scheme and syntax-rules I think this looks kinda ugly to construct these things with literal calls to list and manual quoting. Well, I guess it is simpler and simplicity is a big proint for Clojure. And furthermore I can keep my syntax-rules if I like.

Day 3 had an interesting/tricky task, the Sleeping barber problem, where one had to implement a concurrent solution using the Clojure primitives. Turned out to be tricky, because I’m not really sold on agents. Implementing this problem with Actors would have been easy/trivial, just send some messages around. With agent, you do it the other way ‘round: instead of sending messages to functions, you send functions to threadsafe values.

Changing my thinking took quite forever, but the first step to get to a solution was to define an agent. The first breakthrough was that barber was an agent of the number of people he serviced. So you could implement a function that pauses for 20ms (“working”) and then increments the counter. So, maybe it would make sense to make the number of free waiting spots an actor too? I did this and implemented a function to try to add a customer to the waiting queue (try-occupy) and turn him down if it is full already. Freeing a seat is easy (when the barber finishes a customer and gets a new one), just increment the number of free seats in free-seat.

Now that you have a function to free-seat, you need to tell the barber to free one seat before going off to work on the customer in cut-hair.

If you run it on this point, nothing happens, because no customers arrive. This kinda ticked me off because now we’d need to create a thread that tries to occupy a seat every 10 to 30ms and this did not seem elegant. Yet at the end, I did not see a better solution so I helped myself to a function that sleeps and recursively creates new customers, client-arrive.

When you try starting it at this point, you realize your waiting queue is full with 3 customers but the barber does nothing. So yet again, we need a function that moves people from the waiting queue to the barbers chair, move-to-chair.

Then just start these two threads, wait for 10 seconds and check the value of barber. For me, in the initial run on Clojure 1.4.0 it was around 250 clients serviced which was rather poor, with Clojure 1.2.1 it was three-hundred-something which was better but still poor. I considered running it on a faster computer and it dropped to 122 customers which was really awful.

After some head-scratching I tried putting the move-to-chair thread to sleep for 1ms, to prevent it from gobbling up all computation. Lo and behold, 480-490 customers out of a theoretical maximum of 500, I’d say this is pretty decent.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
; barber <serviced>
(def barber (agent 0))
; seats <free>
(def free-seats (atom 3))

(defn any-seat-occupied? []
  (< @free-seats 3))

(defn free-seat [free]
  (inc free))

(defn try-occupy [free]
  (if (= free 0) 0 (dec free)))

(defn cut-hair [serviced]
  (if (any-seat-occupied?)
    (do
      (swap! free-seats free-seat)
      (Thread/sleep 20)
      (inc serviced))
    serviced))

(def continue-running (atom true))

(defn move-to-chair []
  (loop []
    (if @continue-running
      (do
        ; prevent this thread from gobbling up all processing power
        (Thread/sleep 1)
        (send barber cut-hair)
        (recur)))))

(defn client-arrive []
  (loop []
    (if @continue-running
      (let [time (+ 10 (rand-int 20))]
        (Thread/sleep time)
        (swap! free-seats try-occupy)
        (recur)))))

(future (move-to-chair))
(future (client-arrive))
(Thread/sleep (* 10 1000))
(println @barber)
(reset! continue-running false)
(shutdown-agents)

To my great surprise, I had absolutely no correctness problems at the first try. Maybe it paid off that I was drawing the things on a piece of paper to compose my thoughts?

I was curious how other people solved this and to some point I was actually quite disappointed by some solutions. Ben’s takes the cake, because he writes Clojure with Java syntax which makes my eyes bleed (actually, he writes Io like this too) and his solution is verbose and awkward. This awkward-syntax thing is also in Bash-Shell’s code (seriously, camelCase in Lisp‽) but the actual solution behind it it is similar to mine. Devnoo again is awfully Java-istic. But there is hope, Yannick’s solution seems to me quite idiomatic and Frédéric’s solution is so immensely similar to mine, I was actually wondering. Actually, after I wrote mine, I took some inspiration from him, like the fact that free-sets should better be an atom and not an agent as well as to use future instead of the (.start (Thread. function)) clutch I had before.