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 if
s in keyPressed
can be replaced with a single case
. Also, the dosync
can be moved outside to wrap the case
. In fact, alter
can be moved out too, so that if you e.g. decide to change it to commute
, there's just the one place to make the change. The result:
(def panel
(proxy [JPanel KeyListener] []
(getPreferredSize [] (Dimension. 100 100))
(keyPressed [e]
(let [keyCode (.getKeyCode e)]
(dosync
(apply alter
(case keyCode
37 [x dec]
38 [y dec]
39 [x inc]
40 [y inc])))
(println keyCode)))
(keyReleased [e])
(keyTyped [e])))
You could also rewrite the imports more concisely:
(import [java.awt Color Dimension event.ActionListener])
(import [javax.swing JFrame JPanel])
-- whether you'd want to is a matter of style.
I would rename drawRectangle
to draw-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 while
to (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 for panel
and frame
. 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 ns
form.
HTH
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With