web-dev-qa-db-ja.com

Template Haskellの何が悪いのですか?

Haskellコミュニティでは、テンプレートHaskellが不便な利便性と見なされることが多いようです。これに関して私が観察したことを正確に言葉にするのは難しいですが、これらのいくつかの例を検討してください

私は人々がTemplate Haskellでかなりきちんとしたことをするさまざまなブログ投稿を見てきました。これにより、通常のHaskellでは不可能だったよりきれいな構文と、大幅な定型的な削減が可能になります。それでは、テンプレートHaskellがこのように軽lookedされているのはなぜですか?何が望ましくないのですか?どのような状況でTemplate Haskellを回避する必要があり、なぜですか?

248
Dan Burton

テンプレートHaskellを回避する理由の1つは、全体としてタイプセーフではないため、「Haskellの精神」の多くに反することです。以下に例を示します。

  • ASTコードがどのようなHaskell THを生成するかを制御することはできません。タイプ Exp の値を持つことができますが、それが[Char]または(a -> (forall b . b -> c))などを表す式であるかどうかはわかりません。 THは、関数が特定のタイプの式のみ、または関数宣言のみ、またはデータコンストラクターマッチングパターンのみを生成できることを表現できれば、より信頼性が高くなります。
  • コンパイルしない式を生成できます。存在しない自由変数fooを参照する式を生成しましたか?幸運なことに、実際にコードジェネレーターを使用している場合にのみ、その特定のコードの生成をトリガーする状況でのみ、それを確認できます。単体テストも非常に困難です。

THもまったく危険です。

  • コンパイル時に実行されるコードは、ミサイルの発射やクレジットカードの盗用など、任意のIOを実行できます。 THエクスプロイトを検索するために、これまでにダウンロードしたすべてのcabalパッケージを調べる必要はありません。
  • THは、「モジュールプライベート」関数と定義にアクセスでき、場合によってはカプセル化を完全に破ることができます。

次に、ライブラリ開発者として使用するTH関数を面白くしないいくつかの問題があります。

  • THコードは常に構成可能であるとは限りません。誰かがレンズ用のジェネレーターを作成するとします。そして、ほとんどの場合、そのジェネレーターは、他のTHコードではなく、「エンドユーザー」のみが直接呼び出すことができるように構成されます。たとえば、型コンストラクタのリストを取得して、パラメータとしてレンズを生成します。ユーザーがgenerateLenses [''Foo, ''Bar]と記述するだけでよいのに対し、そのリストをコードで生成するのは難しいです。
  • 開発者は、THコードを作成できることも知らないforM_ [''Foo, ''Bar] generateLensを記述できることをご存知ですか? Qは単なるモナドであるため、通常のすべての関数を使用できます。一部の人々はこれを知らず、そのため、本質的に同じ機能を持つ同じ機能の複数のオーバーロードバージョンを作成し、これらの機能は特定の膨張効果をもたらします。また、ほとんどの人はQモナドでジェネレーターを書く必要がありますが、それはbla :: IO Int; bla = return 3;を書くようなものです。関数に必要以上の「環境」を与えているため、関数のクライアントはその環境をその効果として提供する必要があります。

最後に、TH関数をエンドユーザーとして使用するのを面白くしないものがいくつかあります。

  • 不透明度。 TH関数の型がQ Decである場合、モジュールの最上位で絶対に何でも生成でき、生成されるものをまったく制御できません。
  • 一枚岩。開発者が許可しない限り、TH関数が生成する量を制御することはできません。データベースインターフェースおよびJSONシリアル化インターフェースを生成する関数を見つけた場合、「いいえ、データベースインターフェースのみが必要です。感謝します。独自のJSONインターフェース」
  • 実行時間。 THコードの実行には比較的長い時間がかかります。コードは、ファイルがコンパイルされるたびに新たに解釈され、多くの場合、ロードする必要のある実行中のTHコードには大量のパッケージが必要です。これにより、コンパイル時間が大幅に遅くなります。
169
dflemstr

