La ragione principale è che la trasparenza referenziale (e ancor più la pigrizia) si sottrae all'ordine di esecuzione. Ciò rende banale parallelizzare la valutazione.
Ad esempio, se sia a
, b
e ||
sono referenzialmente trasparenti, non importa se in
a || b
a
viene valutato per primo, b
viene valutato per primo, o b
non viene valutato affatto (perché a
è stato valutato a true
).
In
a || a
non importa se a
viene valutato una o due volte (o, diamine, anche 5 volte ... che non avrebbe senso, ma non importa comunque).
Quindi, se non importa in quale ordine vengono valutati e non importa se vengono valutati inutilmente, allora puoi semplicemente valutare ogni sottoespressione in parallelo. Quindi, potremmo valutare a
e b
in parallelo, e quindi, ||
potrebbe aspettare che uno dei due thread finisca, guarda cosa ha restituito, e se restituisce true
, potrebbe persino annullare l'altro e restituisce immediatamente true
.
Ogni sottoespressione può essere valutata in parallelo. Banalmente.
Nota, tuttavia, che questa non è una bacchetta magica. Alcune delle prime versioni sperimentali di GHC hanno fatto questo, ed è stato un disastro: c'era solo troppo parallelismo potenziale. Anche un semplice programma può generare centinaia, migliaia, milioni di thread e per la stragrande maggioranza delle sottoespressioni che generano il thread richiede molto più tempo rispetto alla valutazione dell'espressione in primo luogo. Con così tanti thread, il tempo di commutazione del contesto domina completamente qualsiasi computazione utile.
Si potrebbe dire che la programmazione funzionale capovolge il problema: in genere, il problema è come separare un programma seriale nella giusta dimensione di "blocchi" paralleli, mentre con la programmazione funzionale, il problema è come raggruppare insieme i sottoprogrammi paralleli in "blocchi" seriali.
Il modo in cui GHC lo fa oggi è che puoi annotare manualmente due sottoespressioni da valutare in parallelo. Questo è in realtà simile a come lo faresti anche in un linguaggio imperativo, mettendo le due espressioni in thread separati. Ma c'è una differenza importante: l'aggiunta di questa annotazione non può mai cambiare il risultato del programma! Può renderlo più veloce, può renderlo più lento, può farne usare più memoria, ma non può cambiare il suo risultato. In questo modo modo è più facile sperimentare con il parallelismo per trovare la giusta quantità di parallelismo e la giusta dimensione dei blocchi.