Giả sử một chức năng có tác dụng phụ. Nếu chúng ta lấy tất cả các hiệu ứng mà nó tạo ra làm tham số đầu vào và đầu ra, thì hàm đó hoàn toàn là thế giới bên ngoài.
Vì vậy, đối với một chức năng không tinh khiết
f' :: Int -> Int
chúng tôi thêm RealWorld vào xem xét
f :: Int -> RealWorld -> (Int, RealWorld)
-- input some states of the whole world,
-- modify the whole world because of the side effects,
-- then return the new world.
sau đó f
là tinh khiết trở lại. Chúng tôi xác định loại dữ liệu được tham số hóa type IO a = RealWorld -> (a, RealWorld)
, vì vậy chúng tôi không cần nhập RealWorld nhiều lần và chỉ có thể viết
f :: Int -> IO Int
Đối với lập trình viên, việc xử lý trực tiếp một RealWorld là quá nguy hiểm, đặc biệt, nếu một lập trình viên nắm trong tay giá trị của loại RealWorld, họ có thể cố gắng sao chép nó, điều này về cơ bản là không thể. (Ví dụ, nghĩ về việc cố gắng sao chép toàn bộ hệ thống tệp. Bạn sẽ đặt nó ở đâu?) Do đó, định nghĩa về IO của chúng tôi cũng gói gọn các trạng thái của toàn thế giới.
Thành phần của các chức năng "không tinh khiết"
Các hàm không tinh khiết này là vô dụng nếu chúng ta không thể xâu chuỗi chúng lại với nhau. Xem xét
getLine :: IO String ~ RealWorld -> (String, RealWorld)
getContents :: String -> IO String ~ String -> RealWorld -> (String, RealWorld)
putStrLn :: String -> IO () ~ String -> RealWorld -> ((), RealWorld)
Chúng tôi muốn
- lấy tên tệp từ bàn điều khiển
- đọc tập tin đó và
- in nội dung của tập tin đó lên bàn điều khiển.
Làm thế nào chúng ta sẽ làm điều đó nếu chúng ta có thể truy cập vào các quốc gia trong thế giới thực?
printFile :: RealWorld -> ((), RealWorld)
printFile world0 = let (filename, world1) = getLine world0
(contents, world2) = (getContents filename) world1
in (putStrLn contents) world2 -- results in ((), world3)
Chúng tôi thấy một mô hình ở đây. Các chức năng được gọi như thế này:
...
(<result-of-f>, worldY) = f worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...
Vì vậy, chúng ta có thể định nghĩa một toán tử ~~~
để ràng buộc chúng:
(~~~) :: (IO b) -> (b -> IO c) -> IO c
(~~~) :: (RealWorld -> (b, RealWorld))
-> (b -> RealWorld -> (c, RealWorld))
-> (RealWorld -> (c, RealWorld))
(f ~~~ g) worldX = let (resF, worldY) = f worldX
in g resF worldY
sau đó chúng ta có thể viết
printFile = getLine ~~~ getContents ~~~ putStrLn
mà không chạm vào thế giới thực.
"Impurization"
Bây giờ giả sử chúng ta muốn làm cho chữ hoa nội dung tập tin là tốt. Uppercasing là một chức năng thuần túy
upperCase :: String -> String
Nhưng để làm cho nó vào thế giới thực, nó phải trả lại một IO String
. Thật dễ dàng để nâng một chức năng như vậy:
impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)
Điều này có thể được khái quát:
impurify :: a -> IO a
impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)
vì vậy impureUpperCase = impurify . upperCase
, và chúng ta có thể viết
printUpperCaseFile =
getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn
(Lưu ý: Thông thường chúng tôi viết getLine ~~~ getContents ~~~ (putStrLn . upperCase)
)
Chúng tôi đã làm việc với các đơn vị cùng
Bây giờ hãy xem những gì chúng tôi đã làm:
- Chúng tôi đã định nghĩa một toán tử
(~~~) :: IO b -> (b -> IO c) -> IO c
kết hợp hai hàm không tinh khiết với nhau
- Chúng tôi đã định nghĩa một hàm
impurify :: a -> IO a
chuyển đổi một giá trị thuần túy thành không tinh khiết.
Bây giờ chúng tôi thực hiện nhận dạng (>>=) = (~~~)
và return = impurify
, xem? Chúng tôi đã có một đơn nguyên.
Ghi chú kỹ thuật
Để đảm bảo nó thực sự là một đơn nguyên, vẫn còn một số tiên đề cần được kiểm tra:
return a >>= f = f a
impurify a = (\world -> (a, world))
(impurify a ~~~ f) worldX = let (resF, worldY) = (\world -> (a, world )) worldX
in f resF worldY
= let (resF, worldY) = (a, worldX)
in f resF worldY
= f a worldX
f >>= return = f
(f ~~~ impurify) worldX = let (resF, worldY) = f worldX
in impurify resF worldY
= let (resF, worldY) = f worldX
in (resF, worldY)
= f worldX
f >>= (\x -> g x >>= h) = (f >>= g) >>= h
Còn lại như tập thể dục.