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) をグローバル var に入れる必要がありますか?
  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

これは、以下に示すマクロソリューションを求めています。優先順位レベルが 2 つしかないという点でごまかしたので、優先順位を追跡するためにスタックを作成する必要はありませんでした。このソリューションは一般化できますが、もう少し手間がかかります。

clojure のマクロについて覚えておくべきトリックは、それらが clojure 構造 (リストのネストされたリスト) を取り、別のリストのリストを返すことです。このcalcマクロは単純に入力を受け取り、それを括弧で囲み、それを clojure リーダーに渡します。これは、入力文字列を構文解析してシンボルのリストにするという面倒な作業をすべて行います。

次に、reorder-equation 関数が中置をプレフィックス順序リストに変換します。そのリストはマクロによって返され、その後 clojure コードとして評価されます。

* と / のチェックは、それらが最初に評価されることを確認します。それが何をするかを見るには

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

ご覧のとおり、方程式を受け取り、それを有効な clojure 式に書き換えてから評価します。

これはごまかしのように思えるかもしれませんが、Clojure に慣れてくると、言語に多くの面倒な作業を任せることができることに気付くでしょう。入力をシンボルのリストに解析し、それらのシンボルを関数名として使用することは完全に理にかなっています。実際のところ、2 つの引数を取る関数はすべて、この計算機で有効です。

(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

試してみますが、コードを機能させることができないため、すべての場所で何が起こっているのかを理解するのは少し難しいです. 基本的に、以下は推測であり、完全な回答を意図したものではありません。うまくいけば、誰かが来て、これを少し編集して、正しく機能させることができます.

基本的な前提から始めます。私の意見では、多くのネストされた無名関数への道があります。#(xyz) が表示されているところはどこでも、おそらく独自の関数に引き出される可能性があります。関数内に関数内に関数を持つことは、どのプログラミング言語でもかなり悪い形式になると確信しており、ここでは悪い形式だと感じています。最初に、元のコードにあるハッシュ関数と (fn) の両方の anon 関数を削除しました。

また、let バインディングで関数をネストするのも好きではありません。

(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 と同じですが、last よりもはるかに効率的です。コレクションが空の場合、nil を返します。

あなたが常にどのような構造で作業しているのか完全にはわからないので (私は vec だと思いますか?)、あなたはそうしているので、last または first のどちらか適切な方を使用することをお勧めします。前回よりも「はるかに効率的」ですが、プログラムがどのように機能するかを理解するのに役立たないため、共有製品ではなく完成品でピークを使用してください (これにも超高速は必要ありません)。

また、(cond) は明確にケーステストする必要があると思います。

引数があいまいでないようにすることで、少し「慣用的」にしようとしました。元のコードでは、大規模な関数 (およびネストされた関数の結果) を 1 つの大きな引数として別の関数に渡しています。そのすべてをより小さな関数に分割することは、もう少し作業が必要な場所です。calc 関数で何が起こっているかがより明確になっていることに注目してください。

calc 内の anon 関数を取り出して、pre-calc という関数に入りました。calc から anon 関数を引き出して、pre-calc の内部で何が起こっているかを明確にする作業を行うことをお勧めします。何が起こっているのか本当に推測できないので、まだ読むのは難しいです。

次のようなものから始めることをお勧めします。これは、どの引数が渡されるか (reduce) を確認するのが難しいためです。item を引数として渡しているため、これがどのように混乱しているかがわかります。次に、パターンに従って item を (read-string) に渡し、その結果を item にバインドしています。これがあなたの意図であるかどうかはわかりませんが、let と呼ばれる引数を渡さず、item を評価することによって作成された関数にそれを渡した結果をバインドします。let バインドされた item-prio にアイテムが渡されているため、これによりさらに混乱が生じます。私はこれをやったことがないので、arg アイテムまたは let バインドされたアイテムがここで評価されているかどうかさえわかりません。

これがコードのその部分です。現在削減されているものを簡単に確認できることに注目してください。

(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

これは、正規表現やマクロを使用せず、代わりに解析ロジックにpartitionandを使用する私のソリューションです。reduce

一般的な考え方は、ユーザー入力を初期値の後のシンボル ペアのシーケンスとして扱うことです。したがって、あなたの算術式は本質的には'(<init-value> (op1 value1) (op2 value2) ...(opN valueN))もちろん、<init-value>それ自体が括弧である可能性があり、その場合は最初に同様に削減する必要があります.

partition次に、シンボルと値のペアのシーケンスを に提供しますreduce。これにより、シンボルが優先順位に従って配置された有効な Clojure 式が構築されます。無効なシンボル (数値リストまたはシンボル以外のもの) の評価を停止reduceし、handy を使用してブロックを終了しますreduced(1.5 で追加)。

重要な概念は、遭遇したリスト (括弧) は最終的に値に還元されるため、再帰的にreduce-d されるということです。この関数peelは、ネストされたリストを処理します。つまり、(((1 + 1)))

少し冗長ですが (わかりやすい変数名を好みます)、正しいです。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)))))

Google のオンライン計算機と照合した例:

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

2 つの制限: Incanter の infix mathematics と同様に、式はスペースで区切られている (つまり1+2-3、サポートされていない)必要があります。また、ユーザー入力を使用しているため、末尾に括弧を付けることができます (これは、より堅牢な REPL 実装で修正する必要があるバグだと考えています)。read-string

クレジット:上記のコーディングの参考として、 Eric Robert のProgramming Abstractions in C (Addison Wesley、1997 年) を使用しました。第 14 章「式ツリー」では、ほぼ同じ問題が説明されています。

于 2013-04-30T05:06:35.833 に答える
-1

慣用的な最小の計算機は REPL です。

中置記法が目標である場合、数字が算術関数 *、/、+、-、% などの関数になるようにリーダーを変更します。したがって、(7 + 5) は Clojure である 7 として読み取られます。関数 (java.lang.Number であることに加えて) は + 5 を引数として取ることができます。これは、Smalltalk で数値が算術演算をメッセージとして理解できるのと同様です。

于 2013-04-23T04:32:48.910 に答える