函数式编程(二)-函数组合

309 阅读4分钟

概述

函数式编程的一个思想是:每个函数只做好一件事情。每个函数就像是一个个乐高积木,当我们在构建复杂系统的时候,我们应该清晰的了解我们接下来需要哪个“部件”,以及在哪里获取这个部件。

但有些时候,你把蓝色 2x2 的方块和灰色 4x1 的方块以某种形式组装到一起,然后意识到:“这是个有用的部件,我可能会常用到它”。

那么你现在想到了一种新的“部件”,它是两种其他部件的组合,在需要的时候能触手可及。这时候,将这个蓝黑色 L 形状的方块组合体放到需要使用的地方,比每次分开考虑两种独立方块的组合要有效的多。

所以我们可以通过定义某种组合方式,让他们称为一种新的函数组合,并在程序的不同部分使用,这种方式称为函数组合。

输出到输入

函数组合的核心在于:一个函数的输出是另一个函数的输入。把数据的流向想像成一个流水线的传送带,在传送带的各个环节负责对物品的不同处理。举个例子,下面是两个常用的函数:

function words(str) {
	return String( str )
		.toLowerCase()
		.split( /\s|\b/ )
		.filter( function alpha(v){
			return /^[\w]+$/.test( v );
		} );
}

function unique(list) {
	var uniqList = [];

	for (let i = 0; i < list.length; i++) {
		// value not yet in the new list?
		if (uniqList.indexOf( list[i] ) === -1 ) {
			uniqList.push( list[i] );
		}
	}

	return uniqList;
}

当我们应用它时我们会这样使用:

var wordsFound = words( text );
var wordsUsed = unique( wordsFound );

然而我们在构建程序的过程中经常需要将这两种函数一起使用,让它更像是一个具备了两种功能的函数套件,并节省代码量,我们可以这样调用:

var wordsUsed = unique( words( text ) );

但是这样看起来比较凌乱,我们可以写一个组合函数的函数:

function compose(fn2,fn1) {
	return function composed(origValue){
		return fn2( fn1( origValue ) );
	};
}

用箭头函数表示

var compose =
	(fn2,fn1) =>
		origValue =>
			fn2( fn1( origValue ) );

最后 我们可以利用这个函数来组合我们之前的两个函数了:

var uniqueWords = compose2( unique, words );
var wordsUsed = uniqueWords(text);

更进一步

当然,在更多时候我们需要一个更加通用的函数组合方式,而不仅仅是只能组合两个函数,而是可以组合任意数量的函数。思路也是非常简单,我们通过一个while循环,不断将需要组合的函数拿出来执行即可。

function compose(...fns) {
	return function composed(result){
		// 拷贝一份保存函数的数组
		var list = fns.slice();

		while (list.length > 0) {
			// 将最后一个函数从列表尾部拿出
			// 并执行它
			result = list.pop()( result );
		}

		return result;
	};
}

我们在之前的两个函数的基础上增加一个函数:

function skipShortWords(list) {
	var filteredList = [];

	for (let i = 0; i < list.length; i++) {
		if (list[i].length > 4) {
			filteredList.push( list[i] );
		}
	}

	return filteredList;
}

然后我们就可以将三个函数组合起来使用:

var biggerWords = compose( skipShortWords, unique, words );

var wordsUsed = biggerWords( text );

其它实现

当然,你可能不会在实际的开发中自己去实现一个compose函数用来组合函数,你一般会选择使用一个三方库,但是了解函数组合的实现有助于我们理解函数式编程的思想。compose函数的原始实现方式与我们这个有一些不同:

let compose = (...fns) => {
    return (...args) => {
       return fns.reverse().reduce((fn1, fn2) => {
           return fn2(fn1(...args));
       })  
    }
}

核心是使用reduce函数来实现循环,这种实现的好处在于代码更加简洁,且使用了reduce这样的函数式编程的方法实现,在性能上跟for循环实现差不多

同样,我们可以使用递归来实现compose函数:

var compose = (...fns) => {
    // 拿出最后两个参数
    var [ fn1, fn2, ...rest ] = fns.reverse();

    var composedFn = (...args) => {
        fn2( fn1( ...args ) );
    }
    if (rest.length == 0) return composedFn;

    return compose( ...rest.reverse(), composedFn );
};

总结

函数组合是一种定义函数的模式,它能将一个函数调用的输出路由到另一个函数的调用上,然后一直进行下去。

因为 JS 函数只能返回单个值,这个模式本质上要求所有组合中的函数(可能第一个调用的函数除外)是一元的,当前函数从上一个函数输出中只接收一个输入。

相较于在我们的代码里详细列出每个调用,函数组合使用 compose(..) 实用函数来提取出实现细节,让代码变得更可读,让我们更关注组合完成的是什么,而不是它具体做什么