Разбор строки с макросом

Я пытаюсь оценить вставленное выражение в строке.

Некоторые примеры данных для оценки моего кода:

(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) дает мне ожидаемый результат

Это первый макрос, который я написал, и я все еще пытаюсь обернуть голову в цитирование и снятие цитат.

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

  2. Как я могу расширить макрос, чтобы иметь дело с вложенными выражениями. Для обработки выражения, как ((Location = "US") or (Priority > 2)), Любые указатели будут оценены. В настоящее время я пытаюсь играть с tree-seq чтобы решить это.

  3. Как я могу сделать это более надежным и более изящным в случае инвалидности 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-в произвольных входных данных, пока ваша грамматика синтаксического анализатора относительно ограничена.

Другие вопросы по тегам