【问题标题】:Why is it possible to sleep / pause execution in Haskell if it's purely functional?如果 Haskell 纯粹是功能性的,为什么它可以在 Haskell 中休眠/暂停执行?
【发布时间】:2014-10-23 22:22:26
【问题描述】:

我看到 Haskell 有一个叫“延迟”的睡眠函数:Control.Concurrent.Thread.Delay

我的问题是:如果 Haskell 纯粹是功能性的,怎么可能有这样的东西?睡眠不是附带效应还是我错过了什么?

【问题讨论】:

  • @simonzack 不正确。 Haskell 纯粹是功能性的。 IO monad 构建了一个单独的副作用程序。通过严格分离 sdie 影响函数(如delay),我们可以处理看起来不纯的事情,但实际上只是纯粹生成指令来做不纯的事情。
  • 如果 Haskell 完全是纯功能性的,那么它的好处就是给房间供暖。除了在不打印任何内容的情况下运行计算之外,您永远不会做任何事情。就目前而言,IO 是可能的,但正如 jozefg 所说,IO monad 或多或少地构建了一个纯指令序列来执行,然后运行时解释它以执行不纯的操作。您可能有兴趣查看 Free monad,它可以让您非常简单地构建“语言”,您可以用不同的方式进行解释。
  • 基于标题,我非常想将这个问题作为离题结束,应该在 travel.stackexchange.com 上。
  • @bheklilr:发热是一种副作用。
  • 你可以说所有的haskell都是纯粹的函数式编程,包括使用IO a类型的东西,除了任何成功绑定到main的魔法实际上都会发生。

标签: haskell


【解决方案1】:

好的,将我的评论移至答案。请注意,IO monad 和这些想法有多个视图。您可以通过唯一性类型等获得类似的结果。

我只是碰巧发现这是最简单和最酷的解释。

伪造杂质

Haskell 是一种纯粹的函数式语言。这应该意味着评估一个 Haskell 程序应该总是产生相同的结果。然而,情况似乎并非如此!看看类似的东西

-- Echo.hs
main :: IO ()
main = getLine >>= putStrLn

根据用户输入,这似乎会有所不同。

实际上,生活在IO 中的任何事物看起来都可以根据从月球状态到薛定谔猫的人寿保险费用等各种情况做出截然不同的事情。

此外,似乎任何语言可以做任何有用的事情一定是不纯的。除非您对观看 CPU 旋转感兴趣,否则程序的存在就是为了产生副作用!

邪恶的口译员

事实上并非如此!适当的心智模型是将IO 想象成类似

data IO a = PutStrLn String a
          | GetLine (String -> a)
          ...

所以IO 可能只是一个数据结构,表示程序执行的一种“计划”。然后邪恶的不纯 Haskell 运行时实际上执行了这个计划,产生了你看到的结果。

不过,这不仅仅是一个小的语义问题,我们可以做类似的事情

runBackwards :: [IO ()] -> IO ()
runBackwards  = foldr (>>) (return ()) . reverse

换句话说,我们可以将我们的“计划”作为正常的、一流的价值观来操纵。

我们可以评估他们,强迫他们,向他们扔一大堆砖头,甚至在他们背后说他们的坏话,他们永远不会产生副作用!他们看不到,普通的 Haskell 代码只能构建 IO 操作以由运行时评估,它无法做任何值得注意的事情。

在某种程度上,您几乎可以将 Haskell 程序视为元编程的终极形式,在运行时动态生成程序并让某些解释器对它们进行评估。

所以当你说

 foo = delay 20

您不是说“将此程序延迟 20 次”,而是说“在此代码构建的程序中,在运行时暂停其执行 20 次”。

谁在乎

问“谁在乎”是公平的:如果这段代码在某个时候运行,谁在乎谁运行它?以这种方式纯功能性有什么好处?它实际上可以产生一些有趣的效果(呵呵)。

例如,想想http://www.tryhaskell.org 之类的东西,显然它需要运行Haskell 代码,但它也不能盲目地执行IO 得到的任何东西!它可以做的是提供IO 的不同实现,同时公开相同的API。

这个新的IO 构建了一个漂亮的树状数据结构,可以很容易地被 Web 后端清理和检查,以确保它永远不会运行邪恶的东西。我们甚至可以将我们的 fake-IO 结构编译为 GHC 提供的普通结构,并在服务器上高效地执行它!因为他们一开始就没有任何邪恶之处,我们只需要相信我们编写的代码。

不再有无穷无尽的小程序式安全漏洞。通过替换IO,我们毫无疑问地知道我们可以执行此代码,并且它永远不会尝试做坏事。

