标题非常具有自我描述性,但有一部分引起了我的注意:
newtype IO a = IO ()
剥离newtype
,我们得到:
State# RealWorld -> (# State# RealWorld, a #)
我不知道State#
代表什么。我们可以用State
代替它:
State RealWorld -> (State RealWorld, a)
那可以这样表达吗?
State (State RealWorld) a
这个特殊的结构引起了我的注意。
我从概念上知道,
type IO a = RealWorld -> (a, RealWorld)
并且@ R.MartinhoFernandes告诉我,我实际上可以将该实现视为ST RealWorld a
,但我只是好奇为什么特定的GHC版本是这样编写的。
答案 0 :(得分:9)
最好不要过于深入地考虑GHC的IO
实现,因为该实现是怪异的和 shady ,并且大部分时间都是由编译器工作的魔术和运气。 GHC使用的破碎模型是IO
动作是从整个现实世界的状态到与整个现实世界的新状态配对的值的函数。有关这是一个奇怪模型的幽默证据,请参阅acme-realworld
包。
这种“工作”的方式:除非你导入名称以GHC.
开头的奇怪模块,否则你不能触及任何这些State#
的东西。您仅可以访问处理IO
或ST
的函数,并确保State#
无法复制或忽略。这个State#
穿过程序,确保实际以正确的顺序调用I / O原语。由于这只是假装,State#
根本不是正常值 - 它的宽度为0,取0位。
为什么State#
采用类型参数?这是一个更漂亮的魔法。 ST
使用它来强制保持状态线程分离所需的多态性。对于IO
,它与特殊魔法RealWorld
类型参数一起使用。
答案 1 :(得分:2)
所以在实践中,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,但保留了它到达那里的答案。
所以我们生活在世界第一,它告诉我们我们的名字,然后神奇地知道我们的年龄。来自世界#2(询问我们的年龄)的副作用不会因参考透明度而发生,但其结果已被获得。哎呀 - 真正的I / O不能那样做。
嗯,只要我们隐藏RawIO
构造函数就可以了!我们只需使所有我们的函数都表现良好并完成它。然后我们可以编写完全合理的bind版本并返回:
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'
因此,当我们在语言中引入了副作用函数时,我们可以将它们写成一个忽略“world”参数的包装器,并在函数运行时执行副作用。然后我们有:
unsafePerformIO mx = let (_, x) = runIO mx (error "RealWorld doesn't exist) in x
当GHC / GHCi 实际需要它们发生时,它们可以执行这些I / O操作。
答案 2 :(得分:0)
有人可以解释GHC对
IO
的定义吗?
它基于I / O的通过行星模型:
IO
计算是一个函数,该函数(从逻辑上)获取世界的状态,并返回修改后的世界以及返回值。当然,GHC实际上并没有遍及整个世界。相反,它传递了一个虚拟的“令牌”,以确保在存在惰性评估的情况下正确进行操作排序,并以实际副作用执行输入和输出!
(摘自Paul Hudak,John Hughes,Simon Peyton Jones和Philip Wadler的A History of Haskell;第26页,共55页)。
使用该说明作为指导:
newtype IO a = IO (FauxWorld -> (# FauxWorld, a #))
其中:
type FauxWorld = State# RealWorld
当I / O模型提供勤奋使用副作用的选项时,为什么要为庞大的世界值烦恼?
[...]一种机器,其最显着的特征是状态[表示]模型与机器之间的差距很大,因此桥接起来的成本很高。 [...]
在适当的时候,功能的主角也意识到了这一点。 语言。[...]
现在,我们讨论实现细节的主题:
我只是好奇为什么特定的GHC版本是这样写的?
主要是为了避免不必要的运行时评估和堆使用:
State# RealWorld
和未装箱的元组(# ..., ... #)
是未提升类型-在GHC中,它们不占用堆空间。放心也意味着无需事先评估即可立即使用它们。
使用State#
来定义IO
(而不是直接使用世界类型)
将RealWorld
简化为抽象标记类型:
type ST# s a = State# s -> (# State# s, a #)
newtype IO a = IO (ST# RealWorld a)
ST#
然后可以在其他地方重用:
newtype ST s a = ST (ST# s a)
有关更多信息,请参见John Launchbury和Simon Peyton Jones的State in Haskell。
Realworld
和未提升类型都是GHC特定的扩展名。