【Haskell】ReaderTデザインパターンについて

2018-09-27

HaskellでReaderTを使って組むとよい、という記事を見てちょっと試してみた感想。

The ReaderT Design Patternに、 アプリ全体で参照したい値や書き換えたい値をReaderTで扱うとよい、というようなことが書いてあった。 要点をよくわかってないけど試したところまでメモしておく。

ReaderT の前に、関連するモナドの動作もよく理解してないので復習しつつ。

IORef

値を書き換えたい場合、IORefが使える:

import Data.IORef (IORef, newIORef, readIORef, writeIORef)

square :: IORef Int -> IO ()
square ref = do
x <- readIORef ref
writeIORef ref $ x * x

main = do
ref <- newIORef 1111
square ref
y <- readIORef ref -- 1234321
print y

扱うにはIOモナド内で行う必要がある。

Reader

値を参照したいだけの場合、Readerモナドが使える。 runReaderで走る内部に環境を暗黙的に引き回すことができて、 askで値の取得ができる:

import Control.Monad.Reader (Reader, runReader, ask)

square :: Reader Int Int
square = do
x <- ask
return $ x * x

main = do
let x = 1111
let y = runReader square x -- 1234321
print y

ReaderT

Readerで渡す環境にIORefを入れてもIOモナドじゃないので値を読んだり書いたりできない。 そこでReaderTモナドを使用する。 ReaderTモナド内部でliftIOでIOモナドを実行できる。 走らせるにはrunReaderTを使う:

import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Control.Monad.Reader (ReaderT, runReaderT, ask)
import Control.Monad.State (liftIO)

square :: ReaderT (IORef Int) IO ()
square = do
ref <- ask
liftIO $ do
x <- readIORef ref
writeIORef ref (x * x)

main = do
v <- newIORef 1111
runReaderT square v
y <- readIORef v -- 1234321
print y

ReaderT Design Pattern

ReaderTの場合と内容は変わらないけど、The ReaderT Design Patternの冒頭で書かれているようにEnvAppを定義してみる:

import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Control.Monad.Reader (ReaderT, runReaderT, ask)
import Control.Monad.State (liftIO)

data Env = Env {
ref :: IORef Int
}

type App = ReaderT Env IO

square :: App ()
square = do
env <- ask
liftIO $ do
x <- readIORef $ ref env
writeIORef (ref env) $ x * x

runApp :: App a -> Env -> IO a
runApp f env = do
runReaderT f env

main = do
ref <- newIORef 1111
let env = Env { ref = ref }
runApp square env
y <- readIORef ref
print y

Haskellで作られているNESエミュレータhnesEmulator型をそのように使っているみたいだった (Nesがアプリ全体の情報を保持する環境か)。

考察

  • 「Better globals」とのことで暗黙的に全体の設定を引き回すことができて必要な箇所で参照できる、というのが利点とのことで、
  • 値を書き換えたい場合にも対応できて、
  • こういう仕組みで作ればアプリ作れるよね

ということでいいかと思ったんだけど、

  • IOモナドの代わりにReaderTモナドになっただけで、全体をモナドとして書かなきゃいけないことは変わりない
  • 引き渡す環境がアプリ全体どこからでも参照・変更される可能性があるというのは、スパゲティになる可能性が高くてダメそう
  • それを考慮して部分によって渡す環境を絞るのは何が必要か決めるのが難しいし面倒

というので問題があるんじゃないか、と思った。

まあ元記事もHaskell自体もよく理解してないのでなんともなんだけど…。