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
eliminating those
cond
or
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:
- Should I put some less generic functions (such as
add-to-num
) into global var? - 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.
This cries out for a macro solution, given below. I did cheat in that there are only 2 precedence levels so I didn't have to work out a stack to keep track of precedence. This solution could be generalized but it take a little more doing.
The trick to remember about macros in clojure is they take clojure structure (which is a nested list of lists) and return a different list of lists. The calc
macro simply takes the input, wraps it in parens and passes it to the clojure reader which does all the heavy lifting of parsing the input string into a list of symbols.
Then the reorder-equation function turns the infix into a prefix order list. That list is returned by the macro and is then evaluated as clojure code.
The check for * and / makes sure they get evaluated first. To see what it does try
(reorder-equation '((1 + 3) * (4 + 4)))
=> (* (+ 1 3) (+ 4 4))
As you can see it takes the equations and rewrites it into a valid clojure expression which will then be evaluated.
This may seem like cheating but as you get more familiar with Clojure you will realize that you can let the language do a lot of the heavy lifting. Parsing input into a list of symbols and using those symbols as function names make perfect sense. As a matter of fact, any function that takes two arguments is valid in our calculator:
(calc "(1 + 3) < (4 + 4)")
=> true
and
(calc "(1 + 3) str (4 + 4)")
=> "48"
The code:
(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)))
It is the correct version of the M Smith's solution, although I used eval
in my code, which is generally a bad idea. I paste it here and hope it can help someone.
(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)))
Example:
(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))"))
Result:
126
12
1891/189
I'll try it out, but I can't get your code to work, so it's a bit hard for me to understand what is happening in every place. Basically, the following is a guess and not intended to be a complete answer. Hopefully someone can come in and edit this down a bit and get it to function correctly.
I'll start with the basic premise: You have, in my opinion, way to many nested and anonymous functions. Everywhere you see a #(xyz) could probably be pulled out into its own function. I'm pretty sure having function inside of function inside of function would be pretty bad form in any programming language, and I feel it is bad form here. I began by removing anon functions, both hashed and the (fn) you have in your original code.
I also don't like nesting functions in my 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 %))))
Further notes:
(peek) is very ambiguous and I generally don't like using it. From the cheatsheets:
For a list or queue, same as first, for a vector, same as, but much more efficient than, last. If the collection is empty, returns nil.
Since I'm not entirely sure what structure you are working with at all times (I think its a vec?) and you do, you may want to use last or first, which ever is more appropriate. Although it is "much more efficient" than last, it's not helping me understand how the program works, so use peek in the finished product but not the shared product (mind you don't really need super speed for this either).
I also think that the (cond) should be unambiguously case-tested.
I attempted to make it a tad more "idiomatic" by making sure the args are less ambiguous. In your original code, you are passing in massive functions (and the results of nested functions) as one large argument to another function. Breaking all of that down to smaller functions is where you need to work more a bit. Notice how it is more clear what is happening in the calc function?
I pulled out the anon function inside calc and entered into a function called pre-calc. I would still suggest pulling out the anon functions from calc and work on clarifying what is happening inside of pre-calc. It is still hard to read because I can't really guess what is happening.
I would suggest starting with something like the following because it is hard to see what args are passed into (reduce). You can see how this is confusing because I am passing item in as an argument then I am following your pattern and passing item into (read-string) and then I am binding that result to item. I'm not sure if this is your intent, but I most certainly would not pass in an arg called let and them bind the result of passing it into a function created by evaluating item. This creates further confusion for me because you have item passed into a let-bound item-prio. I never did this, so I don't even know if the arg item or the let-bound item is being evaluated here.
Here is that part of the code. Notice how it is easy to see what is being reduced now?
(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))
There is a lot more I could write, but as I said, I really don't know how everything is working together. Regardless, this should at least show some of the logic I would use in creating this program. I would try to generalize this a lot more and keep it so that each function is only about 10 LOC each.
As I said, I hope others can either expand on this or edit it to something more palatable.
The smallest idiomatic calculator is the REPL!
If infix notation is the goal, I'd go for changing the reader so that numbers become functions of the arithmetic functions *,/,+,-,% etc, so (7 + 5) would be read as 7, being a Clojure function (in addition to being a java.lang.Number), can take + 5 as arguments, similar to how, in Smalltalk, numbers can understand arithmetic operations as messages.
来源:https://stackoverflow.com/questions/16105847/how-to-write-a-shortest-and-most-idiomatic-cli-calculator-in-clojure