Điểm nghiêm ngặt của Haskell là gì?


90

Tất cả chúng ta đều biết (hoặc nên biết) rằng Haskell lười biếng theo mặc định. Không có gì được đánh giá cho đến khi nó phải được đánh giá. Vì vậy, khi một cái gì đó phải được đánh giá? Có những điểm mà Haskell phải nghiêm khắc. Tôi gọi đây là "điểm nghiêm ngặt", mặc dù thuật ngữ cụ thể này không phổ biến như tôi đã nghĩ. Theo tôi:

Giảm (hoặc đánh giá) trong Haskell chỉ xảy ra ở các điểm nghiêm ngặt.

Vì vậy, câu hỏi là: những gì, chính xác , là những điểm nghiêm Haskell không? Trực giác của tôi nói rằng main, seq/ mô hình tiếng nổ, khớp mô hình và bất kỳ IOhành động nào được thực hiện thông qua mainlà những điểm nghiêm ngặt chính, nhưng tôi thực sự không biết tại sao tôi biết điều đó.

(Ngoài ra, nếu chúng không được gọi là "điểm nghiêm ngặt" thì chúng được gọi là gì?)

Tôi tưởng tượng một câu trả lời tốt sẽ bao gồm một số cuộc thảo luận về WHNF, v.v. Tôi cũng tưởng tượng nó có thể chạm vào giải tích lambda.


Chỉnh sửa: suy nghĩ bổ sung về câu hỏi này.

Như tôi đã suy nghĩ về câu hỏi này, tôi nghĩ rằng sẽ rõ ràng hơn nếu thêm điều gì đó vào định nghĩa về điểm nghiêm ngặt. Các điểm nghiêm ngặt có thể có nhiều bối cảnh khác nhauđộ sâu (hoặc độ nghiêm ngặt) khác nhau . Quay trở lại định nghĩa của tôi rằng "giảm Haskell chỉ xảy ra ở các điểm nghiêm ngặt", chúng ta hãy thêm vào định nghĩa đó điều khoản này: "điểm nghiêm ngặt chỉ được kích hoạt khi bối cảnh xung quanh của nó được đánh giá hoặc giảm."

Vì vậy, hãy để tôi cố gắng giúp bạn bắt đầu với loại câu trả lời mà tôi muốn. mainlà một điểm nghiêm ngặt. Nó được chỉ định đặc biệt là điểm nghiêm ngặt chính của bối cảnh: chương trình. Khi chương trình ( maincủa ngữ cảnh) được đánh giá, điểm nghiêm ngặt của main được kích hoạt. Độ sâu của Main là tối đa: nó phải được đánh giá đầy đủ. Chính thường bao gồm các hành động IO, cũng là các điểm nghiêm ngặt, có ngữ cảnh main.

Bây giờ bạn thử: thảo luận seqvà so khớp mẫu trong các thuật ngữ này. Giải thích các sắc thái của ứng dụng chức năng: nó nghiêm ngặt như thế nào? Làm thế nào là nó không? Về deepseqthì sao? letvà các casecâu lệnh? unsafePerformIO? Debug.Trace? Định nghĩa cấp cao nhất? Các kiểu dữ liệu nghiêm ngặt? Bang mẫu? Vv. Có bao nhiêu trong số những mặt hàng này có thể được mô tả chỉ bằng cách đối sánh seq hoặc mẫu?


10
Danh sách trực quan của bạn có lẽ không trực giao lắm. Tôi nghi ngờ điều đó seqvà khớp mẫu là đủ, với phần còn lại được xác định theo các điều kiện đó. Tôi nghĩ rằng đối sánh mẫu đảm bảo tính nghiêm ngặt của các IOhành động chẳng hạn.
CA McCann

Các số nguyên thủy, chẳng hạn như +trên các kiểu số tích hợp cũng buộc phải nghiêm ngặt và tôi cho rằng điều này cũng áp dụng cho các lệnh gọi FFI thuần túy.
hammar

4
Dường như có hai khái niệm đang bị nhầm lẫn ở đây. Đối sánh mẫu và các mẫu seq và bang là những cách mà một biểu thức có thể trở nên chặt chẽ trong các biểu thức con của nó - nghĩa là, nếu biểu thức hàng đầu được đánh giá, thì biểu thức con cũng vậy. Mặt khác, các hành động IO thực hiện chính là cách đánh giá bắt đầu . Đây là những thứ khác nhau và đó là một loại lỗi khi gộp chúng vào cùng một danh sách.
Chris Smith

