Hình thức yếu đầu bình thường là gì?


290

Những gì hiện Yếu Trưởng Normal Form (WHNF) nghĩa là gì? Những gì hiện hình thức Head Bình thường (HNF) và Normal Form (NF) nghĩa là gì?

Real World Haskell tuyên bố:

Hàm seq quen thuộc đánh giá một biểu thức với cái mà chúng ta gọi là dạng bình thường (viết tắt là HNF). Nó dừng lại một khi nó đến được nhà xây dựng ngoài cùng (phần đầu của Google). Điều này khác với dạng thông thường (NF), trong đó một biểu thức được đánh giá hoàn toàn.

Bạn cũng sẽ nghe các lập trình viên Haskell đề cập đến dạng đầu yếu (WHNF). Đối với dữ liệu bình thường, dạng đầu bình thường yếu giống như dạng bình thường của đầu. Sự khác biệt chỉ phát sinh cho các chức năng, và quá khó hiểu khi quan tâm đến chúng tôi ở đây.

Tôi đã đọc một vài tài nguyên và định nghĩa ( Haskell WikiHaskell Mail ListFree Dictionary ) nhưng tôi không hiểu được. Ai đó có lẽ có thể đưa ra một ví dụ hoặc cung cấp một định nghĩa cư sĩ?

Tôi đoán nó sẽ tương tự như:

WHNF = thunk : thunk

HNF = 0 : thunk 

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

Làm thế nào seq($!)liên quan đến WHNF và HNF?

Cập nhật

Tôi vẫn còn bối rối. Tôi biết một số câu trả lời nói bỏ qua HNF. Từ việc đọc các định nghĩa khác nhau, dường như không có sự khác biệt giữa dữ liệu thông thường trong WHNF và HNF. Tuy nhiên, có vẻ như có một sự khác biệt khi nói đến một chức năng. Nếu không có sự khác biệt, tại sao seqcần thiết cho foldl'?

Một điểm nhầm lẫn khác là từ Haskell Wiki, trạng thái seqgiảm xuống WHNF, và sẽ không làm gì với ví dụ sau. Sau đó, họ nói rằng họ phải sử dụng seqđể buộc đánh giá. Đó không phải là buộc nó đến HNF?

Mã tràn ngăn xếp newbie phổ biến:

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

Những người hiểu seq và đầu yếu ở dạng bình thường (whnf) có thể ngay lập tức hiểu những gì sai ở đây. (acc + x, len + 1) đã có trong whnf, vì vậy seq, làm giảm giá trị thành whnf, không làm gì cho điều này. Mã này sẽ xây dựng thunks giống như ví dụ về bản gốc, chúng sẽ nằm trong một tuple. Giải pháp chỉ là buộc các thành phần của bộ dữ liệu, vd

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

- Wiki Haskell trên Stackoverflow


1
Nói chung, chúng tôi nói về WHNF và RNF. (RNF là những gì bạn gọi là NF)
thay thế vào

5
@monadic R trong RNF có nghĩa là gì?
dave4420

7
@ dave4420: Giảm
marc

Câu trả lời:


399

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

seqlà 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 ynghĩa là bất cứ khi nào yđược đánh giá ở dạng bình thường đầu yếu, xcũ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 acchoặ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á acclen. 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.

31
Hình dạng đầu bình thường đòi hỏi cơ thể của lambda cũng bị giảm, trong khi hình dạng đầu bình thường yếu không có yêu cầu này. Vậy \x -> 1 + 1là WHNF chứ không phải HNF.
hammar

Wikipedia tuyên bố HNF là "[a] thuật ngữ ở dạng đầu bình thường nếu không có beta-redex ở vị trí đầu". Haskell có "yếu" không vì nó không phải là biểu thức con beta-redex?

Làm thế nào để các nhà xây dựng dữ liệu nghiêm ngặt đi vào chơi? Có phải họ chỉ thích kêu gọi seqlập luận của họ?
Bergi

1
@CaptainObingly: 1 + 2 không phải là NF cũng không phải WHNF. Biểu thức không phải lúc nào cũng ở dạng bình thường.
hammar

2
@Zorobay: Để in kết quả, GHCi kết thúc việc đánh giá biểu thức hoàn toàn cho NF, không chỉ với WHNF. Một cách để nói sự khác biệt giữa hai biến thể là cho phép thống kê bộ nhớ với :set +s. Sau đó, bạn có thể thấy rằng foldl' fcuối cùng phân bổ nhiều thunks hơnfoldl' f' .
hammar

43

Phần về hình thức bình thường của Thunks và Weak Head trong mô tả về sự lười biếng của Haskell Wikibooks cung cấp một mô tả rất hay về WHNF cùng với mô tả hữu ích này:

Đánh giá giá trị (4, [1, 2]) từng bước.  Giai đoạn đầu tiên hoàn toàn không được đánh giá cao;  tất cả các hình thức tiếp theo đều ở dạng WHNF và mẫu cuối cùng cũng ở dạng bình thường.

Đánh giá giá trị (4, [1, 2]) từng bước. Giai đoạn đầu tiên hoàn toàn không được đánh giá cao; tất cả các hình thức tiếp theo đều ở dạng WHNF và mẫu cuối cùng cũng ở dạng bình thường.


