浅谈函数式编程

函数式编程(Functional programming)是一种编程范型,它将电脑运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。函数编程语言最重要的基础是 λ演算(lambda calculus)。而且λ演算的函数可以接受函数当作输入(引数)和输出(传出值)。

比起命令式编程,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。

声明式编程与命令式编程

命令式编程(Imperative programming)的意思就是,我们通过编写一条又一条指令去让计算机执行一些动作,这其中一般都会涉及到很多繁杂的细节。

而声明式(Declarative programming)就要优雅很多了,我们通过写表达式的方式来声明我们想干什么,而不是通过一步一步的指示。

//命令式
var CEOs = [];
for(var i = 0; i < companies.length; i++){
    CEOs.push(companies[i].CEO)
}

//声明式
var CEOs = companies.map(c => c.CEO);

命令式的写法要先实例化一个数组,然后再对 companies 数组进行for循环遍历,手动命名、判断、增加计数器,就好像你开了一辆零件全部暴露在外的汽车一样,虽然很机械朋克风,但这并不是优雅的程序员应该做的。常见的面向对象编程是也是一种命令式编程

声明式的写法是一个表达式,如何进行计数器迭代,返回的数组如何收集,这些细节都隐藏了起来。它指明的是做什么,而不是怎么做。除了更加清晰和简洁之外,map 函数还可以进一步独立优化,甚至用解释器内置的速度极快的 map 函数,这么一来我们主要的业务代码就无须改动了。

函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可。

相反,不纯的非函数式的代码会产生副作用或者依赖外部系统环境,使用它们的时候总是要考虑这些不干净的副作用。在复杂的系统中,这对于程序员的心智来说是极大的负担。

纯函数

函数式编程的核心就是借助形式化数学来描述逻辑:lambda 运算。数学家们喜欢将程序描述为数据的变换,这也引入了第一个概念:纯函数。纯函数在这里指函数内外间是“无”关联的,主要有两个特点:

  • 没有副作用(No Side Effect),不会涉及到外部变量的使用或修改

  • 引用透明(Referential transparency),函数内只会依赖传入参数,在任何时候对函数输入相同的参数时,总能输出相同的结果

例如:

// 纯函数
const add10 = (a) => a + 10
// 依赖于外部变量的非纯函数
let x = 10
const addx = (a) => a + x
// 会产生副作用的非纯函数
const setx = (v) => x = v

非纯函数间接地依赖于参数 x。如果你改变了 x 的值,对于相同的 x,addx 会输出不同的结果。这就使得在编译时很难去静态分析和优化程序。不过对 JavaScript 开发者来说更加有用的是,纯函数降低了程序的认知难度。写纯函数时,你仅仅需要关注函数体本身。不必去担心一些外部因素所带来的问题,比如在 addx 函数中的 x 被改变。

不可变数据(immutable)

这里主要是指变量值的不可变。当需要基于原变量值改变时,可通过产生新的变量来确保原变量的不变性,如下

// 可变数据
var arr = ["Functional", "Programming"];
arr[0] = "Other"; // <= 修改了arr[0]的值
console.log(arr)  // => ["Other", "Programming"] // 变量arr值已经被修改


// 不可变数据
var arr = ["Functional", "Programming"];
// 得到新的变量,不修改了原来的值
var newArr = arr.map(item => {
    if(item === "Functional"){
        return "Other"; 
    } else {
        return item;
    }
})
console.log(arr);  // => ["Functional", "Programming"] 变量arr值不变
console.log(newArr); // => ["Other", "Programming"]  产生新的变量newArr

之所以使用这种不变值,除了更好的函数式编程外,还能够维持线程安全可靠,落地在业务中,实际上也能让代码更加清晰。
设想,如果你定义了一个变量 A,A 在其他地方被其他人修改了,这样是不方便定位A的当前值的。关于定义多个变量引发的内存等问题,可以通过重用结构或部分引用的方式来减轻,可参考 immutable.js

