Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic scoping in Clojure?

Tags:

clojure

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.

like image 522
j-g-faustus Avatar asked May 30 '10 23:05

j-g-faustus


2 Answers

Your approach to the problem doesn't seem to be very functional, and you are using eval too often; this smells like bad design.

Instead of using snippets of code that you pass to eval, why not use proper functions instead? If the variables required are fixed for all of the patterns, you can pass them in directly as arguments; if they are not, you can pass in the bindings as a map.

like image 112
Brian Avatar answered Oct 31 '22 17:10

Brian


Your code looks like you are making it harder than it needs to be. I think what you really want is clojure multi-methods. You can use them to better abstract the dispatch table you created in attr-table and you don't need dynamic scoping or globals to make it work.

; helper macro for our dispatcher function
(defmulti html-attr (fn [& args] (take (dec (count args)) args)))

(defmethod html-attr [:LINK "a"]
  [attr tagname aval] {:href aval})

(defmethod html-attr [:LINK "img"]
  [attr tagname aval] {:src aval})

All very concise and functional without requiring globals or even an attr-table.

USER=> (html-attr :LINK "a" "http://foo.com") {:href "http://foo.com}

It doesn't do exactly what your does but a little modification and it would.

like image 29
Jeremy Wall Avatar answered Oct 31 '22 18:10

Jeremy Wall