Saya akan mencoba memberikan penjelasan secara sederhana. Seperti yang telah ditunjukkan orang lain, bentuk normal kepala tidak berlaku untuk Haskell, jadi saya tidak akan mempertimbangkannya di sini.
Bentuk normal
Ekspresi dalam bentuk normal sepenuhnya dievaluasi, dan tidak ada sub-ekspresi yang dapat dievaluasi lebih lanjut (yaitu tidak mengandung thunks yang tidak dievaluasi).
Semua ungkapan ini dalam bentuk normal:
42
(2, "hello")
\x -> (x + 1)
Ungkapan-ungkapan ini tidak dalam bentuk normal:
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
Bentuk normal kepala lemah
Ekspresi dalam bentuk normal kepala lemah telah dievaluasi ke konstruktor data terluar atau abstraksi lambda ( kepala ). Sub-ekspresi mungkin atau mungkin belum dievaluasi . Oleh karena itu, setiap ekspresi bentuk normal juga dalam bentuk normal kepala lemah, meskipun sebaliknya tidak berlaku secara umum.
Untuk menentukan apakah suatu ekspresi dalam bentuk normal kepala lemah, kita hanya perlu melihat bagian terluar dari ekspresi itu. Jika itu adalah konstruktor data atau lambda, itu dalam bentuk normal kepala lemah. Jika itu adalah aplikasi fungsi, itu bukan.
Ekspresi ini dalam bentuk normal kepala lemah:
(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 (:)
Seperti disebutkan, semua ekspresi bentuk normal yang tercantum di atas juga dalam bentuk normal kepala lemah.
Ungkapan-ungkapan ini tidak dalam bentuk normal kepala lemah:
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 (++)
Stack overflow
Mengevaluasi ekspresi ke bentuk normal kepala lemah mungkin mengharuskan ekspresi lain dievaluasi ke WHNF terlebih dahulu. Misalnya, untuk mengevaluasi 1 + (2 + 3)
ke WHNF, pertama-tama kita harus mengevaluasi 2 + 3
. Jika mengevaluasi satu ekspresi mengarah ke terlalu banyak evaluasi bersarang ini, hasilnya adalah stack overflow.
Ini terjadi ketika Anda membangun ekspresi besar yang tidak menghasilkan konstruktor data atau lambdas sampai sebagian besar telah dievaluasi. Ini sering disebabkan oleh jenis penggunaan 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
Perhatikan bagaimana ia harus cukup dalam sebelum bisa membuat ekspresi menjadi bentuk normal kepala lemah.
Anda mungkin bertanya-tanya, mengapa Haskell tidak mengurangi ekspresi batin sebelumnya? Itu karena kemalasan Haskell. Karena tidak dapat diasumsikan secara umum bahwa setiap subekspresi akan diperlukan, ekspresi dievaluasi dari luar pada.
(GHC memiliki penganalisis ketelitian yang akan mendeteksi beberapa situasi di mana subekspresi selalu diperlukan dan kemudian dapat mengevaluasinya lebih awal. Namun ini hanya pengoptimalan, dan Anda tidak boleh bergantung padanya untuk menyelamatkan Anda dari luapan).
Ekspresi semacam ini, di sisi lain, benar-benar aman:
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.
Untuk menghindari pembangunan ekspresi besar ini ketika kita tahu semua subekspresi harus dievaluasi, kami ingin memaksa bagian dalam dievaluasi lebih dulu.
seq
seq
adalah fungsi khusus yang digunakan untuk memaksa ekspresi dievaluasi. Semantiknya adalah bahwa seq x y
setiap kali y
dievaluasi ke bentuk normal kepala lemah, x
juga dievaluasi untuk bentuk normal kepala lemah.
Ini adalah tempat lain yang digunakan dalam definisi foldl'
, varian ketat foldl
.
foldl' f a [] = a
foldl' f a (x:xs) = let a' = f a x in a' `seq` foldl' f a' xs
Setiap iterasi foldl'
memaksa akumulator ke WHNF. Karena itu ia menghindari membangun ekspresi yang besar, dan karena itu menghindari tumpah tumpukan.
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.
Tetapi seperti contoh pada HaskellWiki menyebutkan, ini tidak menyelamatkan Anda dalam semua kasus, karena akumulator hanya dievaluasi ke WHNF. Dalam contoh tersebut, akumulator adalah tupel, sehingga hanya akan memaksa evaluasi konstruktor tupel, dan bukan acc
atau 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.
Untuk menghindari hal ini, kita harus membuatnya sehingga mengevaluasi kekuatan konstruktor tuple evaluasi acc
dan len
. Kami melakukan ini dengan menggunakan 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.