状態管理
状態とは何だろうか。
プログラムは、何らかの方法で入力を受け取り、それを処理して何らかの出力する。もしここに状態がなかったらどうなるだろうか?
状態がなければ、プログラムが使える情報は入力だけ、つまりyesコマンドやcatコマンドレベルのことしかできない(抽象度を下げて考えればそれも無理だ)。したがってプログラミングにおいて、処理と処理の間で維持される値、つまり状態を管理する手段は不可欠である。
Haskellにおける状態管理手法は、以下の四種類に大別される。
関数呼び出し
もっとも基本的な状態管理の手法である。状態を関数の引数として次々渡すことによって保持し、しばしば再帰という形をとる。 特にHaskellの標準ライブラリおよびGHCは再帰処理を重要視しており、もっとも簡単に速い処理を記述する方法であると言える。以下のような初期値とリストの要素を足し合わせる関数は引数accを状態として持つ。
code:haskell
sum' :: Int -> Int -> Int sum' !acc (x:xs) = sum' (acc + x) xs
sum' acc [] = acc
関数呼び出しで渡されている値には当然外から触れることはできず、スレッド間で情報をやり取りしたい並行処理などにおいては不都合が生じる。 そこで、「Haskellのオブジェクトに対する可変な参照」という構造により、状態により自由な居場所を提供する。以下の三種類がよく用いられており、目的によって使い分けられる。 MVar 中身が空のときの読み出しと、値が入っているときの書き込みをブロックする箱的な参照型。ロジックが甘いとデッドロックになるため、扱いに注意が必要である。 TVar 操作をSTMモナド上で記述できるため、複数のTVarに対する操作をアトミックに記述できる。 古典的FRPライブラリのほとんどは、内部で参照型を利用している。 code:haskell
ref <- newIORef ””
forkIO $ do
readIORef ref >>= putStrLn
threadDelay 1000000
forever $ getLine >>= writeIORef
外部と通信する手段を持った構造で記述することで、対話的に計算できないという欠点を克服する。
code:haskell
data Consumer s a = PureC a | Consume (s -> Consumer s a)
Consumer s aは、最終結果aを出す前にs型の値を好きなだけ要求できる。非常に長い文字列をインクリメンタルにパースしたいときなどにこのような挙動が役に立つ。(->) sの部分をFunctorに一般化したFreeモナドは、コルーチンの性質を備えたモナドを自由にデザインでき、かつて一世を風靡した。 ミュータブル配列
GHCは数値などを直接格納するミュータブルな配列を提供しており、一般的にはUnboxed vectorなどを通じて使うことが多い。 参照型と異なりポインタが介在しないため、オーバーヘッドが存在しない。そのため、数値計算など具体的なアルゴリズムを記述したい場合に使おう。 code:haskell
import qualified Data.Vector.Unboxed.Mutable as MV
import qualified Data.Vector.Unboxed as V
import Data.Foldable (for_)
main = do
v <- MV.replicate 16 (0 :: Int)
x <- MV.read v (i - 2)
y <- MV.read v (i - 1)
MV.write v i (x + y + 1)
V.freeze v >>= print