これは私自身の意見です。

  • 使用するのはいです。 $(fooBar ''Asdf)は見た目が良くありません。確かに表面的ですが、貢献します。

  • 書くのはさらにいです。引用は時々機能しますが、多くの場合、手動でAST接木と配管を行う必要があります。 [〜#〜] api [〜#〜] は大きくて扱いにくいため、気にしないがディスパッチする必要があるケースが常に多くあり、気にするケースは複数の類似した形式で存在しますが、同一ではありません(データと新しいタイプ、レコードスタイルと通常のコンストラクタなど)。書くのは退屈で繰り返しが多く、機械的ではないほど複雑です。 改革提案 は、これの一部に対処します(引用をより広く適用可能にします)。

  • ステージの制限は地獄です。同じモジュールで定義された関数をスプライスできないことは、その小さな部分です。他の結果は、トップレベルのスプライスがある場合、モジュール内のそれ以降のすべてがその前のすべてのスコープから外れます。このプロパティを備えた他の言語(C、C++)では、前方宣言を許可することで機能しますが、Haskellではできません。接続された宣言またはそれらの依存関係と依存関係の間で循環参照が必要な場合、通常はめちゃくちゃです。

  • 規律がありません。これが意味することは、ほとんどの場合、抽象化を表現するとき、その抽象化の背後には何らかの原理または概念があるということです。多くの抽象化の場合、それらの背後にある原理はそのタイプで表現できます。型クラスの場合、インスタンスが従うべき法則を定式化でき、クライアントは想定できます。 GHCの 新しいジェネリック機能 を使用して、任意のデータ型(範囲内)でインスタンス宣言の形式を抽象化すると、「合計型では、このように機能し、製品型では、機能します」そのような」。一方、テンプレートHaskellは単なるマクロです。これは、アイデアのレベルでの抽象化ではなく、ASTのレベルでの抽象化であり、プレーンテキストのレベルでの抽象化よりも優れていますが、控えめなものです。*

  • あなたをGHCに結び付けます。理論的には別のコンパイラで実装できますが、実際にはこれが起こることはないでしょう。 (これは、現時点ではGHCでしか実装されていない可能性があるさまざまな型システム拡張とは対照的ですが、将来、他のコンパイラに採用され、最終的に標準化されることは容易に想像できます。)

  • APIは安定していません。新しい言語機能がGHCに追加され、template-haskellパッケージがそれらをサポートするように更新されると、多くの場合、THデータ型に対する後方互換性のない変更が伴います。 THコードを複数のGHCバージョンと互換性を持たせたい場合は、非常に注意し、CPPを使用する必要があります。

  • ジョブには適切なツールを使用し、それで十分な最小のツールを使用する必要があるという一般原則があり、その類推ではテンプレートHaskellは このようなもの です。それを行う方法があり、それがテンプレートHaskellではない場合、それは一般的に望ましいです。

Template Haskellの利点は、他の方法ではできなかったことができるということです。これは大きな方法です。ほとんどの場合、THが使用されることは、コンパイラ機能として直接実装されている場合にのみ実行できます。 THは、これらのことを可能にし、潜在的なコンパイラ拡張機能をより軽量で再利用可能な方法でプロトタイプ化できるため、両方を持つことが非常に有益です(たとえば、さまざまなレンズパッケージを参照)。

テンプレートHaskellに否定的な感情があると思う理由を要約すると、多くの問題を解決しますが、解決する特定の問題については、その問題を解決するのにより適した、より洗練された、規律のあるソリューションがあるべきだと感じています。ボイラープレートを自動的に生成することによって問題を解決するのではなく、ボイラープレートをhaveする必要をなくすもの.

* CPPは、解決できる問題に対してより優れた電力対重量比を持っていると私はよく感じますが。

EDIT 23-04-14:上記で頻繁に取得しようとしていたもので、最近になって正確に取得したのは、抽象化と重複排除の間に重要な違いがあるということです。適切な抽象化は副作用として重複排除をもたらすことが多く、重複はしばしば不十分な抽象化の明白な兆候ですが、それが価値がある理由ではありません。適切な抽象化は、コードを正しく理解しやすく保守しやすくするものです。重複排除はそれを短くするだけです。テンプレートHaskellは、一般的なマクロと同様、重複排除のためのツールです。

51
glaebhoerl

Dflemstrが提起するいくつかのポイントに対処したいと思います。

Typecheck THを心配することはできません。なぜでしょうか?エラーがあったとしても、それはまだコンパイル時間になります。これが私の議論を強化する場合、これはC++でテンプレートを使用するときに受け取るエラーに精神的に似ています。生成されたコードのきれいな印刷バージョンを取得するため、これらのエラーはC++のエラーよりも理解しやすいと思います.

TH expression/quasi-quoterが非常に高度な処理を行い、トリッキーなコーナーが隠れる場合は、おそらくお勧めできませんか?

私は最近取り組んでいる準クォーターでこの規則をかなり破ります(haskell-src-exts/metaを使用)- https://github.com/mgsloan/quasi-extras/tree/master/examples 。これにより、一般化されたリストの内包表記をスプライスできないなど、いくつかのバグが発生することを知っています。ただし、 http://hackage.haskell.org/trac/ghc/blog/Template%20Haskell%20Proposal のアイデアの一部がコンパイラーに組み込まれる可能性は十分にあると思います。それまでは、HaskellをTHツリーに解析するためのライブラリはほぼ完璧な近似です。

コンパイル速度/依存関係については、「zeroth」パッケージを使用して、生成されたコードをインライン化できます。これは、少なくとも特定のライブラリのユーザーにとっては素晴らしいことですが、ライブラリを編集する場合にはこれ以上のことはできません。 TH依存関係がバイナリを肥大化させたのでしょうか?コンパイルされたコードで参照されていないものはすべて省いたと思います。

Haskellモジュールのコンパイル手順のステージング制限/分割はうまくいきません。

RE Opacity:これは、呼び出すライブラリ関数と同じです。 Data.List.groupByの動作を制御することはできません。バージョン番号が互換性について何かを伝える合理的な「保証」/規約があります。それは、多少異なる変更の問題です。

これは、ゼロを使用することで成果が得られる場所です-既に生成されたファイルのバージョン管理をしているので、生成されたコードの形式がいつ変更されたかを常に知ることができます。ただし、生成された大量のコードの場合、差分を見るのは少し厄介かもしれません。そのため、より良い開発者インターフェイスが便利な場所の1つです。

REモノリシズム:独自のコンパイル時コードを使用して、TH式の結果を確実に後処理できます。トップレベルの宣言タイプでフィルタリングするコードはそれほど多くありません。名前:ヘック、これを一般的に行う関数を書くことを想像できます。準クォーターを修正/非モノリタイズするには、 "QuasiQuoter"でパターンマッチし、使用された変換を抽出するか、古い変換を作成します。

28
mgsloan

この回答は、イリシウスが提起した問題への対応です。

  • 使用するのはいです。 $(fooBar '' Asdf)は見た目が良くありません。確かに表面的ですが、貢献します。

同意する。 Haskellの使い慣れたシンボルパレットを使用して、$()が言語の一部であるように見えるように選択されたように感じます。ただし、それはまさに、マクロスプライシングに使用されるシンボルで/望まない/望まないことです。それらは間違いなく溶け込みすぎており、この化粧品の側面は非常に重要です。私はスプライスの{{}}の外観が好きです。なぜなら、それらは視覚的にまったく異なるからです。

  • 書くのはさらにいです。引用は時々機能しますが、多くの場合、手動でAST接木と配管を行う必要があります。 [API] [1]は大きくて扱いにくいため、気にしないがディスパッチする必要があるケースが常に多くあり、気にするケースは複数の類似したフォームに存在する傾向があります(データは同一ではありません) vs. newtype、レコードスタイルvs.通常のコンストラクタなど)。書くのは退屈で繰り返しが多く、機械的ではないほど複雑です。 [改革提案] [2]は、これの一部に対処します(引用をより広く適用可能にします)。

