[编程思想]面向逼格编程-从零开始的函数式编程(Functional Programming)

>面向逼格编程

    其实现在像Lisp和Haskell这种原本的大学实验室专属语言早已走出实验室投入生产环境,三年前发布的JDK8更是引入了Lambda表达式并加入了Function包以提供面向函数编程的基础。  

    可是如今大学校堂里老师们所教授的往往只有“面向过程”和“面向对象”,甚至对面向对象的很多东西也是点到为止,更别提在不专修Lisp或Haskell的情况下“函数式编程”在学生中的普及状态了。

    可以说不知道一点关于函数式编程,都不好出去吹;和朋友沟通交流时对[流、散列、折叠、映射]这些词更是陌生到没有共同语言。因此,为了程序员本身的高逼格需要和在编程时(读别人的代码时)能够更高效简洁的进行生产,对函数式编程还是需要进行一定了解的。

    转载请标明转载自身披白袍's博客:http://blog.csdn.net/shenpibaipao


>什么是函数式编程?

     在Wiki:https://en.wikipedia.org/wiki/Functional_programming中,这样对函数式编程进行了定义:

In computer science, functional programming is a programming paradigm—a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. 
     哇,这么一大段看着就烦。而且定义这种在理解上偏主观的东西其实在对函数式编程全无认知的时候,看起来抽象到不行。

所以,我们从一些实例讲起。


>函数式编程的特点

    不少人都归纳了函数式编程的一些特点:

  • 函数是第一公民;
  • 无副作用;
  • 状态不变;
  • 描述“做什么、是什么”而不是“如何做、为何是”;
  • 过程分解、函数叠加;
  • .....
    我挑了以上重点,一条一条来说,具体的实例代码就用大家都熟悉的OOP编程中的代表——Java来说明吧。


1 函数是第一公民

    这句话得记住了,其重要性就如同面试前端时对HR说的那句话一样重要:“JavaScript中万物皆对象”。高度概括,既有逼格,又留有给HR提问的余地。

    假如面试官顺着这句话接着问:“'函数是第一公民'这句话是什么意思呢?” 答曰:概括来说,函数可作为参数传入也可以作为返回值抛出。其实就这么简单。


    我们来看一个简单的例子:

    Java中的Arrays.sort方法大家都熟悉吧?我们现在对一个Integer数组进行排序,需要这么写:

Integer[] integers = {11,19,13};
Arrays.sort(integers, new Comparator<Integer>() {
	@Override
	public int compare(Integer o1, Integer o2) {
		return o1.compareTo(o2);
	}
});//身披白袍的博客:http://blog.csdn.net/shenpibaipao/article/details/78622498
    如果没用过Arrays.sort方法也没关系,其用法是这样的——Arrays.sort(需要排序的数组,排序的规则)。其中,排序的规则就是Comparator接口,该接口中只有一个抽象方法compare(T o1,T o2),当o1大于o2时返回正值,当o1等于o2时返回0,当o1小于o2时返回负值。至于为什么这样就能排序,我们暂时不去深究,只要知道这个规则就好了。上面重写的compare方法等价于——

@Override
public int compare(Integer o1, Integer o2) {
	if(o1>o2)return 1;
	else if(o1==o2)return 0;
	else return -1;
}//身披白袍的博客:http://blog.csdn.net/shenpibaipao/article/details/78622498
    有没有觉得其实Arrays.sort(arrays,comparator)中的comparator其实就只传入了一个函数,这个函数是这样的:


   由于Java中一个函数(方法)并不是一个对象,并不能通过引用直接传值,因此可以看到这里实际上就把compare单独包装成给一个接口类Comparetor,但本质上传递的就是个这个compare函数。


2 无副作用

    函数的副作用指的是,在其被调用的时候,除了给出返回值以外,还修改了函数外部的状态。比如下面这个代码:

