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

发布于 2024-10-31 12:10:24 字数 222 浏览 3 评论 0原文

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

In a purely functional language, couldn't one still define an "assignment" operator, say, "<-", such that the command, say, "i <- 3", instead of directly assigning the immutable variable i, would create a copy of the entire current call stack, except replacing i with 3 in the new call stack, and executing the new call stack from that point onward? Given that no data actually changed, wouldn't that still be considered "purely functional" by definition? Of course the compiler would simply make the optimization to simply assign 3 to i, in which case what's the difference between imperative and purely functional?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

还不是爱你 2024-11-07 12:10:24

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

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

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

从某种意义上来说,是的。 I/O 单子
构成一个小命令
Haskell 内部的子语言,因此
程序的 I/O 组件可能
看起来与普通命令式相似
代码。但有一点很重要
区别:无特殊
用户需要处理的语义
和。特别是,等式
Haskell 中的推理不是
妥协了。势在必行的感觉
程序中的一元代码不
有损于功能方面
哈斯克尔。经验丰富的职能
程序员应该能够最小化
的必要组成部分
程序,仅使用 I/O monad
最少数量的顶级
测序。单子干净地
将功能性和
命令式程序组件。在
相比之下,命令式语言
功能子集通常不
之间有明确的障碍
纯粹功能性和命令性
世界。

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

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

更新

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

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

您提到了一个 GetStack 它将返回当前堆栈作为数据结构。这确实违反了函数纯度,因为每次调用它时都必然返回不同的东西(不带参数)。但是,如果您向其传递您自己的函数,然后它回调您的函数并将当前堆栈作为参数传递给它,那么函数 CallWithStack 又如何呢?那就完全没问题了。 CallCC 的工作方式有点类似。

Purely functional languages, such as Haskell, have ways of modelling imperative languages, and they are not shy about admitting it either. :)

See http://www.haskell.org/tutorial/io.html, in particular 7.5:

So, in the end, has Haskell simply
re-invented the imperative wheel?

In some sense, yes. The I/O monad
constitutes a small imperative
sub-language inside Haskell, and thus
the I/O component of a program may
appear similar to ordinary imperative
code. But there is one important
difference: There is no special
semantics that the user needs to deal
with. In particular, equational
reasoning in Haskell is not
compromised. The imperative feel of
the monadic code in a program does not
detract from the functional aspect of
Haskell. An experienced functional
programmer should be able to minimize
the imperative component of the
program, only using the I/O monad for
a minimal amount of top-level
sequencing. The monad cleanly
separates the functional and
imperative program components. In
contrast, imperative languages with
functional subsets do not generally
have any well-defined barrier between
the purely functional and imperative
worlds.

So the value of functional languages is not that they make state mutation impossible, but that they provide a way to allow you to keep the purely functional parts of your program separate from the state-mutating parts.

Of course, you can ignore this and write your entire program in the imperative style, but then you won't be taking advantage of the facilities of the language, so why use it?

Update

Your idea is not as flawed as you assume. Firstly, if someone familiar only with imperative languages wanted to loop through a range of integers, they might wonder how this could be achieved without a way to increment a counter.

But of course instead you just write a function that acts as the body of the loop, and then make it call itself. Each invocation of the function corresponds to an "iteration step". And in the scope of each invocation the parameter has a different value, acting like an incrementing variable. Finally, the runtime can note that the recursive call appears at the end of the invocation, and so it can reuse the top of the function-call stack instead of growing it (tail call). Even this simple pattern has almost all of the flavour of your idea - including the compiler/runtime quietly stepping in and actually making mutation occur (overwriting the top of the stack). Not only is it logically equivalent to a loop with a mutating counter, but in fact it makes the CPU and memory do the same thing physically.

You mention a GetStack that would return the current stack as a data structure. That would indeed be a violation of functional purity, given that it would necessarily return something different each time it was called (with no arguments). But how about a function CallWithStack, to which you pass a function of your own, and it calls back to your function and passes it the current stack as a parameter? That would be perfectly okay. CallCC works a bit like that.

七秒鱼° 2024-11-07 12:10:24

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

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

有人建议通过禁止此类类型系统颠覆来真正保证安全;我对它不太熟悉,但您可以在此处阅读相关内容。

Haskell doesn't readily give you ways to introspect or "execute" call stacks, so I wouldn't worry too much about that particular bizarre scheme. However in general it is true that one can subvert the type system using unsafe "functions" such as unsafePerformIO :: IO a -> a. The idea is to make it difficult, not impossible, to violate purity.

Indeed, in many situations, such as when making Haskell bindings for a C library, these mechanisms are quite necessary... by using them you are removing the burden of proof of purity from the compiler and taking it upon yourself.

There is a proposal to actually guarantee safety by outlawing such subversions of the type system; I'm not too familiar with it, but you can read about it here.

黯然 2024-11-07 12:10:24

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

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

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

Immutability is a property of the language, not of the implementation.

An operation a <- expr that copies data is still an imperative operation, if values that refer to the location a appear to have changed from the programmers point of view.

Likewise, a purely functional language implementation may overwrite and reuse variables to its heart's content, as long as each modification is invisible to the programmer. For example, the map function can in principle overwrite a list instead of creating a new, whenever the language implementation can deduce that the old list won't be needed anywhere.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文