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ć, null
gdy 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ć null
po całość. Byłoby żmudne, aby ręcznie sprawdzać każde wyszukiwanie
null
i 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ę
Bind
póź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 Nullable
w 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ć Bind
funkcji, aby powiązać zmienną z zawartością naszej Nullable
wartoś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
, g
lub h
zwraca null), albo będzie to wynik sumowania f
, g
orazh
razem. (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 Bind
operator upewni się, że zmienna będzie zawsze przekazywać tylko prawidłowe wartości wierszy).
Można bawić się z tego i zmienić dowolne f
, g
i h
powró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 Nullable
struktury do lambda.
Oto Bind
operator:
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 a
do
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 Bind
operator zajmie się całą logiką sprawdzania wartości zerowej. Jeśli i tylko jeśli wartość, którą wzywamy,
Bind
jest 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ć Bind
i uzyskać zmienną związaną z wartością wewnętrzną wartością monadycznej ( fval
,
gval
a hval
w przykładowym kodzie) i możemy z nich korzystać bezpiecznie w wiedzy, Bind
któ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ć Bind
operatora 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 Bind
rozwią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 Bind
operatora, 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 do
notacja 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 do
notacji, co oznacza, że możesz w zasadzie pisać własne małe języki, po prostu definiując dwie funkcje!