定义
函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
例如,一些分析技术只能用于具有单一参数的函数。现实中的函数往往有更多的参数。为单一参数情况提供解决方案已经足够了,因为可以将具有多个参数的函数转换为一个单参数的函数链。这种转变是现在被称为“柯里化”的过程。
实现
我们先写一个实现加法的函数 add:
function add (x, y) {
return (x + y)
}
add (1, 3);//4
根据柯里化的条件,写一个 curriedAdd 的函数。
function curriedAdd (x) {
return function(y) {
return x + y
}
}
let addOne=curriedAdd(1);
addOne(3);//4
当然以上实现是有一些问题的:它并不通用,我们想实现一个通用的方法而不是每次都需要针对某个问题实现 currying 化。
此时我们需要用到 JavaScript 中的作用域来保存上一次传进来的参数。 对 curriedAdd 进行抽象,可能会得到如下函数 currying :
function currying (fn, ...args1) {
return function (...args2) {
return fn(...args1, ...args2)
}
}
var increment = currying(add, 1)
increment(2) === 3
// true
在此实现中,currying 函数的返回值其实是一个接收剩余参数并且返回计算值的函数,它的返回值并没有自动被 Currying化。
具体的说,如果需要 currying 化的函数的参数变成3个,按照这种写法
function add3 (x, y, z) {
return (x + y + z)
}
var increment = currying(add3, 1)
increment(2, 3) // 6
increment(2)(3) // increment(...) is not a function
因为 increment(2) 没有被 currying 化,所以会报错。而实际上我们希望increment(2)(3)结果等同于 increment(2, 3),这样需要利用函数递归改进方法。
function trueCurrying(fn, ...args) {
if (args.length >= fn.length) {
return fn(...args)
}
return function (...args2) {
return trueCurrying(fn, ...args, ...args2)
}
}
以上函数很简短,但是这个方法已经可以将js函数 Currying 化。然而 Currying 的概念和如何实现都不是最重要的,重点是:它能够解决编码和开发当中怎样的问题。
使用场景
先举个例子,有这么一大块数据
var data = {
result: "SUCCESS",
interfaceVersion: "1.0.3",
requested: "10/17/2013 15:31:20",
lastUpdated: "10/16/2013 10:52:39",
tasks: [
{id: 104, complete: false, priority: "high",
dueDate: "2013-11-29", username: "Scott",
title: "Do something", created: "9/22/2013"},
{id: 105, complete: false, priority: "medium",
dueDate: "2013-11-22", username: "Lena",
title: "Do something else", created: "9/22/2013"},
{id: 107, complete: true, priority: "high",
dueDate: "2013-11-22", username: "Mike",
title: "Fix the foo", created: "9/22/2013"},
{id: 108, complete: false, priority: "low",
dueDate: "2013-11-15", username: "Punam",
title: "Adjust the bar", created: "9/25/2013"},
{id: 110, complete: false, priority: "medium",
dueDate: "2013-11-15", username: "Scott",
title: "Rename everything", created: "10/2/2013"},
{id: 112, complete: true, priority: "high",
dueDate: "2013-11-27", username: "Lena",
title: "Alter all quuxes", created: "10/5/2013"}
// , ...
]
};
现在我们要处理它,找到用户 Scott 的所有未完成任务,并按到期日期升序排列。原文给了一段这样的方法。
getIncompleteTaskSummaries = function(membername) {
return fetchData()
.then(function(data) {
return data.tasks;
})
.then(function(tasks) {
var results = [];
for (var i = 0, len = tasks.length; i < len; i++) {
if (tasks[i].username == membername) {
results.push(tasks[i]);
}
}
return results;
})
.then(function(tasks) {
var results = [];
for (var i = 0, len = tasks.length; i < len; i++) {
if (!tasks[i].complete) {
results.push(tasks[i]);
}
}
return results;
})
.then(function(tasks) {
var results = [], task;
for (var i = 0, len = tasks.length; i < len; i++) {
task = tasks[i];
results.push({
id: task.id,
dueDate: task.dueDate,
title: task.title,
priority: task.priority
})
}
return results;
})
.then(function(tasks) {
tasks.sort(function(first, second) {
var a = first.dueDate, b = second.dueDate;
return a < b ? -1 : a > b ? 1 : 0;
});
return tasks;
});
};
getIncompleteTaskSummaries('Scott').then(r => console.log(r));
这段代码很长,但是很简单,这不就是我们经常做的事情,当然我觉得这段代码可以稍微改进一下,毕竟我们有了 ES6。
var getIncompleteTaskSummaries = function (membername) {
return Promise.resolve(data)
.then(data => {
return data.tasks;
})
.then(tasks => {
return tasks.filter((task) => {
return task.username === membername
});
})
.then(tasks => {
return tasks.filter((task) => {
return !task.complete
});
})
.then(tasks => {
return tasks.map((task) => {
return {
id: task.id,
dueDate: task.dueDate,
title: task.title,
priority: task.priority
}
});
})
.then(tasks => {
return tasks.sort((first, second) => {
return first.dueDate < second.dueDate ? -1
: first.dueDate > second.dueDate ? 1 : 0;
});
});
};
getIncompleteTaskSummaries('Scott').then(r => console.log(r));
好了,稍微好了一些,这基本上就是我们正常写的代码了。我们可以看到,我们每一次都是把我们要处理的数据加工一下,得到的结果送到下一个函数体内加工。如果按照函数式编程的思想,这些中间量其实都是不需要的。
那么现在我们要用一个叫 ramda 的js组件
const R = require("ramda");
var getIncompleteTaskSummaries = function(membername) {
return Promise.resolve(data)
.then(R.prop('tasks'))
.then(R.filter(R.propEq('username', membername)))
.then(R.reject(R.propEq('complete', true)))
.then(R.map(R.pick(['id', 'dueDate', 'title', 'priority'])))
.then(R.sortBy(R.prop('dueDate')));
};
getIncompleteTaskSummaries('Scott').then(r => console.log(r));
执行这段代码,结果和之前的一样,但是简洁程度不用多说了。
但是看完这个例子,仔细推敲其实就能明白柯里化到底做了什么。如果还是不清楚,仔细看一下 ramda 的源码,大多都是柯里化的函数。在 ramda 中随便挑一个命名为 prop 的函数感受一下:
R.prop('dueDate')
// 第一个参数p为'dueDate'
var prop = /*#__PURE__*/_curry2(function prop(p, obj) {
return path([p], obj);
});
function _curry2(fn) {
return function f2(a, b) {
switch (arguments.length) {
case 0:
return f2;
case 1:
return _isPlaceholder(a) ? f2 : _curry1(function (_b) {
return fn(a, _b);
});
default:
return _isPlaceholder(a) && _isPlaceholder(b) ? f2 : _isPlaceholder(a) ? _curry1(function (_a) {
return fn(_a, b);
}) : _isPlaceholder(b) ? _curry1(function (_b) {
return fn(a, _b);
}) : fn(a, b);
}
};
}
function _isPlaceholder(a) {
return a != null && typeof a === 'object' && a['@@functional/placeholder'] === true;
}
function _curry1(fn) {
return function f1(a) {
if (arguments.length === 0 || _isPlaceholder(a)) {
return f1;
} else {
return fn.apply(this, arguments);
}
};
}
//真正的处理业务的方法
var path = /*#__PURE__*/_curry2(function path(paths, obj) {
var val = obj;
var idx = 0;
while (idx < paths.length) {
if (val == null) {
return;
}
val = val[paths[idx]];
idx += 1;
}
return val;
});
ramda 代码都很简单,我觉得更像教科书,大家可以自己研究一下。
柯里化不是讳莫如深的东西,光看概念是不好理解,实际运用起来就是这么简单,原来还可以这么封装函数。
参考
JavaScript高级程序设计