Minął rok odkąd opublikowałem to pytanie. Po opublikowaniu, zagłębiałem się w Haskell przez kilka miesięcy. Bardzo mi się podobało, ale odłożyłem go na bok, gdy byłem gotowy zagłębić się w Monady. Wróciłem do pracy i skupiłem się na technologiach wymaganych przez mój projekt.
To jest całkiem niezłe. Jest to jednak trochę abstrakcyjne. Mogę sobie wyobrazić ludzi, którzy nie wiedzą, jakie monady są już zdezorientowane z powodu braku prawdziwych przykładów.
Więc pozwólcie, że spróbuję się zastosować, i żeby być naprawdę jasnym, zrobię przykład w języku C #, nawet jeśli będzie on wyglądał brzydko. Na końcu dodam równoważny Haskell i pokażę ci fajny cukier syntaktyczny Haskell, w którym, IMO, monady naprawdę zaczynają się przydać.
Okej, więc jedna z najłatwiejszych Monad nazywa się w Haskell „Może monada”. W języku C # wywoływany jest typ MożeNullable<T> . Zasadniczo jest to niewielka klasa, która po prostu zawiera pojęcie wartości, która jest albo poprawna i ma wartość, albo jest „zerowa” i nie ma żadnej wartości.
Przydatną rzeczą, którą można włożyć w monadę do łączenia wartości tego typu, jest pojęcie niepowodzenia. Tzn. Chcemy być w stanie spojrzeć na wiele wartości zerowalnych i powrócić, nullgdy tylko jedna z nich będzie pusta. Może to być przydatne, jeśli na przykład wyszukujesz wiele kluczy w słowniku lub coś, a na końcu chcesz przetworzyć wszystkie wyniki i jakoś je połączyć, ale jeśli któregokolwiek z kluczy nie ma w słowniku, chcesz wrócić nullpo całość. Byłoby żmudne, aby ręcznie sprawdzać każde wyszukiwanie
nulli zwracać, abyśmy mogli ukryć to sprawdzanie w operatorze powiązania (co jest swego rodzaju monadą, ukrywamy księgowanie w operatorze powiązania, co ułatwia kodowanie używać, ponieważ możemy zapomnieć o szczegółach).
Oto program, który motywuje to wszystko (zdefiniuję
Bindpóźniej, aby pokazać, dlaczego jest fajny).
class Program
{
static Nullable<int> f(){ return 4; }
static Nullable<int> g(){ return 7; }
static Nullable<int> h(){ return 9; }
static void Main(string[] args)
{
Nullable<int> z =
f().Bind( fval =>
g().Bind( gval =>
h().Bind( hval =>
new Nullable<int>( fval + gval + hval ))));
Console.WriteLine(
"z = {0}", z.HasValue ? z.Value.ToString() : "null" );
Console.WriteLine("Press any key to continue...");
Console.ReadKey();
}
}
Teraz zignoruj przez chwilę, że jest już wsparcie dla robienia tego Nullablew C # (możesz dodać razem wartości zerowe int i otrzymasz null, jeśli którykolwiek jest null). Udawajmy, że nie ma takiej funkcji, a jest to tylko klasa zdefiniowana przez użytkownika bez specjalnej magii. Chodzi o to, że możemy użyć Bindfunkcji, aby powiązać zmienną z zawartością naszej Nullablewartości, a następnie udawać, że nie dzieje się nic dziwnego, i użyć ich jak normalnych liczb całkowitych i po prostu dodać je razem. Mamy owinąć wynik w Nullable na końcu, a pustych albo będzie zerowa (jeśli którykolwiek z f, glub hzwraca null), albo będzie to wynik sumowania f, gorazhrazem. (jest to analogiczne do tego, w jaki sposób możemy powiązać wiersz w bazie danych ze zmienną w LINQ i robić to z nim, bezpiecznie wiedząc, że Bindoperator upewni się, że zmienna będzie zawsze przekazywać tylko prawidłowe wartości wierszy).
Można bawić się z tego i zmienić dowolne f, gi hpowrócić nieważną i widać, że cała sprawa zwróci null.
Jasne jest więc, że operator powiązania musi to dla nas sprawdzić i wydzwonić zwracając wartość null, jeśli napotka wartość null, i w przeciwnym razie przekaże wartość wewnątrz Nullablestruktury do lambda.
Oto Bindoperator:
public static Nullable<B> Bind<A,B>( this Nullable<A> a, Func<A,Nullable<B>> f )
where B : struct
where A : struct
{
return a.HasValue ? f(a.Value) : null;
}
Typy tutaj są takie same jak na filmie. Pobiera M a
( Nullable<A>w tym przypadku w składni C #) i funkcję od ado
M b( Func<A, Nullable<B>>w składni w C #) i zwraca an M b
( Nullable<B>).
Kod po prostu sprawdza, czy nullable zawiera wartość, a jeśli tak, to ją wyodrębnia i przekazuje do funkcji, w przeciwnym razie po prostu zwraca null. Oznacza to, że Bindoperator zajmie się całą logiką sprawdzania wartości zerowej. Jeśli i tylko jeśli wartość, którą wzywamy,
Bindjest różna od null, wówczas wartość ta zostanie „przekazana” do funkcji lambda, w przeciwnym razie wyskoczymy wcześniej, a całe wyrażenie jest puste. Pozwala to kod, który piszemy używając monady być całkowicie wolne od tego pustego sprawdzania zachowania, po prostu używać Bindi uzyskać zmienną związaną z wartością wewnętrzną wartością monadycznej ( fval,
gvala hvalw przykładowym kodzie) i możemy z nich korzystać bezpiecznie w wiedzy, Bindktóra zajmie się sprawdzeniem ich pod kątem zerowości przed przekazaniem ich dalej.
Są inne przykłady rzeczy, które możesz zrobić z monadą. Na przykład możesz zmusić Bindoperatora do zadbania o strumień wejściowy znaków i użyć go do napisania kombinacji parserów. Każdy kombinator parserów może wtedy być całkowicie nieświadomy takich rzeczy, jak śledzenie wstecz, awarie parsera itp., I po prostu łączyć mniejsze parsery razem, tak jakby nic nigdy nie poszło źle, wiedząc, że sprytna implementacja Bindrozwiązuje całą logikę stojącą za trudne kawałki. Potem może ktoś dodaje rejestrowanie do monady, ale kod używający monady nie zmienia się, ponieważ cała magia dzieje się w definicji Bindoperatora, reszta kodu pozostaje niezmieniona.
Wreszcie, oto implementacja tego samego kodu w Haskell ( --
rozpoczyna wiersz komentarza).
-- Here's the data type, it's either nothing, or "Just" a value
-- this is in the standard library
data Maybe a = Nothing | Just a
-- The bind operator for Nothing
Nothing >>= f = Nothing
-- The bind operator for Just x
Just x >>= f = f x
-- the "unit", called "return"
return = Just
-- The sample code using the lambda syntax
-- that Brian showed
z = f >>= ( \fval ->
g >>= ( \gval ->
h >>= ( \hval -> return (fval+gval+hval ) ) ) )
-- The following is exactly the same as the three lines above
z2 = do
fval <- f
gval <- g
hval <- h
return (fval+gval+hval)
Jak widać ładna donotacja na końcu sprawia, że wygląda jak prosty kod rozkazujący. I rzeczywiście jest to zgodne z projektem. Monady mogą być używane do enkapsulacji wszystkich użytecznych rzeczy w programowaniu imperatywnym (stan mutable, IO itp.) I używane przy użyciu tej ładnej składni podobnej do imperatywnej, ale za zasłonami są to tylko monady i sprytna implementacja operatora bind! Fajne jest to, że możesz implementować własne monady, implementując >>=i return. Jeśli to zrobisz, te monady również będą mogły korzystać z donotacji, co oznacza, że możesz w zasadzie pisać własne małe języki, po prostu definiując dwie funkcje!