I am only covering one section in this post, but this one section includes file I/O, exception handling and locking. This is a post about the examples from On Lisp, and so isn't a tutorial on any of these topics. Hopefully, it provides a gentle introduction to each topic.
Section 11.2 The with- macroThe with-open-file macro Graham describes is just with-open in Clojure. It can be used with any resource that implements a close method.
(with-open [writer (clojure.java.io/writer "output-file" :append true)] (.write writer "99"))
Clojure has a pair of functions for doing stream I/O. slurp and spit, for reading and writing, both use with-open to manage their streams.
(spit "output.txt" "test" :append true ) (slurp "output.txt")
Graham's unwind-protect becomes a try-catch-finally block, which works just like you would expect.
(try (do (println "What error?") (throw (Exception. "This Error.")) (println "This won't run")) (catch Exception e (.getMessage e)) (finally (println "this runs regardless")))
Graham's with-db example combines mutations, locks and exception handling. In his first example, he rebinds *db* to a new value, locks it, uses the new value, releases the lock and resets the value. In Clojure, you can create dynamic variables, but changes to their values only appear in the current thread. For Clojure datatypes locks are unnecessary.
(def ^:dynamic *db* "some connection") (binding [ *db* "other connection"] (println *db*))
Because the value assigned to a var with binding is only visible on the current thread, this will not work with code that you want to execute on a different thread. If we are using mutable Java objects across different threads, locking can come into play. Clojure has a locking macro which accepts the object to lock and the code to be executed.
Strings in Java are immutable, so I am going to use a StringBuilder. Graham's let form becomes something like this:
(def db2 (StringBuilder. "connection")) (let [old-val (.toString db2)] (.replace db2 0 (.length db2) "new connection") (locking db2 (println (.toString db2))) (.replace db2 0 (.length db2) old-val))
Clearly Graham's call to with-db is preferable to writing the let form over and over. And, as he points out, it is easy enough to add a try-finally block to make a safer implementation.
(defmacro with-db [db & body] `(let [temp# (.toString db2)] (try (.replace db2 0 (.length db2) ~db) (locking db2 ~@body) (finally (.replace db2 0 (.length db2) temp#))))) (with-db "new connection" (println (.toString db2)))
Graham also gives an example that uses both a macro and a function, which has most of the work being done in the function inside of a context created by the macro. I am not going to claim to understand how Common Lisp manages memory, or how that is different from Clojure. Instead, I will simply acknowledge his point, that perhaps a macro can be used to create a context, and the rest of the work can be done in a function.