阅读 1740

从函数式编程说起

前言

“好事者”总结了一个份关于Android开源项目的榜单,榜单里面包含了Android开发中常用的开源库,排在第一的是网络封装框架Retrofit,而RxJava(RxAndroid)则排名十三。榜单是以使用优先级来评判的,Android开发中必不可少的模块排名会高一些(使用多而选择少,所以人人都会去用),例如网络加载框架(Retrofit+okhttp),图片加载框架(glide)等。

但是如果让我选择一个对程序开发模式或者整个App架构影响最大的开源库,我会毫不犹豫的选择RxJava;如果把开发App比喻成做饭,Retrofit或者okhttp充其量是葱、蒜、姜,而RxJava则是主材;葱、蒜、姜能够影响菜的口味,但是最终做出来菜是怎样则是由主材决定的。

榜单中把RxJava的关键字定义成“异步”,这是错误或者说是肤浅的。RxJava本质是一种函数式编程的实现框架,它从根本上改变了Android开发过程中的思维方式,下面来看看什么是函数式编程。

函数式编程是什么?

要厘清函数式编程,我们必须先明确几个概念的区别:

  • 面向对象编程\面向过程编程(编程思想)
  • 函数式编程\指令式编程(编程范式)

前者是编程思想,后者属于编程范式,一般来说前者包含后者。首先需要明确一点:这四个概念不是对立的,他们之间都是互相关联和重叠的;编程思想并不是非此即彼,面向对象也有可能会用到面向过程的思想,面向过程也有可能会用到面向对象的思想。函数式编程和指令式编程也一样,区别在于是否是大部分的思维方式契合某种编程方式。

函数式编程很早的时候就已经出现了,像古老的Lisp语言就是函数式编程的典范,最近的火热的kotlin语言也属于函数式编程语言,看一个kotlin的函数编程例子:

 /**
  * 找出list中某些单词的使用频率,从高到底输出
  */
