経験15年のOCaml ユーザーが Haskell を仕事で半年使ってみた

今の会社に移って半年経ちました。めでたく試用期間終了です。といっても別に試用期間中に密かに首を切られるような事をしたとか、逆に試用期間が終わったからと言ってこれで定年までのうのうと働ける、という訳ではありません。未来は全く判りません。まあとにかく、一つ区切りがやってきました。

金融を知らないQuantsの仕事

私の職業の肩書きには Quantitatitatitatitative という単語がくっついて超カッコよさそう。普通は Quant というと、金融工学や統計数理に詳しい夜もブイブイいわしている超イケメン20代を想像しますが、私は金融とか全然知らないアラフォーお父さんです。それでも Quant です。お願いですから、私に何を買ったらいいかとか、聞かないでください。金融商品とか買った事ないし。というか、逆に教えて欲しいです。

私のチームは、本当の Quant さん達が開発した、金融派生商品を評価するための、(私には正直良くわかんない)アルゴリズムを組み合わせるための言語フレームワークを作るのがお仕事です。 まあ簡単に言うと、細かい部品をイケメンが作ってくれるので、それを糊付けしたり剥したり、カッコよく使えるための言葉を作っていると言っていいでしょう。ほとんどプログラミング言語屋さんです。こういう言語寄りの技術をデリバティブに導入するアイデアは LexiFi (www.lexifi.com) が創めたものです。今まで ad-hoc に作っていた商品リスク計算が体系的に扱えるというので、今ではいろんな銀行や投資銀行で取り組みが行われているようです。

プログラミング言語を扱うプログラミング言語なら、

という性質を持つ関数型でやる、というのが(まあ異論もあるかと思いますが)この頃の流れです。ですので、LexiFi を始め、関数型でのアプローチが取られています。

