2

我喜欢通过制作像计算器这样的小工具来学习一门新语言。

虽然我已经搜索了很多关于特定案例的惯用示例(例如数组和列表的惯用用法),但我不知道如何将它们放在一起以惯用的方式编写这个小计算器。

所以这是我的代码:

(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 '()}变成更易于访问的数据结构

,但我不知道。

希望有人可以给我一些建议或指出我的代码(或我的问题的语法: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 %))))
4

5 回答 5

2

这是 M Smith 解决方案的正确版本,尽管我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
于 2013-05-05T10:01:27.630 回答
2

这需要一个宏观解决方案,如下所示。我确实作弊了,因为只有 2 个优先级,所以我不必计算堆栈来跟踪优先级。这个解决方案可以推广,但需要做更多的工作。

要记住 clojure 中宏的技巧是它们采用 clojure 结构(它是列表的嵌套列表)并返回不同的列表列表。宏简单地接受输入,将其calc包装在括号中并将其传递给 clojure 阅读器,该阅读器完成将输入字符串解析为符号列表的所有繁重工作。

然后 reorder-equation 函数将中缀转换为前缀顺序列表。该列表由宏返回,然后作为 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)))
于 2013-04-22T19:32:54.493 回答
1

这是我的解决方案,它不使用正则表达式或宏,而是使用partitionandreduce作为其解析逻辑。

一般的想法是您将用户输入视为初始值之后的符号对序列。所以你的算术表达式本质上是'(<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不支持),就像 Incanter 的中缀数学一样,并且因为我使用read-string用户输入可以有尾随括号(我认为这是一个错误,我必须使用更强大的 REPL 实现来修复)。

致谢:我使用Eric Robert 的Programming Abstractions in C(Addison Wesley,1997 年)作为上述编码的参考。第 14 章“表达式树”描述了一个几乎相同的问题。

于 2013-04-30T05:06:35.833 回答
1

我会尝试一下,但我无法让你的代码工作,所以我很难理解每个地方发生了什么。基本上,以下是猜测,并非完整的答案。希望有人可以进来并对其进行一些编辑并使其正常运行。

我将从基本前提开始:在我看来,您可以使用许多嵌套和匿名函数。你在任何地方看到#(xyz) 都可能被拉出到它自己的函数中。我很确定在任何编程语言中,函数内部的函数都是非常糟糕的形式,我觉得这里的形式很糟糕。我首先删除了匿名函数,包括散列函数和原始代码中的 (fn)。

我也不喜欢在我的 let-bindings 中嵌套函数。

(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) 非常模棱两可,我通常不喜欢使用它。从备忘单:

对于列表或队列,与 first 相同,对于向量,与 last 相同,但效率更高。如果集合为空,则返回 nil。

由于我不完全确定您始终使用什么结构(我认为它是 vec?)并且您确实这样做了,因此您可能想使用最后一个或第一个,哪个更合适。虽然它比上一个“效率更高”,但它并不能帮助我理解程序是如何工作的,所以在成品中使用 peek 而不是共享产品(请注意,你也不需要超速)。

我还认为 (cond) 应该明确地进行案例测试。

我试图通过确保 args 不那么模棱两可来使它更“惯用”。在您的原始代码中,您将大量函数(以及嵌套函数的结果)作为一个大参数传递给另一个函数。将所有这些分解为更小的功能是您需要做更多工作的地方。注意 calc 函数中发生的事情是如何更清楚的?

我把 calc 里面的 anon 函数取出来,输入了一个叫做 pre-calc 的函数。我仍然建议从 calc 中提取 anon 函数,并努力澄清 pre-calc 内部发生的事情。它仍然很难阅读,因为我真的无法猜测发生了什么。

我建议从以下内容开始,因为很难看到传入的 args 是什么(减少)。您可以看到这是多么令人困惑,因为我将 item 作为参数传递,然后我遵循您的模式并将 item 传递到 (read-string),然后我将该结果绑定到 item。我不确定这是否是你的意图,但我肯定不会传入一个名为 let 的 arg,它们会将传递它的结果绑定到通过评估 item 创建的函数中。这给我造成了进一步的困惑,因为您已将 item 传递到 let-bound item-prio。我从来没有这样做过,所以我什至不知道这里是否正在评估 arg 项目或 let-bound 项目。

这是代码的那一部分。请注意现在很容易看到正在减少的内容?

(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。

正如我所说,我希望其他人可以对此进行扩展或将其编辑为更可口的内容。

于 2013-04-22T10:15:30.197 回答
-1

最小的惯用计算器是 REPL!

如果中缀表示法是目标,我会去更改阅读器,以便数字成为算术函数 *、/、+、-、% 等的函数,因此 (7 + 5) 将被读取为 7,作为 Clojure函数(除了作为 java.lang.Number 之外),可以将 + 5 作为参数,类似于在 Smalltalk 中数字可以将算术运算理解为消息的方式。

于 2013-04-23T04:32:48.910 回答