Là đơn nguyên IO về mặt kỹ thuật không chính xác?


12

Trên wiki haskell có ví dụ sau về việc sử dụng đơn vị IO có điều kiện (xem tại đây) .

when :: Bool -> IO () -> IO ()
when condition action world =
    if condition
      then action world
      else ((), world)

Lưu ý rằng trong ví dụ này, định nghĩa IO ađược đưa ra là RealWorld -> (a, RealWorld)làm cho mọi thứ dễ hiểu hơn.

Đoạn mã này có điều kiện thực hiện một hành động trong đơn vị IO. Bây giờ, giả định rằng conditionFalse, hành động actionnên không bao giờ được thực thi. Sử dụng ngữ nghĩa lười biếng này thực sự sẽ là trường hợp. Tuy nhiên, điều lưu ý ở đây là Haskell nói về mặt kỹ thuật không nghiêm ngặt. Điều này có nghĩa là trình biên dịch được phép chạy, ví dụ, chạy action worldtrên một luồng khác và sau đó loại bỏ tính toán đó khi phát hiện ra nó không cần đến nó. Tuy nhiên, vào thời điểm đó, các tác dụng phụ đã xảy ra.

Bây giờ, người ta có thể thực hiện đơn vị IO theo cách mà các hiệu ứng phụ chỉ được lan truyền khi toàn bộ chương trình kết thúc và chúng ta biết chính xác những tác dụng phụ nào sẽ được thực hiện. Tuy nhiên, đây không phải là trường hợp, vì có thể viết các chương trình vô hạn trong Haskell, rõ ràng có tác dụng phụ trung gian.

Điều này có nghĩa là đơn nguyên IO sai về mặt kỹ thuật, hay có điều gì khác ngăn điều này xảy ra?


Chào mừng bạn đến với Khoa học máy tính ! Câu hỏi của bạn không có chủ đề ở đây: chúng tôi xử lý các câu hỏi về khoa học máy tính , không phải câu hỏi lập trình (xem Câu hỏi thường gặp của chúng tôi ). Câu hỏi của bạn có thể thuộc chủ đề về Stack Overflow .
dkaeae

2
Theo tôi đây là một câu hỏi về khoa học máy tính, bởi vì nó liên quan đến ngữ nghĩa lý thuyết của Haskell, chứ không phải với một câu hỏi lập trình thực tế.
Lasse

4
Tôi không quá quen thuộc với lý thuyết ngôn ngữ lập trình, nhưng tôi nghĩ câu hỏi này thuộc chủ đề ở đây. Nó có thể hữu ích nếu bạn làm rõ "sai" nghĩa là gì ở đây. Tài sản nào bạn nghĩ rằng đơn nguyên IO có mà nó không nên có?
Thằn lằn rời rạc

1
Chương trình này không được đánh máy tốt. Tôi không chắc những gì bạn thực sự muốn viết. Định nghĩa whenlà có thể đánh máy, nhưng nó không có kiểu mà bạn khai báo, và tôi không thấy điều gì làm cho mã đặc biệt này thú vị.
Gilles 'SO- ngừng trở thành ác quỷ'

2
Chương trình này được lấy nguyên văn từ trang Haskell-wiki được liên kết trực tiếp ở trên. Nó thực sự không gõ. Điều này là do nó được viết theo giả định IO ađược xác định là RealWorld -> (a, RealWorld), để làm cho các phần bên trong của IO dễ đọc hơn.
Lasse

Câu trả lời:


12

Đây là một "giải thích" đề xuất của các IOđơn nguyên. Nếu bạn muốn thực hiện "giải thích" này một cách nghiêm túc, thì bạn cần nghiêm túc thực hiện "RealWorld". Không liên quan dù action worldđược đánh giá theo suy đoán hay không, actionkhông có bất kỳ tác dụng phụ nào, tác dụng của nó, nếu có, được xử lý bằng cách trả lại trạng thái mới của vũ trụ nơi các hiệu ứng đó đã xảy ra, ví dụ như một gói mạng đã được gửi. Tuy nhiên, kết quả của chức năng là ((),world)và do đó trạng thái mới của vũ trụ là world. Chúng ta không sử dụng vũ trụ mới mà chúng ta có thể đã đánh giá một cách suy đoán ở bên. Trạng thái của vũ trụ là world.

