• ビジネス
  • IT
  • テクノロジー
  • 医療
  • 建設・不動産
  • TRENDY
  • WOMAN
  • ショッピング
  • 転職
  • ナショジオ
  • 日経電子版
  • 日経BP
  • PR

  • PR

  • PR

  • PR

  • PR

本物のプログラマはHaskellを使う

第29回 グローバル変数の代わりに使えるReaderモナドとWriterモナド

2009/03/05 ITpro

 Haskellでは,グローバル変数の使用による副作用の発生を防ぐため,グローバル変数として使える可能性のある機能は,IOモナドやSTモナドといった特定のモナドでしか利用できないようになっています。しかし,ときにはグローバル変数に頼りたくなることもあるでしょう。

 そんなときでも,本物のグローバル変数を使わなければならない場合は実はあまりありません。たいていは,より制限された何らかのモデルで代用できます。代わりに利用できる機能さえ知っていれば,グローバル変数を使わずに済むのです。

 今回は,グローバル変数の代わりに使用できる二つのモナドを紹介します。

「読み取り専用」のReaderモナド

 プログラムの実行時に決まるような大域的な情報を,プログラムの何個所かで共有するにはどうすればよいでしょうか?

 同じ値を利用する場合,その値を引数やデータ型の一部として関数から関数へと受け渡していくのが常道です。しかし,関数間での受け渡しが幾重にも連なる場合には,このような値の引き回しをわずらわしく感じることがあります。

 そこで「unsafePerformIOを使い,プログラム全体で共有できるような,いわゆるグローバル変数を作る」という手法を採りたくなるかもしれません。しかし,安易なunsafePerformIOの使用は,バグの温床である無用の複雑さをプログラムに持ち込みます。unsafePerformIOでのI/Oアクションは非同期で実行されるため,unsafePerformIOを使うと,スレッドによる並行処理と同様の問題が起こる可能性があるのです。この問題を回避するには,グローバル変数を読み込み専用にして一切の書き込みを行わないよう,注意深くプログラミングする必要があります。

 よりHaskellらしい代替手段としては,StateモナドやSTモナドなどの「状態」を扱うモナドの利用が挙げられます。これらのモナドを利用すれば,値を共有する部分をモナドを利用する部分に局所化できます。モナドを用いることで,グローバル変数よりプログラムの問題を発見しやすくなるという利点もあります。もしモナドを利用するコードがソースコード内の多くの部分に散らばっていれば,部品化や抽象化がうまくいっていない兆候ととらえられるのです。

 ただし,StateモナドやSTモナドにも問題があります。これらのモナドはあくまで状態を扱うためのものなので,状態を「変更を行わない値」として扱いたい場合であっても,その意図を示せません。その結果,後からモナドを扱うユーザーが状態を勝手に書き変えてしまい,意図しないバグの原因になる可能性があります。

 値を共有するだけであれば,「読み取り専用」のモナドを使うべきでしょう。このために提供されているのがReaderモナドです。Readerモナドは,mtlパッケージのControl.Monad.Readerモジュールで提供されています。

newtype Reader r a = Reader {
    {- |
    Runs @Reader@ and extracts the final value from it.
    To extract the value apply @(runReader reader)@ to an environment value.  
    Parameters:

    * A @Reader@ to run.

    * An initial environment.
    -}
    runReader :: r -> a
}

~ 略 ~

instance Functor (Reader r) where
    fmap f m = Reader $ \r -> f (runReader m r)

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

 Readerは「r->a」という型の関数を格納するコンテナです。この関数の引数であるr型の値が,Readerモナドを使って行う計算の中で共有する値になります。「runReaderフィールドを使ってReader型から関数を取り出し,その関数を値に適用するという操作」が,「Readerが保持する暗黙の変数を『共有する値』に束縛する環境」に相当する,と考えればイメージしやすいかもしれません(参考リンク)。こうした性質に着目して,Readerモナドを「環境モナド」と呼ぶこともあります( 参考リンク)。

 Readerモナドのreturnと>>=は,Reader型に格納するための関数を作成します。ただし,returnはワイルドカード・パターンを使ってrの値を切り捨てるラムダ抽象として定義されているため,returnと>>=だけではReaderモナドが保持する値を参照できません。

