21.函数技巧 - 函数式编程之 compose 函数
# 0. 前言
# 1. 如何实现传统的 compose
函数?
例如,假设我们有三个函数 task1
、task2
、task3
,compose
函数可以定义如下:
const compose = (task1, task2, task3) => (...args) =>
task1(task2(task3(...args)));
2
其中,args
是传入 compose
函数的参数,最终传递给 task3
函数执行,task3
函数的返回结果再传递给 task2
。类似的,task2
的执行结果再传递给队首。
JS
实现:
const tasks = [step1, step2, step3, step4]
// 定义实现:
const compose = (...args) => step1(step2(setp3(step4(...args))));
// 优雅实现
const compose = (...funcs) => {
return funcs.reduce((preFun,curFun) => (...args) => preFun(curFun(...args))
}
// 如何调用
compose(tasks)(); // 注:这里需执行下
2
3
4
5
6
7
8
9
10
11
12
对于 reduce
实现的 compose
函数不是很好理解,因为正常我们使用 reduce
返回的通常是一个值,而这里返回的是一个函数,于是存在一个函数递归的逻辑。
整个 reduce
是一个递归的过程,自顶向下构建如下函数:
[task1] → (...args) => task1(...args);
[task1,task2] → (...args) => task1(task2(...args));
[task1,task2,task3] → (...args) => task1(task2(task3(...args)));
2
3
递归逻辑: (...args) => pre(cur(...args))
整个公式很优美,但是比较难思考,下面换个角度来思考这道题,并且尝试将这种编程思想泛化。
# 2. 使用递归实现 compose
函数
首先比较难实现的是两个点:
- 如何实现
step4
→step3
→step2
→step1
,即列表的从后往前的执行。 - 如何将参数透传到最后一个函数。
如果数组的执行次序是,从前往后非常好实现,将数组翻转后操作就比较方便。
这里提供两种处理第一个数据和剩余数组数据的写法。
写法一:arr.slice(1) + reduce(()=>{}, arrp[0])
/* 立即执行写法 */
const compose_immediate1 = (...funcs) => {
/* 翻转数组 */
funcs.reverse();
/* 处理初始值 */
funcs.slice(1).reduce((pre, cur) => {
pre = cur(pre);
return pre;
}, funcs[0]());
};
compose_immediate1(a, b, c);
2
3
4
5
6
7
8
9
10
11
12
写法二:
const compose_immediate2 = (...funcs) => {
/* 翻转数组 */
funcs.reverse();
/* 处理初始值 */
const [first, ...otherFuns] = funcs;
otherFuns.reduce((pre, cur) => {
pre = cur(pre);
return pre;
}, first());
};
compose_immediate2(a, b, c);
2
3
4
5
6
7
8
9
10
11
12
如果不考虑数组翻转,实现起来需要使用 递归 方案,构造一个包裹函数 wrap_fun
,入参接受 callback
函数(即,高阶函数),该高阶函数用于执行下一个任务函数。
const tasks = [step1, step2];
const step1 = () => console.log("step1");
↓
const wrap_step1 = (next) => {
next(); // callback 为下一个任务函数
step1();
}
// 此时设置 next = step2 即可
wrap_step1(step2)
2
3
4
5
6
7
8
9
10
11
当 step2
为最后一个执行函数时,next
传递 step2
即可,若 step2
不为最后一个,next
需要传递 step2
的包裹函数 wrap_step2
,内置了下一层的包裹函数。
举例 step
存在四个任务时,有如下逻辑:
// 利用箭头函数,延迟代码执行, wrap_fun 为包裹函数
// args 为 compose 入参
const step4 = (...args) => console.log(4,args);
↓
const wrap_step4 = (...args) => step4(...args);
↓
const step3 = (res4) => console.log(3,res4);
↓
const wrap_step3 = (...args) => {
// 需调用执行 step4 函数
const res = wrap_step4(...args);
// step3 默认函数
step3(res);
}
↓
const step2 = () => console.log(2);
↓
const wrap_step2 = (...args) => {
// 需调用执行 step3 函数
const res = wrap_step3(...args);
// 运行自己的函数
step2(res);
}
↓
const step1 = () => console.log(1);
↓
const wrap_step1 = (...args) => {
// 需调用执行 step2 函数
const res = wrap_step2(args);
// 运行自己的函数
step1(res);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
基于这个思路,构造 wrapFun
的写法,编写递归逻辑。
function compose_iterable(...tasks: any[]) {
return (...args) => {
function wrapFun(i: number) {
// 最后一个任务,直接调用 args 返回函数值
if (i === tasks.length - 1) return tasks[i](...args);
return tasks[i](wrapFun(i + 1));
}
return wrapFun(0);
};
}
compose_iterable(a, b, c)("test1");
2
3
4
5
6
7
8
9
10
11
有的时候,我们希望 compose
不直接调用,可以将 wrapFun
返回一个箭头函数,主动控制调用时机:
function compose_iterable_delay(...tasks: any[]) {
return (...args) => {
function wrapFun(i: number) {
if (i === tasks.length - 1) return () => tasks[i](...args);
return () => tasks[i](wrapFun(i + 1)());
}
return wrapFun(0);
};
}
compose_iterable(a, b, c)("test1")();
2
3
4
5
6
7
8
9
10
# 3. 传统 compose
函数的特点及后续改进
传统的 compose
函数执行特点如下:
- 参数传递:第一个入参支持多元(接受多个参数),后面的函数呈现柯里化特性(接受一个参数)。
- 执行次序:
task3
→task2
→task1
- 同步函数:所有
task
要求是同步的。
总结:compose
函数为一个高阶函数,它接受多个函数作为参数,并返回一个新的函数。
希望实现的 compose
的特点:
- 支持同步
compose
和 异步compose
的特性。 - 执行次序:根据队列排序 顺序 执行。
- 支持多种处理模式,并且使用
Class
取代原有的Funciton
写法。