private fun test(list : MutableList<String>):TreeMap<String, Int>{
        val words = mutableSetOf("the","abd","to")
        val wordMap = TreeMap<String, Int>()
        list.stream()
                .map { w->w.toLowerCase() }
                .filter { w->!words.contains(w) }
                .forEach{w->wordMap.put(w,wordMap.getOrDefault(w,0)+1}
        return wordMap;
    }

复制代码

相比于传统的Java语言编程方式,函数式编程更加注重“算法”,回忆大学学习算法课程的时候,老师教学过程中的往往是使用伪代码,比如找出A B C三个数中最大值。

Begin(算法开始)
输入 A,B,C
IF A>B 则 A→Max
否则 B→Max
IF C>Max 则 C→Max
Print Max
End (算法结束)
复制代码

而函数式编程语言可能相比更加精炼一些,

Begin(算法开始)
输入 A,B,C
max -> Max(A,B)
max->Max(max,c)
Print max
End (算法结束)

复制代码

函数式编程语言更加关注函数,函数在编程过程中是“一等公民”,上面例子中,就像伪代码一样,代码把整个问题域分解成粒度合适的小问题,通过解决一个个小问题,从而实现整个问题的求解,a->fun1->fun2->b。

在指令式编程过程中我们需要对每一个小问题进行代码封装求解,更加关注“问题域”的解决思路,忽略实现的细节。面向对象程序设计其实也是包含这种思想,花更多的时间关注更高层次问题的抽象,考虑解决复杂的业务场景。

在《函数式编程思维》书上有一句原文:“假如语言不对外暴露那么多出错的可能性,那么开发者就不那么容易犯错”,对于这句话我的理解是:

  • 尽可能的对基础函数做封装,比如对基本数据结构的操作,让调用者尽可能的具有确定性的输入和输出,比如上面例子中对集合的操作map、filter等等;
  • 尽可能让可变的东西变成不可变,例如变量、多线程、集合等等;

前面这两个结论就隐含着函数式编程语言中巨大的设计思路,函数式编程中函数指的是数学概念上的函数f(x),函数的值仅决定于函数参数的值,不依赖其他状态,在纯粹的函数式编程语言中必须遵从两个规范:

  • 不可变性
  • 对基础函数(高阶)封装

为这两条就是函数式编程语言的本质特征。

函数式编程语言的不可变性

说到函数式编程的不可变特性,我们必须先说一说指令式编程的“可变性”,在传统的Java语言中,我们时刻需要保存一些计算“状态”信息,这些可能是一些具体的数据、引用指向等等信息,比如我们要求解0-10的和。

public void sum(){
    int sum = 0;
    for(int i=0;i<10;i++){
        sum += i;
    }
    return sum;
}
复制代码

需要不断对sum赋值,sum这个变量一直在不断的被重用,因此我们说sum是可变的。

这样会带来什么后果?假设sum这个值是一个对象成员变量,如果该对象被多个线程调用,sum值就会处于一种线程不安全的状态,假设sum一个引用类型的变量,经过多个函数的调用引用类型会不断被赋值,最终会导致输出的结果是不可预见的,总结不可变带来的一些问题:

  • 线程不安全
  • 应用不透明
  • 资源被竞争
  • 可测性下降

那在Java中有没有不可变特性的体现呢?String类就是最明显的不可变的实现,String类的实现具体以下几个特性:

  • 声明成 final,包括某些关键的成员变量、类的声明等等
  • 无参数的构造函数

String类的实现本质就是为了“安全的赋值”,当我们定义一个字符串,并且重新赋值,它并非是在同一个内存引用上修改数据,而是重新生成一个新的引用

String a= "1122";
a = "3333";
复制代码

image

那么在函数式编程语言中是怎么实现的“不变性”的呢,一般从以下几个方面进行考虑:

  • 语法层面进行约束和规范。例如kotlin的val不可变变量,可变集合和不可变集合的规范等等;
  • 利用函数进行参数的传递。函数的输入固定,输出也是固定的,所以就保证了函数式参数的不变性;
  • 善用递归等语言特性;
  • 使用高阶函数;

上面的求和例子,在kotlin函数式语言中,实现的方式:

fun sum() : Double{
        val a = 9 // val的不可变量
        // 高阶函数进行数据求和
        return Array<Int>(a,{ w->w+1}).sumByDouble({w->w.toDouble()})
    }
复制代码

可以看到,kotlin贯彻执行了函数式语言的不可变特性,通过高阶函数来对整个过程中产生的临时变量全部转换成函数参数,类似于f(g(x))。

函数式编程语言之所以会流行起来很大原因是因为“不变性”,随着摩尔定律在计算机上面的失效,导致计算机的性能提升更多依赖于多核计算,而多核计算的最大问题在于“并发”的处理,函数式语言通过避免赋值产生的“不变性”恰恰契合了多核计算机的发展。

lambda表达式和高阶函数

lambda表达式在函数式编程语言中往往体现在“匿名函数”上。当你需要一个函数,但是你又不想要给这个一次性函数取一个“不需要”的名字,这个时候匿名函数就可以派上用场了。在Android中匿名函数随处可见,例如设置一个监听:

 mCompleteBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                doSomeThink(v)
            }
        });

复制代码

在Java中这种对匿名函数的写法是极其不优雅和麻烦的,通过lambda表达式去简化这段代码,只保留参数和对应的方法实现:

 mCompleteBtn.setOnClickListener({v->doSomeThink(v)})
复制代码

我们把

{v->doSomeThink(v)}
复制代码

这段代码块称之为lambda表达式,很明显lambda表达式最大的作用就是:

==让代码更简洁==

在多个参数的时候lambda表达式的写法类似

(x,y)->x^2+y^2
复制代码

高阶函数其实就是利用lambda表达式作为参数或者参数返回值的函数,例如:

 val items = listOf(1, 2, 3, 4, 5)
 items.map ({t->t*2})
复制代码

其中map就是高阶函数,

 public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> {
    return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform)
}
复制代码

当然任何高阶函数本质上都是指令式编程的封装,函数式编程会对一些常用的数据结构和集合(set map)做一些函数式封装。

总结

编程范式理论上是没有所谓的好坏之分,只有合适与否。某种意义上来说,函数式编程和指令式编程在底层底层实现并没有本质上的区别,只不过函数式编程通过一系列的规范来保证编程过程中尽可能的遵循“不可变性”,通过对基本数据结构和集合提供足够多的高阶函数,从而让编程者更加关注解决问题的步骤而非具体的实现。

参考文章

  • https://www.ibm.com/developerworks/cn/java/j-ft20/index.html
  • http://www.ruanyifeng.com/blog/2012/04/functional_programming.html