Haskellにおけるミュータブル変数としてのモナド
Haskellでは全ての変数がイミュータブルです. つまり再代入が禁じられています. それでもモナドの力を借りればまるでミュータブル変数を扱っているかのようなプログラムを書くことができます.
本記事ではミュータブル変数として扱える3つの型IORef a
, STRef a
, StateVar a
を紹介します.
IORef
定義
データ型IORef a
はbase
パッケージのData.IORef
で定義されています.
代表的な関数は以下の通りです.
newIORef :: a -> IO (IORef a)
readIORef :: IORef a -> IO a
writeIORef :: IORef a -> a -> IO ()
modifyIORef :: IORef a -> (a -> a) -> IO ()
使い方
関数の型を見れば使い方はわかりますが一応…
例としてフィボナッチ数を求めるプログラムを書いてみました.
注意しなければならないのは, 全ての関数の戻り値はIOアクションであるという点です.
import Control.Monad (forM_)
import Data.IORef
main :: IO ()
main = do
ref1 <- newIORef (1 :: Int)
ref2 <- newIORef (1 :: Int)
forM_ [1..10] $ \_ -> do
x <- readIORef ref1
y <- readIORef ref2
writeIORef ref1 y
writeIORef ref2 (x + y)
print =<< readIORef ref2
実行結果は144
です.
STRef
定義
データ型STRef s a
はbase
パッケージのData.STRef
で定義されていて, ST s
モナドの中で使うことができます.
代表的な関数は以下の通りです.
newSTRef :: a -> ST s (STRef s a)
readSTRef :: STRef s a -> ST s a
writeSTRef :: STRef s a -> a -> ST s ()
modifySTRef :: STRef s a -> (a -> a) -> ST s ()
全てST s
モナドアクションが返っているのがわかります. Haskell入門にも書かれていますが, 型変数s
は常に多相的に扱われるため具体的な型を当てはめることはないみたいです.
使い方
import Control.Monad (forM_)
import Control.Monad.ST (runST)
import Data.STRef
main :: IO ()
main = print $ runST $ do
ref1 <- newSTRef (1 :: Int)
ref2 <- newSTRef (1 :: Int)
forM_ [1..10] $ \_ -> do
x <- readSTRef ref1
y <- readSTRef ref2
writeSTRef ref1 y
writeSTRef ref2 (x + y)
readSTRef ref2
こちらの実行結果も144
です.
State
ミュータブル変数を使いたい理由のひとつが, 「状態を扱いたいから」でしょう. 状態といえばState s
モナドですね.
定義
型State s a
はtransformers
パッケージのControl.Monad.Trans.State.Lazy
とControl.Monad.Trans.State.Strict
で定義されています.
代表的な関数は以下の通り.
get :: State s s
put :: s -> State s ()
modify :: (s -> s) -> State s ()
gets :: (s -> a) -> State s a
runState :: State s a -> s -> (a, s)
evalState :: State s a -> s -> a
execState :: State s a -> s -> s
使い方
型引数s
の部分に整数のタプルを保持することで, フィボナッチ数を求めてみました.
import Control.Monad (forM_)
import Control.Monad.Trans.State.Lazy
fibo :: State (Int, Int) ()
fibo = forM_ [1..10] $ \_ -> modify (\(x, y) -> (y, x + y))
main :: IO ()
main = print $ execState fibo (1, 1)
これの実行結果は(89, 144)
です.
もう少しState
っぽい例を挙げてみましょう.
import Control.Monad.Trans.State.Lazy
data Coord = Coord Int Int
moveX :: Int -> State Coord()
moveX a = modify (\(Coord x y) -> Coord (x+a) y)
moveY :: Int -> State Coord()
moveY a = modify (\(Coord x y) -> Coord x (y+a))
howFarFromOrigin :: State Coord Float
howFarFromOrigin = do
Coord x y <- get
return $ sqrt (fromIntegral (x*x) + fromIntegral (y*y))
main :: IO ()
main = print $ (`evalState` Coord 0 0) $ do
moveX 1
moveY 7
moveX 2
moveY (-3)
howFarFromOrigin
実行結果は5.0
です.
StateVar
定義
型StateVar a
はStateVar
パッケージのData.StateVar
モジュールで定義されています. ただその前に, このモジュールで定義されている型クラスについて触れておきましょう.
class HasGetter t a | t -> a where
get :: monadIO m => t -> m a
instance HasGetter (IO a) a
instance HasGetter (IORef a) a
class HasSetter t a | t -> a where
($=) :: monadIO m => t -> a -> m ()
instance HasSetter (IORef a) a
class HasSetter t a => HasUpdate t a b | t -> a b where
($~) :: monadIO m => t -> (a -> b) -> m ()
instance HasUpdate (IORef a) a
StateVar
では読み込み可能か書き込み可能かを分けて定義しています. 各クラスのインスタンスには, 先ほど見たIORef
も登場していますね.
ちなみにclass HasGetter t a | t -> a
という書き方は, 言語拡張FunctionalDependencies
によって提供されている機能です. 型t
に対し型a
が一意に定まることを表しています.
つまりHasGetter Int Int
とHasGetter Int String
という2つのインスタンスは同時に作ることができません.
ではStateVar
の定義です.
data StateVar a = StateVar (IO a) (a -> IO())
instance HasGetter (StateVar a) a
instance HasSetter (StateVar a) a
instance HasUpdate (StateVar a) a a
getterの役割を持つIO a
とsetterの役割を持つa -> IO()
から, StateVar a
型が作られます. なにはともあれ使い方を見てみましょう.
使い方
まずはStateVar
ではなく, 3つの型クラスのインスタンスになっているIORef
の例から.
import Control.Monad (forM_)
import Data.IORef
import Data.StateVar
main :: IO ()
main = do
ref1 <- newIORef (1 :: Int)
ref2 <- newIORef (1 :: Int)
forM_ [1..10] $ \_ -> do
x <- get ref1
y <- get ref2
ref1 $= y
ref2 $= x + y
print =<< get ref2
次にStateVar
の例です(結局IORef
の力を借りていますけどね…).
import Control.Monad (forM_)
import Data.IORef
import Data.StateVar
main :: IO ()
main = do
ref <- newIORef (1 :: Int)
let sv = StateVar (readIORef ref) (writeIORef ref)
print =<< get sv
sv $= 3
print =<< get sv
sv $~ (+1)
print =<< get sv
これの実行結果は次の通りです.
1
3
4
wai-sessionというパッケージではWebアプリケーションのセッションを管理するためにStateVar
を使っています(そのソースコードがこちら).