【问题标题】:In what sense is the IO Monad pure?IO Monad 在什么意义上是纯粹的?
【发布时间】:2010-10-31 15:47:49
【问题描述】:

我已经将 IO monad 描述为 State monad,其中 state 是“现实世界”。这种 IO 方法的支持者认为,这使得 IO 操作变得纯粹,就像引用透明一样。这是为什么?从我的角度来看,IO monad 中的代码似乎有很多可观察到的副作用。另外,难道不能描述几乎任何非纯函数,比如现实世界的函数吗?例如,我们不能把 C 的 malloc 想象成一个函数,它接受一个 RealWorld 和一个 Int 并返回一个指针和一个 RealWorld,就像在 IO monad 中 RealWorld 是隐含的?

注意:我知道 monad 是什么以及它是如何使用的。请不要回复随机 monad 教程的链接,除非它专门解决了我的问题。

【问题讨论】:

标签: haskell io monads


【解决方案1】:

我认为我听到的最好的解释实际上是最近关于 SO 的。 IO Foo 是创建Foo 的秘诀。另一种常见的、更直接的说法是,它是一个“产生Foo 的程序”。可以执行(多次)以创建Foo 或尝试死。配方/程序的执行是我们最终想要的(否则,为什么要写一个?),但在我们的代码中由 IO 操作表示的东西是配方本身。

该配方是一个纯值,就像String 是一个纯值一样。配方可以以有趣、有时甚至令人惊讶的方式组合和操作,但是这些配方可以组合的许多方式(除了公然非纯的unsafePerformIOunsafeCoerce 等)都是完全参照透明、确定性的,和所有那些好东西。生成的配方绝对不依赖于其他任何东西的状态,而不是它所构建的配方。

【讨论】:

  • “recipe”——很好的教学用词。
  • @luqui "pedagogical" - 关于学习如何阅读您的评论的好词
【解决方案2】:

另外,难道不能描述几乎任何非纯函数,比如现实世界的函数吗?例如,我们不能将 C 的 malloc 视为一个函数,它接受一个 RealWorld 和一个 Int 并返回一个指针和一个 RealWorld,就像在 IO monad 中一样,RealWorld 是隐式的?

当然……

函数式编程的整个想法是将程序描述为小型独立计算构建更大计算的组合。

拥有这些独立计算,您将获得很多好处,从简洁的程序到高效且高效的可并行代码,从惰性到严格保证控制按预期流动 - 没有机会任意数据的干扰或损坏。

现在 - 在某些情况下(如 IO),我们需要不纯的代码。涉及此类操作的计算不能是独立的 - 它们可以改变另一个计算的任意数据。

重点是 - Haskell 总是纯粹的IO 不会改变这一点。

所以,我们不纯的、非独立的代码必须得到一个共同的依赖——我们必须传递一个RealWorld。因此,无论我们要运行什么有状态计算,我们都必须传递 RealWorld 来应用我们的更改 - 任何其他有状态计算想要查看或进行更改都必须知道 RealWorld

无论是通过IO monad 显式还是隐式完成,都无关紧要。您将 Haskell 程序构建为一个巨大的计算来转换数据,而这些数据的一部分是 RealWorld

当您的程序以当前现实世界作为参数运行时,一旦调用了初始main :: IO (),这个现实世界就会通过所有涉及的不纯计算,就像State 中的数据一样。这就是 monadic >>= (bind) 的作用。

如果RealWorld 没有得到(如在纯计算中或没有任何>>=-ing 到main),就没有机会用它做任何事情。而它确实得到的地方,是通过一个(隐式)参数的纯函数传递而发生的。这就是为什么

let foo = putStrLn "AAARGH" in 42

什么都不做 - 为什么IO monad - 像其他任何东西一样 - 是纯粹的。这段代码里面发生的事情当然可能是不纯的,但它都被抓住了,没有机会干扰非连接的计算。

【讨论】:

  • 但是如果一个 IO monad 的内容可以是不纯的,那么组合不纯动作的结果怎么可能是纯的呢?你没有任何意义。
  • 内容不纯;容器不是 - IO monad 的值与任何其他数据一样。而且您没有机会从IO a 中获取内容(没有严重的作弊行为)——因此无论进行什么不纯的操作,它都被困在IO 中。
  • 你的严重作弊是 unsafePerformIO ;)
【解决方案3】:

假设我们有类似的东西:

animatePowBoomWhenHearNoiseInMicrophone :: TimeDiff -> Sample -> IO ()
animatePowBoomWhenHearNoiseInMicrophone
    levelWeightedAverageHalfLife levelThreshord = ...

