Haskellで生産性を高める-Pythonからの移行

(注記:11/30、いただいた翻訳フィードバックを元に記事を修正いたしました。)

最近、Haskellでも生産性の高い作業ができるほどに、この言語を使いこなせるようになりました。定期的にPythonを使いもしますが、今ではWebプロトタイプ作成のほとんどをHaskellで行っています。それで、時間が経ってしまう前に、この言語の学習経験を通じて考えたことなどをまとめたいなと思っています。

データファースト

これはどちらかというと動的言語から静的言語への移行に関しての考えなのですが、Haskellのデータ構造は、ほとんどの場合、データ宣言と型シグネチャで提示されるのに対し、Pythonの場合、おおむねコードによって暗黙に定義されます。

Pythonの関数について私が初めて抱いた考えは、「コードに何が書かれているか?」でしたが、Haskellでは、「データはどう見えるか? この関数は___を受け取り、___を返すか?」でした。

この”データファースト”というアプローチを意識することで、私のコーディングは(たとえPythonに戻っても)ずいぶん改善したと思います。「簡単だから」とか「より問題がわかりやすくなるから」といった理由しかない無意味なデータ構造の変更を、以前より判別できるようになりました。

また、データ構造の変更の制限により、コードの複雑化が避けられるため、コードをより理解しやすくもなります。

読みやすさ

私がPythonを使っていた主な理由は、コードの読みやすさにありました。Haskellは当初、見ようによっては精巧なハンドクラフトの一例と言えなくはありませんでしたが、とにかく見た目はひどいものでした。各パーツはとても明快な反面、その周りはナンセンスなガラクタに囲まれているような感じです。しかし、Haskellは明らかにパワフルでした。

パワフルではあるものの紛らわしくもある”賢い”コードは避けたいのが本音でした。

ただ、読みやすさの評価として私がやったことは、違う命令型の言語を、その違いで評価しただけに過ぎません。これでは英語のネイティブユーザが中国語の読みにくさを論じているのと、さほど変わりはありませんよね。

そうこうするうち、”賢いけど紛らわしい”ことがHaskellの特徴ではないことに気付きました。もちろん、他の言語と同じく、Haskellで”賢い”コードを書くことはできますが、それは一般的なケースではないでしょう。

実際のところ、Haskellでは型システムによる制約があるため、”賢いコード”は賢いことしかできません。Intを返すようになっていればIntを返し、それができなければコンパイルに失敗します。

Haskellがもたらすパワフルで正確な抽象化のメカニズムは、私がPythonでは避けるようにしてきたマジックのようなものに思えることがあります。

読みやすさに気付く

最初の方は、「他の人だって日頃から支障なくHaskellのコードを読んでいる」というような思い込みも必要でしたが、私の場合は、一度壁を乗り越えると、コード自体がとても読みやすくなりました。

1. 型シグネチャ。これは本の章の前に置かれた簡単な要約のようなものです。加えて、それが真であることが保証されています。新しい言語を学ぼうとする時に、これがあれば便利だと思いませんか?

>本章で、TommyはMarket(市場)に行ってduck(アヒル)を買います。

chapter :: Tommy -> Market -> Duck

2. 関数を、小さい関数で構成すると、複雑さが大幅に軽減されます。きちんと名前が付けられていれば、読みやすい関数を書くことができます。

3. 簡潔なため、パワフルなアイデアを表現する時にも多くのコードを必要としません。

中置記号とノイズ

Haskellのコードによく見られる中置関数についても、お話ししておきます。中置関数/演算子は、2つの引数のではなく、そのに置く関数のことです。典型的な例としては、加算の+が挙げられます。

Haskellでよく使用されるのは$、<$>、<-, ->などですが、なじみのない人にとっては、この記号に絶望や腹立たしさを覚えるかもしれません。

しかし諦めることはありませんよ。賢いけど紛らわしいように思えますが、通常使われる中置記号は限られています。一度知ってしまえば使いやすくて簡単であることが分かると思います。私が常用しているのは、だいたい5個ぐらいだと思います。

とは言いましたが、最初はlens ライブラリは避けた方がよいと思います。なぜならlens ライブラリは中置記号があまりにもたくさんあるからです。lensはとても素晴らしいライブラリですが、使わなくても問題はありません。Haskellである程度の大きさのプログラムが書けるようになってから、暇つぶしに試してみるのがよいと思います。

全く新しい語彙

Haskellを勉強していると全く新しい単語を覚える必要があります。例えばFunctorMonadです。

これらの単語を覚えるのが負担に感じる理由がいくつかあります。命令型プログラミングを学び始めた時、多くの新しい語彙があったとしても、多少なりともなじみやすさがあります。ループという単語で連想するものは、そう、輪。レース場のトラックやローラーコースター、それから、リング型のシリアルとか。

人は何かを記憶するとき、すでに記憶にあるものに関連付けようとします。もしあまりにも多くのなじみのない単語が文中に出てくると、人の脳は拒絶してしまう傾向にあります。私はFunctorという単語から何も連想できなかったため、記憶するのが大変でした。

私はこのような単語を覚えるために、自分にとって分かりやすい名前を付け、そのなじみのない単語が出てくるたびに頭で付けた名前に置き換えていました。しばらくすると、自作の同意語が心に刻まれ、“なじみのない単語”は問題ではなくなりました。

例えば、Functor。

Haskellにおいてはmapを可能にするものです。例えばリストはFunctorです。これは、リスト内の全ての要素に別の関数を適用し、新しい結果のリストを返すmap関数があるということを意味します。

