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

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

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

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

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

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

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

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 на Stackoverflow

6 ответов


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

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

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

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

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

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

вы можете задаться вопросом, почему Хаскелл не сокращает внутренние выражения раньше времени? Это из-за лени Хаскелла. Поскольку в целом нельзя предположить, что потребуется каждое подвыражение, выражения оцениваются извне in.

(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.

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

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

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

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

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

f $! x = seq x (f x)