Qual è lo scopo della monade lettrice?


122

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?


21
Si utilizza la monade del lettore se si desidera, a volte, leggere alcuni valori da un ambiente (non modificabile), ma non si desidera passare esplicitamente tale ambiente. In Java o C ++, useresti variabili globali (anche se non è esattamente la stessa).
Daniel Fischer

5
@Daniel: Sembra quasi una risposta
SingleNegationElimination

@TokenMacGuy Troppo breve per una risposta, ed è troppo tardi per me ora pensare a qualcosa di più lungo. Se nessun altro lo fa, lo farò dopo che avrò dormito.
Daniel Fischer

8
In Java o C ++, la monade Reader sarebbe analoga ai parametri di configurazione passati a un oggetto nel suo costruttore che non vengono mai modificati durante la vita dell'oggetto. In Clojure, sarebbe un po 'come una variabile con ambito dinamico utilizzata per parametrizzare il comportamento di una funzione senza doverla passare esplicitamente come parametro.
danidiaz

Risposte:


169

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

  1. Che cosa fa la monade fare ? Di quali operazioni è dotato? Per cosa è buono?
  2. Come viene implementata la monade? Da dove nasce?

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!


6
Risposta molto interessante. Onestamente, l'ho letto di nuovo molte volte, quando voglio recensire monade. A proposito, riguardo all'algoritmo Nagamax, "valori <- mapM (negate. Negamax (negamax (negate color))) possibili" non sembra corretto. Lo so, il codice che fornisci serve solo per mostrare come funziona la monade del lettore. Ma se hai tempo, potresti correggere il codice dell'algoritmo negamax? Perché è interessante quando usi la monade del lettore per risolvere negamax.
chipbk10

4
Quindi 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.
ziggystar

1
Dopo aver letto questo, ne ho capito la maggior parte. La localfunzione ha bisogno di qualche spiegazione in più però ..
Christophe De Troyer

@Philip Ho una domanda sull'istanza di Monad. Non possiamo scrivere la funzione bind come (Reader f) >>= g = (g (f x))?
zeronone

@zeronone dov'è x ?
Ashish Negi

56

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:

  • Espressione = a Reader
  • Variabili libere = usi di ask
  • Ambiente di valutazione = Reader ambiente di esecuzione.
  • Costrutti vincolanti = local

Una 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.


5
Bella risposta. È inoltre possibile utilizzare l' GeneralizedNewtypeDerivingestensione per derivare Functor, Applicative, Monad, ecc per newtypes in base al loro tipo di fondo.
Rein Henrichs

20

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:

  • Il valore viene passato tramite uno dei parametri di input della funzione richiamabile. Gli svantaggi sono: 1) non puoi passare TUTTE le variabili in questo modo - l'elenco dei parametri di input ti lascia a bocca aperta. 2) in sequenza di chiamate di funzione: fn1 -> fn2 -> fn3, la funzione fn2non può avere bisogno parametro che si passa da fn1a fn3.
  • Si passa il valore nell'ambito di qualche monade. Lo svantaggio è: devi capire bene cos'è il concepimento della Monade. Trasmettere i valori in giro è solo una delle tante applicazioni in cui puoi usare le Monadi. In realtà la concezione della Monade è incredibilmente potente. Non arrabbiarti, se non hai ottenuto informazioni immediatamente. Continua a provare e leggi diversi tutorial. La conoscenza che otterrai ti ripagherà.

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 .


3
Un ulteriore svantaggio dell'uso delle monadi per passare implicitamente i dati è che è molto facile ritrovarsi a scrivere un sacco di codice "imperativo" in do-notation, che sarebbe meglio essere refactored in una funzione pura.
Benjamin Hodgson

4
@BenjaminHodgson Scrivere codice dall'aspetto imperativo con monadi nella notazione non significa necessariamente scrivere codice efficace (impuro). In realtà, il codice con effetto collaterale in Haskell potrebbe essere possibile solo all'interno della monade IO.
Dmitry Bespalov

Se l'altra funzione è collegata a quella da una whereclausola, sarà accettata come terzo modo per passare le variabili?
Elmex80s
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.