Haskell 入門


モナドの使い方

Maybe モナド

Haskell では Maybe 型をよく使う。Maybe 型の data 宣言は次のようになる。

Prelude> :info Maybe
data Maybe a = Nothing | Just a -- Defined in `Data.Maybe'

Maybe 型のデータコンストラクタは Nothing と Just a だ。検索が失敗したときは Nothing を返し、検索が成功したときはその値を Just "foo" のようにして返すというような使い方をする。Prelude には標準の関数でペアのリストからキーの値が fst 要素に一致するペアの snd 要素の値を求める lookup という関数がある。この lookup の戻値が Maybe 型だ。

Prelude> lookup 3 [(1,'a'),(2,'b'),(3,'c')]
Just 'c'
Prelude> lookup 4 [(1,'a'),(2,'b'),(3,'c')]
Nothing

この Maybe 型だが、実はモナドなのだ。

このエントリーの目的は Maybe モナドを使って、モナドの利用法を紹介することだ。モナドとは何かというような高尚な話には一切立ち入らない。とにかく Maybe というようなモナドがあった時どう使うのかという事だけを述べる。

モナドを扱うまえに少々用語を整理してみたい。用語といっても、管理人の私的なものだが、用語がないと説明が面倒だ。それに、用語といっても次の4つしかないので混乱はしないだろう。

モナド値
モナドの値のこと。Maybe モナドでは Maybe 型の値のことだ。Notning や Just "foo" などを指す。
モナド型関数( Kleisli 射 )
引数が一つで戻値がモナド値の関数のこと。bar :: a -> Maybe a 型の関数 bar のことだ。
return 関数
引数を一つとり、モナド値にラッピングして返す関数。 return "foo" の戻値は Just "foo" になる。
>>= 演算子( bind 演算子 )
左項のモナド値からコンテナの値をとりだして、右項のモナド型関数の引数として渡す関数。

こういうのは ghci で試してみたほうが分かりやすい。まずモナド値のほうだが、Maybe 型のデータコンストラクタを使うと作ることができる。

Prelude> Just "foo"
Just "foo"
Prelude> Nothing
Nothing

次にモナド型関数だが、引数 x の値を2倍して Just でラッピングして戻す double 関数を作ってみる。

Prelude> double x = Just (x * 2)
Prelude> double 2
Just 4

return 関数は単に引数を Maybe 型にラッピングして返すだけだ。

Prelude> (return 2) :: Maybe Int
Just 2

>>= 関数は左項のモナド値からコンテナの値をとりだして、右項のモナド型関数に渡す。

Prelude> Just 2 >>= double
Just 4

モナドを扱うというといかにも高尚なことをしているようだが、上の4つの部品を使いこなすだけだ。たとえば、モナド型関数 addOne を追加する。

Prelude> addOne x = Just (x + 1)
Prelude> addOne 2
Just 3

すると、つぎのように、return、double、addOne を >>= 演算子で数珠つなぎにつないでいくことができる。

Prelude> return 2 >>= double >>= addOne
Just 5

IOモナドでお馴染みの do 記号は >>= でモナド型関数を数珠つなぎする記法のシンタックスシュガーだから上のプログラムは次のように手続き言語風にも書ける。

Prelude> :set +m
Prelude> do
Prelude| x <- return 2
Prelude| y <- double x
Prelude| addOne y
Prelude|
Just 5

Haskell のなかに do 記法によって別の言語を埋め込んだように見える。

つまり、モナドの使い方とは上の4つの要素を組み合わせて、Haskell 世界の中でモジュール化された DSL (domain-specific language) のような操作性を実現させる方法なのだ。

上の例には Nothing が出てこないが、Nothing は検索などの関数の処理が異常終了した時に使われる。例えば、偶数だけを通過させる passEven 関数を作ってみる。

Prelude> passEven x = if (even x) then Just x else Nothing

そうするとこの関数を介したモナド関数のシークエンスは、偶数だけを処理することになる。passEven 関数が Nothing の戻値を返すと、>>= 演算子の性質から、その後の処理の戻値は全て Nothing になる。>>= 演算子のお陰で、異常終了のための特別な処理をすることなく、passEven の処理が成功した場合も、失敗した場合も同じプログラムで対応することができる。

Prelude> return 2 >>= passEven >>= double
Just 4
Prelude> return 1 >>= passEven >>= double
Nothing

モナドのモジュール性

蛇足になるが、モナドのモジュール性について少し説明を追加する。モナドを利用するときのキーポイントは >>= (bind 演算子) だ。バインド演算子は左項にモナド値をとり、右項にモナド型関数をとる。そうしてその演算の値は再びモナド値になる。

モナド値 >>= モナド型関数 ---> モナド値

従ってバインド演算子を介してモナド型関数を次々につなげていく事ができるが、その値は全てモナド値だ。

モナド値 >>= モナド型関数1 >>= モナド型関数2 >>= モナド型関数3 ---> モナド値

したがって、部品であるモナド値とモナド型関数をバインド演算子を介して組み合わせることで、様々な新しい機能を作っていく事ができる。プログラムをレゴブロックのようにつなげていく事ができるのだ。また、その部品となるモナド型関数は「引数が一つで戻り地がモナド値」の関数であれば何でもいいので、簡単に部品の種類を増やす事ができる。

さらに、大切な事は、こうして作られた新しいプログラム自身がやはり、モナドの部品になってしまうという事だ。

こうして、バインド演算子によって結合されたモナドのプログラムは一つのまとまったモナド世界を形作る。そのモナドの世界では、モナド型関数やモナド値を使って作られたものはすべてそのモナドの世界の部品である事が保証される。

つまり、モナドを扱うプログラマーは、モナド内部でのプログラムの部品化とモナド外部のプログラムからの独立性という2重のモジュール性を簡単に手にすることができる。