Что такое нормальная форма слабой головы? - PullRequest
276 голосов
/ 29 июля 2011

Что означает Нормальная форма слабой головы (WHNF)? Что означает Нормальная форма головы (HNF) и Нормальная форма (NF)?

Real World Haskell заявляет:

Знакомая функция seq вычисляет выражение к тому, что мы называем head нормальная форма (сокращенно HNF). Останавливается, как только достигает крайнего конструктор («голова»). Это отличается от нормальной формы (НФ), в какое выражение полностью вычисляется.

Вы также услышите, как программисты на Haskell ссылаются на нормальную форму слабой головы (WHNF). Для нормальных данных слабая голова нормальная форма такая же, как голова нормальная форма. Разница возникает только для функций, и слишком трудно беспокоить нас здесь.

Я прочитал несколько ресурсов и определений ( Haskell Wiki и Haskell Mail List и Бесплатный словарь ), но я не получаю его. Может ли кто-нибудь привести пример или дать определение непрофессионала?

Я предполагаю, что это будет похоже на:

WHNF = thunk : thunk

HNF = 0 : thunk 

NF = 0 : 1 : 2 : 3 : []

Как seq и ($!) относятся к WHNF и HNF?

Обновление

Я все еще в замешательстве. Я знаю, что некоторые ответы говорят, чтобы игнорировать HNF. Из прочтения различных определений кажется, что нет никакой разницы между обычными данными в WHNF и HNF. Тем не менее, кажется, что есть разница, когда дело доходит до функции. Если не было никакой разницы, почему seq необходимо для foldl'?

Еще одна путаница связана с тем, что Wiki Haskell утверждает, что seq сводится к WHNF, и ничего не будет делать в следующем примере. Затем они говорят, что они должны использовать seq, чтобы форсировать оценку. Разве это не принуждает его к HNF?

Общий код переполнения стека новичков:

myAverage = uncurry (/) . foldl' (\(acc, len) x -> (acc+x, len+1)) (0,0)

Люди, которые понимают seq и слабую голову нормальной формы (whnf), могут сразу пойми что тут не так. (acc + x, len + 1) уже в whnf, поэтому seq, который уменьшает значение до whnf, ничего не делает для этого. Этот код будет создавать thunks точно так же, как и в оригинальном примере они просто будут внутри кортежа. Решение состоит в том, чтобы заставить компоненты кортежа, например

myAverage = uncurry (/) . foldl' 
          (\(acc, len) x -> acc `seq` len `seq` (acc+x, len+1)) (0,0)

- Haskell Wiki на Stackoverflow

Ответы [ 6 ]

383 голосов
/ 31 июля 2011

Я постараюсь дать объяснение в простых терминах. Как уже отмечали другие, нормальная форма головы не относится к Хаскеллу, поэтому я не буду ее здесь рассматривать.

Нормальная форма

Выражение в нормальной форме полностью вычисляется, и никакое подвыражение не может быть оценено в дальнейшем (то есть оно не содержит необработанных блоков).

Все эти выражения в нормальной форме:

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

Слабая голова, нормальная форма

Выражение в нормальной форме со слабой головой было оценено для самого внешнего конструктора данных или лямбда-абстракции ( head ). Подвыражения могут оцениваться или не оцениваться . Поэтому каждое выражение нормальной формы также находится в нормальной форме со слабой головой, хотя в общем случае обратное не имеет места.

Чтобы определить, находится ли выражение в нормальной форме со слабой головой, нам нужно только взглянуть на крайнюю часть выражения. Если это конструктор данных или лямбда, то у него слабая нормальная форма. Если это функциональное приложение, это не так.

Эти выражения в нормальной голове слабые:

