Я постараюсь дать объяснение простыми словами. Как уже отмечали другие, нормальная форма головы не относится к Хаскеллу, поэтому я не буду ее здесь рассматривать.
Нормальная форма
Выражение в нормальной форме полностью вычисляется, и никакое подвыражение не может быть оценено далее (то есть оно не содержит необработанных блоков).
Все эти выражения в нормальной форме:
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
Обратите внимание, как оно должно пройти достаточно глубоко, прежде чем оно сможет привести выражение в нормальную форму слабой головы.
Вы можете удивиться, почему Haskell не сокращает внутренние выражения раньше времени? Это из-за лени Хаскелла. Поскольку в общем случае нельзя предполагать, что каждое подвыражение будет необходимо, выражения оцениваются извне в.
(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.