Se i linguaggi di programmazione funzionale non possono salvare alcuno stato, come fanno alcune cose semplici come leggere l'input di un utente (intendo dire come lo "memorizzano") o memorizzano i dati per quella materia?
Come hai capito, la programmazione funzionale non ha uno stato, ma ciò non significa che non possa memorizzare dati. La differenza è che se scrivo un'affermazione (Haskell) sulla falsariga di
let x = func value 3.14 20 "random"
in ...
Sono garantito che il valore di x
è sempre lo stesso in ...
: nulla può eventualmente cambiarlo. Allo stesso modo, se ho una funzione f :: String -> Integer
(una funzione che prende una stringa e restituisce un numero intero), posso essere certo che f
non modificherà il suo argomento, né cambierà alcuna variabile globale, né scriverà dati su un file e così via. Come ha detto sepp2k in un commento sopra, questa non mutabilità è davvero utile per ragionare sui programmi: scrivi funzioni che piegano, mandano e mutilano i tuoi dati, restituendo nuove copie in modo da poterle concatenare insieme, e puoi essere sicuro che nessuna di quelle chiamate di funzione possono fare qualsiasi cosa "dannosa". Sai che x
è sempre x
, e non devi preoccuparti che qualcuno abbia scritto x := foo bar
da qualche parte tra la dichiarazione dix
e il suo utilizzo, perché è impossibile.
Ora, cosa succede se voglio leggere l'input di un utente? Come ha detto KennyTM, l'idea è che una funzione impura sia una funzione pura che viene passata al mondo intero come argomento e restituisce sia il suo risultato che il mondo. Certo, non vuoi farlo davvero: per prima cosa, è orribilmente goffo, e per un altro, cosa succede se riutilizzo lo stesso oggetto del mondo? Quindi questo viene astratto in qualche modo. Haskell lo gestisce con il tipo IO:
main :: IO ()
main = do str <- getLine
let no = fst . head $ reads str :: Integer
...
Questo ci dice che main
è un'azione IO che non restituisce nulla; eseguire questa azione è ciò che significa eseguire un programma Haskell. La regola è che i tipi di I / O non possono mai sfuggire a un'azione I / O; in questo contesto, introduciamo tale azione utilizzando do
. Pertanto, getLine
restituisce un IO String
, che può essere pensato in due modi: primo, come un'azione che, quando eseguita, produce una stringa; secondo, come una stringa "contaminata" da IO poiché è stata ottenuta in modo impuro. Il primo è più corretto, ma il secondo può essere più utile. Il <-
prende il String
fuori IO String
e lo memorizza in str
-ma visto che siamo in un'azione IO, dovremo avvolgerlo il backup, in modo da non può "fuga". La riga successiva tenta di leggere un numero intero ( reads
) e acquisisce la prima corrispondenza riuscita (fst . head
); questo è tutto puro (no IO), quindi gli diamo un nome con let no = ...
. Possiamo quindi utilizzare sia no
e str
in ...
. Abbiamo quindi memorizzato dati impuri (da getLine
into str
) e dati puri ( let no = ...
).
Questo meccanismo per lavorare con l'IO è molto potente: ti consente di separare la parte pura e algoritmica del tuo programma dal lato impuro dell'interazione con l'utente e di applicarlo a livello di tipo. La tua minimumSpanningTree
funzione non può cambiare qualcosa da qualche altra parte nel tuo codice, o scrivere un messaggio per il tuo utente e così via. É sicuro.
Questo è tutto ciò che devi sapere per utilizzare IO in Haskell; se è tutto quello che vuoi, puoi fermarti qui. Ma se vuoi capire perché funziona, continua a leggere. (E nota che questa roba sarà specifica per Haskell: altre lingue potrebbero scegliere un'implementazione diversa.)
Quindi questo probabilmente sembrava un po 'un trucco, in qualche modo aggiungendo impurità al puro Haskell. Ma non lo è: si scopre che possiamo implementare il tipo di I / O interamente all'interno di Haskell puro (purché ci venga fornito il RealWorld
). L'idea è questa: un'azione IO IO type
è la stessa di una funzione RealWorld -> (type, RealWorld)
, che prende il mondo reale e restituisce sia un oggetto di tipo type
che quello modificato RealWorld
. Definiamo quindi un paio di funzioni in modo da poter utilizzare questo tipo senza impazzire:
return :: a -> IO a
return a = \rw -> (a,rw)
(>>=) :: IO a -> (a -> IO b) -> IO b
ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'
Il primo ci permette di parlare di azioni IO che non fanno nulla: return 3
è un'azione IO che non interroga il mondo reale e si limita a restituire 3
. L' >>=
operatore, pronunciato "bind", ci permette di eseguire azioni IO. Estrae il valore dall'azione IO, lo passa e il mondo reale attraverso la funzione e restituisce l'azione IO risultante. Nota che >>=
applica la nostra regola secondo cui i risultati delle azioni IO non possono mai sfuggire.
Possiamo quindi trasformare quanto sopra main
nel seguente insieme ordinario di applicazioni di funzioni:
main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...
Il runtime Haskell inizia main
con l'iniziale RealWorld
e siamo pronti! Tutto è puro, ha solo una sintassi stravagante.
[ Modifica: come sottolinea @Conal , questo non è in realtà ciò che Haskell usa per fare IO. Questo modello si interrompe se si aggiunge la concorrenza, o addirittura un modo in cui il mondo cambia nel bel mezzo di un'azione di I / O, quindi sarebbe impossibile per Haskell utilizzare questo modello. È accurato solo per il calcolo sequenziale. Quindi, può essere che l'IO di Haskell sia un po 'una schivata; anche se non lo è, non è certo così elegante. L'osservazione di Per @ Conal, guarda cosa dice Simon Peyton-Jones in Tackling the Awkward Squad [pdf] , sezione 3.1; presenta quello che potrebbe equivalere a un modello alternativo lungo queste linee, ma poi lo abbandona per la sua complessità e prende una strada diversa.]
Di nuovo, questo spiega (più o meno) come l'IO e la mutabilità in generale funzionano in Haskell; se questo è tutto ciò che vuoi sapere, puoi smettere di leggere qui. Se vuoi un'ultima dose di teoria, continua a leggere, ma ricorda, a questo punto, siamo andati molto lontano dalla tua domanda!
Quindi l'ultima cosa: risulta che questa struttura - un tipo parametrico con return
e >>=
- è molto generale; si chiama monade, e do
notazione return
, e >>=
funziona con ognuna di esse. Come hai visto qui, le monadi non sono magiche; tutto ciò che è magico è che i do
blocchi si trasformano in chiamate di funzione. Il RealWorld
tipo è l'unico posto in cui vediamo la magia. Anche tipi come []
il costruttore della lista sono monadi e non hanno nulla a che fare con il codice impuro.
Ora sai (quasi) tutto sul concetto di monade (tranne alcune leggi che devono essere soddisfatte e la definizione matematica formale), ma ti manca l'intuizione. Ci sono un numero ridicolo di tutorial sulle monadi online; Mi piace questo , ma hai delle opzioni. Tuttavia, questo probabilmente non ti aiuterà ; l'unico vero modo per ottenere l'intuizione è attraverso una combinazione di utilizzarli e leggere un paio di tutorial al momento giusto.
Tuttavia, non hai bisogno di quell'intuizione per capire IO . Comprendere le monadi in piena generalità è la ciliegina sulla torta, ma puoi usare IO adesso. Potresti usarlo dopo che ti ho mostrato la prima main
funzione. Puoi anche trattare il codice IO come se fosse in un linguaggio impuro! Ma ricorda che c'è una rappresentazione funzionale sottostante: nessuno bara.
(PS: scusa per la lunghezza. Sono andato un po 'lontano.)