Estoy buscando una forma idiomática para obtener variables de ámbito dinámico en Clojure (o un efecto similar) para su uso en plantillas y tal.Ámbito dinámico en Clojure?
Aquí es un problema de ejemplo usando una tabla de búsqueda para traducir los atributos de la etiqueta de algún formato no HTML en HTML, donde la mesa necesita tener acceso a un conjunto de variables suministrada desde otro lugar:
(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
quiero ser capaz de evaluar las reglas (lado izquierdo) así como el resultado (lado derecho), y necesita alguna forma de poner las variables en el alcance en la ubicación donde se evalúa la tabla.
También quiero mantener la lógica de búsqueda y evaluación independiente de cualquier tabla o conjunto de variables en particular.
Supongo que hay problemas similares en las plantillas (por ejemplo, para HTML dinámico), donde no desea volver a escribir la lógica de procesamiento de plantillas cada vez que alguien coloca una nueva variable en una plantilla.
Aquí hay un enfoque que usa variables globales y enlaces. He incluido algo de lógica para la búsqueda en la tabla:
;; 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"}
Esto es bueno porque la lógica de búsqueda es independiente de la mesa, para que pueda ser reutilizado con otras mesas y diferentes variables. (Además, por supuesto, el enfoque de la tabla general es aproximadamente un cuarto del tamaño del código que tenía cuando hice las traducciones "a mano" en un cond gigante.)
No es tan agradable que necesito declara cada variable como global para que funcione el enlace.
Aquí hay otro método que utiliza un "semi-macro", una función con un valor de retorno de sintaxis-citado, que no necesita globales:
(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)] [])))
sólo son necesarios un par de cambios para el resto del código:
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)))
Y obtenemos el mismo resultado sin globales. (Y sin ámbito dinámico.)
¿Existen otras alternativas para pasar conjuntos de enlaces variables declarados en otra parte, sin los valores globales requeridos por Clojure's binding
?
¿Hay una manera idiomática de hacer esto, como Ruby's binding
o Javascript's function.apply(context)
?
actualización
probablemente estaba por lo que es demasiado complicado, aquí es lo que supongo que es una implementación más funcional de lo anterior - no hay variables globales, no hay evals y ningún ámbito dinámico:
(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))))
Esta versión es más corta, más simple y se lee mejor. Entonces, supongo que no necesito un alcance dinámico, al menos no todavía.
PostScript
El enfoque de "evaluar everyting cada vez" en mi actualización anterior resultó ser problemático, y no podía encontrar la manera de poner en práctica todas las pruebas condicionales como un despacho multimétodo (aunque creo que debería ser posible).
Así que terminé con una macro que expande la tabla a una función y un cond. Con ello se mantiene la flexibilidad de la aplicación eval original, pero es más eficiente, se tarda menos de codificación y no necesita ámbito dinámico:
(deftable html-attr [[akey tagname] aval]
[:LINK ["a" "link"]] [:href aval]
[:LINK "img"] [:src aval]
[:LINK] [:ERROR "No match"]
(string? akey) []))))
se expande en
(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) []))
No sé si esto es particularmente funcional, pero sin duda es DSLish (hacer un microlenguaje para simplificar tareas repetitivas) y Lispy (código como datos, datos como código), ambos ortogonales para ser funcionales.
En la pregunta original - cómo hacer un alcance dinámico en Clojure - Supongo que la respuesta es que la manera idiomática de Clojure es encontrar una reformulación que no la necesite.
Creo que tiene un punto. He añadido una versión actualizada, ¿no es así? –
Estoy aceptando su respuesta, ya que me hizo pensar en cómo podría hacerlo sin eval. Soy nuevo en el estilo funcional, mi reacción instintiva es que es increíblemente inútil evaluar algo más de lo que es absolutamente necesario, pero tal vez sea un pensamiento no funcional? –