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