Ping Ring del 6: Clojure


onsdag 15. september 2010 Diverse prosjekter Ping Ring Clojure Samtidighet

Dette er del 6 i artikkelserien Ping Ring hvor jeg implementerer et og samme program i et utall ulike programmeringsspråk - for å se om det er noe å lære gjennom å gjøre det. Introduksjonen kan du lese her.

Det måtte jo nesten komme – en implementasjon av Ping Ring i mitt nye favorittspråk, Clojure. Og dette ble også den implementasjonen jeg liker best sålangt. Les videre for å finne ut hvorfor.

Én av Clojures fire grunnpilarer er at det er designet for samtidighet. Det har derfor en rekke virkemidler for å gjøre den typen programmering enklere, som persistant and immutale data structures, transaksjonelt programvareminne (STM), samt flere datatyper med innebygde samtidighetsprimitiver (var, ref, agent og atom). I dette programmet har jeg brukt future-makroen for å kjøre de ulike delene i separate tråder – en enkel sak basert på agents, som jeg blogget om i fremtidige løfter.

TCP-kommunikasjonen var også greit og passe elegant. For å sende oppretter jeg en vanlig java-socket og bruker metoden spit (!) til å sende en streng gjennom den (linje 12 til 14). For lytte-biten fant jeg funksjonaliteten jeg trengte i Clojure Contrib-biblioteket: create-server tar inn en port og en funksjon som vil bli brukt til å håndtere forespørsler.

Her er koden i sin helhet:

1 (use 'clojure.contrib.server-socket 'clojure.contrib.duck-streams)
2 (import '(java.net Socket) '(java.util Date))
3
4 (defstruct server-args :this-port :other-port :max-delay :initial-ping)
5
6 (def last-ping-time
7      (atom (Date. (long 0)))) ; January 1, 1970, 00:00:00 GMT
8
9 (defn send-ping [args]
10       (future
11         (Thread/sleep 1000)
12         (try (spit ; is this the coolest function name or what?!
13                (Socket. "127.0.0.1"  (args :other-port)) 
14                (str     "PING from " (args :this-port)))
15              (catch Exception e
16                     (println "*** Failed sending ping!")))))
17
18 (defn listen-for-pings [args]
19       (create-server (args :this-port)
20                      (fn [in-stream _]
21                          (reset! last-ping-time (Date.)) ; that's now!
22                          (println "Received" (slurp* in-stream))
23                          (send-ping args))))
24
25 (defn date-diff "get diff of two dates in seconds" [a b]
26       (-> (- (.getTime a) (.getTime b))
27           (/ 1000) int))
28
29 (defn ping-delay "get time since last ping in seconds" []
30         (date-diff (Date.) @last-ping-time))
31
32 (defn ping-delayed? [max-delay]
33       (> (ping-delay) max-delay))
34
35 (defn watch-for-missing-pings [args]
36       (Thread/sleep 5000)
37       (when (ping-delayed? (args :max-delay)) 
38         (println "*** ALERT, RING BROKEN!"
39                  "No ping in" (ping-delay) "seconds.")
40         (send-ping args))
41       (recur args)) ; AGAIN!
42
43 (defn main [args]
44       (println (format "**Clojure Ring Server (%s)" (args :this-port)))
45       (when (args :initial-ping) 
46         (send-ping args))
47       (future (listen-for-pings args))
48       (future (watch-for-missing-pings args)))
49
50 (main (struct server-args ; parse command line args into struct
51               (Integer. (nth *command-line-args* 0)) ; this-port
52               (Integer. (nth *command-line-args* 1)) ; other-port
53               (Integer. (nth *command-line-args* 2)) ; max-delay
54               (Boolean. (nth *command-line-args* 3)))) ; initial-ping

Når det gjelder mengde kode stiller denne implementasjonen på linje med Ruby og Boo, den er enklere enn Erlang-versjonen, og mindre bråkete enn C#. Å beskrive logikken i Clojure-syntax var ganske greit, og programmet bør være enkelt å sette seg inn i om man klarer å undertrykke sin parantesfobi.

Skal jeg være litt kritisk så er det jeg likte minst ved koden det jeg måtte gjøre for å sjekke om en ping er forsinket – dvs. beregningene knyttet til dato/tid (linje 25 til 33). Jeg måtte bryte det opp på denne måten for at det skulle bli forståelig, og skulle ønsket den var enklere. Men man kan vel ikke få alt man ønsker seg?!

Koden for å hente inn argumentene fra kommandolinjen (linje 51 til 54) er også litt mere bråkete enn det du finner i de andre språkene, men det har ingen videre innvirkning på totalinntrykket eller kompleksiteten.

Jeg gjorde denne implementasjonen før jeg hadde satt meg orntlig inn i bruk av Clojure agents (som ikke er det samme som Erlang actors, men som likevel har noen fellestrekk i hvilke problemer de løser). På båten hjem i går implementerte jeg en ny versjon av Ping Ring i Clojure som bruker agents, og den får du se i neste del…

Tidligere i serien: Introduksjon | Del 2 (C#) | Del 3 (Ruby) | Del 4 (Boo) | Del 5 (Erlang).

Kildekoden fra denne blogposten er tilgjengelig på Github. Der står du fritt til å forgrene løsningen og gjøre egne modifikasjoner om du ønsker det (for å illustrere et poeng eller lignende). Som alt annet på bloggen er koden lisensiert under Creative Commons.


comments powered by Disqus