如何在Clojure中编写最短和最惯用的CLI计算器 [英] How to write a shortest and most idiomatic CLI calculator in Clojure

查看:177
本文介绍了如何在Clojure中编写最短和最惯用的CLI计算器的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我喜欢用计算器等小工具学习一门新的语言。



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



所以这里是我的代码:

 (defn pre-process [s] 
用r
(re-seq#\d + | [\ + \-\ * \ / lr]
(clojure.string / replace s #\(| \){(l)r})))

(defn calc-once [stk]
运算符栈,并将其应用到
顶部操作数堆栈中的两个数字
(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 eek 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
一个简单的计算器
[s]
(clean-stk
(reduce
(fn [stk item]
(let [item字符串项)
运算符#{'+' - '*'/}
prio {'+ 0;定义运算符优先级
' - 0
'* 1
'/ 1
'l -1
'r -1
'dummy -2}
add-to-num#(assoc%1:num(conj %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);它的数字
(add-to-num stk item)
(get operators item);它是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 stk))'l)
(recur(calc-once stk))
(assoc stk:opt(pop(:opt stk)))))
:else
print))$)
(apply(partial list {:num'():opt'(dummy)});;堆栈的基本结构
s))))

调用它后:

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

计算如下:

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

我认为我的代码可以通过


    <消除那些 cond




  1. 尝试使 {:num'():opt'()} 转换为更易于访问的数据
    结构


但是我不知道。



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



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



谢谢家伙帮忙我修改了我的代码,现在好像更好。但我仍然有一些问题:


  1. 我应该放一些较不通用的函数(如 add-to-num )转换为全局变量?有没有人发现有时在FP中命名一个函数是非常困难的?特别是对于那些非通用功能。

这里是我的新代码:

 (def prio 
{'+ 0;在此定义运算符优先级
' - 0
'* 1
'/ 1
'l -1
'r -1
'dummy -2})

(def运算符#{'+' - '*'/})

(defn pre-process [s]
用运算符分隔操作数,并用r替换(用l,)
(re-seq#\d + | [\ + \-\ * \ / lr]
(clojure.string / replace s#\(| \){(l)r})) )

(defn calc-once [stk]
从运算符堆栈中取一个运算符,并将其应用于
顶部操作数堆栈中的两个数字
(let [ opt(:opt stk)
num(:num stk)
tmp-num(pop(pop num))
tmp-opt(pop opt)
last-two-num (peek(pop))(peek num)]
last-opt(peek opt)]
(assoc stk
:num(conj tmp-num(apply(eva l最后一个选项)last-two-num))
:opt tmp-opt)))

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

(defn calc
一个简单的计算器
[s]
(process-stk
(reduce
(fn [stk item]
读取字符串项)
add-to-num#(assoc%1:num(conj(:num%1)%2))
add-to-opt#(assoc%1:opt ($)
最后一个prio#(get prio(peek(:opt%))]]
(cond
(数字?项目);它的编号是
(add-to-num stk item)
(获取操作符项);它是运算符
(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意外语法:item))))
(apply列表{:num'():opt'(dummy)});;堆栈的基本结构
s))
#(>(count(:opt%))1)
# (peek(:num%))))


解决方案

是我的解决方案,它不使用正则表达式或宏,而是使用分区 reduce 来解析它的解析逻辑。



一般的想法是将用户输入视为初始值后的符号对序列。因此,您的算术表达式基本上是'(< init-value>(op1 value1)(op2 value2)...(opN valueN))当然,< init-value> 本身可以是一个括号,如果这样也必须减少。



partition 然后将符号/值对的序列提供给 reduce ,它们以优先级排列的符号构造一个有效的Clojure表达式。我停止对无效符号的评估(任何不是数字列表或符号),退出 reduce 块,方便的减少 (加入1.5)。



一个重要的概念是遇到的任何列表(括号)最终都会减少到值,递归地递归 reduce d。函数 peel 处理嵌套列表,即(((1 + 1)))



它有点冗长(我喜欢描述性变量名称),但它是正确的。我检查了几个相当复杂的嵌套表达式对Google。

 (def instructions 
str请输入用空格分隔的算术表达式。\\
即1 + 2/3 * 4))

(defn- error
(错误指令))
([msg](strERROR:(if(nil?msg)
指令
msg))))

^ {:private true}运算符{'* 1
'/ 1
'+ 0
' - 0})
(def ^ {:private true} (键操作员)))

(defn-更高优先?[leftop rightop]
(<(运算符leftop)(运算符rightop)))

(声明parse-expr)

(defn- peel
删除所有外部列表,直到达到包含多个值的
a列表。
[expr]
(if(and(list?expr)(= 1(count expr)))
(recur(first expr))
expr))

(defn-读取值[e]
(if(list? e)
(parse-expr(peel e))
(if(number?e)e)))

(defn- valid-expr?[op right]
(和(operator?op)
(或(number?right)(list?right))))

(defn- higher-precedence-concat [left op right]
(let [right-value(read-value right))
last-left-value(最后一个)
other-left-values(最后一个左)]
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)
(如果(较高优先级?(左前)op)
(高优先级左左对齐)
(列表左侧(读 - ue右)))
(reduce nil)))
(左侧列表左)(其余exprs))))))

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

