Lý do đằng sau làm cho tính không xác định là một tính năng của Haskell là gì?


8

Chúng ta biết rằng trong Prolog - các vị từ không xác định là một tính năng được sử dụng để giảm bớt các vấn đề tổ hợp .

Trong Haskell, chúng ta thấy một số hành vi không xác định tương tự như Prolog trong Danh sách đơn nguyên .

Trong Haskell, chúng ta cũng thấy tính không xác định trong việc chọn thứ tự đánh giá thunk :

Nhưng không có gì để nói với GHC nên chọn loại nào trong số các thunks đó trước, và do đó GHC hoàn toàn tự do lựa chọn bất cứ thứ gì để đánh giá trước.

Điều này thật hấp dẫn - và có phần giải phóng. Tôi đang tự hỏi (ngoài việc quan tâm đến các vấn đề logic như tám nữ hoàng ) thì nguyên tắc này phục vụ cho nguyên tắc gì. Có một số ý tưởng lớn hoặc vấn đề lớn mà họ đang cố gắng giải quyết với chủ nghĩa không xác định?

Câu hỏi của tôi là: lý do đằng sau làm cho tính không xác định trở thành một đặc điểm của Haskell là gì?


Bởi vì đánh giá thunk Haskell là tham chiếu trong suốt (thuần túy), vì vậy thứ tự không có ảnh hưởng đến kết quả. Đó không phải là lập trình không xác định theo nghĩa Prolog, nơi một trong nhiều kết quả có thể được chọn.
Jack

Chắc chắn, nhưng khi bạn đang thiết kế một Trình biên dịch chắc chắn sẽ cần nhiều công việc hơn để thêm tính không xác định? Điều gì thúc đẩy bạn đưa công việc làm thêm vào?
hawkeye

2
Các tiêu chuẩn không xác định thứ tự đánh giá. Tiêu chuẩn không buộc trình biên dịch chọn một thứ tự cụ thể. Điều đó không có nghĩa là trình biên dịch bằng cách nào đó chọn ngẫu nhiên hoặc đại loại như thế. Trình biên dịch không chọn thứ tự một cách xác định, theo quy tắc riêng của nó. Chỉ là các quy tắc không được xác định bởi tiêu chuẩn và được để lại dưới dạng chi tiết triển khai trình biên dịch.
Fyodor Soikin

Cảm ơn @FyodorSoikin - thật hữu ích. Tôi không quan tâm đến việc đó là trình biên dịch hay tiêu chuẩn - chỉ đơn thuần là ý định đằng sau sự lựa chọn thiết kế. Tại sao làm điều đó? Bạn nhận được gì từ điều này?
hawkeye

1
Bằng cách không bao gồm một thứ tự đánh giá cụ thể trong tiêu chuẩn, bạn có thể tự do cho trình biên dịch thay đổi nó theo cách phù hợp, do đó cho phép tối ưu hóa. Đây là thứ khá chuẩn. Ngay cả các bộ vi xử lý cũng gây rối với thứ tự chỉ dẫn nếu chúng có thể chứng minh rằng nó không ảnh hưởng đến kết quả.
Fyodor Soikin

Câu trả lời:


19

Mặc dù đúng là cả hai khía cạnh được trích dẫn trong các câu hỏi đều xuất hiện dưới dạng các hình thức không xác định, nhưng chúng thực sự khác nhau cả về cách chúng hoạt động và trong mục tiêu của chúng. Do đó, bất kỳ câu trả lời phải được chia thành hai phần.

Lệnh đánh giá

Haskell bắt buộc không có thứ tự thực hiện cụ thể nào trong việc đánh giá thunks vì hai lý do.

  1. Trước hết, Haskell là một ngôn ngữ hoàn toàn có chức năng, vì vậy bạn được đảm bảo có tính minh bạch tham chiếu (nếu bạn không làm phiền với unsafePerformIO& co.). Điều này có nghĩa là việc đánh giá bất kỳ biểu thức nào, ví dụ f x sẽ dẫn đến kết quả tương tự cho dù nó được đánh giá bao nhiêu lần và cho dù nó được đánh giá ở phần nào của chương trình (giả sử fxliên kết với cùng các giá trị trong phạm vi được xem xét, của khóa học). Do đó, bắt buộc bất kỳ thứ tự thực hiện cụ thể nào sẽ không có mục đích , bởi vì thay đổi nó sẽ không tạo ra bất kỳ hiệu ứng có thể quan sát được trong kết quả của chương trình. Về vấn đề này, đây không thực sự là một hình thức không thuyết phục, ít nhất là không phải là bất kỳ hình thức nào có thể quan sát được một, vì các thực thi khác nhau có thể có của chương trình đều tương đương về mặt ngữ nghĩa.

