Haskell 在这里耍了个花招。 IO 既是纯的,也不是纯的,这取决于您如何看待它。
在“IO 是纯的”方面,您陷入了一个非常常见的错误,即认为函数返回 IO DBThing,因为它返回的是 DBThing。当有人声称 Stuff -> IO DBThing 类型的函数是纯函数时,他们不是说您可以提供相同的 Stuff 并始终得到相同的 DBThing;正如您正确指出的那样,这是不可能的,也不是很有用!他们节省的是,给定特定的Stuff,您将始终得到相同的IO DBThing。
实际上,您根本无法从IO DBThing 中得到DBThing,因此Haskell 不必担心数据库在不同时间包含不同的值(或不可用)。使用IO DBThing 所能做的就是将它与需要DBThing 的其他东西结合起来,并产生其他类型的IO thing;这种组合的结果是IO thing。
Haskell 在这里所做的是在纯 Haskell 值的操作与程序之外的世界中可能发生的变化之间建立对应关系。有些事情你可以用一些普通的纯值来做,这些事情对不纯的操作没有任何意义,比如改变数据库的状态。因此,使用 IO 值与外部世界之间的对应关系,Haskell 根本不会为您提供对 IO 值的任何操作,将对应于在现实中没有意义的事情世界。
有几种方法可以解释您是如何“纯粹”操纵现实世界的。一种是说IO 就像一个状态单子,只有被线程化的状态是你程序之外的整个世界;=(所以你的Stuff -> IO DBThing 函数确实有一个额外的隐藏参数来接收这个世界,实际上返回一个 DBThing 和另一个世界;它总是被不同的世界调用,这就是为什么即使使用相同的 Stuff 调用它也可以返回不同的 DBThing 值)。另一种解释是IO DBThing 值本身就是一个命令式程序;你的 Haskell 程序是一个完全不做 IO 的纯函数,它返回一个做 IO 的不纯程序,而 Haskell 运行时系统(不纯)执行它返回的程序。
但实际上,这些都只是比喻。关键是 IO 值只是有一个非常有限的界面,它不允许你做任何在现实世界中没有意义的事情。
请注意,monad 的概念实际上并没有出现。 Haskell 的 IO 系统真的不依赖于 monad。 Monad 只是一个方便的接口,它有足够的限制,如果你只使用通用的 monad 接口,你也不能打破 IO 限制(即使你不知道你的 monad 是实际上是 IO)。由于Monad 接口也足够有趣,可以编写很多有用的程序,IO 形成一个 monad 的事实允许大量对其他类型有用的代码在 IO 上通用地重用。
这是否意味着您实际上可以编写纯 IO 代码?并不真地。这是硬币的“当然 IO 不是纯粹的”一面。当您使用花哨的“将 IO 功能组合在一起”时,您仍然需要考虑您的程序一个接一个地(或并行地)执行步骤,影响并受外部条件和系统的影响;简而言之,您必须使用与用命令式语言编写 IO 代码完全相同的推理(仅使用比大多数类型系统更好的类型系统)。使 IO 变得纯粹并不能真正帮助您消除对代码的思考方式中的杂质。
那有什么意义呢?对于一个人来说,它为我们提供了一种编译器强制划分的可以执行 IO 的代码和不能执行 IO 的代码。如果类型上没有 IO 标记,则不涉及不纯 IO。 那在任何语言中都是有用的。编译器也知道这一点; Haskell 编译器可以对在大多数其他语言中无效的非 IO 代码进行优化,因为通常不可能知道给定的代码部分没有有副作用(除非你能看到完整的代码调用的所有内容的实现,可传递)。
另外,由于 IO 是纯粹的,代码分析工具(包括你的大脑)不必专门处理 IO 代码。如果您可以挑选出对与 IO 代码具有相同结构的纯代码有效的代码转换,则可以在 IO 代码上进行。编译器利用了这一点。 IO 代码必须使用的结构排除了许多转换(为了保持在与外界事物有合理对应关系的事物范围内),但它们也会被排除在外任何使用相同结构的纯代码; IO接口的精心构建使得“执行顺序依赖”看起来像普通的“数据依赖”,所以你可以直接使用数据依赖的规则来确定使用IO的规则。