Skip to content

Latest commit

 

History

History
598 lines (411 loc) · 14.9 KB

霖呆呆的函数式编程之路(四)-组合函数.md

File metadata and controls

598 lines (411 loc) · 14.9 KB

组合函数

到了第四章了,函数式编程的魅力似乎越来越大。

对于函数式编程者,他们会将每个函数都当成是一个“部件”,在需要时通过组装不同的“部件”,来拼凑出一个自己想要的“模型”。

专业点的角度来说,就是我们能够定义某种组合方式,来让它们成为一种新的组合函数,程序中不同的部分都可以使用这个函数。这种将函数一起使用的过程叫做组合。

再介绍组合函数的概念之前,我们就已经使用过组合了。

例如在之前我们的一个案例:

unary(adder(3))

上面的表达式,我们将两个函数整合起来,然后将第一个函数调用产生的值(输出)当成第二个函数调用的实参(输入)。画个简图,也就是这样:

functionValue <-- unary <-- adder <-- 3

3adder(..) 的输入。而 adder(..) 的输出是 unary(..) 的输入。unary(..) 的输出是 functionValue。 这就是 unary(..)adder(..) 的组合。

Compose2函数

为了满足上面组合函数的要求,我们可以来构造这么一个简单的函数:

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

// ES6 箭头函数形式写法
var compose2 =
	(fn2,fn1) =>
		origValue =>
			fn2( fn1( origValue ) );

它能够自动创建两个函数的组合,这和我们手动做的是一模一样的。

Words案例

现在有这么一个需求,需要将给定的一个英文字符串,提取其中全部的英文单词,先全部转化小写,然后去除其中重复的单词。

我们可以先来创建这么2个函数:

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 text = "To compose two functions together, pass the \
output of the first function call as the input of the \
second function call.";

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

wordsUsed;
// ["to","compose","two","functions","together","pass",
// "the","output","of","first","function","call","as",
// "input","second"]

在上面的例子中,我们将该过程分为了2步来做。

并且先创建了wordsFound函数,然后将该函数的输出再传递给unique,实际上,上面的效果等同于:

var wordsUsed = unique( words(text) )

所以我们可以将其封装一层:

function uniqueWords(str) {
	return unique( words( str ) );
}

var wordsUsed = uniqueWords(text)

你会发现,其实我们还可以这样写:

var uniqueWords = compose2( unique, words )

var wordsUsed = uniqueWords(text)

这样我们就成功将uniqueWords转化为了无形参的函数。

uniqueWords(..) 接收一个字符串并返回一个数组。它是 unique(..)words(..) 的组合,并且满足我们的数据流向要求:

wordsUsed <-- unique <-- words <-- text

compose函数

在上面我们构造了compose2函数,它能接收2个函数,并将2个函数从右向左的执行。

如果我们能够定义两个函数的组合,我们也同样能够支持组合任意数量的函数。任意数目函数的组合的通用可视化数据流如下:

finalValue <-- func1 <-- func2 <-- ... <-- funcN <-- origValue

我们能够像这样实现一个通用 compose(..) 实用函数:

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

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

		return result;
	};
}

// ES6 箭头函数形式写法
var compose =
	(...fns) =>
		result => {
			var list = fns.slice();

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

			return result;
		};

现在看一下组合超过两个函数的例子。回想下我们的 uniqueWords(..) 组合例子,让我们增加一个 skipShortWords(..),它将所有单词字母数大于4的提取出来:

function skipShortWords(list) {
	return list.filter(str => str.length > 4)
}

让我们再定义一个 biggerWords(..) 来包含 skipShortWords(..)。我们期望等价的手工组合方式是 skipShortWords(unique(words(text))),所以让我们采用 compose(..) 来实现它:

var text = "To compose two functions together, pass the \
output of the first function call as the input of the \
second function call.";

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

var wordsUsed = biggerWords( text );

wordsUsed;
// ["compose","functions","together","output","first",
// "function","input","second"]

