Haskell
HaskellDay 15

モナド入門以前

More than 3 years have passed since last update.


前書き

これはモナドチュートリアルではない、だろう。

この文章を書く動機は、モナドを知らない人がモナドという未知の概念に期待しているものが根本的にずれているかもしれないという懸念である。

命令型言語でプログラミングを覚えた人がモナド、もしくはHaskellや関数型言語(と言われる言語)を学習する際にしばしば受ける助言はこうだ:

「命令型言語で今まで習ったことを全て忘れて取り組むと良いよ」

この助言はそこまで外していないかもしれないが、雑である。

いつか僕の友人がこのようなことを言った:

「プログラマがチームで働く時、必要なのは相手を思いやる気持ちだよね」

この発言は恐らくそこまで外れてはいないが、思いやりで全てを解決しようとすると、全てのコストが非常に高くなる。個々の問題へ目を向け、それぞれの解法を知っておくことによってコストは低くすることが出来ると僕たちは知っている。まあ友人はそんなことより他人のことを考えてくれない特定のメンバーのことが頭にあったのだろうけど。

モナドとは大雑把に言って、「プログラム」である。

もう少し正確に言うならば、Haskellにおいてモナドとは、チューリング完全な静的型付き命令型言語のように扱うことが出来る。


プログラムとしてのモナド


  • なぜモナドが重要なのか


    • 言い換えよう、なぜプログラムが重要なのか



プログラマ(関係者全般を指す)にとってこれは自明であり、プログラマにとってプログラムは重要である。

もう少し他の言語との差異を考慮しつつ説明するならば、「モナドによって僕らはプログラムを第一級の値として扱うことが出来る」からであり、「そのプログラムに適切に型を付けられる」からである。

さらに言うならばそれは「モジュラリティ」のためであり、なぜ関数プログラミングは重要かでJohn Hughesが指摘しているように「問題を部分に分解する能力は解を貼り合せる能力に直接に依存している」のだ、プログラムを適切に分解するには、それを適切に組み合わせる能力が必要なのだ。そのためのモナド則であり、そのためのモナドである。

第一級(first-class)であるとは、おおよそ


  • 変数として名前がつけられる

  • 手続きに引数として渡せる

  • 手続きの結果として返される

  • データ構造に組み込める

ということを意味する(計算機プログラムの構造と解釈 - 1.3.4 値として返される手続き)。ここでは手続きを関数と置き換えて読めばいいだろう。

Haskellでは関数は第一級の身分を持っており、例えば2引数関数の場合、以下の様な型が付く(A, B, Cは既知とする)

function :: A -> B -> C

それに対してモナドを用いた2引数の"プログラム"は例えば以下の様な型をとる(A, B, C, Mを既知とし、Mはモナドとする)

program :: A -> B -> M C

Haskell上で"プログラム"は「モナドを用いた特殊な関数」であるということだ。

つまり、「関数が第一級であるHaskell上では、プログラムを第一級の値として扱える」。

注意点としては、プログラム(モナド)が言語のbuilt-in-featuresでは無い、ということである。

モナドも数ある型クラスの内の一つでしかない。モナドを構築するには、termレベルではλ計算の範囲で可能である。


モナド則とは何か

モナド則(Monad Laws)とはモナドが満たすべき規則であり、Haskellでは以下の3つを指す:

return x >>= f   ==  f x

m >>= return == m
(m >>= f) >>= g == m >>= (\x -> f x >>= g)

モナドとはプログラムであるという観点からモナド則を見てみよう。

ここの等号(==)はHaskellの演算子を意味しない。結果が同じになるくらいに捉えれば良い。


モナド則1

モナド則1つ目は分かり易さのため、左辺を冗長な形に書き換えよう。

return x >>= f            ==  f x

return x >>= (\y -> f y) == f x

xを変数に割り当てて(yとする)からその値をfに渡すこと(law1_lhs)と、fxを直に渡すこと(law1_rhs)は同じでなければならないと言っている。

ここでそれぞれをdo記法で表現しよう。そうすると、モナドは命令型プログラムの様に見える。(<-)を値の変数への割り当て、各行を文(statements; Haskellではactionsと呼ばれる)のように見れば良い。do記法はモナドを命令型言語に「見せる」ための特別なモナド専用のシンタックスシュガー(構文糖衣)であり、そしてモナドは実際のところ命令型プログラムである。

law1_lhs = do

y <- return x
f y
law1_rhs = do
f x

プログラミングしたことがある人なら、それらが同じであることは経験的になんとなく分かるだろう。何を当たり前なことを、と思うかもしれない。この規則はその当たり前の事を要求しているのだ。Haskellでは、モナド(Monad型クラスのインスタンス)が必ずしもモナド則を満たす訳ではない。それはモナドがHaskellのBuilt-in-featuresでないため、もしくは依存型を持っていないためである。Haskellにはこのモナドの様な代数的な性質を要求する数学オブジェクトがしばしば存在する。

