web-dev-qa-db-ja.com

野生でモナド変換子に遭遇したことはありますか?

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

しかし、いくつかの欠点があるようです。モナド変換子の背後にある概念は非常にトリッキーで理解しにくいものです。モナド変換子は非常に複雑な型の署名につながり、パフォーマンスが低下します。

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

52
martingw

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)のいずれにも適合しませんでした。

43
Norman Ramsey

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

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

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

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

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

8
snk_kid

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

当初、デザインは実際には「1つの大きな州のモナド」でした。しかし、それから私は地元の生活時間だけで州を必要とし始めました、そしてそれでもまだ「永続的な」州の文脈で(そして再び、これらの州はすべて州のモナドによって管理されています)。そのために、状態を拡張し、状態モナドを一緒に適応させる状態モナド変換子を導入する必要がありました。また、状態モナドと継続状態モナドの間を自由に移動するためのトランスフォーマーを追加しましたが、わざわざ使用することはありませんでした。

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

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

3
user519985

モナドを学習していたとき、StateTのスタックを使用してアプリケーションを構築しました。ContTIO)離散イベントシミュレーションライブラリを作成します。継続はモナドスレッドを格納するために使用され、StateTは実行可能なスレッドを保持します。さまざまなイベントを待機する中断されたスレッドに使用されるキューおよびその他のキュー。これは非常にうまく機能しました。新しい型ラッパーのMonadインスタンスを作成する方法がわからなかったため、型の同義語にしただけで、非常にうまく機能しました。

最近は、おそらく自分のモナドを最初から作成していたでしょう。ただし、これを行うたびに、「All About Monads」とMTLのソースを見て、バインド操作がどのように見えるかを思い出させてくれるので、ある意味では、結果が出てもMTLスタックの観点から考えています。カスタムモナドです。

3
Paul Johnson

したがって、ログや構成のようにかなりグローバルになりがちなものについては、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の章 を見たことがありますか?

2
snk_kid

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

純粋/非純粋という用語については、複数の概念があるように思われます。あなたの定義「IO =純粋ではない、他のすべて=純粋」は、Peyton-Jonesが「飼いならし効果」で話していることと同じように聞こえます( 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)。結局のところ、Haskellはそれ自体を純粋に関数型言語と呼んでいます(IOが含まれていると思います:-))。

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

2
martingw