HaskellのStateの必要性が, プログラミング言語の処理系を書いた時にわかったので, Stateの良さを語ります
Haskell (その3) Advent Calendar 2017 - Qiitaの1日目の記事です.
Haskell初心者の頃はStateが何故必要なのかわからなかった
私はHaskellを書き始めてから2年ぐらいはStateの存在意義がわかりませんでした.
こんなものは無くて良いと考えていました.
Haskellは純粋関数でシステムを構成して, 引数と返り値だけでものごとを構成しています. 私はそこに魅力を感じてHaskellを学びました. そこで, Stateのような変数をエミュレートするような仕組みを導入したら, せっかく変数なしにシステムを構築してるのが台無しになってしまうと考えていました.
つまり,
Haskell初心者の頃の私は,
常にa -> World
のような関数を使って,
Stateのようなものは使わないのが美しいと考えていたわけです.
変数のようなインターフェイスは結局必要である
Haskellで実用的なプログラムを書くようになって, 薄々この考えが間違っていることには気が付き始めていました. Haskell歴3年目ぐらいで, プログラミング言語処理系を実装してみた時, この考えが間違っていることを確信することが出来ました.
なので, 昔の私に返答しようと思います.
結局, 変数というものは必要です.
以下はncaq/unown-expl: Pokemon exp programming languageというプログラミング言語の抽象構文の実行関数eval
です.
type UnownStateIO a = StateT [a] IO a
eval :: Unown -> UnownStateIO UnownValue
eval (Direct u ) = unownConvertString <$> eval u
eval (Give u ) = eval u >>= modify . (:) >> gets headEx
eval (Increase u ) = (\(UnownInt i) -> UnownInt $ i + 1) <$> eval u
eval (Join u1 u2) = unownSum <$> eval u1 <*> eval u2
eval (Keep pr co) = eval pr >>= c
where c (UnownBool True) = (\h (UnownList t) -> UnownList $ h : t) <$>
eval co <*> eval (Keep pr co)
c _ = return $ UnownList []
eval (Make ) = return $ UnownInt 0
eval (Observe u ) = eval u >>= (\(UnownInt i) -> gets (!! i))
eval (Perform u1 u2) = unownProduct <$> eval u1 <*> eval u2
eval (Quicken u1 u2) = (\(UnownInt i1) (UnownInt i2) -> UnownInt $ i1 ^ i2) <$> eval u1 <*> eval u2
eval (Tell u ) = eval u >>= (\r -> liftIO (putStr $ unownRawShow r) >> return r)
eval (XXXXX u1 u2) = eval u1 >> eval u2
eval (Zoom u1 u2) = UnownBool <$> ((<) <$> eval u1 <*> eval u2)
eval (Exclaim u1 u2) = eval $ XXXXX u1 u2
eval _ = error "not impl"
私は当初この関数を,
たしかeval :: Unown -> [UnownValue] -> IO [UnownValue]
という形式で書いていました.
ログが残っていれば良かったのですが,
gitにその時の関数は残っていませんでした…
これをStateを使わずに,
純粋関数で書くのは大変でした.
eval
の各パターンマッチを見ればわかりますが,
仮想マシンの状態を変更するのはGive
というスタックに値を追加するという1つの命令だけです.
1つなのにも関わらず,
ネストした命令のどこにGive
が含まれているのかわからないため,
全てのeval
の返り値を束縛して,
新しい返り値に設定する必要があります.
返り値を一々変更するのは面倒です.
また,
引数に一々状態を設定するのも面倒です.
Stateとモナドを使えば状態の変更を気にする必要はありません. 変更するべき時だけ変更して, プログラマは値だけを得ることが出来ます. ファンクターとアプリカティブを使って2項の値を簡単に処理することも出来ます.
他の言語の変数に比べたHaskellのStateの優位性
Stateを使った変数のようなインターフェイスは他の可変な変数を備える言語に比べて優れている点があります.
普通の関数に戻せるので状態を柔軟に扱える
今ある状態を保ったり, 状態を保存したり, 新しい状態を作ったりする時, 変数でこれらを実現することは大変です.
継続などの高度な機能が必要とされることもあります.
StateT
は内部は単なる関数なので,
evalStateT
などを使えば,
今ある状態を保ちつつ新しい状態で関数を実行することなども簡単に可能です.
状態もHaskellのデータ構造は基本的に永続データ構造なので,
状態が更新されても今ある状態が破壊されることはありません.
再帰関数でも変数に読み書きできる
Stateを使った変数インターフェイスは再帰する関数でも変数を読み書きすることが出来ます.
変数に読み書きするからと言ってループを必要としません.
他の言語で再帰関数で変数を変更するのには様々な方法がありますが, Stateはそれらに比べて優れています.
グローバル変数を使う方法
これは最も簡単な方法ですが, グローバル変数は何処から読み書きされるかわかりませんし, プログラムの全体で確保されるのでパフォーマンス面でも良くないです.
Stateを使えば状態にアクセスする関数は型で明示されています.
可変な変数を引数として渡す方法
int foo(world w) {
// …
}
のように, 変数を可変な引数として渡して再帰する方法があります.
この方法は, 再帰する時や他に状態を必要とする関数を呼び出すときに, 一々状態を渡す必要があります. 面倒くさいですね. また, こういった言語には値をディープコピーする仕組みが用意されていないことも多く, 状態を複製するなどを行うのが難しいことがあります. Haskellのデータは永続データ構造なのでデータが書き換えられても安心です.
オブジェクトの可変なメンバーにする方法
オブジェクト指向言語を名乗っている言語は大半はこれを使って状態にアクセスしているのではないでしょうか.
これは状態にアクセスするのも簡単ですし, 状態にアクセスするメソッド(関数)もはっきりしているので一見良さそうに思えます.
しかし, 状態を複製するのが難しかったり, 状態にアクセスする新しい関数を書きにくいという問題があります.
Rustなどの言語はメソッドが拡張可能だったり,
Clone
トレイトが存在していたりして割とうまくやっていますが…
Stateを使いましょう
状態を意識しない関数群を書けるならそれに越したことはないですが, 現実のプログラムでは状態を扱うことがよくあります. 節度を守りつつ状態を扱うためにStateを使いましょう.