函数式编程扫盲篇(转载)

1. 概论html

在过去的近十年的时间里,面向对象编程大行其道。以致于在大学的教育里,老师也只会教给咱们两种编程模型,面向过程和面向对象。程序员

孰不知,在面向对象产生以前,在面向对象思想产生以前,函数式编程已经有了数十年的历史。算法

那么,接下来,就让咱们回顾这个古老又现代的编程模型,让咱们看看到底是什么魔力将这个概念,将这个古老的概念,在21世纪的今天再次拉入了咱们的视野。编程

2. 什么是函数式编程缓存

维基百科中,已经对函数式编程有了很详细的介绍。并发

那咱们就来摘取一下Wiki上对Functional Programming的定义:app

In computer sciencefunctional 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,更多地都是在大学中,在实验室中应用,而不多真的应用到真实的生产环境。

先让咱们再来回顾一下伟大的摩尔定律:

一、集成电路芯片上所集成的电路的数目,每隔18个月就翻一番。

二、微处理器的性能每隔18个月提升一倍,而价格降低一半。

三、用一个美圆所能买到的电脑性能,每隔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 x<0 else x ,arr)))

 

这样看上去是否是更赏心悦目呢?

这样咱们就看到了,函数是咱们编程的基本单位。

 

7. 函数式编程的数学本质

忘了是谁说过:一切问题,归根结底到最后都是数学问题。

编程历来都不是难事儿,无非是细心,加上一些函数类库的熟悉程度,加上经验的堆积,而真正困难的,是如何把一个实际问题,转换成一个数学模型。这也是为何微软,Google之类的公司重视算法,这也是为何数学建模大赛在大学计算机系如此被看重的缘由。

先假设咱们已经凭借咱们良好的数学思惟和逻辑思惟创建好了数学模型,那么接下来要作的是如何把数学语言来表达成计算机能看懂的程序语言。

这里咱们再看在第四节中,咱们提到的赋值模型,同一个函数,同一个参数,却会在不一样的场景下计算出不一样的结果,这是在数学函数中彻底不可能出现的状况,f(x) = y ,那么这个函数不管在什么场景下,都会获得一样的结果,这个咱们称之为函数的肯定性。

这也是赋值模型与数学模型的不兼容之处。而函数式编程取消了赋值模型,则使数学模型与编程模型完美地达成了统一

 

8. 函数式编程的抽象本质

相信每一个程序员都对抽象这个概念不陌生。

在面向对象编程中,咱们说,类是现实事物的一种抽象表示。那么抽象的最大做用在我看来就在于抽象事物的重用性,一个事物越具体,那么他的可重用性就越低,所以,咱们再打造可重用性代码,类,类库时,其实在作的本质工做就在于提升代码的抽象性。而再往大了说开来,程序员作的工做,就是把一系列过程抽象开来,反映成一个通用过程,而后用代码表示出来。

在面向对象中,咱们把事物抽象。而在函数式编程中,咱们则是在将函数方法抽象,第六节的滤波器已经让咱们知道,函数同样是可重用,可置换的抽象单位。

那么咱们说函数式编程的抽象本质则是将函数也做为一个抽象单位,而反映成代码形式,则是高阶函数

 

9.状态到底怎么办

咱们说了一大堆函数式编程的特色,可是咱们忽略了,这些都是在理想的层面,咱们回头想一想第四节的变量不变性,确实,咱们说,函数式编程是无状态的,但是在咱们现实状况中,状态不可能一直保持不变,而状态必然须要改变,传递,那么咱们在函数式编程中的则是将其保存在函数的参数中,做为函数的附属品来传递。

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))

呦,这不是递归么!

 

10. 函数式编程和递归

递归是函数式编程的一个重要的概念,循环能够没有,可是递归对于函数式编程倒是不可或缺的。

在这里,我得认可,我确实不知道我该怎么解释递归为何对函数式编程那么重要。我能想到的只是递归充分地发挥了函数的威力,也解决了函数式编程无状态的问题。(若是你们有其余的意见,请赐教)

递归其实就是将大问题无限地分解,直到问题足够小。

而递归与循环在编程模型和思惟模型上最大的区别则在于:

循环是在描述咱们该如何地去解决问题。

递归是在描述这个问题的定义。

那么就让咱们以斐波那契数列为例来看下这两种编程模型。

先说咱们最多见的递归模型,这里,我不采用动态规划来作临时状态的缓存,只是说这种思路:

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,而赋值模型呢?咱们懂的,函数式编程不能赋值,那么怎么办?

 

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#中,仍是在那些更纯粹地支持函数式编程的语言中,尽情地去体验函数式编程带给咱们的快乐把!

原文地址:http://www.cnblogs.com/kym/archive/2011/03/07/1976519.html

相关文章
相关标签/搜索