Clojure: 實現簡單的數學表達式計算

我以前在知乎上回答了問題 按照運算符優先數法,畫出算術表達式求值時,操做數棧和運算符棧的變化過程 。此次一方面也算是溫故而知新,另外一方面藉此領略Clojure函數式編程之美。爲了節省篇幅,本文只考慮 +、-、*、/ 這幾種基本運算符以及只考慮小於10的整數運算。
java

棧變化圖示

咱們人類使用的算術表達式是中綴表達式,而計算機採用後綴表達式,也即逆波蘭式。第一種方式是將表達式轉爲後綴表達式,再進行求值;第二種方式是直接對中綴表達式進行求值。本文就直接採用第二種方式了。git

好比要對錶達式 9-2*4+9/3 進行求值。首先須要使用2個棧來分別保存運算數和運算符,分別記爲 opnd 和 optr。對錶達式逐家符讀入:github

一、讀入字符 '9',由於是運算數,則放入 opnd 棧中,剩餘待讀入字符串爲:-2*4+9/3,此時 opnd 棧和 optr 棧以下圖所示:shell


二、讀入字符 '-',發現是運算符,則先和 optr 棧頂做比較,由於 optr 棧是空棧,故直接入棧 optr。剩餘待讀入字符串爲:2*4+9/3,此時 opnd 棧和 optr 棧以下圖所示:編程


三、接下來讀入字符'2',發現是運算數,則入棧 opnd,剩餘待讀入字符串爲:*4+9/3,此時opnd 棧和 optr 棧以下圖所示:promise


四、接下來讀入字符 '*',發現是運算符,則跟 optr 棧頂字符 '-' 進行比較,'*' 優先級高於 '-',所以直接將 '*' 入棧 optr,剩餘待讀入字符串爲:4+9/3,此時 opnd 棧和 optr 棧以下圖所示:併發


五、接下來讀入字符 '4',由於是運算數,則直接入棧 opnd,剩餘待讀入字符串爲:+9/3,此時 opnd 棧和 optr 棧以下圖所示:編程語言


六、接下來讀入字符 '+',發現是運算符,則跟 optr 棧頂元素 '*' 進行比較,發現 '+' 的優先級小於 '*',則將 optr 出棧,獲得運算符 '*',將 opnd 出2次棧,獲得字符 '4' 和 '2',令 '4' 和 '2' 分別做爲運算符 '*' 的左右操做數(即 4 * 2)獲得結果爲 '8',將 '8' 入棧 opnd。剩餘待讀入字符爲:9/3,此時 opnd 棧和 optr 棧以下圖所示:函數式編程


七、此時 '+' 運算符還未入棧 optr,繼續與 optr 棧頂元素 '-' 進行比較,'+' 優先級不小於 '-',則出棧 optr 獲得運算符 '-',將 opnd 出2次棧,獲得字符 '9' 和 '8',令 '9' 和 '8' 分別做爲運算符 '-' 的左右操做數(即 9 - 8)獲得結果爲 '1',將 '1' 入棧 opnd,此時 optr 棧爲空,將 '+' 運算符入棧 optr,剩餘待讀入字符爲:9/3,此時 opnd 棧和 optr 棧以下圖所示:函數


八、讀入字符 '9',入棧 opnd,剩餘待讀入字符串爲:/3,此時 opnd 棧和 optr 棧以下圖所示:

九、讀入字符 '/',發現是運算符,跟 optr 棧頂 '+' 進行比較,由於 '/' 的優先級高於 '+',則直接將 '/' 入棧 optr,剩餘待讀入字符串爲:3,此時 opnd 棧和 optr 棧以下圖所示;


十、讀入字符 '3',入棧 opnd,此時表達式已所有讀取完成,此時 opnd 棧和 optr 棧以下圖所示:


十一、optr 棧不爲空,則出棧獲得運算符 '/',從 opnd 出棧2次,獲得 '9' 和 '3' 分別做爲左右操做數進行運算(即9/3),獲得結果爲 '3',併入棧 opnd。此時 opnd 棧和 optr 棧以下圖所示:


十二、optr 棧不爲空,則出棧 optr,獲得運算符 '+',從 opnd 出棧2次,獲得 '1' 和 '3' 分別做爲運算符 '+' 的左右操做數進行運算(即1+3),獲得結果 '4'併入棧 opnd,此時 opnd 棧和 optr 棧以下圖所示:


1三、此時 optr 棧已爲空,則將 opnd 出棧,獲得 '4',即爲表達式 9-2*4+9/3 的求值結果。

原理

一、首先需準備2個棧結構,分別用於保存操做數和運算符;

