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ć?
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ć?
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
data Reader env a
takie że
-- Reader is a monad
instance Monad (Reader env)
-- and we have a function to get its environment
ask :: Reader env env
-- finally, we can run a Reader
runReader :: Reader env a -> env -> a
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ą.
import Control.Monad.Reader
data GameState = NotOver | FirstPlayerWin | SecondPlayerWin | Tie
data Game position
= Game {
getNext :: position -> [position],
getState :: position -> GameState
}
getNext' :: position -> Reader (Game position) [position]
getNext' position
= do game <- ask
return $ getNext game position
getState' :: position -> Reader (Game position) GameState
getState' position
= do game <- ask
return $ getState game position
negamax :: Double -> position -> Reader (Game position) Double
negamax color position
= do state <- getState' position
case state of
FirstPlayerWin -> return color
SecondPlayerWin -> return $ negate color
Tie -> return 0
NotOver -> do possible <- getNext' position
values <- mapM ((liftM negate) . negamax (negate color)) possible
return $ maximum values
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
type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict
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:
computePrice :: Reader CurrencyDict Dollars
computePrice
= do currencyDict <- ask
--insert computation here
Być może najbardziej klasycznym przypadkiem użycia jest implementacja interpreterów. Ale zanim się temu przyjrzymy, musimy wprowadzić inną funkcję
local :: (env -> env) -> Reader env a -> Reader env a
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
data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)
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).
newtype Env = Env ([(String, Closure)])
type Closure = (Term, Env)
Kiedy skończymy, powinniśmy otrzymać wartość (lub błąd):
data Value = Lam String Closure | Failure String
A więc napiszmy tłumacza:
interp' :: Term -> Reader Env Value
--when we have a lambda term, we can just return it
interp' (Lambda nv t)
= do env <- ask
return $ Lam nv (t, env)
--when we run into a value, we look it up in the environment
interp' (Var v)
= do (Env env) <- ask
case lookup (show v) env of
-- if it is not in the environment we have a problem
Nothing -> return . Failure $ "unbound variable: " ++ (show v)
-- if it is in the environment, then we should interpret it
Just (term, env) -> local (const env) $ interp' term
--the complicated case is an application
interp' (Apply t1 t2)
= do v1 <- interp' t1
case v1 of
Failure s -> return (Failure s)
Lam nv clos -> local (\(Env ls) -> Env ((nv, clos) : ls)) $ interp' t2
--I guess not that complicated!
Wreszcie możemy go użyć, przekazując trywialne środowisko:
interp :: Term -> Value
interp term = runReader (interp' term) (Env [])
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.
newtype Reader env a = Reader {runReader :: env -> a}
Czytnik to po prostu wymyślna nazwa funkcji! Zdefiniowaliśmy już runReader
, co z pozostałymi częściami API? Cóż, każdy Monad
jest również Functor
:
instance Functor (Reader env) where
fmap f (Reader g) = Reader $ f . g
Teraz, aby otrzymać monadę:
instance Monad (Reader env) where
return x = Reader (\_ -> x)
(Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x
co nie jest takie straszne. ask
jest naprawdę proste:
ask = Reader $ \x -> x
podczas gdy local
nie jest tak źle:
local f (Reader g) = Reader $ \x -> runReader g (f x)
OK, więc monada czytelnika to tylko funkcja. Dlaczego w ogóle czytnik? Dobre pytanie. Właściwie nie potrzebujesz tego!
instance Functor ((->) env) where
fmap = (.)
instance Monad ((->) env) where
return = const
f >>= g = \x -> g (f x) x
Te są jeszcze prostsze. Co więcej, ask
jest tylko id
i local
jest tylko kompozycją funkcji z przełączoną kolejnością funkcji!
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:
import Control.Applicative
-- | A History with timeline type t and value type a.
newtype History t a = History { observe :: t -> a }
instance Functor (History t) where
-- Apply a function to the contents of a historical value
fmap f hist = History (f . observe hist)
instance Applicative (History t) where
-- A "pure" History is one that has the same value at all points in time
pure = History . const
-- This applies a function that changes over time to a value that also
-- changes, by observing both at the same point in time.
ff <*> fx = History $ \t -> (observe ff t) (observe fx t)
instance Monad (History t) where
return = pure
ma >>= f = History $ \t -> observe (f (observe ma t)) t
Do Applicative
instancji oznacza, że jeśli trzeba employees :: History Day [Person]
i customers :: History Day [Person]
można to zrobić:
-- | For any given day, the list of employees followed by the customers
employeesAndCustomers :: History Day [Person]
employeesAndCustomers = (++) <$> employees <*> customers
To znaczy, Functor
i Applicative
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 typu a -> History t b
to funkcja, która odwzorowuje an a
na historię b
wartości; na przykład możesz mieć getSupervisor :: Person -> History Day Supervisor
i getVP :: Supervisor -> History Day VP
. Tak więc instancja Monad for History
dotyczy tworzenia takich funkcji; na przykład getSupervisor >=> getVP :: Person -> History Day VP
jest funkcją, która pobiera Person
historięVP
, które mieli.
Cóż, ta History
monada jest dokładnie taka sama jak Reader
. History t a
jest naprawdę taki sam jak Reader t a
(czyli taki sam jak t -> 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:
newtype Hypercube intersection value = Hypercube { get :: intersection -> value }
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ę dla Hypercube
:
instance Functor (Hypercube intersection) where
fmap f cube = Hypercube (f . get cube)
instance Applicative (Hypercube intersection) where
-- A "pure" Hypercube is one that has the same value at all intersections
pure = Hypercube . const
-- Apply each function in the @ff@ hypercube to its corresponding point
-- in @fx@.
ff <*> fx = Hypercube $ \x -> (get ff x) (get fx x)
Właśnie skopiowałem History
powyższy kod i zmieniłem nazwy. Jak widać, Hypercube
jest też sprawiedliwe Reader
.
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 an a
z "dziurami", które uniemożliwiają ci wiedzieć, o czym a
mówimy. Możesz otrzymać faktyczny a
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ę, dlaczego Reader r a
jest to samo co r -> a
, ponieważ taka funkcja jest również intuicyjnie a
brakująca r
.
Więc Functor
, Applicative
i Monad
przypadki Reader
są bardzo użyteczne uogólnienie dla przypadków, gdy jesteś modelowania coś w tym rodzaju „To a
, że brakuje r
” 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żywa r
i produkuje a
, a Functor
, Applicative
a Monad
przypadki są podstawowe wzory do pracy z Reader
s. Functor
= zrobić, Reader
który modyfikuje wyjście innego Reader
; Applicative
= podłącz dwa Reader
s do tego samego wejścia i połącz ich wyjścia; Monad
= sprawdź wynik a Reader
i użyj go do skonstruowania innego Reader
. Funkcje local
i withReader
= make a, Reader
które modyfikują dane wejściowe na inneReader
.
GeneralizedNewtypeDeriving
rozszerzenia do uzyskania Functor
, 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
funkcja fn2
może nie potrzebować parametru, z którego przekazuje się fn1
do fn3
.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.
do
adnotacjach, które lepiej byłoby zrefaktoryzować w czystą funkcję.
where
klauzulę, czy zostanie zaakceptowana jako trzeci sposób przekazywania zmiennych?