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, runReaderquindi 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 localnon è 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 ided localè solo composizione di funzioni con l'ordine delle funzioni commutate!
Readeruna 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.
localfunzione 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' Applicativeistanza 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è, Functore Applicativeci 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 asu una storia di bvalori; ad esempio, potresti avere getSupervisor :: Person -> History Day Supervisor, e getVP :: Supervisor -> History Day VP. Quindi l'istanza Monad per Historyriguarda 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 VPche 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 Applicativeun'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 Historycodice 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:
ReaderaskReader ambiente di esecuzione.localUna buona analogia è che a Reader r arappresenta un acon "buchi" in esso, che ti impediscono di sapere di cosa astiamo parlando. Puoi ottenere un effettivo solo adopo aver fornito un rper 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 amancante r.
Quindi le istanze di Functor, Applicativee Monaddi Readersono una generalizzazione molto utile per i casi in cui si sta modellando qualcosa del tipo "uno a acui 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 re produce a, e Functor, Applicativee Monadle istanze sono modelli di base per lavorare con Readers. Functor= crea un Readerche modifica l'output di un altro Reader; Applicative= collega due Readers allo stesso ingresso e combina le loro uscite; Monad= controlla il risultato di a Readere usalo per costruirne un altro Reader. Le funzioni locale withReader= fanno un Readerche modifica l'input in un altro Reader.
GeneralizedNewtypeDerivingestensione 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 fn2non può avere bisogno parametro che si passa da fn1a 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 askssolo con .
do-notation, che sarebbe meglio essere refactored in una funzione pura.
whereclausola, sarà accettata come terzo modo per passare le variabili?