Elmでやってみるシリーズ5: 逆ポーランド電卓。
いわゆる一つの逆ポーランド記法(Reverse Polish Notation, RPN)電卓です。入力フォームをSignalに結びつけて入力として使う例になります。私は本物のRPN電卓を使ったことがないので本物とはたぶん動作が違います。
import Graphics.Input (input, button)
data Keys = Number Int | Add | Sub | Mul | Div | Clear | Enter
keys = input Clear
calculator (n, xs) =
let btn = button keys.handle
btnN n = btn (Number n) (show n)
in flow down [
flow right [ btnN 7, btnN 8, btnN 9, btn Add "+" ]
, flow right [ btnN 4, btnN 5, btnN 6, btn Sub "-" ]
, flow right [ btnN 1, btnN 2, btnN 3, btn Mul "*" ]
, flow right [ btnN 0, btn Clear "Clear", btn Enter "Enter", btn Div "/" ]
, if (n==0) then (asText xs) else (asText n)
]
calc : Keys -> (Int, [Int]) -> (Int, [Int])
calc it (num, xs) = case it of
Number n -> (num*10+n, xs)
Enter -> (0, num::xs)
Clear -> (0,[])
_ -> if (length xs < 2)
then (0, xs)
else let top = head xs
second = head (tail xs)
rest = tail (tail xs)
in case it of
Add -> (0, (second+top) :: rest)
Sub -> (0, (second-top) :: rest)
Mul -> (0, (second*top) :: rest)
Div -> (0, (second `div` top) :: rest)
main=lift calculator (foldp calc (0,[]) keys.signal)
実行画面(操作可能)
上記のソースを変更した上で実行したい場合はこちらからどうぞ。ブラウザ内で実行できます。
気づいたこと
- コロン(:)、ダブルコロン(::)はHaskellのそれとちょうど意味が逆になっている。既存のhaskell編集モードを流用したい場合、不便かも。
asパターンはない。asパターンは@ではなく、... as someのように記述。
NG: let (x@{a,b}) = {a=1, b=2} in x
OK: let ({a,b} as x) = {a=1, b=2} in x
case式でリストをパターンマッチで分解できない。具体的には「case (x:xs) of ...」などができず、(x:xs)の部分は単一の変数名しか指定できない。(x:xs)を指定してもエラーにならないので、Elmのバグではなかろうか。(本件は記述が変なのと詳細を覚えてないので削除)
- Inputのhandleは、複数のGUI部品からの入力を集約したSignalを作成するための間接参照点である。(という理解で正しいか?)
- Elmに例外ってないみたいだが、たとえば(head [])するとJavaScriptでの実行時例外になって、プログラムは再開できなくなる(もちろんページ全体をリロードすれば操作は再開できるが)。そうならないようにチェックすれば良いのだが、そういうものか?
- トップレベル定義の型アノテーションを型推論させて表示するには、Elmコマンドの-pもしくは--print-typesでできる。しかしアノテーションはあまり必要な気がしない(逆にアノテーションが本体関数の変更に追随しないことで良くエラーになる)が、ドキュメント情報としてみると便利であろう。
- foldlかfoldrで逆ポーランド電卓を作るのは、すごいH本の例にあったが、上のelmのはfoldpで同型。一般化すると、一連の入力シーケンスにともなって状態を変化させる処理は、適切な粒度のSignal+foldpの抽象化に対応するということか。ファイルからの入力文字ストリームはSignalにできそう。書き込みはどうか?ソケット通信やDBはどうか。
追記: Elm 0.14対応
import Graphics.Input (..)
import Signal
import Signal(Channel)
import Graphics.Element (..)
import Text(..)
import List(..)
type Keys = Number Int | Add | Sub | Mul | Div | Clear | Enter
keys : Channel Keys
keys = Signal.channel Clear
calculator : (Int, List Int) -> Element
calculator (n, xs) =
let btn x = button (Signal.send keys x)
btnN n = btn (Number n) (toString n)
in flow down [
flow right [ btnN 7, btnN 8, btnN 9, btn Add "+" ]
, flow right [ btnN 4, btnN 5, btnN 6, btn Sub "-" ]
, flow right [ btnN 1, btnN 2, btnN 3, btn Mul "*" ]
, flow right [ btnN 0, btn Clear "Clear", btn Enter "Enter", btn Div "/" ]
, if (n==0) then (asText xs) else (asText n)
]
calc : Keys -> (Int, List Int) -> (Int, List Int)
calc it (num, xs) = case it of
Number n -> (num*10+n, xs)
Enter -> (0, num::xs)
Clear -> (0,[])
_ -> if (length xs < 2)
then (0, xs)
else let top = head xs
second = head (tail xs)
rest = tail (tail xs)
in case it of
Add -> (0, (second+top) :: rest)
Sub -> (0, (second-top) :: rest)
Mul -> (0, (second*top) :: rest)
Div -> (0, (second // top) :: rest)
main=Signal.map calculator (Signal.foldp calc (0,[]) (Signal.subscribe keys))