いわゆる一つの逆ポーランド記法(Reverse Polish Notation, RPN)電卓です。入力フォームをSignalに結びつけて入力として使う例になります。私は本物のRPN電卓を使ったことがないので本物とはたぶん動作が違います。
import Graphics.Input (input, button) -- 電卓のボタン操作の種別 data Keys = Number Int | Add | Sub | Mul | Div | Clear | Enter -- GUIからの電卓に対する操作入力列を表現するシグナル。初期値はClear。 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) -- foldpで、入力操作列シグナルに対して、「1回前の電卓状態に、入力操作を適用し、次状態を生成する」という関数をリフト適用する。 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でやってみる」シリーズのまとめエントリ - uehaj's blog
- 作者: Miran Lipovača,田中英行,村主崇行
- 出版社/メーカー: オーム社
- 発売日: 2012/05/23
- メディア: 単行本(ソフトカバー)
- 購入: 25人 クリック: 580回
- この商品を含むブログ (67件) を見る
- 作者: Miran Lipovaca
- 出版社/メーカー: オーム社
- 発売日: 2012/09/21
- メディア: Kindle版
- 購入: 4人 クリック: 9回
- この商品を含むブログを見る
追記: 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))