つまずきの記憶 - モナド(1)

'15年2月6日
タグ Haskell

今回から、モナドの理解に苦戦していた頃の振り返りながら、モナドのどこでつまずいていたのかを書いてみようと思う。今回は難しさの原因などについて。

モナドは難しいのか?

「難しい」「難しくない」というのは相対的なものなので、「誰にとっての話?」「何と比較した場合の話?」などをハッキリさせずに議論してもあまり意味はないのであろうが、しかし、Haskell を学び始めてモナドというものに出会い、「ん??なんか良く分からないなぁ…」くらいの段階の人にとってはやはり難しいと言って良いと思う。

モナドに関する記事をWebで色々と眺めていると、「難しくない」と書かれている記事も少なからずあるようだが、これを初学者が字面通り「そっか、難しくないのね♪」と素直に受け止めるのはたぶんよろしくなくて、「モナドは(分かってしまえば)難しくない」と補って読むのが正しい態度ではないかと思う。「難しくない」と書いてる人たちは恐らく全員がもう分かっちゃってる人たちのはずで、だから「難しくない」と言うのは当然なのである。勉強でもパズルでもスポーツでもいいけれど、「分かった」「できた」という状態に達してしまった後に、「なぜあんな(簡単な)ことが分からなかったのだろうか?できなかったのだろうか?」と感じた経験をお持ちの方も多いのではないだろうか。あれと一緒で、モナドも「分かった」と思える体ができあがるまでがそれなりに大変なのだと思う。

今ではモナドを分かったつもりにはなっているが、「よし、分かった」→「と思ったけど、まだ分かってなかったか…」→「あ、そうか、やっと分かったぞ!」→「いや、気のせいだった…」のようなことを何度も繰り返しているので、今も「本当に分かったと言えるのか?」と自問するとやや自信が無い。自信は無いが、モナドを難しく感じさせている原因については分かったような気がしているので、その原因について、自分のつまずきの歴史を振り返りながら、思い当たることを書いてみる。

モナドは抽象度が高い

例えば、Eq クラスや Ord クラスでつまずく人はほとんどいないのではないかと思う。GHCのライブラリマニュアルで Prelude を見ると、非常に多くのデータ型がEqOrdのインスタンスとして定義されていることが分かるが、それぞれのインスタンスについていちいち定義を確認したりせずとも、(==) はそれぞれのインスタンスにとっての「等しい」状態であるかどうかを判定する関数だろうと想像できるし、実際そう考えて間違い無いはずである。(<) についても、なんらかの意味での大小関係あるいは順序関係のようなものの判定関数であろう、と考えておけばまず間違いはない。Num クラスの(+)や、Enum クラスの succ などについても同様である。つまり、EqOrdNumあたりのクラスはすでに十分馴染みのある概念と同じか類似したものなので、容易に理解したり類推したりできるのである。

ではモナドの演算子 (>>=) の場合はどうだろうか。C言語などのシフト演算子とは無関係である。となれば、このような演算子にそもそも馴染みが無いし、全てのインスタンスに対して共通して考えられる「等しい」とか「足し算」のような概念が思い当たらない。強いて言えば「文脈付きの演算」ということになるのかもしれないが、そんなことを言われてもニュービーにとっては「??」である。つまり、(>>=) が何をやっているのかをインスタンスごとに理解しなければならない。それぞれのインスタンスで全く異なった処理をしているのに (>>=) という記号を共通して使っているわけで、これは抽象化による大きな利点ととらえるべきなのかもしれないが、初心者にとってはモナドの敷居を高くしている要因となっているように思う。

したがって、学ぶ側はインスタンスそれぞれについて (>>=) が何をやっているのかをしっかりと理解しなければならない。この演算子については後にまた触れることにする。

訳語の問題

モナド値のことを、「文脈付きの値」という表現で書かれている文章をよく見かけるようになった。最初に目にしたのは恐らく「ゾウ本」だったと思うが、この訳語はよろしくないと思う。手元の国語辞典で「文脈」にあたってみると、

  1. 文中の語と語、文章中の文と文の論理的なつながりぐあい。文の脈絡、文章の筋道。コンテクスト。
  2. 物事の脈絡・筋道。また、物事の背景。

とある。モナドに関して使われる「文脈」の意味は、最後にある「物事の背景」に一番近いと思うが、「文脈」から最初に思うのは通常1の意味であって、モナドの説明に使われてもしっくりこないのである。

一方、「文脈」に対応している英単語“context”を英和辞典であたってみると、

  1. (事情・情報などの)背景、状況、環境
  2. (文・発話の)前後関係、コンテクスト、文脈、脈絡

