所以在实践中,IO x 只是一些程序(即 CPU 指令、中断等的调度),当它执行完成时,会为我们提供 x 类型的 Haskell 数据结构。 Haskell I/O 的工作方式是这样说,“我们将(功能上)描述如何构建程序,然后 GHC 会做它的事情,你会得到那个程序,然后由你来实际运行它。”生成的程序基本上看起来像一个交错:
[IO stuff] -> [Haskell code] -> [IO stuff] -> ...
它在功能上被编写为一堆纯功能 [Haskell code] -> [IO stuff] 块的组合。
现在,我们如何对这个真正的类型类?一种巧妙的方法是将所有可以发送到底层操作系统的命令作为Request 数据结构,以及操作系统可以作为Response 数据结构发回的响应进行累积。然后,您可以将这些块建模为请求列表和响应列表之间的函数。这是该模型的一个简单版本,大量利用了惰性:
type IO x = [Response] -> ([Request], x)
操作系统现在为这个函数提供了一个惰性列表——暂时不要调用它的头部,你必须首先对传出的请求添加一些东西! -- 你生成了这对惰性请求列表和惰性结果。操作系统读取您的第一个请求,执行它,并将结果作为响应的第一个元素提供。通过这种方式,您可以获得一个定点运算符。现在我们看看return 和bind 的样子:
-- return needs to yield a special symbol of type Request which stops the
-- process of querying the OS.
return x = ([Done], x)
-- bind needs to split the responses between those fed to mx and the rest,
-- assume that every request yields exactly one response so we can examine
-- just the length of x_requests.
bind :: ([Response] -> ([Request], x)) ->
(x -> [Response] -> ([Request], y)) ->
[Response] -> ([Request], y)
bind mx x_to_my responses = (init x_requests ++ y_requests, y)
where (x_requests, x) = mx responses
(y_requests, y) = x_to_my x $ drop (length x_requests - 1) responses
这应该是正确,但有点混乱。稍微不那么令人困惑的是想象一个内部有“真实世界”的状态单子,但不幸的是,这是不正确的:
newtype IO x = RawIO (runIO :: RealWorld -> (RealWorld, x))
这有什么问题?基本上,原来的 RealWorld 仍然存在。例如,我们可以这样写:
RawIO $ \world -> let (world1, x) = runIO (putStrLn "Name?" >> getLine) world
(world2, y) = runIO (putStrLn "Age?" >> getLine) world
in (world1, y)
这是做什么的?它在一个分支宇宙中执行计算:在世界#1 中它提出一个问题(姓名?),而在世界#2 中它提出一个不同的问题(年龄?)。然后它将世界 #2 扔掉,但保留了它到达那里的答案。
所以我们生活在世界#1,它会问我们的名字,然后神奇地知道我们的年龄。由于引用透明性,世界#2(询问我们的年龄)的副作用不会发生),但它的结果已经获得。哎呀——真正的 I/O 做不到。
好吧,只要我们隐藏RawIO 构造函数就可以了!我们只会让所有我们的函数表现良好并完成它。然后我们可以编写完全正常的 bind 和 return 版本:
return x = RawIO $ \world -> (world, x)
bind mx x_to_my = RawIO $ \world -> let (world', x) = runIO mx world in
runIO (x_to_my x) world'
因此,当我们在语言中引入副作用函数时,我们可以只为它们编写一个忽略“世界”参数并在函数运行时执行副作用的包装器。然后我们有:
unsafePerformIO mx = let (_, x) = runIO mx (error "RealWorld doesn't exist) in x
当 GHC/GHCi实际需要这些 I/O 操作时,它可以执行这些操作。