简介
柯里化从何而来
柯里化, 即 Currying 的音译. Currying 是编译原理层面实现多参函数的一个技术.
在说 JavaScript 中的柯里化前, 可以聊一下原始的 Currying 是什么, 又从何而来.
在编码过程中, 身为码农的我们本质上所进行的工作就是 -- 将复杂问题分解为多个可编程的小问题.
Currying 为实现多参函数提供了一个递归降解的实现思路 -- 把接受多个参数的函数变换成接受一个单一参数 (最初函数的第一个参数) 的函数, 并且返回接受余下的参数而且返回结果的新函数, 在某些编程语言中(如 Haskell), 是通过 Currying 技术支持多参函数这一语言特性的.
所以 Currying 原本是一门编译原理层面的技术, 用途是实现多参函数.
柯里化去向哪里
在 Haskell 中, 函数作为一等公民, Currying 从编译原理层面的技术应运而成了一个语言特性. 在语言特性层面, Currying 是什么?
在Mostly adequate guide一书中, 这样总结了 Currying -- 只传递给函数一部分参数来调用它, 让它返回一个函数去处理剩下的参数.
所以 Currying 是应函数式编程而生, 在有了 Currying 后, 大家再去探索去发掘了它的用途及意义. 然后因为这些用途和意义, 大家才积极地将它扩展到其他编程语言中.
在 JavaScript 中实现 Currying
为了实现只传递给函数一部分参数来调用它, 让它返回一个函数去处理剩下的参数这句话所描述的特性. 我们先写一个实现加法的函数 add:
- function add (x, y) {
- return (x + y)
- }
现在我们直接实现一个被 Currying 的 add 函数, 该函数名为 curriedAdd, 则根据上面的定义, curriedAdd 需要满足以下条件:
- curriedAdd(1)(3) === 4
- // true
- var increment = curriedAdd(1)
- increment(2) === 3
- // true
- var addTen = curriedAdd(10)
- addTen(2) === 12
- // true
满足以上条件的 curriedAdd 的函数可以用以下代码段实现:
- function curriedAdd (x) {
- return function(y) {
- return x + y
- }
- }
当然以上实现是有一些问题的: 它并不通用, 并且我们并不想通过重新编码函数本身的方式来实现 Currying 化.
但是这个 curriedAdd 的实现表明了实现 Currying 的一个基础 -- Currying 延迟求值的特性需要用到 JavaScript 中的作用域 -- 说得更通俗一些, 我们需要使用作用域来保存上一次传进来的参数.
对 curriedAdd 进行抽象, 可能会得到如下函数 currying :
- function currying (fn, ...args1) {
- return function (...args2) {
- return fn(...args1, ...args2)
- }
- }
- var increment = currying(add, 1)
- increment(2) === 3
- // true
- var addTen = currying(add, 10)
- addTen(2) === 12
- // true
在此实现中, currying 函数的返回值其实是一个接收剩余参数并且立即返回计算值的函数.** 即它的返回值并没有自动被 Currying 化 **. 所以我们可以通过递归来将 currying 的返回的函数也自动 Currying 化.
- function trueCurrying(fn, ...args) {
- if (args.length>= fn.length) {
- return fn(...args)
- }
- return function (...args2) {
- return trueCurrying(fn, ...args, ...args2)
- }
- }
以上函数很简短, 但是已经实现 Currying 的核心思想了. JavaScript 中的常用库 Lodash 中的 curry 方法, 其核心思想和以上并没有太大差异 -- 比较多次接受的参数总数与函数定义时的入参数量, 当接受参数的数量大于或等于被 Currying 函数的传入参数数量时, 就返回计算结果, 否则返回一个继续接受参数的函数.
Lodash 中实现 Currying 的代码段较长, 因为它考虑了更多的事情, 比如绑定 this 变量等. 在此处就不直接贴出 Lodash 中的代码段, 感兴趣的同学可以去看看看 Lodash 源码, 比较一下这两种实现会导致什么样的差异.
然而 Currying 的定义和实现都不是最重要的, 本文想要阐述的重点是: 它能够解决编码和开发当中怎样的问题, 以及在面对不同的问题时, 选择一个合适的 Currying, 来最恰当的解决问题.
Currying 使用场景
参数复用
固定不变的参数, 实现参数复用是 Currying 的主要用途之一.
上文中的 increment, addTen 是一个参数复用的实例. 对 add 方法固定第一个参数为 10 后, 改方法就变成了一个将接受的变量值加 10 的方法.
延迟执行
延迟执行也是 Currying 的一个重要使用场景, 同样 bind 和箭头函数也能实现同样的功能.
在前端开发中, 一个常见的场景就是为标签绑定 onClick 事件, 同时考虑为绑定的方法传递参数.
以下列出了几种常见的方法, 来比较优劣:
通过 data 属性
<div data-name="name" onClick={handleOnClick} />
通过 data 属性本质只能传递字符串的数据, 如果需要传递复杂对象, 只能通过
JSON.stringify(data)
来传递满足 JSON 对象格式的数据, 但对更加复杂的对象无法支持.(虽然大多数时候也无需传递复杂对象)
通过 bind 方法
<div onClick={handleOnClick.bind(null, data)} />
bind 方法和以上实现的 currying 方法, 在功能上有极大的相似, 在实现上也几乎差不多. 可能唯一的不同就是 bind 方法需要强制绑定 context, 也就是 bind 的第一个参数会作为原函数运行时的 this 指向. 而 currying 不需要此参数. 所以使用 currying 或者 bind 只是一个取舍问题.
箭头函数
<div onClick={() => handleOnClick(data))} />
箭头函数能够实现延迟执行, 同时也不像 bind 方法必需指定 context. 可能唯一需要顾虑的就是在 react 中, 会有人反对在 jsx 标签内写箭头函数, 这样子容易导致直接在 jsx 标签内写业务逻辑.
通过 currying
<div onClick={currying(handleOnClick, data)} />
性能对比
通过 jsPerf 测试四种方式的性能, 结果为: 箭头函数 > bind>currying>trueCurrying.
currying 函数相比 bind 函数, 其原理相似, 但是性能相差巨大, 其原因是 bind 由浏览器实现, 运行效率有加成.
从这个结果看 Currying 性能无疑是最差的, 但是另一方面就算最差的 trueCurrying 的实现, 也能在本人的个人电脑上达到 50w Ops/s 的情况下, 说明这些性能是无需在意的.
而 trueCurrying 方法中实现的自动 Currying 化, 是另外三个方法所不具备的.
到底需不需要 Currying
为什么需要 Currying
为了多参函数复用性
Currying 让人眼前一亮的地方在于, 让人觉得函数还能这样子复用.
通过一行代码, 将 add 函数转换为 increment,addTen 等.
对于 Currying 的复杂实现中, 以 Lodash 为列, 提供了 placeholder 的神奇操作. 对多参函数的复用玩出花样.
- import _ from 'loadsh'
- function abc (a, b, c) {
- return [a, b, c];
- }
- var curried = _.curry(abc)
- // Curried with placeholders.
- curried(1)(_, 3)(2)
- // => [1, 2, 3]
为函数式编程而生
Currying 是为函数式而生的东西. 应运着有一整套函数式编程的东西, 纯函数, compose,container 等等事物.(可阅读mostly-adequate-guide https://legacy.gitbook.com/book/llh911001/mostly-adequate-guide-chinese/details )
假如要写 Pointfree Javascript http://lucasmreis.github.io/blog/pointfree-javascript/ 风格的代码, 那么 Currying 是不可或缺的.
要使用 compose, 要使用 container 等事物, 我们也需要 Currying.
为什么不需要 Currying
Currying 的一些特性有其他解决方案
如果我们只是想提前绑定参数, 那么我们有很多好几个现成的选择, bind, 箭头函数等, 而且性能比 Curring 更好.
Currying 陷于函数式编程
在本文中, 提供了一个 trueCurrying 的实现, 这个实现也是最符合 Currying 定义的, 也提供 了 bind, 箭头函数等不具备的 "新奇" 特性 -- 可持续的 Currying(这个词是本人临时造的).
但是这个 "新奇" 特性的应用并非想象得那么广泛.
其原因在于, Currying 是函数式编程的产物, 它生于函数式编程, 也服务于函数式编程.
而 JavaScript 并非真正的函数式编程语言, 相比 Haskell 等函数式编程语言, JavaScript 使用 Currying 等函数式特性有额外的性能开销, 也缺乏类型推导.
从而把 JavaScript 代码写得符合函数式编程思想和规范的项目都较少, 从而也限制了 Currying 等技术在 JavaScript 代码中的普遍使用.
假如我们还没有准备好去写函数式编程规范的代码, 仅需要在 JSX 代码中提前绑定一次参数, 那么 bind 或箭头函数就足够了.
结论
Currying 在 JavaScript 中是 "低性能" 的, 但是这些性能在绝大多数场景, 是可以忽略的.
Currying 的思想极大地助于提升函数的复用性.
Currying 生于函数式编程, 也陷于函数式编程. 假如没有准备好写纯正的函数式代码, 那么 Currying 有更好的替代品.
函数式编程及其思想, 是值得关注, 学习和应用的事物. 所以在文末再次安利 JavaScript 程序员阅读此书 -- mostly-adequate-guide https://legacy.gitbook.com/book/llh911001/mostly-adequate-guide-chinese/details
参考链接
柯里化 -- 维基百科 https://zh.wikipedia.org/zh/柯里化
JS 函数式编程指南 https://legacy.gitbook.com/book/llh911001/mostly-adequate-guide-chinese/details
Pointfree 编程风格指南 -- 阮一峰 http://www.ruanyifeng.com/blog/2017/03/pointfree.html
来源: https://juejin.im/post/5af13664f265da0ba266efcf