現在私がいる所でも関数型言語 Haskell を使った開発が行われています。私は偶然にも LexiFi が創業して間もないころに企業ポスドクとして一年間働きました。その経歴を評価してもらって今の所に転職したわけです。( http://www.amazon.co.jp/exec/obidos/ASIN/0333992857/showshotcorne-22/ に LexiFi 創業者が仕事の大体について解説していますからご覧ください。山下(山本さんじゃなかったすいません)さんが「関数プログラミングの楽しみ」という邦訳を出されていますが、私は原文も邦訳も読んでません。てか LexiFi の部分に限っては今更読む必要ないw。)

現在の仕事をする前は、Prop-trading の会社 Jane Street (www.janestreet.com) でやはり似非Quantとして働いていました。作っていたシステムの性質は全然違いますが、やはり関数型言語である OCaml で書いていました。転職した理由は主に家庭の事情です(東京オフィスが香港に移ったため)が、OCaml とは違った関数型言語でも仕事をしてみたかったからでもあります。Caml系言語はもう15年書いていますからね…

仕事で Haskell は使えるのか?

さて、本稿のテーマは、「お仕事で OCaml と比べて Haskell は本当に使えるのか、どうか?」です。この半年いろいろと仕事していて感じた事を書こうと思います。ああ、ここで、仕事、というのは、ある程度の人数でチームを組んで、飯の種になるシステムを組んでいくという仕事です。例えば、一人で何か書くのであれば、Haskell でも OCaml でも BrainFuck でもその言語に精通していれば自己責任で出来るでしょ?だから、それは、考えません。

結論からまとめておきます

  • HaskellOCaml 並に普通に業務で使える言語
  • Haskell マニアが嬉しそうに紹介している機能は実はほとんど使う必要が無い。せいぜい Monad transformer位で仕事は出来る
  • Haskell には良いところもあるし、悪いところもある。手放しで神格化するのが一番問題

Haskell はまあ、decent な仕事でも使えるけど、別に Haskell じゃなくても OCaml でも F# でも出来るし〜。それぞれ癖を見極めないかんわね」という、まあ、ごく普通の点に落ち着きます。いや、まあ、Haskell で書いてても意味ないやろ、アホちゃう?やっぱ OCaml やろ、とか言えたら言いたいんですけどね、まあ、ね、私ももうすぐ40だし、家族が居るし、ちっとは大人っぽくせんといかん訳よ。わかるな?

静的型の関数型言語を知ってたら Haskell はまあ普通に書ける

基本的に OCaml も F# も SML も Haskell も Hindley-Milner type system に根ざした ML の一種です。ですから、プログラムの見た目は違いますが、その違いに慣れればこの内一つを使える人は Haskell も書けます。書けないのは、書こうとしないからだけ。仕事で書かないかんのなら嫌でも書けるよになります。その点では、一つでも強静的型付関数型言語をやってれば、すごく入りやすい。例えば OCaml でのプログラミング経験のほとんどがそのまま Haskell でも使えます。
初めて触る関数型言語Haskell とか、初めて触るプログラミング言語Haskell とかいう無理ゲーチャンレンジャーとは比べ物にはなりません。たまに他人にこの無理ゲーを押し付けようとしている人がいますが、はっきり言って正気の沙汰ではない。イエズス会の坊主並です。マヤ人みたいに頭開手術して脳を冷すべき。

とは言え、いくつか障壁はあります:

  • Haskell 独特の type class
  • 遅延評価が基本
  • Purity
  • カテゴリを使ったプログラミング

これらは上に挙げた他の静的型付関数型言語には無いものですから、Haskell に移行する際の障壁になりえます。ただ、幸いな事に一つ一つはそんなに複雑な仕組みではないので理解するのは難しくない。どちらかというと単純な仕組みから出てくる意味不明な結果(とそれを嬉しそうに便利だよ!と喧伝する Haskell 狂の人たちの叫び)に戸惑う方が多いので、使いもしないのに複雑な結果を結果から理解するのではなく、必要に際して少しづつ勉強していくのが Haskell をあくまで道具として気長に使っていく正しい方法だと思います。

さて、私はどうだったか。私は type class の様で type class が無い多重定義型システムのテーマで論文を書いていたので type class は敵としてその利点と難点とか挙動は判っていました。遅延評価や purity はそれ自体さほど難しい物ではありません。カテゴリを使ったプログラミングと言っても、せいぜいが monad です。OCaml でも monad は普通に便利なので前の会社で使って慣れていたので問題ありませんでした。IO monad を使った副作用の隠蔽がしばしば Haskell コミュニティーで話題になりますが、そんな物は副作用がある自分の好きな処理系で IO monad を自作してどう動くかを確認すれば小一時間で理解できることです。そんなわけで Haskell を使って、おー Haskell すごいにゃー、という体験は私には無いのです。あー、 Haskell だとこう書くのね〜、くらい。まあ monad transformer が理念はわかるが使うときにどっちがどっちかわからへんー、位、です。どうです?あなたもなんだかHaskellで仕事ができそうな気がしてきましたか?

さて、その逆はどうでしょうね。私はもう経験したくても経験できないのですが、Haskell プログラマーOCaml のプロジェクトに飛び込んで、どうなるか。やっぱり Hindley Milner の範囲ならお仕事はできるんじゃないでしょうかね。もちろん、私が Haskell 使ってブツブツ言うのと同じで OCaml はここが使えん!とかブログに書いて精神の安定を維持したりするんだと思いますけど。まあ、死にはしねーよ。ブログがササクレル程度だよな。ちょうどココ (http://d.hatena.ne.jp/camlspotter) みたいにな。

さてここからは Haskell について、実際のお仕事で感じたことを、OCaml と比較しながら、いろんな側面から dis る訳ではなくて、腐す訳でもなくて、もしできれば建設的にお話をしていきたいと思います。間違ってたり、いや、その書き方は前世紀の書き方で今はこうするんや、とか、あると思うんですが、科学的な議論を書いているんじゃなくて、半年間で使った印象を書いているので、勘弁してください。でも突っ込み歓迎です。

文法

Haskell の文法やコーディング慣習には、いろいろな工夫がしてあって、プログラムを短く書くことが出来ます。それに比べると OCaml のプログラムは、言語の表現力の比較以前に、プログラムが短く書けるような文法ではありません。Haskell プログラマOCaml のソースを見るとまずその五月蝿さに辟易する事でしょう。OCaml のコミュニティーでは、よく、中身が OCaml (eager で副作用がある) で文法が Haskell だったらいいのに、という話が出るくらいです。言う割には誰もそういう改造を作りませんがw まあ、これは言う場所を注意してください。INRIA とかで言わないようにw

だと言って、短ければ絶対善か? Golf で仕事したかったら野外でお願いします

例えば、Caml でも無いのに Haskell では CamelCase が普通ですね。私はこれは読みにくいと思うんですが… まさに golf プレイヤー(コードを一文字でも減らしてプログラムを書きたい人たち)が狂喜乱舞する文化ですね。

ただ、逆にゴルフ精神を突き詰めすぎて、Haskell プログラマは時に読みにくいプログラムを書くこともあります。例えば、a*b+c/d のように、スペースを入れないプロゴルファー猿とか、短く書ければ正義と勘違いして他人が読んでも判らないポイントフリーを多用するお脳がフリーダムな人も沸いてきます。もちろん、お一人でオナニーするんだったらさっさと短く済ますのは御本人の嗜好ですからどうぞご自由になのですが、チームで働くんだからもう少し長持ちして気持ちよくして欲しいですね。どうせスペースを入れたり、判りやすい名前の一時変数を導入したりしたくらいで、ハードディスクの空きが無くなるほど、あなた、 Haskell 書いて無いでしょ?

だからと言って OCaml がいい訳でもありません。Type-class が無いので唯でさえモジュール名を何度も明示しなければならず、プログラムが長くなりがちなのだから、他の部分はもう少し短く書けるように工夫すべき、でした。Caml 族の発展の初期に方向修正を行えば何とかなったのかもしれませんが、今から大幅に文法を変えるわけにもいきません…せいぜい CamlP4 文法拡張で短い文法を開発するぐらい、か、それとも見限ってブランチ切って僕の考えた最強の駱駝さんを作るか。それは、あなた次第です。

キーワードが短く少ない

今度は、キーワードに限って話しましょう。Haskellソースコードemacs で読み込むと(もちろん、キーワードハイライトをオンにして)、色があまり付いていない事に OCaml プログラマは驚きます。キーワードが余り出現しないのですね。出現しないだけでなく、キーワード自体も短めです。(ex. OCaml との比較: \ <=> function, case <=> match, a <=> 'a) 短いことはいいことじゃないですかね。

OCaml のソースはキーワードが頻出して、Haskell プログラマから見ると汚く見えます。その上、OCaml は明らかに無駄なキーワードがいくつかあります。例えば、

type t = Foo of int

とか。この of は明らかに不要です。3.12.0 の module type of M とかも長いですね(その代わり、今までのキーワードを組み合わせる事で、新しいキーワードや記号を導入せずに済んでいます。) この辺りをすっきり出来れば少し嬉しいかも知れません。でも変な記号入れて誰も覚えられなくなるのも本末転倒ですね。

でも、キーワードが多いのは悪い事ばかりではありません。プログラム中のミスタイプ等で間違って型が通ってしまった事によるバグは一般的にとてもデバッグしにくいのですけれども、冗長な文法はこの種のバグの発生を抑えてくれます。文法に冗長度が低く、型推論が強力な Haskell ではこの問題でしばしば苦労させられます。(例は後の offside ルールなどで説明しますね。)

また、冗長な文法では、ぱっと見のプログラムの構造が(特にキーワードハイライトがあると)判りやすいという効果もあります。例えば Haskell では変数の定義をソースコード中から探し出すときに、特徴的なキーワードが欠けるため、目での検索には苦労します。OCaml でなら let や and を使った肉眼grep が出来るんですが… Haskell では何もキーワードが無いので、ちょっと困ります。私の目が慣れていないだけでしょうか。
これは機械的に検索する場合も同じです。OCaml ならば、let や and と組になっているので、それで結構機械grep できるんですが、Haskell ではそうはいかない。幸い、Haskell はモジュールが貧弱(その代わり type class がある)なので、トップレベルの定義はインデントレベル 0 で始まるはず、というか始めて無いとコ◇ス訳ですけど、

find . -name '*.hs' | xargs grep -n ^定義を探したい名前

で何とかできます。もちろん、grep みたいな low tech 使うんじゃなくて、haddock や hoogle を使えばトップレベルの定義やドキュメントは探せますが、こういうツールが動かせる前、つまり、コンパイル通ってない時点でのソースの検索が面倒かなあ。

$ はいいね!

Haskell の $ は良いです。もちろん、括弧を使う書き方と、 $ を使う書き方と二種類書けてしまう、という点で、TIMTOWTDI っぽいのが気になりますが、それでも open と close が離れがちな括弧の組を $ 一つで書けるのは魅力です。括弧を増やしたり、減らしたりするときのカーソルの移動の量が減りますし、なんと言ってもこの閉じ括弧はどの開き括弧に対応するんかいな、と迷う事が少なくなるのは嬉しい。(ああ、でも emacs なら (show-paren-mode 1) は当然使ってますよね!)

私がもし $ を OCaml に入れるとすれば文法要素としてか、マクロで入れちゃいます。OCaml はインライン展開とかしない(それでも早いのが魅力なのですけど)ので、$ が普通の演算子だとちょっとパフォーマンス的に痛いので。(いや、overhead はたぶん凄く少ないですよ。でもそれでも気になるシチュエーションもあるのです。) でもその代わり、パターンでも書けるようにしたい。Haskell だと $ はパターンには書けないですよね…

Offside rule は何もいいこと無い

百害あって、一利あるか、どうか。インデントでプログラムの意味が変わる offside rule はいらないですよ。

Offside rule はプログラムの制御枠構造 ( C で言うところの { ... }, OCaml で言うところの begin ... end ) の閉じる部分を省略する文法機能です。このおかげで制御構造の閉じの連続 (C や C++ で延々と } } } } が続いているコードを見たことがあるでしょう?) を避ける事が出来、見た目がすっきりします。 上でも書いた (..) に対する $ 同様、対を気にしなくて良いのが嬉しい…と言いたいところですが、それは違う。

