Завдяки лінивій оцінці програма Haskell не робить (майже не може ) робити те, що виглядає, як це робиться.
Розглянемо цю програму:
main = putStrLn (show (quicksort [8, 6, 7, 5, 3, 0, 9]))
Школьною мовою спочатку quicksort
бігав би, потім show
, потімputStrLn
. Аргументи функції обчислюються до запуску цієї функції.
У Haskell все навпаки. Функція починає працювати спочатку. Аргументи обчислюються лише тоді, коли функція їх фактично використовує. І складний аргумент, як список, обчислюється по одному фрагменту за часом, як використовується кожен його фрагмент.
Отже, перше , що відбувається в цій програмі, - це те, що він putStrLn
починає працювати.
РеалізаціяputStrLn
робіт GHC шляхом копіювання символів аргументу String у вихідний буфер. Але коли він входить у цю петлю, show
ще не запустився. Тому, коли йдеться про копіювання першого символу з рядка, Haskell оцінює частку show
та quicksort
виклики, необхідні для обчислення цього символу . Потім putStrLn
переходить до наступного символу. Отже виконання всіх трьох функцій putStrLn
- show
, і quicksort
- переплітається. quicksort
виконується поступово, залишаючи графік неоцінених гронів, коли він пам’ятає, де він зупинився.
Тепер це дико відрізняється від того, що ви можете очікувати, якщо ви знайомі з будь-якою іншою мовою програмування. Непросто уявити, як quicksort
насправді поводиться Haskell з точки зору доступу до пам'яті чи навіть порядку порівнянь. Якби ви могли спостерігати лише за поведінкою, а не за вихідним кодом, ви б не визнавали, що це робить як швидкий корт .
Наприклад, версія C на основі швидкості розбиває всі дані до першого рекурсивного виклику. У версії Haskell перший елемент результату буде обчислений (і навіть може з’явитися на вашому екрані) до того, як перший розділ буде закінчений - дійсно до того, як будь-яка робота взагалі буде виконана greater
.
PS Код Haskell був би більш подібний до швидкості, якби він робив таку ж кількість порівнянь, що і quicksort; код як написано робить в два рази більше порівнянь , тому що lesser
і greater
визначено бути обчислено незалежно, роблячи два лінійних сканування за списком. Звичайно, компілятор в принципі може бути досить розумним, щоб усунути зайві порівняння; або код можна змінити на використанняData.List.partition
.
PPS Класичний приклад алгоритмів Хаскелла, який виявляється не так, як ви очікували, - це сито Ератостена для обчислення простих чисел.