map (+1) [1,2,3,4]
-- results in [2,3,4,5]

そこで私はFunctorを「Mappable(map可能なもの)」と読み替え始めました。Mappableは覚えやすい上に役割を的確に表現していると思いました。リストはFunctor。リストはMappable。

頼れるPrint命令文

Pythonでは、私の主な開発ツールはprint命令文/関数でした。

Haskellでは、私の主な開発ツールは型システムです。通常print命令文を使ってチェックしていたものをチェックしてくれます。例えば、関数がどのようなデータを受け取り、返すのかなどをチェックしてくれます。

しかし、Debug.Traceを使えばHaskellのIO型に悩まされることなくPythonのprint関数のようなことができます。Haskellを始めるときにはこれはすごく便利です。しかし一度Haskellへの移行が済んでしまうと、思いの他この機能は使いません。

もしデバッグ完了後もtrace文をコードに残すのであれば、Pythonで残すよりもHaskellで残す方が、見た目がひどいと感じると思います。

最高のMonadチュートリアル

最高のMonadチュートリアルは、Parsecチュートリアルでした。

Haskellで生産性を高くしたという話には、Monadをいかにして習得したかの説明が付きものです。それでは、ご説明しましょう。

私にはパーサを書く必要がありました。すでにPythonで書いたものがありましたが、パーサプログラミングの経験不足からコードは複雑になってしまい、開発速度は大幅に低下していました。

そこで、こうなったら徹底的に時間を使ってやろうと思いました。これを機にHaskellを試してみようと思いました。

私はYouTubeで、「Parsing Stuff in Haskell」というビデオを見つけました。このビデオではParsecライブラリを使ってHaskellでどのようにJSONパーサを書けばよいのか説明しています。

この動画では、思いがけずMonadとApplicativeをツールとして使い、自分に必要なものを作成する方法も知りました。また、これらの機能や、互いの関係性も学びました。

これらでパーサを書いたことで、その他のコードについても分かってきました。おかげで、その抽象的な性質についても分かってきたのですが、その抽象性には後々まで悩まされました。

また、Parsecは十分な構成を提供してくれたので、パーサを書いた経験が少ない私でも問題ありませんでした。私はプログラマとして数年Pythonを使っていましたが、パーサの専門知識はありませんでした。それでも、(私自身Haskell習得中の者として断言しますが)複雑さ、スピード、読みやすさ、拡張性など、どの点を見ても、その頃より優れたパーサを書くことができたのです。

習得のプロセスが驚くような糧になる

今や、Haskellは私がメインで使うWebプロトタイプ作成言語になっています。それにはいくつかの理由があります。

大前提として、どのテクノロジを使うかを選択できる点があります。これが恵まれていることは私も自覚しています。

  1. プロトタイプが速く書け、通常はそのプロトタイプが製品バージョンになる。
  2. 些細なバグで時間を無駄にしたくない。
  3. 他の言語に比べ、見つかったバグにより多くの意味が含まれているので、問題を深く理解するための助けになる。ここで、「意味が含まれている」とは必ずしも「より困難である」ということではない。
  4. Pythonはスピードをあまり気にしなくてもいいことを教えてくれた。Haskellも同様だが、Haskellの場合、スピードを速めてくれる。
  5. リファクタリングが簡単である。Pythonでは後から重要になるコードの細かい部分の変更を忘れることがあり、常に落ち着かない気分だった。
  6. ライブラリが優れている。Haskellは一定のレベルが保たれた言語であるため、ライブラリの基本品質もずば抜けているように感じる。そして、革新的なライブラリにも富んでいる(真っ先に思い浮かぶのはParsecやQuickCheckだが、他にもあるかも)。
  7. 役に立つコミュニティがある
  8. コードが拡張しやすいため、多くのコアを使うことができる。
  9. Haskellのインフラストラクチャは常に進化している。昨年にはGHC(Haskellのコンパイラ)7.8がリリースされ、元々高速で定評があったWebサーバ、Warpのパフォーマンスが2倍に進化した

最後に、Haskellのコードを書くことで、高い満足度が得られることも忘れてはいけません。これは、これまでのどのコーディング経験より、私にとって価値があります。

何から手を付けるか

何から手を付けるかを見極めるのも大変ですね。

そこで、「私がもう一度Haskellを習得し直さないといけなくなったとしたら、どうするか」を考えてみました。

まず、「Learn you a Haskell for Great Good」の第1章から第8章までを読みます。

それから…

  1. IOを気にしなくていい、ちょっとしたモジュールを書く。たとえば数独を生成する数独モジュールなどです。シードとして乱数を使うことを恐れないでください。起こっていることを確認するためのprint命令文としてDebug.Traceを使ってみましょう。パズルを生成し、Debug.Traceで画面上に表示しましょう。独自のデータ型を作成し、関数を使いましょう(つまり、カスタムのtypeclassは使いません)。
  2. ScottyまたはSpockを使って、作成したモジュールをWebサイトにしてみましょう。Webサイトはあくまでシンプルに、URLは数独を表すものにしましょう。その後は、数独のJSONを生成するURLにしてみましょう。
  3. 実際のIOをいじってみる。Debug.Traceを使わずにパズルを端末に出力してみましょう。
  4. パズルを段階的に追加する方法を考える。数独のファイル形式を作成し、これに対するParsecパーサを書きましょう。ファイル形式はJSONベースにしないでください。

それでは皆さん、頑張ってください!