到处都是邪恶的口译员

事实上,构建数据结构的概念不仅仅适用于IO。这是构建任何旨在提供有限 DSL 的项目的好方法。任何来自

  • 一种查询语言
  • 游戏脚本
  • 代码生成
  • 编写“客户端 haskell”

所有这些都可以通过构建正常的数据结构并将它们“编译”成适当的语言来解决。通常的技巧是使用free monad。如果你是中级 Haskeller,去了解一下吧!

【讨论】:

  • 不应该runBackwards 有类型[IO ()] -> IO ()
  • 据我所知,可以将 Haskell 程序视为一个巨大的纯函数,它产生一系列 IO 操作。 IO 动作在评估时可能会在给定相同输入的情况下产生不同的结果,但 Haskell 程序总是在给定相同输入的情况下产生相同序列的 IO 动作,这意味着它是纯的。
【解决方案2】:

“纯功能”是一个有趣的术语。

乍一看,需要处理现实世界的所有东西都不是纯粹的功能性。但不知何故,Haskell 需要处理现实世界。否则语言就没有意义了。

所以解决方案是 IO monad。我不会在这里详细介绍(我相信您可以找到 IO monad 的另一种解释)。 IO monad 大致是一个类型为RealWorld -> RealWorld 的函数。

这意味着我们不会在我们的程序之外做一些事情,而是假装现实世界将成为我们程序的一部分,每次我们想要改变现实世界中的某些东西时,我们都会创造另一个我们更喜欢的现实世界。

delay 的诀窍在于,我们采用当前世界并创建另一个时间提前给定秒数的世界。

【讨论】:

  • RealWorld 函数解释不是很好。它没有解决 Haskell 的许多实际功能(即多线程),并且根据我的经验,它往往会让人感到困惑而不是启发。
  • GHC 在内部有一个叫做RealWorld 的东西,但它是一个实现细节,如果你从表面上看它实际上有点误导。这只会增加对 IO 使用这种解释的混乱。
  • 这让我想起了一种“虚拟机”。做这种关联有错吗?
  • @TikhonJelvis 没错,解释不如jozefg。实际上,如果我想到 IO,我认为它更像是 jozefgs 版本,但在写那个答案时并没有想到这一点。
【解决方案3】:

纯函数式语言不应具有“延迟”、“writeFile”等函数,因为它们会产生副作用。但是 Haskell 具有这些功能,它通过一个名为 IO 的容器来处理它们,该容器也是一个 monad。 IO 隐藏了这些函数产生的所有副作用。

【讨论】:

  • 我知道这是一个很难的问题,但这是一个真正具有误导性的答案。将 IO 中的所有内容都视为“隐藏副作用”的容器,这是一种错误的思维模式。
  • 因为一般的 monads 和 特别是 IO 不是容器。如果您阅读了@jozefg 接受的答案,他在说明这个想法方面做得很好。在 freenode 上浏览#haskell-beginners 也会有很大帮助。这种讨论经常发生。
  • 没有。 一些 monad 可以通过类比容器来描述,sometimesJust 12 可以被认为是一个带有 12Just 容器,但是 Nothing 构造函数是什么呢?它显然不是一个盒子,因为它不包含空虚。它虚无。东西的缺席。根本没有盒子。实际上Maybe a 不是容器,它是一个上下文,您可以在其中描述a 类型值的存在或不存在。
  • 那篇文章(说单子作为计算是一种更自然的思考方式)是相当有争议的。谈话页面显示了这一点。最重要的是,很明显,wiki 只是选择了一些 的 Monad 作为介绍。即便如此,所有的解释都要求你完全忽略“Monad”部分,只将它们视为 Functor。 Parser aSnap () 网络应用程序或 Tardis,它们模拟时间旅行呢?将这些视为“容器”是没有用的。这只是迈向真正理解 Monad 作为计算上下文的一小步。
  • 这就是重点。总有人不理解某事,当遇到问题时,他们只是尝试用他们确实理解的方式重新构建它。在这种情况下,它不理解 monad,因此只能将它们视为容器。您完全没有解决我评论中唯一有意义的部分。你的心智模型将如何适应那些明显不像容器完全起作用的monad?解析?操作?折断?名单还在继续……
猜你喜欢
  • 2021-12-18
  • 1970-01-01
  • 2020-03-24
  • 2017-11-06
  • 2016-06-28
  • 1970-01-01
  • 1970-01-01
  • 2010-09-22
相关资源
最近更新 更多