รูปแบบปกติที่อ่อนแอคืออะไร


290

สิ่งที่ไม่อ่อนแอหัวหน้าฟอร์มปกติ (WHNF) หมายถึง? อะไรรูปแบบหัวปกติ (HNF) และปกติแบบฟอร์ม (NF) หมายถึง?

รัฐในโลกแห่งความจริง Haskell :

ฟังก์ชัน 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'?

จุดที่สับสนอีกประการหนึ่งคือจาก Haskell Wiki ซึ่งระบุว่าseqลดลงเป็น WHNF และจะไม่ทำอะไรกับตัวอย่างต่อไปนี้ จากนั้นพวกเขาบอกว่าต้องใช้seqเพื่อบังคับให้การประเมินผล นั่นไม่ได้บังคับให้ HNF เหรอ?

newbie รหัสทั่วไปของการโอเวอร์โฟลว์:

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

ผู้ที่เข้าใจรูปแบบปกติและหัวที่อ่อนแอ (whnf) สามารถเข้าใจได้ทันทีว่าเกิดอะไรขึ้นที่นี่ (acc + x, len + 1) มีอยู่แล้วใน whnf ดังนั้น seq ซึ่งลดค่าเป็น whnf ไม่ได้ทำสิ่งนี้ รหัสนี้จะสร้าง thunks เช่นเดียวกับตัวอย่าง foldl ต้นฉบับพวกเขาจะอยู่ภายใน tuple การแก้ปัญหาคือเพียงเพื่อบังคับส่วนประกอบของ tuple เช่น

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: Reduced
marc

คำตอบ:


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

หัวอ่อนแอแบบปกติ

นิพจน์ในรูปแบบปกติที่อ่อนแรงได้รับการประเมินไปยังตัวสร้างข้อมูลนอกสุดหรือนามธรรมแลมบ์ดา ( หัว ) Sub-แสดงออกหรืออาจจะไม่ได้รับการประเมิน ดังนั้นทุกรูปแบบการแสดงออกปกติยังอยู่ในรูปแบบปกติของหัวที่อ่อนแอแม้ว่าสิ่งที่ตรงกันข้ามจะไม่ถือโดยทั่วไป

ในการตรวจสอบว่าการแสดงออกอยู่ในรูปแบบปกติที่อ่อนแอหรือไม่เราจะต้องดูที่ส่วนนอกสุดของการแสดงออก ถ้าเป็นตัวสร้างข้อมูลหรือแลมบ์ดามันจะอยู่ในรูปแบบปกติที่อ่อนแอ หากเป็นแอปพลิเคชั่นฟังก์ชั่นนั่นไม่ใช่

นิพจน์เหล่านี้อยู่ในรูปแบบปกติของผู้ที่อ่อนแอ:

(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 ไม่ลดการแสดงออกภายในก่อนเวลา? นั่นเป็นเพราะความเกียจคร้านของ Haskell เนื่องจากไม่สามารถสันนิษฐานได้โดยทั่วไปว่าต้องการนิพจน์ย่อยทุกอันนิพจน์จึงถูกประเมินจากภายนอก

(GHC มีตัววิเคราะห์ความเข้มงวดที่จะตรวจจับสถานการณ์บางอย่างที่จำเป็นต้องใช้ subexpression เสมอและสามารถประเมินได้ล่วงหน้าเวลานี่เป็นเพียงการเพิ่มประสิทธิภาพเท่านั้นและคุณไม่ควรใช้มันเพื่อช่วยคุณประหยัดจากการล้น)

ในทางกลับกันการแสดงออกชนิดนี้ปลอดภัยอย่างสมบูรณ์:

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 เท่านั้น ในตัวอย่างสะสมเป็น tuple ดังนั้นมันจะบังคับให้การประเมินผลของตัวสร้าง tuple และไม่ได้หรือacclen

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.

เพื่อหลีกเลี่ยงนี้เราจะต้องทำให้มันเพื่อให้การประเมินการประเมินผล tuple กองกำลังสร้างของและacc เราทำเช่นนี้โดยใช้lenseq

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 + 1WHNF แต่ไม่ใช่ HNF
hammar

วิกิพีเดียระบุว่า HNF คือ "[a] คำนี้อยู่ในรูปแบบปกติของหัวถ้าไม่มีเบต้า - เรกซ์ในตำแหน่งหัว" Haskell "อ่อน" เพราะไม่มีการแสดงออกย่อยเบต้าเรดexหรือไม่

เครื่องมือสร้างข้อมูลที่เข้มงวดมาสู่การเล่นได้อย่างไร พวกเขาชอบโทรหาseqข้อโต้แย้งของพวกเขา?
Bergi

1
@CaptainOb ชัด: 1 + 2 ไม่ใช่ NF หรือ WHNF นิพจน์ไม่ได้อยู่ในรูปแบบปกติเสมอไป
hammar

2
@Zorobay: เพื่อพิมพ์ผลลัพธ์ GHCi จะประเมินการแสดงออกโดยสมบูรณ์ให้เป็น NF ไม่ใช่แค่ WHNF :set +sวิธีหนึ่งที่จะบอกความแตกต่างระหว่างสองสายพันธุ์คือการเปิดใช้งานหน่วยความจำที่มีสถิติ แล้วคุณจะเห็นว่าfoldl' fปลายขึ้นจัดสรร thunks foldl' f'มากกว่า
hammar

43

ส่วนบนThunks และ Weak Head แบบธรรมดาในคำอธิบาย Haskell Wikibooks ของความเกียจคร้านให้คำอธิบายที่ดีมากของ 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. บางครั้งผู้คนเรียกนิพจน์ "thunks" ที่ไม่ได้ประเมินค่า แต่ฉันไม่คิดว่าจะเป็นวิธีที่ดีในการทำความเข้าใจ
  3. Head normal form (HNF) นั้นไม่เกี่ยวข้องกับ Haskell มันแตกต่างจาก WHNF ที่ร่างกายของการแสดงออกแลมบ์ดายังได้รับการประเมินในระดับหนึ่ง

คือการใช้seqในการfoldl'บังคับใช้การประเมินผลจากการ WHNF HNF?

1
@snmcdonald: ไม่ Haskell ไม่ได้ใช้ประโยชน์จาก HNF การประเมินseq expr1 expr2จะประเมินการแสดงออกครั้งแรกexpr1ที่จะ WHNF expr2ก่อนที่จะประเมินการแสดงออกที่สอง
เฮ็น Apfelmus

26

คำอธิบายที่ดีพร้อมตัวอย่างให้ไว้ที่http://foldoc.org/Weak+Head+Normal+Form Head แบบฟอร์มปกติทำให้บิตของนิพจน์ภายในฟังก์ชันนามธรรมง่ายขึ้นในขณะที่รูปแบบปกติ "อ่อนแอ" หยุดที่ abstractions ของฟังก์ชัน .

จากแหล่งที่มาถ้าคุณมี:

\ 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
Zhen

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
ทางเลือก
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.