函数式编程的缺陷

什么时候你不想使用函数式编程? 它不擅长什么?

我更倾向于寻找整个范式的缺点,而不是像“没有广泛使用”或“没有好的调试器可用”这样的事情。到目前为止,这些答案可能是正确的,但它们涉及的是 FP 是一个新概念(一个不可避免的问题) ,而不是任何固有的品质。

相关阅读:

26669 次浏览

如果您的语言没有提供良好的机制来通过您的程序探测状态/异常行为(例如,一元绑定的语法糖) ,那么任何涉及状态/异常的任务都会变得很麻烦。(即使有了这些糖,一些人可能会发现在 FP 中处理状态/异常更加困难。)

函数习惯用法通常会导致大量的控制反转或懒惰,这通常会对调试(使用调试器)产生负面影响。(由于不可变性/参照透明度(计算机科学) ,FP 不太容易出错,这意味着需要更少的调试,因此这一点有所抵消。)

以下是我遇到的一些问题:

  1. 大多数人发现函数式编程很难理解。这意味着您编写函数式代码可能会更加困难,而且几乎可以肯定的是,其他人将更加难以获得它。
  2. 函数式编程语言通常比 c 这样的语言要慢。随着时间的推移,这个问题变得越来越小(因为计算机变得越来越快,编译器变得越来越聪明)
  3. 由于没有像命令式程序那样广泛传播,因此很难找到常见编程问题的库和示例。(例如,几乎总是更容易为 Python 找到一些东西,然后是 Haskell)
  4. 缺乏工具,尤其是用于调试的工具。它肯定不像为 C # 打开 Visual Studio 或为 Java 打开 eclipse 那么容易。

除了速度或采用问题以及解决一个更基本的问题之外,我听说函数式编程很容易为现有的数据类型添加新的函数,但是添加新的数据类型却很“困难”。考虑一下:

(使用 SMLnj 编写。另外,请原谅这个有些做作的例子。)

datatype Animal = Dog | Cat;


fun happyNoise(Dog) = "pant pant"
| happyNoise(Cat) = "purrrr";


fun excitedNoise(Dog) = "bark!"
| excitedNoise(Cat) = "meow!";

我可以很快补充以下内容:

fun angryNoise(Dog) = "grrrrrr"
| angryNoise(Cat) = "hisssss";

但是,如果我向 Animal 添加一个新类型,我必须遍历每个函数来添加对它的支持:

datatype Animal = Dog | Cat | Chicken;


fun happyNoise(Dog) = "pant pant"
| happyNoise(Cat) = "purrrr"
| happyNoise(Chicken) = "cluck cluck";


fun excitedNoise(Dog) = "bark!"
| excitedNoise(Cat) = "meow!"
| excitedNoise(Chicken) = "cock-a-doodle-doo!";


fun angryNoise(Dog) = "grrrrrr"
| angryNoise(Cat) = "hisssss"
| angryNoise(Chicken) = "squaaaawk!";

但是请注意,对于面向对象语言来说,情况正好相反。向抽象类中添加一个新的子类非常容易,但是如果您想为所有要实现的子类向抽象类/接口中添加一个新的抽象方法,那么可能会非常繁琐。

函数式编程的一大缺点是,在理论层面上,它与硬件以及大多数命令式语言不匹配。(这是它一个明显优势的另一面,能够表达你想做的 什么,而不是你想让计算机做的 怎么做。)

例如,函数式编程大量使用递归。这在纯 Lambda 微积分中很好,因为数学的“堆栈”是无限的。当然,在真正的硬件上,堆栈是非常有限的。在大型数据集上进行天真的递归可以使你的程序走向繁荣。大多数函数式语言都优化了尾部递归,这样就不会发生这种情况,但是让算法尾部递归可能会迫使你做一些相当不美观的代码操作(例如,尾部递归映射函数创建一个向后列表,或者必须建立一个差异列表,所以它必须做额外的工作才能返回到一个正常的映射列表,而不是非尾部递归的版本)。

(感谢贾里德•厄普代克(Jared Updike)的差异清单建议。)

Philip Wadler 就此写了一篇论文(题为“为什么没有人使用函数式编程语言”) ,并解决了阻止人们使用 FP 语言的实际陷阱:

更新: ACM 访问者无法访问的旧链接:

我只是想插播一则轶事因为我们说话这会儿,我正在学习 Haskell。我学习 Haskell 是因为将函数和动作分离的想法吸引了我隐式并行背后有一些非常性感的理论因为纯函数和非纯函数是分离的。

