私は、LINQがRx.NETに進化したC#のバックグラウンドを持っていますが、常にFPに関心を持っていました。 F#でモナドといくつかのサイドプロジェクトを紹介した後、次のレベルに進む準備ができました。
さて、Scalaの人々からの無料のモナドに関するいくつかの話と、HaskellまたはF#での複数の書き込みの後、私は理解のためにインタープリターがオンになっている文法がIObservable
チェーンに非常に似ていることを発見しました。
FRPでは、チェーン内にとどまる副作用や障害を含む小さなドメイン固有のチャンクから操作定義を作成し、一連の操作と副作用としてアプリケーションをモデル化します。無料のモナドでは、私が正しく理解していれば、ファンクタとして操作を行い、coyonedaを使用してそれらを持ち上げることによって同じことを行います。
いずれかのアプローチに向かって針を傾ける両方の違いは何ですか?サービスまたはプログラムを定義する際の根本的な違いは何ですか?
モナド は
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);
また、モナドになるには、pure
とbind
が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
を追加し、join
とbind
の両方のデフォルト実装を提供しました。ただし、これらは循環的な定義であることに注意してください。したがって、少なくともいずれかをオーバーライドする必要があります。また、pure
はFunctor
から継承されるようになりました。
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
からフリーモナドを構築できます。