Haskellで大域変数が欲しい時はReaderモナドを使いましょう

何の知識もなくHaskellでコードを書いていると大域変数を引き回してしまうことが多々あり、非常にイライラします。Haskellで大域変数を実現したいときはReaderモナドを使えば良いようです。微妙にリファレンスが少なくてヤキモキしたので知見を並べていきます。



まずは公式リファレンス。

Control.Monad.Reader - Hackage


続いて定義。

newtype Reader e a = Reader { runReader :: e -> a }

instance Monad (Reader r) where
    return a = Reader $ \_ -> a
    m >>= f  = Reader $ \r -> runReader (f (runReader m r)) r

上を見ればわかるとおり、Readerというのは環境変数(e)を受け取って値を返す関数のことなのです。その関数内では、環境変数を引数に明示しないままあたかも存在しているかのようにコーディングできます。Readerで道路を組み立てて、最後にrunReaderで環境変数を走らせるという感覚がわかりやすいかもしれません。

また、インスタンスの定義からdo記法内でaの型が異なっても良いことがわかります。(僕はここを理解するのに手間取った)

なぜなら、

f :: (a -> m b)

によりモナドに包まれる型が変化して良いようにモナドが定義されているからです。fの引数に(runReader m r)の結果がバインドされて、fから新たなReaderが返ります。


より上層のMonadReaderクラスのインスタンスで補助関数も定義されています。

instance MonadReader r (Reader r) where
    ask       = Reader id
    local f m = Reader $ runReader m . f

askは環境変数を返します。localは環境変数にfを適用した新たな環境のmを返します。runReader mと結合しているだけなので、fの影響は一時的です。

ここまで分かれば、他の関数も読めると思います。
最後に簡単なサンプルを載せておきます。

import Control.Monad.Reader

main = print $ runReader add10 1
-- 11

add10 :: Reader Int Int
add10 = do
  x <- ask                          -- 環境変数(x=1)を得る
  y <- local (+1) add10             -- localの使用例, y=12
  s <- reader . length . show $ x   -- 返り値は自由である例
  return (x+10)                     -- 環境変数に10を加えた値を返す関数を返す


以上です。
Readerモナドを導入することにより大域変数が隠蔽されます。すると、各関数はより自分の役割に集中し、見た目にもその役割が明確になります。積極的に活用していきましょう。