Reverse Polish notation calculator
『すごいHaskellたのしく学ぼう!』 の 10章の逆ポーランド記法電卓を Groovy, Scala, Clojure, Scheme で書いてみる。
Groovy
def solveRPN(expr) { expr.tokenize().inject([]){ acc, v -> "*" == v ? [acc[1] * acc[0], *acc.drop(2)] : "+" == v ? [acc[1] + acc[0], *acc.drop(2)] : "-" == v ? [acc[1] - acc[0], *acc.drop(2)] : [v as double, *acc] }[0] } assert solveRPN("10 4 3 + 2 * -") == -4.0 assert solveRPN("2 3.5 +") == 5.5 assert solveRPN("90 34 12 33 55 66 + * - +") == -3947.0 assert solveRPN("90 34 12 33 55 66 + * - + -") == 4037.0 assert solveRPN("90 3.8 -") == 86.2 assert solveRPN("") == null
0, 1 の順なら pop でも対応できそうだが、1, 0 の順に参照しなければならないので drop と組み合わせた。
意味はないが空文字が渡されたら null を返すようにしてみた。
関連するメソッドの動作。
groovy:000> "".tokenize() ===> [] groovy:000> "".split() ===> [Ljava.lang.String;@83f700d groovy:000> [].head() ERROR java.util.NoSuchElementException: Cannot access first() element from an empty List at groovysh_evaluate.run (groovysh_evaluate:2) ... groovy:000> [][0] ===> null
Scala
val solveRPN: String => Double = _.split(' ').foldLeft(List.empty[Double]){ case (x::y::ys, "*") => (y * x)::ys case (x::y::ys, "+") => (y + x)::ys case (x::y::ys, "-") => (y - x)::ys case (xs, number) => number.toDouble::xs }.head assert( solveRPN("10 4 3 + 2 * -") == -4.0 ) assert( solveRPN("2 3.5 +") == 5.5 ) assert( solveRPN("90 34 12 33 55 66 + * - +") == -3947.0 ) assert( solveRPN("90 34 12 33 55 66 + * - + -") == 4037.0 ) assert( solveRPN("90 3.8 -") == 86.2 )
Scala はパターンマッチがあるので Haskell と変わらない。
14章に Maybe を使った安全版があるので Scala ならそっちを実装するべきだったかもしれない。
空文字にも対応していない。
split が曲者だった。
scala> "".split(' ') res0: Array[String] = Array("") scala> "".split("\\s+") res1: Array[java.lang.String] = Array("") scala> """\s+""".r.split("") res2: Array[String] = Array("") scala> """\S+""".r.findAllIn("") res3: scala.util.matching.Regex.MatchIterator = empty iterator
Clojure
(defn solve-rpn [expr] (defn f [[x y & ys :as s] w] (cond (= "*" w) (cons (* y x) ys) (= "+" w) (cons (+ y x) ys) (= "-" w) (cons (- y x) ys) :else (cons (Double/parseDouble w) s))) (->> expr (re-seq #"\S+") (reduce f []) first)) (assert (= (solve-rpn "10 4 3 + 2 * -") -4.0) ) (assert (= (solve-rpn "2 3.5 +") 5.5) ) (assert (= (solve-rpn "90 34 12 33 55 66 + * - +") -3947.0) ) (assert (= (solve-rpn "90 34 12 33 55 66 + * - + -") 4037.0) ) (assert (= (solve-rpn "90 3.8 -") 86.2) ) (assert (= (solve-rpn "") nil) )
パターンマッチはないが分配束縛があるのでそれを使った。
nil 耐性があるので引数の時点で分配している。
user=> (clojure.string/split "" #"\s+") [""] user=> (re-seq #"\S+" "") nil user=> (first nil) nil user=> (first []) nil
Scheme
(define (solve-rpn expr) (define (f v acc) (cond ((equal? "*" v) (cons (* (cadr acc) (car acc)) (cddr acc))) ((equal? "+" v) (cons (+ (cadr acc) (car acc)) (cddr acc))) ((equal? "-" v) (cons (- (cadr acc) (car acc)) (cddr acc))) (else (cons (string->number v) acc)))) (car (fold f '() (string-split expr #/\s+/)))) (solve-rpn "10 4 3 + 2 * -") ;=> -4 (solve-rpn "2 3.5 +") ;=> 5.5 (solve-rpn "90 34 12 33 55 66 + * - +") ;=> -3947 (solve-rpn "90 34 12 33 55 66 + * - + -") ;=> 4037 (solve-rpn "90 3.8 -") ;=> 86.2 (solve-rpn "") ;=> #f
まだ、どんな関数があるのかよく知らない。
今風な API Doc が欲しい。
gosh> (string-split "" #/\s+/) ("") gosh> (string->number "") #f gosh> (car '()) *** ERROR: pair required, but got () gosh> (use srfi-1) #<undef> gosh> (reduce + 10 '(1 2 3)) 6 gosh> (reduce + 10 '()) 10
終わりに
簡単そうに思えたが書いてみるといろいろ気づくことがあった。
言語によって微妙に違うので注意したい。
2012-06-11 追記
Clojure 版の f はローカル関数にしたつもりだったがグローバルから参照できてしまった。
defn ではなく letfn を使って f を定義するように修正する。
(defn solve-rpn [expr] (letfn [(f [[x y & ys :as s] w] (cond (= "*" w) (cons (* y x) ys) (= "+" w) (cons (+ y x) ys) (= "-" w) (cons (- y x) ys) :else (cons (Double/parseDouble w) s)))] (->> expr (re-seq #"\S+") (reduce f []) first))) (assert (= (solve-rpn "10 4 3 + 2 * -") -4.0) ) (assert (= (solve-rpn "2 3.5 +") 5.5) ) (assert (= (solve-rpn "90 34 12 33 55 66 + * - +") -3947.0) ) (assert (= (solve-rpn "90 34 12 33 55 66 + * - + -") 4037.0) ) (assert (= (solve-rpn "90 3.8 -") 86.2) ) (assert (= (solve-rpn "") nil) )