web-dev-qa-db-ja.com

FreeモナドとReactive Extensionsはどのように関連していますか?

私は、LINQがRx.NETに進化したC#のバックグラウンドを持っていますが、常にFPに関心を持っていました。 F#でモナドといくつかのサイドプロジェクトを紹介した後、次のレベルに進む準備ができました。

さて、Scalaの人々からの無料のモナドに関するいくつかの話と、HaskellまたはF#での複数の書き込みの後、私は理解のためにインタープリターがオンになっている文法がIObservableチェーンに非常に似ていることを発見しました。

FRPでは、チェーン内にとどまる副作用や障害を含む小さなドメイン固有のチャンクから操作定義を作成し、一連の操作と副作用としてアプリケーションをモデル化します。無料のモナドでは、私が正しく理解していれば、ファンクタとして操作を行い、coyonedaを使用してそれらを持ち上げることによって同じことを行います。

いずれかのアプローチに向かって針を傾ける両方の違いは何ですか?サービスまたはプログラムを定義する際の根本的な違いは何ですか?

14

モナド

モナド

  • endofunctor 。私たちのソフトウェアエンジニアリングの世界では、これは単一の無制限の型パラメーターを持つデータ型に対応すると言えます。 C#では、これは次のような形式になります。

    class M<T> { ... }
    
  • そのデータ型に対して定義された2つの操作:

    • return/pureは「純粋な」値(つまり、T値)を取り、それをモナドに「ラップ」します(つまり、M<T>を生成します値)。 returnはC#の予約済みキーワードなので、今後はpureを使用してこの操作を参照します。 C#では、pureは次のようなシグネチャを持つメソッドになります。

      M<T> pure(T v);
      
    • bind/flatmapはモナド値(M<A>)と関数fを受け取ります。 fは純粋な値を取り、モナド値(M<B>)を返します。これらから、bindは新しいモナド値(M<B>)を生成します。 bindには次のC#署名があります。

      M<B> bind(M<A> mv, Func<A, M<B>> f);
      

また、モナドになるには、purebindが3つのモナド則に従う必要があります。

C#でモナドをモデル化する1つの方法は、インターフェイスを構築することです。

interface Monad<M> {
  M<T> pure(T v);
  M<B> bind(M<A> mv, Func<A, M<B>> f);
}

(注:物事を簡潔で表現力豊かにするために、この回答ではコードを自由に使っています。)

これで、Monad<M>の具体的な実装を実装することで、具体的なデータタイプのモナドを実装できます。たとえば、次のモナドをIEnumerableに実装します。

class IEnumerableM implements Monad<IEnumerable> {
  IEnumerable<T> pure(T v) {
    return (new List<T>(){v}).AsReadOnly();
  }

  IEnumerable<B> bind(IEnumerable<A> mv, Func<A, IEnumerable<B>> f) {
    ;; equivalent to mv.SelectMany(f)
    return (from a in mv
            from b in f(a)
            select b);
  }
}

(私は意図的にLINQ構文を使用して、LINQ構文とモナドの関係を呼び出しています。ただし、LINQクエリをSelectManyへの呼び出しに置き換えることができます。)

さて、IObservableのモナドを定義できますか?それはそう思われるでしょう:

class IObservableM implements Monad<IObservable> {
  IObservable<T> pure(T v){
    Observable.Return(v);
  }

  IObservable<B> bind(IObservable<A> mv, Func<A, IObservable<B>> f){
    mv.SelectMany(f);
  }
}

モナドがあることを確認するには、モナドの法則を証明する必要があります。これは簡単なことではありません(そして、Rx.NETを十分に理解していないので、仕様だけで証明できるかどうかもわかりません)が、これは有望なスタートです。この議論の残りの部分を容易にするために、この場合モナドの法則が成り立つと仮定しましょう。

無料モナド

単一の「無料モナド」はありません。むしろ、フリーモナドはファンクタから構築されるモナドのクラスです。つまり、ファンクタFを指定すると、Fのモナドを自動的に導出できます(つまり、Fのフリーモナド)。

ファンクター

モナドと同様に、ファンクタは次の3つの項目で定義できます。

  • 単一の無制限の型変数でパラメーター化されたデータ型。
  • 2つの操作:

    • pureは、純粋な値をファンクターにラップします。これはモナドのpureに似ています。実際、モナドでもあるファンクタの場合、2つは同一でなければなりません。
    • fmapは、指定された関数を介して、入力の値を出力の新しい値にマップします。その署名は次のとおりです。

      F<B> fmap(Func<A, B> f, F<A> fv)
      

モナドと同様に、ファンクタはファンクタの法則に従う必要があります。

モナドと同様に、次のインターフェイスを介してファンクタをモデル化できます。

interface Functor<F> {
  F<T> pure(T v);
  F<B> fmap(Func<A, B> f, F<A> fv);
}

さて、モナドはファンクタのサブクラスなので、Monadを少しリファクタリングすることもできます:

interface Monad<M> extends Functor<M> {
  M<T> join(M<M<T>> mmv) {
    Func<T, T> identity = (x => x);
    return mmv.bind(x => x); // identity function
  }

  M<B> bind(M<A> mv, Func<A, M<B>> f) {
    join(fmap(f, mv));
  }
}

ここでは、メソッドjoinを追加し、joinbindの両方のデフォルト実装を提供しました。ただし、これらは循環的な定義であることに注意してください。したがって、少なくともいずれかをオーバーライドする必要があります。また、pureFunctorから継承されるようになりました。

IObservableと無料モナド

ここで、IObservableのモナドを定義し、モナドはファンクタのサブクラスであるため、IObservableのファンクタインスタンスを定義できる必要があります。これが1つの定義です。

class IObservableF implements Functor<IObservable> {
  IObservable<T> pure(T v) {
    return Observable.Return(v);
  }

  IObservable<B> fmap(Func<A, B> f, IObservable<A> fv){
    return fv.Select(f);
  }
}

IObservableに定義されたファンクタがあるので、そのファンクタからフリーモナドを構築できます。そして、それがIObservableとフリーモナドの関係です。つまり、IObservableからフリーモナドを構築できます。

6
Nathan Davis