2010-05-30 21 views
5

Sto cercando un modo idiomatico per ottenere variabili con ambito dinamico in Clojure (o un effetto simile) per l'uso in modelli e così via.Ambito dinamico in Clojure?

Ecco un problema ad esempio utilizzando una tabella di ricerca di tradurre tag attributi da parte di alcuni formati non-HTML in HTML, dove il tavolo ha bisogno di accedere a una serie di variabili forniti da altrove:

(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 

voglio essere in grado di valutare le regole (lato sinistro) e il risultato (lato destro), e hanno bisogno di un modo per inserire le variabili nell'ambito della posizione in cui viene valutata la tabella.

Voglio anche mantenere la logica di ricerca e valutazione indipendente da qualsiasi tabella o gruppo di variabili.

Suppongo che vi siano problemi simili coinvolti nei modelli (ad esempio per HTML dinamico), in cui non si desidera riscrivere la logica di elaborazione dei modelli ogni volta che qualcuno inserisce una nuova variabile in un modello.

Ecco un approccio che utilizza variabili globali e associazioni. Ho incluso una logica per la Tabella di ricerca:

;; 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"} 

Questo è bello in quanto la logica di ricerca è indipendente dal tavolo, in modo che possa essere riutilizzato con altri tavoli e diverse variabili. (Inoltre, l'approccio generale alla tabella è circa un quarto della dimensione del codice che avevo quando eseguivo le traduzioni "a mano" in un gigante.)

Non è così bello in quanto ho bisogno di dichiarare ogni variabile come globale affinché l'associazione funzioni.

Ecco un altro approccio con un "semi-macro", una funzione con un valore di ritorno della sintassi citato, che non ha bisogno globali:

(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)]  []))) 

sono necessari solo un paio di modifiche al resto del codice:

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))) 

E otteniamo lo stesso risultato senza globali. (E senza ambito dinamico.)

Esistono altre alternative per passare lungo insiemi di associazioni di variabili dichiarate altrove, senza le globali richieste da Clojure binding?

Esiste un modo idiomatico per eseguire questa operazione, ad esempio Ruby's binding o Javascript's function.apply(context)?

Aggiornamento

probabilmente stavo facendo troppo complicato, qui è quello che presumo è un'implementazione più funzionale di quanto sopra - no global, no evals e senza scoping dinamico:

(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)))) 

Questa versione è più breve, più semplice e legge meglio. Quindi suppongo di non aver bisogno di scope dinamiche, almeno non ancora.

Postscript

L'approccio "valutare everyting ogni volta" nel mio aggiornamento di cui sopra si è rivelato essere problematico, e non riuscivo a capire come implementare tutti i test condizionali come un dispaccio multimethod (anche se io penso che dovrebbe essere possibile).

Quindi ho finito con una macro che espande la tabella in una funzione e in una cond. Questo mantiene la flessibilità dell'attuazione eval originale, ma è più efficiente, richiede meno di codifica e non ha bisogno di scoping dinamico:

(deftable html-attr [[akey tagname] aval] 
    [:LINK ["a" "link"]] [:href aval] 
    [:LINK "img"]  [:src aval] 
    [:LINK]    [:ERROR "No match"] 
    (string? akey)  [])))) 

espande in

(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) [])) 

Non so se questo è particolarmente funzionale, ma è sicuramente DSLish (creare una microlingua per semplificare i compiti ripetitivi) e Lispy (codice come dati, dati come codice), entrambi i quali sono ortogonali all'essere funzionali.

Sulla domanda originale - come eseguire lo scoping dinamico in Clojure - Suppongo che la risposta diventi che il modo idiomatico Clojure è trovare una riformulazione che non ne abbia bisogno.

risposta

6

L'approccio al problema non sembra essere molto funzionale e si utilizza troppo spesso eval; questo odora di cattivo design.

Invece di utilizzare snippet di codice passato a eval, perché non utilizzare invece le funzioni appropriate? Se le variabili richieste sono fisse per tutti i pattern, puoi passarli direttamente come argomenti; se non lo sono, puoi passare i binding come una mappa.

+0

Penso che tu abbia un punto: ho aggiunto una versione aggiornata, meglio? –

+0

Accetto la tua risposta dal momento che mi ha fatto pensare a come avrei potuto farlo senza eval. Sono nuovo allo stile funzionale, la mia reazione istintiva è che è incredibilmente dispendioso valutare qualcosa di più di quello che è assolutamente necessario, ma forse è un pensiero non funzionale? –

6

Il tuo codice sembra che tu stia rendendo più difficile di quanto debba essere. Penso che quello che vuoi veramente sia il multi-metodo del clojure. Puoi usarli per astrarre meglio la tabella di spedizione che hai creato in attr-table e non hai bisogno di scope o globi dinamici per farlo funzionare.

; 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}) 

Tutto molto conciso e funzionale senza richiedere globals o anche un attr-table.

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

Non fa esattamente ciò che la fa, ma una piccola modifica e sarebbe

.
+0

Grazie per il suggerimento. A prima vista questo sembra più digitando rispetto alla versione della tabella, poiché le ripetizioni di "defmethod html-attr" più la lista degli argomenti sono più caratteri delle regole attuali, ma ne darò un'occhiata. In ogni caso siamo d'accordo sul fatto che stavo rendendo più difficile di quanto dovrebbe essere :) –

+0

j-g-faustus: Dai un'occhiata a clojure.template; usato correttamente, rimuoverà la verbosità. – Brian

+0

Brian: Grazie, lo cercherò. –