Bạn có thể có một thời gian khó khăn mà nghiêm túc. Có nhiều cách tốt nhất là nghịch lý bề ngoài và vô nghĩa. Đồng thời đặc biệt là không rõ ràng hoặc điên rồ với quan điểm này.

"Đợi, đợi đã," bạn nói. " RealWorldchỉ là một 'mã thông báo'. Nó không thực sự là trạng thái của toàn bộ vũ trụ." Được rồi, "giải thích" này không giải thích gì. Tuy nhiên, như một chi tiết triển khai , đây là cách mô hình GHC IO. 1 Tuy nhiên, điều này có nghĩa là chúng ta có những "chức năng" kỳ diệu thực sự có tác dụng phụ và mô hình này không cung cấp hướng dẫn nào cho ý nghĩa của chúng. Và, vì các chức năng này thực sự có tác dụng phụ, mối quan tâm bạn nêu lên là hoàn toàn chính xác. GHC không cần phải đảm bảo RealWorldvà các chức năng đặc biệt này không được tối ưu hóa theo cách thay đổi hành vi dự định của chương trình.

Cá nhân (như có lẽ là hiển nhiên bây giờ), tôi nghĩ rằng mô hình "xuyên thế giới" IOnày chỉ là vô dụng và khó hiểu như một công cụ sư phạm. (Cho dù nó hữu ích cho việc triển khai, tôi không biết. Đối với GHC, tôi nghĩ nó giống như một cổ vật lịch sử.)

Một cách tiếp cận khác là xem IOnhư một yêu cầu mô tả với các trình xử lý phản hồi. Có một số cách để làm điều này. Có lẽ dễ tiếp cận nhất là sử dụng một công trình đơn nguyên miễn phí, cụ thể là chúng ta có thể sử dụng:

data IO a = Return a | Request OSRequest (OSResponse -> IO a)

Có nhiều cách để làm cho điều này tinh vi hơn và có một số tính chất tốt hơn, nhưng đây đã là một cải tiến. Nó không đòi hỏi những giả định triết học sâu sắc về bản chất của thực tế để hiểu. Tất cả đều nói rằng đó IOlà một chương trình tầm thường Returnkhông làm gì ngoài việc trả về một giá trị, hoặc đó là một yêu cầu cho hệ điều hành với một trình xử lý cho phản hồi. OSRequestcó thể là một cái gì đó như:

data OSRequest = OpenFile FilePath | PutStr String | ...

Tương tự, OSResponsecó thể là một cái gì đó như:

data OSResponse = Errno Int | OpenSucceeded Handle | ...

(Một trong những cải tiến mà có thể được thực hiện là để làm cho mọi việc hơn gõ rất an toàn mà bạn biết bạn sẽ không nhận được OpenSucceededtừ một PutStryêu cầu.) Mô hình này IOlà mô tả các yêu cầu mà có được giải thích bởi một số hệ thống (đối với "thực" IOđơn nguyên này là thời gian chạy Haskell), và sau đó, có lẽ, hệ thống đó sẽ gọi trình xử lý mà chúng tôi đã cung cấp với một phản hồi. Tất nhiên, điều này cũng không đưa ra bất kỳ dấu hiệu nào về cách PutStr "hello world"xử lý một yêu cầu như thế nào , nhưng nó cũng không giả vờ. Nó làm cho rõ ràng rằng điều này đang được ủy quyền cho một số hệ thống khác. Mô hình này cũng khá chính xác. Tất cả các chương trình người dùng trong các HĐH hiện đại cần phải yêu cầu HĐH thực hiện bất cứ điều gì.