(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.
41 голосов
/ 18 февраля 2012

Раздел Нормальная форма "Сундуки и слабая голова" в Haskell Wikibooks Описание лени содержит очень хорошее описание WHNF вместе с этим полезным описанием:

Evaluating the value (4, [1, 2]) step by step. The first stage is completely unevaluated; all subsequent forms are in WHNF, and the last one is also in normal form.

Оценка значения (4, [1, 2]) шаг за шагом.Первый этап полностью неоценен;все последующие формы находятся в формате WHNF, а последняя также находится в нормальной форме.

26 голосов
/ 30 июля 2011

Программы на Haskell являются выражениями , и они выполняются путем выполнения вычисления .

Чтобы оценить выражение, замените все приложения функций их определениями.Порядок, в котором вы делаете это, не имеет большого значения, но он по-прежнему важен: начните с самого внешнего приложения и продолжайте слева направо;это называется отложенная оценка .

Пример:

   take 1 (1:2:3:[])
=> { apply take }
   1 : take (1-1) (2:3:[])
=> { apply (-)  }
   1 : take 0 (2:3:[])
=> { apply take }
   1 : []

Оценка останавливается, когда больше не осталось функциональных приложений для замены.Результат в нормальной форме (или в уменьшенной нормальной форме , RNF).Независимо от того, в каком порядке вы оцениваете выражение, вы всегда будете иметь одну и ту же нормальную форму (но только если оценка завершится).

Существует немного другое описание для отложенной оценки.А именно, он говорит, что вы должны оценивать все до слабая голова, нормальная форма только .Существует ровно три случая, когда выражение должно быть в WHNF:

  • Конструктор: constructor expression_1 expression_2 ...
  • Встроенная функция со слишком малым количеством аргументов, например (+) 2 или sqrt
  • Лямбда-выражение: \x -> expression

Другими словами, заголовок выражения (т. Е. Самое внешнее приложение функции) не может быть вычислен дальше, но функцияАргумент может содержать выражения без оценки.

Примеры WHNF:

3 : take 2 [2,3,4]   -- outermost function is a constructor (:)
(3+1) : [4..]        -- ditto
\x -> 4+5            -- lambda expression

Примечания

  1. «Голова» в WHNF не относится к головесписка, но для самого внешнего приложения функции.
  2. Иногда люди называют неоцененные выражения "thunks", но я не думаю, что это хороший способ понять это.
  3. Нормальная форма головы (HNF) не имеет значения для Haskell.Он отличается от WHNF тем, что тела лямбда-выражений также в некоторой степени оцениваются.
25 голосов
/ 29 июля 2011

Хорошее объяснение с примерами дано на http://foldoc.org/Weak+Head+Normal+Form Обычная форма головы упрощает даже биты выражения внутри абстракции функции, в то время как нормальная форма "слабой" головы останавливается на абстракциях функции.

Из источника, если у вас есть:

\ x -> ((\ y -> y+x) 2)

, который находится в нормальной форме со слабой головой, но не в нормальной форме головы ... потому что возможное приложение застряло внутри функции, которая не может быть оцененапока.

Реальную нормальную форму головы будет сложно реализовать эффективно.Это потребует возни внутри функций.Таким образом, преимущество нормальной формы со слабой головой состоит в том, что вы все еще можете реализовывать функции как непрозрачный тип, и, следовательно, он более совместим с компилируемыми языками и оптимизацией.

12 голосов
/ 29 июля 2011

WHNF не хочет, чтобы тело лямбда-выражения оценивалось, поэтому

WHNF = \a -> thunk
HNF = \a -> a + c

seq хочет, чтобы его первый аргумент был в WHNF, поэтому

let a = \b c d e -> (\f -> b + c + d + e + f) b
    b = a 2
in seq b (b 5)

оценивается как

\d e -> (\f -> 2 + 5 + d + e + f) 2

вместо того, что бы использовать HNF

\d e -> 2 + 5 + d + e + 2
5 голосов
/ 29 июля 2011

В общем, предположим, что у вас есть какой-то стук, t.

Теперь, если мы хотим вычислить t в WHNF или NHF, которые являются одинаковыми, за исключением функций, мы обнаружим, что получаем что-то вроде

t1 : t2, где t1 и t2 - гром. В этом случае t1 будет вашим 0 (или, скорее, громом к 0 без дополнительной распаковки)

seq и $! evalute WHNF. Обратите внимание, что

f $! x = seq x (f x)
...