函数式编程太火了,我也想学!!!
函数式编程太难了,该怎么入门???
嗯,函数式编程这么火,是一定要学的,但是怎么学呢?本文通过一个例子来介绍函数式编程的用法,先声明本文没有复杂的概念,也没有各种定义,只有一个由浅入深的例子
背景
假设有一个如下的数据结构,type 是 1-n,flag 表示当前元素的状态,页面大概是一个列表,可以通过类型和选择状态来过滤列表内容
const list = [
{
type: 1,
flag: true,
},
{
type: 2,
flag: false,
},
{
type: 3,
flag: true,
},
];
可能的需求有下面这些:
- 获取全部的列表
- 获取一个或多个类型的列表
- 获取否定类型的列表
- 获取指定状态的列表
- 获取指定状态,指定类型的列表
呃呃呃,有点懵是不是,思考下这个程序该怎么写?如果写一个函数满足上面所有的需求呢?
获取全部的列表
下面先来写第一个需求,需要用到数组的 filter 和函数的默认值,就是这么简单
function getList(
filter = function () {
return true;
}
) {
return list.filter(filter);
}
调用getList
就能获取全部的列表,但上面的参数默认值其实可以提取出一个公共函数
// 高阶函数
function bool(flag) {
return function () {
return !!flag;
};
}
bool(true); // function { return true; }
bool(false); // function { return false; }
快来用 bool 来重写上面的代码吧,更简洁了是不是
function getList(filter = bool(true)) {
return list.filter(filter);
}
抽象出来的 bool 是一个高阶函数,bool 让返回布尔值的函数变得可以复用,高阶函数让函数可以被复用
获取一个或多个类型的列表
如果想获取指定类型的列表,最简单的写法如下
function getList(filter = bool(true)) {
return list.filter(filter);
}
getList(function (data) {
return data.type === 1;
});
getList(function (data) {
return data.type === 2;
});
getList(function (data) {
return data.type === 3;
});
上面代码中多次调用data.type
,我们把获取对象属性的功能抽象出来
function pick(obj, prop) {
return obj[prop];
}
pick(data, 'type'); // data.type
下面用 pick 函数改写上面的代码
getList(function (data) {
return pick(data, 'type') === 1;
});
getList(function (data) {
return pick(data, 'type') === 2;
});
getList(function (data) {
return pick(data, 'type') === 3;
});
好像更复杂了。。。要是能把 type 参数提前绑定呢?绑定函数参数好像是函数柯里化,下面的 currying 函数
function currying(func, ...args) {
return func.bind(null, ...args);
}
function add(x, y) {
return x + y;
}
const add10 = currying(add, 10);
add10(1); // 11
add10(5); // 15
但 currying 的绑定顺序是从左到右的,上面我们希望的先绑定第二个参数,如果能反转一下参数顺序就好了
function reverseArgs(...args) {
return args.reverse();
}
reverseArgs(1, 2, 3); // [3, 2, 1]
怎么才能把 reverseArgs 和我们的函数结合起来呢?下面介绍一个神奇的高阶函数
// 返回一个函数A,A在被执行时会依次执行传入的函数参数
// compose(fn1, fn2, fn3)(1) 相当于fn3(...fn2(...fn1(1)))
function compose(...fns) {
return function (...args) {
return fns.reduce((prev, fn) => fn(...[].concat(prev)), args);
};
}
有了 currying 和反转参数的代码再来改造下我们上面的代码,豁然开朗有木有
const reversePick = compose(reverseArgs, pick); // 参数顺序被反转 reversePick('type', data)
const pickType = currying(reversePick, 'type'); // 先绑定第一个参数 pickType(data)
getList(function (data) {
return pickType(data) === 1;
});
getList(function (data) {
return pickType(data) === 2;
});
getList(function (data) {
return pickType(data) === 3;
});
但上面的代码并不能执行,由于数组 filter 函数,传给每个 filter 的参数并不是一个,而是三个,这就影响到了我们的 reverseArgs,操作,随意需要先进性裁切操作,还需要引入一个函数
function sliceArgs(num, ...args) {
return args.slice(0, num);
}
const sliceArgs2 = currying(sliceArgs, 2);
const reversePick = compose(sliceArgs2, reverseArgs, pick);
上面的代码中的另一个问题是存在多次判断的代码,下面提取出来
function isEqual(x, y) {
return x === y;
}
const isType1 = currying(isEqual, 1);
const isType2 = currying(isEqual, 2);
const isType3 = currying(isEqual, 3);
getList(function (data) {
return isType1(pickType(data));
});
getList(function (data) {
return isType2(pickType(data));
});
getList(function (data) {
return isType3(pickType(data));
});
显然代码好像更复杂了,别急继续往下看,上面代码重复写了三个函数,利用前面的compose
可以把这个函数也消除,是不是很神奇
// compose(pickType, isType1(data) = function () { isType1(pickType(data)) }
getList(compose(pickType, isType1));
getList(compose(pickType, isType2));
getList(compose(pickType, isType3));
上面解决了一个类型的判断,如果相判断 1 或 2 该怎么做呢?还需要一个或操作的高阶函数
function or(...fns) {
return function (...agrs) {
return fns.some(fn => fn(...args))
}
}
getList(compose(pickType, or(isType1, isType2))
获取否定类型的列表
下面来看看如何获取否定类型的列表,比如获取所有 type 非 1 的,看起来需要一个取非的函数
function not(fn) {
return function (...args) {
return !fn(...args)
}
}
getList(compose(pickType, not(isType1))
获取指定状态的列表
下面来看看如何获取指定类型的列表,其实参考上面的过程就很容易得出
const isFlagTrue = currying(isEqual, true);
const isFlagFalse = currying(isEqual, false);
const pickFlag = currying(compose(sliceArgs2, reverseArgs, pick), 'flag'); // currying reverseArgs 见上面
getList(compose(pickFlag, isFlagTrue)); // 获取flag为true的列表
getList(compose(pickFlag, isFlagFalse)); // 获取flag为false的列表
获取指定状态,指定类型的列表
有了获取指定状态和指定类型的代码,那么如何指定两个呢?我们需要一个并操作的高阶函数
function and(...fns) {
return function (...agrs) {
return fns.every((fn) => fn(...args));
};
}
// 获取类型为1,flag为true的列表
getList(and(compose(pickType, isType1), compose(pickFlag, isFlagTrue)));
// 获取类型不为1,flag为true的列表
getList(and(compose(pickType, not(isType1)), compose(pickFlag, isFlagTrue)));
// 获取类型为1或2,并且flag为true的列表
getList(
and(compose(pickType, or(isType1, isType2)), compose(pickFlag, isFlagTrue))
);
总结
整个过程我们抽象了很多通用函数,这些函数都是可以复用的
function bool(flag) {
return function () {
return !!flag;
};
}
function pick(obj, prop) {
return obj[prop];
}
function currying(func, ...args) {
return func.bind(null, ...args);
}
function reverseArgs(...args) {
return args.reverse();
}
function sliceArgs(num, ...args) {
return args.slice(0, num);
}
function compose(...fns) {
return function (...args) {
return fns.reduce((prev, fn) => fn(...[].concat(prev)), args);
};
}
function isEqual(x, y) {
return x === y;
}
function or(...fns) {
return function (...args) {
return fns.some((fn) => fn(...args));
};
}
function not(fn) {
return function (...args) {
return !fn(...args);
};
}
function and(...fns) {
return function (...args) {
return fns.every((fn) => fn(...args));
};
}
完整的业务代码如下,有了这些函数,代码变得非常简单,易读,并且非常灵活,可以随意组合
const sliceArgs2 = currying(sliceArgs, 2);
const pickType = currying(compose(sliceArgs2, reverseArgs, pick), 'type');
const isType1 = currying(isEqual, 1);
const isType2 = currying(isEqual, 2);
const isType3 = currying(isEqual, 3);
const pickFlag = currying(compose(sliceArgs2, reverseArgs, pick), 'flag');
const isFlagTrue = currying(isEqual, true);
const isFlagFalse = currying(isEqual, false);
const list = [
{
type: 1,
flag: true,
},
{
type: 2,
flag: false,
},
{
type: 3,
flag: true,
},
];
function getList(filter = bool(true)) {
return list.filter(filter);
}
// 获取类型为1,flag为true的列表
getList(and(compose(pickType, isType1), compose(pickFlag, isFlagTrue)));
// 获取类型不为1,flag为true的列表
getList(and(compose(pickType, not(isType1)), compose(pickFlag, isFlagTrue)));
// 获取类型为1或2,并且flag为true的列表
getList(
and(compose(pickType, or(isType1, isType2)), compose(pickFlag, isFlagTrue))
);
下面给出对应的非函数式写法的代码,对比一下,大量的过程式代码,区别还是很大的,如果是你,你会使用那种方式呢?
function getList(filter = bool(true)) {
return list.filter(filter);
}
// 获取类型为1,flag为true的列表
getList(function (data) {
return data.type === 1 && data.flag === true;
});
// 获取类型不为1,flag为true的列表
getList(function (data) {
return data.type !== 1 && data.flag === true;
});
// 获取类型为1或2,并且flag为true的列表
getList(function (data) {
return (data.type === 1 || data.type === 2) && data.flag === true;
});
最后给大家推荐一本 javascript 函数式编程的书籍《JavaScript 函数式编程》,如果对函数式感兴趣的话就赶紧入手吧
原文网址:http://yanhaijing.com/javascript/2018/03/01/functional-programming-practice/