Спробую дати пояснення простими словами. Як зазначали інші, нормальна форма голови не поширюється на Haskell, тому я тут не буду її розглядати.
Нормальна форма
Вираз у нормальній формі оцінюється повністю, і жоден підвираз не може бути оцінений далі (тобто він не містить не оцінених громів).
Усі ці вирази мають нормальну форму:
42
(2, "hello")
\x -> (x + 1)
Ці вирази не мають нормальної форми:
1 + 2 -- we could evaluate this to 3
(\x -> x + 1) 2 -- we could apply the function
"he" ++ "llo" -- we could apply the (++)
(1 + 1, 2 + 2) -- we could evaluate 1 + 1 and 2 + 2
Слабка голова нормальної форми
Вираз у нормальній формі слабкої голови оцінювали до самого зовнішнього конструктора даних або лямбда-абстракції ( головка ). Підвиразки можуть бути, а можуть і не бути оцінені . Тому кожен вираз нормальної форми також знаходиться у слабкій нормальній формі голови, хоча взагалі протилежне.
Щоб визначити, чи є вираз у слабкій голові нормальної форми, нам залишається лише подивитися на зовнішню частину виразу. Якщо це конструктор даних або лямбда, він у слабкій голові нормальної форми. Якщо це функціональний додаток, це не так.
Ці вирази у слабкій голові нормальної форми:
(1 + 1, 2 + 2) -- the outermost part is the data constructor (,)
\x -> 2 + 2 -- the outermost part is a lambda abstraction
'h' : ("e" ++ "llo") -- the outermost part is the data constructor (:)
Як уже згадувалося, всі перераховані вище вирази нормальної форми також знаходяться у слабкій нормальній формі голови.
Ці вирази не мають слабкої голови у звичайній формі:
1 + 2 -- the outermost part here is an application of (+)
(\x -> x + 1) 2 -- the outermost part is an application of (\x -> x + 1)
"he" ++ "llo" -- the outermost part is an application of (++)
Стек переливів
Оцінка вираження до нормальної форми слабкої голови може вимагати, щоб спочатку інші вирази були оцінені в WHNF. Наприклад, щоб оцінити 1 + (2 + 3)
WHNF, ми спочатку повинні оцінити2 + 3
. Якщо оцінка одного виразу призводить до занадто великої кількості цих вкладених оцінок, результатом є переповнення стека.
Це відбувається, коли ви створюєте великий вираз, який не створює жодних конструкторів даних або лямбда, поки значна частина його не буде оцінена. Вони часто спричинені таким видом використання foldl
:
foldl (+) 0 [1, 2, 3, 4, 5, 6]
= foldl (+) (0 + 1) [2, 3, 4, 5, 6]
= foldl (+) ((0 + 1) + 2) [3, 4, 5, 6]
= foldl (+) (((0 + 1) + 2) + 3) [4, 5, 6]
= foldl (+) ((((0 + 1) + 2) + 3) + 4) [5, 6]
= foldl (+) (((((0 + 1) + 2) + 3) + 4) + 5) [6]
= foldl (+) ((((((0 + 1) + 2) + 3) + 4) + 5) + 6) []
= (((((0 + 1) + 2) + 3) + 4) + 5) + 6
= ((((1 + 2) + 3) + 4) + 5) + 6
= (((3 + 3) + 4) + 5) + 6
= ((6 + 4) + 5) + 6
= (10 + 5) + 6
= 15 + 6
= 21
Зверніть увагу, як воно має пройти досить глибоко, перш ніж воно зможе отримати вираз у слабкій голові нормальної форми.
Вам може бути цікаво, чому Хаскелл не скорочує внутрішні вирази достроково? Це через лінь Хаскелла. Оскільки в цілому не можна припустити, що буде потрібна кожна субекспресія, вирази оцінюються ззовні в.
(GHC має аналізатор суворості, який виявить деякі ситуації, коли субекспресія завжди потрібна, і потім вона може оцінити її достроково. Однак це лише оптимізація, і вам не слід покладатися на неї, щоб врятувати вас від переливів).
З іншого боку, цей вираз є абсолютно безпечним:
data List a = Cons a (List a) | Nil
foldr Cons Nil [1, 2, 3, 4, 5, 6]
= Cons 1 (foldr Cons Nil [2, 3, 4, 5, 6]) -- Cons is a constructor, stop.
Щоб уникнути побудови цих великих виразів, коли ми знаємо, що всі підекспресії повинні бути оцінені, ми хочемо змусити внутрішні частини оцінюватися достроково.
seq
seq
- це особлива функція, яка використовується для примушування оцінювання виразів. Його семантика полягає в тому, seq x y
що щоразу, коли y
оцінюється нормальна форма слабкої голови,x
вона також оцінюється як нормальна форма слабкої голови.
Це серед інших місць, що використовуються у визначенні foldl'
, суворого варіанту foldl
.
foldl' f a [] = a
foldl' f a (x:xs) = let a' = f a x in a' `seq` foldl' f a' xs
Кожна ітерація foldl'
примушує акумулятор до WHNF. Тому він уникає нарощування великого вираження, і тому уникає переповнення стека.
foldl' (+) 0 [1, 2, 3, 4, 5, 6]
= foldl' (+) 1 [2, 3, 4, 5, 6]
= foldl' (+) 3 [3, 4, 5, 6]
= foldl' (+) 6 [4, 5, 6]
= foldl' (+) 10 [5, 6]
= foldl' (+) 15 [6]
= foldl' (+) 21 []
= 21 -- 21 is a data constructor, stop.
Але, як згадується в прикладі HaskellWiki, це не врятує вас у всіх випадках, оскільки акумулятор оцінюється лише WHNF. У прикладі акумулятор є кортежем, тому він буде примусово оцінювати конструктор кортежу, а не acc
або len
.
f (acc, len) x = (acc + x, len + 1)
foldl' f (0, 0) [1, 2, 3]
= foldl' f (0 + 1, 0 + 1) [2, 3]
= foldl' f ((0 + 1) + 2, (0 + 1) + 1) [3]
= foldl' f (((0 + 1) + 2) + 3, ((0 + 1) + 1) + 1) []
= (((0 + 1) + 2) + 3, ((0 + 1) + 1) + 1) -- tuple constructor, stop.
Щоб цього уникнути, ми повинні зробити так, щоб оцінювати силу конструктора кортежу acc
і len
. Ми робимо це за допомогою seq
.
f' (acc, len) x = let acc' = acc + x
len' = len + 1
in acc' `seq` len' `seq` (acc', len')
foldl' f' (0, 0) [1, 2, 3]
= foldl' f' (1, 1) [2, 3]
= foldl' f' (3, 2) [3]
= foldl' f' (6, 3) []
= (6, 3) -- tuple constructor, stop.