非持久性数据结构能否以纯粹的功能方式使用?

时间:2016-10-23 12:22:52

标签: scala haskell f# functional-programming

这是关于函数式编程的一般性问题,但我也对它在特定语言中的答案感兴趣。

我只有初学者关于函数式语言的知识,所以请耐心等待。

我的理解是,函数式语言把重点放在不同于命令式语言的数据结构上,因为它们喜欢不变性:持久性数据结构

例如,它们都有一个不可变列表概念,您可以从现有列表x :: l和两个新项y :: ll形成新列表xy l,不需要复制data.modify(someArgument) 的所有元素。这可能是由新的列表对象实现的,内部指向旧的列表对象作为尾部。

在命令式语言中,很少使用这样的数据结构,因为它们不像c风格的数组那样提供良好的引用局部性。

一般来说,找到支持功能风格的数据结构是它自己的努力,所以如果一个人不总是那么做就会很棒。

现在,如果有正确的语言支持,人们如何在函数式编程中使用所有经典数据结构。

通常,命令式语言中的数据结构具有在其上定义的修改操作(伪代码):

newData = modified(data, someArgument)

写这个的功能方式是

data

一般问题是这通常需要复制数据结构 - 除非语言知道modified实际上不会被其他任何东西使用:然后,修改可以以变异的形式完成原来没有人能分辨出来。

有一大类案例,语言可以推断出&#34;从未在别处使用的属性&#34;:当newData = modified(modified(data, someArgument)) 的第一个参数是未绑定的值时,如下例所示:< / p>

data

此处modified(data, someArgument)可能会在其他地方使用,但Data modified(Data const& data) { // returns a modified copy } Data modified(Data && data) { // returns the modified original } 显然不是。

这在C ++中被称为&#34; rvalue&#34;,并且在C ++的最新版本中,具有讽刺意味的是其他功能完全没有用,否则可以在这样的rvalues上重载。

例如,可以写:

<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

这意味着在C ++中,实际上可以采用任何可变的高效数据结构并将其转换为具有不可变的api,可以像纯函数一样有效地使用,就像命令式版本一样。

(需要注意的是,在C ++中仍然有时需要强制转换rvalue。当然,需要注意实现这样的数据结构,即使用rvalue重载时。虽然改进了。)

现在我的问题:

实际的功能语言是否有类似的机制?或者由于某些其他原因这不是必需的吗?

(我标记了一些我特别感兴趣的特定语言。)

4 个答案:

答案 0 :(得分:6)

确实,持久数据结构比可变数据结构慢。对此没有争议。有时差异可以忽略不计(迭代链表而不是数组),有时它可能很大(反向迭代),但这不是重点。使用不可变数据的选择是(或应该是)有意识的数据:我们为了稳定而交易性能。

考虑到这一点:对于大多数(并非所有)现代程序而言,本地性能不是问题。对于今天的程序,真正的性能瓶颈在于并行化 - 在具有共享内存的本地计算机上,以及在不同的计算机上。随着我们现在处理的数据量的增加,挤出内存位置和分支预测的最后一点并不会削减它。我们需要规模。并猜测并行程序中的第一个错误来源?那是对的 - 突变。

现代节目的另一个重要问题是稳定性。程序可能会崩溃的日子已经一去不复返了,你刚刚重新启动它并继续工作。今天的程序需要在没有人工干预的无头服务器上工作数月或数年。今天的计划不能只是抛出数字武器,期望人类找出问题所在。在这种情况下,本地性能不如稳定性和并行化重要得多:购买(或租用)另外十台服务器要比雇用人员不时重新启动程序便宜得多。

确实可以使用变异制作可并行化且稳定的程序。理论上这是可能的。这更加艰难。对于不可变数据,您必须首先瞄准

然后,这里有一些观点:我们已经在那里了。您经常在代码中使用goto指令?你有没有考虑过这是为什么?你可以使用goto做各种各样的表演技巧,但我们选择不这样做。在编程历史的某些时刻,我们已经确定goto比它的价值更麻烦。同样的事情发生在原始指针上:许多语言根本没有它们,其他语言则严格保护它们,甚至在那些不受限制地访问原始指针的语言中,它现在被认为是一种糟糕的形式使用它们。今天,我们处于下一阶段的中间:首先我们放弃了goto,然后我们放弃了原始指针,现在我们慢慢放弃变异。

然而,如果你真的发现自己出于正当理由推动本地性能的发展,并且你已经确定不可变数据确实是瓶颈(记住:先测量,然后优化),那么大多数函数式语言(除了Haskell和Elm)都会让你逃避变异,尽管很不情愿。就像C#中的原始指针一样,你可以有变异,你只需要明确(并且小心)它。例如,在F#中,您可以拥有可变变量,原始数组,可变记录,类,接口等。这是可能的,只是不推荐。到目前为止,普遍的共识是只要它本地化(即不泄漏到外部)就可以使用变异,并且你真的知道你正在做什么,而你#&# 39;记录了它,并将其测试为死亡。

一个常见的例子就是&#34;值构造&#34;,你的函数最终产生一个不可变的值,但在做这个时会做各种混乱的事情。一个例子是F#核心库如何实现List.map:通常,因为列表是从前到后迭代的,但是从前到后构造,需要首先通过迭代构造转换后的列表,然后将其反转。所以F#编译器在这里作弊并在列表中进行变异,以避免额外的反转。

另一个关于&#34; locality&#34;关心。还记得我提到你可以用goto做各种各样的表演技巧吗?嗯,那不再那么真实了。由于程序员开始编写没有goto的程序,二进制代码变得更加可预测,因为跳转现在由编译器生成,而不是由人编码。这使得CPU可以预测它们,并根据这些预测优化处理。最终的结果是,现在您实际上可能通过不加区分地使用goto而不是使用可接受的更高级别的工具(如循环)来获得更差性能。在当天,CPU无法做到那么聪明,因此不使用goto的选择纯粹是一种稳定性衡量标准。但现在事实证明,实际上对于表现有帮助,谁会想到?

我认为同样的事情也会因不变性而发生。我不确定 将如何发生,但我确信它会发生。即使在今天,如果没有特殊的硬件,仍然可以在编译时进行一些优化:例如,如果编译器知道变量是不可变的,它可能决定将它缓存在寄存器中很长一段时间,甚至可以将其升级完全不变的。确实,今天的大多数实际编译器都没有执行所有这些可能的优化(尽管它们确实执行 some ),但它们会。我们才刚刚开始。 : - )

