Haskellの壁

この記事はHaskell Advent Calendarのために書いたものです。


先ずは自己紹介でも。
大学の専門は機械でした。制御システム工学。まあ情報(工学|科学)科ではないということです。仕事で使っている言語はPHP実用主義的な言語ですね。Haskellとは正反対です。
Haskellは私的に使うために習得しているところ。出来ればHaskellでプロトタイプ作成くらいはさくっと出来るようにはしたい。
ホーム言語というか得意な言語とかあるといいよなあー、とか思いつつ大学生協でRWHを立ち読みし、よしこれにしようと思い立ったのがHaskellはじめだったかな。
ポリシーが明快なものは良いですね。PerlとかRubyとかPythonとか結構好きです。PHPとか好きではない。最近はClojureなんかも結構いいなあ、などと思っていたり。

それはともかく、このHaskell習得時期にAdventCalendarに参加されているHaskellerの皆さんの記事が非常にためになって感動している昨今です。
僕は技術的なネタがないので、初心者的な立場からHaskellの参入障壁など書いていこうかと思います。
結構適当なことを書こうと思うので指摘歓迎です。

Haskellは初心者に優しくない。易しくない。

何故か。他の言語で当たり前に出来ることがHaskellでは出来ないからです。
日本の、というか世界も同じかもしれませんが、Haskellユーザは変態が多いためか、変態的な議題をとりあげてはキャッキャウフフして楽しんでいます。端から見てると超楽しそうです。俺も混じりたい。


しかし命令型言語から関数型言語に入った僕としてはこのHaskellの情報の無さはあまりに厳しい。
正確に言うならば、情報自体は多いのですが、初心者向け、もしくは実用向けの情報が非常に少ない。
日本の、というか世界も同じかもしれませんが、Haskellユーザは自身の過去の苦労からか、モナドや型クラスあたりのチュートリアルなどを書いてくれます。
しかし僕はUnix Timeからフォーマットされた文字列に変換するための情報が欲しいのです。*1
HaskellMysqlハンドラを用いてDBにアクセスし*2、日本語を文字化けを起こさずにブラウザに表示する*3ための情報が欲しいのです。


他の言語では検索すれば簡単に出てくるようなことが、Haskellでは同じように出来ないのです。


結論を言えば、初心者というか僕はHaskellの逆引き辞典が欲しいです。
PHP逆引きとかRuby逆引きとかを一冊買ってきて、それらの中身を一個一個Haskellの手法に変換して明示した感じの物が欲しい。まあ時間あれば僕が作るかもしれないけど。今のところ無いですごめんなさい。

Why Haskell?編

  • モナドがよく分からない
  • モナドが俺に何をもたらしてくれるかわからない
  • 参照透過性があんまり分からない
  • 参照透過性が俺に何をもたらしてくれるのかわからない
  • 遅延処理がいまいち分からない
  • 遅延処理が俺に何をもたらしてくれるのかわからない
  • カリー化がどうも分からない
  • カリー化が俺に何をもたらしてくれるのかわからない


Cやjavaといった命令型言語からきた人から見れば、Haskellは命令型言語ではあまり意識されていない概念を持ち出してきて、世界を反転させてしまったように見える。
純粋性と遅延処理。

  1. 世界は副作用に満ちていたのに、大部分を純粋にして、副作用をモナドに閉じ込めた。
  2. 世界は順序を全て指定しなければならなかったのに、それを反転させて世界は混沌に包まれた。秩序はモナドに閉じ込めた。
  3. 純粋性と遅延処理を取り込む副作用として、モナドは取り入れられた?

IO物語完。

まあなんというかこの辺の性質は個々の意味を見ていても分からないのかな。
純粋性と遅延処理は相性いいよね。副作用に満ちた世界で遅延処理とか地獄だよね。
まあ知らないけど。John Hughesが言ってる*4
最近色んな言語が遅延処理を取り込んでいるけど、大丈夫なのだろうか。

