yunomuのブログ

酒とゲームと上から目線

Haskellでdcっぽいのを作る その2 型安全とは

Haskellでdcっぽいのを作る その1 - yunomuのラクに生きたい
これを書いてから、なんか、なんとなく気持ち悪い感じで日々を過ごしていたんですが、
なんていうかアレですよ、「それモナドにしないの?」って、言われているような気がしていて。

参考:「モナモナ言わないモナド入門」
http://mew.org/~kazu/material/2011-monad.pdf

何か具体的にはわからないんだけど、そう囁くのよ。私のゴーストが。

そんで前回のあの、エラーメッセージが遅延されてしまうバージョンのdcを見ていて、
「失敗系はerrorじゃなくてMaybeだろ!」
と唐突に思った。

Haskellは型安全とか言いますけど、よくよく考えると実行時エラーを出すerror関数って型安全とか関係ないんですよね。型無いし。実行してみないとエラーが出るかどうかさえわからないというのは、型安全ではない。
ということで、Maybeさんの出番です。Maybeは「もしかしてこの型? それとも失敗?」という型で、モナドインスタンスらしいです。

今回のプログラムでは、Stackのpop関数とpeek関数が失敗する可能性を持っています。どちらもスタックに値が無い場合はエラーになる。当たり前のことです。なので以前はerrorで返してたんですけど、戻り値をMaybeにした。
before: dc_haskell/Stack.hs at master from yunomu/exercises - GitHub
after: dc_haskell/Stack2.hs at master from yunomu/exercises - GitHub

まあ、サラっと「Maybeにした」とか書いてますけど、どこをMaybeにするかってので2日くらい悩んでます。特にpopは、失敗時は値だけNothingでスタックは入力のやつをそのまま返せばいいんじゃないかとか思ったりして。これは結局、bindの性質を考えるに、Maybeの利点を活かすっていうか、原理的に戻り値の型はMaybeにした方がいいというのに気づいて、こうなった。今思えば当たり前なんですけど。
peekは見たまんまです。状態変わらないし。

これで一応、変な入力をした時にエラーメッセージ出力が遅延されることもなく、即座にエラーになるようになった。まあここまでは良し。

で、それが実際プログラム全体にとってどう影響するのかというのが
dc_haskell/dc2.hs at master from yunomu/exercises - GitHub
これ。
前のバージョンと比較するとcalcがほとんどの機能をprocに預けてすごく小さくなってたり、procから呼び出す関数が色々増えていたりとかで、なんかかなり違うものになっちゃってますけど、結構Haskellっぽくなったんじゃない?

Maybeのマニュアル漁ってたら、超便利なmaybeって関数を見つけたので多用しています。

maybe :: b -> (a -> b) -> Maybe a -> b

訳すと

maybe デフォルト値b aをb'に変換する関数 aかもしれないもの -> bかb'

という感じ。今の私が見ると余計わかりづらい。
こいつは便利なので、caseっぽいところを全部これで置き換えました。maybe関数ってHaskell界の評判はどんなもんなんでしょうね。なんとなくcaseよりはいい気がするんだけど。

で、問題のope関数。

ope :: Stack Int -> (Int -> Int -> Int) -> Maybe (Stack Int)
ope s f = do (v1, s1) <- pop s
             (v2, s2) <- pop s1
             return (push s2 (f v1 v2))

これでいいんだけど、これでいいじゃんって気づくまでにやっぱり2日くらいかかった。
要はこういうことなんですけど、

ope s f = pop s >>= \(v1, s1) -> pop s1 >>= \(v2, s2) -> return (push s2 (f v1 v2))

このbindの動きっていうか、この式を想像するのに2日以上かかったというか。

結構悩みましたが、悩んだおかげでそれなりにHaskellっぽいコードが書けた気がします。
dcって演算子順位構文解析とかしなくていいし楽かなぁと思ってましたけど、そんな適当に選んだ割にはHaskell的にはかなり良い題材でしたね。スタックを作るってのはやっぱりいい題材だわ。ただ、スタック単体だとerror関数で済んでしまうから、うん、やっぱみんなHaskellではdcを実装したらいいんじゃないかな。

ところで、やっぱりコンパイルするとプロンプトが思ったタイミングで出てくれないのは相変わらずです。

今回の収穫は、Haskellで型安全に書くのはそれなりに大変なんじゃないかというのに気づいたことです。