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.
Penso che tu abbia un punto: ho aggiunto una versione aggiornata, meglio? –
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? –