Semplicemente non capisco quale problema risolvono.
Semplicemente non capisco quale problema risolvono.
Risposte:
Le monadi non sono né buone né cattive. Lo sono e basta. Sono strumenti che vengono utilizzati per risolvere problemi come molti altri costrutti di linguaggi di programmazione. Un'applicazione molto importante è quella di rendere la vita più facile per i programmatori che lavorano in un linguaggio puramente funzionale. Ma sono utili in linguaggi non funzionali; è solo che le persone raramente si rendono conto che stanno usando una Monade.
Che cos'è una monade? Il modo migliore per pensare a una Monade è come un modello di progettazione. Nel caso dell'I / O, probabilmente potresti pensare che sia poco più di una pipeline glorificata in cui lo stato globale è ciò che viene passato tra le fasi.
Ad esempio, prendiamo il codice che stai scrivendo:
do
putStrLn "What is your name?"
name <- getLine
putStrLn ("Nice to meet you, " ++ name ++ "!")
C'è molto di più da fare qui di quanto non sembri. Per esempio, si noterà che putStrLn
ha la seguente firma: putStrLn :: String -> IO ()
. Perchè è questo?
Pensaci in questo modo: facciamo finta (per semplicità) che stdout e stdin siano gli unici file su cui possiamo leggere e scrivere. In un linguaggio imperativo, questo non è un problema. Ma in un linguaggio funzionale, non puoi mutare lo stato globale. Una funzione è semplicemente qualcosa che accetta un valore (o valori) e restituisce un valore (o valori). Un modo per aggirare questo è usare lo stato globale come valore che viene passato dentro e fuori da ogni funzione. Quindi potresti tradurre la prima riga di codice in qualcosa del genere:
global_state <- (\(stdin, stdout) -> (stdin, stdout ++ "What is your name?")) global_state
... e il compilatore saprebbe stampare tutto ciò che è stato aggiunto al secondo elemento di global_state
. Ora non so te, ma odierei programmare così. Il modo in cui ciò è stato reso più semplice è stato l'uso delle Monadi. In una Monade, passi un valore che rappresenta un tipo di stato da un'azione all'altra. Questo è il motivo per cui putStrLn
ha un tipo di ritorno IO ()
: sta restituendo il nuovo stato globale.
Allora perché ti importa? Bene, i vantaggi della programmazione funzionale rispetto al programma imperativo sono stati discussi a morte in diversi punti, quindi non risponderò a questa domanda in generale (ma vedi questo documento se vuoi ascoltare il caso della programmazione funzionale). Per questo caso specifico, tuttavia, potrebbe essere utile capire cosa Haskell sta cercando di realizzare.
Molti programmatori pensano che Haskell cerchi di impedire loro di scrivere codice imperativo o usare effetti collaterali. Questo non è del tutto vero. Pensaci in questo modo: un linguaggio imperativo è uno che consente effetti collaterali per impostazione predefinita, ma ti consente di scrivere codice funzionale se lo desideri (e sei disposto a gestire alcune delle contorsioni che richiederebbero). Haskell è puramente funzionale per impostazione predefinita, ma ti consente di scrivere codice imperativo se lo desideri (cosa che fai se il tuo programma deve essere utile). Il punto non è rendere difficile la scrittura di codice con effetti collaterali. È per assicurarti di essere esplicito sull'avere effetti collaterali (con il sistema dei tipi che lo impone).
goto
(come argomento per la programmazione strutturata) un po 'più tardi nel documento, caratterizzando tali argomenti come "infruttuosi". Eppure nessuno di noi desidera segretamente goto
il ritorno. È semplicemente che non si può sostenere che goto
non è necessario per le persone che lo usano ampiamente.
Mi mordo !!! Le monadi da sole non sono in realtà una ragion d'essere per Haskell (le prime versioni di Haskell non le avevano nemmeno).
La tua domanda è un po 'come dire "C ++ quando guardo la sintassi, mi annoio così tanto. Ma i modelli sono una caratteristica molto pubblicizzata di C ++, quindi ho visto un'implementazione in qualche altro linguaggio".
L'evoluzione di un programmatore Haskell è uno scherzo, non è pensato per essere preso sul serio.
Una Monade ai fini di un programma in Haskell è un'istanza della classe di tipo Monad, vale a dire, è un tipo che capita a supporto di un certo piccolo insieme di operazioni. Haskell ha un supporto speciale per i tipi che implementano la classe di tipo Monad, in particolare il supporto sintattico. Praticamente ciò che ne risulta è ciò che è stato definito un "punto e virgola programmabile". Quando si combina questa funzionalità con alcune delle altre funzionalità di Haskell (funzioni di prima classe, pigrizia di default) ciò che si ottiene è la capacità di implementare determinate cose come librerie tradizionalmente considerate funzionalità linguistiche. Ad esempio, è possibile implementare un meccanismo di eccezione. È possibile implementare il supporto per continuazioni e coroutine come libreria. Haskell, la lingua non supporta le variabili mutabili:
Chiedete "Forse / Identità / Monadi della divisione sicura ???". La monade Maybe è un esempio di come è possibile implementare (molto semplice, solo un'eccezione) la gestione delle eccezioni come libreria.
Hai ragione, scrivere messaggi e leggere l'input dell'utente non è univoco. IO è un pessimo esempio di "monadi come caratteristica".
Ma per iterare, una "caratteristica" da sola (ad es. Monadi) in isolamento dal resto della lingua non appare necessariamente immediatamente utile (una nuova grande funzionalità di C ++ 0x sono riferimenti di valore, non significa che puoi prendere li fuori dal contesto C ++ perché la sua sintassi ti annoia e necessariamente vede l'utilità). Un linguaggio di programmazione non è qualcosa che si ottiene gettando un sacco di funzioni in un secchio.
Tutti i programmatori scrivono programmi, ma le somiglianze finiscono qui. Penso che i programmatori differiscano molto più di quanto la maggior parte dei programmatori possa immaginare. Prendi qualsiasi "battaglia" di vecchia data, come la digitazione di variabili statiche vs tipi di solo runtime, script vs compilato, stile C vs orientato agli oggetti. Scoprirai impossibile argomentare razionalmente che un campo è inferiore, perché alcuni sfornano codice eccellente in un sistema di programmazione che mi sembra inutile o addirittura del tutto inutile.
Penso che persone diverse la pensino diversamente, e se non sei tentato dallo zucchero sintattico o soprattutto dalle astrazioni che esistono solo per comodità e che in realtà hanno un costo di runtime significativo, allora stai lontano da tali lingue.
Ti consiglierei comunque di provare almeno a familiarizzare con i concetti a cui ti stai arrendendo. Non ho nulla contro qualcuno che è veemente pro-puro-C, a patto che capiscano davvero qual è il grosso problema delle espressioni lambda. Sospetto che la maggior parte non diventerà immediatamente un fan, ma almeno sarà lì in fondo alle loro menti quando troveranno il problema perfetto quale sarebbe stato oh molto più facile da risolvere con lambda.
E, soprattutto, cerca di evitare di essere infastidito dai fanboy, soprattutto da persone che non sanno di cosa stanno parlando.
Haskell applica la Trasparenza referenziale : dati gli stessi parametri, ogni funzione restituisce sempre lo stesso risultato, indipendentemente da quante volte la chiamate.
Ciò significa, ad esempio, che su Haskell (e senza Monadi) non è possibile implementare un generatore di numeri casuali. In C ++ o Java puoi farlo usando variabili globali, memorizzando il valore intermedio "seed" del generatore casuale.
Su Haskell la controparte delle variabili globali sono Monadi.
Random
nell'oggetto.
Una specie di vecchia domanda, ma è davvero una buona domanda, quindi risponderò.
Puoi pensare alle monadi come blocchi di codice per i quali hai il controllo completo su come vengono eseguiti: cosa dovrebbe restituire ogni riga di codice, se l'esecuzione dovrebbe fermarsi in qualsiasi momento, se qualche altra elaborazione dovrebbe avvenire tra ciascuna riga.
Darò alcuni esempi di cose che le monadi consentono che altrimenti sarebbe difficile. Nessuno di questi esempi è in Haskell, solo perché la mia conoscenza di Haskell è un po 'traballante, ma sono tutti esempi di come Haskell abbia ispirato l'uso delle monadi.
parser
Normalmente, se volessi scrivere un parser di qualche tipo, dire di implementare un linguaggio di programmazione, dovresti leggere la specifica BNF e scrivere un sacco di codice loopy per analizzarlo, o dovresti usare un compilatore del compilatore come Flex, Bison, Yacc ecc. Ma con le monadi, puoi creare una specie di "compilatore di compilatori" proprio in Haskell.
I parser non possono davvero essere fatti senza monadi o linguaggi speciali come yacc, bisonte ecc.
Ad esempio, ho preso la specifica del linguaggio BNF per il protocollo IRC :
message = [ ":" prefix SPACE ] command [ params ] crlf
prefix = servername / ( nickname [ [ "!" user ] "@" host ] )
command = 1*letter / 3digit
params = *14( SPACE middle ) [ SPACE ":" trailing ]
=/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]
nospcrlfcl = %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
; any octet except NUL, CR, LF, " " and ":"
middle = nospcrlfcl *( ":" / nospcrlfcl )
trailing = *( ":" / " " / nospcrlfcl )
SPACE = %x20 ; space character
crlf = %x0D %x0A ; "carriage return" "linefeed"
E lo ho ridotto a circa 40 righe di codice in F # (che è un'altra lingua che supporta le monadi):
type UserIdentifier = { Name : string; User: string; Host: string }
type Message = { Prefix : UserIdentifier option; Command : string; Params : string list }
let space = character (char 0x20)
let parameters =
let middle = parser {
let! c = sat <| fun c -> c <> ':' && c <> (char 0x20)
let! cs = many <| sat ((<>)(char 0x20))
return (c::cs)
}
let trailing = many item
let parameter = prefixed space ((prefixed (character ':') trailing) +++ middle)
many parameter
let command = atLeastOne letter +++ (count 3 digit)
let prefix = parser {
let! name = many <| sat (fun c -> c <> '!' && c <> '@' && c <> (char 0x20)) //this is more lenient than RFC2812 2.3.1
let! uh = parser {
let! user = maybe <| prefixed (character '!') (many <| sat (fun c -> c <> '@' && c <> (char 0x20)))
let! host = maybe <| prefixed (character '@') (many <| sat ((<>) ' '))
return (user, host)
}
let nullstr = function | Some([]) -> null | Some(s) -> charsString s | _ -> null
return { Name = charsString name; User = nullstr (fst uh); Host = nullstr (snd uh) }
}
let message = parser {
let! p = maybe (parser {
let! _ = character ':'
let! p = prefix
let! _ = space
return p
})
let! c = command
let! ps = parameters
return { Prefix = p; Command = charsString c; Params = List.map charsString ps }
}
La sintassi della monade di F # è piuttosto brutta rispetto a quella di Haskell e probabilmente avrei potuto migliorarlo un po ', ma il punto da portare a casa è che strutturalmente, il codice parser è identico al BNF. Non solo questo avrebbe richiesto molto più lavoro senza le monadi (o un generatore di parser), ma non avrebbe avuto quasi nessuna somiglianza con le specifiche, e quindi sarebbe stato terribile sia da leggere che da mantenere.
Multitasking personalizzato
Normalmente, il multitasking è pensato come una funzionalità del sistema operativo - ma con le monadi, è possibile scrivere il proprio scheduler in modo tale che dopo ogni monade delle istruzioni, il programma passerebbe il controllo allo scheduler, che quindi sceglierebbe un'altra monade da eseguire.
Un ragazzo ha creato una monade "compito" per controllare i loop di gioco (sempre in F #), in modo che invece di dover scrivere tutto come una macchina a stati che agisca su ogni Update()
chiamata, potrebbe semplicemente scrivere tutte le istruzioni come se fossero un'unica funzione .
In altre parole, invece di dover fare qualcosa del genere:
class Robot
{
enum State { Walking, Shooting, Stopped }
State state = State.Stopped;
public void Update()
{
switch(state)
{
case State.Stopped:
Walk();
state = State.Walking;
break;
case State.Walking:
if (enemyInSight)
{
Shoot();
state = State.Shooting;
}
break;
}
}
}
Potresti fare qualcosa del tipo:
let robotActions = task {
while (not enemyInSight) do
Walk()
while (enemyInSight) do
Shoot()
}
LINQ to SQL
LINQ to SQL è in realtà un esempio di monade e funzionalità simili potrebbero essere facilmente implementate in Haskell.
Non entrerò nei dettagli poiché non ricordo tutto così accuratamente, ma Erik Meijer lo spiega abbastanza bene .
Se hai familiarità con i modelli GoF, le monadi sono come il modello Decorator e il modello Builder messi insieme, su steroidi, morsi da un tasso radioattivo.
Ci sono risposte migliori sopra, ma alcuni dei vantaggi specifici che vedo sono:
le monadi decorano un tipo di nucleo con proprietà aggiuntive senza cambiare il tipo di nucleo. Ad esempio, una monade potrebbe "sollevare" la stringa e aggiungere valori come "isWellFormed", "isProfanity" o "isPalindrome" ecc.
allo stesso modo, le monadi consentono di conglomerare un tipo semplice in un tipo di raccolta
le monadi consentono l'associazione tardiva delle funzioni a questo spazio di ordine superiore
le monadi consentono di mescolare funzioni e argomenti arbitrari con un tipo di dati arbitrario, nello spazio di ordine superiore
le monadi consentono di fondere funzioni pure e apolidi con una base impura e piena di stato, in modo da poter tenere traccia di dove si trova il problema
Un esempio familiare di monade in Java è Elenco. Prende alcune classi principali, come String, e le "solleva" nello spazio monade di List, aggiungendo informazioni sull'elenco. Quindi lega nuove funzioni in quello spazio come get (), getFirst (), add (), empty (), ecc.
Su larga scala, immagina che invece di scrivere un programma, hai appena scritto un grosso Builder (come il modello GoF), e il metodo build () alla fine sputava qualsiasi risposta il programma avrebbe dovuto produrre. E che potresti aggiungere nuovi metodi a ProgramBuilder senza ricompilare il codice originale. Ecco perché le monadi sono un potente modello di design.