私もこれに同意しますが、「THの新しい方向性」のコメントの一部が示すように、すぐに使えるAST引用の欠如は重大な欠陥ではありません。このWIPパッケージでは、これらの問題をライブラリ形式で解決しようとしています。 https://github.com/mgsloan/quasi-extras これまでのところ、通常よりも数か所でスプライシングを許可しており、ASTでパターンマッチングを行うことができます。

  • ステージの制限は地獄です。同じモジュールで定義された関数をスプライスできないことは、その小さな部分です。他の結果は、トップレベルのスプライスがある場合、モジュール内のそれ以降のすべてがその前のすべてのスコープから外れます。このプロパティを備えた他の言語(C、C++)では、前方宣言を許可することで機能しますが、Haskellではできません。接続された宣言またはそれらの依存関係と依存関係の間で循環参照が必要な場合、通常はめちゃくちゃです。

以前は循環TH定義が不可能であるという問題に遭遇しました...かなり面倒です。解決策はありますが、いです-生成されたすべての宣言を結合するTH式で循環依存関係に関係するものをラップします。これらの宣言ジェネレーターの1つは、Haskellコードを受け入れる準クォーターになります。

  • 原理的ではありません。これが意味することは、ほとんどの場合、抽象化を表現するとき、その抽象化の背後には何らかの原理または概念があるということです。多くの抽象化の場合、それらの背後にある原理はそのタイプで表現できます。型クラスを定義するとき、インスタンスが従うべき法則を定式化でき、クライアントは想定できます。 GHCの[新しいジェネリック機能] [3]を使用して、任意のデータ型(範囲内)でインスタンス宣言の形式を抽象化すると、「合計型の場合、このように機能し、製品型の場合、 「。しかし、テンプレートHaskellは単なる愚かなマクロです。これは、アイデアのレベルでの抽象化ではなく、ASTのレベルでの抽象化であり、プレーンテキストのレベルでの抽象化よりも優れていますが、控えめなものです。

