Charlie Harvey

Clojure — Day Three

I suppose it was inevitable after my cockiness on day two that day three should be a challenging one. I felt very stretched; mostly I was getting stuck on syntax, though. The actual coding was great fun. I’ve come rather to like Clojure; I definitely think it’ll be one of the languages I’ll play with in the future. I think that with a bit more playing I could actually click with a Lisp — something that hasn’t happened for me before.

accounts.clj

Use refs to create a vector of accounts in memory. Create debit and credit functions to change the balance of an account.

This was by far the easier of the assignments, though I did cheat a little by using a map (that is to say hash) rather that a vactor for convenience. It felt a little ugly to overwrite the map entry each time I wanted to make a change to an account, I will get round to looking at how other folks approached this problem eventually. But here is the code with a couple of comments.; I’m using an hash keyed on ac number to hold my accounts for convenience (def accounts (ref {} )) ; makes a new account (effectively the same as updating an existing one) (defn new-account [newac] (dosync (alter accounts conj newac))) ; retrieves an account from accounts ref (defn get-account [acnum] (@accounts acnum)) ; retrieves the balance of an account (defn account-balance [ac] (ac :balance)) ; retrieves the name on an account (defn account-name [ac] (ac :name)) ; sets the balance on an account given an account number and a new balance (defn set-balance [acnum balance] (new-account {acnum {:name (account-name(get-account acnum)), :balance balance}})) ; credit an account given the account number and an amount by which to credit it (defn credit-account [acnum amt] (set-balance acnum (+ (account-balance (get-account acnum)) amt ))) ; debit an account given the account number and an amount by which to credit it (defn debit-account [acnum amt] (set-balance acnum (- (account-balance (get-account acnum)) amt )))

The set-balance function was the most brain-melting for me. It creates a new account with the existing details, but with a changed balance. The credit and debit-account methods are simply wrappers for set-balance. I did get a bit stuck on the debit method, which turned out to be to do with the order of the arguments. 5-100 != 100-5 as it turns out. If I were to change this code I suppose I’d use account number as the parameter of account-name and account-balance which would make it a bit more consistent. Lets run it through the REPL. Clojure user=> (new-account {:12345 {:name "John Smith", :balance "20"}}) {:12345 {:name "John Smith", :balance "20"}} user=> (println (get-account :12345)) {:name John Smith, :balance 20} nil user=> (println (account-balance (get-account :12345))) 20 nil user=> (println (account-name (get-account :12345))) John Smith nil user=> (set-balance :12345 50) {:12345 {:name "John Smith", :balance 50}} user=> (credit-account :12345 10) {:12345 {:name "John Smith", :balance 60}} user=> (debit-account :12345 5) {:12345 {:name "John Smith", :balance 55}} user=>

barbershop.clj

In this section I’m going to outline a single problem called sleeping barber. It was created by Edsger Dijkstra in 1965. It has these characteristics:

  • A barber shop takes customers
  • Customers arrive at random intervals, from 10 to 30 milliseconds.
  • The barber shop has 3 chairs in the waiting room.
  • The barbershop has one barber and one chair.
  • When the barber’s chair is empty, a customer sits in the chair, wakes up the barber, and gets a haircut.
  • If the chairs are occupied, all new customers will turn away.
  • Haircuts take 20 milliseconds.
  • After a customer receives a haircut, he gets up and leaves.

Write a multithreaded program to determine how many haircuts a barber can give in 10 seconds

Good old CS questions like this can be great fun. My implementation is a bit strange. I think I ought to have implemented concurrency with agents or perhaps with refs; but instead I used futures (which seemed interesting to learn). Now that wouldn't be such a big deal but I suspect that its possible that both threads may be able to access the waiting-room queue at the same time. Well, maybe I’ll write a better version one day. But this will do for now, and it certainly taught me a lot!

In the first bit of code, I set some agents and some flags up. It is nice to be able to use punctuation in function names. As you can see I use a LinkedBlockingQueue from java as my "waiting room" which was all I found in the research homework. I use agents to track the number of haircuts and to do my logging. This is overkill, but it is always interesting to play with a new tool. ; flag to keep going when this gets set false stop threads (def continue? (atom true)) ; barbers has 3 chairs ; I use http://docs.oracle.com/javase/6/docs/api/java/util/concurrent/LinkedBlockingQueue.html#size() ; to model the waiting room. (def waiting-room (java.util.concurrent.LinkedBlockingQueue. 3)) ; number of haircuts barber has done. use an agent for this. not crucial but nice (def num-haircuts (agent 0)) ; we will keep a list of logs for info (def log-agent (agent ()))

These next two functions are used by the agents; for logging and for incrementing the agent’s state. I think it is really nice to be able to call a function ++1, though I can see that it may be ill advised to do so in the context of production code.; increment my-agent by 1 (defn ++1 [my-agent] (+ 1 my-agent)) ; log something (defn logmsg [my-agent msg] (println msg))

