月影谈为什么我们要用函数式编程
Posted 奇舞周刊
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了月影谈为什么我们要用函数式编程相关的知识,希望对你有一定的参考价值。
关注奇舞周刊,由360前端团队奇舞团带你领略前端前沿技术
这可能是最简单易懂的函数式编程介(扯)绍(淡)了
目前前端界(以及其他一些领域)对函数式编程大体上两种态度,一些人是觉得函数式编程特牛逼,尤其是现在许多新生的框架和库都在标榜自己的函数式特征。而另一些人,又觉得函数式编程学起来很难,而且似乎也没有什么卵用,理由是在自己经历的项目里面很难看到具体的函数式编程应用场景,甚至其中许多人认同一个观点,觉得函数式编程只适合于学术研究,很难在工程项目中实际使用。
不管你在阅读本文之前属于哪一种人,又或者你是刚接触函数式编程的新人,都没有关系。本文不是研究函数式编程范式的学术研究,而函数式编程作为一个可以说是程序设计理论中最古老的编程范式,在它几十年上百年的发展历史中,已经积累了大量的资料和素材,对于想要在学术领域里完全弄明白它的同学,完全可以在网上、书店里找到各种资料。本文的重点不在于概念,而在于实战。因此,你不会听到太多各种函数式编程的名词讨论,比如诸如 Curry、Mond 之类的专业术语。相反,我们主要来讨论函数式编程在前端领域内使用的一些实际例子,了解为什么前端需要学习函数式编程,使用函数式编程写代码能给我们带来什么。如果弄明白了这些,那么关于函数式编程不实用的谣言也就不攻自破了。
数据抽象或过程抽象
为什么我们接受面向过程或面向对象思想很容易,而我们要完全接受函数式编程却感觉难得多?
我认为这个问题大体上可以这么解释:
人脑本能地容易理解“看得见“、“摸得着”的物体,对于“运动”和“变化”一类不着形的东西,人脑理解起来要略微地费劲一些。而人类要做好一件复杂的事情,大脑有两种抽象方向,一种是对实体进行抽象,另一种是对过程进行抽象:
简答来说,即在软件设计的过程中,如果要保证软件产品的功能稳定可用,同时要保证它的灵活性和可扩展性,那么系统就要有变化的部分和不变的部分。哪些部分应当设计成“不变”,哪些部分应当设计成“可变”,在这个取舍过程中,FP(函数式编程)和 OOP(面向对象编程)正是走了两条不同的路线。
面向对象对数据进行抽象,将行为以对象方法的方式封装到数据实体内部,从而降低系统的耦合度。而函数式编程,选择对过程进行抽象,将数据以输入输出流的方式封装进过程内部,从而也降低系统的耦合度。两者虽是截然不同,然而在系统设计的目标上可以说是殊途同归的。
面向对象思想和函数式编程思想也是不矛盾的,因为一个庞大的系统,可能既要对数据进行抽象,又要对过程进行抽象,或者一个局部适合进行数据抽象,另一个局部适合进行过程抽象,这都是可能的。数据抽象不一定以对象实体为形式,同样过程抽象也不是说形式上必然是 functional 的,比如流式对象(InputStream、OutputStream)、Express 的 middleware,就带有明显的过程抽象的特征。但是在通常情况下,OOP更适合用来做数据抽象,FP更适合用来做过程抽象。
纯函数
再具体深入下去之前,我们先来解答一个问题,那就是为什么用 FP 或过程抽象能够降低系统的耦合度。这里我们要先理解一个概念,这个概念叫“纯函数”。
根据定义,如果一个函数符合两个条件,它被称为纯函数:
此函数在相同的输入值时,总是产生相同的输出。函数的输出和当前运行环境的上下文状态无关。
此函数运行过程不影响运行环境,比如不会触发事件、更改环境中的对象、终端输出值等。
简单来说,也就是当一个函数的输出不受外部环境影响,同时也不影响外部环境时,该函数就是纯函数。
javascript 内置函数中有不少纯函数,也有不少非纯函数。
比如以下函数是纯函数:
String.prototype.toUpperCase
Array.prototype.map
Function.prototype.bind
以下函数不是纯函数:
Math.random
Date.now
document.body.appendChild
Array.prototype.sort
为什么要区分纯函数和非纯函数呢?因为在系统里,纯函数与非纯函数相比,在可测试性、可维护性、可移植性、并行计算和可扩展性方面都有着巨大的优势。
在这里我用可测试性来举例:
对于纯函数,因为是无状态的,测试的时候不需要构建运行时环境,也不需要用特定的顺序进行测试:
test(t => {
t.is(add(10, 20), 30); //add(x,y) 是个纯函数,不需要为它构建测试环境
...
});
对于非纯函数,就比较复杂:
test.before(t => {
let list = document.createElement('ul');
list.id = 'xxxxxx';
...
});
test(t => {
let list = document.getElementById('xxxxxx');
t.is(sortList(list).innerhtml, `<ul>
...
</ul>`);
});
test.after(t => {
...
document.removeChild(list);
});
函数式编程能够减少系统中的非纯函数
首先我们看一个例子:
//two impure functions
function setColor(el, color){
el.style.color = color;
}
function setColors(els, color){
els.forEach(el => setColor(el, color));
}
let items1 = document.querySelectorAll('ul > li:nth-child(2n + 1)');
let items2 = document.querySelectorAll('ul > li:nth-child(3n + 1)');
setColors(items2, 'green');
setColors(items1, 'red');
在这里我们有两个彼此依赖的非纯函数,setColor(el, color) 和 setColors(els, color)。在测试的时候,我们需要构建环境来测试两个函数。
现在,我们用函数式编程思想来改造这个系统:
//only one impure function
function batch(fn){
return function(target, ...args){
if(target.length >= 0){
return Array.from(target).map(item => fn.apply(this, [item, ...args]));
}else{
return fn.apply(this, [target, ...args]);
}
}
}
function setColor(el, color){
el.style.color = color;
}
let setColors = batch(setColor);
let items1 = document.querySelectorAll('ul > li:nth-child(2n + 1)');
let items2 = document.querySelectorAll('ul > li:nth-child(3n + 1)');
setColors(items2, 'green');
setColors(items1, 'red');
在这里,我们建立一个过程抽象的高阶函数 batch(fn),这个函数的作用是,对它的输入函数返回一个新的函数,这个函数与输入函数的区别是,如果调用的第一个实参是一个数组,那么将这个数组展开,用每一个值依次调用输入函数,返回一个数组,包活每次调用返回的结果。
batch(fn) 本身虽然看似复杂,但是有意思的事,这个函数无疑是纯函数,所以 batch(fn) 自身的测试是非常简单的:
test(t => {
let add = (x, y) => x + y;
let listAdd = batch(add);
t.deepEqual(listAdd([1,2,3], 1), [2,3,4]);
});
由于我们上面举的例子 setColor 和 setColors 虽然不是纯函数,但是却非常简单,因此似乎设计 batch(fn) 的意义不大,有把系统变得更复杂的嫌疑。然而,对于有许多操作 DOM 的函数的框架或库,有了 batch(fn),我们就可以实现很简单的接口(对单一元素操作),然后利用 batch(fn) 获得更复杂接口(对元素进行批量操作),从而大大降低系统本身的复杂的,提升可维护性。
注意一点,batch(fn) 输出的函数有副作用,然而 batch(fn) 用闭包将输出的函数的副作用限制在了 batch(fn) 的作用域内。
Ramda.js 的 lift 方法
Ramda.js 的 lift 方法和 batch 有一点点类似,不过功能更强大。让我们来用它实现一个有一点点“烧脑”的效果,来作为这篇文章的结尾:
async function reducer(promise, action){
let res = await promise;
return action(res);
}
function continuous(...functors){
return async function(input){
return await functors.reduce(reducer, input)
}
}
function sleep(ms){
return new Promise(resolve => setTimeout(resolve, ms));
}
async function setColor(item, color){
await sleep(500);
item.style.color = color;
}
let comb = R.lift((el, color) => {
return [el, color];
});
let changeColorTo = (args) => R.partial(setColor, args);
let items = Array.from(list.children);
let task = R.map(changeColorTo, comb(
items,
['red', 'orange', 'yellow']
));
continuous(...task)(0);
奇舞周刊
——————————————————
领略前端技术 阅读奇舞周刊
长按二维码,关注奇舞周刊
▼
以上是关于月影谈为什么我们要用函数式编程的主要内容,如果未能解决你的问题,请参考以下文章