この記事は、Haskellを勉強してある程度分かったけど、Monadで挫折した人のための記事です。10分間で、Monadに対する納得感を得ることを目的としています。他の言語でいう「モナド」にも通用する内容ですが、Haskell の文法や用語を用いますので、他の言語の利用者に分かるかは不明です。
Haskellを勉強したのですから、
- 代数データ型
- 型クラス
は分かっていることにします。Monad は、単なる型クラスの一つで、それ以上でもそれ以下でもありませんから、この二つが分かってないと話になりません。
また、言語内DSL(以下、DSLと略記)という考え方を知っていることも仮定します。Monad とは、DSLのフレームワークという直感を与えるのが、この記事の主眼ですからね。
さらに、構造化定理をいう単語を聞いてもビビらない人を想定しています。逐次、反復、分岐があれば、計算しうる計算はすべて記述できるという定理です。Monad の説明にありがちな圏論を持ち出されるより、よっぽどいいでしょう?
Monad とは何か?
繰り返しになりますが、Monad とは単に型クラスの一つです。そして、Monad とは、DSLのためのフレームワークです。つまり、ある代数型を Moand 型クラスのインスタンスにすれば、何らかの DSL になるということです!
DSLの例を挙げてみましょう。
代数データ型
驚くべきことに、Haskell で m a という形の代数データ型を定義すると、それはDSLになる可能性を秘めています。一番簡単な以下のような代数データ型でさえ、(何もしないとういう)DSLになります。
data Identity a = Identity a
一般的に関数は a -> b という型の形を持っています。一方、a -> m b という形の関数は、m のところに「計算以外の仕事」という意味が含まれるようになります。(「計算以外の仕事」のことを命令型では「副作用」、関数型ではエフェクトと呼んだりします。) Monad のインスタンスとは、この計算以外の仕事をする DSL なのです。
Functor 型クラス
代数データ型を定義すれば、DSLになることが分かりましたが。次は、このDSLの中で、(a -> b の形をした)「既存の関数」が使いたくなるでしょう。この既存の関数と DSL を結びつけるのが Functor 型クラスです。
class Functor m where (<$>) :: (a -> b) -> m a -> m b -- 実際は fmap
こんな風に使えます。
> (+1) <$> Identity 1 Identity 2
Applicative 型クラス
DSLというからには、構造化定理でいう逐次、反復、分岐ができるべきでしょう。この内の二つ、つまり逐次と反復を実現するのが、Applicative 型クラスです。
class Functor m => Applicative m where return :: a -> m a -- 実際は pure (<*>) :: m (a -> b) -> m a -> m b
逐次と反復がないと実現できない sqnc という関数をこれらの関数を使って定義してみましょう。
sqnc :: Applicative m => [m a] -> m [a] sqnc [] = return [] -- 実際は pure にしてね sqnc (x:xs) = (:) <$> x <*> sqnc xs
以下のように逐次実行できます。
sqnc [putStrLn "Hello", putStrLn "World"] Hello World [(),()]
また、sqnc の中では再帰が使えていますから、反復できているのも明らかです。
Monad 型クラス
残るは分岐です。これを実現するのが、Monad です。
class Applicative m => Monad m where (>>=) :: m a -> (a -> m b) -> m b
分岐の例を一つ示しましょう。ファイルがあったら削除するというコードは、以下のようになります。
doesFileExist :: FilePath -> IO Bool removeFile :: FilePath -> IO () removeFileIfExist :: FilePath -> IO () removeFileIfExist file = doesFileExist file >>= \exist -> if exist then removeFile file else return ()