原則にとらわれないことをするなら、原則にとらわれません。唯一の違いは、抽象化のためのメカニズムを実装したコンパイラを使用すると、抽象化に漏れがないことを確信できることです。おそらく、言語設計の民主化は少し怖いですね! THライブラリの作成者は、ドキュメントを適切に作成し、提供するツールの意味と結果を明確に定義する必要があります。原理的なTHの良い例は、派生パッケージです。 http://hackage.haskell.org/package/derive -DSLを使用して、派生/実際の派生を指定/します。

  • あなたをGHCに結び付けます。理論的には別のコンパイラで実装できますが、実際にはこれが起こることはないでしょう。 (これは、現時点ではGHCでしか実装されていない可能性があるさまざまな型システム拡張とは対照的ですが、将来、他のコンパイラに採用され、最終的に標準化されることは容易に想像できます。)

それはかなり良い点です-TH AP​​Iはかなり大きくて不格好です。再実装するのは難しいようです。ただし、実際にはHaskell ASTを表す問題を切り分ける方法はほんのわずかです。 TH ADTをコピーし、内部のAST表現にコンバーターを書き込むと、そこにかなりの道が開けると思います。これは、(重要ではない)haskell-src-metaを作成する努力と同等です。 TH ASTをきれいに印刷し、コンパイラの内部パーサーを使用することで、簡単に再実装することもできます。

私は間違っているかもしれませんが、THは、実装の観点から見て、コンパイラ拡張の複雑なものとは思いません。これは実際には「シンプルに保つ」ことの利点の1つであり、基本層を理論的に魅力的で静的に検証可能なテンプレートシステムにしないことです。

  • APIは安定していません。新しい言語機能がGHCに追加され、template-haskellパッケージがそれらをサポートするように更新されると、多くの場合、THデータ型に対する後方互換性のない変更が伴います。 THコードを複数のGHCバージョンと互換性を持たせたい場合は、非常に注意し、CPPを使用する必要があります。

これも良い点ですが、いくぶん劇的になりました。最近APIが追加されましたが、広範囲に破損を誘発することはありませんでした。また、前述の優れたASTクォートを使用すると、実際に使用する必要があるAPIを大幅に削減できると思います。個別の関数を必要とする構築/マッチングがなく、代わりにリテラルとして表現される場合、APIのほとんどは消えます。さらに、作成するコードは、Haskellに類似した言語のAST表現により簡単に移植できます。


要約すると、THは強力で半ば無視されたツールだと思います。憎しみが少ないと、より活発なライブラリのエコシステムにつながり、より多くの言語機能プロトタイプの実装を促進できます。 THは強力なツールであり、ほとんど何でも/ do /できることが確認されています。アナーキー!まあ、この力は、あなたがその制限のほとんどを克服し、非常に原則的なメタプログラミングアプローチが可能なシステムを構築することができると私は考えています。 「適切な」実装の設計が次第に明らかになるため、「適切な」実装をシミュレートするにはいハックを使用する価値があります。