我已经学了折叠函数类三天了。Fold 似乎有一个非常简单的应用程序: 获取一个列表并将其减少到一个值。Haskell 为此实现了 foldlfoldr。这两个函数的实现大相径庭。有一个 foldl的替代实现,称为 foldl'。除此之外,还有一个版本,其语法略有不同,称为 foldr1foldl1,其初始值不同。其中对于 foldl1有一个相应的 foldl1'实现。好像所有这一切并不令人震惊,fold[lr].*需要作为参数并在内部使用的函数有两个独立的签名,只有一个变体在无限列表(r)上工作,只有其中一个在常量内存中执行(我理解为(L) ,因为它只需要一个 redex)。要理解为什么 foldr可以在无限列表上工作,至少需要对语言的懒惰行为有一定的了解,并且需要了解一些细节,即并非所有函数都会强制计算第二个参数。对于那些在大学里从未见过这些功能的人来说,网上的图表非常令人困惑。没有 foldr1等价物。我找不到 Haskell 序曲中的任何函数的任何描述。序曲是一种预先加载的发行版,随核心而来。我最好的资源实际上是一个我从未见过的家伙(凯尔) ,他花费了大量的时间来帮助我。

对了,折叠不需要将列表减少为非列表类型的标量,列表的标识函数可以写成 foldr (:) [] [1,2,3,4](可以将高亮累积到列表中的高亮部分)。

我还是继续读书吧。

我很难想到函数式编程的许多缺点。不过话说回来,我曾经是函数式编程国际会议的主席,所以你可以有把握地认为我是有偏见的。

我认为,主要的不利因素与孤立和进入壁垒有关。学习编写 很好函数程序意味着学习不同的思维方式,以及 做好它需要大量的时间和精力投入。没有老师学习是困难的。这些特性带来了一些负面影响:

  • 一个新手写的函数式程序很可能会不必要地慢下来,比如说,一个 C 语言程序由一个新手写到 C 语言程序。另一方面,一个 C + + 程序由一个新手写到 C 语言程序也同样可能会不必要地慢下来。(所有那些闪闪发光的特征...)

    一般来说,专家不难编写快速的函数式程序; 事实上,一些在8核和16核处理器上性能最好的并行程序现在都是用 Haskell编写的。

  • 与 Python 或 VisualBasic 相比,开始函数式编程的人更有可能在实现所承诺的生产率提高之前就放弃。只是在书籍和开发工具的形式上没有那么多的支持。

  • 可以交谈的人越来越少了。Stackoverflow 就是一个很好的例子; 定期访问该网站的 Haskell 程序员相对较少(尽管部分原因是 Haskell 程序员拥有自己的活跃论坛,这些论坛比 Stackoverflow 更古老、更成熟)。

    你不能很容易地与你的邻居交谈,这也是事实,因为函数式编程的概念比诸如 Smalltalk、 Ruby 和 C + + 等语言背后的面向对象的概念更难教授和学习。而且,面向对象社区已经花费了数年的时间来为他们所做的事情做出很好的解释,而函数式编程社区似乎认为他们的东西显然很棒,并且不需要任何特殊的隐喻或词汇表来解释。(他们错了。我仍然在等待第一本伟大的书 功能设计模式。)

  • 懒惰函数式编程的一个众所周知的缺点(适用于 Haskell 或 Clean,但不适用于 ML 或 Scheme 或 Clojure)是,很难预测评估 懒惰函数式编程的时间和空间成本,即使是专家也无法做到。这个问题是范式的基础,并且不会消失。有很多优秀的工具可以发现时间和空间行为 事后,但是要有效地使用它们,你必须已经是专家了。

抛开函数式编程具体实现的细节不谈,我看到了两个关键问题:

  1. 相对于命令式问题而言,选择一个真实世界问题的函数模型似乎相当罕见。当问题领域是必需的时候,使用具有该特性的语言是一个自然而合理的选择(因为通常建议最小化规范和实现之间的距离,以减少微妙的 bug 的数量)。是的,这可以克服一个足够聪明的编码器,但如果你需要摇滚明星编码器的任务,这是因为它太难了。

  2. 由于某些我从未真正理解的原因,函数式编程语言(或者它们的实现或社区?)更有可能希望所有的东西都用他们的语言。用其他语言编写的库的使用要少得多。如果其他人拥有某个复杂操作的特别好的实现,那么使用它而不是创建自己的操作会更有意义。我怀疑这在一定程度上是因为使用复杂的运行时使得处理外部代码(尤其是高效地处理)变得相当困难。我希望在这一点上被证明是错误的。

我认为这两者都是由于函数式编程被编程研究人员比普通编码人员更多地使用而导致的普遍缺乏实用主义。一个好的工具可以使一个专家做出伟大的事情,但是一个伟大的工具能够使普通人接近一个专家能够正常做的事情,因为这是更加困难的任务。