IO monad在技术上是否不正确?


12

在haskell Wiki上,有以下有条件使用IO monad的示例(请参见此处)

when :: Bool -> IO () -> IO ()
when condition action world =
    if condition
      then action world
      else ((), world)

请注意,在此示例中,对的定义IO aRealWorld -> (a, RealWorld)为了使所有内容都易于理解。

该代码段有条件地在IO monad中执行一个动作。现在,假设conditionFalse,则action永远不应执行该操作。使用惰性语义确实是这种情况。然而,需要注意这里是Haskell是从技术上来说不严格。这意味着,例如,允许编译器抢先action world在其他线程上运行,然后在发现不需要时放弃该计算。但是,到那时,副作用已经发生了。

现在,可以以这样一种方式来实现IO monad,即仅在整个程序完成时才传播副作用,并且我们确切知道应该执行哪些副作用。但是,事实并非如此,因为有可能在Haskell中编写无限的程序,而这些程序显然具有中间的副作用。

这是否表示IO monad在技术上是错误的,还是有其他阻止此情况发生的方法?


欢迎来到计算机科学!您的问题不在这里:我们处理计算机科学问题,而不是编程问题(请参阅FAQ)。您的问题可能是Stack Overflow上的热门话题。
dkaeae,

2
我认为这是一个计算机科学问题,因为它涉及Haskell的理论语义,而不涉及实际的编程问题。
拉瑟

4
我对编程语言理论不太熟悉,但是我认为这个问题在这里很热门。如果您在这里弄清楚“错”的含义,可能会有所帮助。您认为IO monad应该拥有什么属性?
离散蜥蜴

1
该程序的类型不正确。我不确定您实际上打算写什么。的定义是when可键入的,但是没有您声明的类型,并且我看不出是什么使此特定代码变得有趣。
吉尔(Jills)'所以

2
该程序从上方直接链接的Haskell-wiki页面上逐字读取。它确实没有键入。这是因为它是在IO a定义为的假设下编写的RealWorld -> (a, RealWorld),以使IO的内部结构更具可读性。
拉瑟

Answers:


12

这是对IO单子的建议“解释” 。如果您想认真对待这种“解释”,那么就需要认真对待“现实世界”。不管是否action world进行投机评估,action没有任何副作用都是无关紧要的,它的影响(如果有的话)是通过返回已发生这些影响的宇宙的新状态(例如,已发送网络数据包)来处理的。但是,该函数的结果为((),world),因此宇宙的新状态为world。我们不会使用我们可能会在侧面进行推测性评估的新宇宙。宇宙的状态是world

您可能很难认真对待这一点。充其量从表面上讲是矛盾的和荒谬的,有很多方法。从这个角度来看,并发尤其是非显而易见的或疯狂的。

“等等,”您说。“ RealWorld只是一个'令牌'。它实际上不是整个宇宙的状态。” 好的,那么这个“解释”什么也不能解释。然而,作为实施细节,这是GHC建模的方式IO1但是,这意味着我们确实具有神奇的“功能”,而这些功能实际上确实具有副作用,并且该模型没有为其含义提供指导。而且,由于这些功能实际上有副作用,因此您提出的关注点是完全正确的。GHC 确实必须尽力确保RealWorld这些特殊功能没有以改变程序预期行为的方式进行优化。

就个人而言(目前可能已经很明显了),我认为这种“遍及世界”的模式IO作为一种教学工具,毫无用处且令人困惑。(我不知道它是否对实现有用。对于GHC,我认为它更多是历史产物。)

一种替代方法是将IO响应请求视为描述请求。有几种方法可以做到这一点。可能最方便的方法是使用免费的monad构造,特别是我们可以使用:

data IO a = Return a | Request OSRequest (OSResponse -> IO a)

有很多方法可以使它更复杂并具有更好的性能,但这已经是一种改进。不需要了解关于现实本质的深刻哲学假设。它所说明的只是IO一个琐碎的程序Return,什么都不做,只返回一个值,或者是对操作系统的请求,并带有用于响应的处理程序。OSRequest可以是这样的:

data OSRequest = OpenFile FilePath | PutStr String | ...

同样,OSResponse可能类似于:

data OSResponse = Errno Int | OpenSucceeded Handle | ...

(可以做的改进之一是使事情更安全,使您知道不会OpenSucceededPutStr请求中得到。)此模型IO描述的是描述被某些系统解释的请求(对于“真实” IOmonad,这是(Haskell运行时本身),然后,该系统可能会调用我们提供的响应处理程序。当然,这也不能表示PutStr "hello world"应如何处理类似的请求,但也不能假装。它明确表明将其委托给其他系统。这个模型也很准确。现代操作系统中的所有用户程序都需要向操作系统发出请求以执行任何操作。

该模型提供了正确的直觉。例如,许多初学者将诸如<-运算符之类的东西视为“解包” IO,或者具有(不幸地得到加强)这样的观点IO String,比如说是“包含” String(然后<-将其取出)的“容器” 。这种请求-响应视图使此观点明显错误。中没有文件句柄OpenFile "foo" (\r -> ...)。强调这一点的通常比喻是,蛋糕食谱中没有蛋糕(在这种情况下,“发票”可能会更好)。

该模型还可以并发使用。我们可以轻松地为OSRequestlike创建一个构造函数Fork :: (OSResponse -> IO ()) -> OSRequest,然后运行时可以将这个额外的处理程序产生的请求与它喜欢的普通处理程序进行交织。有了一些技巧,您可以使用此(或相关技术)更直接地对诸如并发之类的事物进行实际建模,而不仅仅是说“我们向OS发出请求,事情就发生了”。IOSpec就是这样工作的。

1 Hugs使用了基于连续的实现,IO该实现与我描述的大致相似,尽管使用的是不透明函数而不是显式数据类型。HBC还使用了基于延续的实现,该实现位于旧的基于请求-响应流的IO之上。NHC(因此也称为YHC)使用了thunk,即IO a = () -> a虽然虽然()被称为World,但它并未进行状态传递。JHC和UHC使用的基本方法与GHC相同。


感谢您的启发性答复,它确实有所帮助。您对IO的实现花了一些时间来思考,但是我同意它更加直观。您是否声称该实现不会像RealWorld实现那样遭受副作用排序的潜在问题?我无法立即看到任何问题,但我也不清楚它们是否存在。
拉瑟

一则评论:看来OpenFile "foo" (\r -> ...)实际上应该是Request (OpenFile "foo") (\r -> ...)
拉瑟

@Lasse Yep,应该已经在Request。要回答您的第一个问题,这IO显然对评估顺序不敏感(模底),因为它是惰性值。所有的副作用(如果有)将由解释该值的事物产生。在该when示例中,是否action被评估无关紧要,因为它只是一个值,就像Request (PutStr "foo") (...)我们不会将其用于解释这些请求的事情一样。就像源代码一样;急切或懒惰地减少它并不重要,直到将其提供给解释器,都不会发生任何事情。
德里克·埃尔金斯

嗯,我明白了。这是一个非常聪明的定义。最初,我认为当整个程序完成执行时,必然会发生所有副作用,因为您必须先构建数据结构才能解释它。但是由于请求包含继续,因此您仅需构建第一个数据就Request可以开始看到副作用。在评估延续性时,可能会产生后续的副作用。聪明!
拉瑟
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.