5
Tôi biết mọi người nói bỏ qua hình thức đầu bình thường, nhưng bạn có thể đưa ra một ví dụ trong sơ đồ đó bạn có hình dạng đầu bình thường như thế nào không?
CMCDragonkai

28

Các chương trình Haskell là các biểu thức và chúng được điều hành bằng cách thực hiện đánh giá .

Để đánh giá một biểu thức, thay thế tất cả các ứng dụng chức năng theo định nghĩa của chúng. Thứ tự bạn làm điều này không quan trọng lắm, nhưng nó vẫn quan trọng: bắt đầu với ứng dụng ngoài cùng và tiến hành từ trái sang phải; điều này được gọi là đánh giá lười biếng .

Thí dụ:

   take 1 (1:2:3:[])
=> { apply take }
   1 : take (1-1) (2:3:[])
=> { apply (-)  }
   1 : take 0 (2:3:[])
=> { apply take }
   1 : []

Đánh giá dừng lại khi không còn ứng dụng chức năng nào để thay thế. Kết quả là ở dạng bình thường (hoặc giảm dạng bình thường , RNF). Bất kể bạn đánh giá một biểu thức theo thứ tự nào, bạn sẽ luôn kết thúc với cùng một hình thức bình thường (nhưng chỉ khi việc đánh giá chấm dứt).

Có một mô tả hơi khác nhau để đánh giá lười biếng. Cụ thể, nó nói rằng bạn chỉ nên đánh giá mọi thứ về hình dạng bình thường . Có chính xác ba trường hợp cho một biểu thức nằm trong WHNF:

  • Một nhà xây dựng: constructor expression_1 expression_2 ...
  • Hàm dựng sẵn có quá ít đối số, như (+) 2hoặcsqrt
  • Một biểu thức lambda: \x -> expression

Nói cách khác, phần đầu của biểu thức (tức là ứng dụng hàm ngoài cùng) không thể được đánh giá thêm nữa, nhưng đối số hàm có thể chứa các biểu thức không được đánh giá.

Ví dụ về WHNF:

3 : take 2 [2,3,4]   -- outermost function is a constructor (:)
(3+1) : [4..]        -- ditto
\x -> 4+5            -- lambda expression

Ghi chú

  1. "Đầu" trong WHNF không đề cập đến đầu danh sách, mà là ứng dụng chức năng ngoài cùng.
  2. Đôi khi, mọi người gọi các biểu thức không được đánh giá là "thunks", nhưng tôi không nghĩ đó là một cách tốt để hiểu nó.
  3. Hình thức đầu bình thường (HNF) không liên quan đến Haskell. Nó khác với WHNF ở chỗ cơ thể của biểu thức lambda cũng được đánh giá ở một mức độ nào đó.

Là việc sử dụng seqtrong foldl'lực lượng đánh giá từ WHNF để HNF?

1
@snmcdonald: Không, Haskell không sử dụng HNF. Đánh giá seq expr1 expr2sẽ đánh giá biểu thức đầu tiên expr1đến WHNF trước khi đánh giá biểu thức thứ hai expr2.
Apfelmus Heinrich

26

Một lời giải thích tốt với các ví dụ được đưa ra tại http://printoc.org/Weak+Head+N normal + Form Dạng bình thường đơn giản hóa ngay cả các bit của một biểu thức bên trong một trừu tượng hàm, trong khi dạng bình thường của đầu "yếu" dừng ở trừu tượng hàm .

Từ nguồn, nếu bạn có:

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

đó là ở dạng đầu yếu, nhưng không ở dạng bình thường ... vì ứng dụng có thể bị kẹt bên trong một chức năng chưa thể đánh giá được.

Đầu thực tế hình thức bình thường sẽ khó thực hiện hiệu quả. Nó sẽ yêu cầu chọc vào bên trong các chức năng. Vì vậy, lợi thế của dạng bình thường đầu yếu là bạn vẫn có thể thực hiện các chức năng như một loại mờ, và do đó nó tương thích hơn với các ngôn ngữ được biên dịch và tối ưu hóa.


12

WHNF không muốn đánh giá cơ thể của lambdas, vì vậy

WHNF = \a -> thunk
HNF = \a -> a + c

seq muốn đối số đầu tiên của nó nằm trong WHNF, vì vậy

let a = \b c d e -> (\f -> b + c + d + e + f) b
    b = a 2
in seq b (b 5)

đánh giá

\d e -> (\f -> 2 + 5 + d + e + f) 2

thay vì, những gì sẽ được sử dụng HNF

\d e -> 2 + 5 + d + e + 2

Hoặc tôi hiểu nhầm ví dụ, hoặc bạn trộn 1 và 2 trong WHNF và HNF.
Zhen

5

Về cơ bản, giả sử bạn có một số loại thunk , t.

Bây giờ, nếu chúng ta muốn đánh giá tWHNF hoặc NHF, giống nhau ngoại trừ các hàm, chúng ta sẽ thấy rằng chúng ta có được một cái gì đó như

t1 : t2ở đâu t1t2là thunks. Trong trường hợp này, t1sẽ là 0(hoặc đúng hơn, một thunk để 0không có thêm hộp thư)

seq$!đánh giá WHNF. Lưu ý rằng

f $! x = seq x (f x)

1
@snmcdonald Bỏ qua HNF. seq nói rằng khi điều này được đánh giá thành WHNF, hãy đánh giá đối số đầu tiên với WHNF.
thay thế
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.