3

在纯粹的函数式语言中,不能仍然定义一个“赋值”运算符,比如“<-”,这样命令,比如“i <- 3”,而不是直接分配不可变变量 i,会创建整个当前调用堆栈的副本,​​除了在新调用堆栈中将 i 替换为 3,并从该点开始执行新调用堆栈?鉴于没有实际更改数据,根据定义,这是否仍被视为“纯功能性”?当然编译器会简单地进行优化以简单地将 3 分配给 i,在这种情况下,命令式和纯函数式有什么区别?

4

3 回答 3

5

纯粹的函数式语言,例如 Haskell,有建模命令式语言的方法,而且他们也不羞于承认这一点。:)

请参阅http://www.haskell.org/tutorial/io.html,特别是 7.5:

那么,到最后,Haskell 是否只是简单地重新发明了命令式轮子?

从某种意义上说,是的。I/O monad 构成了 Haskell 内部的一种小型命令式子语言,因此程序的 I/O 组件可能看起来类似于普通的命令式代码。但是有一个重要的区别:没有用户需要处理的特殊语义。特别是,Haskell 中的等式推理并没有受到影响。程序中一元代码的命令式感觉并没有减损 Haskell 的功能方面。一个有经验的函数式程序员应该能够最小化程序的命令式组件,只使用 I/O monad 进行最少量的顶层排序。monad 将函数式和命令式程序组件清晰地分开。相比之下,

因此,函数式语言的价值不在于它们使状态突变成为不可能,而是它们提供了一种方法,允许您将程序的纯功能部分与状态突变部分分开。

当然,您可以忽略这一点并以命令式风格编写整个程序,但是您不会利用该语言的功能,那么为什么要使用它呢?

更新

你的想法并不像你想象的那么有缺陷。首先,如果只熟悉命令式语言的人想要遍历一系列整数,他们可能想知道如何在没有增加计数器的方法的情况下实现这一点。

但当然,您只需编写一个充当循环主体的函数,然后让它调用自己。函数的每次调用都对应一个“迭代步骤”。并且在每次调用的范围内,参数都有不同的值,就像一个递增的变量。最后,运行时可以注意到递归调用出现在调用的末尾,因此它可以重用函数调用堆栈的顶部而不是增加它(尾调用)。即使是这个简单的模式也几乎具有您的想法的所有味道 - 包括编译器/运行时悄悄地介入并实际发生突变(覆盖堆栈顶部)。它不仅在逻辑上等同于带有变异计数器的循环,而且实际上它使 CPU 和内存在物理上做同样的事情。

您提到了一个GetStack将当前堆栈作为数据结构返回的。这确实违反了功能纯度,因为每次调用它时必然会返回不同的东西(没有参数)。但是一个函数怎么样CallWithStack,你将自己的函数传递给它,它会回调你的函数并将当前堆栈作为参数传递给它?那完全没问题。CallCC有点像这样。

于 2011-04-09T19:23:40.620 回答
4

Haskell 不会轻易地为您提供自省或“执行”调用堆栈的方法,所以我不会太担心那个特殊的奇怪方案。但是总的来说,确实可以使用不安全的“函数”来颠覆类型系统,例如unsafePerformIO :: IO a -> a. 这个想法是使违反纯洁变得困难,而不是不可能。

实际上,在许多情况下,例如在为 C 库创建 Haskell 绑定时,这些机制是非常必要的……通过使用它们,您可以从编译器中消除证明纯度的负担,并由您自己承担。

有一个提议是通过禁止对类型系统的这种颠覆来实际保证安全性;我对它不太熟悉,但你可以在这里阅读。

于 2011-04-09T19:44:41.380 回答
2

不变性是语言的属性,而不是实现的属性。

如果从程序员的角度来看,引用该位置的值似乎已更改,则复制数据的操作a <- expr仍然是命令式操作。a

同样,一个纯粹的函数式语言实现可以覆盖和重用变量到它的核心内容,只要每个修改对程序员是不可见的。例如map,只要语言实现可以推断出任何地方都不需要旧列表,该函数原则上可以覆盖列表而不是创建新列表。

于 2011-04-09T19:56:50.337 回答