Haskell: Readerモナドの実験

Readerモナドで定義されているlocal関数とasks関数の実験をしてみた。
local関数とasks関数を使うと、環境(データを記憶する領域のようなもの)を局所的に操作できることがわかった。
これらの関数をうまく利用すれば、プログラミング言語でおなじみの「スコープ」の概念を綺麗に表現できるようになると思われる。
Haskellインタプリタを実装したい開発者にとっては嬉しい話に違いない。

コードは以下の通り。ReaderTでReaderモナドとIOモナドをレイヤー化している(多分)。こうしておけば、liftIO関数とputStr関数を組み合わせて、IOも同時にできてしまうようだ。(実はまだあんまりよくわかっていない) つまり、昔ながらのprintデバッグが可能になる。

% vim reader.hs
import Control.Monad.Reader

data Env = Env { envMsg :: String }

emptyEnv = Env { envMsg = "" }

append msg env = env { envMsg = (envMsg env) ++ msg }

level0 :: String -> ReaderT Env IO String
level0 msg = do
    local (append msg) level1

level1 :: ReaderT Env IO String
level1 = do
    msg <- asks envMsg
    debug ("level1-1: " ++ msg)
    local (append ", geeks!") level2
    debug ("level1-2: " ++ msg)
    return (msg ++ ", world")

level2 :: ReaderT Env IO String
level2 = do
    msg <- asks envMsg
    debug ("level2-1: " ++ msg)
    local (append " How are you?") level3
    debug ("level2-2: " ++ msg)
    return (msg ++ "This will be ignored...")

level3 :: ReaderT Env IO String
level3 = do
    msg <- asks envMsg
    debug ("level3:   " ++ msg)
    return (msg ++ "This will be ignored too")

debug :: String -> ReaderT Env IO ()
debug s = do
    liftIO $ putStrLn s

main :: IO ()
main = do
   result <- runReaderT (level0 "hello") emptyEnv
   putStrLn ("result:   " ++ result)

実行すると以下のようになる。注目すべきは、level3を頂点にして、それ以降のメッセージが前のものに戻っていっている所。これは、環境が局所的に操作されている証拠。

% ghci reader.hs
*Main> main
level1-1: hello
level2-1: hello, geeks!
level3:   hello, geeks! How are you?
level2-2: hello, geeks!
level1-2: hello
result:   hello, world

local関数は局所的(local)に環境を操作するから、localという関数名がついているのだと思う。

Perlにはlocal演算子があるけれども、そのlocal演算子と今回扱ったlocal関数は感覚的に似ている。

それにしても、Readerモナドの簡単なサンプルが見つからなくて苦労した。

今までPerlPHPのようにドキュメントやサンプルが無数にあるのが当たり前な環境になれていたので、Haskellを勉強するようになってからそれらの有難さが身にしみてわかるようになった。