Що таке нормальна форма слабкої голови?


290

Що означає нормальна форма слабкої голови (WHNF)? Що означає звичайна форма голови (HNF) та нормальна форма (NF)?

Реальний світ Хаскелл заявляє:

Знайома функція seq оцінює вираз у тому, що ми називаємо головою нормальної форми (скорочено HNF). Він зупиняється, як тільки дістається до самого зовнішнього конструктора («головки»). Це відрізняється від звичайної форми (NF), в якій вираз повністю оцінюється.

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

Я прочитав декілька ресурсів та визначень ( Haskell Wiki та Haskell Mail List та Free Dictionary ), але не розумію. Може хтось може навести приклад або надати визначення мирян?

Я здогадуюсь, це буде схоже на:

WHNF = thunk : thunk

HNF = 0 : thunk 

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

Як seqі як($!) пов'язані з WHNF і HNF?

Оновлення

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

Ще одна seqпроблема плутанини - з Haskell Wiki, де йдеться про те, що вона зменшується до WHNF, і нічого не зробить на наступному прикладі. Тоді вони кажуть, що їм треба користуватисяseq для примусового оцінювання. Хіба це не примушує це до HNF?

Звичайний код, що переповнює стек для новачків:

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

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

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

- Haskell Wiki на Stackoverflow


1
Як правило, ми говоримо про WHNF та RNF. (RNF - те, що ви називаєте NF)
альтернатива

5
@monadic Що означає R у RNF?
dave4420

7
@ dave4420: зменшено
марк

Відповіді:


399

Спробую дати пояснення простими словами. Як зазначали інші, нормальна форма голови не поширюється на 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.

31
Нормальна форма голови вимагає також зменшення тіла лямбда, тоді як нормальна форма слабкої голови не має цієї вимоги. Так \x -> 1 + 1є WHNF, але не HNF.
хаммар

Вікіпедія стверджує, що HNF - це "[a] термін у звичайній формі голови, якщо в голові немає бета-редексу". Хіба Haskell "слабкий", тому що він не бета-редексує під вирази?

Як суворі конструктори даних вступають у гру? Вони просто люблять звертатися seqдо своїх аргументів?
Бергі

1
@CaptainObvious: 1 + 2 не є ні NF, ні WHNF. Вирази не завжди є нормальною формою.
хаммар

2
@Zorobay: Щоб надрукувати результат, GHCi в кінцевому підсумку оцінює вираз повністю NF, а не тільки WHNF. Один із способів визначити різницю між двома варіантами - це включити статистику пам'яті за допомогою :set +s. Потім ви можете бачити, що в foldl' fкінцевому підсумку виділяється більше гронів, ніжfoldl' f' .
хаммар

43

У розділі , присвяченому санки і слабкою головний нормальній формі в Haskell Вікіпідручник опис ліні забезпечує дуже хороший опис WHNF поряд з цим корисним зображенням:

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

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


5
Я знаю, що люди кажуть, що ігнорують нормальну форму голови, але чи можете ви навести приклад на цій схемі, як у вас виглядає нормальна форма голови?
CMCDragonkai

28

Програми 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. Іноді люди називають неоцінені вирази "громом", але я не думаю, що це хороший спосіб зрозуміти це.
  3. Нормальна форма голови (HNF) не має значення для Haskell. Він відрізняється від WHNF тим, що тіла виразів лямбда також оцінюються певною мірою.

Чи застосовується seqв foldl'силі оцінювання від WHNF до HNF?

1
@snmcdonald: Ні, Haskell не використовує HNF. Оцінювання seq expr1 expr2оцінить перший вираз expr1WHNF перед оцінкою другого виразу expr2.
Генріх Апфельмус

26

Хороше пояснення з прикладами наведено на веб-сторінці http://foldoc.org/Weak+Head+Normal+Form Head. Нормальна форма спрощує навіть біти вираження всередині функції абстракції, тоді як "слабка" нормальна форма голови зупиняється на абстракції функцій. .

З джерела, якщо у вас є:

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

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

Реально нормальну форму голови важко було б ефективно реалізувати. Це зажадає заглядати всередину функцій. Таким чином, перевага слабкої голови в звичайній формі полягає в тому, що ви все ще можете реалізовувати функції як непрозорий тип, а значить, він більше сумісний із компільованими мовами та оптимізацією.


12

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

Або я неправильно розумію приклад, або ви змішуєте 1 і 2 у WHNF та HNF.
Чень

5

В принципі, у вас є якесь - то стукіт, t.

Тепер, якщо ми хочемо оцінити tWHNF чи NHF, які однакові, окрім функцій, ми виявимо, що ми отримуємо щось на зразок

t1 : t2де t1і t2є громи. У цьому випадку t1буде вашим 0(а точніше, обдуром, коли 0не потрібно додатково розблокувати)

seqта $!оцінити WHNF. Зауважте, що

f $! x = seq x (f x)

1
@snmcdonald Ігноруй HNF. seq каже, що коли це оцінюється WHNF, оцініть перший аргумент до WHNF.
альтернатива
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.