1 什么是函数式编程
函数式编程(Functional Programming),顾名思义就是基于函数的编程模式,非过程、非对象。In computer science, functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids state and mutable data.
这是来自维基百科给出的定义,简单说就是函数式编程是一种编程模型,他将计算机运算看做是数学中函数的计算,并且避免了状态以及变量的概念。
2 理解变量不变性
函数式编程中的变量不变性就是指一个函数无论多少线程进行并发调用都不会存在计算结果不一致的情况。因为函数式编程中没有赋值操作,不会保存状态,也就是无状态性。
3 函数式编程崛起
上面讲到函数式编程的基本特征,接下来揭幕函数式编程为何再次崛起。一直以来,作为函数式编程代表的Lisp,还有Haskell,更多都是在大学、实验室中应用,而很少应用到真实的生产环境。
先让我们再来回顾一下伟大的摩尔定律:
1. 集成电路芯片上所集成的电路的数目,每隔18个月就翻一番。
2. 微处理器的性能每隔18个月提高一倍,而价格下降一半。
3. 用一个美元所能买到的电脑性能,每隔18个月翻两番。按照摩尔的预测,整个信息产业按照摩尔定律飞速地向前发展着,但近年来,我们却可以发现摩尔定律逐渐地失效了,芯片上元件的尺寸是不可能无限地缩小的,这就意味着芯片上所能集成的电子元件的数量一定会在某个时刻达到一个极限。那么当技术达到这个极限时,我们又该如何适应日益增长的计算需求?电子元件厂商给出了答案:多核。
多核并行程序设计就这样被推到了前线,而命令式编程天生的缺陷却使并行编程模型变得非常复杂,无论是信号量,还是锁的概念,都使程序员不堪其重。
就这样,函数式编程终于在数十年后,终于走出实验室,来到了真实的生产环境中,无论是冷门的Haskell,Erlang,还是Scala,F#,都是函数式编程成功的典型。
4 函数
对象是面向对象的第一型,那么函数式编程也是一样,函数是函数式编程的第一型。在函数式编程中努力用函数来表达所有的概念,完成所有的操作。函数是编程的基本单元。在函数式编程中,函数是基本单位,是第一型,他几乎被用作一切,包括最简单的计算,甚至连变量都被计算所取代。在函数式编程中,变量只是一个名称,而不是一个存储单元,这是函数式编程与传统的命令式编程最典型的不同之处。
5 函数式编程的数学本质
编程从来都不是难事儿,无非是细心,加上一些函数类库的熟悉程度,加上经验的堆积,而真正困难的,是如何把一个实际问题,转换成一个数学模型。这也是为什么微软,Google之类的公司重视算法,这也是为什么数学建模大赛在大学计算机系如此被看重的原因。先假设依据良好的数学思维和逻辑思维已经建立好数学模型,那么接下来要做的就是如何把数学语言表达成计算机能看懂的程序语言。
函数式编程模型中赋值模型,同一个函数,同一个参数,却会在不同的场景下计算出不同的结果,这是在数学函数中完全不可能出现的情况。f(x) = y ,那么这个函数无论在什么场景下,都会得到同样的结果,这个我们称之为函数的确定性。赋值模型与数学模型的不兼容之处。而函数式编程取消了赋值模型,则使数学模型与编程模型完美地达成了统一。
6 函数式编程的抽象本质
抽象对于每个程序员都不陌生。在面向对象编程中,类是现实事物的一种抽象表示。那么抽象的最大作用在我看来就在于抽象事物的重用性,一个事物越具体,那么他的可重用性就越低,因此,我们再打造可重用性代码(类、类库),其实在做的本质工作就在于提高代码的抽象性。而再往大了说开来,程序员做的工作,就是把一系列过程抽象开来,反映成一个通用过程,然后用代码表示出来。
在面向对象中,我们把事物抽象。而在函数式编程中,我们则是在将函数方法抽象,函数一样是可重用,可置换的抽象单位。
7 状态如何解决
上面讲述了一堆函数式编程的特点,变量不变性,函数式编程是无状态的,可在现实情况中,状态不可能一直保持不变,而状态必然需要改变、传递,那么在函数式编程中的则是将其保存在函数的参数中,作为函数的附属品来传递。ps:在Erlang中,进程之间的交互传递变量是靠“信箱”的收发信件来实现,其实从本质而言,也是将变量作为一个附属品来传递么!
来看个例子,我们在这里举一个求x的n次方的例子,我们用传统的命令式编程来写一下:
def expr(x,n):
result = 1
for i in range(1,n+1):
result = result * x
return result
if __name__ == '__main__':
print(expr(2,5))
这里,一直在对result变量赋值,但在函数式编程中的变量是具有不变性的,那么我们为了保持result的状态,就需要将result作为函数参数来传递以保持状态:
def expr(num,n):
if n==0:
return 1
return num*expr(num,n-1)
if __name__ == '__main__':
print(expr(2,5))
这就是递归!
8 函数式编程和递归
递归是函数式编程的一个重要的概念,循环可以没有,但是递归对于函数式编程却是不可或缺的。上面讲到状态如何解决引出递归,递归充分地发挥了函数的威力,也解决了函数式编程无状态的问题。
递归其实就是将大问题无限地分解,直到问题足够小。
而递归与循环在编程模型和思维模型上最大的区别则在于:
1. 循环是在描述我们该如何地去解决问题。
2. 递归是在描述这个问题的定义。
那么以斐波那契数列为例来看下这两种编程模型。
最常见的递归模型,不采用动态规划来做临时状态的缓存:
def Fib(a):
if a==0 or a==1:
return 1
else:
return Fib(a-2)+Fib(a-1)
递归是在描述什么是斐波那契数列,这个数列的定义就是一个数等于他的前两项的和,并且已知Fib(0)和Fib(1)等于1。而程序则是用计算机语言来把这个定义重新描述了一次。循环模型:
def Fib(n):
a=1
b=1
n = n - 1
while n>0:
temp=a
a=a+b
b=temp
n = n-1
return b
这里则是在描述我们该如何求解斐波那契数列,应该先怎么样再怎么样。代码上明显可以看到,递归相比于循环,具有着更加良好的可读性。
但递归可能产生StackOverflow,而赋值模型呢?
9 尾递归和伪递归
之前说到了递归和循环各自的问题,那怎么来解决这个问题,函数式编程抛出了答案:尾递归。什么是尾递归,用最通俗的话说:就是在最后一部单纯地去调用递归函数,这里我们要注意“单纯”这个字眼。
尾递归的原理,其实尾递归就是不要保持当前递归函数的状态,而把需要保持的东西全部用参数给传到下一个函数里,这样就可以自动清空本次调用的栈空间。这样的话,占用的栈空间就是常数阶的了。
在看尾递归代码之前,还是先来明确一下递归的分类,我们将递归分成“树形递归”和“尾递归”。什么是树形递归,就是把计算过程逐一展开,最后形成的是一棵树状的结构,比如之前的斐波那契数列的递归解法。
那么来看下斐波那契尾递归的写法:
def Fib(a,b,n):
if n==0:
return b
else:
return Fib(b,a+b,n-1)
如上分析,尾递归其实本质上就是个“伪递归”。既然我们可以优化,对于大多数的函数式编程语言的编译器来说,对尾递归同样提供了优化,使尾递归可以优化成循环迭代的形式,使其不会造成堆栈溢出的情况。
10 函数式编程总览
函数式编程的应用场合:1. 数学推理
2. 并行程序
函数式编程最适合地还是解决局部性的数学小问题,要让函数式编程来做CRUD,做传统的逻辑性很强的Web编程,就有些免为其难了。
文章大部分内容摘自 函数式编程扫盲篇