私の理想的なバージョンのnirvanaでは、実際には多くの言語がコンパイラーからこれらの多様なライブラリーに移動します。機能がライブラリとして実装されているという事実は、忠実に抽象化する能力に大きな影響を与えません。

定型コードに対する典型的なHaskellの答えは何ですか?抽象化。私たちの好きな抽象化は何ですか?関数と型クラス!

タイプクラスを使用すると、一連のメソッドを定義して、そのクラスで汎用的なあらゆる方法で使用できます。ただし、これ以外に、クラスがボイラープレートを回避する唯一の方法は、「デフォルト定義」を提供することです。さて、ここに原理的でない機能の例があります!

  • 最小限のバインディングセットは宣言可能ではなく、コンパイラでチェック可能です。これにより、相互再帰により不注意な定義が生じ、ボトムが生成される可能性があります。

  • これは非常に便利で強力ですが、孤立インスタンス http://lukepalmer.wordpress.com/2009/01/25/a-world-without-orphans/ により、スーパークラスのデフォルトを指定することはできません=これらにより、数値の階層を適切に修正できます!

  • メソッドのデフォルトのTHのような機能を使用すると、 http://www.haskell.org/haskellwiki/GHC.Generics になります。これは素晴らしいことですが、これらのジェネリックを使用してコードをデバッグした唯一の経験は、ADTに誘導される型のサイズとASTと同じくらい複雑なため、ほとんど不可能です。 https://github.com/mgsloan/th-extra/commit/d7784d95d396eb3abdb409a24360beb03731c88c

    言い換えれば、これはTHが提供する機能の後にありましたが、言語の全領域である構築言語を型システム表現に持ち上げなければなりませんでした。あなたの一般的な問題、複雑な問題ではうまく機能していると思いますが、THハッカーよりもはるかに恐ろしいシンボルの山をもたらす傾向があります。

    THは、出力コードの値レベルのコンパイル時計算を提供しますが、ジェネリックは、コードのパターンマッチング/再帰部分を型システムに強制的に持ち上げます。これにより、いくつかの非常に便利な方法でユーザーが制限されますが、複雑さはそれだけの価値があるとは思いません。

THとLISPのようなメタプログラミングを拒否すると、インスタンスの宣言のような、より柔軟なマクロ拡張ではなく、メソッドのデフォルトのようなものが優先されるようになったと思います。予期せぬ結果につながる可能性のあるものを避ける規律は賢明ですが、Haskellの有能な型システムが他の多くの環境よりも信頼性の高いメタプログラミングを可能にすることを無視してはなりません(生成されたコードをチェックすることによって)。

14
mgsloan

Template Haskellの実際的な問題の1つは、GHCのバイトコードインタープリターが使用可能な場合にのみ機能することです。これは、すべてのアーキテクチャに当てはまるわけではありません。したがって、プログラムでTemplate Haskellを使用する場合、またはテンプレートHaskellを使用するライブラリに依存している場合、ARM、MIPS、S390、またはPowerPC CPUを搭載したマシンでは実行されません。

これは実際に関連しています: git-annex はHaskellで書かれたツールで、ストレージを心配するマシンで実行するのが理にかなっています。個人的に、git-annexを NSLU 2 (32 MBのRAM、266MHz CPUで実行しています。このようなハードウェアでHaskellが正常に動作することをご存知ですか?)Template Haskellを使用する場合、これは不可能です。

(ARMのGHCについての状況は最近改善されており、7.4.2も機能すると思いますが、ポイントはまだ残っています)。

8

なぜTHが悪いのですか?私にとっては、これは次のようになります。

THを自動生成するために使用しようとしていることに気付くほど多くの反復コードを生成する必要がある場合、あなたは間違っています!

考えてみてください。 Haskellの魅力の半分は、その高レベルの設計により、他の言語で記述する必要のある大量の無駄な定型コードを回避できることです。 needコンパイル時のコード生成の場合、基本的には言語またはアプリケーションの設計が失敗したと言っています。そして、プログラマーは失敗することを好みません。

時々、もちろん、それは必要です。ただし、デザインを少し賢くすることで、THを必要としないようにすることができます。

(もう1つは、THは非常に低レベルです。高度な高レベルの設計はありません。GHCの内部実装の詳細の多くが公開されています。 )

6