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.
- 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ử f
và x
liê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 map
có 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.
- 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 consumer
kế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 . producer
là 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ả producer
trước khi đưa ra consumer
. Trong thực tế, ngay khi consumer
cần phần tử đầu tiên của danh sách đầu vào, thì mã tương ứng từ producer
sẽ 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 consumer
vàproducer
sẽ đượ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.SatSolver
thư 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ư getSolution
và getAllSolutions
. 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 SatSolver
giá 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 MonadPlus
lớ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 SatSolver
các giá trị trả về kết quả của chúng được gói vào một MonadPlus
thể hiện. Vì vậy, giả sử bạn có công thức p || !q
và 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ý SatSolver
cấ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 solve
trong 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 Show
ví 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
. Maybe
là 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 solve
như thể nó trả về một Maybe SatSolver
giá 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, task
và task2
, 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 Alternative
typeclass, đó 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 Alternative
lớ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.