あどけない話

Internet technologies

Haskell の Monad とは言語内DSLのフレームワークである

この記事は、Haskellを勉強してある程度分かったけど、Monadで挫折した人のための記事です。10分間で、Monadに対する納得感を得ることを目的としています。他の言語でいう「モナド」にも通用する内容ですが、Haskell の文法や用語を用いますので、他の言語の利用者に分かるかは不明です。

Haskellを勉強したのですから、

  • 代数データ型
  • 型クラス

は分かっていることにします。Monad は、単なる型クラスの一つで、それ以上でもそれ以下でもありませんから、この二つが分かってないと話になりません。

また、言語内DSL(以下、DSLと略記)という考え方を知っていることも仮定します。Monad とは、DSLフレームワークという直感を与えるのが、この記事の主眼ですからね。

さらに、構造化定理をいう単語を聞いてもビビらない人を想定しています。逐次、反復、分岐があれば、計算しうる計算はすべて記述できるという定理です。Monad の説明にありがちな圏論を持ち出されるより、よっぽどいいでしょう?

Monad とは何か?

繰り返しになりますが、Monad とは単に型クラスの一つです。そして、Monad とは、DSLのためのフレームワークです。つまり、ある代数型を Moand 型クラスのインスタンスにすれば、何らかの DSL になるということです!

DSLの例を挙げてみましょう。

  • Maybe は、失敗する可能性がある計算のためのDSLです
  • State は、あたかもグローバル変数のような状態があるかのように見せかけるための DSLです
  • IO は、命令プログラミングのための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 ()

まとめ

m a という形の型を Functor、Applicative、Monad と出世させていけば、利用できる機能が多くなり、最後にりっぱな DSL となったことが分かったでしょう。Monad を利用するだけなら、これ以上 Monad のことを深追いする必要はありません。あとは、たくさんコードを書いて慣れるのみです。

もう少し詳しく勉強したい人のために、参考文献/記事を挙げておきます。