示例对于谷歌的在线计算器

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

两个限制:表达式必须是空格分隔的(即 1 + 3 不支持)就像Incanter的中缀数学,并且因为我使用读取字符串,用户输入可以具有尾部括号(我认为这是一个错误,我必须用更强大的REPL实现来解决)。



学分:我使用了 Eric Robert的C编程抽象(Addison Wesley,1997)作为编码上述的参考。第14章表达树描述了一个几乎相同的问题。


I like to learn a new language by making small tool like calculator.

Although I already searched a lot idiomatic examples about specific cases(such as idiomatic usage of array and list), I have no idea how to put those together to write this small calculator in an idiomatic way.

So here is my code:

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

After calling it:

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

It can calculate like:

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

I think my code could be improved by

  1. eliminating those cond

    or

  2. try to make the {:num '() :opt '()} into a more accessible data structure

, but I have no idea.

Hopefully someone can give me some suggestions or point out problems with my code (or the grammers of my question :P).

====================================Thank you :)================================

Thank you guys for help. I modified my code, it seems better now. But I still have some questions:

  1. Should I put some less generic functions (such as add-to-num) into global var?
  2. Does anybody discover that sometimes naming a function in FP is pretty hard? Especially for those non-generic functions.

And here is my new code:

(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 %))))

解决方案

Here is my solution, which does not use regex or macros, and which instead uses partition and reduce for its parsing logic.

The general idea is that you treat the user input as a sequence of symbol pairs after the initial value. So your arithmetic expression is essentially '(<init-value> (op1 value1) (op2 value2) ...(opN valueN)) Of course, the <init-value> could itself be a parenthetical, and if so must first be reduced as well.

partition then provides the sequence of symbol/value pairs to reduce, which constructs a valid Clojure expression with symbols arranged by precedence. I halt evaluation on invalid symbols (anything not a number list or symbol), exiting the reduce block with the handy reduced (added in 1.5).

An important concept is that any lists (parenthesis) encountered ultimately reduce to values, and so are recursively reduce-d. The function peel handles nested lists, i.e. (((1 + 1)))

It a little verbose (I prefer descriptive variable names), but it's correct. I checked several rather complex nested expressions against Google.

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

Example checked against google's online calculator:

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

Two limitations: expressions must be space delimited (i.e. 1+2-3 not supported) just like Incanter's infix mathematics, and because I use read-string the user input can have trailing parens (I consider this a bug I'll have to fix with a more robust REPL implementation).

Credits: I used Eric Robert's Programming Abstractions in C (Addison Wesley, 1997) as a reference in coding the above. Chapter 14 "Expression Trees" describes an almost identical problem.

这篇关于如何在Clojure中编写最短和最惯用的CLI计算器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