ฉันจะพยายามอธิบายอย่างง่าย ตามที่คนอื่น ๆ ได้ชี้ให้เห็นว่ารูปแบบปกติของหัวไม่สามารถใช้ได้กับ 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 และไม่ได้หรือ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.
เพื่อหลีกเลี่ยงนี้เราจะต้องทำให้มันเพื่อให้การประเมินการประเมินผล tuple กองกำลังสร้างของและ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.