Динамический обзор в Clojure? - PullRequest
5 голосов
/ 31 мая 2010

Я ищу идиоматический способ получения динамически изменяемых переменных в Clojure (или аналогичный эффект) для использования в шаблонах и тому подобное.

Ниже приведен пример проблемы с использованием таблицы поиска для перевода атрибутов тега из некоторого не-HTML-формата в HTML, где таблице требуется доступ к набору переменных, предоставленных из других источников:

(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

Я хочу иметь возможность оценить правила (слева), а также результат (справа), и мне нужен какой-то способ поместить переменные в область действия в том месте, где оценивается таблица.

Я также хочу, чтобы логика поиска и оценки не зависела от какой-либо конкретной таблицы или набора переменных.

Полагаю, в шаблонах есть похожие проблемы (например, для динамического HTML), когда вы не хотите переписывать логику обработки шаблона каждый раз, когда кто-то помещает новую переменную в шаблон.

Вот один из подходов, использующий глобальные переменные и привязки. Я включил некоторую логику для поиска в таблице:

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

Это хорошо, потому что логика поиска не зависит от таблицы, поэтому ее можно повторно использовать с другими таблицами и другими переменными. (Плюс, конечно, общий табличный подход составляет примерно четверть размера кода, который был у меня, когда я делал переводы «вручную» в гигантском состоянии.)

Это не так приятно, потому что мне нужно объявить каждую переменную как глобальную для привязки к работе.

Вот еще один подход, использующий «полу-макрос», функцию с возвращаемым в синтаксисе возвращаемым значением, которой не нужны глобальные переменные:

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

Остальная часть кода требуется всего пара изменений:

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

И мы получаем тот же результат без глобалов. (И без динамического определения объема.)

Существуют ли другие альтернативы для передачи наборов привязок переменных, объявленных в другом месте, без глобальных переменных, требуемых binding Clojure?

Есть ли идиоматический способ сделать это, например Ruby's binding или Javascript function.apply(context)?

Обновление

Я, вероятно, делал это слишком сложным, вот что я предполагаю, это более функциональная реализация вышеперечисленного - без глобалов, уловок и без динамического определения объема:

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

Эта версия короче, проще и читается лучше. Так что я полагаю, что мне не нужна динамическая область видимости, по крайней мере, пока.

Постскриптум

Подход «оценивать каждый раз» в моем обновлении выше оказался проблематичным, и я не мог понять, как реализовать все условные тесты в виде мультиметодической отправки (хотя я думаю, что это должно быть возможно).

Итак, я получил макрос, который расширяет таблицу до функции и cond. Это сохраняет гибкость исходной реализации eval, но более эффективно, требует меньше кодирования и не требует динамического определения объема:

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

расширяется до

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

Я не знаю, является ли это особенно функциональным, но это, безусловно, DSLish (сделать микроязык для упрощения повторяющихся задач) и Lispy (код как данные, данные как код), оба из которых ортогональны функциональности.

На первоначальный вопрос - как сделать динамическое определение объема в Clojure - я полагаю, что ответ заключается в том, что идиоматический способ Clojure состоит в том, чтобы найти переформулировку, которая не нуждается в этом.

Ответы [ 2 ]

7 голосов
/ 31 мая 2010

Ваш подход к проблеме не кажется очень функциональным, и вы используете eval слишком часто; это пахнет как плохой дизайн.

Вместо использования фрагментов кода, который вы передаете eval, почему бы не использовать вместо этого надлежащие функции? Если требуемые переменные являются фиксированными для всех шаблонов, вы можете передать их непосредственно в качестве аргументов; если это не так, вы можете передать привязки в виде карты.

6 голосов
/ 31 мая 2010

Ваш код выглядит так, как будто вы делаете его сложнее, чем нужно. Я думаю, что вы действительно хотите, это многоплановые методы clojure. Вы можете использовать их, чтобы лучше абстрагировать таблицу диспетчеризации, которую вы создали в attr-table , и вам не нужны динамические области видимости или глобальные переменные для ее работы.

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

Все очень лаконично и функционально, без глобальных или даже таблиц атрибутов.

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

Это не совсем то, что вы делаете, но небольшая модификация, и это будет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...