Haskell 纯函数和文件

Kub*_*ski 2 haskell functional-programming

我读过这篇关于 Haskell IO 方法的文章: https: //wiki.haskell.org/IO_inside

我了解 getChar 的工作原理,但我不知道如何使以下函数变得纯粹

getFile :: 字符串 -> 文件

其中 String 是文件名,File 是某种定义的类型,可用于进一步操作文件。

在我看来,这个函数不可能是正确的,因为它违反了规则:“如果函数的结果发生变化,那应该是因为它的参数发生了变化。” (引自上面文章)

文件可以在磁盘上更改,因此文件类型可以不同,例如它可以保存不同的权限集。

我错了什么?

Ben*_*Ben 5

你是对的,你不能做出一个getFile具有你想要的行为、类型String -> File和纯的。你的问题的简单答案是,因为这是不可能的,Haskell 不会这样做。

但 Haskell 不需要这样做。我们当然需要一种通过名称识别磁盘上的文件并打开它的方法(并且我们假设我们可以通过您的File类型来表示打开的文件)。这是 Haskell 需要支持的真正需求。但根本没有理由它必须具有特定的类型String -> File;因为这是不可能的,我们应该制作其他不同类型的东西,但仍然可以满足这种编程需求。

你说你了解如何getChar工作,但是 Haskell 如何提供一些东西来满足你的需求的答案getFile与它如何提供一些东西来满足getChar.

在命令式语言中,getChar是不带任何参数并为您提供 , 命令式的东西Char。在 Haskell 中,“不带参数但给你一个Char”的东西只翻译为类型Char。但getChar显然不可能Char,因为这意味着它是一种特殊的 Char。相反,我们给它类型IO Char。类型事物IO a是会产生 的动作a,而不是其a本身。getChar :: IO Char一个会产生 的动作也是如此Char。它不必一直是一个单一的操作Char,而是一个单一的操作,但每次我们运行该操作时,我们都会Char从输入流中获取下一个操作。

同样,getFile在 Haskell 中应该有类型String -> IO File。它是一个“文件打开操作工厂”;一旦我们给了它文件的名称,生成的操作就知道它应该查找什么名称,但只有当操作实际运行时,它才会查看磁盘并查看那里是否有文件。getFile不必总是为每个输入返回相同的“打开文件” String,它必须返回打开该名称的文件的相同过程。该过程始终相同,只是当您尝试执行该操作时会发生受磁盘状态影响的情况。

Haskell 语言不提供任何实际运行这些IO操作的方法。Haskell 代码是纯粹的,纯粹构建IO动作而不依赖于外部世界。Haskell 的运行时系统可以执行它们,并且它会对操作执行这些main :: IO a操作(如果您在 GHCi 提示符下输入它们)。但它是如何发生的并不是 Haskell 语言的一部分,就像 Java 不明确定义系统调用如何工作一样。

这里的所有都是它的。在命令式语言中会产生副作用的事物Input1 -> Input2 -> Output在 Haskell 中不能具有该类型,因此我们使用不同的类型来表示它们;它们最终为Input1 -> Input2 -> IO Output1

注意我没有提到单子;IO 一个 monad,它提供了很多方便的计算工具IO,但它IO本身让我们可以处理外部副作用,因为 Haskell 运行时的特殊处理,而不是因为它是一个 monad。


只是为了完整性:我注意到这篇文章使用了“世界传递”解释IO,这实际上相当于IO我上面使用的“=不透明动作”故事,但使用了不同的隐喻。2

但是,如果您理解该文章中解释的故事,您就会知道制作一个可以完成非纯函数工作的纯函数的方法是添加一个额外的参数和一个额外的返回值。文章称这个值的类型为ValRealWorld,但重点是它是抽象的。你只能通过调用一个IO动作来获得一个,这需要你进入前一个世界。运行时系统将第一个提供给main,因此您可以将它线程化到所有调用中。在内存中它是一个空值,但可以说它代表整个宇宙的状态:每个磁盘的状态,所有正在传输的网络数据包,所有准备敲击按键或移动鼠标的人等等; 一切

所以getChar不纯的Char类型就变成了World -> (Char, World);你在从输入流中读取一个字符之前给它宇宙的状态,它会返回下一个字符以及在你的下一个 IO 操作之前世界的状态。在 Haskell 语言内部,没有解释它如何“计算”下一个宇宙应该是什么,我们只需要假设它可以(就像动作隐喻一样,我们没有解释动作是如何执行的,我们只是必须假设运行时能够执行它们)。当然,在语言之外我们不需要做任何事情来“计算”宇宙,因为我们可以利用真实宇宙中时间的流逝来始终拥有一个与程序所需的宇宙序列一致的可用宇宙。

类似地,为了表示不纯的,getFile :: String -> File我们只需添加一个额外的参数和返回值,使其成为getFile :: String -> World -> (File, World). 和我们处理的方式一模一样getChar

我没有提到的一个大问题是,尽管 是RealWorld抽象的,所以我们无法窥视它的内部或弥补它,但一旦运行时给我们一个,我们原则上可以存储它并重用它。这不是真实宇宙的运作方式,所以我们必须防止这种情况发生。Haskell 通过不实际将额外的世界参数暴露给普通代码来实现这一点;相反,我们有IO代表“获取并返回一个世界”的类型。IO是抽象的,因此您无法查看其内部来访问World. 相反,内置库公开了用于排序和组合类型的函数IO a,并且这些函数非常小心,以防止意外使用同一个World值两次。(还有其他语言具有类型系统功能,可确保值必须World使用一次,因此以将值暴露给用户代码的方式使用这种“世界传递”系统进行IO )World

IO它仍然与这张单子图片不太相关。这是提供用于组合和排序IO值的内置函数的一种非常方便的方法,但也可以完全不使用 monad 的概念来完成。


1这个过程与 OO 程序员一直不动声色地进行的建模并没有太大不同。例如,文件系统不是类的实例;而是类的实例。它不是存在于记忆中并且有方法的东西。但这并不能阻止面向对象程序员创建一个类来表示文件系统,这样您就可以调用该类实例上的方法,这些方法将被转换为文件系统上的较低级别操作。

2 GHC中实际实现的实际细节杂乱且复杂;它不完全是这两种解释中的任何一种,而且与 Haskell 中的实际编程无关。