我试图了解功能语言的核心概念:
“函数式语言的一个核心概念是函数的结果取决于其输入,并且仅取决于其输入。没有副作用!”
我的问题是,如果一个函数只在其本地环境中进行更改并返回结果,它如何与数据库或文件系统交互?根据定义,这不是访问实际上是全局变量或全局状态的东西吗?
用于解决或解决此问题的最常见模式是什么?
我试图了解功能语言的核心概念:
“函数式语言的一个核心概念是函数的结果取决于其输入,并且仅取决于其输入。没有副作用!”
我的问题是,如果一个函数只在其本地环境中进行更改并返回结果,它如何与数据库或文件系统交互?根据定义,这不是访问实际上是全局变量或全局状态的东西吗?
用于解决或解决此问题的最常见模式是什么?
仅仅因为函数式语言是函数式的(甚至可能像 Haskell 一样完全纯粹!),并不意味着用该语言编写的程序在运行时必须是纯粹的。
例如,在处理副作用时,Haskell 的方法可以很简单地解释:让整个程序本身是纯的(意味着函数总是为相同的参数返回相同的值并且没有任何副作用),但是让main
函数的返回值是一个可以运行的动作。
试图用伪代码解释这一点,这里有一些用命令式、非函数式语言编写的程序:
main:
read contents of abc.txt into mystring
write contents of mystring to def.txt
上面的main
过程就是:描述如何执行一系列动作的一系列步骤。
将此与 Haskell 等纯函数式语言进行比较。在函数式语言中,一切都是表达式,包括主函数。因此,可以像这样阅读上述程序的等价物:
main = the reading of abc.txt into mystring followed by
the writing of mystring to def.txt
因此,main
是一个表达式,在评估时将返回一个操作,描述为了执行程序要做什么。这个动作的实际执行发生在程序员的世界之外。这就是它的工作原理;以下是可以编译和运行的实际 Haskell 程序:
main = readFile "abc.txt" >>= \ mystring ->
writeFile "def.txt" mystring
a >>= b
在这种情况下,可以说是“动作a
后面跟着动作的结果a
” b
,而运算符的结果是组合动作a和b。上面的程序当然不是惯用的 Haskell;可以将其重写如下(删除多余的变量):
main = readFile "abc.txt" >>=
writeFile "def.txt"
...或者,使用语法糖和 do-notation:
main = do
mystring <- readFile "abc.txt"
writeFile "def.txt" mystring
上述所有程序不仅是等价的,而且就编译器而言它们是相同的。
这就是文件、数据库系统和 Web 服务器可以编写为纯函数程序的方式:通过程序将动作值线程化,以便将它们组合起来,最后在main
函数中结束。这给了程序员对程序的巨大控制权,这也是为什么纯函数式编程语言在某些情况下如此吸引人的原因。
处理函数式语言中的副作用和杂质的最常见模式是:
例子:
set!
var
Haskell 有点作弊——它的解决方案是,对于访问文件系统或数据库的函数,当时整个宇宙的状态,包括 filesystem/db 的状态,将被传递给函数。 (1) 因此,如果你可以复制整个宇宙在那一刻的状态,那么你可以从这样的函数中得到两次相同的结果。当然,你不能在那个瞬间复制整个宇宙的状态,所以函数返回不同的值......
但是 Haskell 的解决方案,恕我直言,并不是最常见的。
(1)不确定这里的具体情况。感谢 CAMcCann 指出这个比喻被过度使用并且可能不是那么准确。
访问数据库与其他输入输出情况没有什么不同,例如print(17)
.
在热切评估的语言中,如 LISP 和 ML,有效编程的常用方法只是使用副作用,就像在大多数其他编程语言中一样。
在 Haskell 中,IO 问题的解决方案是使用 monads。例如,如果您检查HDBC,一个 haskell 数据库库,您可以看到那里的许多函数返回 IO 操作。
一些语言,如 Clean,使用唯一性类型来强制执行 Haskell 对 monad 所做的相同类型的顺序性,但如今这些语言更难找到。