Разбор строки с макросом
Я пытаюсь оценить вставленное выражение в строке.
Некоторые примеры данных для оценки моего кода:
(def data {:Location "US-NY-Location1"
:Priority 3})
(def qual "(Location = \"US\")")
Я хотел бы qual
строка, которая будет преобразована во что-то вроде этой формы и оценена с помощью clojure
(= (:Location data) "US")
Я написал следующий макрос для достижения этой цели:
(defmacro parse-qual [[data-key op val] data-map]
`(~op ((keyword (str (quote ~data-key))) ~data-map) ~val))
и вспомогательная функция:
(defn eval-qual [qual-str data]
(eval `(parse-qual ~(clojure.edn/read-string qual-str) ~data)))
(eval-qual qual data)
дает мне ожидаемый результат
Это первый макрос, который я написал, и я все еще пытаюсь обернуть голову в цитирование и снятие цитат.
Я хочу знать, есть ли более эффективный способ достижения вышеуказанного? (Или вообще без необходимости макроса)
Как я могу расширить макрос, чтобы иметь дело с вложенными выражениями. Для обработки выражения, как
((Location = "US") or (Priority > 2))
, Любые указатели будут оценены. В настоящее время я пытаюсь играть сtree-seq
чтобы решить это.Как я могу сделать это более надежным и более изящным в случае инвалидности
qual
строка.
Я также написал вторую итерацию parse-qual
макрос следующим образом:
(defmacro parse-qual-2 [qual-str data-map]
(let [[data-key op val] (clojure.edn/read-string qual-str)]
`(~op ((keyword (str (quote ~data-key))) ~data-map) ~val)))
и на macroexpand
бросает следующее:
playfield.core> (macroexpand `(parse-qual-2 qual data))
java.lang.ClassCastException: clojure.lang.Symbol cannot be cast to java.lang.String
И я в недоумении, как отладить это!
Некоторая дополнительная информация:
macroexpand
parse-qual на REPL дает мне следующее:
playfield.core> (macroexpand
`(parse-qual ~(clojure.edn/read-string qual) data))
(= ((clojure.core/keyword (clojure.core/str (quote Location))) playfield.core/data) "US")
Спасибо @Alan Thompson, я смог написать это в виде функции следующим образом, это также позволяет оценивать вложенные выражения.
(def qual "(Location = \"US\")")
(def qual2 "((Location = \"US\") or (Priority > 2))")
(def qual3 "(Priority > 2)")
(def qual4 "(((Location = \"US\") or (Priority > 2)) and (Active = true))")
(defn eval-qual-2 [qual-str data]
(let [[l op r] (clojure.edn/read-string qual-str)]
(cond
(and (seq? l)
(seq? r)) (eval (list op (list eval-qual-2 (str l) data) (list eval-qual-2 (str r) data)))
(seq? l) (eval (list op (list eval-qual-2 (str l) data) r))
(seq? r) (eval (list op (list (keyword l) data) (list eval-qual-2 (str r) data)))
:else (eval (list op (list (keyword l) data) r)))))
(eval-qual-2 qual data) ; => false
(eval-qual-2 qual2 data) ; => true
(eval-qual-2 qual3 data) ; => true
(eval-qual-2 qual3 data) ; => true
2 ответа
Вам не нужен или не нужен макрос для этого. Простая функция может обрабатывать данные, как это.
Макросы предназначены только для преобразования исходного кода - вы эффективно добавляете расширение компилятора при написании макроса.
Для преобразования данных просто используйте простую функцию.
Вот схема того, как вы могли бы это сделать:
(ns tst.demo.core
(:use demo.core tupelo.core tupelo.test)
(:require
[clojure.tools.reader.edn :as edn] ))
(def data {:Location "US-NY-Location1"
:Priority 3})
(def qual "(Location = \"US\")")
(dotest
(let-spy [
ast (spyx (edn/read-string qual))
ident-str (first ast)
ident-kw (keyword ident-str)
op (second ast)
data-val (last ast)
expr (list op (list ident-kw data) data-val)
result (eval expr)
]
))
и результаты:
----------------------------------
Clojure 1.9.0 Java 10.0.1
----------------------------------
(edn/read-string qual) => (Location = "US")
ast => (Location = "US")
ident-str => Location
ident-kw => :Location
op => =
data-val => "US"
expr => (= (:Location {:Location "US-NY-Location1", :Priority 3}) "US")
result => false
Обратите внимание, что вам все еще нужно исправить "США" часть местоположения, прежде чем он даст вам true
результат.
Документы для let-spy
здесь и здесь.
Обновить
Для вложенных выражений вы обычно хотите использовать postwalk.
И не забывайте CheatSheet Clojure!
Вот пример использования Instaparse для определения грамматики для критериев и анализа входных данных строки в синтаксическом дереве:
(def expr-parser
(p/parser
"<S> = SIMPLE | COMPLEX
SIMPLE = <'('> NAME <' '> OP <' '> VAL <')'>
COMPLEX = <'('> S <' '> BOOLOP <' '> S <')'>
<BOOLOP> = 'or' | 'and'
NAME = #'[A-Za-z]+'
VAL = #'[0-9]+' | #'\".+?\"' | 'true' | 'false'
OP = '=' | '>'"))
И функция для анализа, а затем перевода частей анализируемого дерева, для более легкой оценки позже:
(defn parse [s]
(pt/transform
{:NAME keyword
:OP (comp resolve symbol)
:VAL edn/read-string}
(expr-parser s)))
Некоторые примеры выходных данных:
(parse "(Location = \"US\")")
=> ([:SIMPLE :Location #'clojure.core/= "US"])
(parse "(((Location = \"US\") or (Priority > 2)) and (Active = true))")
=>
([:COMPLEX
[:COMPLEX [:SIMPLE :Location #'clojure.core/= "US"] "or" [:SIMPLE :Priority #'clojure.core/> 2]]
"and"
[:SIMPLE :Active #'clojure.core/= true]])
Затем функция для оценки критериев по карте, не используя eval
:
(defn evaluate [m expr]
(clojure.walk/postwalk
(fn [v]
(cond
(and (coll? v) (= :SIMPLE (first v)))
(let [[_ k f c] v]
(f (get m k) c))
(and (coll? v) (= :COMPLEX (first v)))
(let [[_ lhs op rhs] v]
(case op
"or" (or lhs rhs)
"and" (and lhs rhs)))
:else v))
(parse expr)))
(evaluate {:location "US"} "(location = \"US\")")
=> (true)
Это также работает для вложенных выражений:
(evaluate
{:distance 1 :location "MS"}
"((distance > 0) and ((location = \"US\") or ((distance = 1) and (location = \"MS\"))))")
=> (true)
Как я могу сделать это более надежным и более изящным в случае инвалидности
qual
строка.
Дополнительным преимуществом использования Instaparse (или аналогичного) является сообщение об ошибке "бесплатно". Ошибки Instaparse будут довольно печататься в REPL, но их также можно рассматривать как карты, содержащие особенности ошибок.
(defn parse [s]
(let [parsed (expr-parser s)]
(or (p/get-failure parsed) ;; check for failure
(pt/transform
{:NAME keyword
:OP (comp resolve symbol)
:VAL edn/read-string}
parsed))))
(parse "(distance > 2") ;; missing closing paren
=> Parse error at line 1, column 14:
(distance > 2
^
Expected:
")" (followed by end-of-string)
В целом такой подход должен быть более безопасным, чем eval
-в произвольных входных данных, пока ваша грамматика синтаксического анализатора относительно ограничена.