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 Monadjest 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. askjest naprawdę proste:
ask = Reader $ \x -> x
podczas gdy localnie 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, askjest tylko idi localjest tylko kompozycją funkcji z przełączoną kolejnością funkcji!
Readerjest 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.
localFunkcja 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 Applicativeinstancji 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, Functori Applicativepozwalają 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 bto funkcja, która odwzorowuje an ana historię bwartości; na przykład możesz mieć getSupervisor :: Person -> History Day Supervisori getVP :: Supervisor -> History Day VP. Tak więc instancja Monad for Historydotyczy tworzenia takich funkcji; na przykład getSupervisor >=> getVP :: Person -> History Day VPjest funkcją, która pobiera PersonhistorięVP , które mieli.
Cóż, ta Historymonada jest dokładnie taka sama jak Reader. History t ajest 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 Applicativeinstancję 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 Historypowyższy kod i zmieniłem nazwy. Jak widać, Hypercubejest też sprawiedliwe Reader.
To trwa i trwa. Na przykład tłumacze języka również sprowadzają się do Reader, gdy zastosujesz ten model:
ReaderaskReaderśrodowisko wykonawcze.localDobrą analogią jest to, że a Reader r areprezentuje an az "dziurami", które uniemożliwiają ci wiedzieć, o czym amówimy. Możesz otrzymać faktyczny atylko wtedy, gdy podasz an, raby 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 ajest to samo co r -> a, ponieważ taka funkcja jest również intuicyjnie abrakująca r.
Więc Functor , Applicativei Monadprzypadki Readersą 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 ato coś, co zużywa ri produkuje a, a Functor, Applicativea Monadprzypadki są podstawowe wzory do pracy z Readers. Functor= zrobić, Readerktóry modyfikuje wyjście innego Reader; Applicative= podłącz dwa Readers do tego samego wejścia i połącz ich wyjścia; Monad= sprawdź wynik a Readeri użyj go do skonstruowania innego Reader. Funkcje locali withReader= make a, Readerktóre modyfikują dane wejściowe na inneReader .
GeneralizedNewtypeDerivingrozszerzenia 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 -> fn3funkcja fn2może nie potrzebować parametru, z którego przekazuje się fn1do 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ć askstylko.
doadnotacjach, które lepiej byłoby zrefaktoryzować w czystą funkcję.
whereklauzulę, czy zostanie zaakceptowana jako trzeci sposób przekazywania zmiennych?