Given immutability (which is often encouraged and said to be one of building blocks of functional programming) and CQS (which says that commands should not return a value other than void/unit), how do these work together?
Non ci sono comandi nella programmazione funzionale. Periodo.
Il risultato di una funzione può dipendere solo dai suoi argomenti, e deve essere lo stesso ogni volta che si chiama la funzione con gli stessi argomenti. Non ci possono essere mutazioni né effetti collaterali. Le funzioni devono essere referenzialmente trasparenti, il che significa che posso sempre sostituire l'applicazione della funzione con il suo risultato ovunque nel programma senza cambiare il significato del programma. (In particolare, questo significa che se ho una procedura che restituisce void
(cioè niente), posso sostituirla con niente ovunque, che ovviamente cambierà il significato del programma, e quindi una procedura non può esistere nella programmazione funzionale.)
Ovviamente, qualcosa come un comando deve essere ancora realizzabile, altrimenti i tuoi programmi sarebbero piuttosto noiosi. Come hanno detto Simon Peyton-Jones durante l'introduzione a Haskell, se il tuo programma non ha effetti collaterali, non può stampare un risultato, non può chiedere input, non può interagire con la rete o il file system, tutto ciò che fa è il calore la CPU, dopo la quale qualcuno del pubblico ha interposto che il riscaldamento della CPU è anche un effetto collaterale, quindi, tecnicamente parlando, se vuoi che il tuo programma non abbia effetti collaterali, non puoi nemmeno eseguirlo. Chiaramente, è ridicolo.
Quindi, gli effetti collaterali devono essere modellati in qualche modo.
Un modo per farlo è immaginare di poter descrivere in qualche modo lo stato dell'intero universo in una struttura dati e quindi passare un'istanza di quella struttura di dati che descrive lo stato corrente del mondo in un funzione, e la funzione restituisce quindi una nuova istanza di tale infrastruttura che descrive il nuovo stato del mondo. Naturalmente, questo non può letteralmente lavorare, descrivere lo stato del mondo richiede almeno la stessa quantità di memoria di tutto il mondo. È un modello concettuale.
C'è un piccolo problema: la funzione potrebbe passare il vecchio stato del mondo a un'altra funzione, ad esempio, ora ci sono due funzioni che hanno lo stesso stato del mondo, ed entrambe potrebbero restituire un nuovo stato diverso del mondo. Oppure, una funzione potrebbe aggrapparsi al vecchio stato del mondo, e in seguito tornare indietro, viaggiando in modo efficace nel tempo nel passato. Certo, questo deve essere in qualche modo proibito. Una soluzione sarebbe costituita da tipi lineari, che sono tipi che assicurano che i suoi valori vengano utilizzati una sola volta. Pulito fa questo, ha tipi di mondo, che sono un tipo speciale di tipi lineari per tali valori mondiali.
In Haskell, tutti gli effetti collaterali devono aver luogo in una monade. Le Monade sono un modo per aumentare il calcolo con una struttura aggiuntiva. Potete immaginarlo come la monade che trasmette il valore del mondo descritto sopra attraverso le diverse funzioni, ma senza mai esporre direttamente il valore alle funzioni. (In realtà, una volta che si dispone di I / O monodico, non si ha più bisogno del valore del mondo, la monade stessa fornisce la struttura per gli effetti collaterali.) I precedenti tentativi di modellazione I / O in Haskell includevano l'I / O basato su flussi pigri e I / O basati su continuazione, che funzionano anche, ma è stato trovato che l'I / O monadico è più facile da utilizzare.
In tutti questi modelli diversi, gli effetti collaterali verranno sempre visualizzati nei tipi di funzioni di effetti collaterali. IOW, i tuoi comandi ora sono funzioni che restituiscono, accettano un argomento, o entrambi, un valore di un tipo a effetto collaterale.
Un bell'effetto collaterale (hah!) di questo è che diversi tipi di effetti collaterali possono ottenere i propri tipi, invece di avere un tipo "tutto questo è un effetto collaterale". Ad esempio, in Haskell, esiste il ben noto tipo IO
, che in pratica dice "tutto può succedere". Ma ci sono anche tipi come Reader
(legge lo stato globale, un esempio OO sarebbe un oggetto di configurazione globale Singleton che è immesso dalla dipendenza), Writer
(scrive lo stato globale, ad esempio un logger, di nuovo, tipicamente risolto in OO con un oggetto logger globale singleton che viene iniettato), State
(stato condiviso mutevole, ma nessun altro tipo di effetto collaterale), STM
( Memoria transazionale software , come State
ma transazionale per parallelismo).
La porzione "separazione" di CQS viene raggiunta utilizzando il sistema di tipi. I comandi hanno semplicemente tipi incompatibili con le query. Cioè un "comando" che cambia qualche intero globale avrà State Int
da qualche parte nel suo tipo, un comando che stampa un intero avrà tipo Int -> IO Int
e così via.