読者です 読者をやめる 読者になる 読者になる

モナドとはモナドである

この記事を読む前に、絶対に理解出来ないモナドチュートリアルに一度目を通してみてほしい。モナドを理解していく上で、とても重要なことが書かれている。


改めて言おう、モナドモナド。コンテナだとかプログラマブルセミコロンだという説明では、モナドのすべてを正確に表せるとは言い難い。では、モナドを過不足なく説明できる、モナド以外の言葉はあるのか?

実は、モナドを表現し、かつモナドで表現される言葉は存在する。その一つは手続きである。手続き型言語の「手続き」だ。

手続きとは何か

手続きは結果を持つ

おおよそすべての手続きは何らかの結果を持つ。Haskellの()、C言語のvoid、PythonのNone、Rubyのnilなども結果の一種だ。結果が出ないとしたら、そのプログラムは停止しないか、途中で異常終了するだろう。

手続きには最小単位が存在する

処理系が扱っている以上、手続きが際限なく分解できるということはあり得ない。抽象的な文脈なら、「文字を出力する」のも最小単位と見てよいだろう。

「手続きAを実行した後手続きBを実行する」といったものも手続きである

当たり前だが、大事な性質だ。これによって手続きを組み合わせ、大きなプログラムを作ることができる。

「何もしない」のも手続きである

何もしなくても手続きになる。ならないと色々と困る。ただし、「結果を持つ」という性質は満たさなければならない。

さて、手続きの4つの性質を確認したが、以下のような疑問が残る。

  • 「Aを実行した後Bを実行する」とき、Aの結果はどこに行くの?

多くの手続き型言語では代入構文を使って手続きの結果を保存している。Pythonならこんな感じだ。

s = raw_input()

しかし、結果を利用するためだけに変数の仕組みを持ち出すのは美しくない。「変数なんてどの言語でも備えているだろ」と思うかもしれないが、今回はそうではないのだ。そこで、「関数」を使おう。

  • 「Aを実行し、その結果を手続きを返す関数に渡す。そして、返ってきた手続きBを実行する。」

要するに、「現在のスコープに結果がある」という条件を満たせばよい。

Haskellで手続きを表現する

以上の点を踏まえて、Haskellで手続きを表現する方法を考えてみよう。

手続きは「結果」を持つ。

結果の型をaとすると、少なくとも手続きの型はT aのようになる。単なる値と区別しないと作る意味がない。

手続きには最小単位が存在する

とりあえず、以下のものを最小単位としてみよう。

Helloworld :: T () -- Hello, worldを表示する。
GetLine :: T String -- 文字列を入力する。
PutStrLn :: String -> T () -- 文字列を引数に取り、その文字列を表示するという手続きを返す。

ちょっと待った!HelloworldやMyGetLineはコンストラクタなのに(コンストラクタは大文字で始まる)、自由に型を指定することができるのか?

できる、そう、GADTならね。GHCのGADTs拡張を使うと、コンストラクタとその型を列挙して代数的データ型を定義できる。