僕は代数的性質がプログラミングに直接役に立つと考えている。それは代数的性質が、設計上コードの見通しを立て易くし、実装上コードの複雑度を減らし、テストにおいてその記述を容易にするからである。だがここでは脱線するため省略する。


モナド則2

モナド則の2つ目も冗長な形に書き換えよう。

m >>= return            ==  m

m >>= (\z -> return z) == m

mを実行して結果を変数に割り当て(zとする)、そのzreturnに渡した結果(law2_lhs)と、m自体が同じであるという要請だ。

law2_lhs = do

z <- m
return z
law2_rhs = do
m

モナドをプログラムとみなす観点から言えば、モナド則1と2を合わせて、「returnは値を返す外に何もしない文(statements)でなければならない」という要請を意味する。


モナド則3

モナド則3つめはbind(>>=)に関する要請だ。

これもη展開して変数を補完すると対称性が見易くなる。優先順位を表す括弧も多めに補完しておく。

(m >>= f) >>= g                      ==  m >>= (\x -> f x >>= g)

(m >>= \x -> f x) >>= \y -> g y == m >>= (\x -> f x >>= \y -> g y)
(m >>= (\x -> f x)) >>= (\y -> g y) == m >>= (\x -> (f x >>= (\y -> g y)))

これは「プログラム(又はstatements)が3つあった場合、それらの結合順序が同じならば、結合方法に依らずに結果が同じになること」を要求している。

例えばそう、既存のプログラムが3つあった時(m :: Monad n => n a; f , g :: Monad n => a -> n b)、プログラムは第一級であるので、先の二つ(m, f)を結合しておいて再利用のために名前を付けておき(prog1)、後で3つ目gをつなげたいかもしれない(law3_lhs):

prog1 = do

x <- m
f x
law3_lhs = do
y <- prog1
g y

または、f, gを先に結合させておいて(prog2)、後でmを結合させたいかもしれない(law3_rhs):

prog2 x = do

y <- f x
g y
law3_rhs = do
x <- m
prog2 x

このlaw3_lhslaw3_rhsの結果が等しくなければならないと言っている。

このような要求は実プログラミング上、自然に発生してくる。プログラムの結合順番が同じなら結果は同じになってほしいと自然に思うはずだし、この結果が食い違うと僕らはプログラムの合成に余計な気を使わなければならない。

モナド則3番目はそういったプログラムの結合に関する要求を表している。


モナド則とは、つまりモナドとは

モナド則を満たしたならば、僕らはモナドを心置きなく命令型言語として扱うことが出来る。モナド則とはその為の最低限のハードルである。

ここでモナドの定義に戻ろう。

class  Monad m  where

-- | Sequentially compose two actions, passing any value produced
-- by the first as an argument to the second.
(>>=) :: forall a b. m a -> (a -> m b) -> m b

-- | Inject a value into the monadic type.
return :: a -> m a

MonadのMinimal complete definitionはbind(>>=)とreturnだ。これらはそれぞれモナド則を考慮して、次の様な意味を持つことが分かる。



  • returnは何もしない、単にモナディックな値を返すだけの、最も小さい文(statement)である

  • bind(>>=)は二つの文(statements)を直列に結合する


    • 直列に結合するとは、後の文は先の文の結果を使うことが出来るという意味

    • また、結合順序が一致すれば結合方法に依らず、結果は同じになる




より正確な単語の説明

「モナドはプログラム」であるということを説明した後ではあるが、この文言はキャッチーさを重視してあることと、自然言語の曖昧性のために意図通りに読み取られないかもしれない。そのためもう少し正確に単語を説明しよう。


Monad 型クラス

Haskellで定義されているMonad型クラスは、「手続き」という概念そのものだ。

そして、「命令型プログラミング言語の抽象」でもある。

命令型プログラミング言語とは何かと聞かれたら、前述の通り、文(statements)とその結合(bind)によって定義される言語である。


個々のMonadインスタンス

例えばIOモナドやMaybe, Listモナドといった、Monad型クラスのインスタンスになっている概念を指す。

これは個々の「命令型プログラミング言語」に相当する。

世の中には様々な命令型言語(C, Java, Python, ...)が存在するが、それら言語に対応するMonadインスタンスを作る事は可能だろう。


「個々のMonadインスタンス」を用いた関数

IOモナドの関数や、Maybeモナドの具体的な関数に相当する。

これは個々の具体的に構成された「プログラム」を意味する。

program :: A -> M B

以上から、「モナドはプログラム」であるという文言は大分雑であり、「モナドは手続き」とか「モナドはプログラミング言語の抽象」とか言った方がより正確ではある。ただそう言ったとしても僕の経験上、反応は曖昧なものである。また、それぞれの単語が持つ自然言語の曖昧性、つまり概念そのものを指す場合や、概念の具体例を指す場合があったり、説明の省略も頻繁にあるため、何を意味しているかは個々の文脈から判断する必要がある。この自然言語の曖昧性はモナド自体の抽象度の高さと相まってモナドの理解を妨げているように思う。さらに個々人のモナドの不完全な理解度による言及が拍車をかけているようにも思う。


