Haskellを後にして去る
(journal.infinitenegativeutility.com)- 2009年ごろから2019年ごろまで Haskell を主言語として使っていたプログラマが、今では個人プロジェクトや求職において Haskell を第一の選択肢に置かなくなった
- Haskell の魅力はモナドや DSL そのものよりも、代数的なコード推論と強い型システムがもたらす安全なリファクタリング、不変条件の表現にあった
- 離れるようになった理由は、高度な型機能を好む文化、洗練されていないツール、GHC と標準ライブラリの継続的な非互換変更が積み重なったためである
- それでも高階関数、
do記法、レンズ、宣言的ライブラリ、高度な型抽象化は、他の言語では簡単に置き換えられない長所として残っている - Haskell はより良いプログラマになるために学ぶ価値は大きいが、組織の標準言語にするならツール品質と言語安定性のコストを受け入れる必要がある
Haskellを標準言語として使っていた時期と現在の立ち位置
- 約10年間、Haskell は新規プロジェクトの標準言語だった
- 2009年ごろに Haskell に触れてから、2019年ごろに Ruby と C++ を主に使う職場へ移るまで、自分を Haskell プログラマだと認識していた
- コマンドラインツール、Web サービス、グラフィックアプリケーション、小さなスクリプトなど、さまざまな用途で Haskell を使っていた
- 今では Haskell を完全に拒絶しているわけではないが、個人プロジェクトでも標準言語ではなく、以前のように「Haskell の職場」をわざわざ探すこともない
- この判断は Haskell が悪いという結論ではなく、個人的に Haskell から離れた理由に近い
Haskellに惹かれた理由
-
代数的にコードを扱える性質
- Haskell の最大の魅力は、コードを記号的・代数的に推論できる点だった
- たとえば
double n = n + nという関数があるとき、double xをx + xに置き換える変換は Haskell では常に正しい - 多くの命令型言語では、
double(i++)と(i++) + (i++)が異なる動作をすることがある- 前者は
iを1回増やす - 後者は
iを2回増やす
- 前者は
- この性質のおかげで、小さな機械的変換を積み重ねて大きなリファクタリングをより安全に進められる
- Ruby のコードベースでの大規模なリファクタリングは非常に不安になり得るが、Haskell のコードベースでははるかに容易な場合がある
-
厳格だが過度に騒がしくない型システム
- Haskell の型システムは厳格でありながら、型推論のおかげで Java のように型注釈が過剰に多くならない
- 型クラスは、型を意識しなければならない場面でも煩雑さを減らしてくれる
- 良い Haskell コードは「壊れやすい(brittle)」性質を持つ
- 誤った抽象化が静かに歪んで誤ったプログラムを許してしまうことがない
- 問題があればコンパイルに失敗する
NonEmpty型は、少なくとも1つの要素を持つリストを表すmapのような長さを保持する操作はNonEmptyを返すappendのような長さを増やす操作もNonEmptyを返すfilterのように結果が空になり得る操作は通常のリストを返す
- ファイルパスのリストが
NonEmptyでなければならない関数には、コマンドライン引数をそのまま渡せず、まず空でないことを確認しなければならない - 後から拡張子フィルタの段階を追加すると結果が空になり得るため再確認が必要になるが、空リストを誤って渡す経路はコンパイラが防いでくれる
-
型で不変条件を保つやり方
- Haskell を長く使うと、プログラムの不変条件を型で表し、コンパイラに検証させるやり方に慣れていく
- これは複雑な型レベル計算だけを意味するものではない
- SQL エスケープの有無を
RawStringとSanitizedStringのようなラッパー型で区別できる- Web API が
RawStringを返しても、データベース層はSanitizedStringだけを受け取るようにできる
- Web API が
Maybeフィールドが多い表面的な表現を、必要な情報の存在が保証された内部表現に変換できる- 独立してテストできる程度にだけジェネリックにするというやり方も可能である
-
DSLと高階関数が自然な言語
- Haskell はマクロなしでも簡潔なドメイン固有言語を作りやすい
- config-ini は INI 設定ファイルを扱うライブラリの例である
- 命令型のパースインターフェースではなく宣言的インターフェースを使う
- 設定型の特定部分を INI ファイル構造の特定部分に対応付ける
- 同じインターフェースで読み込み、書き込み、差分最小更新を行う
- Ruby の DSL は動的メタプログラミングやグローバルな monkeypatching に依存し得るが、Haskell の DSL は柔軟な構文やモナドのオーバーロードのような機能によって、特定のファイルやコード領域に閉じ込めやすい
- Haskell は高階関数の扱いも自然にする
(+)は2つの数値引数を取って足す関数としてそのまま使えるpairwiseProduct = zipWith (*)のように、2つのリストの要素ごとの積を非常に短く表現できる
- Haskell は、Perl が文字列を扱わせてくれるように関数を扱わせてくれる言語だ、というたとえがふさわしい
Haskellから離れた3つの要因
-
難解なスタイルを好む空気
- 1つ目の要因は、スタイル上の新奇性への嗜好である
- Haskell コミュニティは新しい抽象化を継続的に実験し構築している
- 抽象代数学や圏論から持ち込まれたアプローチ
- より多くの計算を型レベルへ持ち上げて誤った状態を制限するアプローチ
- Servant の型レベル Web API や comonad でメニューシステムを表現した事例 は、問題領域を新しい方法で表現しようとする実験である
- しかし日常的なコードでは、こうした実験的アプローチが、最初に見える利点以上の問題を生むことがある
- あるコンパイラプロジェクトの例では、AST ノードが式の型を型パラメータとして持っていた
- 型エラーのある構文木を作ると、コンパイラ自体がコンパイルできなくなる
- 当初はいくつかのバグを捕まえられた
- 多くの最適化パスが型規律に合わないとき、キャストや GHC をなだめるための工夫でコードが複雑になった
- 最終的には AST から型パラメータを取り除いた
- 「fancy types」アプローチを試したプロジェクトのうち約4分の3では、十分な価値はなかったと評価している
- Simple Haskell の流れは、実験的機能を避け最小限の拡張にとどめるときに、この言語の長所を最も多く得られるという考えに基づいている
- より複雑な型機能はコミュニティやライブラリに広く浸透している
- 複雑な型抽象化は変更や保守を難しくし得るし、単純化した抽象化を最新の型レベル抽象化に戻そうとする試みが、コードサイズの増加、性能低下、可読性低下につながった経験もある
- ただし Trees that Grow のように、コンパイラ段階ごとにインデックス付けされたデータ構造は、有用だった「fancy type-level feature」の例として残っている
-
日常作業で輝けなかったツール
- 2つ目の要因は、ぎこちないツール体系である
- Haskell には、標準に近いビルドシステムや開発ツールが複数ある
- これらのツールは概ね必要な仕事はしてくれるが、「愛したくなる」水準ではなかった
ormoluのように最小限の手間で役目を果たすツールもあったが、一部のツールでは継続的なバグや回避策が必要だった- Haskell のツールは10年間で改善したが、2019年時点では Rust Cargo の使い勝手や安定性に近いものはなかった
- 新しくてより良いものを作り続けようとする Not-Invented-Here 的傾向も問題だった
- パッケージのテキストフォーマットまで hpack で作り直そうとする流れがある
- こうした試みが何年も95%完成の状態にとどまることに苛立ちを覚える
- Haskell のツールがひどいと言いたいわけではないが、「そこそこ良い」では高まった基準を満たせない
-
続き続ける非互換な変更
- 3つ目の要因は、Haskell 言語と GHC が経験している継続的な変化である
- GHC は事実上、唯一合理的な公開 Haskell 実装として扱われている
- Haskell は、過去の誤りや不整合を、標準ライブラリや言語の基本部分においても段階的に修正しようとしている
- こうした変化は、言語をよりクリーンで一貫した基盤へ導き得るが、利用者には継続的に小さな摩擦を生む
- 2015年の Foldable-Traversable Prelude changes は、そのような変化を示す例である
- 古い Haskell プロジェクトは、外部依存がなくても最新の GHC でビルドできないことがある
- superclass の追加
- import の削除
- あいまいになった多相性のための型シグネチャ追加
- 修正はたいてい小さいが積み重なり続け、以前と少しずつ違う部分が利用者の注意を要求する
- Rust の epochs 更新方式 は、安定機能に対する後方互換契約を明示し、意図的な breaking change を epoch の後ろに隠す方式として対照的である
それでも恋しく思うHaskellの長所
- Haskell の代数的なコード記述とリファクタリングは、今でも強みである
- Rust も Haskell と多くの長所を共有しているが、小さな機械的リファクタリングがプログラムの意味を保つという確信は、Haskell ほど容易には得られない
- 型システムも、他の言語が通常は持たない機能を提供する
- 2023年の主流言語は、2009年より静的型付け機能が大きく進歩している
- それでも higher-kinded types 上の抽象化のような機能は Haskell の差別化要因である
- Haskell のライブラリエコシステムは入り混じっている
- 半ば保守されたオープンソースライブラリ
- 個々の作者が自分流に世界を再構成しようとする小さな王国
- 必要だが互いに異なる文字列類似型たち
- それでも Haskell には、他の言語では半分くらいしか実現できない優れたライブラリや抽象化がある
- レンズは、十分に使ってみないと良さが見えにくい強力な抽象化である
- Brick は宣言的なブロックで非常に優れたターミナル UI ライブラリを提供する
- diagrams と music-suite も宣言的 API の好例である
do記法も大きく恋しく思う機能である- 重要なのは
do自体より、束縛を追加してもインデントの深さが増え続けない記法である - Ruby のネストした
flat_mapは軸が増えるたびにインデントが深くなる - Haskell の
do記法は同じ組み合わせを平坦に表現できる
- 重要なのは
do記法は、async/awaitスタイルの構造化並行性、同時トランザクション、確率的プログラム を同じ軽量な構文で埋め込めるようにする
Haskellを学ぶべきか、使うべきか
- 「この言語を学ぶべきか、使うべきか」という問いへの答えは、目標によって異なる
- より良いプログラマになることが目標なら、Haskell は学ぶ価値が大きい
- Haskell は興味深く強力な言語である
- Haskell を学んだ経験はプログラミング能力を高めたと評価している
- 実用が目標なら、より慎重であるべきだ
- Haskell の長所は実際に存在する
- 個人や組織が前述の摩擦をどれだけ受け入れられるかと比べる必要がある
- Haskell から十分な利益を得て、小さな不便を小さな傷の水準にとどめられる個人や組織も存在する
- 明確なイエス・ノーを求めるなら、答えは「Haskell を使わないほうがよい」に近い
- エンジニアリング組織を作るなら、Haskell を標準言語として据えないという判断である
- 良いツールの価値と悪いツールのコストをどちらも知っているからである
- 安定した言語、明確なコードエコシステム、優れたツールをすぐ提供できる言語を好む
- それでも Haskell は、プログラムの安全性と正確性のための優れた道具を提供する
- 特定の文脈では、悪いツールや不要な type family を防ぐ linting を受け入れる価値があるかもしれない
- Haskell はもはや個人的に合う言語ではないが、Haskell がこの世界に存在しなかったなら、世界はもっと悪かっただろうという結論に至る
1件のコメント
Hacker News の意見
Haskell を10年ほど使って、最近は生計を立てる手段としては離れたが、著者の最初の指摘には確かに同意する: Haskell コミュニティは学びを非常に重視する
好奇心旺盛な人たちと働き、常に教えることも学ぶこともあるのはよいが、試したアイデアを捨てるのは苦手な方なので、プロフェッショナルな Haskell コードベースは厳格に管理しないと、「できるけれど使わない方がよいもの」がたまりやすい
ただし ツールチェーンについては意見が違う。Haskell のツールがいまいちなのは事実だが、Python、JS、Java、Rust、Elm を使ってみると、たいていのツールもいまいちで、むしろ Haskell のツールチェーンが恋しくなった。cargo は素晴らしいが例外に近く、Rust が古くなった後もこの状態を避けられるのかは気になる
たまに作業するプロジェクトがあるが、次に開いたらまた動かないだろうと100%確信している。Python の依存関係・仮想環境管理も同じくらいひどいが、Poetry はかなりよく、Ruby は chruby/rbenv/rvm のどれを使っても同じようにうまく動き、Bundler もきちんと回る。Java は欠点があっても IDE 体験は最高水準で、リファクタリングは Haskell ほど安全ではなくても非常に簡単にできる
Bundler はおおむね悪くなく、同意できない設計上の選択はあっても、正しい道をたどれば多くの部分がうまく噛み合う。言語ツールも十分に壊れにくい形で作ることができるし、cargo を直接それほど多く使ったわけではないが、本当に同クラス最高なら、その状態を維持しても驚かない
ツール面で Haskell には、知る限り他の言語が競争しにくいものが1つある: Hoogle
Hoogle は驚くほど優れている。Haskell でやりたいことを伝えると、Haskell でできることを教えてくれる。Rust でも似た試みがあり、Nim 用の Noogle も作ろうとしたが、「関数に引数を渡すこと」と「関数を呼び出すこと」が明確に分かれる言語では、同じようには機能しない
Result、Boxが絡む Rust コードを見るたびに、Hoogle と同じくらい有用な Roogle があればと思う。それ以外の Haskell ツールには長所がない。かなりの部分は、既存のものを改善するより「革新的な」何かを作ろうとするコミュニティの本能のせいに見える完璧ではないが、5年前と比べればずっとましだ
もっと多くのプロジェクトがこういう方向を取ってくれるとよい
最大の問題は、GHCが標準ライブラリである特定の
baseバージョンに結び付いている点だと思うbaseが変わり、新しい GHC バージョンがそのバージョンだけをサポートすると、より新しい GHC を使うために事実上すべての依存関係を変えなければならない。GHC 9.6 でコンパイルしたコードがなぜbase4.18.0.0 を使わなければならないのか、まだ理解できていない。GHC バイナリがコンパイル対象コードのData.Listモジュールのバージョンをなぜ気にする必要があるのかbaseが公開する GHC 専用要素が特定の GHC バージョンに紐づくのは理解できるが、残りまでそうであるべきかは分からない。ただ、これを直すためにbaseを複数のパッケージに分割する作業が進行中だと理解している: https://gitlab.haskell.org/ghc/ghc-wiki-mirror/-/blob/master...baseコードを無効だとして拒否する可能性があるbase実装にリンクされたライブラリ同士がData.Listのインスタンスを受け渡しするなら、問題が起きるたとえば
Data.Listのリストがあるとき、異なる2つのライブラリ関数の結果をそのリストに連結できるだろうかbaseは誰もが望む目標だ。現状は歴史的理由と、コンパイラ内部に結び付いた基本要素が非基本要素と結合していることによるところが大きく、十分な労力をかければ改善できるHaskellへの関心が徐々に冷めた大きな理由が3つあるとすれば、難解なコードを称賛して保守を難しくするスタイル上の新しもの好み、日常作業をより鈍重にするぎこちないツール、たまにではあるが継続的に気を配る必要があり、定期的に破壊的変更をもたらす絶え間ない変化だった
妥当な指摘。2010〜2012年にHaskellをかなり学んだが、言語自体は素晴らしい一方で、ドキュメントとツールは扱いにくかった。コミュニティはCabalと悪名高いCabal hellからStackへ移り、その後またCabalへ戻り、全体としては良くなった
一方で、他のプログラミング言語も関数型プログラミングの要素を取り込んだ。たとえばJavaはStreams、関数、ラムダ、代数的データ型、records、パターンマッチングを追加し、構文はHaskellほど優雅でなくても、関数型プログラミングの基本概念は入っている
そうして自ら終わりなき苦痛の穴を作り出したが、窓の外の木に光が作る色を見られるほど光線が低くはないため、かろうじて耐えられるものになった。外から来た者たちは、自ら作った闇に適応した方言と、光の中では繁栄できるがそこでは窒息する方言との間で引き裂かれ、急速に正気を失う
コミュニティがまたCabalへ戻ったのなら、何を見落としているのか気になる
こうした理由から**F#**へ移り、あまり振り返らなくなった。ときどき高階カインド型が恋しくなることはあるが、F#にもジェネリクスはあり、正直に言うとHaskellよりも単純なコードを書くよう強制される面があって、たいていその結果のほうが良い
ただしF#はHaskellのような「達人っぽさ」は与えてくれない。Haskellのようでありながら、徹底して実務的だ。F#で多くのことを片付け、楽しく使っているが、たまにHaskellとはなぜうまくいかなかったのかと、ほのかに振り返ってしまう
Haskellを使わないと何かを逃しているのではないかとよく気になっていたが、自分の目的にはおそらくそうではなさそうだ
Haskellを学ぶのは異星人の言語を解読しようとするようなものだ。普通のif-elseループや単純な変数代入に慣れているなら、頭がこんがらがる覚悟をしたほうがいい
monadは宇宙怪物ではなく奇妙な抽象概念だが、頭をかきながら人生の選択を疑わせることがある。ライブラリも、必要なものを探すには半分空っぽの中古品店をあさるような感じで、ドキュメントも半分は象形文字のように読める
性能も現実には遅延評価のせいで予想以上にメモリを食い、なぜ遅くなるのか長々と悩むことがある。仕事もHaskellに全振りした非常にニッチな会社でない限り見つけにくく、デバッグは関数型の象形文字の山から針を探すように感じられる
Scalaを使ってきたが、2つの世界の長所を与えてくれると思う。代数的に推論でき、リファクタリング後にコンパイルできればたいてい動く
型推論とmonadもうまく機能し、Javaエコシステムの利点も得られる。難解になりすぎる場合は、ルールを破ってJavaに近い書き方もできる。他の人がどう考えているのか気になる
ただしsbtは気が狂いそうで、開発体験の中でおそらく一番気に入らない部分だ
Haskellの統括主体とメンテナーたちは、実際にこの言語が研究、実験、学術的探究に向いたものであることを意図しているように見える
それ自体は問題ないが、どこかの時点で、Haskellを実用的で安定した道具として使おうとするプログラマーの利害と衝突せざるを得ない。必要なのは、実験的で最先端のものを別トラックとして示せる能力だと思う。pragmaであれパッケージレベルの宣言であれ、パッケージを見ただけで取り込むべきか分かるようにする必要がある
そうすれば実務志向の開発者が「私たちはstable Haskellだけを使う」といった方針を採用できる。Haskellの採用が高まりすぎると、言語が慣性に閉じ込められ、最先端の概念を押し進められなくなるという「Avoid success at all costs」の力学は認めつつも、日常的な開発者がより制限され安定したHaskellの部分集合にとどまれる公式トラックが必要だ
Javaは絶対にこういうものを得られないだろう。https://www.stackage.org/
単純さは結局、見る人次第だ。華やかな型機能も、その動作を内面化していれば型をより表現力豊かにし、コードをより安全に、さらにはより単純にする使い道がある
かなり良い記事。ただ弱い点は、望んでいないのに特定の新しく派手な型機能を使えと押し付けられるような圧力を感じたことがない、というところ。
Servant の型レベル HTTP API が嫌なら、一段下がって warp を使えばいい。ライブラリは、過度に複雑な型が落とし穴になり得ることを理解しているので、しばしばこのように階層化されている。どこまで行くべきかについての直感は必要で、その過程には失敗も含まれる。
Servant のドキュメントは読んでみたが、当然ながら、自分が持っている以上の Haskell の理解を前提としていた。
この記事は本当に刺さった。1年半ほど、かなり産業寄りのアプリケーションで Haskell を使っていて、その前にも https://github.com/mattgreen/hython のような経験がそれなりにあった。
20年のキャリアの中で久しぶりにかなり多くのことを学ばなければならず、それはドメインと言語が組み合わさった結果だ。刺激的でもあり消耗もするし、忙しい家庭生活と一緒に抱えるにはかなり大きな塊かもしれない。
それでも言語は優れている。マネージャーはおおむね top-down 方式で書くことを勧めていて、それは Haskell の柔軟性が開発意図をよく伝えられるため、どう読まれるべきかを深く考えてそこから始めろ、という意味だ。多くの言語で
functionを何度も打ち込んで脳が省電力モードに入るのとは大きな発想の転換で、本当に書きやすいよう設計されている感じがする。しかし、制御されたエフェクトを monad で扱っても、構文はいまだに流し読みしにくい。1行があまりに密になり得るせいなのかもしれない。早期リターンがものすごく恋しいし、Rust の優れた LSP も恋しい。Haskell は 9.x の内部変更のせいで、パターンマッチを埋めてくれる機能も最近失ったし、monad を積むのはいまだに嫌で、よく間違える。コンパイル時間は、特に lens ライブラリに出くわすと過酷になり得る。
デフォルトの遅延評価も好きではない。コミュニティも、Haskell プログラムはこのデフォルトのせいで空間リークにずっと弱く、多くのプログラムでかなり長い間検出されないことがあり得る、とある程度認めている。システムプログラマーとしては悲鳴を上げたくなる。
それでもコミュニティは、私が見た中で最も強いプログラマー集団の一つだ。ミームのように「頭がいい」という話を並べたいわけではなく、本当に深く考え、前へ押し進めるところが大きい。私たちのドメインであるプログラム解析には Haskell が手袋のようにぴったり合うが、それ以外の領域ではいまだに少し慎重だ。本質的には OCaml だが構文がより良いプログラミング言語への渇きがまだある。
インターフェースに ExceptT を露出させる必要もなく、内部で早期リターン用にだけ使って、関数は Either を返すようにすればいい。
結局、大きな traction を得られなかったのは残念だ。
OCaml よりオブジェクト指向と関数型プログラミングの統合が優れていて、慣用的な OCaml コードは単純な変換で移しやすいほど似ている。