La monade del lettore è così complessa e sembra essere inutile. In un linguaggio imperativo come Java o C ++, non esiste un concetto equivalente per la monade del lettore, se non mi sbaglio.
Puoi farmi un semplice esempio e chiarire un po 'questo?
La monade del lettore è così complessa e sembra essere inutile. In un linguaggio imperativo come Java o C ++, non esiste un concetto equivalente per la monade del lettore, se non mi sbaglio.
Puoi farmi un semplice esempio e chiarire un po 'questo?
Risposte:
Non aver paura! La monade del lettore in realtà non è così complicata e ha un'utilità davvero facile da usare.
Ci sono due modi per avvicinarsi a una monade: possiamo chiedere
Dal primo approccio, la monade del lettore è un tipo astratto
data Reader env a
tale che
-- 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
Allora come lo usiamo? Bene, la monade del lettore è utile per passare le informazioni di configurazione (implicite) attraverso un calcolo.
Ogni volta che hai una "costante" in un calcolo di cui hai bisogno in vari punti, ma in realtà vorresti essere in grado di eseguire lo stesso calcolo con valori diversi, dovresti usare una monade di lettura.
Le monadi di lettura vengono anche utilizzate per eseguire ciò che le persone OO chiamano iniezione di dipendenza . Ad esempio, l' algoritmo negamax viene utilizzato frequentemente (in forme altamente ottimizzate) per calcolare il valore di una posizione in una partita a due giocatori. Tuttavia, l'algoritmo stesso non si preoccupa del gioco a cui stai giocando, tranne per il fatto che devi essere in grado di determinare quali sono le posizioni "successive" nel gioco e devi essere in grado di dire se la posizione corrente è una posizione di vittoria.
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
Questo funzionerà quindi con qualsiasi gioco a due giocatori finito e deterministico.
Questo modello è utile anche per cose che non sono realmente iniezione di dipendenza. Supponiamo che tu lavori nel campo della finanza, potresti progettare una logica complicata per valutare un asset (diciamo un derivato), che va benissimo e puoi fare a meno di monadi puzzolenti. Ma poi, modifichi il tuo programma per gestire più valute. Devi essere in grado di convertire tra valute al volo. Il tuo primo tentativo è definire una funzione di primo livello
type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict
per ottenere prezzi spot. Puoi quindi chiamare questo dizionario nel tuo codice ... ma aspetta! Non funzionerà! Il dizionario di valuta è immutabile e quindi deve essere lo stesso non solo per la vita del tuo programma, ma dal momento in cui viene compilato ! Allora cosa fai? Bene, un'opzione sarebbe usare la monade Reader:
computePrice :: Reader CurrencyDict Dollars
computePrice
= do currencyDict <- ask
--insert computation here
Forse il caso d'uso più classico è nell'implementazione di interpreti. Ma, prima di esaminarlo, dobbiamo introdurre un'altra funzione
local :: (env -> env) -> Reader env a -> Reader env a
Ok, quindi Haskell e altri linguaggi funzionali sono basati sul lambda calcolo . Lambda calcolo ha una sintassi simile a
data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)
e vogliamo scrivere un valutatore per questa lingua. Per fare ciò, dovremo tenere traccia di un ambiente, che è un elenco di vincoli associati ai termini (in realtà saranno chiusure perché vogliamo fare lo scoping statico).
newtype Env = Env ([(String, Closure)])
type Closure = (Term, Env)
Quando abbiamo finito, dovremmo ottenere un valore (o un errore):
data Value = Lam String Closure | Failure String
Quindi, scriviamo l'interprete:
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!
Infine, possiamo usarlo passando un ambiente banale:
interp :: Term -> Value
interp term = runReader (interp' term) (Env [])
E questo è tutto. Un interprete completamente funzionale per il lambda calcolo.
L'altro modo per pensare a questo è chiedere: come viene implementato? La risposta è che la monade del lettore è in realtà una delle più semplici ed eleganti di tutte le monadi.
newtype Reader env a = Reader {runReader :: env -> a}
Reader è solo un nome di fantasia per le funzioni! Abbiamo già definito, runReader
quindi per quanto riguarda le altre parti dell'API? Bene, ogni Monad
è anche un Functor
:
instance Functor (Reader env) where
fmap f (Reader g) = Reader $ f . g
Ora, per ottenere una monade:
instance Monad (Reader env) where
return x = Reader (\_ -> x)
(Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x
che non è così spaventoso. ask
è davvero semplice:
ask = Reader $ \x -> x
mentre local
non è così male:
local f (Reader g) = Reader $ \x -> runReader g (f x)
Ok, quindi la monade del lettore è solo una funzione. Perché avere Reader? Buona domanda. In realtà, non ne hai bisogno!
instance Functor ((->) env) where
fmap = (.)
instance Monad ((->) env) where
return = const
f >>= g = \x -> g (f x) x
Questi sono ancora più semplici. Inoltre, ask
è solo id
ed local
è solo composizione di funzioni con l'ordine delle funzioni commutate!
Reader
una funzione con qualche implementazione particolare della classe di tipo monade? Dirlo prima mi avrebbe aiutato a rimanere un po 'meno perplesso. Prima non l'ho capito. A metà ho pensato "Oh, ti permette di restituire qualcosa che ti darà il risultato desiderato una volta fornito il valore mancante". Ho pensato che fosse utile, ma improvvisamente mi sono reso conto che una funzione fa esattamente questo.
local
funzione ha bisogno di qualche spiegazione in più però ..
(Reader f) >>= g = (g (f x))
?
x
?
Ricordo di essere rimasto perplesso come te, finché non ho scoperto da solo che le varianti della monade Lettore sono ovunque . Come l'ho scoperto? Perché ho continuato a scrivere codice che si è rivelato essere piccole variazioni.
Ad esempio, a un certo punto stavo scrivendo del codice per trattare i valori storici ; valori che cambiano nel tempo. Un modello molto semplice di questo è funzioni dai punti del tempo al valore in quel punto nel tempo:
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
L' Applicative
istanza significa che se hai employees :: History Day [Person]
e customers :: History Day [Person]
puoi farlo:
-- | For any given day, the list of employees followed by the customers
employeesAndCustomers :: History Day [Person]
employeesAndCustomers = (++) <$> employees <*> customers
Cioè, Functor
e Applicative
ci permettono di adattare funzioni regolari e non storiche per lavorare con le storie.
L'istanza della monade viene compresa più intuitivamente considerando la funzione (>=>) :: Monad m => (a -> m b) -> (b -> m c) -> a -> m c
. Una funzione di tipoa -> History t b
è una funzione che mappa una a
su una storia di b
valori; ad esempio, potresti avere getSupervisor :: Person -> History Day Supervisor
, e getVP :: Supervisor -> History Day VP
. Quindi l'istanza Monad per History
riguarda la composizione di funzioni come queste; per esempio, getSupervisor >=> getVP :: Person -> History Day VP
è la funzione che ottiene, per ogni Person
, la cronologia dei messaggi VP
che hanno avuto.
Bene, questo History
monade è in realtà esattamente la stessa di Reader
. History t a
è davvero lo stesso di Reader t a
(che è lo stesso di t -> a
).
Un altro esempio: recentemente ho realizzato prototipi di progetti OLAP ad Haskell. Un'idea qui è quella di un "ipercubo", che è una mappatura dalle intersezioni di un insieme di dimensioni ai valori. Ci risiamo:
newtype Hypercube intersection value = Hypercube { get :: intersection -> value }
Una delle operazioni comuni sugli ipercubi è l'applicazione di funzioni scalari multiposizione ai punti corrispondenti di un ipercubo. Questo lo possiamo ottenere definendo Applicative
un'istanza per 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)
Ho appena copiato il History
codice sopra e ho cambiato i nomi. Come puoi vedere, Hypercube
è anche giusto Reader
.
Va avanti e avanti. Ad esempio, gli interpreti linguistici si riducono anche a Reader
, quando si applica questo modello:
Reader
ask
Reader
ambiente di esecuzione.local
Una buona analogia è che a Reader r a
rappresenta un a
con "buchi" in esso, che ti impediscono di sapere di cosa a
stiamo parlando. Puoi ottenere un effettivo solo a
dopo aver fornito un r
per riempire i buchi. Ci sono tantissime cose del genere. Negli esempi precedenti, una "cronologia" è un valore che non può essere calcolato finché non si specifica un'ora, un ipercubo è un valore che non può essere calcolato finché non si specifica un'intersezione e un'espressione del linguaggio è un valore che può viene calcolato fino a quando non si forniscono i valori delle variabili. Ti dà anche un'intuizione sul perché Reader r a
è lo stesso di r -> a
, perché una tale funzione è anche intuitivamente a
mancante r
.
Quindi le istanze di Functor
, Applicative
e Monad
di Reader
sono una generalizzazione molto utile per i casi in cui si sta modellando qualcosa del tipo "uno a a
cui manca un r
" e consentono di trattare questi oggetti "incompleti" come se fossero completi.
Ancora un altro modo per dire la stessa cosa: una Reader r a
è una cosa che consuma r
e produce a
, e Functor
, Applicative
e Monad
le istanze sono modelli di base per lavorare con Reader
s. Functor
= crea un Reader
che modifica l'output di un altro Reader
; Applicative
= collega due Reader
s allo stesso ingresso e combina le loro uscite; Monad
= controlla il risultato di a Reader
e usalo per costruirne un altro Reader
. Le funzioni local
e withReader
= fanno un Reader
che modifica l'input in un altro Reader
.
GeneralizedNewtypeDeriving
estensione per derivare Functor
, Applicative
, Monad
, ecc per newtypes in base al loro tipo di fondo.
In Java o C ++ puoi accedere a qualsiasi variabile da qualsiasi luogo senza alcun problema. I problemi si verificano quando il codice diventa multi-thread.
In Haskell hai solo due modi per passare il valore da una funzione all'altra:
fn1 -> fn2 -> fn3
, la funzione fn2
non può avere bisogno parametro che si passa da fn1
a fn3
.La monade Reader passa semplicemente i dati che desideri condividere tra le funzioni. Le funzioni possono leggere quei dati, ma non possono cambiarli. Questo è tutto ciò che fa la monade Reader. Bene, quasi tutti. Ci sono anche numerose funzioni come local
, ma per la prima volta puoi restare asks
solo con .
do
-notation, che sarebbe meglio essere refactored in una funzione pura.
where
clausola, sarà accettata come terzo modo per passare le variabili?