Hi,
I'm looking for an idiomatic way to get dynamically scoped variables in Clojure (or a similar effect) for use in templates and such.
Here is an example problem using a lookup table to translate tag attributes from some non-HTML format to HTML, where the table needs access to a set of variables supplied from elsewhere:
(def *attr-table*
; Key: [attr-key tag-name] or [boolean-function]
; Value: [attr-key attr-value] (empty array to ignore)
; Context: Variables "tagname", "akey", "aval"
'(
; translate :LINK attribute in <a> to :href
[:LINK "a"] [:href aval]
; translate :LINK attribute in <img> to :src
[:LINK "img"] [:src aval]
; throw exception if :LINK attribute in any other tag
[:LINK] (throw (RuntimeException. (str "No match for " tagname)))
; ... more rules
; ignore string keys, used for internal bookkeeping
[(string? akey)] [] )) ; ignore
I want to be able to evaluate the rules (left hand side) as well as the result (right hand side), and need some way to put the variables in scope at the location where the table is evaluated.
I also want to keep the lookup and evaluation logic independent of any particular table or set of variables.
I suppose there are similar issues involved in templates (for example for dynamic HTML), where you don't want to rewrite the template processing logic every time someone puts a new variable in a template.
Here is one approach using global variables and bindings. I have included some logic for the table lookup:
;; Generic code, works with any table on the same format.
(defn rule-match? [rule-val test-val]
"true if a single rule matches a single argument value"
(cond
(not (coll? rule-val)) (= rule-val test-val) ; plain value
(list? rule-val) (eval rule-val) ; function call
:else false ))
(defn rule-lookup [test-val rule-table]
"looks up rule match for test-val. Returns result or nil."
(loop [rules (partition 2 rule-table)]
(when-not (empty? rules)
(let [[select result] (first rules)]
(if (every? #(boolean %) (map rule-match? select test-val))
(eval result) ; evaluate and return result
(recur (rest rules)) )))))
;; Code specific to *attr-table*
(def tagname) ; need these globals for the binding in html-attr
(def akey)
(def aval)
(defn html-attr [tagname h-attr]
"converts to html attributes"
(apply hash-map
(flatten
(map (fn [[k v :as kv]]
(binding [tagname tagname akey k aval v]
(or (rule-lookup [k tagname] *attr-table*) kv)))
h-attr ))))
;; Testing
(defn test-attr []
"test conversion"
(prn "a" (html-attr "a" {:LINK "www.google.com"
"internal" 42
:title "A link" }))
(prn "img" (html-attr "img" {:LINK "logo.png" })))
user=> (test-attr)
"a" {:href "www.google.com", :title "A link"}
"img" {:src "logo.png"}
This is nice in that the lookup logic is independent of the table, so it can be reused with other tables and different variables. (Plus of course that the general table approach is about a quarter of the size of the code I had when I did the translations "by hand" in a giant cond.)
It is not so nice in that I need to declare every variable as a global for the binding to work.
Here is another approach using a "semi-macro", a function with a syntax-quoted return value, that doesn't need globals:
(defn attr-table [tagname akey aval]
`(
[:LINK "a"] [:href ~aval]
[:LINK "img"] [:src ~aval]
[:LINK] (throw (RuntimeException. (str "No match for " ~tagname)))
; ... more rules
[(string? ~akey)] [] )))
Only a couple of changes are needed to the rest of the code:
In rule-match? The syntax-quoted function call is no longer a list:
- (list? rule-val) (eval rule-val)
+ (seq? rule-val) (eval rule-val)
In html-attr:
- (binding [tagname tagname akey k aval v]
- (or (rule-lookup [k tagname] *attr-table*) kv)))
+ (or (rule-lookup [k tagname] (attr-table tagname k v)) kv)))
And we get the same result without globals. (And without dynamic scoping.)
Are there other alternatives to pass along sets of variable bindings declared elsewhere, without the globals required by Clojure's binding
?
Is there an idiomatic way of doing this, like Ruby's binding
or Javascript's function.apply(context)
?
Update
I was probably making it too complicated, here is what I assume is a more functional implementation of the above - no globals, no evals and no dynamic scoping:
(defn attr-table [akey aval]
(list
[:LINK "a"] [:href aval]
[:LINK "img"] [:src aval]
[:LINK] [:error "No match"]
[(string? akey)] [] ))
(defn match [rule test-key]
; returns rule if test-key matches rule key, nil otherwise.
(when (every? #(boolean %)
(map #(or (true? %1) (= %1 %2))
(first rule) test-key))
rule))
(defn lookup [key table]
(let [[hkey hval] (some #(match % key)
(partition 2 table)) ]
(if (= (first hval) :error)
(let [msg (str (last hval) " at " (pr-str hkey) " for " (pr-str key))]
(throw (RuntimeException. msg)))
hval )))
(defn html-attr [tagname h-attr]
(apply hash-map
(flatten
(map (fn [[k v :as kv]]
(or
(lookup [k tagname] (attr-table k v))
kv ))
h-attr ))))
This version is shorter, simpler and reads better. So I suppose I have no need for dynamic scoping, at least not yet.
Postscript
The "evaluate everyting every time" approach in my update above turned out to be problematic , and I couldn't figure out how to implement all the conditional tests as a multimethod dispatch (although I think it should be possible).
So I ended up with a macro that expands the table to a function and a cond. This retains the flexibility of the original eval implementation, but is more efficient, takes less coding and doesn't need dynamic scoping:
(deftable html-attr [[akey tagname] aval]
[:LINK ["a" "link"]] [:href aval]
[:LINK "img"] [:src aval]
[:LINK] [:ERROR "No match"]
(string? akey) [] ))))
expands into
(defn html-attr [[akey tagname] aval]
(cond
(and
(= :LINK akey)
(in? ["a" "link"] tagname)) [:href aval]
(and
(= :LINK akey)
(= "img" tagname)) [:src aval]
(= :LINK akey) (let [msg__3235__auto__ (str "No match for "
(pr-str [akey tagname])
" at [:LINK]")]
(throw (RuntimeException. msg__3235__auto__)))
(string? akey) []))
I don't know whether this is particularly functional, but it is certainly DSLish (make a microlanguage to simplify repetitive tasks) and Lispy (code as data, data as code), both of which are orthogonal to being functional.
On the original question - how to do dynamic scoping in Clojure - I suppose the answer becomes that the idiomatic Clojure way is to find a reformulation that doesn't need it.