现在,让我们回忆一下第 3 章中出现的 partialRight(..) 来让组合变的更有趣。我们能够构造一个由 compose(..) 自身组成的右偏函数应用,通过提前定义好第二和第三参数(unique(..)words(..));我们把它称作 filterWords(..)(如下)。

然后,我们能够通过多次调用 filterWords(..) 来完成组合,但是每次的第一参数却各不相同。

function skipShortWords(list) {
	return list.filter(str => str.length > 4)
}
function skipLongWords(list) { 
	return list.filter(str => str.length <= 4)
}

var filterWords = partialRight( compose, unique, words );

var biggerWords = filterWords( skipShortWords );
var shorterWords = filterWords( skipLongWords );

biggerWords( text );
// ["compose","functions","together","output","first",
// "function","input","second"]

shorterWords( text );
// ["to","two","pass","the","of","call","as"]

花些时间考虑一下基于 compose(..) 的右偏函数应用给了我们什么。

甚至我们可以结合前面一章的notwhenidentity函数来重构一下:

// 取反辅助函数
function not(predicate) {
    return function negated(...args) {
        return !predicate(...args)
    }
}
// 判断某个条件成立之后执行fn
function when(predicate, fn) {
    return function conditional(...args) {
        if (predicate(...args)) {
            return fn(...args)
        }
    }
}
// 传一个返回一个
function identity(v) {
    return v;
}

var isLong = (str) => str.length > 4;
var isShort = not(isLong)
var returnLong = when(isLong, identity)
var returnShort = when(isShort, identity)

function skipShortWords(list) {
    return list.filter(str => returnLong(str))
}
function skipLongWords(list) {
    return list.filter(str => returnShort(str))
}

var filterWords = partialRight(compose, unique, words)

var biggerWords = filterWords(skipShortWords)
var shorterWords = filterWords(skipLongWords)

biggerWords( text );
// ["compose","functions","together","output","first",
// "function","input","second"]

shorterWords( text );
// ["to","two","pass","the","of","call","as"]

compose的不同实现方式

reduce实现

function compose(...fns) {
	return fns.reverse().reduce( function reducer(fn1,fn2){
		return function composed(...args){
			return fn2( fn1( ...args ) );
		};
	} );
}

// ES6 箭头函数形式写法
var compose =
	(...fns) =>
		fns.reverse().reduce( (fn1,fn2) =>
			(...args) =>
				fn2( fn1( ...args ) )
		);

递归实现

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

	var composedFn = function composed(...args){
		return fn2( fn1( ...args ) );
	};

	if (rest.length == 0) return composedFn;

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

// ES6 箭头函数形式写法
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 );
	};

pipe函数

我们早期谈及的是从右往左顺序的标准 compose(..) 实现。这么做的好处是能够和手工组合列出参数(函数)的顺序保持一致。

不足之处就是它们排列的顺序和它们执行的顺序是相反的,这将会造成困扰。同时,不得不使用 partialRight(compose, ..) 提早定义要在组合过程中 第一个 执行的函数。

相反的顺序,从右往左的组合,有个常见的名字:pipe(..)

pipe(..)compose(..) 一模一样,除了它将列表中的函数从左往右处理。

function pipe(...fns) {
	return function piped(result){
		var list = fns.slice();

		while (list.length > 0) {
			// 从列表中取第一个函数并执行
			result = list.shift()( result );
		}

		return result;
	};
}

实际上,我们只需将 compose(..) 的参数反转就能定义出来一个 pipe(..)

var pipe = reverseArgs( compose );

回忆下之前的通用组合的例子:

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

pipe(..) 的方式来实现,我们只需要反转参数的顺序:

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

pipe(..) 的优势在于它以函数执行的顺序排列参数,某些情况下能够减轻阅读者的疑惑

抽象

先来介绍2个简单且实用的函数

prop函数

将任意对象的任意属性通过属性名提取出来。让我们把这个实用函数称为 prop(..)

function prop(name,obj) {
	return obj[name];
}

// ES6 箭头函数形式
var prop =
	(name,obj) =>
		obj[name];

使用:

var obj = { x: 1, y: 2 }

prop('x', obj)
// 1

