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

自前のデータ型を Monad のインスタンスにする。

自前のデータ型を定義して、それを Monad クラスのインスタンスにしたら、Monad や Applicative などの関数で箱入りデータを扱うのを初心者に説明するのに便利だと思いついた。

そこで M a という自前のデータ型を作り Monad クラスのインスタンスにしようとしたら、ghc 7.10.3 では大変なことになっていた。

ghc 7.10.3 ではデータ型を Monad クラスのインスタンスにするには、そのデータ型は Functor クラスと Applicative クラスのインスタンスでないといけないようだ。Monad クラスの superclass が Applicative クラスで、Applicative クラスの superclass が Functor クラスになっているためだ。

したがって、次のように M a 型を Monad クラスのインスタンスにするには、まず、Functor クラスのインスタンスにして、その後、今度は Applicative のインスタンスにして、最後に Monad クラスのインスタンスにしなければならない。

Prelude> import Control.Monad

Prelude Control.Monad> data M a = M a deriving Show

Prelude Control.Monad> instance Functor M where fmap f (M x) = M (f x)
Prelude Control.Monad> fmap (*2) (M 2)
M 4

Prelude Control.Monad> instance Applicative M where pure = M; (M f) <*> (M x) = M (f x)
Prelude Control.Monad> (*2) <$> (M 2)
M 4

Prelude Control.Monad> instance Monad M where return = pure; (M x) >>= f = f x
Prelude Control.Monad> M 2 >>= return . (*2)
M 4

ややこしいことになったものだが、それだけ Applicative が重要視されるようになったのだろう。ともあれ、自前のデータ型を Monad クラスのインスタンスにできたのでホッとした。

これを使って Monad を説明しようとしたら次のようになるだろう。

Hakel のデータ型には Int や String などの生のデータの他に、代数的データ型という箱に入ったデータがあります。

例えば Maybe 型の Just 3 などです。普通これらの箱入りデータには、関数を適用できません。例えば、Just 3 の値を2倍にしようとして、(*2) (Just 3) としても型不適合が起きてしまいます。

しかし、これらの箱入りデータの中には、Monad クラスのインスタンスとして定義されているものもあります。Monad クラスのインスタンスには Monad クラスの関数を関数適用できます。Monad クラスの関数は >>= 演算子と return 関数です。これらの関数を使うと箱入りデータの中身を箱を開けずに加工することができます。

説明では分かりにくいので、自前で定義した M a 型の箱入りデータを Monad クラスの関数で操作してみましょう。M a 型のデータはあらかじめ Monad のインスタンスとして登録したので、Monad クラスの関数が使えます。

まず整数 2 を M a 型のデータにして、箱入りデータにしてみましょう。

Prelude Control.Monad> M 2
M 2

しかし、一旦箱入りデータにしてしまった M 2 には (*2) を適用しようとするとエラーになってしまいます。そこで、>>= 演算子の出番です。>>= 演算子は M a 型の箱に入れられた生データを取り出して、左項の a -> M b 型の関数に渡す働きがあります。しかしどんな関数にでも値を渡せるわけではなく、a -> M b 型でなければなりません。そこで a -> M b 型の関数を作ってみましょう。これは受け取った整数を2倍にして M b 型の箱入りデータとして返す関数です。

Prelude Control.Monad> let double x = M (x*2)
Prelude Control.Monad> :t double
double :: Num a => a -> M a

double を :t コマンドで型を調べると確かに a -> M a 型になっていることがわかります。

それでは、先ほどの M 2 と double を >>= で連結してみましょう。

Prelude Control.Monad> M 2 >>= double
M 4

確かに M a 型の箱に入ったデータが2倍の値を保持した箱入りデータになっているのがわかります。しかし、>>= で a -> M b 型の関数に渡され、処理されて戻されたデータは、やはり箱入りデータであることがわかります。箱からは取り出せません。その代わり、この箱型データは次々に関数に連結することができます。例えば次のプログラムは M 2 を doubule に連結し、それをさらにもう一度 double に連結することを示しています。

Prelude Control.Monad> M 2 >>= double >>= double
M 8

その結果 M 2 の箱入りデータの中身は 4 倍されて M 8 という箱入りデータになっています。このように M a 型の箱入りデータが Monad のインスタンスになっているとき、そのデータは次々に a -> M b 型の関数に連結していくことができ、箱の中身を自由に加工できます。しかし、その場合でも戻されるのは必ず箱入りデータで、そのデータを取り出すためには、Monad とは別の関数を定義しなければなりません。

実はこの性質が Monad クラスの目的なのです。>>= 演算子と a -> M b 型の関数を連結してプログラムを作っている限り、箱入りデータを全く箱から出さずに自由に加工できるのです。IO モナドは副作用を純粋関数に伝えることができませんが、それは IO モナドの箱入りデータである IO 型のデータは >>= 演算子によって加工されるかぎり IO a 型という箱から出てくることはないからです。

Monad のこのような性質は、副作用のあるデータを他から隔離するためだけに利用されるわけではありません。箱入りデータの箱を開けずに中身を操作できるため、箱入りデータのみからなるプログラムを作成することができ、プログラムの独立性を確保することができます。

このように Monad の箱入りデータを箱のまま加工できるという性質は、様々な便利な機能を Haskell 提供しています。

return の説明は省略。

追記

Haskell のモナドの使い方は M a 代数的データ型という箱入りデータを、 >>= 演算子を介して a -> M a 型の関数と結合することによって、箱を開けずに中身のデータを加工するというイメージができさえすれば、納得できる。

そのためには、できるだけ簡単な箱と、できるだけ簡単な中身のデータが必要だが、残念ながら、標準のデータ型にはそのようなものはない。IO モナドは取り扱いが難しいし、リストモナドや Maybe モナドは構造が複雑すぎる。

そこで、値1個で箱のバリエーションのない M a 型というこれ以上シンプルにできないモナドを作ってみたらいいのではないかと思った。うまくいったかどうかは自分ではわからない。

Haskell を学び始めた人で IO モナドで躓く人は多いのではないかと思う。しかし、IOモナドに限らずモナドの考え方は Haskeller には必須知識だ。だから、IO モナドから始めるのではなく、操作性を主体としたモナドの考え方のイメージを作ってしまえばそのあとの学習がかなり楽になるのではないだろうか。IO モナドはモナドを理解してから取り組むと簡単に理解できるだろう。

Haskell がなかなか普及しないとはいえ、並列処理のプログラムの時代になったら、Haskell は必須のプログラム言語になるような気がする。人生の早いうちに Haskell と親しむことが大切ではないだろうか。


by tnomura9 | 2016-10-17 14:55 | Haskell | Comments(0)
<< Monoid クラスの使い方 箱入りデータ >>