Prelude Control.Monad.Reader> runReader (return 0) "group"
0
Prelude Control.Monad.Reader> runReader (return 0 >>= return) "category"
0
Prelude Control.Monad.Reader> runReader (return "semi-group" >> return 0) "category"
0

 Readerモナドが保持する値を参照するには,MonadReaderのaskメソッドを使います。

class (Monad m) => MonadReader r m | m -> r where
    -- | Retrieves the monad environment.
    ask   :: m r
    {- | Executes a computation in a modified environment. Parameters:

    * The function to modify the environment.

    * @Reader@ to run.

    * The resulting @Reader@.
    -}
    local :: (r -> r) -> m a -> m a

Prelude Control.Monad.Reader> runReader ask "group"
"group"
Prelude Control.Monad.Reader> runReader (return 0 >> ask) "category"
"category"

 askがどのようにして値を参照可能にしているかを見ていきましょう。Reader型のMonadReaderクラスのインスタンスの定義は以下の通りです。

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

 askはidを格納しているため,「runReader ask」は「Readerモナドが保持する値を参照する」という期待通りの操作になります。

 returnだけを使っている式やaskだけを使っている式については,ここまでの説明で理解できたと思います。では,>>=や>>を使って組み立てたReaderモナドを利用する式全体が正しくふるまうのはなぜでしょうか? もう一度Readerモナドでの>>=演算子の定義を見てみましょう。

instance Monad (Reader r) where
~ 略 ~
    m >>= k  = Reader $ \r -> runReader (k (runReader m r)) r

 >>=では,環境であるrをrunReaderで左辺の式mと右辺の式kに渡しています。その結果,mとkで値が共有され,Readerモナド全体でaskメソッドを使って値を参照できるようになるのです。

 ここまではReaderモナド全体で同じ値を使用することを前提に話を進めてきました。ときには,Readerモナド全体で利用しているのとは別の値を一時的に共有したくなるかもしれません。そんなときにStateモナドを使うのは明らかに悪い方法です。Readerモナドが持つ「読み取り専用」の意図が失われるのに加え,一時的に変更した値を元に戻すのを忘れてバグを発生させる可能性もあります。

 そこで,一時的に違う値を使いたいという要求に対応するメソッドが用意されています。MonadReaderクラスのもう一つのメソッドであるlocalです。

class (Monad m) => MonadReader r m | m -> r where
~ 略 ~
    local :: (r -> r) -> m a -> m a

instance MonadReader r (Reader r) where
~ 略 ~
    local f m = Reader $ runReader m . f

 localは,Readerモナドが保持する値に適用する関数fと,Readerモナドが作成する関数を合成します。この結果,localは「関数fの適用によって変更された値を扱うためのスコープ」として働くのです。実際に試してみましょう。

Prelude Control.Monad.Reader> runReader (local ("semi-" ++) ask) "group"
"semi-group"
Prelude Control.Monad.Reader> runReader (local ("semi-" ++) (return () >> ask)) "group"
"semi-group"
Prelude Control.Monad.Reader> runReader (local ("semi-" ++) (return ()) >> ask) "group"
"group"

 localによって実際に値が変更されていることと,localの外側にあるaskには値の変更による影響が及んでいないことを確認できました。

 localは便利ですが,MonadReaderクラスの制約により,Readerモナドがもともと保持していた型しか使用できません。このような制限が邪魔になる場合もあります。そこで,Control.Monad.Readerモジュールでは,localの型を一般化したバージョンであるwithReaderという関数が用意されています。

withReader :: (r' -> r) -> Reader r a -> Reader r' a
withReader f m = Reader $ runReader m . f

Prelude Control.Monad.Reader> runReader (withReader show ask) 0
"0"
Prelude Control.Monad.Reader> runReader (withReader show (return "category") >> ask) 0
0
Prelude Control.Monad.Reader> runReader (withReader (const 0) ask) "group"
0
Prelude Control.Monad.Reader> runReader (withReader (const 0) (return 0) >> ask) "group"
"group"

 withReaderもlocalと同じように働いているのがわかると思います。

あなたにお薦め

連載新着

連載目次を見る

今のおすすめ記事

ITpro SPECIALPR

What’s New!

経営

アプリケーション/DB/ミドルウエア

クラウド

運用管理

設計/開発

クライアント/OA機器

ネットワーク/通信サービス

セキュリティ

もっと見る