Diversi modi di vedere una monade


29

Durante l'apprendimento di Haskell ho affrontato molti tutorial cercando di spiegare cosa sono le monadi e perché le monadi sono importanti in Haskell. Ognuno di loro ha usato analogie, quindi sarebbe più facile coglierne il significato. Alla fine della giornata, ho finito con 3 diversi punti di vista su cosa sia una monade:

Visualizza 1: Monade come etichetta

A volte penso che una monade sia un'etichetta per un tipo specifico. Ad esempio, una funzione di tipo:

myfunction :: IO Int

myfunction è una funzione che ogni volta che viene eseguita genererà un valore Int. Il tipo di risultato non è Int ma IO Int. Pertanto, IO è un'etichetta del valore Int che avvisa l'utente di sapere che il valore Int è il risultato di un processo in cui è stata eseguita un'azione IO.

Di conseguenza, questo valore Int è stato contrassegnato come valore proveniente da un processo con IO, quindi questo valore è "sporco". Il tuo processo non è più puro.

Visualizza 2: Monade come uno spazio privato in cui possono accadere cose brutte.

In un sistema in cui tutto il processo è puro e rigoroso a volte è necessario avere effetti collaterali. Quindi, una monade è solo un piccolo spazio che ti viene concesso per fare effetti collaterali cattivi. In questo spazio ti è permesso di sfuggire al mondo puro, andare all'impuro, rendere il tuo processo e poi tornare con un valore.

Visualizza 3: Monade come nella teoria delle categorie

Questa è l'opinione che non capisco del tutto. Una monade è solo una funzione della stessa categoria o sottocategoria. Ad esempio, si hanno i valori Int e come sottocategoria IO Int, che sono i valori Int generati dopo un processo IO.

Queste opinioni sono corrette? Qual è più preciso?


5
# 2 non è ciò che una monade è in generale. In realtà, è praticamente limitato a IO, e non è una visione utile (cfr. Cosa non è una Monade ). Inoltre, "rigoroso" è generalmente usato per nominare una proprietà che Haskell non possiede (vale a dire una valutazione rigorosa). A proposito, neanche le Monadi non cambiano questo (di nuovo, vedi Cosa non è una Monade).

3
Tecnicamente, solo il terzo è corretto. Monad è endofunctor, per cui sono definite operazioni speciali: promozione e rilegatura. Le monadi sono numerose: una lista monade è l'esempio perfetto per ottenere l'intuizione dietro le monadi. le strutture readS sono ancora migliori. Abbastanza sorprendente, le monadi sono utilizzabili come strumenti per infilare implicitamente lo stato nel linguaggio funzionale puro. Questa non è una proprietà che definisce le monadi: è una coincidenza che il threading dello stato possa essere implementato nei loro termini. Lo stesso vale per IO.
permeakra,

Common Lisp ha il suo compilatore come parte del linguaggio. Haskell ha Monadi.
Will Ness,

Risposte:


33

Le visualizzazioni n. 1 e n. 2 non sono corrette in generale.

  1. Qualsiasi tipo di tipo di dati * -> *può funzionare come un'etichetta, le monadi sono molto più di questo.
  2. (Ad eccezione della IOmonade) i calcoli all'interno di una monade non sono impuri. Rappresentano semplicemente calcoli che percepiamo avere effetti collaterali, ma sono puri.

Entrambi questi equivoci derivano dal concentrarsi sulla IOmonade, che in realtà è un po 'speciale.

Proverò a elaborare un po 'il n. 3, senza entrare nella teoria delle categorie, se possibile.


Calcoli standard

Tutti i calcoli in un linguaggio di programmazione funzionale, può essere visto come funzioni con un tipo di origine e un tipo di destinazione: f :: a -> b. Se una funzione ha più di un argomento, possiamo convertirla in una funzione a un argomento tramite il curry (vedi anche wiki di Haskell ). E se abbiamo solo un valore x :: a(una funzione con argomenti 0), possiamo convertirlo in una funzione che prende un argomento del tipo di unità : (\_ -> x) :: () -> a.

Possiamo costruire programmi più complessi da quelli più semplici componendo tali funzioni usando l' .operatore. Ad esempio, se abbiamo f :: a -> be g :: b -> cotteniamo g . f :: a -> c. Nota che funziona anche per i nostri valori convertiti: se lo abbiamo x :: ae lo convertiamo nella nostra rappresentazione, otteniamo f . ((\_ -> x) :: () -> a) :: () -> b.

Questa rappresentazione ha alcune proprietà molto importanti, vale a dire:

  • Abbiamo una funzione molto speciale: la funzione di identitàid :: a -> a per ogni tipo a. È un elemento identitario rispetto a .: fè uguale sia a f . idche a id . f.
  • L'operatore di composizione della funzione .è associativo .