class Test{
	private static int sum=0;
	public void addSum(){
		Test.sum++;//改变外部状态
		System.out.println(Test.sum);//未改变外部状态
	}
}//身披白袍的博客:http://blog.csdn.net/shenpibaipao/article/details/78622498
    严格来说,函数的无副作用是对函数式编程的一种 要求,而不是单纯的一种特色。这个要求产生的原因有很多,其中一个比较重要的原因是:往往函数式编程会用到较多的递归,那么如果某个递归改变了外部状态,我们很难追溯到事件源。

    函数式编程与外界的交互只有参数和返回值,而且就算是传参也要尽量进行Currying过程,这个暂时挂起不表。总之在函数式编程中,我们要尽量避免函数副作用的发生。之所以说是“尽量避免”,那是因为完全的避免是很不切实际的,在生产过程中总有相对的需求的。在Clojure(一种运行在JVM上的Lisp)为了代码的灵活,都没有完全限死副作用的产生。


3.1 状态不变

    状态不变在某种意义上其实也是无副作用思想的一种延伸。我们来看下面这个幂运算函数:

public int pow(int a,int b){
	int sum = 1;
	while (b-- >0){
		sum*=a;
	}
	return sum;
}//身披白袍的博客:http://blog.csdn.net/shenpibaipao/article/details/78622498
   sum的值不断发生变化,我们在一直改变这个状态。
   之所以强调状态不变,是因为在函数式编程中有个概念就是映射。已知映射过程:x->f(x),那么这时候只给了一个状态x,返回了一个经过映射的状态f(x),这期间x是完全没有经过任何变化,且映射是稳定的,没有经过第二种状态的。

   但上面的幂运算函数的映射是什么样的呢?(a,b)->sum->f(a,b)。看起来似乎一点毛病都没有,最后返回的还是f(a,b)啊。但你有没有想过这么一种情况,sum这个变量的地址被另外一个进程读取并改写了地址指向的值。比如我求pow(3,4),运算到3^2的时候,sum=9,但有个暴力用户直接通过写内存把sum写成了1,于是最后的结果就变成了pow(3,4)=9。

    我这里当做例子的这个幂运算函数的逻辑很简单,可能会让人对这种情况不以为然;但在系统复杂度很高、函数逻辑复杂的时候,在进行函数式编程时你不能否认会有这种情况的出现。那么此时这个映射就是不稳定的,原因就在于我们进行了对sum的赋值操作,打断了从x到f(x)的直接映射。


    那么我们应该怎么做呢?

    函数式编程的一个思想就是传递状态,而不去修改状态。3^4可以看做是3*3*3*3的一个过程。我每次向下一个函数传递我上一个函数的状态,就可以保证状态不变和映射的稳定。所以上面那个求幂函数应当这么改写:

public int pow2(int a,int b){
	if(b==0)return 1;
	return a*pow2(a,b-1);
}//身披白袍的博客:http://blog.csdn.net/shenpibaipao/article/details/78622498
    在这个函数中,我们只传递了状态(幂次b),而没有任何一个赋值语句去改变任何一个状态。

    肯定有人要说,这特么不就是递归么?是的,就是递归。函数式编程的思想某种意义上就是函数的叠加,体现到代码上,其表现形式就是递归。

    既然提到了递归,我们就顺便来说说尾递归。


3.2 尾递归

    递归虽然在代码上非常直观,但有个坏处就是StackOverFlow,在工程中是一个极大的隐患。为了克服这一个缺点,在编程语言中,或者说编译器中都提供了对尾递归的优化。

    以java对尾递归的优化为例,对上面的求幂函数pow2经过“尾递归”改写,将变成以下函数.

