Хаскелл: что такое слабая голова нормальной формы?


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

Реальный Мир Хаскелл гласит:

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

вы также услышите, как программисты 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'?

еще один момент путаницы - из Вики 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 так же, как в исходном примере foldl, они просто будут внутри кортежа. Решение состоит только в том, чтобы заставить компоненты кортежа, например,

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

-Haskell Wiki on Stackoverflow

6 255

6 ответов:

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

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

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

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

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 имеет a строгость анализатор, который будет обнаруживать некоторые ситуации, когда подвыражение всегда необходимо, и он может затем оценить его заранее. Это только оптимизация, но и вы не должны полагаться на это, чтобы спасти вас от переполнения).

этот вид выражения, с другой стороны, совершенно безопасен:

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.

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

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

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

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

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

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

программы 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 тем, что тела лямбда-выражений также оцениваются на некоторые степень.

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

в принципе, предположим у вас есть какой-то стук, t.

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

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

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

f $! x = seq x (f x)