@ChrisSmith Tôi không cố gắng nhầm lẫn hai trường hợp khác nhau đó; nếu có bất cứ điều gì tôi yêu cầu làm rõ thêm về cách họ tương tác. Sự nghiêm khắc xảy ra bằng cách nào đó, và cả hai trường hợp đều quan trọng, mặc dù khác nhau, các phần của sự nghiêm khắc "xảy ra". (và @ monadic: ಠ_ಠ)
Dan Burton

Nếu bạn muốn / cần phòng để thảo luận về các khía cạnh của câu hỏi này, mà không cần cố gắng một câu trả lời đầy đủ, cho phép tôi đề nghị sử dụng các ý kiến trên / r / Haskell bài viết của tôi cho câu hỏi này
Dan Burton

Câu trả lời:


46

Một nơi tốt để bắt đầu là hiểu bài báo này: Ngữ nghĩa tự nhiên cho sự lười biếng (Launchbury). Điều đó sẽ cho bạn biết khi nào các biểu thức được đánh giá cho một ngôn ngữ nhỏ tương tự như Core của GHC. Sau đó, câu hỏi còn lại là làm thế nào để ánh xạ Haskell đầy đủ đến Core, và hầu hết bản dịch đó được đưa ra bởi chính báo cáo Haskell. Trong GHC, chúng tôi gọi quá trình này là "desugaring", vì nó loại bỏ đường cú pháp.

Chà, đó không phải là toàn bộ câu chuyện, bởi vì GHC bao gồm toàn bộ sự tối ưu hóa giữa quá trình gỡ rối và tạo mã, và nhiều phép biến đổi này sẽ sắp xếp lại Core để mọi thứ được đánh giá vào những thời điểm khác nhau (đặc biệt phân tích mức độ nghiêm ngặt sẽ khiến mọi thứ được đánh giá sớm hơn). Vì vậy, để thực sự hiểu chương trình của bạn sẽ được đánh giá như thế nào , bạn cần nhìn vào Core do GHC sản xuất.

Có lẽ câu trả lời này có vẻ hơi trừu tượng đối với bạn (tôi không đề cập cụ thể đến các mẫu bang hoặc seq), nhưng bạn đã yêu cầu một cái gì đó chính xác , và đây là điều tốt nhất chúng tôi có thể làm.


18
Tôi luôn thấy thú vị rằng trong cái mà GHC gọi là "gỡ rối", đoạn cú pháp bị loại bỏ bao gồm cú pháp thực tế của chính ngôn ngữ Haskell ... ngụ ý, có vẻ như GHC thực sự là một trình biên dịch tối ưu hóa cho GHC Ngôn ngữ cốt lõi, tình cờ cũng bao gồm một giao diện người dùng rất phức tạp để dịch Haskell thành Core. :]
CA McCann

Mặc dù vậy, các hệ thống kiểu chữ không phát triển chính xác ... đặc biệt, nhưng không chỉ liên quan đến việc dịch kính chữ sang các từ điển rõ ràng, như tôi nhớ lại. Và tất cả những thứ TF / GADT mới nhất, như tôi hiểu, đã làm cho khoảng cách đó vẫn rộng hơn.
sclv


20

Tôi có thể sẽ viết lại câu hỏi này như, Haskell sẽ đánh giá một biểu thức trong trường hợp nào? (Có lẽ giải quyết "dạng đầu bình thường đến yếu.")

Đối với ước lượng gần đúng đầu tiên, chúng tôi có thể chỉ định điều này như sau:

  • Việc thực hiện các hành động IO sẽ đánh giá bất kỳ biểu thức nào mà chúng “cần”. (Vì vậy, bạn cần biết liệu hành động IO có được thực thi hay không, ví dụ: tên của nó là chính, hoặc nó được gọi từ chính VÀ bạn cần biết hành động đó cần gì.)
  • Một biểu thức đang được đánh giá (này, đó là một định nghĩa đệ quy!) Sẽ đánh giá bất kỳ biểu thức nào mà nó cần.