具体的なモナド、具体的な小さなプログラミング言語

具体的なモナドを見ていこう。


Maybeモナド

このプログラミング言語は、文が一つでも失敗すると全体のプログラムが失敗するという特徴を持っている。


Listモナド

文が複数の可能性を返す事が出来るプログラミング言語である。しばしば非決定性計算と呼ばれる。


IOモナド

任意のIOが実行出来る命令型言語である。馴染みある人も多いだろう。


Stateモナド

文の背後に状態を一つ持つ事が可能なプログラミング言語である。その状態にはAPI(get/put)を用いてアクセスする。


Exceptモナド

文で値が返る変わりに例外が投げられる可能性があるプログラミング言語である。


モナドの組み合わせ方

bind(>>=)を文の直列的な組み合わせと説明した。「問題を部分に分解する能力は解を貼り合せる能力に直接に依存している」のだ、他の組み合わせ方は無いものだろうか?もちろんある。


  • bind(>>=)による組み合わせ


    • 直列的な組み合わせ方

    • Monadic Composition



  • 高階関数的な組み合わせ


    • Categorical Compositionと呼ばれたりする



  • モナドの機能の組み合わせ


    • Monad Transformer

    • Extensible Effects



高階関数的な組み合わせについて補足を入れよう。

モナドはプログラムである、そしてプログラムがプログラムを受け取る、もしくはプログラムがプログラムを返すと言うことは高階プログラムと言えるだろう。Haskellでプログラムはモナドを使った単なる関数であり、Haskellでは高階関数が存在することから、高階プログラムも同様に存在するし記述出来る。それは例えば以下の様な型をとるだろう(A, B, C, Mを既知とし、Mはモナドとする):

hoProgram_arg :: M A -> M B -> M C

hoProgram_ret :: A -> B -> M (M C)

引数としてMonadicな値を受け取っていたり、プログラムM上で(M C)の値を返したりしていることが読み取れるだろう。


言語の汎用性と生産性のトレードオフ

モナドによって何が変わるのかといったら設計である。僕らはモナドを用いることに依って、様々な「広さ」の言語をHaskell内に作り出せることが出来る。言語の広さとは、ここではその言語が解くことが可能な問題の範囲を意味する。

言語の広さはいわば汎用性である。そして汎用性と生産性にはトレードオフが存在する。

言語が広ければ種々の問題を解決出来るが、個々の問題を解く事においては生産性が低い。

言語を狭めて特定の問題に特化すれば汎用性は下がるが、その領域において生産性は上げられる。

そうしてHaskellで問題を解こうとした時に戸惑うことになる。標準ライブラリで用意されている各種モナドは、汎用性が高すぎるからだ。もちろんそれらを使ってガリガリ問題を解いても問題ないが、問題を適切に解ける様な周辺言語を構築する方法を一アプローチとして知っておくといいと思う。DSLを構築してある程度範囲を絞っておく事で、その後の試行錯誤が容易になるだろう。

モナドを用いたテクニックに関しては以前スライドを作ったので、そちらを参照してほしい。


モナドへの期待と抽象度の高さ

Haskell学習者がHaskell内の機能の一つであるモナドに期待するものは、あくまで「言語内の機能」であることが多いように思われる。例えばイテレータとかジェネレータとかコルーチンとか非同期機能とかOptionalとか例外機能とかだ。しかしここまで記述してきた通り、モナドはそれらの機能より遥かに抽象度が高い概念である。この想定外の抽象度の高さはあまり経験しないために戸惑うかもしれない。加えて、難しい難しいと噂されている割にはモナドの定義は非常に単純である。抽象度が高いならば定義が簡素になっていくことは考えてみれば至極当然のことであるが、やはりその様な経験は他の言語ではあまり体験出来ない。

命令型言語で複数のプログラミングを習得してきた人にとって、未知の領域への探索は遠い昔のことかもしれない。そういう人にとってHaskellの学習はそれを要求されることが多く、そのため「今まで習ったことは忘れて取り組む」必要があるかもしれない。その場合は心しておくといいと思う、Haskellでは抽象的(曖昧ではない)なモノを抽象的なまま扱うことがよくあるからだ。

ただし、モナドを理解した後はその既存の知識が必要になってくることは覚えておいていいだろう。モナドは命令型プログラミング言語そのものだからだ。


まとめ

モナドのおかげでプログラムがモジュラリティを伴って僕たちの前に現れる。


終わりに

モナドが何であるか、何の役に立つのかということについて長い間考えてきた人間にとって、モナドを一から独力で理解するのはやはり大変だろうと感じている。なので他の人がスムーズにモナドを理解出来る下地を作れたらと思う。

4年くらい前の僕の足掻いている様が残っているので貼っておこう。

Haskellはまだまだ発展途上だと思っていて、未だ使い途が見つかっていない数学オブジェクトがごろごろ転がっているHaskellを僕は本当にクールだと思っている。代数的性質が先に転がっているのだ、嬉しいに決まっている。なのでそれらオブジェクトの使い途をみんなで考えようぜーというようなことをやっていけたらと思う。