Tuy nhiên, việc thay đổi thứ tự thực hiện có thể ảnh hưởng đến hiệu suất của chương trình và để trình biên dịch tự do thao tác theo thứ tự là cơ bản để đạt được hiệu suất đáng kinh ngạc mà trình biên dịch như GHC có thể có được khi biên dịch cao như vậy trình độ ngôn ngữ. Ví dụ, suy nghĩ về một chuyển đổi hợp nhất dòng cổ điển:

map f . map g = map (f.g)

Sự bình đẳng này đơn giản có nghĩa là việc áp dụng hai hàm cho một danh sách mapcó cùng kết quả hơn là áp dụng một lần thành phần của hai hàm thay thế. Điều này chỉ đúng vì tính minh bạch tham chiếu và là một loại chuyển đổi mà trình biên dịch luôn có thểáp dụng, không có vấn đề gì. Nếu thay đổi thứ tự thực hiện của ba hàm có ảnh hưởng đến kết quả của biểu thức, thì điều này là không thể. Mặt khác, biên dịch nó ở dạng thứ hai thay vì đầu tiên có thể có tác động hiệu suất rất lớn, vì nó tránh việc xây dựng một danh sách tạm thời và chỉ đi qua danh sách một lần. Việc GHC có thể tự động áp dụng chuyển đổi như vậy là kết quả trực tiếp của tính minh bạch tham chiếu và thứ tự thực hiện không cố định và đó là một trong những khía cạnh quan trọng của hiệu suất tuyệt vời mà Haskell có thể đạt được.

  1. Haskell là một ngôn ngữ lười biếng . Điều này có nghĩa là không cần phải đánh giá bất kỳ biểu thức cụ thể nào trừ khi kết quả của nó thực sự cần thiết và điều này cũng có thể không bao giờ. Lười biếng là một tính năng đôi khi được tranh luận và một số ngôn ngữ chức năng khác tránh nó hoặc giới hạn nó để chọn tham gia, nhưng trong ngữ cảnh của Haskell, đây là một tính năng chính trong cách sử dụng và thiết kế ngôn ngữ. Sự lười biếng là một công cụ mạnh mẽ khác nằm trong tay trình tối ưu hóa của trình biên dịch và quan trọng nhất là cho phép mã được soạn thảo dễ dàng.

Để xem ý tôi là gì khi dễ sáng tác, hãy xem xét một ví dụ khi bạn có một hàm producer :: Int -> [Int]thực hiện một số tác vụ phức tạp để tính toán danh sách một số loại dữ liệu từ một đối số đầu vào và consumer :: [Int] -> Intđó là một hàm phức tạp khác tính toán từ một danh sách dữ liệu đầu vào. Bạn đã viết riêng chúng, kiểm tra chúng, tối ưu hóa chúng rất cẩn thận và sử dụng chúng một cách cô lập trong các dự án khác nhau. Bây giờ trong dự án tiếp theo, bạn phải gọi consumerkết quả củaproducer. Trong một ngôn ngữ không lười biếng, điều này có thể không tối ưu, vì có thể trường hợp tác vụ kết hợp có thể được thực hiện hiệu quả nhất mà không cần xây dựng cấu trúc danh sách tạm thời. Để có được một triển khai được tối ưu hóa, bạn sẽ phải thực hiện lại nhiệm vụ kết hợp từ đầu, kiểm tra lại và tối ưu hóa lại nó.

Trong haskell điều này là không cần thiết, và gọi thành phần của hai chức năng consumer . producerlà hoàn toàn tốt. Lý do là chương trình không bắt buộc phải thực sự tạo ra toàn bộ kết quả producertrước khi đưa ra consumer. Trong thực tế, ngay khi consumercần phần tử đầu tiên của danh sách đầu vào, thì mã tương ứng từ producersẽ chạy đến mức cần thiết để sản xuất nó, và không còn nữa. Khi yếu tố thứ hai là cần thiết, nó sẽ được tính toán. Nếu một số phần tử sẽ không cần thiết consumer, nó sẽ không được tính toán, tiết kiệm hiệu quả các tính toán vô dụng. Việc thực hiện consumerproducersẽ được xen kẽ một cách hiệu quả, không chỉ tránh việc sử dụng bộ nhớ của cấu trúc danh sách trung gian, mà còn có thể tránh các tính toán vô dụng, và việc thực thi có thể tương tự như phiên bản kết hợp viết tay mà bạn phải tự viết. Đây là những gì tôi có nghĩa là thành phần . Bạn đã có hai đoạn mã được kiểm tra và thực hiện tốt và bạn có thể soạn chúng để lấy miễn phí một đoạn mã được kiểm tra tốt và hiệu suất.

Đơn nguyên không phá hủy

Việc sử dụng các hành vi không xác định được cung cấp bởi các đơn vị Danh sách và các đơn vị tương tự thay vào đó là hoàn toàn khác nhau. Ở đây, vấn đề không phải là việc cung cấp cho trình biên dịch các phương tiện để tối ưu hóa chương trình của bạn, mà là các tính toán thể hiện rõ ràng và chính xác vốn không đặc biệt.

