谁能解释一下 GHC 对 IO 的定义?
Can anybody explain GHC's definition of IO?
标题很漂亮self-descriptive,但有一部分引起了我的注意:
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
剥离 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 版本是这样写的。
最好不要对 GHC 对 IO
的实现考虑太深,因为该实现 奇怪 和 shady大部分时间都是靠编译器的魔力和运气来工作的。 GHC 使用的损坏模型是 IO
动作是从整个现实世界的状态到与整个现实世界的新状态配对的值的函数。有关这是一个奇怪模型的幽默证明,请参阅 acme-realworld
包。
"works" 的方式:除非您导入名称以 GHC.
开头的奇怪模块,否则您永远无法触及任何这些 State#
东西。您 仅 有权访问处理 IO
或 ST
的函数,并确保 State#
不会被复制或忽略。 State#
通过程序线程化,这确保了 I/O 原语实际上以正确的顺序被调用。由于这都是假装的,所以 State#
根本不是一个正常值——它的宽度为 0,占 0 位。
为什么 State#
采用类型参数?这是一个更漂亮的魔法。 ST
使用它来强制保持状态线程分离所需的多态性。对于 IO
,它与特殊魔法 RealWorld
类型参数一起使用。
所以在实践中,IO x
只是一些程序(即 CPU 指令、中断等的调度),当它执行完后,会交给我们一个 Haskell x
类型的数据结构。 Haskell I/O 的工作方式是说,"we will (functionally) describe how to construct the program which does stuff, and then GHC will do its thing, you'll get that program, and then it's up to you to actually run it." 结果程序基本上看起来像一个交错:
[IO stuff] -> [Haskell code] -> [IO stuff] -> ...
并且它在功能上被编写为一堆纯功能 [Haskell code] -> [IO stuff]
块的组合。
现在,我们如何用真实类型class建模?一种聪明的方法是累积所有可以发送到底层 OS 的命令作为 Request
数据结构,以及 OS 可以作为 Response
发回的响应数据结构。然后,您可以将这些块建模为请求列表和响应列表之间的函数。这是该模型的一个简单版本,大量利用了惰性:
type IO x = [Response] -> ([Request], x)
OS 现在为这个函数提供了一个惰性列表——暂时不要调用它的头部,你必须首先对传出请求进行 cons 操作! -- 并且您生成这对惰性请求列表和惰性结果。 OS 读取您的第一个请求,执行它,并将结果作为响应的第一个元素提供。通过这种方式,您可以得到一个定点运算符。现在我们看看 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
这应该正确,但有点令人困惑。想象一个内部有 "the real world" 的状态 monad 有点不那么令人困惑,但不幸的是,这是 不正确的 :
newtype IO x = RawIO (runIO :: RealWorld -> (RealWorld, x))
这是怎么回事?基本上这是原始真实世界持续存在的事实。例如,我们可以写:
RawIO $ \world -> let (world1, x) = runIO (putStrLn "Name?" >> getLine) world
(world2, y) = runIO (putStrLn "Age?" >> getLine) world
in (world1, y)
这是做什么的?它在分支宇宙中执行计算:在世界 #1 中它问一个问题(姓名?),在世界 #2 中它问另一个问题(年龄?)。然后它将世界 #2 扔掉,但保留它到达那里的答案。
所以我们生活在世界#1,它问我们的名字,然后神奇地知道我们的年龄。由于引用透明性,world #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'
所以当我们在语言中引入副作用函数时,我们可以只为它们编写一个包装器,忽略 "world" 参数并在函数为 运行 时执行副作用。然后我们有:
unsafePerformIO mx = let (_, x) = runIO mx (error "RealWorld doesn't exist) in x
可以在 GHC/GHCi 实际上 需要它们时执行这些 I/O 操作。
Can anybody explain GHC's definition of IO
?
它基于 I/O 的 pass-the-planet 模型:
An IO
computation is a function that (logically) takes the state of the world, and returns a modified world as well as the return value. Of course, GHC does not actually pass the world around; instead, it passes a dummy “token”, to ensure proper sequencing of actions in the presence of lazy evaluation, and performs input and output as actual side effects!
(来自 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 模型提供了认真使用副作用的选项时,为什么还要为庞大的世界值烦恼?
[...] a machinery whose most eminent characteristic is state [means] the gap between model and machinery is wide, and therefore costly to bridge. [...]
This has in due time also been recognized by the protagonists of functional
languages.[...]
现在我们讨论实施细节的话题:
I'm just curious as to why the particular GHC version is written like it is?
主要是为了避免不必要的运行时评估和堆使用:
State# RealWorld
和未装箱的元组 (# ..., ... #)
是 未提升的类型 - 在 GHC 中,它们不占用 space 在堆中。未举起也意味着它们可以立即使用,无需事先评估。
使用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 特定的扩展。
标题很漂亮self-descriptive,但有一部分引起了我的注意:
newtype IO a = IO (State# RealWorld -> (# State# RealWorld, a #))
剥离 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 版本是这样写的。
最好不要对 GHC 对 IO
的实现考虑太深,因为该实现 奇怪 和 shady大部分时间都是靠编译器的魔力和运气来工作的。 GHC 使用的损坏模型是 IO
动作是从整个现实世界的状态到与整个现实世界的新状态配对的值的函数。有关这是一个奇怪模型的幽默证明,请参阅 acme-realworld
包。
"works" 的方式:除非您导入名称以 GHC.
开头的奇怪模块,否则您永远无法触及任何这些 State#
东西。您 仅 有权访问处理 IO
或 ST
的函数,并确保 State#
不会被复制或忽略。 State#
通过程序线程化,这确保了 I/O 原语实际上以正确的顺序被调用。由于这都是假装的,所以 State#
根本不是一个正常值——它的宽度为 0,占 0 位。
为什么 State#
采用类型参数?这是一个更漂亮的魔法。 ST
使用它来强制保持状态线程分离所需的多态性。对于 IO
,它与特殊魔法 RealWorld
类型参数一起使用。
所以在实践中,IO x
只是一些程序(即 CPU 指令、中断等的调度),当它执行完后,会交给我们一个 Haskell x
类型的数据结构。 Haskell I/O 的工作方式是说,"we will (functionally) describe how to construct the program which does stuff, and then GHC will do its thing, you'll get that program, and then it's up to you to actually run it." 结果程序基本上看起来像一个交错:
[IO stuff] -> [Haskell code] -> [IO stuff] -> ...
并且它在功能上被编写为一堆纯功能 [Haskell code] -> [IO stuff]
块的组合。
现在,我们如何用真实类型class建模?一种聪明的方法是累积所有可以发送到底层 OS 的命令作为 Request
数据结构,以及 OS 可以作为 Response
发回的响应数据结构。然后,您可以将这些块建模为请求列表和响应列表之间的函数。这是该模型的一个简单版本,大量利用了惰性:
type IO x = [Response] -> ([Request], x)
OS 现在为这个函数提供了一个惰性列表——暂时不要调用它的头部,你必须首先对传出请求进行 cons 操作! -- 并且您生成这对惰性请求列表和惰性结果。 OS 读取您的第一个请求,执行它,并将结果作为响应的第一个元素提供。通过这种方式,您可以得到一个定点运算符。现在我们看看 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
这应该正确,但有点令人困惑。想象一个内部有 "the real world" 的状态 monad 有点不那么令人困惑,但不幸的是,这是 不正确的 :
newtype IO x = RawIO (runIO :: RealWorld -> (RealWorld, x))
这是怎么回事?基本上这是原始真实世界持续存在的事实。例如,我们可以写:
RawIO $ \world -> let (world1, x) = runIO (putStrLn "Name?" >> getLine) world
(world2, y) = runIO (putStrLn "Age?" >> getLine) world
in (world1, y)
这是做什么的?它在分支宇宙中执行计算:在世界 #1 中它问一个问题(姓名?),在世界 #2 中它问另一个问题(年龄?)。然后它将世界 #2 扔掉,但保留它到达那里的答案。
所以我们生活在世界#1,它问我们的名字,然后神奇地知道我们的年龄。由于引用透明性,world #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'
所以当我们在语言中引入副作用函数时,我们可以只为它们编写一个包装器,忽略 "world" 参数并在函数为 运行 时执行副作用。然后我们有:
unsafePerformIO mx = let (_, x) = runIO mx (error "RealWorld doesn't exist) in x
可以在 GHC/GHCi 实际上 需要它们时执行这些 I/O 操作。
Can anybody explain GHC's definition of
IO
?
它基于 I/O 的 pass-the-planet 模型:
An
IO
computation is a function that (logically) takes the state of the world, and returns a modified world as well as the return value. Of course, GHC does not actually pass the world around; instead, it passes a dummy “token”, to ensure proper sequencing of actions in the presence of lazy evaluation, and performs input and output as actual side effects!
(来自 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 模型提供了认真使用副作用的选项时,为什么还要为庞大的世界值烦恼?
[...] a machinery whose most eminent characteristic is state [means] the gap between model and machinery is wide, and therefore costly to bridge. [...]
This has in due time also been recognized by the protagonists of functional languages.[...]
现在我们讨论实施细节的话题:
I'm just curious as to why the particular GHC version is written like it is?
主要是为了避免不必要的运行时评估和堆使用:
State# RealWorld
和未装箱的元组(# ..., ... #)
是 未提升的类型 - 在 GHC 中,它们不占用 space 在堆中。未举起也意味着它们可以立即使用,无需事先评估。使用
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 特定的扩展。