Haskellにおけるミュータブル変数としてのモナド

2019-04-20 programming Haskell

Haskellでは全ての変数がイミュータブルです. つまり再代入が禁じられています. それでもモナドの力を借りればまるでミュータブル変数を扱っているかのようなプログラムを書くことができます.

本記事ではミュータブル変数として扱える3つの型IORef a, STRef a, StateVar aを紹介します.

IORef

定義

データ型IORef abaseパッケージの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 abaseパッケージの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 atransformersパッケージのControl.Monad.Trans.State.LazyControl.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 aStateVarパッケージの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 IntHasGetter 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を使っています(そのソースコードがこちら).