programA :: IO ()
programA = animatePowBoomWhenHearNoiseInMicrophone 3 10000

programB :: IO ()
programB = animatePowBoomWhenHearNoiseInMicrophone 3 10000

这是一个观点:

animatePowBoomWhenHearNoiseInMicrophone 是一个纯函数,因为它对于相同输入 programAprogramB 的结果完全相同。您可以使用main = programAmain = programB,它会完全一样。

animatePowBoomWhenHearNoiseInMicrophone 是一个接收两个参数并产生程序描述的函数。如果您将 main 设置为它或通过绑定将其包含在 main 中,Haskell 运行时可以执行此描述。

IO 是什么? IO 是一种用于描述命令式程序的 DSL,以“纯 Haskell”数据结构和函数编码。

“complete-haskell”又名 GHC 既是“pure-haskell”的实现,也是 IO 解码器/执行器的命令式实现。

【讨论】:

  • animatePowBoomWhenHearNoiseInMicrophone 可能是我见过的最好的示例代码函数名称。打得好,先生。
  • 只是为了让一件事更精确,monads 是 E DSLs,就像在 embedded DSLs 中一样,因为 monadic 值是普通的 Haskell 值,例如getLinelet x = print 42 in x.
【解决方案4】:

这很简单归结为extensional equality

如果您要调用getLine 两次,那么这两个调用都会返回一个IO String,每次在外部看起来都完全相同。如果您要编写一个函数来获取 2 个IO Strings 并返回一个Bool 来表示检测到它们之间的差异,那么就不可能检测到任何可观察属性的任何差异。它不能询问任何其他函数它们是否相等,并且任何使用>>= 的尝试也必须在IO 中返回一些外部相等

