柯里化与反柯里化

柯里化与反柯里化

最近在看一本书《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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏快乐八哥

JavScript中的循环

循环知识 第一部分: 重复运行的代码就可以使用循环来解决。JavaScript的重复机制为循环(loop) for:适合重复动作已知次数的循环。 while:w...

23270
来自专栏测试开发架构之路

程序员面试50题(4)—把字符串转换成整数[算法]

题目:输入一个表示整数的字符串,把该字符串转换成整数并输出。例如输入字符串"345",则输出整数345。 分析:这道题尽管不是很难,学过C/C++语言一般都能实...

418100
来自专栏Play & Scala 技术分享

为Play初学者准备的Scala基础知识

37760
来自专栏C语言及其他语言

[每日一题]统计字符

这也是一道字符串类型中比较常规的题(但含自定义函数哦),但前提得知道一个函数哦,就会简单很多!!! 如果你不知道,写完这题你就知道了哦!!! 题目描述 编写一...

38980
来自专栏华章科技

从Zero到Hero,一文掌握Python关键代码

首先,什么是 Python?根据 Python 创建者 Guido van Rossum 所言,Python 是一种高级编程语言,其设计的核心理念是代码的易读性...

9230
来自专栏快乐八哥

JavaScript循环读书笔记

循环知识:自我重复的风险 第一部分: 重复运行的代码就可以使用循环来解决。JavaScript的重复机制为循环(loop) for:适合重复动作已知次数的循环。...

20870
来自专栏用户2442861的专栏

#define和typedef的用法与区别及面试问题

在C/C++语言中,typedef常用来定义一个标识符及关键字的别名,它是语言编译过程的一部分,但它并不实际分配内存空间,实例像:

91410
来自专栏决胜机器学习

PHP数据结构(二十二) ——快速排序

PHP数据结构(二十二)——快速排序 (原创内容,转载请注明来源,谢谢) 一、概述 前面的插入排序,都是以移动的方式进行排序。快速排序,则是以交换的方式进行...

40690
来自专栏较真的前端

一些你可能不知道的前端小技巧

21760
来自专栏积累沉淀

Python快速学习第七天

魔法方法、属性和迭代器 本文内容全部出自《Python基础教程》第二版 在Python中,有的名称会在前面和后面都加上两个下划线,这种写法很特别。...

36450

扫码关注云+社区

领取腾讯云代金券