Jak koreluje wolna monada i rozszerzenia reaktywne?

14

Pochodzę z tła C #, gdzie LINQ ewoluował w Rx.NET, ale zawsze interesował się FP. Po krótkim wprowadzeniu do monad i niektórych pobocznych projektach na F # byłem gotów spróbować przejść do następnego poziomu.

Teraz, po kilku rozmowach na temat wolnej monady od ludzi ze Scali i licznych napisaniach w Haskell lub F #, znalazłem gramatyki z tłumaczami dla zrozumienia, które są dość podobne do IObservablełańcuchów.

W FRP tworzysz definicję operacji z mniejszych fragmentów specyficznych dla domeny, w tym skutków ubocznych i awarii, które pozostają w łańcuchu, i modelujesz swoją aplikację jako zestaw operacji i efektów ubocznych. W wolnej monadzie, jeśli dobrze to zrozumiałem, robisz to samo, wykonując operacje jako funktory i podnosząc je za pomocą coyoneda.

Jakie byłyby różnice między tymi, które przechylają igłę w kierunku któregokolwiek z podejść? Jaka jest podstawowa różnica przy definiowaniu usługi lub programu?

MLProgrammer-CiM
źródło
2
Oto ciekawy sposób myślenia o problemie ... FRP można postrzegać jako monadę, nawet jeśli zwykle nie jest tak sformułowane . Większość (choć nie wszystkie) monady są izomorficzne w stosunku do wolnej monady . Ponieważ Contjest to jedyna monada, jaką widziałem, której nie można wyrazić za pomocą wolnej monady, prawdopodobnie można założyć, że FRP może być. Jak prawie wszystko inne .
Jules
2
Według Erika Meijera, projektanta LINQ i Rx.NET, IObservablejest instancją monady kontynuacyjnej.
Jörg W Mittag
1
Nie mam teraz czasu, aby dopracować szczegóły, ale domyślam się, że zarówno rozszerzenia RX, jak i podejście do darmowej monady osiągają bardzo podobne cele, ale mogą mieć nieco inne struktury. Możliwe, że RX Observable same w sobie są monadami, a następnie można zmapować wolne obliczenia monad na te, używając obserwowalnych - to bardzo z grubsza to, co oznacza „wolny” w „wolnej monadzie”. A może związek nie jest tak bezpośredni, a ty tylko zastanawiasz się, w jaki sposób są wykorzystywane do podobnych celów.
Tikhon Jelvis

Odpowiedzi:

6

Monady

Monada składa się z

  • Endofunctor . W naszym świecie inżynierii oprogramowania możemy powiedzieć, że odpowiada to typowi danych z pojedynczym, nieograniczonym parametrem typu. W języku C # byłoby to coś w rodzaju:

    class M<T> { ... }
    
  • Dwie operacje zdefiniowane dla tego typu danych:

    • return/ pureprzyjmuje „czystą” wartość (tj. Twartość) i „zawija” ją w monadę (tzn. tworzy M<T>wartość). Ponieważ returnjest to zastrzeżone słowo kluczowe w języku C #, pureodtąd będę się odnosił do tej operacji. W języku C # purebyłaby metoda z podpisem:

      M<T> pure(T v);
      
    • bind/ flatmapprzyjmuje wartość monadyczną ( M<A>) i funkcję f. fprzyjmuje wartość czystą i zwraca wartość monadyczną ( M<B>). Z nich bindtworzy nową wartość monadyczną ( M<B>). bindma następujący podpis w języku C #:

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

Ponadto, aby być monadą purei bindmuszą przestrzegać trzech praw monady.

Teraz jednym ze sposobów modelowania monad w języku C # byłoby zbudowanie interfejsu:

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

(Uwaga: aby wszystko było krótkie i wyraziste, w ramach tej odpowiedzi skorzystam z pewnej wolności z kodem).

Teraz możemy zaimplementować monady dla konkretnych typów danych, wdrażając konkretne implementacje Monad<M>. Na przykład możemy zaimplementować następującą monadę dla 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);
  }
}

(Celowo używam składni LINQ do wywołania związku między składnią LINQ a monadami. Pamiętaj jednak, że możemy zastąpić zapytanie LINQ wywołaniem do SelectMany).

Czy możemy teraz zdefiniować monadę IObservable? Wydawałoby się tak:

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);
  }
}

Aby mieć pewność, że mamy monadę, musimy udowodnić jej prawa. Może to być trywialne (i nie znam się na Rx.NET, aby wiedzieć, czy można je udowodnić na podstawie samej specyfikacji), ale jest to obiecujący początek. Aby ułatwić pozostałą część tej dyskusji, załóżmy, że obowiązują w tym przypadku prawa monad.

Darmowe Monady

Nie ma pojedynczej „wolnej monady”. Wolne monady są raczej klasą monad zbudowanych z funktorów. To znaczy, biorąc pod uwagę funktor F, możemy automatycznie wyprowadzić monadę F(tj. Wolną monadę F).

Functors

Podobnie jak monady, funktory można zdefiniować za pomocą następujących trzech elementów:

  • Typ danych sparametryzowany za pomocą jednej, nieograniczonej zmiennej typu.
  • Dwie operacje:

    • pureotacza czystą wartość funktorem. Jest to analogiczne do puremonady. W rzeczywistości dla funktorów, które są również monadami, oba powinny być identyczne.
    • fmapodwzorowuje wartości na wejściu na nowe wartości na wyjściu za pomocą danej funkcji. Jego podpis to:

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

Podobnie jak monady, funktory są zobowiązane do przestrzegania praw funktorów.

Podobnie jak monady, możemy modelować funktory za pomocą następującego interfejsu:

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

Teraz, ponieważ monady są podklasą funktorów, moglibyśmy również Monadtrochę zmienić :

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));
  }
}

Tutaj dodałem dodatkową metodę joini zapewniłem domyślne implementacje zarówno joini bind. Należy jednak pamiętać, że są to definicje kołowe. Więc musisz zastąpić co najmniej jedno lub drugie. Zauważ też, że pureteraz jest dziedziczony Functor.

IObservable i darmowe monady

Ponieważ zdefiniowaliśmy monadę dla, IObservablea ponieważ monady są podklasą funktorów, z tego wynika, że ​​musimy być w stanie zdefiniować instancję funktora IObservable. Oto jedna definicja:

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);
  }
}

Teraz, gdy mamy zdefiniowany funktor IObservable, możemy z niego zbudować wolną monadę. I właśnie tak IObservableodnosi się do wolnych monad - mianowicie, że możemy zbudować wolną monadę IObservable.

Nathan Davis
źródło
Wnikliwe zrozumienie teorii kategorii! Szukałem czegoś, co nie mówiło o tym, jak są tworzone, a raczej o różnicach podczas budowania funkcjonalnej architektury i kompozycji efektu modelowania z jednym z nich. FreeMonad może być wykorzystywany do tworzenia DSL dla operacji potwierdzonych, podczas gdy IObservables bardziej dotyczy dyskretnych wartości w czasie.
MLProgrammer-CiM
1
@ MLProgrammer-CiM, zobaczę, czy mogę dodać jakieś spostrzeżenia na ten temat w ciągu najbliższych kilku dni.
Nathan Davis,
Chciałbym praktyczny przykład darmowych monad
ja ... - ... - ”-”