Mô hình này cung cấp trực giác đúng. Ví dụ, nhiều người mới bắt đầu xem những thứ như <-toán tử là "hủy ghép nối" IOhoặc có quan điểm (không may được củng cố) rằng IO Stringmột "container" chứa "chứa" String(và sau đó <-lấy chúng ra). Quan điểm phản hồi yêu cầu này làm cho quan điểm này rõ ràng sai. Không có xử lý tập tin bên trong OpenFile "foo" (\r -> ...). Một điểm tương đồng phổ biến để nhấn mạnh điều này là không có bánh trong công thức làm bánh (hoặc có thể "hóa đơn" sẽ tốt hơn trong trường hợp này).

Mô hình này cũng hoạt động dễ dàng với đồng thời. Chúng ta có thể dễ dàng có một hàm tạo để OSRequestthích Fork :: (OSResponse -> IO ()) -> OSRequestvà sau đó bộ thực thi có thể xen kẽ các yêu cầu được tạo bởi trình xử lý bổ sung này với trình xử lý bình thường theo cách nó thích. Với một số thông minh, bạn có thể sử dụng điều này (hoặc các kỹ thuật liên quan) để thực sự mô hình hóa những thứ như đồng thời trực tiếp hơn thay vì chỉ nói "chúng tôi đưa ra yêu cầu cho HĐH và mọi thứ xảy ra." Đây là cách IOSpecthư viện hoạt động.

1 Hugs đã sử dụng một triển khai dựa trên tiếp tục IOtương tự như những gì tôi mô tả mặc dù có chức năng mờ thay vì một loại dữ liệu rõ ràng. HBC cũng đã sử dụng triển khai dựa trên tiếp tục được xếp lớp trên IO dựa trên luồng phản hồi yêu cầu cũ. NHC (và do đó YHC) được sử dụng thunks, tức là xấp xỉ IO a = () -> amặc dù ()được gọi World, nhưng nó không phải là làm nhà nước đi qua. JHC và UHC sử dụng về cơ bản giống như GHC.


Cảm ơn câu trả lời khai sáng của bạn, nó thực sự có ích. Việc triển khai IO của bạn mất một chút thời gian để tôi suy nghĩ, nhưng tôi đồng ý rằng nó trực quan hơn. Bạn có cho rằng việc triển khai này không gặp phải các vấn đề tiềm ẩn khi đặt hàng tác dụng phụ như triển khai RealWorld không? Tôi không thể thấy ngay bất kỳ vấn đề nào, nhưng tôi cũng không rõ rằng chúng không tồn tại.
Lasse

Một nhận xét: có vẻ như OpenFile "foo" (\r -> ...)thực sự nên được Request (OpenFile "foo") (\r -> ...)?
Lasse

@Lasse Yep, nó nên có với Request. Để trả lời câu hỏi đầu tiên của bạn, điều này IOrõ ràng không nhạy cảm với thứ tự đánh giá (đáy modulo) vì đó là một giá trị trơ. Tất cả các tác dụng phụ (nếu có) sẽ được thực hiện bởi điều diễn giải giá trị này. Trong whenví dụ, sẽ không có vấn đề gì nếu actionđược đánh giá, bởi vì đó sẽ chỉ là một giá trị Request (PutStr "foo") (...)mà chúng tôi sẽ không cung cấp cho điều diễn giải các yêu cầu này. Nó giống như mã nguồn; không có vấn đề gì nếu bạn giảm bớt háo hức hoặc lười biếng, không có gì xảy ra cho đến khi nó được trao cho một thông dịch viên.
Derek Elkins rời SE

À vâng tôi thấy điều đó. Đây là một định nghĩa thực sự thông minh. Lúc đầu tôi nghĩ rằng tất cả các tác dụng phụ nhất thiết phải xảy ra khi toàn bộ chương trình đã thực hiện xong, bởi vì bạn phải xây dựng cơ sở hạ tầng trước khi bạn có thể diễn giải nó. Nhưng vì một yêu cầu chứa phần tiếp tục, bạn chỉ phải xây dựng dữ liệu của lần đầu tiên Requestđể bắt đầu thấy tác dụng phụ. Tác dụng phụ sau đó có thể được tạo ra khi đánh giá sự tiếp tục. Tài giỏi!
Lasse
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.