とある。モナドの説明で使われている意味は明らかに1の方だと思うのだが、違うだろうか?「文脈付きの値」よりも、「(それぞれのインスタンスに応じた)状況や状態を伴った値」のように訳した方が分かりやすいように思う。Stateモナドなどはまさに「状態を伴った値」であるし、IOモナドだって“read world”という「状態」あるいは「環境」を伴ったものであるから、「状態を伴った値」のような表現の方が適当に思える。

「ゾウ本」の原著のオンライン版を久しぶりに見てみたらずいぶん書き換えられたようで(イラストも増えて)、印刷物とはかなり違っている。11章の表題は、印刷物では“APPLICATIVE FUNCTORS”であるが、オンライン版では“Functors, Applicative Functors and Monoids”となっている。この章の最初の方で、印刷物ではFunctorの値のことを“values with an added context”と書かれているのだが、オンライン版では“computational context”となっている。単なる“context”では分かりにくいと思ったのだろうか?

「文脈」という訳語が良くないと思う理由はもうひとつある。
たとえば f :: Ord a => a -> b のような型の関数がある。この“Ord a =>”の部分は、「型aOrdクラスのインスタンスじゃないとダメよ」という意味で、「型クラス制約」などと呼ばれているが、この制約のことも「文脈」と言われる場合があるようなのである1。同じ言葉が別の意味で使われるのは(それこそ「文脈」によって区別はできるけれど)紛らわしいので避けた方が良いと思う。

「文脈付きの値」という用法はすでに定着してしまっているのだろうか?「文脈」あるいは「context」がモナドの説明で使われているかどうか、手元にある本でサッと調べてみたところ、「ふつう本」では使われてないようだし、「RWH本」でも使われていないようだ。「ゾウ本」が出る前後あたりの、比較的最近になってから使われ始めたのだろうか。
もう定着してしまったのなら仕方ないかもしれないが、そうでなければもっと分かりやすい別の訳語を使ったほうが良いと思うのだが…。

モナドの背景と、(>>=)(>>)

正確には覚えていないが、(>>) を最初に見たのはIOに関する処理を読んでいたときだったと思う。たとえば、getChar >>= putChar は入力から読み込んだ文字を受け取って出力するが、getChar >> putChar 'A' は入力が何であってもそれは捨てられて A が出力される、というような説明だった。それ以降しばらくの間、(>>) という演算子を軽視、というか無視していた。

(>>) にはdefaultの実装があり、「ゾウ本」のp.294では次のように定義されている:

(>>) :: (Monad m) => m a -> m b -> m b
m >> n = m >>= \_ -> n

この定義を見ると、第1引数が結果に影響を与えるようには見えない。つまり、(a,b) から b を取り出す snd 関数のようなもの、と思っていた。ところが、Maybeモナドの項を読んでいたら Nothing >> Just 1 = Nothing のような記述が出てきた。最初は誤植だと思った。だって、>> の手前は捨てられるんだから、Nothing は関係なくて Just 1 が結果になるはずだよなー、と思いながらGHCiに Nothing >> Just 1 を問い合わせてみたら Nothing が返ってきて混乱してきた。

このあたりも、モナドでつまずきやすい箇所のひとつではないかと思う。

ソースコードの表面に現れず、モナドの仕組みの背後でアレコレ働いてくれてるものがあるおかげでコードを簡潔に書けるようになる一方、背後にあって見えないが故に学習の妨げになっている、という側面もあるように思う。

Maybe における (>>=) は「ゾウ本」では次のように定義されている:

Nothing >>= f = Nothing
Just x >>= f = f x

この定義を使って (>>) を書き換えると次のようになる:

Nothing >> n = Nothing >>= \_ -> n = Nothing
Just x >> n = Just x >>= \_ -> n = n

これで明らかなように、捨てられるのは第1引数それ自体ではなく、Just x だった場合の x だけであって、NothingJust かという情報は >> 以降に伝搬しているのである。

ついでにリストの場合も見てみよう。同じく「ゾウ本」からリストモナドの (>>=) の定義を引いてくると:

xs >>= f = concat (map f xs)

となっている。この定義で (>>) を書き換えると、

xs >> ys = xs >>= \_ -> ys = concat (map (\_ -> ys) xs)

となる。(\_ -> ys)xs の各要素を(その値に関係無く) ys に置き換えるものだから、ys(length xs)個連結したものになる。たとえば、[1,2] >> "abc" = "abcabc" となる。この演算で捨てられたものは xs の中身だけで、xs の長さは伝わっているのである。

個々のインスタンスにおける「モナド m の持つ環境・文脈」を、「m a >> m b の演算において >> の後ろまで伝搬するもの」と考えると良いかもしれない。最初の方で、「(>>=)が何をやっているのかをしっかりと理解しなければならない」と書いたが、その際に (>>) が何を捨て何を伝搬するのかも合わせて把握しておくと良いと思う。

(次回に続く)


  1. たとえば「実践本」のp.134、あるいは「やさしいHaskell入門(バージョン98)」の「5 型クラスと多重定義」などで見られる。