54

私のビジネス分野 (金融機関のバック オフィス IT) では、ソフトウェア コンポーネントがグローバルな構成を保持し、その進行状況をログに記録し、ある種のエラー処理や計算の短絡を行うことは非常に一般的です。 Haskell の Reader、Writer、Maybe モナドなどでうまくモデル化でき、モナド変換子と一緒に構成できます。

しかし、いくつかの欠点があるようです: モナド変換子の背後にある概念は非常にトリッキーで理解しにくく、モナド変換子は非常に複雑な型シグネチャにつながり、パフォーマンスにいくらかのペナルティを課します。

だから私は疑問に思っています:上記の一般的なタスクを扱うとき、モナド変換子はベストプラクティスですか?

4

6 に答える 6

44

Haskell コミュニティは、この問題に関して分裂しています。

  • John Hughes は、モナドを教えるよりもモナド変換子を教える方が簡単であり、彼の学生は「変換子を最初に」アプローチする方がうまくいくと報告しています。

  • GHC 開発者は一般にモナド変換子を避け、必要なすべての機能を統合する独自のモナドをロールアップすることを好みます。(ちょうど今日、GHC は私が3 日前に定義したモナド変換子を使用しないとはっきりと言われた。 )

私にとって、モナド変換子はポイントフリー プログラミング (つまり、名前付き変数を使用しないプログラミング) によく似ています。結局のところ、それらは型レベルで正確にポイントフリーでプログラミングされています。ときどき名前を紹介できると便利なので、ポイントフリーのプログラミングは決して好きではありません。

私が実際に観察しているのは

  • Hackage で利用できるモナド変換子の数は非常に多く、それらのほとんどは非常に単純です。これは、独自のインスタンスを作成するよりも大きなライブラリを学習する方が難しいという問題の典型的な例です。

  • Writer、State、Environment などのモナドは非常に単純なので、モナド トランスフォーマーのメリットはあまりないと思います。

  • モナド変換子が輝くところは、モジュール性と再利用です。このプロパティは、Liang、Hudak、Jones の画期的な論文「Monad Transformers and Modular Interpreters」で美しく実証されています。

上記の一般的なタスクを扱う場合、モナド変換子はベストプラクティスですか?

私はそうは言いません。 モナド変換子ベスト プラクティスとなるのは、さまざまな方法でモナド変換子を構成して再利用することで作成できる、関連する抽象化の製品ラインがある場合です。このような場合、問題領域にとって重要な (GHC で拒否されたもののような) モナド変換子をいくつか開発し、(a) 複数の方法でそれらを構成します。(b) ほとんどの変圧器でかなりの量の再利用を達成する。(c) 各モナド変換子に自明でない何かをカプセル化している。

GHC で拒否された私のモナド変換子は、上記の基準 (a)/(b)/(c) のいずれも満たしていませんでした。

于 2010-05-03T19:40:28.323 に答える
8

モナド変換子の背後にある概念は非常にトリッキーで理解しにくいものです。モナド変換子は非常に複雑な型アノテーションにつながります

これは少し誇張だと思います。

  • トランスフォーマーの特定のモナドスタックを使用することは、プレーンなモナドよりも使用が難しくありません。レイヤー\スタックについて考えるだけで大​​丈夫です。ほとんどの場合、純粋関数(または特定のIOアクション)を2回以上持ち上げる必要はありません。
  • すでに述べたように、モナドスタックをニュータイプで非表示にし、一般化された派生を使用して、モジュールでデータコンストラクターを非表示にします。
  • 関数型シグネチャで特定のMonadスタックを使用しないようにし、MonadIO、MonadReader、MonadStateなどのMonad型クラスで一般的なコードを記述します(Haskell 2010で標準化されている柔軟なコンテキスト拡張を使用します)。
  • fclabelsなどのライブラリを使用して、モナドのレコードの一部にアクセスするボイラープレートアクションを減らします。

モナド変換子はあなたの唯一のオプションではありません。カスタムモナドを作成し、継続モナドを使用することができます。IO(グローバル)、ST(ローカルおよび制御、IOアクションなし)、MVar(同期)、TVar(トランザクション)に可変の参照/配列があります。

モナド変換子の潜在的な効率の問題は、mtl/transformersライブラリのソースにバインド/リターンするインラインプラグマを追加するだけで軽減できると聞いています。

于 2010-05-03T19:21:52.723 に答える
3

モナドを学んでいた頃、私は StateT ContT IO のスタックを使用して離散イベント シミュレーション ライブラリを作成するアプリケーションを構築しました。継続はモナド スレッドを格納するために使用され、StateT は実行可能なスレッド キューを保持し、その他のキューは中断されたスレッドがさまざまなイベントを待機するために使用されました。それはかなりうまくいきました。newtype ラッパーの Monad インスタンスの書き方がわからなかったので、それを型のシノニムにしたところ、うまくいきました。

最近では、おそらく自分のモナドをゼロからロールしたでしょう。しかし、これを行うたびに、「All About Monads」と MTL のソースを見て、バインド操作がどのように見えるかを思い出します。カスタムモナドです。

于 2010-05-04T20:35:50.610 に答える
3