Từ danh sách trực quan của bạn, các hành động chính và IO thuộc danh mục đầu tiên, đối sánh seq và mẫu thuộc danh mục thứ hai. Nhưng tôi nghĩ rằng danh mục đầu tiên phù hợp hơn với ý tưởng của bạn về "điểm nghiêm ngặt", bởi vì đó thực tế là cách chúng tôi làm cho việc đánh giá trong Haskell trở nên có thể quan sát được những hiệu ứng cho người dùng.

Đưa ra tất cả các chi tiết cụ thể là một nhiệm vụ lớn, vì Haskell là một ngôn ngữ lớn. Nó cũng khá tinh tế, bởi vì Concurrent Haskell có thể đánh giá mọi thứ một cách suy đoán, mặc dù cuối cùng chúng tôi không sử dụng kết quả: đây là giống thứ ba của những thứ gây ra đánh giá. Loại thứ hai được nghiên cứu khá kỹ: bạn muốn xem xét tính chặt chẽ của các chức năng liên quan. Loại đầu tiên cũng có thể được coi là một loại "nghiêm ngặt", mặc dù điều này hơi khó hiểu bởi vì evaluate xseq x $ return ()thực tế là những thứ khác nhau! Bạn có thể xử lý nó đúng cách nếu bạn cung cấp một số loại ngữ nghĩa cho đơn nguyên IO (việc truyền RealWorld#mã thông báo rõ ràng hoạt động cho các trường hợp đơn giản), nhưng tôi không biết liệu có tên cho loại phân tích mức độ nghiêm ngặt phân tầng này nói chung hay không.


17

C có khái niệm về điểm trình tự , là các đảm bảo cho các hoạt động cụ thể rằng một toán hạng sẽ được đánh giá trước toán hạng kia. Tôi nghĩ đó là khái niệm hiện có gần nhất, nhưng thuật ngữ tương đương về cơ bản điểm nghiêm ngặt (hoặc có thể là điểm lực ) phù hợp hơn với suy nghĩ của Haskell.

Trên thực tế, Haskell không phải là một ngôn ngữ hoàn toàn lười biếng: ví dụ, đối sánh mẫu thường nghiêm ngặt (Vì vậy, việc thử đối sánh mẫu buộc đánh giá phải xảy ra ít nhất đủ xa để chấp nhận hoặc từ chối đối sánh.

Các lập trình viên cũng có thể sử dụng seqnguyên thủy để buộc một biểu thức đánh giá bất kể kết quả có được sử dụng hay không.

$!được định nghĩa về seq.

- Lười biếng so với không nghiêm khắc .

Vì vậy, suy nghĩ của bạn về !/ $!seqvề cơ bản là đúng, nhưng so khớp mẫu phải tuân theo các quy tắc tinh tế hơn. Tất nhiên, bạn luôn có thể sử dụng ~để buộc kết hợp mẫu giày lười. Một điểm thú vị từ cùng một bài báo:

Bộ phân tích độ nghiêm ngặt cũng tìm kiếm các trường hợp mà biểu thức con luôn được yêu cầu bởi biểu thức bên ngoài và chuyển chúng thành đánh giá háo hức. Nó có thể làm được điều này bởi vì ngữ nghĩa (về "đáy") không thay đổi.

Hãy tiếp tục đi xuống lỗ thỏ và xem tài liệu để biết cách tối ưu hóa do GHC thực hiện:

Phân tích mức độ nghiêm ngặt là một quá trình mà GHC cố gắng xác định, tại thời điểm biên dịch, dữ liệu nào chắc chắn sẽ 'luôn cần thiết'. Sau đó, GHC có thể xây dựng mã để chỉ tính toán những dữ liệu đó, thay vì quy trình bình thường (chi phí cao hơn) để lưu trữ phép tính và thực hiện nó sau này.

- GHC Optimisations: Phân tích mức độ nghiêm ngặt .

Nói cách khác, mã nghiêm ngặt có thể được tạo ở bất kỳ đâu dưới dạng tối ưu hóa, bởi vì việc tạo dữ liệu thu thập là tốn kém không cần thiết khi dữ liệu sẽ luôn cần thiết (và / hoặc chỉ có thể được sử dụng một lần).

… Không thể thực hiện thêm đánh giá về giá trị; nó được cho là ở dạng bình thường . Nếu chúng ta đang ở bất kỳ bước trung gian nào để chúng ta đã thực hiện ít nhất một số đánh giá trên một giá trị, thì nó ở dạng bình thường đầu yếu (WHNF). (Ngoài ra còn có 'dạng đầu bình thường', nhưng nó không được sử dụng trong Haskell.) Đánh giá đầy đủ thứ gì đó trong WHNF sẽ giảm nó thành thứ gì đó ở dạng bình thường…

- Wikibooks Haskell: Sự lười biếng

(Một thuật ngữ ở dạng head normal nếu không có beta-redx ở vị trí đầu 1. Redx là head redx nếu nó chỉ được đứng trước bởi các trình tóm tắt lambda của không phải redxes 2. ) Vì vậy, khi bạn bắt đầu thực hiện một cú đánh, bạn đang làm việc trong WHNF; Khi không còn những đòn roi để ép buộc, bạn đang ở trạng thái bình thường. Một điểm thú vị khác:

… Nếu tại một thời điểm nào đó, chúng tôi cần, chẳng hạn như in ra cho người dùng, chúng tôi cần phải đánh giá đầy đủ về nó…

Mà tự nhiên ngụ ý rằng, trên thực tế, bất kỳ IOhành động thực hiện từ main thực hiện đánh giá hiệu lực, mà nên được rõ ràng xem xét rằng chương trình Haskell làm, trên thực tế, làm việc. Bất cứ thứ gì cần trải qua trình tự được xác định trong mainphải ở dạng bình thường và do đó phải được đánh giá nghiêm ngặt.

Tuy nhiên, CA McCann đã hiểu đúng trong các bình luận: điều duy nhất đặc biệt mainlà nó mainđược định nghĩa là đặc biệt; khớp mẫu trên phương thức khởi tạo là đủ để đảm bảo trình tự được áp đặt bởi IOđơn nguyên. Về mặt đó, chỉ seqvà khớp mẫu là cơ bản.


4
Trên thực tế, câu trích dẫn "nếu tại một thời điểm nào đó chúng tôi cần, chẳng hạn như in z ra cho người dùng, chúng tôi cần đánh giá đầy đủ về nó" là không hoàn toàn chính xác. Nó cũng nghiêm ngặt như Showví dụ cho giá trị được in.
nominolo

10

Haskell là AFAIK không phải là một ngôn ngữ lười biếng thuần túy, mà là một ngôn ngữ không nghiêm ngặt. Điều này có nghĩa là nó không nhất thiết phải đánh giá các điều khoản vào thời điểm cuối cùng có thể.

Bạn có thể tìm thấy một nguồn tốt cho mô hình "lười biếng" của haskell tại đây: http://en.wikibooks.org/wiki/Haskell/Laziness

Về cơ bản, điều quan trọng là phải hiểu sự khác biệt giữa một cú đánh và dạng tiêu đề yếu WHNF.

Sự hiểu biết của tôi là haskell kéo các phép tính đi ngược lại so với các ngôn ngữ mệnh lệnh. Điều này có nghĩa là trong trường hợp không có các mẫu "seq" và tiếng nổ, nó cuối cùng sẽ là một số loại tác dụng phụ buộc đánh giá một cú đánh, điều này có thể gây ra các đánh giá trước (sự lười biếng thực sự).

Vì điều này sẽ dẫn đến rò rỉ không gian khủng khiếp, trình biên dịch sau đó sẽ tìm ra cách thức và thời điểm để đánh giá các lần thu hồi trước thời hạn để tiết kiệm dung lượng. Sau đó, lập trình viên có thể hỗ trợ quá trình này bằng cách đưa ra các chú thích về mức độ nghiêm ngặt (en.wikibooks.org/wiki/Haskell/Strictness, www.haskell.org/haskellwiki/Performance/Strictness) để giảm hơn nữa việc sử dụng không gian ở dạng côn lồng nhau.

Tôi không phải là chuyên gia về ngữ nghĩa hoạt động của haskell, vì vậy tôi sẽ chỉ để lại liên kết như một tài nguyên.

Một số tài nguyên khác:

http://www.haskell.org/haskellwiki/Performance/Laziness

http://www.haskell.org/haskellwiki/Haskell/Lazy_Evaluation


6

Lười biếng không có nghĩa là không làm gì cả. Bất cứ khi nào mẫu chương trình của bạn khớp với một casebiểu thức, nó sẽ đánh giá một thứ gì đó - dù sao cũng chỉ là đủ. Nếu không, nó không thể tìm ra RHS nào để sử dụng. Không thấy bất kỳ biểu thức chữ hoa chữ thường nào trong mã của bạn? Đừng lo lắng, trình biên dịch đang dịch mã của bạn sang một dạng Haskell rút gọn mà chúng khó tránh khỏi việc sử dụng.

Đối với một người mới bắt đầu, một nguyên tắc cơ bản letlà lười biếng, caselười biếng hơn.


2
Lưu ý rằng mặc dù caseluôn buộc đánh giá trong GHC Core, nhưng nó không thực hiện điều này trong Haskell thông thường. Ví dụ, hãy thử case undefined of _ -> 42.
hammar

2
casetrong GHC Core đánh giá đối số của nó thành WHNF, trong khi casetrong Haskell đánh giá đối số của nó nhiều khi cần thiết để chọn nhánh thích hợp. Trong ví dụ của hammar, điều đó không hoàn toàn, nhưng case 1:undefined of x:y:z -> 42nó đánh giá sâu hơn WHNF.
Tối đa

Và cũng case something of (y,x) -> (x,y)không cần đánh giá somethinggì cả. Điều này đúng cho tất cả các loại sản phẩm.
Ingo

@Ingo - điều đó không chính xác. somethingsẽ cần được đánh giá tới WHNF để đạt được hàm tạo tuple.
John L

John - Tại sao? Chúng tôi biết rằng nó phải là một bộ tuple, vậy đâu là điểm đánh giá nó? Nó đủ nếu x và y bị ràng buộc với mã đánh giá bộ tuple và trích xuất vị trí thích hợp, nếu bản thân chúng cần thiết.
Ingo

4

Đây không phải là một câu trả lời đầy đủ nhắm đến nghiệp chướng, mà chỉ là một phần của câu đố - trong phạm vi mà đây là về ngữ nghĩa, hãy nhớ rằng có nhiều chiến lược đánh giá cung cấp cùng một ngữ nghĩa. Một ví dụ điển hình ở đây - và dự án cũng nói lên cách chúng ta thường nghĩ về ngữ nghĩa Haskell - là dự án Eager Haskell, đã thay đổi hoàn toàn các chiến lược đánh giá trong khi vẫn giữ nguyên ngữ nghĩa: http://csg.csail.mit.edu/ pubs / haskell.html


2

Trình biên dịch Glasgow Haskell dịch mã của bạn sang một ngôn ngữ giống như phép tính Lambda được gọi là cốt lõi . Trong ngôn ngữ này, một cái gì đó sẽ được đánh giá, bất cứ khi nào bạn khớp với mẫu đó bằng một câu lệnh case. Vì vậy, nếu một hàm được gọi, hàm tạo ngoài cùng và chỉ nó (nếu không có trường bắt buộc) sẽ được đánh giá. Bất cứ thứ gì khác được đóng hộp trong nháy mắt. (Thunks được giới thiệu bởi letcác ràng buộc).

Tất nhiên đây không phải là chính xác những gì xảy ra trong ngôn ngữ thực. Trình biên dịch chuyển đổi Haskell thành Core theo một cách rất phức tạp, khiến nhiều thứ có thể trở nên lười biếng và bất cứ thứ gì luôn cần đến sự lười biếng. Ngoài ra, có các giá trị và bộ giá trị không được đóng hộp luôn nghiêm ngặt.

Nếu bạn cố gắng đánh giá một chức năng bằng tay, về cơ bản bạn có thể nghĩ:

  • Cố gắng đánh giá hàm tạo ngoài cùng của trả về.
  • Nếu cần bất kỳ điều gì khác để có được kết quả (nhưng chỉ khi nó thực sự cần thiết) cũng sẽ được đánh giá. Thứ tự không quan trọng.
  • Trong trường hợp IO, bạn phải đánh giá kết quả của tất cả các câu lệnh từ câu đầu tiên đến câu cuối cùng trong đó. Điều này phức tạp hơn một chút, vì đơn nguyên IO thực hiện một số thủ thuật để buộc đánh giá theo một thứ tự cụ 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.