函数柯里化(curry)

函数柯里化的本质是,可以在调用一个函数的时候传入更少的参数,而这个函数会返回另外一个函数并且能够接收其他参数。

下面的示例中,我们创建了一个柯里化函数 add,接收两个参数。当我们传递一个参数时,会得到一个中间函数 add1,它仅仅会接收一个参数。

const add = R.curry((a, b) => a + b)
add(1, 2) // => 3
const add1 = add(1)
add1(2) // => 3
add1(10) // => 11

函数组合(compose)

const formalGreeting = (name) => `Hello ${name}`
const casualGreeting = (name) => `Sup ${name}`
const male = (name) => `Mr. ${name}`
const female = (name) => `Mrs. ${name}`
const doctor = (name) => `Dr. ${name}`
const phd = (name) => `${name} PhD`
const md = (name) => `${name} M.D.`
formalGreeting(male(phd("Chet"))) // => "Hello Mr. Chet PhD"

每个函数仅完成了一个简单的事情,我们很容易就可以将它们组合在一起。

使用 map, reduce 等数据处理函数

强大的 JavaScript 有着越来越多的高能处理数据函数,其中包含了 map、 reduce、 filter 等。

map 能够对原数组中的值进行逐个处理并产生新的数组,一个简单例子

// map
var data = [1, 2, 3];
var squares = data.map( (item, index, array) =>  item * item );
console.log(squares); // => [1, 4, 9]
console.log(data);// =>  [1, 2, 3] data 还是那个 data 

reduce 能够对原数组中的各个值进行结合处理,来产生新的值,如下面例子中,previous 代表上一个结果值,current 代表当前值,reduce 函数可以传入第二个参数作为 previous 初始值,不传时则 previous 初始值为数组中第一个值

// reduce
var sum = [1, 2, 3].reduce( (previous, current, index, array) => previous + current );
console.log(sum); // => 6

使用 Java 8 lambda表达式的 map 和 reduce

List<Integer> costBeforeTax = Arrays.asList(100, 200, 300, 400, 500);
       costBeforeTax.stream()
               .map((cost) -> cost + .12 * cost)
               .forEach(System.out::println);

RxJava 中使用 map 转换数据格式

Observable.just("images/logo.png") // 输入类型 String
    .map(new Func1<String, Bitmap>() {
        @Override
        public Bitmap call(String filePath) { // 参数类型 String
            return getBitmapFromPath(filePath); // 返回类型 Bitmap
        }
    })
    .subscribe(new Action1<Bitmap>() {
        @Override
        public void call(Bitmap bitmap) { // 参数类型 Bitmap
            showBitmap(bitmap);
        }
    });

ReactiveX (Reactive Extensions) 一般简写为 Rx,
ReactiveX 是一个使用可观察数据流进行异步编程的编程接口,结合了观察者模式和函数式编程的精华。

函数式编程的好处

由于命令式编程语言也可以通过类似函数指针的方式来实现高阶函数,函数式的最主要的好处主要是不可变性带来的。没有可变的状态,函数就是引用透明(Referential transparency)的和没有副作用(No Side Effect)。

一个好处是,函数即不依赖外部的状态也不修改外部的状态,函数调用的结果不依赖调用的时间和位置,只要给定输入参数,返回的结果必定相同,这样写的代码容易进行推理,不容易出错。这使得单元测试和调试都更容易。

另一个好处是:由于(多个线程之间)不共享状态,不会造成资源争用(Race condition),也就不需要用锁来保护可变状态,也就不会出现死锁,这样可以更好地并发起来,尤其是在对称多处理器(SMP)架构下能够更好地利用多个处理器(核)提供的并行处理能力。

参考资料:

mostly-adequate-guide
谈谈函数式编程
给 JavaScript 开发者讲讲函数式编程
JavaScript函数式编程(一)
函数编程语言
什么是函数式编程思维?
函数式编程初探