postwalk 评估算术表达式

postwalk to evaluate arithmetic expression

我正在尝试使用 Instaparse 制作一个简单的算术表达式求值器。解析器似乎工作正常,但我不知道如何评估返回的嵌套向量。目前我正在使用postwalk,像这样

(ns test5.core
  (:require [instaparse.core :as insta])
  (:require [clojure.walk  :refer [postwalk]])
  (:gen-class))


(def WS
  (insta/parser
    "WS = #'\s+'"))


(def transform-options
  {:IntLiteral read-string})


(def parser
  (insta/parser
    "AddExpr = AddExpr '+' MultExpr
      | AddExpr '-' MultExpr
      | MultExpr

     MultExpr = MultExpr '*' IntLiteral
      | MultExpr '/' IntLiteral
      | IntLiteral

     IntLiteral = #'[0-9]+'"

    :auto-whitespace WS))


(defn parse[input]
  (->> (parser input)
       (insta/transform transform-options)))


(defn visit [node]
  (println node)
  (cond
    (number? node) node
    (string? node) (resolve (symbol node))
    (vector? node)
      (cond
        (= :MultExpr (first node)) (visit (rest node))
        (= :AddExpr (first node)) (visit (rest node))
        :else node)
    :else node))


(defn evaluate [tree]
  (println tree)
  (postwalk visit tree))


(defn -main
  [& args]
  (evaluate (parse "1 * 2 + 3")))

postwalk 确实遍历了向量,但我得到了一个嵌套列表作为结果,例如

((((1) #'clojure.core/* 2)) #'clojure.core/+ (3))

使用org.clojure/core.match。根据您当前的语法,您可以将评估函数编写为:

(defn eval-expr [expr]
  (match expr
         [:MultExpr e1 "*" e2] (* (eval-expr e1)
                                  (eval-expr e2))
         [:MultExpr e1 "/" e2] (/ (eval-expr e1)
                                  (eval-expr e2))
         [:AddExpr e1 "+" e2] (+ (eval-expr e1)
                                 (eval-expr e2))
         [:AddExpr e1 "-" e2] (- (eval-expr e1)
                                 (eval-expr e2))
         [:MultExpr e1] (eval-expr e1)
         [:AddExpr e1] (eval-expr e1)
         :else expr))

并评估:

(-> "1 * 2 + 3"
    parse
    eval-expr)
;; => 5

这个确切的问题就是我首先创建 Tupelo Forest 库的原因。

请参阅talk from Clojure Conj 2017

我开始了some docs here. You can also see live examples here


更新

以下是使用 Tupelo Forest 库的方法:

首先,使用 Hiccup 格式定义抽象语法树 (AST) 数据:

  (with-forest (new-forest)
    (let [data-hiccup      [:rpc
                            [:fn {:type :+}
                             [:value 2]
                             [:value 3]]]
          root-hid         (add-tree-hiccup data-hiccup)

结果:

(hid->bush root-hid) => 
[{:tag :rpc}
 [{:type :+, :tag :fn}
  [{:tag :value, :value 2}]
  [{:tag :value, :value 3}]]]

显示 walk-tree 如何使用 "display interceptor"

  disp-interceptor {:leave (fn [path]
                             (let [curr-hid  (xlast path)
                                   curr-node (hid->node curr-hid)]
                               (spyx curr-node)))}
  >>        (do
              (println "Display walk-tree processing:")
              (walk-tree root-hid disp-interceptor))

结果:

Display walk-tree processing:
curr-node => {:tupelo.forest/khids [], :tag :value, :value 2}
curr-node => {:tupelo.forest/khids [], :tag :value, :value 3}
curr-node => {:tupelo.forest/khids [1037 1038], :type :+, :tag :fn}
curr-node => {:tupelo.forest/khids [1039], :tag :rpc}

然后定义运算符和拦截器来转换子树,例如 (+ 2 3) => 5

  op->fn           {:+ +
                    :* *}
  math-interceptor {:leave (fn [path]
                             (let [curr-hid  (xlast path)
                                   curr-node (hid->node curr-hid)
                                   curr-tag  (grab :tag curr-node)]
                               (when (= :fn curr-tag)
                                 (let [curr-op    (grab :type curr-node)
                                       curr-fn    (grab curr-op op->fn)
                                       kid-hids   (hid->kids curr-hid)
                                       kid-values (mapv hid->value kid-hids)                                           
                                       result-val (apply curr-fn kid-values)]
                                   (set-node curr-hid {:tag :value :value result-val} [])))))}  
  ]  ; end of let form

; imperative step replaces old nodes with result of math op
(walk-tree root-hid math-interceptor)

然后我们可以显示包含 (+ 2 3):

结果的修改后的 AST 树
(hid->bush root-hid) => 
[{:tag :rpc} 
  [{:tag :value, :value 5}]]

你可以see the live code here.

这不使用 Instaparse 或 clojure.walk,但这是我仅使用 reduce:

评估中缀数学的东西
(defn evaluate
  "Evaluates an infix arithmetic form e.g. (1 + 1 * 2)."
  [e]
  (let [eval-op (fn [op a b]
                  (let [f (resolve op)]
                    (f a b)))]
    (reduce
      (fn [[v op] elem]
        (cond
          (coll? elem)
          (if op
            [(eval-op op v (first (evaluate elem))) nil]
            [(first (evaluate elem)) nil])

          (and op (number? elem))
          [(eval-op op v elem) nil]

          (number? elem)
          [elem nil]

          (symbol? elem)
          [v elem]

          :else
          (throw (ex-info "Invalid evaluation" {:v v :op op :elem (type elem)}))))
      [0 nil]
      e)))

(first (evaluate (clojure.edn/read-string "(1 * 2 + 3)")))
=> 5
(first (evaluate (clojure.edn/read-string "(1 * 2 + (3 * 5))")))
=> 17

这需要输入字符串表示有效的 Clojure 列表。我也有这个分组功能 multiplication/division:

(defn pemdas
  "Groups division/multiplication operations in e into lists."
  [e]
  (loop [out []
         rem e]
    (if (empty? rem)
      (seq out)
      (let [curr (first rem)
            next' (second rem)]
        (if (contains? #{'/ '*} next')
          (recur (conj out (list curr next' (nth rem 2)))
                 (drop 3 rem))
          (recur (conj out curr) (rest rem)))))))

(pemdas '(9.87 + 4 / 3 * 0.41))
=> (9.87 + (4 / 3) * 0.41)