{-# LANGUAGE GADTs #-}

data T x where
    Helloworld :: T () -- Hello, worldを表示する。
    GetLine :: T String -- 文字列を入力する。
    PutStrLn :: String -> T () -- 文字列を引数に取り、その文字列を表示するという手続きを返す。

GADTはとても便利な機能だ。以降の性質も、GADTを使って表現しよう。

「Aを実行し、その結果を手続きを返す関数に渡す。そして、返ってきた手続きBを実行する。」

これを実現するには、手続きAと、結果を受け取る関数がコンストラクタの引数に含まれていればよい。

infixl 1 :>>=
data T x where
    Helloworld :: T ()
    GetLine :: T String
    PutStrLn :: String -> T ()

    (:>>=) :: T r -> (r -> T a) -> T a

(:>>=)という演算子はなにかに似ている気がするが、今は考えないでほしい。

「何もしない」のも手続きである

ただ結果を保持するだけのコンストラクタを追加する。

data T x where
    Helloworld :: T ()
    GetLine :: T String
    PutStrLn :: String -> T ()

    (:>>=) :: T r -> (r -> T a) -> T a
    Return :: a -> T a

これで、文字を入出力するという操作ができる、手続きの枠組みができた。では、使ってみよう。

Return 42 :: T Int -- 42を返す
Helloworld :>>= \_ -> Return 42 -- Hello, world!した後42を返す
GetLine :>>= PutStrLn -- 入力した文字列をそのまま返す
GetLine :>>= PutStrLn . reverse -- 入力した文字列を逆転させて返す

確かに表現力はある。実は、これがモナドだ。

正体

モナドは、上で定義した4つの性質のうち、「結果を持つ」「合成できる」「何もしない」が保証される構造だ。

Haskellの強力な仕組みである型クラスを使うと、このように表現できる。

class Monad m where
    return :: a -> m a
    (>>=) :: m a -> (a -> m b) -> m b

上で定義したTは、直接Monadインスタンスにできる。

instance Monad T where
    return = Return
    (>>=) = (:>>=)

Monadインスタンスにすると、これから出てくる様々なモナドを、returnと(>>=)という二つの関数で統一的に扱える。しかも、do記法というHaskellの非常に便利な機能を使えるようになる。

do記法を用いると、さながら手続き型言語のように手続きを記述することができる。いや、do自体が手続き型言語であると言った方が良いだろう。

echoReversed = do
    s <- GetLine
    PutStrLn (reverse s)

もっと様々な手続きを書きたければ、Tに新たなコンストラクタを追加すればよい。モナドは、実はとても簡単に作れるものなのだ。

もっと簡単に

(:>>=)とReturnはモナドに必ず必要なため、この部分だけを切り出した構造を作れば、より簡単にモナドを作れそうだ。

data Program x where
    (:>>=) :: Program r -> (r -> Program a) -> Program a
    Return :: a -> Program a

ところが、これだけだと手続きの最小単位がないため意味がない。手続きの最小単位のための型を別に作るようにしてみよう。

data Program t x where
    Unit :: t a -> Program t a
    (:>>=) :: Program t r -> (r -> Program t a) -> Program t a
    Return :: a -> Program t a

data MyActions x where
    Helloworld :: MyActions ()
    GetLine :: MyActions String
    PutStrLn :: String -> MyActions ()

type T = Program MyActions x

こうして得られたTもやはりモナドになる。機能を拡張したいときは、Programを変えずに、MyActionsを変更すれば大丈夫。

このような、機能の最小単位の集まりからモナドを作る仕組みは、Operationalモナドと呼ばれている。これからのHaskellプログラミングで、非常に大きな役割を果たすだろう。

IOの世界

今までモナドを自作してきたが、HaskellではIOモナドという非常に重要なモナドがあらかじめ定義されている。なんと、IOモナドは、処理系がモナドを分解して、現実世界に対する副作用に変換できるのだ!

IOモナドとして表現される手続きをmainとして定義すると、処理系がそれを解釈してコンピュータに副作用を発生させる。これにより、Haskellのプログラムは使えるようになる。

main = putStrLn "Hello, world!"

これを実行すれば、あなたのターミナルにはHello, world!と表示されるだろう。神秘的だ…

IOモナドの中身をプログラムが把握することはできないが、Operationalモナドの中身を把握することは可能だ。これは、処理系が現実世界の中身を把握できなくても、IOモナドの中身を把握できるのと似ている。ということは、Operationalモナドを解釈してIOに影響を及ぼす処理系を作れるのではないだろうか?……作れる。

toIO :: Program MyActions a -> IO a

toIO (Unit HelloWorld) = putStrLn "Hello, world!"
toIO (Unit (PutStrLn str)) = putStrLn str
toIO (Unit GetLine) = getLine

toIO (Return a) = return a
toIO (m :>>= k) = toIO m >>= toIO . k

そう、まさに言語と処理系の関係。Operationalモナドは、一つの手続き型言語を、命令の一覧から自動生成してしまうモナドなのだ。しかも、IOなどの特定の環境に依存しない(クロスプラットフォーム)。処理系がプログラミング言語の挙動をエミュレートする――メタ世界の住人が我々をエミュレートする――という関係が、言語の中に出てきただけなのだ。

ここで作ったOperationalモナドは、より洗練された実装がある。実用する際はこちらを使おう。

ここで紹介した以外にも、Haskellの世界には様々なモナドがある。モナドの六つの系統モナドのすべてを参考に、様々なモナドを使って、そして作ってみてほしい。

まとめ

  • 手続きモナド
  • モナド手続きであり、それ自体が一つの言語
  • モナドは沢山あり、それらを変換する処理系を作ることができる
  • すべてのモナドはOperationalに通ず