命令型言語は順序が命。だけど「/* このメソッドを呼ぶ際は以下のメソッドを先に呼ぶこと */」とかもしコメントに書いてあったら爆発しろ!て思うよね。誰だって思う。


そして抽象化という側面ではモナドは制御の抽象化を意味する。らしい。
プログラミングの歴史はあまり知らないのだけど、多分if文てそんなに抽象化されてないよね。それはいつも隠されてきた気がする。サブルーチンによる隠蔽。カプセル化。マクロ。
モナドはデータ型に対するアクセスの統一を行った。どんなモナドインスタンスであっても(>>=)で扱えるわけだ。


結局モナドってなんなの。
モナドって象さ。物を運ぶコンテナで、中身の扱い方は指定出来て、連結出来る。色んな側面があるってこと。


関係ないかもしれないけど、モナドの概念を聞いたときに僕はjQueryオブジェクトを思い出した。DOMを扱うコンテナで、各メソッドの最後に自身を返しているんだったか(実装知らない)。

情報収集編

  • ふつうのHaskellプログラミング(ふつける)
  • Real World Haskell(RWH)
  • プログラミングHaskell
  • @shelarcyさんの記事が理解出来ない
  • Haskellerがみんな変態すぎるので初心者向けの記事がない
  • Parserが簡単にかけるのはいいなあ。

RWHを最初に買った。
RWHは7章くらいまではなんとなーくわかったので、それ以降興味のある章を読もうとしたわけだ。僕は。
端的に言うとHaskellに遅延学習はやめた方がいい。情報専門の人ならいいかもしれないけど。
遅延学習とは、新しい概念が頻出する分野では行ってはいけない。
人間はスタックマシンじゃないんだ。新しい用語が3つくらい同時に現れて、さらにそれらを調べたら4つくらいまた現れるのが続くとか一体どうすればいいんだよ。

同じ時くらいにふつける買った。
だいたい読んで、またまた何となく分かった気がする。遅延処理のしくみおもしれー、と思う。しかし例が簡単すぎるためか、もやもやが晴れない。
まあここでのもやもやは新しい概念が頻出する際にはしようがないとも言える。
derivingの説明がいまいち分からない。え、どんなクラスにも使えるのそれ。仕組み分からないしさっぱり分からん。
(.)と($)の違いがいまいち分からない。

そうして何となく構文は分かるようになってくる。


しかし@shelarcyさんの記事本物のプログラマはHaskellを使う | 日経 xTECH(クロステック)が理解できない。 遅延学習しようとしても残念ながら人間はスタックマシンじゃないんだ。よし、後で読むことにしよう。
なんだかHaskellerで分かっている人と分かっていない人の差が激し過ぎる気がする。まあHaskell記事書く人の中で。
ふつける丸写しブログにイラッとする。語尾変えているところが余計いらつく。
Parserが簡単にかけるのは良いよなーとかおもう。LL(1)の意味は分からないながら。


まあこの時期ろくにコード書こうとしなかったのが良くなかった。まあ書こうとしてもすぐ詰まるわけだが。


結局Real World Haskellをよく使っているかも。Real Worldと書いてあるだけあって実用的な部分の詳細な記述は助かる。
ネットの記事は知識増やすにはいいかもしれないけど。

Hello World〜Tutorial編

  • エラーが読めない
  • offside ruleがちょっと謎
  • (++)とか(/=)とか純粋な言語ならではだよなあ

Haskellはエラーが特徴的だ。多分。PHPとかJavaのようにスタックトレースとか出てこないから。大抵が型のエラーな訳だ。
そのエラーがいまいちわからないからとりあえず行番号と列だけ見て直そうとする。
後で冷静に読んでみると意外とこうした方が良いかもよ!というメッセージとか書いてあったりする。
まあエラーに慣れるまでは仕様がないだろうか。

