你什么时候不想使用函数式编程?它有什么不擅长的?
我更多的是寻找整个范式的缺点,而不是“没有广泛使用”或“没有好的调试器可用”之类的东西。到目前为止,这些答案可能是正确的,但它们处理的 FP 是一个新概念(一个不可避免的问题),而不是任何固有的品质。
有关的:
你什么时候不想使用函数式编程?它有什么不擅长的?
我更多的是寻找整个范式的缺点,而不是“没有广泛使用”或“没有好的调试器可用”之类的东西。到目前为止,这些答案可能是正确的,但它们处理的 FP 是一个新概念(一个不可避免的问题),而不是任何固有的品质。
有关的:
我很难想到函数式编程的许多缺点。再说一次,我是函数式编程国际会议的前任主席,所以你可以放心地认为我有偏见。
我认为主要的缺点与孤立和进入障碍有关。学习编写好的函数式程序意味着学习以不同的方式思考,并且要做好它需要大量的时间和精力投入。没有老师很难学习。这些属性会导致一些缺点:
一个新人编写的函数式程序很可能会变得不必要地慢——比说,比一个 C 新手编写的 C 程序更有可能。另一方面,一个新人编写的 C++ 程序的可能性差不多会不必要地慢。(所有那些闪亮的功能......)
一般来说,专家编写快速的函数式程序是没有困难的;事实上,一些在 8 核和 16 核处理器上性能最好的并行程序现在是用Haskell编写的。
与开始使用 Python 或 Visual Basic 的人相比,开始函数式编程的人更有可能在实现所承诺的生产力收益之前就放弃了。书籍和开发工具的形式没有那么多的支持。
可以聊天的人越来越少。Stackoverflow 就是一个很好的例子。相对较少的 Haskell 程序员会定期访问该网站(尽管部分原因是 Haskell 程序员有自己的活跃论坛,这些论坛比 Stackoverflow 更老、更完善)。
与你的邻居交谈也不是很容易,因为函数式编程概念比 Smalltalk、Ruby 和 C++ 等语言背后的面向对象概念更难教授和更难学习。而且,面向对象的社区花费了数年时间来为他们所做的事情开发出很好的解释,而函数式编程社区似乎认为他们的东西显然很棒,不需要任何特殊的隐喻或词汇来解释。(他们错了。我还在等待第一本伟大的书功能设计模式。)
惰性函数式编程的一个众所周知的缺点(适用于 Haskell 或 Clean,但不适用于 ML、Scheme 或 Clojure)是很难预测评估惰性函数式程序的时间和空间成本——即使专家也做不到它。这个问题是范式的基础,并且不会消失。有很好的工具可以事后发现时间和空间行为,但要有效地使用它们,你必须已经是专家。
函数式编程的一大缺点是,在理论上,它与硬件以及大多数命令式语言都不匹配。(这是它明显优势之一的反面,能够表达你想要做的事情,而不是你希望计算机如何做。)
例如,函数式编程大量使用递归。这在纯 lambda 演算中很好,因为数学的“堆栈”是无限的。当然,在真正的硬件上,堆栈是非常有限的。在大型数据集上天真地递归可以使您的程序蓬勃发展。大多数函数式语言都优化了尾递归,因此不会发生这种情况,但是使算法尾递归可能会迫使您做一些相当不美观的代码体操(例如,尾递归映射函数创建一个向后列表或必须建立一个差异列表,因此与非尾递归版本相比,它必须做额外的工作才能以正确的顺序返回正常映射列表)。
(感谢 Jared Updike 的差异列表建议。)
如果您的语言没有提供良好的机制来通过您的程序检测状态/异常行为(例如,单子绑定的语法糖),那么任何涉及状态/异常的任务都会成为一件苦差事。(即使有这些糖,有些人可能会发现处理 FP 中的状态/异常更加困难。)
函数式惯用语经常做大量的控制反转或懒惰,这通常对调试(使用调试器)产生负面影响。(由于不变性/引用透明性,FP 不太容易出错,这在一定程度上抵消了这一点,这意味着您需要减少调试频率。)
Philip Wadler 写了一篇关于此的论文(名为Why No One Uses Functional Programming Languages)并解决了阻止人们使用 FP 语言的实际陷阱:
更新:具有 ACM 访问权限的用户无法访问旧链接:
除了速度或采用问题以及解决一个更基本的问题之外,我听说在函数式编程中,为现有数据类型添加新函数非常容易,但添加新数据类型“很难”。考虑:
(用 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!";
但是请注意,对于面向对象的语言,情况正好相反。向抽象类添加新的子类非常容易,但是如果您想向抽象类/接口添加新的抽象方法以供所有子类实现,则可能会很乏味。
我只是想讲一个轶事,因为我现在正在学习 Haskell。我正在学习 Haskell,因为将函数与动作分离的想法很吸引我,并且由于将纯函数与非纯函数隔离,隐式并行化背后有一些非常性感的理论。
我已经学习了折叠类函数三天了。Fold 似乎有一个非常简单的应用:获取一个列表并将其缩减为单个值。Haskell 实现了foldl
, 和foldr
为此。这两个函数有很大不同的实现。有一个替代实现foldl
,称为foldl'
。除此之外,还有一个版本的语法略有不同foldr1
,并且foldl1
具有不同的初始值。其中有对应的foldl1'
for实现foldl1
。好像所有这些都不是令人兴奋的,这些功能fold[lr].*
require 作为参数并在减少内部使用有两个单独的签名,只有一个变体适用于无限列表 (r),并且只有一个变体在常量内存中执行(据我所知 (L),因为它只需要 a redex
)。理解为什么foldr
可以在无限列表上工作至少需要对语言惰性行为有一个不错的理解,以及并非所有函数都会强制评估第二个参数的次要细节。对于那些在大学里从未见过它们的人来说,这些函数的在线图表简直令人困惑。没有perldoc
相等的。我找不到关于 Haskell 前奏曲中的任何功能的单一描述。前奏曲是一种预装核心的发行版。我最好的资源真的是一个我从未见过的人(Cale),他以巨大的时间帮助我。
哦,折叠不必将列表简化为非列表类型的标量,可以编写列表的恒等函数foldr (:) [] [1,2,3,4]
(可以累积到列表的亮点)。
/me 回去读书。
以下是我遇到的一些问题:
抛开函数式编程的具体实现细节来看,我看到了两个关键问题:
选择一些现实世界问题的函数模型而不是命令式模型似乎比较少见。当问题域势在必行时,使用具有该特性的语言是自然而合理的选择(因为通常建议尽量减少规范和实现之间的距离,以减少细微错误的数量)。是的,这可以通过一个足够聪明的编码器来克服,但如果你需要摇滚明星编码器来完成这项任务,那是因为它太难了。
出于某种我从未真正理解的原因,函数式编程语言(或者可能是它们的实现或社区?)更有可能希望用它们的语言来拥有一切。用其他语言编写的库的使用要少得多。如果其他人对某些复杂的操作有特别好的实现,那么使用它而不是自己制作会更有意义。我怀疑这部分是由于使用了复杂的运行时,这使得处理外来代码(尤其是高效地处理)相当困难。我很想在这一点上被证明是错误的。
我想这两者都归结为由于编程研究人员比普通编码人员更强烈地使用函数式编程而导致普遍缺乏实用主义。一个好的工具可以让专家做出伟大的事情,但一个伟大的工具是让普通人能够接近专家通常可以做的事情,因为这是更困难的任务。