Calcoli monadici

Supponiamo di voler selezionare e lavorare con una speciale categoria di calcoli, il cui risultato contiene qualcosa di più del semplice valore restituito. Non vogliamo specificare cosa significhi "qualcosa in più", vogliamo mantenere le cose il più generali possibile. Il modo più generale di rappresentare "qualcosa in più" è rappresentarlo come una funzione di tipo - un tipo mdi tipo * -> *(cioè converte un tipo in un altro). Quindi per ogni categoria di calcoli con cui vogliamo lavorare, avremo una funzione di tipo m :: * -> *. (In Haskell, mè [], IO, Maybe, etc.) e la categoria volontà contiene tutte le funzioni di tipi a -> m b.

Ora vorremmo lavorare con le funzioni in tale categoria allo stesso modo del caso di base. Vogliamo essere in grado di comporre queste funzioni, vogliamo che la composizione sia associativa e vogliamo avere un'identità. Abbiamo bisogno:

  • Avere un operatore (chiamiamolo così <=<) che compone le funzioni f :: a -> m be g :: b -> m cin qualcosa come g <=< f :: a -> m c. E deve essere associativo.
  • Per avere una funzione di identità per ogni tipo, chiamiamola return. Vogliamo anche che f <=< returnsia uguale fe uguale a return <=< f.

Qualsiasi m :: * -> *per cui abbiamo tali funzioni returned <=<è chiamato una monade . Ci consente di creare calcoli complessi da quelli più semplici, proprio come nel caso di base, ma ora i tipi di valori di ritorno vengono trasformati da m.

(In realtà, ho leggermente abusato del termine categoria qui. Nel senso della teoria delle categorie possiamo definire la nostra costruzione una categoria solo dopo che sappiamo che obbedisce a queste leggi.)

Monadi di Haskell

In Haskell (e altri linguaggi funzionali) lavoriamo principalmente con valori, non con funzioni di tipo () -> a. Quindi, invece di definire <=<per ogni monade, definiamo una funzione (>>=) :: m a -> (a -> m b) -> m b. Tale definizione alternativa è equivalente, possiamo esprimere >>=usando <=<e viceversa (prova come un esercizio o vedi le fonti ). Il principio ora è meno ovvio, ma rimane lo stesso: i nostri risultati sono sempre di tipi m ae componiamo funzioni di tipi a -> m b.

Per ogni monade che creiamo, non dobbiamo dimenticare di verificarlo returne di <=<avere le proprietà richieste: associatività e identità sinistra / destra. Espressi usando returne >>=sono chiamati leggi della monade .

Un esempio: elenchi

Se scegliamo mdi esserlo [], otteniamo una categoria di funzioni di tipi a -> [b]. Tali funzioni rappresentano calcoli non deterministici, i cui risultati potrebbero essere uno o più valori, ma anche nessun valore. Questo dà origine alla cosiddetta lista monade . La composizione di f :: a -> [b]e g :: b -> [c]funziona come segue: g <=< f :: a -> [c]significa calcolare tutti i possibili risultati di tipo [b], applicare ga ciascuno di essi e raccogliere tutti i risultati in un unico elenco. Espresso in Haskell

return :: a -> [a]
return x = [x]
(<=<) :: (b -> [c]) -> (a -> [b]) -> (a -> [c])
g (<=<) f  = concat . map g . f

o usando >>=

(>>=) :: [a] -> (a -> [b]) -> [b]
x >>= f  = concat (map f x)

Si noti che in questo esempio i tipi restituiti erano [a]quindi era possibile che non contenessero alcun valore di tipo a. In effetti, per una monade non esiste tale requisito che il tipo restituito debba avere tali valori. Alcune monadi hanno sempre (come IOo State), ma altre no, come []o Maybe.

La monade IO

Come ho già detto, la IOmonade è in qualche modo speciale. Un valore di tipo IO aindica un valore di tipo acostruito interagendo con l'ambiente del programma. Quindi (diversamente da tutte le altre monadi), non possiamo descrivere un valore di tipo IO ausando una costruzione pura. Ecco IOsemplicemente un tag o un'etichetta che distingue i calcoli che interagiscono con l'ambiente. Questo è (l'unico caso) in cui le viste n. 1 e n. 2 sono corrette.

