人気ブログランキング | 話題のタグを見る

All About Monads 読解 15

The Reader monad

Overview

Computations (モナド値) のタイプ
 共通の環境の値を利用する computation に使われる。

バインド演算子の動作:
 モナド値は環境 (environment) を引数とし、値を戻値とする関数だ。バインド演算子は左項の戻値に右後の関数を関数適用させるが、左項と右項は共通の環境を利用する。

どんな処理に有用か:
 モナドの関数どうしで、変数の値や、環境を共有している場合。

Zero と plus
 なし

代表的な型:
 Reader [(String,Value)] a

Motivation

ある種のプログラムでは、共有する環境で動作する場合がある。この場合の環境とは、たとえば、複数の変数とその値である。こういうプログラムでは、環境変数の値を読んだり、環境変数の値を変更したりする。しかし、これらのプログラムは State モナドを使うほど汎用化されている必要がない。

Reader モナドはこれらのプログラムに特化してデザインされていて、State モナドを使うよりは簡潔で読みやすい記述ができる。

Definition

次に示す定義は、multi-parameter タイプクラスと funDeps という言語拡張を使っている。標準の Haskell 98 レポートにはない仕様だ。しかし、Reader モナドを使いこなすのに、これらの言語拡張の詳細を知る必要はない。

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

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

Reader モナドのコンテナの中のデータは e (環境) -> a 型の関数だ。Reader モナドの (モナド型関数を >>= で連結した連鎖の) 最終的な値を得るのは簡単だ。単に (runReader reader) environment を実行するだけだ。

return 関数は単に値 a を返す関数 \e -> a を作って Reader 型のコンテナに入れるだけだ。バインド演算子 (>>=) の動作は少し複雑だ。まず左項の (Reader r) パターンによって関数 r :: e -> a が取り出される。また右項の関数 f :: a -> Reader e a は、a 型の値を引数に取り、Reader e a 型を返す関数だ。この関数 f は Reader モナドの Kleisli 射である。バインド演算子は、無名関数 \e -> runReader (f (r e)) e を作成して、Reader データコンストラクタのコンテナに入れる。

Reader データコンストラクタで Reader e a 型のコンテナに入れられる無名関数

/e -> runReader (f (r e)) e

についてみてみると、これは1引数 e の関数だ。この引数 e にまず r が関数適用される。その戻値は a 型の値だ。これに f を関数適用すると Reader e a 型の値が戻る。さらにこの Reader e a 型の値に runReader アクセサを関数適用して得られる e -> a 型の関数を e に関数適用すると a 型の値が戻される。こうしてできた e -> a 型の無名関数を Reader データコンストラクタで Reader e a 型にくるんでバインド演算子の動作が完了する。

ややこしい仕組みだが、これのおかげで、環境(変数)をモナドのプログラムから隠蔽しながら次のように、モナド値と Kleisli 射との連携を作っていくことができる。

Prelude> import Control.Monad.Reader
Prelude Control.Monad.Reader> runReader (return 2) [("var0",0)]
2
Prelude Control.Monad.Reader> runReader (return 2 >>= \a -> return (a*a)) [("var0",0)]
4

このように、バインド演算子は左項の Reader モナド値を抽出して左の Kleisli 関数に引数として与えるが、両者とも共通の環境を共有している。

class MonadReader e m | m -> e where
    ask   :: m e
    local :: (e -> e) -> m a -> m a

instance MonadReader (Reader e) where
    ask       = Reader id
    local f c = Reader $ \e -> runReader c (f e)

asks :: (MonadReader e m) => (e -> a) -> m a
asks sel = ask >>= return . sel

MonadReader クラスは数多くの便利な関数を Reader モナドに提供している。たとえば、ask 関数は環境の値を取り出す。これは普通 selector または lookup 関数と組み合わせて使われる。

Prelude Control.Monad.Reader> runReader (ask) [("var0",0)]
[("var0",0)]
Prelude Control.Monad.Reader> runReader (do env <- ask; let (Just val) = lookup "var0" env in return val) [("var0",0)]
0

また、 local 関数は第1引数で環境を加工して第2引数の Kleisli 射を実行する。

Prelude Control.Monad.Reader> runReader (local (("var1",1):) ask) [("var0",0)]
[("var1",1),("var0",0)]

このように、どのモナドも return 関数でデータをモナド値にラッピングし、>>= 演算子で左項のモナド値から生のデータを取り出して、右項の Kliesli 射を関数適用するという点は共通している。言い換えると、この return と >>= の働きを掴んでいたら、モナドのプログラムはどんなモナドについても皆同じ様式でプログラムできる事になる。

