Dzięki leniwej ocenie program Haskell nie (prawie nie może ) robić tego, na co wygląda.
Rozważ ten program:
main = putStrLn (show (quicksort [8, 6, 7, 5, 3, 0, 9]))
W gorliwym języku najpierw quicksort
biegał show
, potemputStrLn
. Argumenty funkcji są obliczane przed uruchomieniem tej funkcji.
W Haskell jest odwrotnie. Funkcja jest uruchamiana jako pierwsza. Argumenty są obliczane tylko wtedy, gdy funkcja faktycznie ich używa. Argument złożony, taki jak lista, jest obliczany po jednym fragmencie, gdy jest używany.
Więc pierwszą rzeczą, która dzieje się w tym programie, jest to, że putStrLn
zaczyna działać.
Implementacja GHCputStrLn
działa poprzez kopiowanie znaków argumentu String do bufora wyjściowego. Ale kiedy wchodzi w tę pętlę, show
jeszcze nie działa. Dlatego też, kiedy zamierza skopiować pierwszy znak z ciągu, Haskell ocenia ułamek wywołań show
i quicksort
potrzebnych do obliczenia tego znaku . Następnie putStrLn
przechodzi do następnego znaku. Zatem wykonanie wszystkich trzech funkcji putStrLn
- show
, i quicksort
- jest przeplatane. quicksort
wykonuje się przyrostowo, pozostawiając wykres nieocenionych fragmentów w miarę zapamiętywania miejsca, w którym zostało przerwane.
To bardzo różni się od tego, czego można by się spodziewać, jeśli znasz, wiesz, jakikolwiek inny język programowania. Nie jest łatwo wyobrazić sobie, jak quicksort
faktycznie zachowuje się Haskell pod względem dostępu do pamięci, a nawet kolejności porównań. Gdybyś mógł tylko obserwować zachowanie, a nie kod źródłowy, nie rozpoznałbyś tego, co robi jako szybki sort .
Na przykład wersja C quicksort dzieli na partycje wszystkie dane przed pierwszym wywołaniem rekurencyjnym. W wersji Haskell, pierwszy element wyniku zostanie obliczony (i może nawet pojawić się na ekranie) przed zakończeniem działania pierwszej partycji - a właściwie przed wykonaniem jakiejkolwiek pracy greater
.
PS Kod Haskell byłby bardziej podobny do szybkiego sortowania, gdyby wykonywał taką samą liczbę porównań jak w przypadku szybkiego sortowania; napisany kod wykonuje dwa razy więcej porównań, ponieważ lesser
i greater
są określone do obliczania niezależnie, wykonując dwa liniowe skanowanie listy. Oczywiście w zasadzie kompilator może być wystarczająco inteligentny, aby wyeliminować dodatkowe porównania; lub kod można zmienić do użycia Data.List.partition
.
PPS Klasycznym przykładem algorytmów Haskella, które okazały się nie zachowywać się zgodnie z oczekiwaniami, jest sito Eratostenesa do obliczania liczb pierwszych.