セカイノカタチ

世界のカタチを探求するブログ。関数型言語に興味があり、HaskellやScalaを勉強中。最近はカメラの話題も多め

マーブルワーズ

モナドを理解しにくい理由

先日モナドを悟った、id:qtamakiです。(´・ω・`)

Haskellの勉強を始めて、ほぼ一年が経ちました。

最近になってようやっと、モナドが何なのか把握してきた気がするのですが、ここまで理解するのにだいぶ時間がかかってしまいました。
そこで、現時点での理解を振り返ってみて、どうしてこんなに時間がかかってしまったのか、自分なりに考えてみました。

そもそも、CのポインターJavaオブジェクト指向など、コンピューティングやプログラミングパラダイムの根幹を担う概念を理解するのはとても大変ですよね。
思い返すと、始めてこれらの概念に触れた当時、そうとう頭の筋肉を疲労して時間もかかった記憶があります。
Cのポインターは1年ぐらい。オブジェクト指向に至っては、概念の理解に1年、実践で使いこなせるようになるのに3年ぐらいかかったと思います。
関数型言語に関しては、やはりモナドの概念が難しいとされているので、理解に時間がかかるのはしょうがないことなのかもしれません。

だけど「これを知っていればもっと早く理解で来ていたかもしれない」というハマリポイントもやっぱりあります。ただ単に罠で、モナドの概念とは直接関係なくて、ただ単に記法の問題だったり、ツールの問題だったりするので、サクッと回避してモナドの理解に集中したいですね。

戻り値のオーバーロードが分かりにくい

何でモナドが理解しにくいかというと、概念その物が複雑だからと言う側面もあるけど、Haskellの言語機能が(手続き型言語の人間からみて)解りにくく、さらにモナドの実現に言語機能が深く関わっているからだ。

特にhaskellでは、戻り値がオーバーロードする。

正確には「違う」と怒られそうだけど、CやJavaから見ると、オーバーロードしているようにしか見えない。
そして、モナドってのは、関数と関数を数珠つなぎにしていくことこそ概念の本質とするため、次の関数とつないで始めて型が決まる(推論される)という概念の理解が必須で、これが難しかった。

たとえば、

return 1 >>= Just

このとき、returnは、

(Num t) => t -> Maybe t

と解釈される。

return 1 >>= (:[])

は、

(Num t) => t -> [t]

と解釈される。

Javaがもし戻り値のオーバーロードを許していたとして、こんな感じか。

Maybe x = return(1);
List m = return(1);

ジェネリクスと違って、returnの別の実装が選択される。

型引数が解りにくい

じゃあ、こうしたらどうかと、

return 1 >>= (:[]) >>= print

と書くとエラー!と怒られたりする。(´・ω・`)
これは、>>=の定義であるところの、[m a -> (a -> m b) -> m b]のmの部分が,とIOで違うからなんだけど、そんなの知るかい!ってなるよね。
mは、Monadなら何でも良いんだけど,式の途中で変わる事が出来ない。
mを
と決めたら最後まで[]を貫いてほしかったわけだ。
おなじく、aとbも、どんな型でも構わないが,aとa,bとbは同じ型じゃなきゃ困る。

return 1 >>= (:[]) >>= \x -> [print x]

これならコンパイルが通る。

Showを実装してないとghciがエラーになるのが解りにくい

エラー!って怒られるじゃん!嘘つき。(´・ω・`)
いや、待ってほしい。そのエラーメッセージは,ひょっとしてこんな感じではないか?

arising from a use of `print'

これは、コンパイルエラーではなく,ghciが結果をprintしようとしてprint関数が実装されていないので発生しているエラーだ。(ドヤァ)
試しに以下を実行して欲しい。

let a = return 1 >== (:[]) >>= \x -> [print x]

この場合結果を評価せずに変数に入れる。そして、少なくともコンパイルエラーは出ない。
評価する。

a

さっきと同じエラーが出たはずだ。

sequence a

今度は,画面に表示出来たと思う。これは、print xが反すIO ()にShowクラスが実装されて折らず,printが実行できない事を示している。
だけど、初心者はコンパイルエラーと区別出来ないでしょ。(´・ω・`)

ポイントフリーが解りにくい

return 1 >>= print

は、実際は

return 1 >>= \x -> print x

となる。print が、(Show a) => a -> IO ()のシグネチャをもつ関数なので、省略してしまっている。
>>=が、 m a -> (a -> m b) -> mbなので、いずれにせよ、真ん中の関数に(たまたま)合致する。
全然解んないので,getLineなんかも、 return 1 >>= getLineと書くとエラー!と怒られたりする。(´・ω・`)
どーなってんの?と。
getLineのシグネチャが、IO Stringなんで、引数を取らない。(a -> m b)ではないので、エラーとなる。

ghciで関数定義できないのが解りにくい

正確にいうと、定義できるのだが、普通には出来ない。

makeList:: Integer -> a -> [a]
makeList x y = take x $ repeat y

とかやりたいわけだが、別にファイルを開いて編集して保存してロードしないと確かめられない。
えっと、REPLのいいとことって、その場で実行できることですよね?

これでは,台無しではないか。

特に初心者には,ある程度の量のタイプを別のファイルで行わなければならないのが心理的にきつい。(´・ω・`)
それで、REPL上で出来る事を試そうとして,型引数の違いやら、Showの実装の問題やらにハマってしまう。
IOモナドの例題に必ず書いてあるdo記法も複数行が前提なので使えない。
こんなんやったら、REPLないほうがよいわ。(´・ω・`)

結論として

モナドの説明だと思った?残念、ただの愚痴でした。><
今度は、モナドの説明を書きたい。(///)