Example

example16.hs はテンプレートファイルを読み込んで、環境を設定するプログラムだ。テンプレートファイルでは変数の使用やテンプレートのインクルードができる。Readerモナドを利用することで、ask 関数と local 関数を使うことができる。ask 関数は環境からデータを読み出し、local 関数は環境の編集を行う。

example16.hs はコピペで動かすことができるが、import 部分の変更が必要だ。

import Text.ParserCombinators.Parsec
import Text.ParserCombinators.Parsec.Token

import System.IO.Error hiding (try) -- "try" is also defined in the Parsec libraries
import Control.Monad
import System.Environment
import Data.Maybe
import Data.List (intersperse)
import Control.Monad.Reader

また、example16.hs をテストするために template.txt ファイルが必要になるのでこれもコピーしておく必要がある。

実行例は次のようになる。

Prelude> :l example16.hs
[1 of 1] Compiling Main ( example16.hs, interpreted )
Ok, modules loaded: Main.
*Main> :main template.txt '$<#1>'
Loading package transformers-0.3.0.0 ... linking ... done.
Loading package bytestring-0.9.2.1 ... linking ... done.
Loading package mtl-2.1.1 ... linking ... done.
Loading package array-0.4.0.0 ... linking ... done.
Loading package deepseq-1.3.0.0 ... linking ... done.
Loading package text-0.11.2.0 ... linking ... done.
Loading package parsec-3.1.2 ... linking ... done.
'
this is just plain text, it contains $, | and [ but no actual
template!

= boring but necessary test
'*Main>

example16.hs は main 関数の部分を除くと3つのパートからなっている。1つ目はテンプレートのデータ型を定義した部分だ。それぞれのデータ型は Show クラスのインスタンスにしてあるので、ghci で試して見ることができる。

*Main> T "hello"
hello
*Main> V (T "hello")
${hello}
*Main> Q (T "hello")
$"hello"
*Main> D (T "hello") (T "world")
hello=world
*Main> I (T "hello") [(D (T "foo") (T "bar"))]
$<hello|foo=bar>
*Main> C [(T "hello"), (V (T "world"))]
hello${world}
*Main> NT "hello" (T "world")
[hello]world[END]

2番めの部分は、テンプレートファイルから文字列を取り出すためのパーサの記述だ。

*Main> parse name "" "[hello]"
Right "hello"
*Main> parse end "" "[END]"
Right "[END]"
*Main> parse templateFile "" "[#1]world[END]"
Right [[#1]world[END]
]

3番目の部分は環境からデータを読みだしたり、書き込んだりする部分だ。この部分に Reader モナドが使われている。環境は Environment 型で定義されている。

data Environment = Env {templates::[(String,Template)], variables::[(String,String)]}

*Main> templates (Env [("hello", T "world")] [("foo", "bar")])
[("hello",world)]

lookupVar 関数は環境の変数名から、変数の内容を取り出す。

*Main> lookupVar "foo" (Env [("hello", T "world")] [("foo", "bar")])
Just "bar"

lookupTemplate 関数は環境のテンプレート名からテンプレートを取り出す。

*Main> lookupTemplate "hello" (Env [("hello", T "world")] [("foo", "bar")])
Just world

addDefs は環境の variables 部分に新しい定義を追加する。

*Main> variables (addDefs [("hoge", "fuga")] (Env [("hello", T "world")] [("foo", "bar")]))
[("hoge","fuga"),("foo","bar")]

Reader モナドが使われているのは resolve 関数だ。

resolveDef 関数は Definition 型を (String, String) 型に変換する Reader モナドの関数。したがって、Reader 型からコンテナ内の関数を runReader 関数で取り出して使う。

*Main> runReader (resolveDef (D (T "foo") (T "bar"))) (Env [("", T "")] [("","")])
("foo","bar")

Reader モナドに特徴的な関数は環境の値を取り出す ask 関数だ。example16.hs の例では ask 関数ではなく ask 関数のユーティリティ関数 asks だ。asks 関数の定義は次のようになる。

asks :: (MonadReader r m) => (r -> a) -> m a
asks f = do
    r <- ask
    return (f r)

つまり、環境を ask で取り出してそれに関数 f を関数適用した結果を return 関数で返す。

*Main> runReader (asks (*2)) 2
4

asks を使った resolve 関数の定義は次のようになる。

resolve (V t)    = do varName  <- resolve t
                      varValue <- asks (lookupVar varName)
                      return $ maybe "" id varValue

resolve の引数が (V t) 型の場合 t の値を取り出して、それをキーに環境を検索し、キーがヒットしたらその値を返すという動作をする。

*Main> runReader (resolve (V (T "foo"))) (Env [("hoge", T "fuga")] [("foo", "bar")])
"bar"

上のプログラムの maybe 関数は Data.Maybe モジュールの関数で、型は次のようになる。

*Main> :t maybe
maybe :: b -> (a -> b) -> Maybe a -> b

第1引数は初期値で、第2引数は関数 f で、第3引数が Maybe a 型だ。第3引数が Just x のときは x に関数 f を適用して f x を返し、Nothing の時は初期値をそのまま帰す。ghci でテストすると次のようになる。

*Main> runReader (asks (*2)) 2
4

また、local を使った定義は resolve 関数の引数が (I t ds) の場合に見られる。

resolve (I t ds) = do tmplName <- resolve t
                      body     <- asks (lookupTemplate tmplName)
                      case body of
                        Just t' -> do defs <- mapM resolveDef ds
                                      local (addDefs defs) (resolve t')
                        Nothing -> return ""

上の resolve (I t ds) は、最初にテンプレート名 t のテンプレートを環境から検索する。仮に検索されたテンプレートが V (T String) 型だったとする。また、local の第1引数は、環境を加工する。この場合 addDefs だから環境 Env {templates :: [(String, Template)], variables :: [(String,String)]} の variables リストに、(ローカルに)引数の定義リストを付け加える。local の第2引数は (resolve (V (T String))) だから variables リストの中から String に合致するペアを探索することになる。この場合(ローカルに)環境に付け加えられたリストも検索することになる。

*Main> runReader (resolve (I (T "foo") [D (T "bar") (T "baz")])) (Env [("foo", V (T "qux"))] [("qux", "foobar")])
"foobar"

*Main> runReader (resolve (I (T "foo") [D (T "bar") (T "baz")])) (Env [("foo", V (T "bar"))] [("qux", "foobar")])
"baz"

Haskell のプログラムは関数で記述されているので、個々の関数のテストが ghci で簡単にできる。ソースの読解は、個々の関数のテストをしているうちに自然に分かってくる。

次に示すのが All About Monads に掲載されている example16.hs の Reader モナドに関する部分の抜粋だ。asks と local の出現している当たりに注目すると良いだろう。

-- This the abstract syntax representation of a template
--              Text       Variable     Quote        Include                   Compound
data Template = T String | V Template | Q Template | I Template [Definition] | C [Template]
data Definition = D Template Template

-- Our environment consists of an association list of named templates and
-- an association list of named variable values.
data Environment = Env {templates::[(String,Template)],
                        variables::[(String,String)]}

-- lookup a variable from the environment
lookupVar :: String -> Environment -> Maybe String
lookupVar name env = lookup name (variables env)

-- lookup a template from the environment
lookupTemplate :: String -> Environment -> Maybe Template
lookupTemplate name env = lookup name (templates env)

-- add a list of resolved definitions to the environment
addDefs :: [(String,String)] -> Environment -> Environment
addDefs defs env = env {variables = defs ++ (variables env)}
                      
-- resolve a Definition and produce a (name,value) pair
resolveDef :: Definition -> Reader Environment (String,String)
resolveDef (D t d) = do name <- resolve t
                        value <- resolve d
                        return (name,value)

-- resolve a template into a string
resolve :: Template -> Reader Environment (String)
resolve (T s)    = return s
resolve (V t)    = do varName  <- resolve t
                      varValue <- asks (lookupVar varName)
                  return $ maybe "" id varValue
resolve (Q t)    = do tmplName <- resolve t
                      body     <- asks (lookupTemplate tmplName)
                      return $ maybe "" show body
resolve (I t ds) = do tmplName <- resolve t
                      body     <- asks (lookupTemplate tmplName)
                      case body of
                        Just t' -> do defs <- mapM resolveDef ds
                                      local (addDefs defs) (resolve t')
                        Nothing -> return ""
resolve (C ts)   = (liftM concat) (mapM resolve ts)

前へ 目次 次へ
by tnomura9 | 2013-06-17 13:36 | Haskell | Comments(0)
<< 最近のピンキー ぷぷっぴどぅ〜 >>