Ai fini di questa risposta, definisco "linguaggio puramente funzionale" per indicare un linguaggio funzionale in cui le funzioni sono referenzialmente trasparenti, vale a dire che chiamare la stessa funzione più volte con gli stessi argomenti produrrà sempre gli stessi risultati. Questa è, credo, la solita definizione di un linguaggio puramente funzionale.
I linguaggi di programmazione funzionale pura non consentono effetti collaterali (e sono quindi di scarsa utilità nella pratica perché qualsiasi programma utile ha effetti collaterali, ad esempio quando interagisce con il mondo esterno).
Il modo più semplice per ottenere la trasparenza referenziale sarebbe in effetti vietare gli effetti collaterali e ci sono davvero lingue in cui questo è il caso (per lo più quelli specifici di dominio). Tuttavia non è certamente l'unico modo e i linguaggi puramente funzionali più generici (Haskell, Clean, ...) consentono effetti collaterali.
Dire anche che un linguaggio di programmazione senza effetti collaterali è poco utile in pratica non è davvero giusto, penso, certamente non per linguaggi specifici di dominio, ma anche per linguaggi di uso generale, immagino che un linguaggio possa essere abbastanza utile senza fornire effetti collaterali . Forse non per le applicazioni console, ma penso che le applicazioni GUI possano essere ben implementate senza effetti collaterali, ad esempio nel paradigma reattivo funzionale.
Per quanto riguarda il punto 1, è possibile interagire con l'ambiente in linguaggi puramente funzionali, ma è necessario contrassegnare esplicitamente il codice (funzioni) che li introduce (ad esempio in Haskell per mezzo di tipi monadici).
Questo è un po 'troppo per semplificarlo. Il semplice fatto di disporre di un sistema in cui le funzioni con effetti collaterali devono essere contrassegnate come tali (simile alla correttezza const in C ++, ma con effetti collaterali generali) non è sufficiente per garantire la trasparenza referenziale. È necessario assicurarsi che un programma non possa mai chiamare una funzione più volte con gli stessi argomenti e ottenere risultati diversi. Puoi farlo facendo cose del generereadLine
essere qualcosa che non è una funzione (è quello che Haskell fa con la monade IO) o potresti rendere impossibile chiamare più volte le funzioni con effetti collaterali con lo stesso argomento (ecco cosa fa Clean). In quest'ultimo caso, il compilatore assicurerebbe che ogni volta che chiamate una funzione con effetti collaterali, lo facciate con un nuovo argomento, e respingerebbe qualsiasi programma in cui passate lo stesso argomento a una funzione con effetti collaterali due volte.
I linguaggi di programmazione funzionale pura non consentono di scrivere un programma che mantenga lo stato (il che rende la programmazione molto imbarazzante perché in molte applicazioni è necessario lo stato).
Ancora una volta, un linguaggio puramente funzionale potrebbe benissimo impedire lo stato mutabile, ma è certamente possibile essere puro e avere ancora uno stato mutabile, se lo si implementa nello stesso modo in cui ho descritto con gli effetti collaterali sopra. Lo stato realmente mutabile è solo un'altra forma di effetti collaterali.
Detto questo, i linguaggi di programmazione funzionale scoraggiano sicuramente lo stato mutevole, specialmente quelli puri. E non penso che ciò renda la programmazione imbarazzante, al contrario. A volte (ma non tanto spesso) lo stato mutabile non può essere evitato senza perdere prestazioni o chiarezza (motivo per cui lingue come Haskell hanno strutture per lo stato mutevole), ma molto spesso può.
Se sono idee sbagliate, come sono nate?
Penso che molte persone leggano semplicemente "una funzione deve produrre lo stesso risultato quando viene chiamata con gli stessi argomenti" e ne deduco che non è possibile implementare qualcosa di simile readLine
o un codice che mantenga uno stato mutabile. Quindi semplicemente non sono consapevoli dei "trucchi" che i linguaggi puramente funzionali possono usare per introdurre queste cose senza rompere la trasparenza referenziale.
Anche lo stato mutevole è fortemente scoraggiante nei linguaggi funzionali, quindi non è affatto un salto dal presupposto che non sia permesso affatto in quelli puramente funzionali.
Potresti scrivere uno snippet di codice (possibilmente piccolo) che illustri il modo idiomatico di Haskell per (1) implementare gli effetti collaterali e (2) implementare un calcolo con stato?
Ecco un'applicazione in Pseudo-Haskell che chiede all'utente un nome e lo saluta. Lo pseudo-Haskell è un linguaggio che ho appena inventato, che ha il sistema IO di Haskell, ma usa una sintassi più convenzionale, nomi di funzioni più descrittivi e non ha do
notazione (in quanto ciò distrarrebbe da come funziona esattamente la monade IO):
greet(name) = print("Hello, " ++ name ++ "!")
main = composeMonad(readLine, greet)
L'indizio qui è che readLine
è un valore di tipo IO<String>
ed composeMonad
è una funzione che accetta un argomento di tipo IO<T>
(per un certo tipo T
) e un altro argomento che è una funzione che accetta un argomento di tipo T
e restituisce un valore di tipo IO<U>
(per un certo tipo U
). print
è una funzione che accetta una stringa e restituisce un valore di tipo IO<void>
.
Un valore di tipo IO<A>
è un valore che "codifica" una determinata azione che produce un valore di tipo A
. composeMonad(m, f)
produce un nuovo IO
valore che codifica l'azione di m
seguito dall'azione di f(x)
, dove x
è il valore prodotto eseguendo l'azione di m
.
Lo stato mutevole sarebbe simile al seguente:
counter = mutableVariable(0)
increaseCounter(cnt) =
setIncreasedValue(oldValue) = setValue(cnt, oldValue + 1)
composeMonad(getValue(cnt), setIncreasedValue)
printCounter(cnt) = composeMonad( getValue(cnt), print )
main = composeVoidMonad( increaseCounter(counter), printCounter(counter) )
Ecco mutableVariable
una funzione che prende valore di qualsiasi tipo T
e produce a MutableVariable<T>
. La funzione getValue
accetta MutableVariable
e restituisce un valore IO<T>
che produce il valore corrente. setValue
prende a MutableVariable<T>
e a T
e restituisce un IO<void>
valore che imposta il valore. composeVoidMonad
equivale ad composeMonad
eccezione del fatto che il primo argomento è un IO
che non produce un valore sensibile e il secondo argomento è un'altra monade, non una funzione che restituisce una monade.
In Haskell c'è dello zucchero sintattico, che rende meno doloroso tutto questo calvario, ma è ancora ovvio che lo stato mutevole è qualcosa che la lingua non vuole davvero che tu faccia.