setProp函数

我们处理对象属性的时候,也需要定义下反操作的工具函数:setProp(..),为了将属性值设到某个对象上。

function setProp(name,obj,val) {
	var o = Object.assign( {}, obj );
	o[name] = val;
	return o;
}

使用:

var obj = { x: 1, y: 2 }

var obj2 = setProp('z', obj, 3)
// { x: 1, y: 2, z: 3 }

makeObjProp函数

function makeObjProp(name,value) {
	return setProp( name, {}, value );
}

// ES6 箭头函数形式
var makeObjProp =
	(name,value) =>
		setProp( name, {}, value );

提示: 这个实用函数在 Ramda 库中被称为 objOf(..)

回顾ajax案例

让我们回顾一下第二章介绍的ajax案例

function ajax (url, data, callback) {
	// ...
}
var getUser = partial( ajax, "/api/user" );
var getLastOrder = partial( ajax, "/api/order", { orderId: -1 } );

var output = (str) => console.log(sgr);

getLastOrder(function orderFound(order) {
  getUser({ userId: order.userId }, function userFound (user) {
    output(user.name)
  })
})

如上,我们在给getLastOrder函数传递最后一个参数(一个回调函数orderFound)

该函数用查询到的订单信息order中的userId查询当前订单的用户,并输出用户的姓名name.

可以看到上面的函数需要orderuser两个形参。

我们可以用现有的函数式编程的知识将其转化为一个无形参的函数getLastOrder.

移除user形参

从里向外,我们先想想如何移除user这个形参。

首先output函数是需要接收user.name这个参数的。我们可以用什么样的方式来移除这个参数呢。

在这里我们的目的是想要获取user中的name属性:

定义一个extractName函数:

var extractName = partial(prop, 'name')

之后我们就可以直接用:

extractName(user)

这样的方式获取到user.name

接着你是不是也想到可以用compose了呢?

var outputUserName = compose( output, extractName )

想一下我们需要的数据流是什么样:

output <-- extractName <-- user

下一步,让我们缩小关注点,看下例子中嵌套的这块查找操作的调用:

getLastOrder( function orderFound(order){
	getUser( { userId: order.userId}, outputUserName );
} );

我们刚刚创建的 outputUserName(..) 函数是提供给 getUser(..) 的回调。所以我们还能定义一个函数叫做 processUser(..) 来处理回调参数,使用 partialRight(..)

var processUser = partialRight( getUser, outputUserName )

让我们用新函数来重构下之前的代码:

getLastOrder(function orderFound(order) {
	processUser({ userId: order.userId })
})

Ok,至此,user这个形参已经被我们干掉了。

移除order形参

接下来你可以用类似的方式移除掉order形参。

首先是获取userId

var extractUserId = partial(prop, 'userId')

接着你需要定义一个函数来解决{ userId: order.userId }这个问题。我们可以使用上面的makeObjProp函数:

var userData = partial(makeObjProp, 'userId')

为了使用 processUser(..) 来完成通过 order 值查找一个人的功能,我们需要的数据流如下:

processUser <-- userData <-- extractUserId <-- user

所以我们只需要再使用一次 compose(..) 来定义一个 lookupUser(..)

var lookupUser = compose( processUser, userData, extractUserId )

完整流程

然后,就是这样了!把这整个例子重新组合起来,不带任何的“形参”:

function ajax (url, data, callback) {
	// ...
};
var getUser = partial( ajax, "/api/user" );
var getLastOrder = partial( ajax, "/api/order", { orderId: -1 } );

var output = (str) => console.log(sgr);

var extractName = partial(prop, 'name');
var outputUserName = compose( output, extractName );
var processUser = partialRight( getUser, outputUserName )
var extractUserId = partial(prop, 'userId')
var userData = partial(makeObjProp, 'userId')
var lookupUser = compose( processUser, userData, extractUserId )

getLastOrder( lookupUser )

Look!不带任何的形参。

总结

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

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

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

组合 ———— 声明式数据流 ———— 是支撑函数式编程其他特性的最重要的工具之一。