private int pow3(int sum,int a,int b){
	return b==0?sum:pow3(sum*a, a,b-1);//我们修改了这里,不再进行a * pow(a,b-1)的操作
}
//对外的调用接口powP
public int powP(int a,int b){
	return pow3(1,a,b);
}//身披白袍的博客:http://blog.csdn.net/shenpibaipao/article/details/78622498

    对尾递归的优化,实际上是由编译器和程序员共同完成的。递归之所以占用栈空间,是因为每一次函数入栈,在栈上的该层里,这个函数总留有一些要处理的操作,要等待下一层的返回值返回后才能进行操作,于是一层压一层,栈被逐渐堆满。比如在pow2中,我们总要等待pow2(a,b-1)的返回值,才能进行a*pow2(a,b-1)的操作。

    我们现在返回来看pow3,我们通过pow3(int sum, int a, int b )把所有状态全部传给下一层,本层内不再有任何待命操作。于是编译器就可以直接把入栈的本层函数直接出栈,再在本层入栈下一个的递归的函数,从而不再可能出现StackOverFlow的情况。

    之所以说“对尾递归的优化是由程序员和编译器共同完成的”,是因为尾递归其实就是一种递归的特殊写法,如果没有编译器进行编译优化(比如java中会优化为一个类似while的循环)或语言对内存管理的支持,你程序员的代码写得再好,该爆栈的还是会爆栈,这一点需要注意。


4 描述“做什么、是什么”而不是“如何做、为何是”

    我们以函数式编程如何处理斐波那契数列为例:

//数列序数从1开始,不考虑n<=0的情况
//如 f(1)=1 f(2)=1 f(3)=2 f(4)=3 f(5)=5
public int fin(int n){
	if(n<=2) return 1;
	return fin(n-2)+fin(n-1);
}//身披白袍的博客:http://blog.csdn.net/shenpibaipao/article/details/78622498
    可以看到,在求斐波那契数列的函数fin中,我们做了这样的映射:



    实际上我们其实是对斐波那契数列做了一次定义,也就是“是什么”,而不是告诉计算机该如何去求。为了分清这个概念,我们来说说如果是告诉计算机“如何做”,那么这个函数应该怎么写:(其实这里是一种面对过程的编程思想)

public int fin2(int n){
	if(n<=2)return 1;
	else{
		int n_2 = 1;
		int n_1 = 1;
		int nNow = n_2+n_1;
		while(n-- > 2){
			nNow = n_2+n_1;
			n_2 = n_1;
			n_1 = nNow;
		}
		return nNow;
	}
}//身披白袍的博客:http://blog.csdn.net/shenpibaipao/article/details/78622498
    上面这段代码描述了求取斐波那契数列的过程,它告诉了计算机该“如何去做”。看着就很麻烦,也很冗余。这里也体现出函数式编程的一大优点,对于数学模型能够用很简短的代码去把它表示出来。


5 函数分解和函数叠加

    对于一个函数,它可以被分解成数个函数的叠加态。换句话说就是,一个高阶函数是由数个低阶函数拼组而成的。

    其实这也并不难理解,例如这个式子:(3+5)*8=64


    我们通过两个低阶函数f和g完成了g·f,这就意味着系统中只需要存在g和f这两个函数就可以完成任何g·f··g·f·f·.....的高阶叠加,最大限度地保证了系统的扩展性。毕竟我们可能不止需要计算(3+5)*8,还需要计算(3+5)*(3+5)+3。我们不可能为每一个高阶函数都定义一个专门的函数去处理这些表达式。

    上文中提到了Currying过程,关于这个过程的定义可以到这里查看:https://en.wikipedia.org/wiki/Currying,简单来说,Currying致力于削减一个函数的参数个数,这也就迫使程序员尽量保证将一个复杂的过程化简成一个又一个简单的过程,然后通过系统中这些原子化的操作去复合为不同的高阶过程。

    这一思想在Java中的Lambda表示式和Function包中得到了很好的体现,我会在>其他文章<里介绍Lambda表达式。


>尾言

    其实我们大多数人在平常编程时早已在不经意间用到了函数式编程的实现。无论是面对过程编程、面对对象编程还是函数式编程,其实都不是对立的,相互融合也是常有的事。如何把握好函数式编程的核心要义,以体现其恪守的映射理念,才是这种编程思想的难点。

   入门就到这里了,要想更深入地应用函数式编程,需要结合特定的语言进行理解,比如Java的函数式接口。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值