Monada czytelnicza jest tak złożona i wydaje się bezużyteczna. W języku imperatywnym, takim jak Java czy C ++, nie ma odpowiednika koncepcji monady czytelnika, jeśli się nie mylę.
Czy możesz podać mi prosty przykład i trochę to wyjaśnić?
haskell
monads
reader-monad
chipbk10
źródło
źródło
Odpowiedzi:
Nie bój się! Monada czytnika nie jest w rzeczywistości tak skomplikowana i ma naprawdę łatwe w użyciu narzędzie.
Istnieją dwa sposoby podejścia do monady: możemy zapytać
Od pierwszego podejścia monada czytelnika jest jakimś abstrakcyjnym typem
takie że
Jak więc tego używamy? Cóż, monada czytnika jest dobra do przekazywania (niejawnych) informacji konfiguracyjnych przez obliczenia.
Za każdym razem, gdy masz w obliczeniach „stałą”, której potrzebujesz w różnych punktach, ale tak naprawdę chciałbyś móc wykonać te same obliczenia z różnymi wartościami, powinieneś użyć monady czytającej.
Monady czytnika są również używane do robienia tego, co ludzie OO nazywają wstrzykiwaniem zależności . Na przykład algorytm negamax jest często używany (w wysoce zoptymalizowanych formach) do obliczania wartości pozycji w grze dwuosobowej. Jednak sam algorytm nie dba o to, w jaką grę grasz, poza tym, że musisz być w stanie określić, jakie "następne" pozycje są w grze i musisz być w stanie stwierdzić, czy aktualna pozycja jest pozycją zwycięską.
To będzie działać z każdą skończoną, deterministyczną grą dla dwóch graczy.
Ten wzorzec jest przydatny nawet w przypadku rzeczy, które tak naprawdę nie są wstrzykiwaniem zależności. Załóżmy, że pracujesz w finansach, możesz zaprojektować skomplikowaną logikę wyceny aktywów (powiedzmy pochodną), co jest dobre i dobre i możesz obejść się bez śmierdzących monad. Ale potem modyfikujesz swój program, aby obsługiwał wiele walut. Musisz mieć możliwość przeliczania walut w locie. Pierwszą próbą jest zdefiniowanie funkcji najwyższego poziomu
aby uzyskać ceny spot. Następnie możesz wywołać ten słownik w swoim kodzie ... ale czekaj! To nie zadziała! Słownik walut jest niezmienny i dlatego musi być taki sam nie tylko przez cały czas trwania programu, ale od momentu jego kompilacji ! Więc co robisz? Cóż, jedną z opcji byłoby użycie monady Reader:
Być może najbardziej klasycznym przypadkiem użycia jest implementacja interpreterów. Ale zanim się temu przyjrzymy, musimy wprowadzić inną funkcję
OK, więc Haskell i inne języki funkcjonalne są oparte na rachunku lambda . Rachunek lambda ma składnię, która wygląda następująco
i chcemy napisać ewaluatora dla tego języka. Aby to zrobić, będziemy musieli śledzić środowisko, które jest listą powiązań powiązanych z terminami (w rzeczywistości będą to zamknięcia, ponieważ chcemy wykonywać statyczne określanie zakresu).
Kiedy skończymy, powinniśmy otrzymać wartość (lub błąd):
A więc napiszmy tłumacza:
Wreszcie możemy go użyć, przekazując trywialne środowisko:
I to wszystko. W pełni funkcjonalny interpreter rachunku lambda.
Innym sposobem myślenia o tym jest pytanie: jak to jest realizowane? Odpowiedź jest taka, że monada czytelnika jest właściwie jedną z najprostszych i najbardziej eleganckich ze wszystkich monad.
Czytnik to po prostu wymyślna nazwa funkcji! Zdefiniowaliśmy już
runReader
, co z pozostałymi częściami API? Cóż, każdyMonad
jest równieżFunctor
:Teraz, aby otrzymać monadę:
co nie jest takie straszne.
ask
jest naprawdę proste:podczas gdy
local
nie jest tak źle:OK, więc monada czytelnika to tylko funkcja. Dlaczego w ogóle czytnik? Dobre pytanie. Właściwie nie potrzebujesz tego!
Te są jeszcze prostsze. Co więcej,
ask
jest tylkoid
ilocal
jest tylko kompozycją funkcji z przełączoną kolejnością funkcji!źródło
Reader
jest więc funkcja z określoną implementacją klasy typu monad? Powiedzenie tego wcześniej pomogłoby mi w nieco mniejszym zdziwieniu. Po pierwsze, nie rozumiałem. W połowie pomyślałem: „Och, to pozwala Ci zwrócić coś, co da pożądany rezultat, gdy podasz brakującą wartość”. Pomyślałem, że to przydatne, ale nagle zdałem sobie sprawę, że funkcja robi dokładnie to.local
Funkcja wymaga trochę więcej wyjaśnień choć ..(Reader f) >>= g = (g (f x))
?x
?Pamiętam, jak byłeś zdziwiony, dopóki sam nie odkryłem, że warianty monady Reader są wszędzie . Jak to odkryłem? Ponieważ ciągle pisałem kod, który okazał się być jego małymi wariacjami.
Na przykład w pewnym momencie pisałem kod dotyczący wartości historycznych ; wartości, które zmieniają się w czasie. Bardzo prostym modelem tego są funkcje od punktów w czasie do wartości w danym momencie:
Do
Applicative
instancji oznacza, że jeśli trzebaemployees :: History Day [Person]
icustomers :: History Day [Person]
można to zrobić:To znaczy,
Functor
iApplicative
pozwalają nam dostosowywać regularne, niehistoryczne funkcje do pracy z historiami.Instancję monady można najbardziej intuicyjnie zrozumieć, biorąc pod uwagę funkcję
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
. Funkcja typua -> History t b
to funkcja, która odwzorowuje ana
na historięb
wartości; na przykład możesz miećgetSupervisor :: Person -> History Day Supervisor
igetVP :: Supervisor -> History Day VP
. Tak więc instancja Monad forHistory
dotyczy tworzenia takich funkcji; na przykładgetSupervisor >=> getVP :: Person -> History Day VP
jest funkcją, która pobieraPerson
historięVP
, które mieli.Cóż, ta
History
monada jest dokładnie taka sama jakReader
.History t a
jest naprawdę taki sam jakReader t a
(czyli taki sam jakt -> a
).Inny przykład: ostatnio prototypowałem projekty OLAP w Haskell. Jednym z pomysłów jest „hipersześcian”, czyli odwzorowanie przecięć zestawu wymiarów na wartości. Znowu zaczynamy:
Jedną z typowych operacji na hipersześcianach jest zastosowanie wielomiejscowych funkcji skalarnych do odpowiednich punktów hipersześcianu. Możemy to uzyskać, definiując
Applicative
instancję dlaHypercube
:Właśnie skopiowałem
History
powyższy kod i zmieniłem nazwy. Jak widać,Hypercube
jest też sprawiedliweReader
.To trwa i trwa. Na przykład tłumacze języka również sprowadzają się do
Reader
, gdy zastosujesz ten model:Reader
ask
Reader
środowisko wykonawcze.local
Dobrą analogią jest to, że a
Reader r a
reprezentuje ana
z "dziurami", które uniemożliwiają ci wiedzieć, o czyma
mówimy. Możesz otrzymać faktycznya
tylko wtedy, gdy podasz an,r
aby wypełnić dziury. Takich rzeczy jest mnóstwo. W powyższych przykładach „historia” to wartość, której nie można obliczyć, dopóki nie określisz czasu, hipersześcian to wartość, której nie można obliczyć, dopóki nie określisz przecięcia, a wyrażenie językowe to wartość, która może nie będą obliczane, dopóki nie podasz wartości zmiennych. Daje też intuicję, dlaczegoReader r a
jest to samo cor -> a
, ponieważ taka funkcja jest również intuicyjniea
brakującar
.Więc
Functor
,Applicative
iMonad
przypadkiReader
są bardzo użyteczne uogólnienie dla przypadków, gdy jesteś modelowania coś w tym rodzaju „Toa
, że brakujer
” i pozwala traktować te „niekompletnych” obiekty, jak gdyby były one kompletne.Jeszcze innym sposobem na powiedzenie to samo: a
Reader r a
to coś, co zużywar
i produkujea
, aFunctor
,Applicative
aMonad
przypadki są podstawowe wzory do pracy zReader
s.Functor
= zrobić,Reader
który modyfikuje wyjście innegoReader
;Applicative
= podłącz dwaReader
s do tego samego wejścia i połącz ich wyjścia;Monad
= sprawdź wynik aReader
i użyj go do skonstruowania innegoReader
. Funkcjelocal
iwithReader
= make a,Reader
które modyfikują dane wejściowe na inneReader
.źródło
GeneralizedNewtypeDeriving
rozszerzenia do uzyskaniaFunctor
,Applicative
,Monad
, itd. Dla newtypes na podstawie ich typów bazowych.W Javie lub C ++ możesz bez problemu uzyskać dostęp do dowolnej zmiennej z dowolnego miejsca. Problemy pojawiają się, gdy kod staje się wielowątkowy.
W Haskell masz tylko dwa sposoby na przekazanie wartości z jednej funkcji do drugiej:
fn1 -> fn2 -> fn3
funkcjafn2
może nie potrzebować parametru, z którego przekazuje sięfn1
dofn3
.Monada Reader po prostu przekazuje dane, które chcesz udostępniać między funkcjami. Funkcje mogą odczytywać te dane, ale nie mogą ich zmieniać. To wszystko, co robi monada Reader. Cóż, prawie wszyscy. Istnieje również wiele funkcji, takich jak
local
, ale po raz pierwszy możesz się trzymaćasks
tylko.źródło
do
adnotacjach, które lepiej byłoby zrefaktoryzować w czystą funkcję.where
klauzulę, czy zostanie zaakceptowana jako trzeci sposób przekazywania zmiennych?