如何将instaparse输出转换为可以评估的函数?

Dav*_*lie 5 parsing clojure

我正在使用instaparse来解析最终用户使用的简单查询语言,该语言评估为布尔结果,例如"(年龄> 35)AND(GENDER ="MALE")",此查询需要应用于数千个用于确定每行是否符合表达式的数据行.

我的问题是将instaparse的输出转换为随后针对每一行进行求值的函数的最佳方法是什么?例如,上面的查询将转换为类似的东西

fn [AGE GENDER](和(=年龄35)(=性别"男性"))

请注意我是Clojure noob ...

Mic*_*zyk 9

您可以为查询语言编写一个小编译器,使用instaparse生成一个解析树,使用常规Clojure函数将其转换为Clojure代码,最后eval生成一个Clojure函数,然后将其应用于您的记录.

初始调用eval将有点昂贵,但结果函数将等同于在源文件中手动编写的函数,并且不会带来性能损失.事实上,这是一个罕见的有效用例之一eval- 生成一个函数,其代码以真正动态的方式构造,然后将被调用很多次.

显然,当遵循这种方法时,您需要确保您不会无意中允许不受信任的源执行任意代码.

为了演示,这里是一个基于非常简单的语法的instaparse解析器,它只能解析你的示例查询:

(def p (insta/parser "

expr = and-expr | pred
and-expr = <'('> expr <')'> ws? <'AND'> ws? <'('> expr <')'>
pred = (atom ws? rel ws? atom)
rel = '<' | '>' | '='
atom = symbol | number | string
symbol = #'[A-Z]+'
string = <'\"'> #'[A-Za-z0-9]+' <'\"'>
number = #'\\d+'
<ws> = <#'\\s+'>

"))
Run Code Online (Sandbox Code Playgroud)

对于示例查询,这将生成以下分析树:

[:expr
 [:and-expr
  [:expr
   [:pred [:atom [:symbol "AGE"]] [:rel ">"] [:atom [:number "35"]]]]
  [:expr
   [:pred
    [:atom [:symbol "GENDER"]]
    [:rel "="]
    [:atom [:string "MALE"]]]]]]
Run Code Online (Sandbox Code Playgroud)

我们现在可以编写一个多方法,在收集符号时将其转换为Clojure表达式; ctx这里的参数是一个原子,它包含迄今为止遇到的符号集:

(defmulti expr-to-sexp (fn [expr ctx] (first expr)))

(defmethod expr-to-sexp :symbol [[_ name] ctx]
  (let [name (clojure.string/lower-case name)
        sym  (symbol name)]
    (swap! ctx conj sym)
    sym))

(defmethod expr-to-sexp :string [[_ s] ctx]
  s)

(defmethod expr-to-sexp :number [[_ n] ctx]
  (Long/parseLong n))

(defmethod expr-to-sexp :atom [[_ a] ctx]
  (expr-to-sexp a ctx))

(defmethod expr-to-sexp :rel [[_ name] ctx]
  (symbol "clojure.core" name))

(defmethod expr-to-sexp :pred [[_ left rel right] ctx]
  (doall (map #(expr-to-sexp % ctx) [rel left right])))

(defmethod expr-to-sexp :and-expr [[_ left right] ctx]
  `(and ~(expr-to-sexp left ctx) ~(expr-to-sexp right ctx)))

(defmethod expr-to-sexp :expr [[_ child] ctx]
  (expr-to-sexp child ctx))
Run Code Online (Sandbox Code Playgroud)

让我们将它应用于我们的示例解析树:

(expr-to-sexp (p "(AGE > 35) AND (GENDER = \"MALE\")") (atom #{}))
;= (clojure.core/and (clojure.core/> age 35) (clojure.core/= gender "MALE"))

(let [ctx (atom #{})]
  (expr-to-sexp (p "(AGE > 35) AND (GENDER = \"MALE\")") ctx)
  @ctx)
;= #{age gender}
Run Code Online (Sandbox Code Playgroud)

最后,这是一个使用上面的函数来构建Clojure函数:

(defn compile-expr [expr-string]
  (let [expr (p expr-string)
        ctx  (atom #{})
        body (expr-to-sexp expr ctx)]
    (eval `(fn [{:keys ~(vec @ctx)}] ~body))))
Run Code Online (Sandbox Code Playgroud)

您可以像这样使用它:

(def valid? (compile-expr "(AGE > 35) AND (GENDER = \"MALE\")"))

(valid? {:gender "MALE" :age 36})
;= true

(valid? {:gender "FEMALE" :age 36})
;= false
Run Code Online (Sandbox Code Playgroud)