私は最近、F# のコンテキストでのモナド構成に "陥りました"。私は状態モナドに強く依存する DSL を書きました: すべてのコンポーネントは状態モナドに依存しています: パーサー (状態モナドに基づくパーサーモナド)、変数マッチングテーブル (内部型の複数)、識別子ルックアップテーブル。これらのコンポーネントはすべて連携して動作するため、同じ状態モナドに依存しています。したがって、さまざまなローカル状態をまとめる状態構成の概念と、各アルゴリズムに独自の状態の可視性を与える状態アクセサーの概念があります。

当初、設計は実際には「1 つの大きな状態モナド」でした。しかし、その後、ローカルのライフタイムのみを持つ状態が必要になり始めましたが、それでも「永続的な」状態のコンテキスト内にあります (繰り返しになりますが、これらの状態はすべて状態モナドによって管理されます)。そのためには、状態を拡張し、状態モナドを一緒に適応させる状態モナド変換子を導入する必要がありました。状態モナドと継続状態モナドの間を自由に移動するためのトランスフォーマーも追加しましたが、わざわざ使用することはありませんでした。

したがって、質問に答えるには: はい、モナド変換子は「野生」に存在します。それでも、私はそれらを「箱から出して」使用することに強く反対します。モジュール間の小さな手作りのブリッジを使用して、シンプルなビルディング ブロックでアプリケーションを作成します。そこから始めないでください。

型シグネチャについて: このタイプのプログラミングは、目隠しチェスをするのと非常によく似ていると考えるようになりました (そして、私はチェス プレーヤーではありません): 関数を「見る」レベルのスキルが必要です。と種類が一致します。型シグネチャは、安全上の理由から明示的に型制約を追加する必要がない限り (または、F# レコードなどで型制約を与えるようにコンパイラが強制する場合を除いて)、ほとんどの場合気を散らすものになります。

于 2010-12-07T15:56:12.663 に答える
2

ログや設定のようにかなりグローバルになりがちなものは、IO モナドに入れることをお勧めしますか? (確かに非常に限られたセットの) 例を見ると、Haskell コードは純粋な (つまり、まったくモナドではない) か、IO モナドのどちらかである傾向があると思います。それともこれは誤解ですか?

これは誤解だと思います.IOモナドだけが純粋ではありません. Write/T/Reader/T/State/T/ST モナドのようなモナドは純粋に機能します。このまったく役に立たない例のように、これらのモナドのいずれかを内部的に使用する純粋な関数を書くことができます:

foo :: Int -> Int
foo seed = flip execState seed $ do
    modify $ (+) 3
    modify $ (+) 4
    modify $ (-) 2

これが行っているのは、状態を暗黙的にスレッド化/配管することだけです。手動で明示的に行うことは、ここでの do 表記は、命令的に見えるようにするための優れた構文糖衣を提供するだけです。ここでは IO アクションを実行できません。外部関数を呼び出すこともできません。STモナドを使用すると、純粋な関数インターフェイスを持ちながら、ローカルスコープで実際の可変参照を使用できます。そこではIOアクションを実行できず、純粋に機能します。

いくつかの IO アクションを回避することはできませんが、すべてを IO にフォールバックしたくはありません。なぜなら、そこには何でも行くことができ、ミサイルが発射される可能性があり、制御できないからです。Haskell には、さまざまな程度の安全性/純度で効果的な計算を制御するための抽象化があります。IO モナドは最後の手段にする必要があります (ただし、完全に回避することはできません)。

あなたの例では、モナドトランスフォーマーまたはトランスフォーマーでそれらを構成するのと同じことを行うカスタムメイドのモナドの使用に固執する必要があると思います。私はカスタム モナドを (まだ) 書いたことはありませんが、モナド トランスフォーマーをかなり使用しています (仕事ではなく、私自身のコードです)。 .

モナド変換子を使用するReal World Haskellの章を見たことがありますか?

于 2010-05-03T22:36:35.757 に答える
2

これは誤解だと思います.IOモナドだけが純粋ではありません. Write/T/Reader/T/State/T/ST モナドのようなモナドは純粋に機能します。

純粋/非純粋という用語については、複数の概念があるように私には思えます。あなたの定義「IO = unpure、その他すべて = pure」は、Peyton-Jones が「Taming Effects」( http://ulf.wiger.net/weblog/2008/02/29/peyton-jones-taming ) で話している内容に似ています。 -effects-the-next-big-challenge/ )。一方、Real World Haskell (Monad Transformer の章の最終ページ) では、純粋関数とモナド関数を一般的に対比して、両方の世界に異なるライブラリが必要であると主張しています。ところで、IO も純粋であると主張することができます。これは、タイプRealWorld -> (a, RealWorld)の State 関数にカプセル化されている副作用です。結局のところ、Haskell は自分自身を純粋関数型言語と呼んでいます (IO が含まれていると思います :-))。

私の質問は、理論的に何ができるかについてではなく、ソフトウェア エンジニアリングの観点から有用であることが証明されていることについてです。モナドトランスフォーマーは効果のモジュール性 (および一般的な抽象化) を可能にしますが、それはプログラミングが向かうべき方向ですか?

于 2010-05-04T08:05:42.590 に答える