函数式编程吓到你了吗?
函数式编程吓到你了吗?
原文:https://medium.com/hackernoon/does-functional-programming-scare-you-fb95552ff354
函数式编程近来一直在兴起。吹嘘函数式编程中如此多的惊人特性,大量新的函数式编程语言已经出现。所有这些较新的语言都试图减少程序员在转向函数式编程时面临的摩擦。凭借其独特的功能集,其中许多功能都面向特定的受众,例如,Scala 通过在 JVM 上提供函数式和面向对象哲学的独特组合,一直以 Java 和面向对象社区为目标。Elixir 以其独特的 ruby 特性(如语法和基于 actor 模型的进程间通信,建立在 Erlang VM 之上)瞄准了 Ruby 社区。还有更多的语言,它们提供了一些似乎难以抗拒的特性。
即使有这么多吸引人的特性,函数式编程仍然不是那么主流。
函数式范式的需求和好处是毋庸置疑的,事实上,它已经变得如此明显,甚至传统的非函数式语言,如 C++、Java 和 C#也添加了许多特性,以使用户体验函数式范式。
虽然阅读这些语言看起来很酷,但是开始使用函数式语言并不容易,特别是那些长期使用传统的过程式或面向对象语言如 C、C++、Java、Ruby 等编程的人。
函数范式充满了有趣但令人生畏的术语,如无状态、高阶函数、currying、单子、范畴理论等等。
虽然它非常强大,但它是一个巨大的范式转变,需要大量的学习和遗忘。
我们可以尝试建立在我们已经知道的基础上,而不是专注于复杂性和跳入未知。
在这一系列的博客文章中,我们将尝试重温传统编程语言(或普通编程)中的一些基本概念,并看看这些概念在函数式编程范式中是如何发生巨大变化的。
我们开始吧:
递归
在有些问题中,递归是非常自然的选择,例如斐波那契数列问题或涉及树遍历的问题。斐波那契数的定义暗示了一个递归的解决方案
f(n) = f(n-1) + f(n-2)
树遍历也是如此。
几乎每次你在任何中等规模的项目中使用递归编写代码时(用非函数式语言),你都会被要求重新访问你的代码并编写一个等价的迭代代码。我自己被问过几次。虽然很多时候这很容易做到,但有时迭代解可能不像递归解那么简单。
为什么在命令式编程中递归是可怕的?
每当您调用一个函数(它确实包括调用它自己)时,返回地址和参数都被推送到调用堆栈上。堆栈是有限的,所以如果递归太深,最终会耗尽堆栈空间。当你的输入是一个变量,而你对它没有控制权时,通常你不想冒险。
函数式语言的处理方式不同吗?
如果编译器以某种方式找到了优化所需空间的方法,并且我们可以更频繁地编写递归代码,这不是很好吗?
大多数函数式语言正是这样做的。他们使用不同的技术来优化递归调用,以处理堆栈溢出问题。其中一项技术是尾部调用优化。
具体来说,如果是尾部递归(如果递归调用是最后一次调用,并且没有对返回值的操作),他们几乎不需要分配任何额外的堆栈空间就可以完成。
C++中斐波那契数的尾部递归版本
int fib(int term,int val = 1,int prev = 0)
{
if(term == 0)返回 prev
if(term == 1)返回 val
返回 fib(term — 1,val+prev,val);
}
函数式语言中的这种代码是经过优化的,不需要额外的堆栈空间。
这使得递归成为函数范型的自然选择。
因此,递归似乎是一个显而易见的选择的问题,我们不需要思考和额外努力去写迭代代码。
你试过以迭代的方式编写快速排序吗?现在用递归做同样的尝试,看看方法是如何变化的。
递归是函数式编程的核心,所以最好精通它。
数组与列表
C++或 Java 提供的基本数据结构是什么?如果你仔细考虑的话,数组是这些语言中唯一有本地支持的数据结构(和类/结构)。您必须编写的所有其他内容(或者使用它附带的标准库)。
数组附带了一个微不足道但非常重要的随机访问特性。
你如何遍历一个数组?大多数情况下,您将使用循环结构。如何对一个数组的所有元素求和?再次使用循环结构?
现在让我们稍微改变一下问题。
给定一个二叉查找树,你将如何遍历和打印所有的节点?你会使用循环结构还是递归?
对 BST 中的所有数字求和怎么样?
如果您看到构造的选择,它会根据您正在遍历的数据结构而改变。虽然您可能对数组使用递归,或者对 BST 使用非递归解决方案,但是解决方案不会很自然地出现。
链表呢?
你将如何把一个链表中的所有数字相加?
你能把 list 想象成一个递归结构,就像一个元素指向另一个 list 一样吗?
如果你把一个链表看作递归结构,你可以应用一些递归方法来解决许多问题。
所有数字的求和或乘法或类似的问题,我们在迭代每个元素并应用一些函数(如乘法或平方等)后积累数据是一个非常常见的任务。所有的函数式语言都为这种叫做归约或折叠的操作提供了某种抽象。
Javascript 中数组所有数字的总和:
var sum = [1,2,3]。reduce(add,0);
函数 add(a,b) {
返回 a+b;
}
console . log(sum);// 6
数组/列表中的其他常见操作呢?
您可能已经观察到,我们经常对数组/列表执行一些操作,例如,选择符合特定标准的元素子集,通过对原始数组执行一些标准操作(例如,从文件中读取的所有链接中删除新行字符),在对每个元素应用一些标准操作(例如,所有数字的平方和等)后累积结果。
这些是如此的普遍,以至于大多数语言都提供了这样做的捷径,这样我们就不必一直编写自己的递归代码(当然,其中许多都是使用递归和列表实现的)。
Map、Reduce、Filter、Fold 等是函数式语言中提供的一些常见构造,它们极大地减少了循环构造的必要性。
流水线、高阶函数、currying 等
到目前为止,我们只涉及了一些琐碎但基本的概念,这些概念将有助于我们理解函数范式。在接下来的文章中,我将会谈到许多剩余的概念。请继续关注第二部分。
黑客中午是黑客如何开始他们的下午。我们是 @AMI 家庭的一员。我们现在接受投稿,并乐意讨论广告&赞助机会。
要了解更多信息,请阅读我们的“关于”页面、在脸书上点赞/给我们发消息,或者简单地说, tweet/DM @HackerNoon。