Per la IOmonade:

  • Composizione f :: a -> IO be g :: b -> IO cmezzi: calcolo fche interagisce con l'ambiente, quindi calcolo gche utilizza il valore e calcola il risultato interagendo con l'ambiente.
  • returnaggiunge semplicemente il IO"tag" al valore (semplicemente "calcoliamo" il risultato mantenendo intatto l'ambiente).
  • Le leggi della monade (associatività, identità) sono garantite dal compilatore.

Alcune note:

  1. Dato che i calcoli monadici hanno sempre il tipo di risultato di m a, non c'è modo di "sfuggire" alla IOmonade. Il significato è: una volta che un calcolo interagisce con l'ambiente, non è possibile costruirne uno che non lo fa.
  2. Quando un programmatore funzionale non sa come fare qualcosa in modo puro, può (come ultima risorsa) programmare l'attività mediante un calcolo con stato all'interno della IOmonade. Questo è il motivo per cui IOviene spesso chiamato sin bin di un programmatore .
  3. Si noti che in un mondo impuro (nel senso della programmazione funzionale) la lettura di un valore può anche cambiare l'ambiente (come consumare l'input dell'utente). Ecco perché funzioni come getChardevono avere un tipo di risultato di IO something.

3
Bella risposta. Chiarirei che IOnon ha una semantica speciale dal punto di vista linguistico. E ' non è speciale, si comporta come qualsiasi altro codice. Solo l'implementazione della libreria di runtime è speciale. Inoltre, esiste un modo speciale di escape ( unsafePerformIO). Penso che questo sia importante perché le persone spesso pensano IOcome un elemento di linguaggio speciale o un tag dichiarativo. Non è.
usr

2
@usr buon punto. Aggiungerei che unsafePerformIO è davvero non sicuro e dovrebbe essere usato solo da esperti. Ti consente di interrompere tutto, ad esempio, puoi creare una funzione coerce :: a -> bche converte due tipi qualsiasi (e nella maggior parte dei casi arresta in modo anomalo il programma). Vedi questo esempio - puoi convertire anche una funzione in Intecc.
Petr Pudlák il

Un'altra monade "magica speciale" sarebbe ST, che ti consente di dichiarare riferimenti alla memoria che puoi leggere e scrivere a tuo piacimento (anche se solo all'interno della monade), e quindi puoi estrarre un risultato chiamandorunST :: (forall s. GHC.ST.ST s a) -> a
sara

5

Visualizza 1: Monade come etichetta

"Di conseguenza, questo valore Int è stato contrassegnato come valore proveniente da un processo con IO, quindi questo valore è" sporco "."

"IO Int" non è in genere un valore Int (sebbene possa essere in alcuni casi come "return 3"). È una procedura che genera un valore Int. Esecuzioni diverse di questa "procedura" possono produrre valori Int diversi.

Una monade è un "linguaggio di programmazione" incorporato (imperativo): all'interno di questo linguaggio è possibile definire alcune "procedure". Un valore monadico (di tipo ma) è una procedura in questo "linguaggio di programmazione" che genera un valore di tipo a.

Per esempio:

foo :: IO Int

è una procedura che genera un valore di tipo Int.

Poi:

bar :: IO (Int, Int)
bar = do
  a <- foo
  b <- foo
  return (a,b)

è una procedura che genera due In (possibilmente diversi).

Ogni "linguaggio" di questo tipo supporta alcune operazioni:

  • due procedure (ma e mb) possono essere "concatenate": è possibile creare una procedura più ampia (ma >> mb) fatta della prima e poi della seconda;

  • inoltre l'output (a) del primo può influire sul secondo (ma >> = \ a -> ...);

  • una procedura (ritorno x) può produrre un valore costante (x).

I diversi linguaggi di programmazione incorporata differiscono per le cose gentili che supportano come:

  • dando valori casuali;
  • "biforcazione" (la [] monade);
  • eccezioni (lancio / cattura) (The Either e monad);
  • continuazione esplicita / supporto callcc;
  • invio / ricezione di messaggi ad altri "agenti";
  • creare, impostare e leggere variabili (locali per questo linguaggio di programmazione) (la monade ST).

1

Non confondere un tipo monadico con la classe monade.

Un tipo monadico (ovvero un tipo che è un'istanza della classe monade) risolverebbe un problema particolare (in linea di principio, ogni tipo monadico ne risolve uno diverso): Stato, Casuale, Forse, IO. Tutti sono tipi con contesto (ciò che chiamate "etichetta", ma non è ciò che li rende una monade).

Per tutti loro, vi è la necessità di "concatenare operazioni con scelta" (un'operazione dipende dal risultato del precedente). Qui entra in gioco la classe monade: fai in modo che il tuo tipo (risolvendo un determinato problema) sia un'istanza della classe monade e il problema del concatenamento è risolto.

Vedi Cosa risolve la classe monade?

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.