柯里化与反柯里化

最近在看一本书《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.maxMath.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函数式编程》中提到了,高阶函数的几个特性:

  1. 以一个函数作为参数
  2. 以一个函数作为返回值

柯里化/反柯里化只是其中的一小部分。
其实柯里化还分为了向右柯里化向左柯里化(大概就是preArgsargs的调用顺序问题了)

用函数构建出新的函数,将函数组合在一起,这个是贯穿这本书的一个理念,在现在大量的面向对象编程开发中,能够看到这么一本书,感觉很是清新。从另一个角度看待JavaScript这门语言,强烈推荐,值得一看

文章部分示例代码:https://github.com/Jiasm/currying-uncurrying

参考资料

http://2ality.com/2011/11/uncurrying-this.html