我使用instaparse来解析最终用户使用的简单查询语言,该语言评估为布尔结果,例如“(AGE> 35)AND(GENDER =”MALE“)”,然后需要应用此查询到数千行数据来决定每行是否符合表达式。
我的问题是将instaparse的输出转换为随后针对每一行进行求值的函数的最佳方法是什么?例如,上述查询将转换为类似
的内容[AGE GENDER](和(=年龄35)(=性别“男性”))
请注意我是Clojure noob ......
答案 0 :(得分:8)
您可以为查询语言编写一个小编译器,使用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+'>
"))
对于示例查询,这将生成以下分析树:
[:expr
[:and-expr
[:expr
[:pred [:atom [:symbol "AGE"]] [:rel ">"] [:atom [:number "35"]]]]
[:expr
[:pred
[:atom [:symbol "GENDER"]]
[:rel "="]
[:atom [:string "MALE"]]]]]]
我们现在可以编写一个多方法,在收集符号时将其转换为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))
让我们将它应用于我们的示例解析树:
(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}
最后,这是一个使用上面的函数来构建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))))
您可以像这样使用它:
(def valid? (compile-expr "(AGE > 35) AND (GENDER = \"MALE\")"))
(valid? {:gender "MALE" :age 36})
;= true
(valid? {:gender "FEMALE" :age 36})
;= false
答案 1 :(得分:1)
我不确定我理解你的问题。但是,这是我的猜测:)
您可以使用clojure的“过滤器”功能来重新集合集合。我不熟悉instaparse,所以我只是模拟一些测试数据(集合):
(def ls [{:age 10, :gender "m"} {:age 15 :gender "fm"}] )
所以“ls”是我们的收藏品。要将集合限制为性别值为“m”且年龄值大于5的元素,我们应用以下过滤器:
(filter (fn [a] (if (and (= (:gender a) "m") (> (:age a) 5)) a)) ls)
结果是:
({:age 10, :gender "m"})
或者您可以将过滤器“fn”表示为匿名函数:
(filter #(if (and (= (:gender %1) "m") (> (:age %1) 5)) %1) ls)
希望有所帮助。注意!如果您可以发布您尝试处理的“结果数据”样本,那么它很有帮助:)