Grazie alla valutazione pigra, un programma Haskell non lo fa (quasi non può ) fare quello che sembra.
Considera questo programma:
main = putStrLn (show (quicksort [8, 6, 7, 5, 3, 0, 9]))
In una lingua desiderosa, prima quicksort
correva, poi show
, poiputStrLn
. Gli argomenti di una funzione vengono calcolati prima che la funzione inizi a essere eseguita.
In Haskell è l'opposto. La funzione inizia a essere eseguita per prima. Gli argomenti vengono calcolati solo quando la funzione li utilizza effettivamente. E un argomento composto, come un elenco, viene calcolato un pezzo alla volta, man mano che viene utilizzato ogni pezzo di esso.
Quindi la prima cosa che accade in questo programma è che putStrLn
inizia a funzionare.
L'implementazione di GHCputStrLn
funziona copiando i caratteri dell'argomento String in un buffer di output. Ma quando entra in questo ciclo, show
non è ancora stato eseguito. Pertanto, quando va a copiare il primo carattere dalla stringa, Haskell valuta la frazione di show
e quicksort
chiama necessarie per calcolare quel carattere . Quindi putStrLn
passa al personaggio successivo. Quindi l'esecuzione di tutte e tre le funzioni putStrLn
- show
, e quicksort
- è intercalata. quicksort
viene eseguito in modo incrementale, lasciando un grafico di thunk non valutati mentre va a ricordare dove è stato interrotto.
Ora questo è molto diverso da quello che potresti aspettarti se hai familiarità, sai, con qualsiasi altro linguaggio di programmazione. Non è facile visualizzare come quicksort
si comporta effettivamente in Haskell in termini di accessi alla memoria o anche l'ordine dei confronti. Se potessi solo osservare il comportamento, e non il codice sorgente, non riconosceresti cosa sta facendo come un quicksort .
Ad esempio, la versione C di quicksort partiziona tutti i dati prima della prima chiamata ricorsiva. Nella versione Haskell, il primo elemento del risultato verrà calcolato (e potrebbe anche apparire sullo schermo) prima che la prima partizione sia terminata, anzi prima che venga fatto qualsiasi lavoro greater
.
PS Il codice Haskell sarebbe più simile a quicksort se facesse lo stesso numero di confronti come quicksort; il codice come scritto fa il doppio dei confronti perché lesser
e greater
sono specificati per essere calcolati in modo indipendente, facendo due scansioni lineari attraverso l'elenco. Ovviamente è possibile in linea di principio che il compilatore sia abbastanza intelligente da eliminare i confronti extra; oppure il codice potrebbe essere modificato per essere utilizzatoData.List.partition
.
PPS Il classico esempio di algoritmi Haskell che si rivelano non comportarsi come ci si aspettava è il setaccio di Eratostene per il calcolo dei numeri primi.