1

我正在尝试评估字符串中的中缀表达式。

一些用于评估我的代码的示例数据:

(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

我不知道如何调试它!

一些额外的信息:

macroexpandREPL 上的 parse-qual 给了我以下信息:

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
4

2 回答 2

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

而且,不要忘记Clojure 备忘单!

于 2018-08-29T20:29:07.070 回答
1

这是一个使用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总体而言,只要您的解析器语法相对有限,这种方法应该比-ing 任意输入更安全。

于 2018-08-30T01:18:41.177 回答