Một ví dụ về những gì tôi muốn nói là được cung cấp bởi giao diện của Data.Boolean.SatSolverthư viện. Nó cung cấp một bộ giải SAT DPLL rất đơn giản được triển khai trong Haskell. Như bạn có thể biết, việc giải bài toán SAT liên quan đến việc tìm một phép gán các biến boolean thỏa mãn công thức boolean. Tuy nhiên, có thể có nhiều hơn một bài tập như vậy, và người ta có thể cần tìm bất kỳ bài tập nào trong số chúng, hoặc lặp đi lặp lại trên tất cả chúng, tùy thuộc vào ứng dụng. Do đó, nhiều thư viện sẽ có hai chức năng khác nhau như getSolutiongetAllSolutions. Thư viện này thay vào đó chỉ có một chức năng solve, với loại sau:

solve :: MonadPlus m => SatSolver -> m SatSolver

Ở đây, kết quả là một SatSolvergiá trị được bọc bên trong một đơn loại không xác định, tuy nhiên bị hạn chế để thực hiện MonadPluslớp loại. Lớp loại này là lớp đại diện cho loại không xác định được cung cấp bởi danh sách đơn nguyên, và trong thực tế danh sách là các trường hợp. Tất cả các hàm hoạt động trên SatSolvercác giá trị trả về kết quả của chúng được gói vào một MonadPlusthể hiện. Vì vậy, giả sử bạn có công thức p || !qvà bạn muốn giải quyết nó bằng cách hạn chế kết quả được đặt thành qđúng, thì cách sử dụng là như sau (các biến được đánh số thay vì được xác định theo tên):

expr = Var 1 :||: Not (Var 2)

task :: MonadPlus m => m SatSolver
task = do
  pure newSatSolver
  assertTrue expr
  assertTrue (Var 2)

Lưu ý cách thể hiện đơn nguyên và ký hiệu che dấu tất cả các chi tiết cấp thấp về cách các hàm quản lý SatSolvercấu trúc dữ liệu và cho phép chúng tôi thể hiện rõ ràng ý định của mình.

Bây giờ, nếu bạn muốn có được tất cả các kết quả, bạn chỉ cần sử dụng solvetrong ngữ cảnh mà kết quả của nó phải là một danh sách. Sau đây sẽ in tất cả các kết quả trên màn hình (giả sử một Showví dụ SatSolver, không tồn tại, nhưng hãy tha thứ cho tôi điểm này).

main = sequence . map print . solve task

Tuy nhiên, danh sách không phải là trường hợp duy nhất của MonadPlus. Maybelà một ví dụ khác. Vì vậy, nếu bạn chỉ cần một giải pháp, bất kể là giải pháp nào, bạn chỉ có thể sử dụng solvenhư thể nó trả về một Maybe SatSolvergiá trị:

main = case solve task of 
  Nothing -> putStrLn "No solution"
  Just result -> print result

Bây giờ giả sử rằng bạn có hai nhiệm vụ để xây dựng, tasktask2, và bạn muốn để có được một giải pháp cho một trong hai. Một lần nữa, mọi thứ kết hợp lại với nhau để cho phép chúng tôi soạn các khối xây dựng có sẵn:

combinedTask = task <|> task2

trong đó <|>một hoạt động nhị phân được cung cấp bởi Alternativetypeclass, đó là một siêu lớp MonadPlus. Điều này một lần nữa cho chúng tôi thể hiện rõ ràng ý định của chúng tôi, sử dụng lại mã mà không thay đổi. Chủ nghĩa không điều kiện được thể hiện rõ ràng bằng mã, không bị chôn vùi dưới các chi tiết về cách thức không thuyết phục được thực hiện. Tôi đề nghị bạn hãy xem các tổ hợp được xây dựng trên đầu Alternativelớp loại để có thêm ví dụ.

Các đơn vị không đặc thù như danh sách không chỉ là một cách để thể hiện các bài tập hay mà còn cung cấp một cách để thiết kế mã thanh lịch và có thể tái sử dụng, thể hiện rõ ràng ý định trong việc thực hiện các nhiệm vụ vốn không phải là không xác định.


Tôi không nghĩ rằng taskviệc thực hiện của bạn là hoàn toàn đúng. assertTruemất hai tham số và bạn chỉ đưa ra một tham số. Bạn vẫn cần thêm một chút rõ ràng hơn về SatSolvergiá trị giữa các hàm nếu bạn sẽ sử dụng doký hiệu.
4 lâu đài

Ah! Phiên bản đầu tiên tôi đang viết đã sử dụng một chuỗi >> =, vì vậy bạn có thể (và phải) tránh đặt tên đối số. Đây có vẻ là một trường hợp mà ký hiệu không dài dòng hơn. Vui lòng chỉnh sửa hoặc tôi sẽ thực hiện ngay khi tôi quay lại văn phòng.
gigabyte
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.