1. 概论
在过去的近十年的时间里,面向对象编程大行其道。以至于在大学的教育里,老师也只会教给我们两种编程模型,面向过程和面向对象。
孰不知,在面向对象产生之前,在面向对象思想产生之前,函数式编程已经有了数十年的历史。
那么,接下来,就让我们回顾这个古老又现代的编程模型,让我们看看究竟是什么魔力将这个概念,将这个古老的概念,在21世纪的今天再次拉入了我们的视野。
2. 什么是函数式编程
在维基百科中,已经对函数式编程有了很详细的介绍。
那我们就来摘取一下Wiki上对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.
简单地翻译一下,也就是说函数式编程是一种编程模型,他将计算机运算看做是数学中函数的计算,并且避免了状态以及变量的概念。
接下来,我们就来剖析下函数式编程的一些特征。
3. 从并发说开来
说来惭愧,我第一个真正接触到函数式编程,要追溯到两年以前的《Erlang程序设计》,我们知道Erlang是一个支持高并发,有着强大容错性的函数式编程语言。
因为时间太久了,而且一直没有过真正地应用,所以对Erlang也只是停留在一些感性认识上。在我眼里,Erlang对高并发的支持体现在两方面,第一,Erlang对轻量级进程的支持(请注意此处进程并不等于操作系统的进程,而只是Erlang内部的一个单位单元),第二,就是变量的不变性。
4. 变量的不变性
在《Erlang程序设计》一书中,对变量的不变性是这样说的,Erlang是目前唯一变量不变性的语言。具体的话我记不清了,我不知道是老爷子就是这么写的,还是译者的问题。我在给这本书写书评的时候吹毛求疵地说:
我对这句话有异议,切不说曾经的Lisp,再到如今的F#都对赋值操作另眼相看,低人一等。单说如今的Java和C#,提供的final和readonly一样可以支持变量的不变性,而这个唯一未免显得有点太孤傲了些。
让我们先来看两段程序,首先是我们常见的一种包含赋值的程序:
class Account:
def __init__(self,balance):
self.balance = balance
def desposit(self,amount):
self.balance = self.balance + amount
return self.balance
def despositTwice(self):
self.balance = self.balance * 2
return self.balance
if __name__ == '__main__':
account = Account(100)
print(account.desposit(10))
print(account.despositTwice())
这段程序本身是没有问题的,但是我们考虑这样一种情况,现在有多个进程在同时跑这一个程序,那么程序就会被先desposit 还是先 despositTwice所影响。
但是如果我们采用这样的方式:
def makeAccount(balance):
global desposit
global despositTwice
def desposit(amount):
result = balance + amount
return result
def despositTwice():
result = balance * 2
return result
def dispatch(method):
return eval(method)
return dispatch
if __name__ == '__main__':
handler = makeAccount(100)
print(handler('desposit')(10))
print(handler('despositTwice')())
这时我们就会发现,无论多少个进程在跑,因为我们本身没有赋值操作,所以都不会影响到我们的最终结果。
但是这样也像大家看到的一样,采用这样的方式没有办法保持状态。
这也就是我们在之前概念中看到的无状态性。
5. 再看函数式编程的崛起
既然已经看完了函数式编程的基本特征,那就让我们来想想数十年后函数式编程再次崛起的幕后原因。
一直以来,作为函数式编程代表的Lisp,还是Haskell,更多地都是在大学中,在实验室中应用,而很少真的应用到真实的生产环境。
先让我们再来回顾一下伟大的摩尔定律:
1、集成电路芯片上所集成的电路的数目,每隔18个月就翻一番。
2、微处理器的性能每隔18个月提高一倍,而价格下降一半。
3、用一个美元所能买到的电脑性能,每隔18个月翻两番。
一如摩尔的预测,整个信息产业就这样飞速地向前发展着,但是在近年,我们却可以发现摩尔定律逐渐地失效了,芯片上元件的尺寸是不可能无限地缩小的,这就意味着芯片上所能集成的电子元件的数量一定会在某个时刻达到一个极限。那么当技术达到这个极限时,我们又该如何适应日益增长的计算需求,电子元件厂商给出了答案,就是多核。
多核并行程序设计就这样被推到了前线,而命令式编程天生的缺陷却使并行编程模型变得非常复杂,无论是信号量,还是锁的概念,都使程序员不堪其重。
就这样,函数式编程终于在数十年后,终于走出实验室,来到了真实的生产环境中,无论是冷门的Haskell,Erlang,还是Scala,F#,都是函数式编程成功的典型。
6. 函数式编程的第一型
我们知道,对象是面向对象的第一型,那么函数式编程也是一样,函数是函数式编程的第一型。
我们在函数式编程中努力用函数来表达所有的概念,完成所有的操作。
在面向对象编程中,我们把对象传来传去,那在函数式编程中,我们要做的是把函数传来传去,而这个,说成术语,我们把他叫做高阶函数。
那我们就来看一个高阶函数的应用,熟悉js的同学应该对下面的代码很熟悉,让哦我们来写一个在电子电路中常用的滤波器的示例代码。
def Filt(arr,func):
result = []
for item in arr:
result.append(func(item))
return result
def MyFilter(ele):
if ele < 0 :
return 0
return ele
if __name__ == '__main__':
arr = [-5,3,5,11,-45,32]
print('%s' % (Filt(arr,MyFilter)))
哦,之前忘记了说,什么叫做高阶函数,我们给出定义:
在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:
那么,毫无疑问上面的滤波器,就是高阶函数的一种应用。
在函数式编程中,函数是基本单位,是第一型,他几乎被用作一切,包括最简单的计算,甚至连变量都被计算所取代。在函数式编程中,变量只是一个名称,而不是一个存储单元,这是函数式编程与传统的命令式编程最典型的不同之处。
让我们看看,变量只是一个名称,在上面的代码中,我们可以这样重写主函数:
if __name__ == '__main__':
arr = [-5,3,5,11,-45,32]
func = MyFilter
print('%s' % (Filt(arr,func)))
当然,我们还可以把程序更精简一些,利用函数式编程中的利器,map,filter和reduce :
if __name__ == '__main__':
arr = [-5,3,5,11,-45,32]
print('%s' % (map(lambda x : 0 if x0:
temp=a
a=a+b
b=temp
n = n-1
return b
这里则是在描述我们该如何求解斐波那契数列,应该先怎么样再怎么样。
而我们明显可以看到,递归相比于循环,具有着更加良好的可读性。
但是,我们也不能忽略,递归而产生的StackOverflow,而赋值模型呢?我们懂的,函数式编程不能赋值,那么怎么办?
11. 尾递归,伪递归
我们之前说到了递归和循环各自的问题,那怎么来解决这个问题,函数式编程为我们抛出了答案,尾递归。
什么是尾递归,用最通俗的话说:就是在最后一部单纯地去调用递归函数,这里我们要注意“单纯”这个字眼。
那么我们说下尾递归的原理,其实尾递归就是不要保持当前递归函数的状态,而把需要保持的东西全部用参数给传到下一个函数里,这样就可以自动清空本次调用的栈空间。这样的话,占用的栈空间就是常数阶的了。
在看尾递归代码之前,我们还是先来明确一下递归的分类,我们将递归分成“树形递归”和“尾递归”,什么是树形递归,就是把计算过程逐一展开,最后形成的是一棵树状的结构,比如之前的斐波那契数列的递归解法。
那么我们来看下斐波那契尾递归的写法:
def Fib(a,b,n):
if n==0:
return b
else:
return Fib(b,a+b,n-1)
这里看上去有些难以理解,我们来解释一下:传入的a和b分别是前两个数,那么每次我都推进一位,那么b就变成了第一个数,而a+b就变成的第二个数。
这就是尾递归。其实我们想一想,这不是在描述问题,而是在寻找一种问题的解决方案,和上面的循环有什么区别呢?我们来做一个从尾递归到循环的转换把!
最后返回b是把,那我就先声明了,b=0
要传入a是把,我也声明了,a=1
要计算到n==0是把,还是循环while n!=0
每一次都要做一个那样的计算是吧,我用临时变量交换一下。temp=b ; b=a+b;a=temp。
那么按照这个思路一步步转换下去,是不是就是我们在上面写的那段循环代码呢?
那么这个尾递归,其实本质上就是个“伪递归”,您说呢?
既然我们可以优化,对于大多数的函数式编程语言的编译器来说,他们对尾递归同样提供了优化,使尾递归可以优化成循环迭代的形式,使其不会造成堆栈溢出的情况。
12. 惰性求值与并行
第一次接触到惰性求值这个概念应该是在Haskell语言中,看一个最简单的惰性求值,我觉得也是最经典的例子:
在Haskell里,有个repeat关键字,他的作用是返回一个无限长的List,那么我们来看下:
take 10 (repeat 1)
就是这句代码,如果没有了惰性求值,我想这个进程一定会死在那里,可是结果却是很正常,返回了长度为10的List,List里的值都是1。这就是惰性求值的典型案例。
我们看这样一段简单的代码:
def getResult():
a = getA() //Take a long time
b = getB() //Take a long time
c = a + b
这段代码本身很简单,在命令式程序设计中,编译器(或解释器)会做的就是逐一解释代码,按顺序求出a和b的值,然后再求出c。
可是我们从并行的角度考虑,求a的值是不是可以和求b的值并行呢?也就是说,直到执行到a+b的时候我们编译器才意识到a和b直到现在才需要,那么我们双核处理器就自然去发挥去最大的功效去计算了呢!
这才是惰性求值的最大威力。
当然,惰性求值有着这样的优点也必然有着缺点,我记得我看过一个例子是最经典的:
def Test():
print('Please enter a number:')
a = raw_input()
可是这段代码如果惰性求值的话,第一句话就不见得会在第二句话之前执行了。
13. 函数式编程总览
我们看完了函数式编程的特点,我们想想函数式编程的应用场合。
1. 数学推理
2. 并行程序
那么我们总体地说,其实函数式编程最适合地还是解决局部性的数学小问题,要让函数式编程来做CRUD,来做我们传统的逻辑性很强的Web编程,就有些免为其难了。
就像如果要用Scala完全取代今天的Java的工作,我想恐怕效果会很糟糕。而让Scala来负责底层服务的编写,恐怕再合适不过了。
而在一种语言中融入多种语言范式,最典型的C#。在C# 3.0中引入Lambda表达式,在C# 4.0中引入声明式编程,我们某些人在嘲笑C#越来越臃肿的同时,却忽略了,这样的语法糖,带给我们的不仅仅是代码书写上的遍历,更重要的是编程思维的一种进步。
好吧,那就让我们忘记那些C#中Lambda背后的实现机制,在C#中,还是在那些更纯粹地支持函数式编程的语言中,尽情地去体验函数式编程带给我们的快乐把!
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |