柯里化和偏函数
写在前面
本文的目的在于使用函数式编程解决常见问题,本文仅代表个人对函数式编程的一些粗浅认识,仅供参考,如有错漏,欢迎指出。
什么是柯里化
柯里化是把一个多参数函数转换为嵌套的单参数函数的过程
举个栗子
一个相加函数add = (x, y) => x + y,如果写成柯里化函数可以这样写:
function curryAdd(x) {
return function (y) {
return x + y;
};
}
console.log("curryAdd(4)(5) :>> ", curryAdd(4)(5)); // 9
这里使用ES5的语法,是为了能更明显的体现函数嵌套的关系,当x=4时,返回一个匿名函数:
function (y) {
return 4 + y
}
// 和curryAdd(4)是等价的
柯里化函数的通用定义
第一步 判断参数是否为函数
const curry = (fn) => {
if (typeof fn !== "function") {
throw Error("no function");
}
};
第二步 传入参数并使用apply调用函数
const curry = (fn) => {
if (typeof fn !== "function") {
throw Error("no function");
}
return function curriedFn(...args) {
return fn.apply(null, args);
};
};
const multiply = (x, y, z) => x * y * z;
console.log(curry(multiply)(1,2,3)); // 6
相比第一步,增加了
return function curriedFn(...args) {
return fn.apply(null, args);
};
通过curry(multiply)(1,2,3), args会指向[1,2,3],相当于multiply(1,2,3),结果为6
第三步 将多参数函数转换为单参数函数的柯里化函数
const curry = (fn) => {
if (typeof fn !== "function") {
throw Error("no function");
}
return function curriedFn(...args) {
if (args.length < fn.length) {
return function () {
const as = args.concat([].slice.call(arguments));
return curriedFn.apply(null, as);
};
}
return fn.apply(null, args);
};
};
相比第二步增加了
if (args.length < fn.length) {
return function () {
const as = args.concat([].slice.call(arguments));
return curriedFn.apply(null, as);
};
}
};
说明一下:
- args.length < fn.length 判断传入参数的个数与函数参数个数是否一致,一致则直接调用函数
- args起到一个存储参数的作用,const as = args.concat([].slice.call(arguments)); 每进入一层嵌套函数,args都把arguments加进来,作为下一次嵌套函数的参数,直到传入参数的个数与函数参数个数是否一致
- curriedFn.apply(null, as),把前面的参数作为下一次嵌套函数的参数
const multiply = (x, y, z) => x * y * z;
console.log(curry(multiply)(1)(2)(3)); // 6
最后得到的结果就是每个嵌套函数的结果
柯里化在开发中应用
由已知函数创建新的函数
在数组中查找数字:
const arr = ["hello", "world", "123666"];
const hasNumber = (str) => str.match(/[0-9]+/); // 是否匹配数字
const fn = (fn, ary) => ary.filter(fn); // 筛选函数
const filter = curry(fn);
const match = curry(hasNumber);
const findNumInArr = filter(match);
console.log(findNumInArr(arr));
可以由简单的两个函数组合成一个新的函数findNumInArr,这里把数组作为最后一个参数传入,实际上是有意为之,开发中程序员经常处理数组一类的数据结构,把数组作为最后一个参数传入,能够很方便的创建如findNumInArr/findEvenInArr等可复用的函数。
固定部分函数参数
开发中可能会遇到这样的函数:
const logHelper = (type, position, message) => {
console.log(type, position, message);
};
logHelper("ERROR", "Error at State.js", "this is a error.");
logHelper("WARN", "Warn at State.js", "this is a warn.");
logHelper("INFO", "Info at State.js", "this is a info.");
可以通过柯里化固定前两个参数,起到简洁代码的效果。
const errorLogger = curry(logHelper)("ERROR")("Error at State.js");
const warnLogger = curry(logHelper)("WARN")("Warn at State.js");
const infoLogger = curry(logHelper)("INFO")("Info at State.js");
errorLogger("this is a error");
warnLogger("this is a warn");
infoLogger("this is a info");
// ERROR Error at State.js this is a error.
// WARN Warn at State.js this is a warn.
// INFO Info at State.js this is a info.
偏函数
假如一个setTimeout函数:
setTimeout(() => {
console.log("Hello World"), 1000;
});
按上面所讲,我们能隐藏1000这个时间吗?能用curry解决吗?答案是否定的,因为curry应用参数的方式是从左往右,一次处理一个参数,所以不能做到。
虽然我们可以像这样调换函数参数的顺序来变相实现,但实际不得不创建warpper这样的封装器,而这正是可以使用偏函数的地方。
const warpper = (time, fn) => {
setTimeout(fn, time);
};
const delay = curry(warpper)(1000);
delay(() => console.log("Hello World"));
偏函数的通用定义
const partial = (fn, ...partialArgs) => {
let args = partialArgs;
return function (...fullArgs) {
let arg = 0;
for (let i = 0; i < args.length && arg < fullArgs.length; i++) {
if (args[i] === undefined) {
args[i] = fullArgs[arg++];
}
}
return fn.apply(null, args);
};
};
const delayOne = partial(setTimeout, undefined, 1000);
delayOne(() => console.log("Hello World"));
简单说明一下,通过闭包,我们获取到传入的参数args = [undefine, 1000],返回函数会记住args的值(是的,我们再次使用了闭包),fullArgs的值就是delayOne函数的参数
比较不好理解的就是以下这部分:
for (let i = 0; i < args.length && arg < fullArgs.length; i++) {
if (args[i] === undefined) {
args[i] = fullArgs[arg++];
}
}
我们来逐步分析这段代码
当 i= 0时
args = [undefined, 1000]
fullArgs = [() => console.log(‘Hello World’)]
在if循环内
args[0] = fullArgs[0]
args = [() => console.log(‘Hello World’), 1000]
此时我们已经得到足够的参数执行方法
fn.apply(null, args);
即
setTimeout(() => console.log(‘Hello World’), 1000)
注意
partial函数有个bug,用不同的参数再次调用,都只会返回第一次的结果,原因是用参数替换undefined值从而修改partialArgs,而数组传递的只是引用。这里仅仅是讨论这种仅使用部分函数参数的思想,“原来可以这么玩”,实际开发中推荐使用lodash.partial方法。
总结
柯里化函数将多参数函数转换为嵌套的单参数函数,可以用来固定较多参数函数的一部分,用于简洁代码,有时候只需要用到前面参数和最后一个参数,中间参数处于未知状态,这正是偏函数应用的地方。