After a few weekends exploring Clojure I came up with this program. It allows you to move a little rectangle in a window. Here’s the code:
(import java.awt.Color)
(import java.awt.Dimension)
(import java.awt.event.KeyListener)
(import javax.swing.JFrame)
(import javax.swing.JPanel)
(def x (ref 0))
(def y (ref 0))
(def panel
(proxy [JPanel KeyListener] []
(getPreferredSize [] (Dimension. 100 100))
(keyPressed [e]
(let [keyCode (.getKeyCode e)]
(if (== 37 keyCode) (dosync (alter x dec))
(if (== 38 keyCode) (dosync (alter y dec))
(if (== 39 keyCode) (dosync (alter x inc))
(if (== 40 keyCode) (dosync (alter y inc))
(println keyCode)))))))
(keyReleased [e])
(keyTyped [e])))
(doto panel
(.setFocusable true)
(.addKeyListener panel))
(def frame (JFrame. "Test"))
(doto frame
(.add panel)
(.pack)
(.setDefaultCloseOperation JFrame/EXIT_ON_CLOSE)
(.setVisible true))
(defn drawRectangle [p]
(doto (.getGraphics p)
(.setColor (java.awt.Color/WHITE))
(.fillRect 0 0 100 100)
(.setColor (java.awt.Color/BLUE))
(.fillRect (* 10 (deref x)) (* 10 (deref y)) 10 10)))
(loop []
(drawRectangle panel)
(Thread/sleep 10)
(recur))
Despite being an experienced C++ programmer I found it very challenging to write even a simple application in a language that uses a radically different style than what I’m used to.
On top of that, this code probably sucks. I suspect the globalness of the various values is a bad thing. It’s also not clear to me if it’s appropriate to use references here for the x and y values.
Any hints for improving this code are welcome.
Those
ifs inkeyPressedcan be replaced with a singlecase. Also, thedosynccan be moved outside to wrap thecase. In fact,altercan be moved out too, so that if you e.g. decide to change it tocommute, there’s just the one place to make the change. The result:You could also rewrite the imports more concisely:
— whether you’d want to is a matter of style.
I would rename
drawRectangletodraw-rectangle(that’s the idiomatic style for function names in Clojure) and, more to the point, rewrite it as a pure function accepting the coordinates as explicit arguments. Then you could write a small wrapper around that to use your Refs, if indeed your design would benefit from the use of Refs. (Hard to say without knowing how you might want to use & modify them etc.)Prefer
whileto(loop [] ... (recur))(see(doc while)and(clojure.contrib.repl-utils/source while)).Also — and this is important — don’t put anything except definitions at top level. That’s because top-level forms are actually executed when the code is compiled (try loading a library with a
(println :foo)at top level). That infinite loop should be wrapped inside a function; the standard name for a “main” function in Clojure is-main; same goes forpanelandframe. This doesn’t apply when playing around at the REPL, of course, but it’s an important gotcha to know about.Incidentally,
(doto foo ...)returns foo, so you can just write(doto (proxy ...) (.setFocusable true) ...).Otherwise, I’d say the code is fine. Normally you’d want to put it in a namespace; then all the imports would go in the
nsform.HTH