二、需對運算符的優先級進行劃分;

三、表達式從左到右逐字符讀入;

四、操做數直接入棧;

五、運算符入棧前需先進行斷定:若是運算符棧爲空,則直接入棧。不然取出棧頂,進行優先級比較,若是待讀入運算符比棧頂運算符優先級高,則直接入棧待讀入運算符。不然須先出棧,進行運算完後方入棧待讀入運算符;

六、當執行計算時,從操做數棧中連續出棧2次,先出棧的爲右操做數,後出棧的爲左操做數;

七、當表達式所有讀取完成,則以運算符棧是否爲空爲依據,依次出棧對操做數進行計算,計算的結果再次入棧操做數棧;

八、當運算符棧爲空時,出棧操做數棧,即爲這次表達式的計算結果。

實現

本文限於篇幅,只考慮的運算符爲:+、-、*、/,操做數爲小於10的整。因此無須處理複雜的運算符優先級關係,而且在讀入表達式時,只須對單個字符判斷爲操做數或運算符。

1、爲何是Clojure

clojure是基於JVM的函數式編程語言,它借簽了Haskell、Lisp等函數式語言的優勢,並造成本身的獨有風格。它也是一門動態語言:變量不可變,atom、ref、promise、future、delay使它天生適用於多核高併發開發任務。完備的宏定義、數據協議和類型、多重方法等賦予這門語言無限的可能。極盡JVM之所能,易於和java的互操做性,立於JVM的強大生態之上,又還有什麼理由不選擇Clojure呢?

2、安裝Clojure

Clojure是基於JVM的語言,因此你應該已經安裝好JVM環境了。

wget https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein
mv lein ~/bin/
chmod a+x ~/bin/lein

lein腳本已經處於你的PATH之中,如若否則,請在 ~/.profile 裏添加:

export PATH=~/.profile:$PATH

而後執行:

source ~/.profile

第一次執行 lein 會進行自我安裝,安裝完畢便可經過 lein repl 進入交互式開發環境。

咱們經過 lein new 來建立項目:

lein new calc

3、代碼實現

棧直接用java提供的 java.util.Stack 類,源代碼在 src/calc/core.clj 中,以下:

(ns calc.core
  (:import [java.util Stack]))

; 運算符及優先級定義
(def ^:private op_map {\+ 5, \- 5, \* 6, \/ 6})

(defn isdigit? [c] (#{\0 \1 \2 \3 \4 \5 \6 \7 \8 \9} c))

(defn -main [& args]
  (let [_expr (seq (nth args 0))]
    (loop [expr _expr opnd (Stack.) optr (Stack.)]
      (if (empty? expr)
        (do
          ;(println opnd optr)
          (loop [not_ept (not (empty? optr))]
            (if (not not_ept) (println (str "計算結果爲:" (.. opnd (pop))))
              (do
                (let [op_c (.. optr (pop)) rn (.. opnd (pop)) ln (.. opnd (pop))
                      v (case op_c
                          \+ (+ ln rn)
                          \- (- ln rn)
                          \* (* ln rn)
                          \/ (/ ln rn))]
                  ;(println op_c ln rn v opnd optr)
                  (.. opnd (push v))
                  (recur (not (empty? optr))))))))
        (let [[c & _rest] expr] (if (isdigit? c) (.. opnd (push (Integer. (str c))))
                                  (if (empty? optr) (.. optr (push c))
                                                        (loop [not_ept (not (empty? optr))]
                                                          (if (not not_ept)
                                                            (.. optr (push c))
                                                            (let [op_c (.. optr (peek))]
                                                              (if (> (op_map c) (op_map op_c)) (.. optr (push c))
                                                                (do
                                                                  (.. optr (pop))
                                                                  (let [rn (.. opnd (pop)) ln (.. opnd (pop))
                                                                        v (case op_c
                                                                            \+ (+ ln rn)
                                                                            \- (- ln rn)
                                                                            \* (* ln rn)
                                                                            \/ (/ ln rn))]
                                                                    (.. opnd (push v))
                                                                    ;(println ln rn v opnd optr)
                                                                    (recur (not (empty? optr)))))
                                                                ))))))
          (recur _rest opnd optr))))))

在 project.clj 中配置入口,增長一行:

:main calc.core/-main

在命令行中執行:

$ lein run 9-2*4+9/3
計算結果爲:4

此程序只是爲了演示,並不通用。寫慣了命令式程序,很容易被函數式語言弄得暈頭轉向。欲練神功,先自廢武功~

另:感謝 敏敏郡主 ^_^

相關文章
相關標籤/搜索