纯函数式语言真的能保证不变性吗?



在纯函数式语言中,难道不能定义一个"赋值"操作符,比如"<-",这样的命令,比如"i <- 3",而不是直接赋值不可变变量i,将创建整个当前调用堆栈的副本,除了用新调用堆栈中的3替换i,并从那个点开始执行新的调用堆栈吗?如果没有数据发生实际变化,那么根据定义,这不仍然被认为是"纯功能"吗?当然,编译器会简单地进行优化,将3赋值给i,在这种情况下,命令式和纯函数式之间的区别是什么?

纯函数式语言,如Haskell,有对命令式语言建模的方法,而且它们也不回避承认这一点。:)

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

所以,最后,Haskell简单地重新发明了命令式转轮?

在某种意义上,是的。I/O单子构成一个小祈使句Haskell中的子语言,因此程序的I/O组件可以看起来类似于普通祈使句代码。但有一点很重要区别:没有特别之处用户需要处理的语义与。特别是,等式Haskell中的推理不是妥协。命令式的感觉程序中的一元代码则不需要的功能方面的减损Haskell。经验丰富的功能程序员应该能够最小化的命令式成分程序,只使用I/O单子最少量的顶级测序。单子很干净将功能和命令式程序组件。在相比之下,命令式语言与功能子集一般不会两者之间有明确的界限吗纯粹的功能性和命令性世界。

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

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

你的想法并不像你想象的那样有缺陷。首先,如果只熟悉命令式语言的人想要遍历一段整数,他们可能会想,如果没有增加计数器的方法,这怎么可能实现。

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

你提到的GetStack将返回当前堆栈作为数据结构。这确实违反了函数的纯洁性,因为每次调用它(没有参数)时,它必然返回不同的结果。但是对于函数CallWithStack,您将自己的函数传递给它,它回调到您的函数并将当前堆栈作为参数传递给它。那完全没问题。CallCC的工作原理与此类似。

Haskell并没有轻易地提供内省或"执行"调用堆栈的方法,所以我不会太担心那个特殊的奇怪方案。然而,一般来说,确实可以使用不安全的"函数"(如unsafePerformIO :: IO a -> a)来破坏类型系统。这样做的目的是让违背纯洁变得困难,而不是不可能。

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

有人建议通过禁止这种类型系统的破坏来实际保证安全;我对它不太熟悉,但是你可以在这里读到。

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

复制数据的操作a <- expr仍然是一个命令式操作,如果从程序员的角度来看,指向位置a的值似乎已经改变。

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

最新更新