【问题标题】:Can anybody explain GHC's definition of IO?谁能解释 GHC 对 IO 的定义?
【发布时间】:2015-02-18 15:41:07
【问题描述】:

标题非常自我描述,但有一个部分引起了我的注意:

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 的内置。至少请参阅 this recent post 以全面了解其工作原理。

标签: haskell io ghc


【解决方案1】:

最好不要太深入地考虑 GHC 的 IO 实现,因为该实现是奇怪阴暗,并且大部分时间都通过编译器魔法和运气。 GHC 使用的损坏模型是IO 动作是从整个现实世界的状态到与整个现实世界的新状态配对的值的函数。有关这是一个奇怪模型的幽默证明,请参阅 acme-realworld 包。

这种“工作方式”:除非您导入名称以 GHC. 开头的奇怪模块,否则您永远无法触及这些 State# 中的任何东西。您有权访问处理IOST 并确保State# 不能被复制或忽略的函数。这个State# 通过程序线程化,确保I/O 原语实际上以正确的顺序被调用。由于这都是为了伪装,State# 根本不是一个正常值——它的宽度为 0,占用 0 位。

为什么State# 接受类型参数?这是一个更漂亮的魔法。 ST 使用它来强制保持状态线程分离所需的多态性。对于IO,它与特殊的魔术RealWorld 类型参数一起使用。

【讨论】:

  • 哈,现在我很想尝试 acme-realworld。
  • 我不会因为如果你使用内部实现细节就不健全,就说世界传递模型从根本上被打破了;这同样适用于许多抽象,包括 Data.Map 之类的东西!其他语言明确使用世界传递将“世界”暴露给用户代码;他们使用类型系统特性来确保线性,而 Haskell 通过不公开任何可能非线性使用世界的操作来确保线性。
  • @Ben,(对我而言)面对并发似乎没有什么意义。它还需要一些......特别小心......以防止编译器优化把事情搞砸
  • @dfeuer 通过记住从 IO 原语返回的世界包括任意 更改(从纯模型的角度来看),而不仅仅是原始模型所做的更改。但这与 IO 的“description-of-an-action”模型完全相同; main 程序未指定真实可变世界中的变化。无论哪种方式,您都需要求助于操作语义来真正说出正在发生的事情。
  • @dfeur 我确实更喜欢 IO 的“description-of-an-action”模型。但我认为世界传递是一个完全有用的解释。它们都更容易从不同的起点获得(如果您不了解状态单子,则世界传递很容易理解,如果您喜欢元编程,则操作更直观)。声称 world-passing 是一个损坏的 模型(与它在 GHC 中具有令人讨厌的实现相反)似乎与其他明确使用它的语言不一致,并且仍然具有健全的声明性语义。
【解决方案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)

操作系统现在为这个函数提供了一个惰性列表——暂时不要调用它的头部,你必须首先对传出的请求添加一些东西! -- 你生成了这对惰性请求列表和惰性结果。操作系统读取您的第一个请求,执行它,并将结果作为响应的第一个元素提供。通过这种方式,您可以获得一个定点运算符。现在我们看看returnbind 的样子:

 -- 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 操作时,它可以执行这些操作。

【讨论】:

    【解决方案3】:

    谁能解释 GHC 对IO 的定义?

    它基于 I/O 的 pass-the-planet 模型:

    IO 计算是一个函数,它(逻辑上)获取世界状态,并返回修改后的世界以及返回值。当然,GHC 实际上并没有环游世界。相反,它传递一个虚拟的“令牌”,以确保在存在惰性求值的情况下正确排序操作,并将输入和输出作为实际副作用执行!

    (来自 A History of Haskell,作者 Paul Hudak、John Hughes、Simon Peyton Jones 和 Philip Wadler;第 26 页,共 55 页。)

    使用该描述作为指导:

    newtype IO a   =  IO (FauxWorld -> (# FauxWorld, a #))
    

    地点:

    type FauxWorld =  State# RealWorld
    

    当 I/O 模型提供了努力使用副作用的选项时,为什么还要为庞大的世界值烦恼?

    [...] 一种机器,其最显着的特征是状态 [意味着] 模型和机器之间的差距很大,因此弥合成本很高。 [...]
    这在适当的时候也得到了功能的主角们的认可 语言。[...]

    Niklaus Wirth.

    现在我们正在讨论实现细节:

    我只是好奇为什么特定的 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 和 unlifted 类型都是 GHC 特定的扩展。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-27
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多