Offside rule では枠構造の閉じは省略されているのではなくて、目に見えにくいインデントになっている。これ問題です。インデントはキーワードハイライトされませんから、どこで構造が閉じられているのか、特に do 式で非常にわかりづらい。もちろん、次のような式では問題ありません

-- プログラムに意味はありません
foo bar = do
    x <- get
    y <- get
    z <- case x + y of do
        0 -> return 0
        n -> compute n
    return (z * z)

でもネストが深くなって、間隙が広がってくるとこれが難しい:

foo bar = do
    x <- get
    y <- get
    z <- case x + y of do
        0 -> return 0
        1 -> ...
        2 -> foobar
               ++ length $ boo
                                ++ [ "xxx"
                                   ; ...
                                   ; ...
                                   ]
                                ++ hoge
        n -> compute n
   return (z * z)

これはエラーになるんですが、わかりますか?わかるなら、まだお若いんですね。もうおじさんはこれだけ行が離れると横の位置が同じかどうか目がチカチカしてわかりません。

この目のチカチカを避けるためか、どうか、出来るだけ間隙を狭くするために、Haskellプログラマーは無意識にワンライナーになります。例えば上の例だと 2 のケースをだらだらーと一行に書きたがるのですね。その結果、一行500文字の Haskellコードなどが産み出されるのです。私は出来るだけ長くプログラム書くキャリアを続けたいんで、フォントは大きいんですよ。何ポイントか知らないけど、30inch のモニタでウィンドウいっぱいいっぱいにして190文字位しか一行に出せないんです。500文字のためには30inchモニタが三枚必要なんです。あなたは金を出してくれるんですか?ってことです。さすがに Haskell プログラマも一行が長すぎるとやはり気が咎めるらしいのですが、それでやる事と言えば、変数名の長さをケチるのです。CamelCase は始まりでしかありません。ただ、\ a -> とか。a たあなんだよ。読む人のこと考えろよ。