offside ruleが原因のエラーとか初め良く分からない。
というかoffside ruleによって綺麗だけど見慣れないインデントになる。
命令型言語の「アジの開き」とか出てこないからむしろそっちが懐かしくなる。嘘だけど。

<?php
// アジの開き (コードは極適当)
foreach ($arr as $key => $val) {
	if (in_array($val, range(1,4))) {
		if ($key != 0) {
			$ret[] = $val;
		} else {
			continue;
		}
	} else {
		if ($key != 0) {
			$ret[] = $val;
		} else {
			continue;
		}
	}
}

命令型言語使用者はこれらのインデントを見て、無意識に「ああ低次のことやってるんだな」とか思う。高次のことをやろうとすると自然、インデントは揃うからね。
ということでインデントパターンはそれなりに情報を持っていると思うのだけど、その情報もないし。
doのoffside ruleはbind(>>=)を命令型言語-likeに分かり易くしているのかもしれないが、あれは頭の中での脱糖が困難だ。流れが逆を向いているから。
もしかしてreturnて命令型言語あたりから取られているのではあるまいか...と思ったりしたものだが、まさかね。
新しい概念は慣れが必要だ。少しずつ消化しなければならない。だからそういう時は先に使い方を知るのがよい。理論なんて後付けでどうにかなる。しかし実学のための情報が無い。


リスト連結(++)とかNotEquals(/=)とか命令型言語からしたら全く意味が違っていて驚くべきところかもしれないが、僕は一切違和感がなかった。
それくらいHaskellのコードは命令型言語との類似性がない。これはまあ言語同士が中途半端に混じらなくてよかったのかも知れないけど。

型クラス編

  • 型クラスとインスタンスの関係がしっくりこない
  • Applicativeが分からない
  • Arrowが..

OOPではクラスからインスタンスが生成されるわけなのだけど、Haskellでは「ある型」を「型クラス」の「インスタンス」に「する」。初めはこの意味がわからない。
OOPとは全く別物と分かっていてもなんかしっくりこない。まあinstanceて具体例とかいう意味だから間違っちゃないよな、とか考えつつ心の平安を取り戻す。
型クラスはこれまたイメージしにくい概念だ。
しかし知ってしまえばOOPよりずっと自然な気がしてくる。


メタプログラミングRuby」でRubyの黒魔術をふむふむと読んで行くと、open classというものがあることを知る。
Rubyでは全てがオブジェクトであり、Int型を処理する新しい関数を作るとき、Intのクラスをコードの最中で「開き」、メソッドを「加える」ことが出来る。

class Int
	def new_method(arg)
		# ... something new
	end
end

というのも、rubyscalaでは加算演算子(+)もInt型のようになにかのクラスのメソッドであり、 例えば(3 + 5)という式は次のように内部で置き換えられるようだ。

  3.+(5)

これはかなり面白い仕組みだけど、Haskellで同じようなことをしたかったら、Intを扱う関数を書くだけでいいんじゃないかと気づく。

new_func :: Int -> Int -> Int
new_func =     -- ... something new

黒魔術すら要らなかった。
さらにHaskellではこれをIntだけでなく、一般的な数値まで広げることが出来る。

new_func :: (Num a) => a -> a -> a
new_func =     -- ... something new

これだけだ。代わりに、型クラスNumのインスタンスになった型は、演算子(+)や(-)などを定義しなければならない。IntやDoubleといった組み込み型はもちろんそれらが定義されている。


これらRubyHaskellの違いは何だろう。
Rubyは継承によりコードの再利用を可能にしているというメリットがあり、Haskellでは型クラス毎にメソッドを再定義しないといけないというデメリットはある。
*5
OOPはデータ型と関数を依存させることによって成立しているが、Haskellではそれらを型という高次の概念によって疎に結合させていると考えられる。
まあなんとなくRubyも高次の概念が欲しかったような気がする。それを実現するために、高次のものを作るのではなく、クラスをオブジェクトに落とすという逆転の発想を取ったわけだ。*6