【讨论】:

    【解决方案5】:

    我会让 Martin Odersky answer this

    IO monad 不会使函数成为纯函数。它只是让它显而易见 它是不纯的。

    听起来足够清晰。

    【讨论】:

    【解决方案6】:

    尽管它的标题有点奇怪(因为它与内容不完全匹配),但下面的 haskell-cafe 线程包含了一个关于 Haskell 不同 IO 模型的精彩讨论。

    http://www.mail-archive.com/haskell-cafe@haskell.org/msg79613.html

    【讨论】:

    • 当他引用 Bertrand Russel 时,OP 是正确的:Here's a quote from Bertrand Russell about philosophy (read: Haskell). He's actually being humorous, but it applies, in a way: "The point of philosophy is to start with something so simple as not to seem worth stating, and to end with something so paradoxical no one will believe it."
    【解决方案7】:

    嗯,这就是我们在大学里学到的——

    当函数总是为指定的输入返回相同的值(或者相同的表达式总是在相同的上下文中计算为相同的值)时,函数是引用透明的。因此,例如,如果 getChar 仅具有 () -> CharChar 的类型签名,则 getChar 将不会是引用透明的,因为如果使用相同的参数多次调用此函数,则会得到不同的结果。

    但是,如果你引入 IO monad,那么 getChar 可以有类型 IO Char 并且这个类型只有一个值 - IO Char。所以getChar 总是返回相同的值,无论用户真正按下了哪个键。

    但是您仍然能够从这个IO Char 事物中“获得”潜在价值。好吧,不是真的得到,而是使用绑定运算符 (>>=) 传递给另一个函数,这样您就可以使用用户在程序中进一步输入的 Char。

    【讨论】:

      【解决方案8】:

      Philip Wadler 写道:

      在不纯的语言中,像tick 这样的操作将由
      () -> () 类型的函数表示。需要虚假参数() 来延迟效果,直到应用该函数,并且由于输出类型为(),人们可能会猜测该函数的目的在于副作用。相比之下,这里tick 的类型为M ():不需要虚假参数,M 的出现明确表明可能会发生什么样的影响

      我不明白 M () 如何使空参数列表 () 不那么虚假,但 Wadler 很清楚 monad 只是表示一种副作用,它们不会消除它

      【讨论】:

      • 我也同意这一点。我认为(没有冒犯)有点虚伪,因为声明 print "Hello" 总是返回 IO () 它是纯粹的。
      【解决方案9】:

      在什么意义上一元IO 类型是纯的?

      从某种意义上说,IO 类型的值是 Standard ML 的一部分抽象命令式代码理想情况下只能由 Haskell 实现的 RTS 处理 - 在 How to Declare an Imperative 中,Philip Wadler 提供了有关如何实现这一点的提示:

      (* page 26 *)
      type 'a io   = unit -> 'a
      
      infix >>=
      val >>=      : 'a io * ('a -> 'b io) -> 'b io
      fun m >>= k  = fn () => let
                                val x = m ()
                                val y = k x ()
                              in
                                y
                              end
      
      val return   : 'a -> 'a io
      fun return x = fn () => x
      
      (* page 27 *)
      val execute : unit io -> unit
      fun execute m = m ()
      

      不过,not everyone 认为这种情况是可以接受的:

      [...] 一种基于机器的无状态计算模型,其最 显着特点是状态[意味着]模型和机械之间的差距很大,因此弥合成本很高。 [...]
      这在适当的时候也得到了功能的主角们的认可 语言。他们以各种棘手的方式引入了状态(和变量)。 纯粹的功能特性因此受到损害和牺牲。 [...]

      Niklaus Wirth.

      ...Miranda(R) 的任何人?


      我将 IO monad 描述为 State monad,其中状态是“现实世界”。

      这将是经典的pass-the-planet I/O 模型,Clean 直接使用它:

      import StdFile
      import StdMisc
      import StdString
      
      Start :: *World -> *World
      Start w = putString "Hello, world!\n" w
      
      putString :: String *World -> *World
      putString str world
          # (out, world1) = stdio world
          # out1          = fwrites str out
          # (_, world2)   = fclose out1 world1
          = world2
      
      putChar :: Char *World -> *World
      putChar c w = putString {c} w
      

      这种 I/O 方法的支持者认为,这使得 I/O 操作变得纯粹,就像引用透明一样。这是为什么呢?

      因为它通常是正确的。

      来自标准的 Haskell 2010 库模块Data.List

      mapAccumL _ s []        =  (s, [])
      mapAccumL f s (x:xs)    =  (s'',y:ys)
                                 where (s', y ) = f s x
                                       (s'',ys) = mapAccumL f s' xs
      

      如果这个习惯用法如此普遍以至于它有特定的定义来支持它,那么它作为 I/O 模型(具有合适的状态类型)的使用就不足为奇了 - 来自@987654328 的第 14-15 页@John Launchbury 和 Simon Peyton Jones:

      那么,I/O 操作到底是如何执行的呢?整个节目的意义 由顶级标识符mainIO的值给出:

      mainIO :: IO ()
      

      mainIO 是一个 I/O 状态转换器,通过以下方式应用于外部世界状态 操作系统。从语义上讲,它返回了一个新的世界状态,并且 其中体现的变化应用于现实世界。

      (...回到 main 被称为 mainIO 的时候。)

      最新的Clean Language ReportI/O on the Unique World,第 24 页,共 148 页)更详细:

      赋予初始表达式的世界是一个抽象数据结构,一个*World类型的抽象世界 从程序中看到具体的物理世界。抽象世界原则上可以包含 anything 函数式程序在执行期间需要与具体世界交互的内容。世界可以看作一个 状态和世界的修改可以通过定义在世界或世界的一部分上的状态转换函数来实现。经过 要求这些状态转换函数在唯一世界上工作,抽象世界的修改可以直接 在真实的物理世界中实现,不会损失效率,也不会失去参考透明度。

      就语义而言,关键点是:要使以 I/O 为中心的程序的更改生效,该程序必须返回最终的世界状态值

      现在考虑这个小的 Clean 程序:

      Start :: *World -> *World
      Start w = loopX w
      
      loopX :: *World -> *World
      loopX w
          # w1 = putChar 'x' w
          = loopX w1
      

      Obviously 永远不会返回最终的 World 值,因此根本不应该看到 'x'...

      另外,难道不能像现实世界的函数那样描述几乎任何非纯函数吗?

      是的;这或多或少是 FFI 在 Haskell 2010 中的工作方式。


      在我看来,monadic IO 类型内的代码似乎有很多可观察到的副作用。

      如果您使用的是 GHC,它不是外观 - 来自 A History of Haskell(第 26 页,共 55 页)作者:Paul Hudak、John Hughes、Simon Peyton Jones 和 Philip Wadler:

      当然,GHC 实际上并没有环游世界;相反,它传递一个虚拟的“令牌”,以确保在存在惰性求值的情况下正确排序操作,并将输入和输出作为实际副作用执行!

      但这只是一个实现细节:

      IO 计算是一个函数,它(逻辑上)获取世界状态,并返回修改后的世界以及返回值。

      逻辑不适用于现实世界。

      马文·李·明斯基。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-03-03
        • 2011-02-23
        • 1970-01-01
        • 2011-09-29
        • 2017-04-27
        • 1970-01-01
        • 2020-08-31
        • 2013-03-06
        相关资源
        最近更新 更多