また、Haskell では不用意にインデントをいじりを間違うと、プログラムの構造が変わってしまう恐れがあります。上のようにエラーになる例は、まだ良いのですが、同じ型の do 式がネストしてた日には、型検査も通ってしまってバグが発生する事があります。このデバッグは原因がアホらしい上に非常に難しく、深刻です。特に他人と共同で作業している場合、いつの間にかインデントが変わっていて適当にマージされてしまいもう訳がわからない(コンフリクトが起こることを期待するばかりです)。閉じ構造が明示的にキーワードで書いてあればコンパイラが数が合わなければ見つかるのに… (なんだか横の奴が Darcs だとプログラムのパースツリーを見てマージしてくれるらしいから大丈夫みたいだぜ、とか言ってるけど、、、ホンマかいな。こいつ時々適当なこと言うからな…そもそも俺ら使ってんの Darcs ちゃうやろ!

もうちょっとカジュアル言うと、ある程度インデントがそろったプログラムで、途中で do を入れたり、削ったりする時に、エディタである領域のインデントを一定数増やしたり、減らしたりするのがとても面倒ですね。一行一行ちこちこと調整するのもオカシイし、emacs だと (string-rectangle start end string) もありますけど、例によって老眼っぽいと何文字入れたら良いか数えられない。これが面倒臭いので Haskell プログラマはインデントが変わるようなプログラムの修正をするくらいなら、妙ちきりんなカテゴリの概念使ったら格好良く書けるよーとか、キモい言い訳をしながら、スクラッチから関数を書き直してしまうヴァカが多い気がしてなりません。はい、パラノです。
この件では入社直後にぶち切れて offside-trap.el (http://bitbucket.org/camlspotter/offside-trap) を作り、そのお陰でかなり個人的にましになりました。目の前でポコッと30行くらい一度にインデントを修正してやると、横で見てる人がビビるので楽しいです。あとはカーソルのある行と同じインデントレベルの行を上手いことハイライトしてくれるエディタ支援があると嬉しいですね。大体こういうことを言っているとエディタと言語は関係ないとかまたネヴォケた人がどっかから必ず出てきますが、そうですか、ed で仕事しててください。

結論として、offside は見た目はいいかもしれんけど、

offside => インデント面倒い => 改行しない => ワンライナー => 読めない => だから少しでも読みやすいように offside

の offside spiral。害が多すぎて駄目。私は目が疲れそうになったら即刻 do { ; ; ; } と書きますねぇ。皆さんも書いた方がいいと思いますよぉ。OCaml みたいに begin end だと確かに手が疲れるけど、これなら一文字づつだから。

OCaml にもインデントの問題が

ひるがえって、OCaml では begin end が時々嫌んなりますよね。特に副作用がある言語なので、 Haskell の do { ; ; ; } より多めに書くことになりますから。 OCaml にも CamlP4 拡張で whitespace thing (http://people.csail.mit.edu/mikelin/ocaml+twt/ )てのがありまして、 offside rule で書けるのですが、私は offside rule は上の理由でヤ!

begin end の代わりに普通の括弧 ( ) でも意味は同じだし、短くていいのですけど、制御構造の枠は別の色が付いていると嬉しいなあ、という理由で普通皆さん begin end を使うのです。でももちょっと短く書ければ嬉しいねえ。何がいいのかな? (; ;) とかどうかな?泣き笑い?きもいな。

OCaml は明示的に開いた枠構造は明示的に閉じなければいけませんから、インデントが変わったからと言ってプログラムの意味が変わったりしない。それは良いのですが、その周辺で別の問題があります。function、match や try をネストした時です:

match foo with
| Bar ->
    match boo with
    | Zee -> ...
    | Goo -> ...
| Poo -> ...

このインデントはオカシイですね。 Poo のケースはインデントからすると foo の match に使われていますが、実は boo のケースです。これは caml-mode.el とか tuareg-mode.el を使って自動インデントさせれば | Poo -> が | Zee や | Goo の所に移動するので、すぐ判るのですが、でも、それでも、たまにこれではまったバグで苦しんだりします。

この問題は Haskell の do とちょっと似ていて、 function, match, try には終了記号が無いのが原因です。終了記号を明示するためには、OCaml では begin end を入れてやる必要がある:

match foo with
| Bar ->
    begin match boo with
    | Zee -> ...
    | Goo -> ...
    end
| Poo -> ...

しかし、この begin end が面倒なのよね。Goo -> を書いて、よし、これで Bar のケースは終わり、って時に、あ、end 必要ジャン、と気づいて、終了記号の end を書くのは良いんですが、そこからカーソルを上に持っていって match の前に開始記号 begin を入れなきゃいけない。コードが長くなると、このカーソルの動かすのが結構いやなんですよ。

これを解決するには、

  • end を書いたときに自動的に match の前に begin を入れるエディタ支援を作る
  • function, match, try の文法を変えて、終了記号を明示的に書く。例えば、function ... done, match ... done, try ... done

てのが思いつきます。一つ目は、エディタごとに支援を書かなきゃいけないのと、end を書いたときに、どこに begin を入れたら良いか、実は不明だって事が問題。(ああ、インデントレベル見て自動的に入れるのが良さそうです) 後者は、function, match, try がネストすると閉じ記号が連続してウザくなりがちなことです:

match foo with
| Bar ->
    match boo with
    | Zee -> ...
    | Goo ->
         match
         ...
              try
                   ...
              done
         ...
         done
    done
| Poo -> ...
done

ダンダーンダダーン! OPA (http://d.hatena.ne.jp/camlspotter/20100108/1262954926) ではこの方法を取っていますが、done の繰り返しを少しでも避けるため、デフォルトケース | _ -> があった場合は done を省略できるようになっています。デフォルトケースの後は、そのレベルでケースを書くはずが無いので。これはちょっと面白いアプローチですね。

と、見て来た様に、Haskell, OCaml 共々この点については問題があり、私はどちらも良いとは思えません。今私が考えているのは、Haskell では { ; } を使う、 OCaml では文法はそのままで、コンパイラがインデントレベルをチェックして、意味上は同じケースレベルなのにインデントがずれていると警告を発するというものです。

match foo with
| Bar ->
    match boo with
    | Zee -> ...
    | Goo -> ...
| Poo -> ...

例えば、この例だと、Poo のケースは boo の match なのに、そのお仲間の Zee や Goo より左に存在するから警告する、とか。これは結構良いんじゃないかと思いますがどうでしょうか。

List comprehension

[0..9] とかはいいと思うんですけど、filter とか、map とかまで list comprehension で書いてあるコードを見ると、ただの obfuscation にしか見えません。基本的に、リストからリスト作るときは十中八九、map か fold なんで、じゃあ map なり fold なりで書いてほしいですね。

名前空間: 節操の無い full import はやめてほしい

Haskell では他のモジュールで使われている型や値の名前を使用するには、それをインポートしなければいけません。そのために import 文があります:

import Hoge(foo)

この文では Hoge モジュールの foo を使うと宣言しています。こう書くと以後 foo と書けば Hoge の foo、つまり、Hoge.foo の事になります。一つのモジュールで定義されている複数の名前を使いたいときは、import Hoge(foo, bar, boo) と書くのですが… 面倒臭いですよね。なので、

import Hoge

と書くことで Hoge モジュールで定義された全ての名前が使えるようになります。手軽ですね! これは私は何と呼ばれているか知らないのですが、full import と呼ぶことにします。

Full import は手軽ですけど、多用すると訳が判らなくなります:

import Hage
import Hege
import Hige
import Hoge
import Huge

f = pon

さて、ここで、pon はどのモジュールで定義されているのでしょうか。Hage.pon? Hige.pon? これだけじゃあ、判りません。このように、full import しすぎると名前空間が平らになりすぎてしまい、コードを読むとき、どの名前がどこで定義されているか、トレーサビリティが下がってしまいます。pon を探すには、grep とかしなきゃいけない。エディタからシェルに移る。grep する。コードを読む。また判らない名前が出てくる。full import 沢山。また grep。ハッキリ言いますが、人間はスタックマシーンではありません。がんばってスタックマシーンをエミュレートしても、この grep => 読む => また grep を二三回繰り返すと、一番初めに何を調べたかったのか、覚えているのは難しい。時間の、無駄、です。

実際 ghc コンパイラは full import は止めた方がいいよと警告を出してくれます。多分、そういう意図で警告を出してくれてるんだと思います:

$ ghc -c -Wall pon.hs
pon.hs:1:0:
    Warning: The import of `Hage' is redundant
               except perhaps to import instances from `Hage'
             To import instances alone, use: import Hage()

まあ、例によって(type class instance の使われ方を知らないと)訳のわからん警告ですがw

名前空間をガンガン広げたい場合は full import じゃなくて、qualified import を使うべきだと思います:

import qualified Hage as A
import qualified Hege as E
import qualified Hige as I
import qualified Hoge as O
import qualified Huge as U

f = I.pon

こう書くと、例えば Hige の中の名前 name は I.name としてアクセスできるようになります。逆に、 I.pon と見れば、モジュール Hige の中の pon だとすぐ判る訳ですね。

これは OCaml でも同じです。OCaml の import は open と言います。この open は full import の機能しかありませので、やはり、多用すると名前空間がノッペリしてしまってダメになっちゃいます。ですので、某所では open は一ファイル4っつまで!それ以上開きたいときは open じゃなくて、module alias, module I = Hige を使えってルールになっています。(module I = Hige は import qualified Hige as I と同じような意味です。)

かように気軽過ぎる full import はコードを書く人ならともかく、読む身からすると大変に辛いのですが、lazy な言語 Haskell の lazy なプログラマ達は基本的にあまりこれを気にしません。これ、絶対良くないですよ。

え、俺は気をつけてる?それは良かった。でもね、某巨大有名 Haskellプロジェクトの約1400個の *hs ファイルを調べたら、こんなんです:

  • ファイルあたり、平均して 6個の full imports (汗
  • 10個以上の full imports があるファイルは全体の 22% (ううぅ
  • ファイルあたりの full imports の最大は61個 (うげっぇえええ、えんがちょ、えんがちょ!

コード読む気無くなりますよ 61個も開かれたら。完全に周辺モジュールの状況を理解している人しか、コード読めません。一見さんは帰れ、そう言われているようです。これは新規参入障壁ですよ。秘密結社でコード書いてるんじゃないんですから。

これはプログラマの慣習の問題だろう、と考える方もいるかもしれませんが、私はそうは思いません。これは一部 Haskell の言語仕様にも関係があると思います。例えば、 OCaml では open が full import に相当するのですが、例えば open を 10個も並べたモジュールはほとんど見たことが無い。(見た瞬間に私修正します。) なぜか。幸か不幸か OCaml には Haskell の様な type class による多重定義が無いので、関数はしばしばそのモジュール名を明示します。List.length とか Array.length とか。(まあ、言い換えると型を書いているのと同じなんですけど。) この様にモジュールの関数にアクセスするのが普通なので open List とか open Array とか、あまり書く必要が無いんですね。そして List.length, Array.length は Haskell の様に qualified import の様な文を唱えなくてもはじめから使えるようになっています。一方 Haskell では何か外の物を使うには(Prelude はともかく)、まず import を使わないといけません。そして import で qualified とか import list 書くのは面倒。人間は面倒が嫌いです。はい、ようこそ full import の世界へ… ただ、Haskell の import には type class instance のどれを使うかを指定する意味もあるので import なしに OCaml の様に自由に外部の値にアクセスするのも、はばかられるのかもしれません。

で、まあこうブツブツ言っていても、世の中には full import が 20個とか、そういう Haskell モジュールに満ち溢れていて、それを使って生きていかなければいけない。こういう露出狂コードを読むにはカーソル下にある名前がどこで定義されているか自動的に調べてくれるエディタ支援ツールが必須です。例えば OCaml だと、まさにこのために私は OCamlSpotter を作りました。Haskell ではどんなツールがあるのか私はまだよくわかってませんが、下調べしたところ、型推論後のソースツリーにはそういう情報がある程度有るので、その情報をコンパイラから抽出すれば良いはずです。ただ、ローカル変数や式の型については泣きたくなるほどガン無視…なんてこった、という時点から先に進んでいません。GHCi にもそういう機能があるそうですけど、GHCi でいちいちモジュールロードするのが時間が掛かりすぎる巨大プロジェクトとか、そもそもうまくロードできない環境とかあるんですよ。
でもね、そもそも、せっかく Haskell っていうイケてる事になってる言語つかってんだから、ツールなくても他人が読みやすいコードを書きたいし、書いてほしいですよねえ。もしチームで仕事するならね。Full import の数は減らしましょう!

Exhaustiveness check

あんまり Haskell の人って pattern match の exhaustiveness check にこだわんないよね… こだわったほうがいいですよ。Sum type 拡張して後でプログラム実行時に run time error とか、wild card (_ -> ...) 使ってて想定外のデフォルト動作で苦しむよりは、ちゃんと exhaustiveness check して、安易な wild card は使うのをよした方がいいです。

Laziness

遅延評価は便利ですけど常にはいりません。終わり。

ていうか、今まで散々言われていることの繰り返しです

  • lazy なアルゴリズムの計算量を見積もるのが大変。
  • lazy なアルゴリズムをちょっと変えると計算量が驚くほど変わってしまう場合がある。変化が読めない。
  • Haskell の operational semantics なんか誰も判らない (このペーパーにある、とかアホな事言わないで下さい。直感として、Haskell のプログラムをポイと渡されて、何がどのタイミングで起こるってすぐに正しく言える人はいないということです

Lazy ですと色々楽しくプログラムが書ける事は確かですが、本質的に lazy やないとどもならん、というのは実際の業務ではほとんどありません。zip [0..] list とか、面白いかもしれないけど…だから何?時々こういうのの発展形の意味不明な短いコードを見るとムカムカします。俺はパズル解きに仕事してんじゃねぇよ。コメントさえ書いてくれてればいいんですがねぇ。そういう時に限って無いのです。

それより心配なのは、lazy で計算量が読めないのをいいことに、計算量について深く考えずにとりあえず動くコードが生産されてしまう所です。

Laziness makes Haskell programmers lazy.

例としては以前 primes の話を前書きました。あんな例でさえ誰も長い間計算量について気がついていなかった。では仕事の non-trivial なコードでは? Haskell で本当に speed sensitive なプログラムを書くのはかなり大変な事なのではないでしょうか。私の今の仕事はそこまで speed critical ではないんですが、Tsuru capital では Haskell を使った High Frequency Trading をやっています。どうやってスピードを追求しているのか… (2011/04/04訂正) Arbitrage をやっていると聞いていましたが、実際には Statistical arbitrage なようです。Stat arb では常時高頻度な取引はさほど必要ありません。Haskell でもなんとかいけるでしょう。

Purity

Purity で私が素直に感動したのは、Haskell では動いているコードの一部をカットして、別の場所に移しても、ちゃんと動くってことです。これは、状態が無い、というか、あっても隠蔽されてるからですね。恰好良く言えば参照透明。副作用のある言語だと、リファクタリングの際にはコード片をよーく見て、コード片が依存している状態があれば、そいつについてちゃんと面倒を見てあげないとバグってしまいます。Haskell ではこれがない。素晴らしい。

Purity で私が純粋にむかつくのは、この状態を持たせたい時に面倒くさすぎること。ある時、あるデータファイル群が何度も何度もロードされるのを気づいたので、いくらなんでもこれは I/O で重いだろうとキャッシュを入れようとしました。キャッシュと言えば状態です。元のコードはキャッシュの事など考えて書かれていませんから、データファイル読み込み関数からモナドで状態を引き回すコードを書き始めました。問題はこの読み込み関数がかなり広範囲に使われているので、モナドで書き直す末端が数十ヶ所。空のキャッシュを作ってこの末端まで全部モナディックに変更。変更は百箇所位。うわーっ!面倒臭せー!!

必死こいて直してたら、一言、グローバルキャッシュを unsafePerformIO で作ればいいじゃんとかサクッと言われた訳です。確かに、そうすると、引き回しがいりません。でもね、昨晩は Haskell は pure だからイイ!並列もカンタン!!(私:おいおい嘘だろ)って言ってた人が一夜明けたら unsafe でいいよーですよ。Haskell は「純粋」という看板は下ろすべきだと思いますね。どういう場合 unsafe を使うか、しっかり説明すべきです。(どっかでされてるのだと思いますが)

関数型パラダイムの中で副作用は十分に管理可能だと私は考えます。Haskell の様にデフォルトで不可能にして、もうどうしようもない時に unsafe を唱えさせるか、OCaml の様に、もっとプログラマを信用してプログラマに最小限最大効果の副作用コードを書かせるか。これはもう好みの問題ではないでしょうか。

型システム

型システムは Hindley Milner parametric polymorphism ベースで大変によろしい。この型システムの良いところは人間でも十分に理解可能かつ表現力もある、ということに尽きます。どんな言語でも、人間、プログラム書いてるときってある程度無意識に型推論して書いていると思うんです。なので、理論的には判ってなくても、何となく、自分の書いているプログラムの型が判る、程度の型システムの理解は必要だと思うのですが、その範囲内で一番強力なのが HM なのかなあ、と思います。大体、他の僕の考えた最強の型システムってやつは強力かもしれないけど人間では理解できない物が多いからね。

No value polymorphism

Purity の所で腐しましたが、pure なおかげで value polymorphism が無いのは嬉しいですね。得に、モナドを使ったプログラムを書く場合には x = y >>= z などと書いて、 x が polymorphic である事が多いのですけれど、value polymorphism では、これはそのままでは polymorphic にならない。eta expansion が必要です。まあ、見た目が悪いと言うことなんですけど… Haskell ではそれがいらないのは、いいですね。

Overloading is nice, but...

Type class は結構好いです。OCaml にもあったらと思いますねえ。ただ、副作用がある言語なので、dictionary dispatching の abstraction と application が implicit に入ると副作用が何時起こるかわかり辛くなりそうです。ああ、わかんない? Type class と副作用はあんまり相性良くないと思ってください。

Type class による多重定義は大変強力なのですけれど、強力なだけに、問題もあり… この間は IO () の型を持つ巨大なプリント関数を、プリントするのではなく、行の文字列を返すべく [String] に変えていたのですが、IO a も [a] もモナドなのですね。だから、どこぞに >> が残っていたのですが、それがそのまま型検査を通っていたのですよ。IO a では m1 >> m2 は m1 を実行してから m2 を実行する、なので、m1 での出力も m2 での出力もちゃんとファイルに書き込まれていました。[String]では… m1 >> m2 と書くと、m1 で作った文字列リストはただ単に捨てられてしまうのですね。これで何時間無駄にしたと…クソッ、今思い出しても…

基本的に強静的型付に慣れたプログラマの心は、型システムに依存しすぎているため、一旦型システムを突破してしまったこういうバグに対し非常に脆弱です。もし Haskell の型システムがここまで強力ではなく、IO a と [a] での >> の多重定義が無ければ、事前に型エラーとしてレポートされていたのですが…強すぎるのも、問題です。いやだからと言って、どこまでも弱くして全部プログラマが型を明示的に書く言語が良い、という訳ではなく。Haskell には他の型付関数型言語にない、こういう落とし穴があるんだなあと。

deriving

deriving で show とかのメソッドを自動生成するのは、良いですね!! 便利に使っています! OCaml にも普通にあると嬉しいなあ。一応、type-conv とか使えば現在でも同じ事ができますよ! (Haskell じゃないので、 explicit dispatch が必要ですけど)

言語の外! これが結局重要

Haskell を使って素晴らしいのは OCaml 同様、その道のエキスパートと仕事ができること、それに尽きます。まあ、関数型言語プログラマコモディティ化してない、現在の所、と言っておいた方が良いのかもしれませんけど。多くの人達はきちんとしたコンピュータサイエンスの勉強・研究をしてきた経歴を持ち、プログラミング能力も高い。

ただ、問題もあります。これらの人達はまあ、何というか、私は言わない事にしますね。おお、ちょうどいい、こんなことを言っている方がいましたよ:

まあ、あれですよ。みんないい人達なんですけど、私を含め、彼らは、大学や研究所とかで、完全個人プレーヤーとして最適化されてきた経歴の人々なんです。Haskell の人はそういう人が特に多い印象を持ちますね。そういう人達をチームプレーで使うのは大変です。ソフトウェア工学的なアプローチは通用しません。なぜなら、それぞれが、あまりに特殊技能の持ち主達なので、人月(月には肉の意味もありますね)みたいなコモディティ化は難しい。よほどマネジメントがしっかしりて方向性を打ち出したり、独創/独走しないように two-man cell で働かせたり、とかしないといけないです。まあ、私はこんな事を書く位ですから、すごいチームプレーヤーですよ。まかせてください!
(私はプログラマを数値化、交換可能な単位に還元できるという前提のソフトウェア工学って嫌い。究極的には経営側理論っしょ?一人の雇われ職人として生きていきたい人は、こんな物崇めてはいけません。ここから外れるニッチなプロ中のプロを目指そう。)

終わりに: ワナビーに一言

それでもHaskellなら、Haskellならきっと何とかしてくれる

  • 糞な同僚のコードから解放される
  • 糞な同僚から解放される
  • 糞から解放される
  • 解放される

結構こういう呟きをしているワナビーが日本には結構居ますが、世の中そんな厨二病的に都合良く出来てません。関数型ニートになってしまって、どうにも使い物にならなくなる前に、さっさと目を覚ませ!

言語が純粋であろうが遅延評価であろうが、素晴らしかろうが、それは糞や、糞な同僚や、糞の同僚のコードとは、何の相関関係もありません。原因は別の所にあります。そしてそれに気づかず道具さえ変えれば何とかなると思っているあなたの頭の中にも糞がつまっています。そういう糞から本当に開放されたければ、あなた自身が糞と対峙する以外に道は無いのです。糞を無くすべく努力するか、もし無理なら、次の転職先を選ぶときに糞じゃない所を注意深く探すだけ。それだけです。もしその過程の結果として Haskell が使えるようになれば、それはそれは喜ばしいことですね。

じゃーねー