Haskellは実装の継承を諦める代わりに、豊かで(型クラスの継承は多重継承が可能だ)柔軟な(型(Type)を柔軟に*classify*出来るわけだ)型クラス(Type class)を得た。
Haskellは実装がおおくなるけど、コードの再利用は参照透過性がある程度担保してくれるし、そもそもコード自身も他の言語に比べ大分短く書くことが出来るようだし。

とりあえず初心者は型クラスについて詳しく知るべきなのだろう。モナドもその型クラス生態系の一つにすぎないのだから。


なんとなく、Haskellは何やら相性の良いものをいろいろ集めて構成されている気がする。

モナド

  • モナドのイメージわかない
  • モナドの扱い方がいまいちわからない
  • Writer,Reader,Stateが分からない
  • WriterT,ReaderT,StateTがry

モナドモナド則を満たすコンテナ的な物だと分かったとしても、以前モナドは初心者にとって脅威だ。
原因は主にモナドが中々イメージ出来ないから。そのモナドを調べて行くと、スーパークラスにFunctorとかApplicativeだか持っているわけだ。え。なにそれ。
そしてとりあえずMonadの説明でWriterとか見て行くと、Monoidとか現れるわけだ。え。なにそれ。
そして第2に、モナドに包まれた中身に手を出すにはどうしたら良いか分からない。とりあえずliftMでも使っておけばいいですか。
liftMは対象のモナドがどんな物であろうとかまわないところとかには感動した。型推定のおかげ?
あ、Applicativeスタイルというものがあるらしいね。3日くらい前のHaskell Advent Calendarに書いてあってとてもためになった。
Writer,Reader,State,WriterT,ReaderT,StateTに関してはこのスライドが分かり易かった。


恐らくモナドの仕組みを知るよりも使い方を知って行った方がいいのだろうね。


結局モナドは「アジの開き」を抽象化してくれるのだろうか?なんとなくそんな気はするけど、その直接の解法になるのかはまだわからない。

まあモナドって象だから、ちょっと便利なコンテナとして使っている物とかbind(>>=)ばりばり使うためのものとかだったり、色んなものがあるんですよねきっと。

一体俺はどこに居るのか編

  • どれだけ進んだのかわからない
  • どこまで行けば良いものかわからない
  • というか知るべきこと多いよね。
  • Template Haskell???
  • 並列プログラミングかー。
  • どこまで内部を知れば良いかな
  • あ、メモ化って勝手に行われるわけではないんだ。


で、僕は今どの辺にいるのだろう。どこまで行けば実用に足りるだろうね。
OOPにはポリモーフィズムとかカプセル化とか、GoFデザインパターンとか分かり易い目安があるわけだけど、Haskellにはそういう設計にデザインパターンは確立されていないようで。
まあモナド自体一種のパターンなのだろうけど。それらのネストはちょっと面倒らしいしモナド変換子はまだよくわかってないし。
少しコードを書いてみて、データ型が大事であろうことはわかった。恐らくそこから設計していくのだろうね。設計の際にもデータ型とメソッドの疎結合が良い感じかもしれないけどどうだろう。期待。


Haskellは新しい概念が多いということを何度も書いているわけだが、それ以外にも知っておかなければならないことが多い気がする。
遅延処理に関しては、例えば内部動作無視して

foldl (+) 0 [1..100000]

とかつい書いてしまうと無駄なサンクを大量に生成してしまうし、計算時間のオーダーだって大抵ぱっと見はわからない。
型クラスやその他Haskellの性質、特徴に関しても使いこなすには結構な知識と経験が必要な気がする。


