去年の暮から一ヶ月ほど、Haskellを学んでいる。目的は色々あるが、まったく新しいプログラミング言語を学ぶ経験をもう一度するのが最大の目的だ。
C++17の新機能を余すところなく解説した「江添亮の詳説C++17」は3月に出版される。C++20にはまだ間がある。C++の入門書を書く絶好の機会だ。入門書というのは毎月腐るほど出版されているが、良書は少ない。結局、参考書の中で最も売れ行きが容易に予想可能なのは、最も読み手がいるであろう入門者向けの本であり、入門書の出版がいたずらに増え、粗製濫造されているとみえる。入門書は上級者は読まないので適切な批判もされにくいのでこのような惨状になっているのだろう。
私の入門書はそのような悪書にしたくない。しかし、私はすでにC++の初心者ではないし、C++の初心者がつまづく点がわからない。というより、ここしばらくまったく新しいプログラミング言語を学んだことはない。C++以外にもPython、JavaScript、bashなどを必要に迫られて書き散らすこともあるが、相当古いJavaScript以外は真剣に学んだことがない。
これはよくない。入門書を書く前に、少し時間を割いてでも、全く新しいプログラミング言語に入門すべきだ。そのための言語としてはHaskellがふさわしい。と、こう考えた。
なぜHaskellなのか。HaskellはC++とは全く違った言語だからだ。例えばJavaScriptは表面的にはC++とかなり似た文法を持っているので、全く新しいプログラミング言語を学ぶ目的には使えない。PythonやRubyは文法的には少し違うが、すでに触ったことがあり、それほど苦労した覚えがないのでやはりだめだ。Scalaは汚らわしいJavaの実行環境を私の神聖なメインPCにインストールしたくないために不適だ。
Haskellを学ぶには、Learn you a Haskell for great goodという本が良いらしいので、早速買い求めて読み始めた。この本はとても軽くて読みやすい本だ。そしてユーモアが多い。真面目な参考書に個々までのユーモアは必要なのかと疑問に思うぐらいに多いのだが、しかし全くの初心者が読む本には、これぐらいのユーモアが必要なのではないかと思う。
というのも、だ。私自身、久しぶりに経験して驚いているのだが、全く新しいことを学ぶのは途方もなく疲れる上に非効率的な作業だからだ。不思議なもので、それほど難しいはずはない内容なのに、なぜかなかなか頭に入らない。一日に本を読む時間は何時間もあるが、短期的にいくら時間をかけても内容が頭に入ってこない。一晩寝るなどして時間を開けないとわずか数ページの内容も読み進めることができない。文字を読むことはできるにしても、その内容が理解できず、また残らないとしたら意味がない。
そういう賽の河原のような不毛な作業を続けるには、ユーモアがとても効果的だということがわかる。ただでさえ退屈な作業なのに、ユーモア欠落症患者によって書かれた文章まで読まされるのでは続くものも続かない。
私が書く入門書にはユーモアが必要だということがわかる。
プログラミング言語を学ぶには、プログラミングの実行環境が手元にあったほうがよい。しかしHaskellの実行環境はあまりすぐれているとは言い難い。Haskell PlatformはDebian系ならばapt一発で入るのでまだいいのだが、クールなキッズは皆使うというStackは、私の手元ではまともに動いた試しがない。
そもそもだ。CabalもStackもてんでなっていない。パッケージマネージャと名乗るのもおこがましい。だいたい管理できていない。パッケージを追加することはできるが削除できないのだから。削除するにはどうするか? すべてのパッケージを削除して最初からインストールをやり直す。富豪にもほどがある。Stackもだめだ。結局管理できていない。管理できないからどうするか? とりあえずプロジェクトごとに特定のディレクトリ下に全部環境を構築しよう。富豪にもほどがある。
そういうわけで、Haskellの実行環境を整えるだけで1日を浪費した。C++の入門書では環境構築についてそれなりにページを割かなければならないと痛感した。具体的にはWindowsとMacはC++の学習に不適切なOSなのでGNU/Linuxを推奨したい。主要なディストロならC++の実行環境はデフォルトで入っている。
さて、そうこうしているうちに一ヶ月たち、流石にHaskellである程度のコードは書けるようになってきた。まだモナドについて完全に理解していないのだが、理解できるのも時間の問題だ。なんだ、Haskellも言うほど難しくはないではないか。
しかし、プログラミング言語の学習に当たってはこの時期が一番つらい。プログラミング言語の学習過程では、序盤はさっぱりわからず、中盤はすべてを理解したつもりになり、終盤でやはりわからない状況になる。
プログラミング言語の学習の中盤でつまづくのは、言語の以外な落とし穴だ。例えば私の場合は以下のような例で躓いた。
モナドを学び始めたので、確認がてら、以下のようなコードを書いた。
getContents >>= 複雑な式>>= putStr
しかしこれでは見づらいので、複雑な式を関数に分けることにした。複雑な式の中身は特に重要ではないので、ここでは単にpureとする。
-- こうすればカリー化によって引数を書く必要がないぞ。Haskellは完全に理解した。
f = pure
getContents >>= f >>= putStr
その後、fはそのままにしたまま別の処理を試したくなった。
f = pure
getContents >>= 別の式 >>= putStr
するとエラーになった。意味がわからない。
Haskellではf = pureと書くとfは多相にならず、型を決定しなければならないが、型を推論するための文脈がないのでエラーになるというのが原因だ。解決方法としては、fの型を明示的に書く、引数を書く、GHC拡張を使うなどの方法がある。
しかし、これは割と初心者が引っかかりやすい落とし穴ではないか。例えば以下のコードがなぜエラーになるか、初心者がわかるのだろうか。
f = pure
main = do
print ( f 0 == Just 0 )
print ( f 0 == [0] )
どうでもいいことだが、C++を長年やっていた人間として、f = pureとしたあとにf 0 == [0]と書くと、fはpureになり文脈上リストなのでf 0は[0]になるというのは割と驚きがあるし、リストという割と特別扱いされているような昨日でさえ、単なるライブラリ実装であり、(:)はデータコンストラクターにすぎないというのも驚きがある。
早くモナドを完全に理解したい。