Благодаря ленивой оценке программа на Haskell не (почти не может ) делать то, что выглядит так.
Рассмотрим эту программу:
main = putStrLn (show (quicksort [8, 6, 7, 5, 3, 0, 9]))
На нетерпеливом языке сначала запускается quicksort
, затем show
, затем putStrLn
. Аргументы функции вычисляются до ее запуска.
В Хаскеле все наоборот. Функция запускается первой. Аргументы вычисляются только тогда, когда функция фактически использует их. И составной аргумент, такой как список, вычисляется по одному фрагменту за раз, так как каждый его фрагмент используется.
Итак, первая вещь, которая происходит в этой программе, заключается в том, что putStrLn
начинает работать.
Реализация GHC putStrLn
работает путем копирования символов аргумента String в выходной буфер. Но когда он входит в этот цикл, show
еще не запущен. Следовательно, когда он копирует первый символ из строки, Haskell вычисляет долю вызовов show
и quicksort
, необходимых для вычисления этого символа . Затем putStrLn
переходит к следующему персонажу. Таким образом, выполнение всех трех функций & mdash; putStrLn
, show
и quicksort
& mdash; чередуется quicksort
выполняется постепенно, оставляя график неоцененных thunks , чтобы запомнить, где он остановился.
Теперь это сильно отличается от того, что вы можете ожидать, если вы знакомы с любым другим языком программирования. Нелегко представить, как на самом деле quicksort
ведет себя в Haskell с точки зрения доступа к памяти или даже порядка сравнений. Если бы вы могли наблюдать только поведение, а не исходный код, , вы бы не узнали, что он делает, как быструю сортировку .
Например, версия C быстрой сортировки разделяет все данные перед первым рекурсивным вызовом. В версии на Haskell первый элемент результата будет вычислен (и даже может появиться на вашем экране) до того, как будет завершен запуск первого раздела & mdash; действительно, прежде чем вообще будет выполнена какая-либо работа на greater
.
P.S. Код на Haskell был бы более похож на quicksort, если бы он выполнял то же число сравнений, что и quicksort; написанный код выполняет в два раза больше сравнений, потому что lesser
и greater
определены для вычисления независимо, выполняя два линейных сканирования по списку. Конечно, в принципе возможно, чтобы компилятор был достаточно умен, чтобы исключить дополнительные сравнения; или код может быть изменен для использования Data.List.partition
.
P.P.S. Классическим примером алгоритмов Хаскелла, который не работает так, как вы ожидали, является сито Эратосфена для вычисления простых чисел.