答案 1 :(得分:3)

我非常确定像 alias analysis 这样的功能(检查数据是否在别处使用)不是Scala编译器的一部分(也不是Haskell和Clojure等其他FP语言的一部分) 。 Scala中的集合API(例如)明确地分为immutablemutable个包。 immutable数据结构使用结构共享的概念来否定复制数据的需要,从而减少使用不可变结构的开销(就时间数据量而言)。 / p>

正如您已经指出的那样,像cons ::这样的方法创建了一个新的不可变结构,它在引擎盖下包含对任何现有不可变数据的引用(而不是复制它)。

mutableimmutable类型之间的转换(例如,在Scala中)会复制mutable数据(通常以懒惰的方式),而不是使用任何机制,例如检查mutable结构是否未在其他任何地方引用并允许它进行变异。

当我第一次从Java迁移到Scala时,我最初认为在处理不可变结构时必须创建的(通常)大量时态数据可能是一个性能约束,并涉及一些聪明的技术来实际允许突变这样做是安全的,但事实并非如此,因为这个想法是不可变数据永远不会指向更年轻的价值观。由于在创建旧值时不存在较年轻的值,因此在创建时不能指向它,并且由于值从未被修改,因此以后也不能指向它。结果是像Scala / Haskell这样的FP语言能够生成所有这些时态数据,因为垃圾收集器能够在很短的时间内删除它。

简而言之,Scala / Haskell(我不确定F#)不允许不可变结构的变异,因为像当前JVM这样的运行时环境的状态具有非常有效的垃圾收集,因此具有时间性数据可以很快删除。当然,正如我确定你知道的那样,包含可变元素的不可变结构在Scala这样的FP语言中是完全可能的,但是虽然可变元素可以改变,但是不可变容器不能。元素既不能添加/删除。

答案 2 :(得分:1)

1)函数式编程语言支持持久性数据结构。当数据结构转换为另一个数据结构或对产生新数据结构的数据结构进行任何操作时,数据结构的未更改部分将通过链接重用,尤其是在列表的情况下。

  

在计算中,持久数据结构是一种数据结构,在修改时始终保留自身的先前版本。这样的数据结构实际上是不可变的,因为它们的操作不会(可见地)就地更新结构,而是总是产生新的更新结构。

2)在纯粹懒惰的函数式语言中,计算被推迟,并且只有当表达式的结果用于最终值/结果时才进行评估。这种机制有助于避免不必要的计算。

答案 3 :(得分:0)

Haskell中的ST(状态线程)monad是一种确保按顺序调用某些操作的方法(不可能在该序列之外进行修改)。在ST中,您可以在Haskell中使用命令式,可变的数据结构。请注意,Haskell被认为是为数不多的纯功能语言之一。