柯里化与反柯里化
最近在看一本书《JavaScript函数式编程》
里边提到了一个名词,柯里化(currying)
,阅读后发现在日常开发中经常会用到柯里化
函数。
以及还有他的反义词反柯里化(unCurrying)
柯里化
被称为部分计算函数,也就是会固定一部分参数,然后返回一个接收剩余参数的函数。目的是为了缩小适用范围,创建一个针对性更强的函数。反柯里化
正好与之相反,我们是要扩大一个函数的适用范围,比如将Array
独有的push
应用到一个Object
上去。
两种方案的通用代码实现
function currying (func, ...preArgs) {
let self = this
return function (...args) {
return func.apply(self, [].concat(preArgs, args))
}
}
function unCurrying (func) {
return function (reference, ...args) {
return func.apply(reference, args)
}
}
两种方案的简单示意
currying
foo(arg1, arg2)
// =>
foo(arg1)(arg2)
unCurrying
obj.foo(arg1, arg2)
// =>
foo(obj, arg1, arg2)
柯里化currying
一个柯里化函数的简单应用,我们有一个进行三个参数求和的函数。
我们可以调用currying
传入sum
获得sum1
,一个固定了第一个参数为10
的求和函数
然后我们又调用currying
传入sum1
获得sum2
,在原有的固定了一个参数的基础上,再次固定一个参数20
这时我们调用sum2
时仅需传入一个参数即可完成三个参数的求和:10 + 20 + n
let sum = (a, b, c) => a + b + c // 一个进行三个参数求和的函数
let sum1 = currying(sum, 10) // 固定第一个参数
console.log(sum1(1, 1)) // 12
console.log(sum1(2, 2)) // 14
let sum2 = currying(sum1, 20) // 固定第二个参数
console.log(sum2(1)) // 31
console.log(sum2(2)) // 32
帮助人理解currying
最简单的例子就是XXX.bind(this, yourArgs)()
写过React
的人应该都知道,在一些列表需要绑定事件时,我们大致会有这样的代码:
{
// ...
clickHandler (id) {
console.log(`trigger with: ${id}`)
},
render () {
return (<ul>
{this.state.data.map(item =>
<li onClick={this.clickHandler.bind(this, item.id)}>{item.name}</li>
)}
</li>)
}
}
这样我们就能在点击事件被触发时拿到对应的ID
了。这其实就是一个函数柯里化的操作
我们通过bind
生成了多个函数,每个函数都固定了第一个参数index
,然后第二个参数才是event
对象。
又或者我们有如下结构的数据,我们需要新增一列数据的展示description
,要求格式为所在部门-姓名
。
const data = [{
section: 'S1',
personnel: [{
name: 'Niko'
}, {
name: 'Bellic'
}]
}, {
section: 'S2',
personnel: [{
name: 'Roman'
}]
}]
如果用普通函数的处理方法,可能是这样的:
let result = data.map(sections => {
sections.personnel = sections.personnel.map(people => {
people.description = `${sections.section}-${people.name}`
return people
})
return sections
})
或者我们可以用currying
的方式来实现
let result = data.map(sections => {
sections.personnel = sections.personnel.map(currying((section, people) => {
people.description = `${section}-${people.name}`
return people
}, sections.section))
return sections
})
使用柯里化还有一种好处,就是可以帮助我们明确调用函数的参数。
我们创建一个如下函数,一个看似非常鸡肋的函数,大致作用如下:
- 接收一个函数
- 返回一个只接收一个参数的函数
function curry (func) {
return function (arg) {
return func(arg)
}
}
我们应该都用过一个全局函数parseInt
用来将String
转换为Number
parseInt('10') // 10
但其实,parseInt
不止接收一个参数。parseInt('10', 2) // 2
第二个参数可以用来标识给定值的基数,告诉我们用N
进制来处理这个字符串
所以当我们直接将一个parseInt
传入map
中时就会遇到一些问题:
['1', '2', '3', '4'].map(parseInt) // => 1, NaN, NaN, NaN
因为map
回调的返回值有三个参数当前item
、当前item对应的index
、调用map的对象引用
所以我们可以用上边的curry
函数来解决这个问题,限制parseInt
只接收一个参数
['1', '2', '3', '4'].map(curry(parseInt)) // => 1, 2, 3, 4
缩小适用范围,创建一个针对性更强的函数
反柯里化unCurrying
虽说名字叫反柯里化。。但是我觉得也只是部分理念上相反,而不是向
Math.max
和Math.min
,又或者[].pop
和[].push
这样的完全相反。
就像柯里化是缩小了适用范围,所以反柯里化所做的就是扩大适用范围。
这个在开发中也会经常用到,比如某宝有一个经典的面试题:
如何获取一个页面中所用到的所有标签,并将其输出?
// 普通函数的写法
let tags = []
document.querySelectorAll('*').forEach(item => tags.push(item.tagName))
tags = [...new Set(tags)] // => [a, b, div, ...]
因为qsa
返回的是一个NodeList
对象,一个类数组的对象,他是没有直接实现map
方法的。
而反柯里化
就是用来帮助它实现这个的,扩大适用范围,让一些原本无法调用的函数变得可用
let map = unCurrying([].map)
let tags = map(document.querySelectorAll('*'), item => item.tagName)
tags = [...new Set(tags)] // 其实可以合并到上边那一行代码去,但是这样看起来更清晰一些
又或者早期写JavaScript
时对arguments
对象的处理,这也是一个类数组对象。
比如一些早期版本的currying
函数实现(手动斜眼):
function old_currying () {
let self = this
let func = arguments[0]
let preArgs = [].slice.call(arguments, 1)
return function () {
func.call(self, [].concat(preArgs, arguments))
}
}
里边用到的[].slice.call
经过一层封装后,其实就是实现的unCurrying
的效果
网上流传的一个有趣的面试题
有大概这么一道题,如何实现下面的函数:
var a = func(1)
console.log(+a) // => 1
console.log(+a(2)) // => 3
console.log(+a(2)(3)) // 6
这里是一个实现的方案:https://github.com/Jiasm/notebook/blob/master/currying.js
一个柯里化实现的变体。
小记
在《JavaScript函数式编程》中提到了,高阶函数的几个特性:
- 以一个函数作为参数
- 以一个函数作为返回值
柯里化/反柯里化只是其中的一小部分。
其实柯里化还分为了向右柯里化
、向左柯里化
(大概就是preArgs
和args
的调用顺序问题了)
用函数构建出新的函数,将函数组合在一起,这个是贯穿这本书的一个理念,在现在大量的面向对象编程开发中,能够看到这么一本书,感觉很是清新。从另一个角度看待JavaScript
这门语言,强烈推荐,值得一看。
文章部分示例代码:https://github.com/Jiasm/currying-uncurrying