Tôi sẽ cố gắng đưa ra một lời giải thích bằng những thuật ngữ đơn giản. Như những người khác đã chỉ ra, hình thức bình thường không áp dụng cho Haskell, vì vậy tôi sẽ không xem xét nó ở đây.
Hình thức bình thường
Một biểu thức ở dạng bình thường được đánh giá đầy đủ và không có biểu thức phụ nào có thể được đánh giá thêm nữa (nghĩa là nó không chứa thunks chưa được đánh giá).
Các biểu thức này đều ở dạng bình thường:
42
(2, "hello")
\x -> (x + 1)
Các biểu thức này không ở dạng bình thường:
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
Đầu yếu hình thức bình thường
Một biểu thức ở dạng bình thường của đầu yếu đã được đánh giá cho hàm tạo dữ liệu ngoài cùng hoặc trừu tượng lambda (phần đầu ). Biểu thức phụ có thể hoặc không thể được đánh giá . Do đó, mọi biểu hiện dạng bình thường cũng ở dạng yếu ở đầu bình thường, mặc dù ngược lại không giữ chung.
Để xác định xem một biểu thức có ở dạng yếu ở đầu bình thường hay không, chúng ta chỉ phải nhìn vào phần ngoài cùng của biểu thức. Nếu đó là một công cụ xây dựng dữ liệu hoặc lambda, thì nó ở dạng đầu yếu. Nếu đó là một ứng dụng chức năng, thì không.
Những biểu hiện ở dạng yếu đầu bình thường:
(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 (:)
Như đã đề cập, tất cả các biểu thức hình thức bình thường được liệt kê ở trên cũng ở dạng yếu đầu bình thường.
Những biểu hiện này không ở dạng yếu đầu bình thường:
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 (++)
Chồng tràn
Đánh giá một biểu thức thành dạng bình thường ở đầu yếu có thể yêu cầu các biểu thức khác được đánh giá trước WHNF. Ví dụ: để đánh giá 1 + (2 + 3)
WHNF, trước tiên chúng ta phải đánh giá 2 + 3
. Nếu việc đánh giá một biểu thức dẫn đến quá nhiều các đánh giá lồng nhau này, kết quả là tràn ngăn xếp.
Điều này xảy ra khi bạn xây dựng một biểu thức lớn không tạo ra bất kỳ hàm tạo dữ liệu hoặc lambdas nào cho đến khi một phần lớn của nó được đánh giá. Chúng thường được gây ra bởi loại sử dụng này 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
Chú ý làm thế nào nó phải đi khá sâu trước khi nó có thể có được biểu hiện ở dạng yếu đầu bình thường.
Bạn có thể tự hỏi, tại sao Haskell không giảm các biểu hiện bên trong trước thời hạn? Đó là vì sự lười biếng của Haskell. Vì nói chung không thể giả định rằng mọi biểu hiện phụ sẽ cần thiết, các biểu thức được đánh giá từ bên ngoài vào.
(GHC có một bộ phân tích nghiêm ngặt sẽ phát hiện một số tình huống luôn luôn cần một sự thay thế phụ và sau đó có thể đánh giá nó trước thời hạn. Tuy nhiên, đây chỉ là một tối ưu hóa và bạn không nên dựa vào nó để cứu bạn khỏi tràn).
Mặt khác, loại biểu hiện này là hoàn toàn an toàn:
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.
Để tránh xây dựng các biểu thức lớn này khi chúng tôi biết tất cả các biểu thức con sẽ phải được đánh giá, chúng tôi muốn buộc các phần bên trong được đánh giá trước thời hạn.
seq
seq
là một hàm đặc biệt được sử dụng để buộc các biểu thức được ước tính. Ngữ nghĩa của nó có seq x y
nghĩa là bất cứ khi nào y
được đánh giá ở dạng bình thường đầu yếu, x
cũng được đánh giá thành dạng bình thường đầu yếu.
Đó là một trong những nơi khác được sử dụng trong định nghĩa foldl'
, biến thể nghiêm ngặt của foldl
.
foldl' f a [] = a
foldl' f a (x:xs) = let a' = f a x in a' `seq` foldl' f a' xs
Mỗi lần lặp lại foldl'
buộc bộ tích lũy thành WHNF. Do đó, nó tránh xây dựng một biểu thức lớn, và do đó nó tránh tràn ra khỏi ngăn xếp.
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.
Nhưng như ví dụ trên HaskellWiki đã đề cập, điều này không giúp bạn tiết kiệm trong mọi trường hợp, vì bộ tích lũy chỉ được ước tính cho WHNF. Trong ví dụ, bộ tích lũy là một tuple, vì vậy nó sẽ chỉ buộc đánh giá của hàm tạo tuple, và không acc
hoặc 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.
Để tránh điều này, chúng ta phải làm cho nó để đánh giá các hàm xây dựng tuple đánh giá acc
và len
. Chúng tôi làm điều này bằng cách sử dụng 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.