L'innovazione più evidente notata dalle persone nuove a Haskell è che esiste una separazione tra il mondo impuro che si occupa di comunicare con il mondo esterno e il mondo puro di calcolo e algoritmi. Una domanda frequente per i principianti è "Come posso liberarmi IO
, ovvero convertirmi IO a
in a
?" Il modo per farlo è usare monadi (o altre astrazioni) per scrivere codice che esegua effetti IO e catene. Questo codice raccoglie dati dal mondo esterno, ne crea un modello, esegue alcuni calcoli, possibilmente impiegando codice puro, e genera il risultato.
Per quanto riguarda il modello sopra, non vedo nulla di terribilmente sbagliato nel manipolare le GUI nella IO
monade. Il problema più grande che deriva da questo stile è che i moduli non sono più compostabili, cioè perdo la maggior parte delle mie conoscenze sull'ordine di esecuzione globale delle istruzioni nel mio programma. Per recuperarlo, devo applicare un ragionamento simile a quello del codice GUI imperativo e simultaneo. Nel frattempo, per il codice impuro, non GUI, l'ordine di esecuzione è ovvio a causa della definizione dell'operatore della IO
monade >==
(almeno fintanto che esiste un solo thread). Per il codice puro, non importa affatto, tranne in casi angolari per aumentare le prestazioni o evitare le risultanti valutazioni ⊥
.
La più grande differenza filosofica tra console e IO grafico è che i programmi che implementano il primo sono generalmente scritti in stile sincrono. Ciò è possibile perché esiste (lasciando da parte segnali e altri descrittori di file aperti) solo una fonte di eventi: il flusso di byte comunemente chiamato stdin
. Le GUI sono intrinsecamente asincrone e devono reagire agli eventi della tastiera e ai clic del mouse.
Una filosofia popolare di fare l'IO asincrono in modo funzionale si chiama Programmazione reattiva funzionale (FRP). Recentemente ha avuto molta trazione in linguaggi impuri e non funzionali grazie a librerie come ReactiveX e framework come Elm. In breve, è come visualizzare elementi della GUI e altre cose (come file, orologi, allarmi, tastiera, mouse) come fonti di eventi, chiamate "osservabili", che emettono flussi di eventi. Questi eventi vengono combinate utilizzando gli operatori familiari come map
, foldl
, zip
, filter
, concat
, join
, ecc, per la produzione di nuovi flussi. Questo è utile perché lo stato del programma stesso può essere visto come scanl . map reactToEvents $ zipN <eventStreams>
del programma, dove N
è uguale al numero di osservabili mai considerati dal programma.
Lavorare con gli osservabili FRP consente di recuperare la componibilità perché gli eventi in uno stream sono ordinati in tempo. Il motivo è che l'astrazione del flusso di eventi consente di visualizzare tutti gli oggetti osservabili come scatole nere. In definitiva, la combinazione di flussi di eventi con operatori restituisce alcuni ordini locali in fase di esecuzione. Questo mi obbliga ad essere molto più onesto su quali invarianti si basa il mio programma, in modo simile al modo in cui tutte le funzioni di Haskell devono essere referenzialmente trasparenti: se voglio estrarre dati da un'altra parte del mio programma, devo essere esplicito ad dichiarare un tipo appropriato per le mie funzioni. (La monade IO, essendo un linguaggio specifico del dominio per la scrittura di codice impuro, aggira efficacemente questo)