The next function is a sort of uber thread which we start off later on. It kills the program after daylength milliseconds and prints the logs out. Note that the second sleep is only there to make sure that the other threads get a chance to finish. ; this is a thread to keep an eye on the rest of the threads. We don’t want the program running on indefinitely (defn opening_hours [daylength] ( (println) ; for some reason a call to my logmsg in the first line causes clojure to throw a ClassCastException (send log-agent logmsg "!!!! Open for business !!!!") (Thread/sleep daylength) (swap! continue? not) ; some threads may still be running when we get here. The longest wait is 30ms ; for a customer so wait for that plus a bit before closing the barbers (Thread/sleep 35) (send log-agent logmsg "!!!! Closing time !!!!") (send log-agent logmsg (str "\nBarber completed " @num-haircuts " haircuts")) (System/exit 0)))

OK the next two functions are the customer and the barber. They use the put and poll methods of the LinkedBlockingQueue. And they both look at the continue? flag to keep their loops looping. In customer we put an hairy customer into the waiting room, and then sleep for 10..30 milliseconds, after logging the fact. In barber, when-let checks if there is any customers in the queue. As soon as there is, the next customer gets polled from the queue, has their haircut and the num-haircuts agent state is incremented.; pushes a customer into the waiting room if there is a space in it, after waiting from 10..30ms (defn customer [] (future (while @continue? (.put waiting-room "Hairy customer" ) ; add a customer to the queue (send log-agent logmsg (str "+ Customer joins queue (queue has " (.size waiting-room) " customers in)")) ; customers arrive at random intervals from 10..30 ms (Thread/sleep (+ 10 (rand-int 2)))))) ; if there is a customer in the queue, cuts their hair after waiting 20ms, increments number of haircuts (defn barber [] (future (while @continue? (when-let [item (.poll waiting-room)] ; grab a customer from the queue if there is one (send log-agent logmsg "- Customer got haircut") (Thread/sleep 20) ; haircuts take 20 ms (send num-haircuts ++1)))))

The final function is responsible for kicking all the threads off. I found the dorun repeatedly 1 future construction somewhere on stackoverflow, but then managed to clear my browsing history and not to be able to find it again. Once barbershop is defined we call it and that is the end of the program.; kicks off the loops for this barbers (defn barbershop [] (dorun (repeatedly 1 customer)) ; customers arrive one at a time (dorun (repeatedly 1 barber)) ; barber cuts customers hair 1 at a time (opening_hours 10000)) ; how many haircuts in 10 seconds? (barbershop)

Here is a sample run. I changed the time down to 200 milliseconds for the sake of brevity. My code reckons the number of haircuts at 480..490, which sounds a little high to me.$ java -cp /usr/share/java/clojure.jar clojure.main barbershop.clj !!!! Open for business !!!! + Customer joins queue (queue has 1 customers in) - Customer got haircut + Customer joins queue (queue has 1 customers in) + Customer joins queue (queue has 2 customers in) - Customer got haircut + Customer joins queue (queue has 2 customers in) - Customer got haircut + Customer joins queue (queue has 2 customers in) + Customer joins queue (queue has 3 customers in) - Customer got haircut + Customer joins queue (queue has 3 customers in) - Customer got haircut + Customer joins queue (queue has 3 customers in) - Customer got haircut + Customer joins queue (queue has 3 customers in) - Customer got haircut + Customer joins queue (queue has 3 customers in) - Customer got haircut + Customer joins queue (queue has 3 customers in) - Customer got haircut + Customer joins queue (queue has 3 customers in) - Customer got haircut + Customer joins queue (queue has 3 customers in) !!!! Closing time !!!! Barber completed 10 haircuts $

Bye Clojure, Haskell Next

Its been fun learning a little Clojure. There is a lot to like about it and I’m definitely going to learn more in the future. The concurrency seemed easier to understand than that of Erlang at least for me. The sheer quantity of brackets wasn’t quite as much fun for me, but my VimClojure rig eased that difficulty a fair bit. Higher order functions look like they’ll be as easy as you’d expect of a Lisp and are one of the main things I want to get my head around.

I’m now approaching Haskell, the last of the seven languages in the book. Its been pretty intense so far and I suspect that Haskell won’t be any easier than the others. Its got that sort of reputation. We shall see in the next installment.


Comments

  • Be respectful. You may want to read the comment guidelines before posting.
  • You can use Markdown syntax to format your comments. You can only use level 5 and 6 headings.
  • You can add class="your language" to code blocks to help highlight.js highlight them correctly.

Privacy note: This form will forward your IP address, user agent and referrer to the Akismet, StopForumSpam and Botscout spam filtering services. I don’t log these details. Those services will. I do log everything you type into the form. Full privacy statement.