Благодаря ленивому вычислению программа на 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, которые ведут себя не так, как вы ожидали, является решето Эратосфена для вычисления простых чисел.