如何写Clojure中最短的,最地道的CLI计算器(How to write a shortest

2019-08-31 13:19发布

我想通过生成类似计算器的小工具来学习新的语言。

虽然我已经搜索了很多关于具体案件成语例子(如数组和列表的习惯用法),我不知道如何把这些一起写在习惯的方法这个小计算器。

因此,这里是我的代码:

(defn pre-process [s]
  "Seperate operands with operators and replace ( with l, ) with r"
  (re-seq #"\d+|[\+\-\*\/lr]" 
          (clojure.string/replace s #"\(|\)" {"(" "l" ")" "r"})))

(defn calc-once [stk] 
  "Take one operator from operator stack and apply it to 
  top two numbers in operand stack"
  (let [opt (:opt stk)
        num (:num stk)
        tmp-num (pop (pop num))
        tmp-opt (pop opt)
        last-two-num [(peek (pop num)) (peek num)]
        last-opt (peek opt)]
    (assoc stk 
           :num (conj tmp-num (apply (eval last-opt) last-two-num))
           :opt tmp-opt)))

(defn clean-stk [stk]
  (loop [stk stk]
    (if (> (count (:opt stk)) 1)
      (recur (calc-once stk))
      (peek (:num stk)))))

(defn calc
  "A simple calculator"
  [s]
  (clean-stk 
    (reduce
      (fn [stk item]
        (let [item (read-string item)
              operators #{'+ '- '* '/}
              prio {'+ 0 ; Define operator priority here
                    '- 0
                    '* 1
                    '/ 1
                    'l -1
                    'r -1
                    'dummy -2}
              add-to-num #(assoc %1 :num (conj (:num %1) %2))
              add-to-opt #(assoc %1 :opt (conj (:opt %1) %2))
              item-prio (get prio item)
              last-prio #(get prio (peek (:opt %)))]
          (cond
            (number? item) ; It's number
            (add-to-num stk item)
            (get operators item) ; It's operator
            (loop [stk stk]
              (if (<= item-prio (last-prio stk))
                (recur (calc-once stk))
                (add-to-opt stk item)))
            (= 'l item) ; (
            (add-to-opt stk item)
            (= 'r item) ; )
            (loop [stk stk]
              (if (not= (peek (:opt stk)) 'l)
                (recur (calc-once stk))
                (assoc stk :opt (pop (:opt stk)))))
            :else
            (println "Unexpected syntax: " item))))
        (apply (partial list {:num '() :opt '(dummy)}) ;; Basic structure of stack
               s))))

调用它之后:

(calc (pre-process (read-line))))

它可以计算,如:

(1 + 3) * ( 4 + 4)
32

我觉得我的代码可以通过改善

  1. 消除这些cond

    要么

  2. 尽量使{:num '() :opt '()}成更易于访问的数据结构

,但我不知道。

希望有人可以给我一些建议或指出我的代码(或我的问题的grammers:P)问题。

====================================谢谢:)========== ======================

谢谢你们的帮助。 我修改了代码,现在看来更好。 但我仍然有一些问题:

  1. 我应该少放一些通用的功能(如add-to-num )为全局变量?
  2. 有没有人发现,有时在命名FP的功能是相当困难? 特别是对那些非通用功能。

这里是我的新代码:

(def prio 
  {'+ 0 ; Define operator priority here
   '- 0
   '* 1
   '/ 1
   'l -1
   'r -1
   'dummy -2})

(def operators #{'+ '- '* '/})

(defn pre-process [s]
  "Seperate operands with operators and replace ( with l, ) with r"
  (re-seq #"\d+|[\+\-\*\/lr]" 
          (clojure.string/replace s #"\(|\)" {"(" "l" ")" "r"})))

(defn calc-once [stk] 
  "Take one operator from operator stack and apply it to 
  top two numbers in operand stack"
  (let [opt (:opt stk)
        num (:num stk)
        tmp-num (pop (pop num))
        tmp-opt (pop opt)
        last-two-num [(peek (pop num)) (peek num)]
        last-opt (peek opt)]
    (assoc stk 
           :num (conj tmp-num (apply (eval last-opt) last-two-num))
           :opt tmp-opt)))

(defn process-stk [stk checker fn-ret]
  (loop [stk stk]
    (if (checker stk)
      (recur (calc-once stk))
      (fn-ret stk))))

(defn calc
  "A simple calculator"
  [s]
  (process-stk 
    (reduce
      (fn [stk item]
        (let [item (read-string item)
              add-to-num #(assoc %1 :num (conj (:num %1) %2))
              add-to-opt #(assoc %1 :opt (conj (:opt %1) %2))
              item-prio (get prio item)
              last-prio #(get prio (peek (:opt %)))]
          (cond
            (number? item) ; It's number
            (add-to-num stk item)
            (get operators item) ; It's operator
            (process-stk stk #(<= item-prio (last-prio %))
                         #(add-to-opt % item)) 
            (= 'l item) ; (
            (add-to-opt stk item)
            (= 'r item) ; )
            (process-stk stk #(not= (peek (:opt %)) 'l)
                           #(assoc % :opt (pop (:opt %))))
            :else
            (println "Unexpected syntax: " item))))
        (apply (partial list {:num '() :opt '(dummy)}) ;; Basic structure of stack
               s))
    #(> (count (:opt %)) 1)
    #(peek (:num %))))

Answer 1:

这里是我的解决方案,不使用正则表达式或宏,并而是使用partition ,并reduce其分析逻辑。

总的想法是,你把用户输入的符号对初始值后的序列。 所以,你的算术表达式本质上是'(<init-value> (op1 value1) (op2 value2) ...(opN valueN))当然, <init-value>本身可能是一个括号,如果是的话,首先必须减少。

partition随后提供符号/值对的序列,以reduce ,它构造与由优先级排列的符号的有效的Clojure表达。 我停止对无效码元(任何不是一个号码列表或符号)的评价中,离开reduce与块手持reduced (在1.5中加入)。

一个重要的概念是,任何名单(括号内)遇到最终减少到值,因此是递归reduce -d。 功能peel处理嵌套列表,即(((1 + 1)))

这有点冗长(我喜欢描述性的变量名),但它是正确的。 我查了一些相当复杂的嵌套表达式对抗谷歌。

(def instructions
  (str "Please enter an arithmetic expression separated by spaces.\n"
       "i.e. 1 + 2 / 3 * 4"))

(defn- error
  ([]    (error instructions))
  ([msg] (str "ERROR: " (if (nil? msg) 
                         instructions 
                         msg))))

(def ^{:private true} operators {'* 1
                                 '/ 1
                                 '+ 0
                                 '- 0})
(def ^{:private true} operator? (set (keys operators)))

(defn- higher-precedence? [leftop rightop]
  (< (operators leftop) (operators rightop)))

(declare parse-expr)

(defn- peel
  "Remove all outer lists until you reach
   a list that contains more than one value." 
  [expr]
  (if (and (list? expr) (= 1 (count expr)))
    (recur (first expr))
    expr))

(defn- read-value [e]
  (if (list? e)
    (parse-expr (peel e))
    (if (number? e) e)))

(defn- valid-expr? [op right]
  (and (operator? op) 
       (or (number? right) (list? right))))

(defn- higher-precedence-concat  [left op right]
  (let [right-value (read-value right)
        last-left-value (last left)
        other-left-values (drop-last left)]
    (concat other-left-values `((~op ~last-left-value ~right-value)))))

(defn- parse-expr [s]
  (let [left             (read-value (first s))
        exprs            (partition 2 (rest s))
        [[op right] & _] exprs]
    (if (and left (valid-expr? op left))
      (let [right (read-value right)]
        (reduce (fn [left [op right]]
                  (if (valid-expr? op right)
                    (if (higher-precedence? (first left) op)
                      (higher-precedence-concat left op right) 
                      (list op left (read-value right)))
                    (reduced nil)))
          (list op left right) (rest exprs))))))

(defn calc [input]
  (try 
    (let [expr (-> (str "(" input ")") 
                   read-string ;; TODO: use tools.reader?
                   peel)]
      (if (list? expr)  
        (if-let [result (eval (parse-expr expr))]
          result
          (error))
        (error)))
  (catch java.lang.RuntimeException ex
    (error (.getMessage ex)))))

例如检查对谷歌的在线计算器 :

(calc "10 + 2 * 100 / ((40 - 37) * 100 * (2 - 4 + 8 * 16))")
=> 1891/189
(double *1)
=> 10.00529100529101

两个限制:表达式必须是空间分隔(即1+2-3不支持) 就像魔咒师缀数学 ,因为我用read-string用户输入可以有尾随括号(我认为这是我不得不修复一个bug具有更强大的REPL执行)。

积分:我用埃里克·罗伯茨的编程抽象用C (艾迪生韦斯利,1997年),如编码上述的参考。 第14章“表达式树”描述了一个几乎相同的问题。



Answer 2:

这十分需要宏观的解决方案,下面给出。 我没有欺骗在只有2个优先级别,所以我并没有制定出一个堆栈跟踪优先。 该解决方案可以推广,但它需要多一点这样做。

要记住用Clojure宏的诀窍是他们采取的Clojure结构(这是清单的嵌套列表),并返回序列的不同列表。 该calc的宏简单地获取输入,把它封装在括号和把它传递给Clojure的读取器不解析输入字符串转换成符号的列表的所有繁重。

然后,重新排序方程功能将缀成一个前缀顺序列表。 这份名单是由宏返回,然后被评估为Clojure的代码。

对于*和/的检查使他们可以优先评估。 要了解它并尝试

(reorder-equation '((1 + 3) * (4 + 4)))
 =>   (* (+ 1 3) (+ 4 4))

正如你可以看到它采用的公式,它改写成随后将进行评估一个有效的Clojure表达。

这似乎是欺骗但是当你更熟悉的Clojure你会发现,你可以让语言做了很多繁重的工作。 解析输入的符号列表,并使用这些符号功能名称完全合理。 作为事实上,这两个参数的任何功能是在我们的计算器有效:

(calc "(1 + 3) < (4 + 4)")
=> true

(calc "(1 + 3) str (4 + 4)")
=> "48"

代码:

(defn reorder-equation [ arg ]
  (if (seq? arg)
    (let [[f s & r] arg
          f (reorder-equation f)]
      (cond
        (#{"*" "/"} (str s)) ( let [[t ft & r2 ] r
                                    t (reorder-equation t)]
                               (if ft
                                 (list ft (list s f t) (reorder-equation r2))
                                 (list s f t)))
        (nil? s) f
        :else (list s f (reorder-equation r))))
    arg))




(defmacro calc [inp] 
  (let [tr (read-string (str "(" inp ")"))]
    (reorder-equation tr)))


Answer 3:

它是M·史密斯的解决方案的正确版本,虽然我用eval在我的代码,这通常是一个坏主意。 我在这里贴吧,希望它可以帮助别人。

(defn calc [ arg ]
  (if (seq? arg)
    (let [[f s & r] arg
          f (calc f)]
      (if (nil? s) 
        f
        (let [[t ft & r2 ] r
              t (calc t)
              new-f ((resolve s) f t)]
          (cond
            (#{"*" "/"} (str s)) 
            (if ft
              (calc (concat `(~new-f) (rest r)))
              new-f)

            (nil? s) f

            :else 
            (if (#{"+" "/"} (str ft))
              (calc (concat `(~new-f) (rest r)))
              ((resolve s) f (calc r)))))))
    arg))

(defn main [inp]
  (let [tr (read-string (str "(" inp ")"))]
    (calc tr)))

例:

(println (main "2 - 4 + 8 * 16"))
(println (main "(1 + 2) * (10 - 4) / 9 * 6"))
(println (main "10 + 2 * 100 / ((40 - 37) * 100 * (2 - 4 + 8 * 16))"))

结果:

126
12
1891/189


Answer 4:

我会尝试一下,但我不能让你的代码工作,所以这是一个有点我很难理解什么是在每一个地方发生的事情。 基本上,下面是一个猜测,并非是一个完整的答案。 希望有人能进来,编辑这个倒有几分得到它才能正常工作。

我的基本前提开始:你有,在我看来,办法很多嵌套和匿名函数。 不论你看到一个#(XYZ)很可能被划到了其自身的功能。 我敢肯定具有函数内部函数的函数内部将任何编程语言非常糟糕的形式,我觉得这是不好的形式在这里。 我开始通过消除匿名函数,既散列和(FN)你有你的原代码。

我也不喜欢在我松懈绑定嵌套功能。

(def prio 
  {'+ 0 ; Define operator priority here
   '- 0
   '* 1
   '/ 1
   'l -1
   'r -1
   'dummy -2})

(def operators #{'+ '- '* '/})

(defn pre-process [s]
  "Seperate operands with operators and replace ( with l, ) with r"
  (re-seq #"\d+|[\+\-\*\/lr]" 
          (clojure.string/replace s #"\(|\)" {"(" "l" ")" "r"})))

(defn calc-once [stk] 
  "Take one operator from operator stack and apply it to 
  top two numbers in operand stack"
  (let [opt (:opt stk)
        num (:num stk)
        tmp-num (pop (pop num))
        tmp-opt (pop opt)
        last-two-num [(peek (pop num)) (peek num)]
        last-opt (peek opt)]
    (assoc stk 
           :num (conj tmp-num (apply (eval last-opt) last-two-num))
           :opt tmp-opt)))

(defn process-stk [stk checker fn-ret]
  (loop [stk stk]
    (if (checker stk)
      (recur (calc-once stk))
      (fn-ret stk))))

(defn assoc-to-item [item]
  #(assoc %1 item (conj (item %1) %2)))

(defn priority [item]
  (get prio item))

(defn create-checker [op item v]
  (op item v))

(defn pre-calc [stk item s]
  (reduce
   (let [item (read-string item)
         add-to-num (assoc-to-item :num)
         add-to-opt (assoc-to-item :opt)
         item-prio (priority item)
         last-prio (priority (last (:opt)))]
     (cond
      (number? item) ; It's number
      (add-to-num stk item)

      (get operators item) ; It's operator
      (process-stk stk
                   (create-checker <= item-prio (last-prio))
                   add-to-opt) 

      (= 'l item) ; (
      (add-to-opt stk item)

      (= 'r item) ; )
      (process-stk stk
                   (create-checker not= (peek (:opt)) 'l)
                   #(assoc % :opt (pop (:opt %))))
      :else
      (println "Unexpected syntax: " item))))
  (apply (partial list {:num '() :opt '(dummy)}) ;; Basic structure of stack
         s))

(defn calc [s]
  "A simple calculator"
  (process-stk (pre-calc stk item s)
               #(> (count (:opt %)) 1)
               #(peek (:num %))))

其它说明:

(PEEK)是非常模糊的,我一般不喜欢使用它。 从备忘 :

对于数组或队列一样,同样首先,对于一个矢量,一样,但要比更有效率,最后一次。 如果集合为空,则返回零。

因为我不能完全确定你在任何时候工作什么结构(我认为它的A VEC?),你这样做,你可能希望使用最后一个或第一,这是以往任何时候都更合适。 虽然比去年“更有效”,这不是帮助我理解程序是如何工作的,所以在成品中使用偷看而不是共享的产品(头脑,你并不真的需要超高速这两种)。

我也觉得(条件)应该是明确的情况下测试。

我试图通过确保ARG游戏更加明确,使其一点点更加“地道”。 在你的原代码,你传递大量的功能(和嵌套函数的结果)为一体的大型参数另一个函数。 打破了这一切到更小的功能,在这里你需要工作多一点。 注意它是如何更清楚什么是在计算功能发生了什么?

我掏出匿名函数内部计算,并进入到一个名为预计算功能。 我还是建议从钙拉出不久的功能和澄清什么是预先计算的内部发生的工作。 它仍然是难以阅读,因为我真的不能猜测发生了什么。

我建议先从像下面这样,因为它是很难看到传递什么ARGS到(减少)。 你可以看到这是如何混乱,因为我传递作为一个参数,然后我按照你的图案并通过项目到项目(读字符串),然后我绑定的结果项。 我不知道如果这是你的意图,但我肯定不会在ARG调用放过并将它们传递到它通过评估的项目创建一个函数的结果绑定。 这对我造成进一步的混乱,因为你已经该项目已通过成让结合的项目,PRIO。 我从来没有这么做过,所以我甚至不知道如果ARG项目或让结合的项目正在这里进行评估。

下面是代码的一部分。 注意它是如何容易看到现在正在减少?

(defn stack-binding [item]
  (let [item (read-string item)
        add-to-num (assoc-to-item :num)
        add-to-opt (assoc-to-item :opt)
        item-prio (priority item)
        last-prio (priority (last (:opt)))]
    (cond
     (number? item) ; It's number
     (add-to-num stk item)

     (get operators item) ; It's operator
     (process-stk stk
                  (create-checker <= item-prio (last-prio))
                  add-to-opt) 

     (= 'l item) ; (
     (add-to-opt stk item)

     (= 'r item) ; )
     (process-stk stk
                  (create-checker not= (peek (:opt)) 'l)
                  #(assoc % :opt (pop (:opt %))))
     :else
     (println "Unexpected syntax: " item))))

(defn pre-calc [stk item s]
  (reduce (stack-binding item)
          (apply (partial list {:num '() :opt '(dummy)}) ;; Basic structure of stack
                 s))

还有很多我可以写,但正如我所说,我真的不知道如何一切工作在一起。 无论如何,这至少应该表现出一些我会在创造这个程序中使用的逻辑。 我会尝试推广这一多了很多,并保持它使每一个功能是每个大约只有10 LOC。

正如我所说的,我希望其他人可以在此展开或编辑它的东西更加可口。



Answer 5:

最小的惯用计算器是REPL!

如果中缀表示法是我们的​​目标,我会去改变阅读器,以使数字成为算术函数功能*,/,+, - ,%等,所以(7 + 5)将被解读为7,是一个Clojure的功能(除了是一个java.lang.Number中),可以采取+ 5作为参数,类似于如何,在Smalltalk,数字可以理解算术运算作为消息。



文章来源: How to write a shortest and most idiomatic CLI calculator in Clojure