しかし言語機能で必要な知識が多いことは、悪いことばかりではないと考えている。チーム開発するときの一番の問題は、知識の共有コストだからね。知識の共有を言語がある程度担保してくれるというのは大きなメリットだろう。
もちろんフレームワークやその上のアプリ層での共有でもかまわないのだけどね。言語に詳しいとて初めからフレームワークアーキテクチャを知っているとは限らないわけだし、アプリの内部知識については尚更期待出来ない。
言語層での知識共有による阿吽の呼吸は、多くの面倒な事務処理を省いてくれるはずだ。多分。
反面、デメリットはもちろん巨大な参入障壁だ。初心者はそのへん、ある程度覚悟しなければならない。その道程が他言語より数倍長い、ということくらいは。


それと最近話題のDSLも、チーム内での暗黙の了解を増やす手っ取り早い方法だと思っている。

クラッチからソフトウェアを作るならば、どんな言語を使用しようとも、そのソフトウェアを形作るために指定すべき必要最低限の情報量は変わらない。
言語の違いとは、それら情報を組み立てるのに必要な骨組みの量が変わるだけだ。
それらを減らすにはどうすればいい?
組み立てる骨組みの量をを減らしたいならあらかじめ汎用的な土台を作っておけば良い。フレームワークがそれだ。
指定する元の情報量を減らしたければ、暗黙の了解を増やせば良い。DSLがそれだ。
DSLが有るならば、プログラマはその下のベタな制御構造を考えずに、指定すべき情報のみを与えれば良くなる。

どちらを選ぶかは構築するものの性質に依るのだろうけど。指定すべき情報が多いならDSLを作った方が生産性が高いだろうね。
HaskellDSLの書き易さは特筆に値するとかなんとか。知らないけど。

自然言語か数学か。

PerlRuby自然言語を目指して作られたとかなんとかということを聞いたことが有るような無いような気がするけど、Haskellは数学(というか論理?)を目指して作られたよう気がするね。
頭の中をそのままぶちまけるのではなくて、頭の中で練って作り上げたルールをそのまま書き下して行く感じとかね。だから英語より数学が得意な人はHaskell使うといいよ!
とはいえ頭の使い方が命令型言語とは大分異なる、ということは強調してもいいかもしれない。
PHPからHaskellに移ると、PHPの糞な部分はなくなるけど、代わりにHaskell特有の問題がやってくる。まあ当然のことです。考え方を変えなければ行けないばかりか、考えなければいけない、直面する問題も異なるということ。


あ、Haskellにfor文やwhile文は存在しないけど、再帰って高校でやった漸化式をそのまま書き下しているだけだから、全く臆する必要ないと思うよ!大学の微分方程式はもっと高度な漸化式な訳だし、離散数列とか楽勝でしょう。

a_0 = 1,  a_1 = 1, a_n = a_{n-1} + a_{n-2}

fib 0 = 1
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

まあこれ効率悪いぽいけど。あと(n < 0)を考慮してない。けどまあこの数学の記述そのまま感はすごい。


\{ (x,y) | x \in N, y \in N \}

[(x,y) | x <- [1..], y <- [1..]]  -- 無限リスト

内包。これもそのままだよな。



あとforとwhileの無さに関して言えば、Scalaのコップ本P432にこんなことが書いてあった。

すべてのfor式は、map, flatMap, filterの3つの高階関数によって表現出来る。

flatMapとはHaskellではconcatMapのこと。
つまり、forを使うところではその3つのmapやらなんやらでなんとかなるので、再帰はforで表現出来ないことだけをやればいいんだぜ!ということ。


ということでHaskell楽しいからやろうぜ。

*1:3日前出来た

*2:一昨日なんとかできた

*3:昨日なんとか出来た

*4:Masterminds of Programming/O'Reilly, P208

*5:Haskellにも型クラスの継承があるが、それもメソッドを実装しなければならない。おおまかに言えば、OOPでInterface同士が継承できるようなものだ。

*6:Rubyではクラスもオブジェクトだ