为什么在F#中允许使用mutable?他们何时必不可少?

时间:2015-04-14 18:53:07

标签: f#

来自C#,试图理解我的语言。

根据我的理解,F#的主要好处之一就是你放弃了状态的概念,这应该(在很多情况下)使事情更加健壮。

如果是这种情况(并纠正我,如果不是这样),为什么允许我们用mutable来打破这个原则?对我来说,感觉它们不属于语言。我知道你不必使用它们,但是它为你提供了偏离轨道并以OOP方式思考的工具。

任何人都可以举例说明可变值的必要值吗?

3 个答案:

答案 0 :(得分:8)

声明(无状态)代码的当前编译器不是很聪明。这导致大量的内存分配和复制操作,这相当昂贵。改变对象的某些属性允许在新状态下重新使用对象,这要快得多。

想象一下,你创造了一个游戏,10000个单位每秒移动60个刻度。您可以在F#中执行此操作,包括在单个CPU核心上与可变四元组或八叉树进行冲突。

现在想象单位和四叉树是不可变的。编译器没有比每秒分配和构造600000单位并每秒创建60个新树更好的想法。这排除了其他管理结构的任何变化。在具有复杂单元的实际用例中,这种解决方案将太慢。

F#是一种多范式语言,它使程序员能够编写功能性的,面向对象的,并在某种程度上编写命令式程序。目前,每种变体都有其有效用途。也许,在未来的某个时刻,更好的编译器将允许更好地优化声明性程序,但是现在,当性能成为问题时,我们必须回到命令式编程。

答案 1 :(得分:7)

除了其他方面,具有使用可变状态的能力通常对于性能原因很重要。

考虑实现API List.take: count : int -> list : 'a list -> 'a list,它返回一个仅包含输入列表中第一个count元素的列表。

如果您受到不变性的约束,列表只能从前到后构建。实施take然后归结为

  • 使用输入中的第一个count人物来回建立结果列表: O(计数)
  • 反转该结果并返回 O(计数)

出于性能原因,F#运行时具有神奇的特殊能力,可以在需要时从前到后构建列表(即,将最后一个人的尾部变为指向新的尾部元素)。用于List.take的基本算法是:

  • 从输入的第一个count人员前后建立结果列表: O(计数)
  • 返回结果

相同的渐近性能,但实际上它在这种情况下使用变异的速度是其两倍。

普遍的可变状态可能是一场噩梦,因为代码难以推理。但是,如果您将代码考虑在内以便将可变状态紧密封装(例如,在List.take的实现细节中),那么您可以在有意义的情况下享受其优势。因此,将不可变性作为默认值,但仍然允许可变性,是该语言非常实用且有用的功能。

答案 2 :(得分:6)

首先,在我看来,使F#强大的原因不仅仅是默认的不变性,而是整个功能组合,例如:默认的不变性,类型推断,轻量级语法,总和(DUs)和产品类型(元组),默认情况下模式匹配和currying。可能更多。

这些使得F#在默认情况下非常实用,它们会让你以某种方式编程。特别是当您使用可变状态时,它们会让您感到不舒服,因为它需要mutable关键字。这种意义上的不舒服意味着更加小心。这正是你应该做的。

可变状态不是禁止的或邪恶的本身,但它应该是受控。明确使用mutable的需要就像一个警告标志,让您意识到危险。如何控制它的好方法是在函数内部使用它。这样你就可以拥有自己的内部可变状态,并且仍然是完全线程安全的,因为你没有共享可变状态。实际上,即使内部使用可变状态,您的函数仍然可以是引用透明的。

为什么F#允许可变状态;如果没有它的可能性,编写通常的现实代码是非常困难的。例如在Haskell中,类似随机数的东西不能像在F#中那样完成,而是需要明确地通过状态进行线程化。

当我编写应用程序时,我倾向于以非常实用的方式拥有大约95%的代码库,只需1:1便携式就可以毫不费力地说Haskell。但是在系统边界或某些性能关键的内部循环中使用可变状态。这样你就可以获得两全其美的效果。

相关问题