掘金“最棒的”柯里化(curry)指南 | 函数式编程

2,405 阅读4分钟

在面试中经常会遇见面试官问你了解过柯里化吗?或者接触过高阶函数吗?亦或是能写一个curry工具函数把普通的函数变成高阶函数吗?不知道你们遇到这些问题是对答如流还是一脸懵逼,不过都没关系,看完这篇文章想必你一定能掌握柯里化相关的知识技能。

文章分三块,第一块讲柯里化的概念,第二块讲柯里化的实现,第三块举一个实际项目中的例子帮助大家了解其优点并运用柯里化。

柯里化的概念

概念:柯里化的概念只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。 例子: 下面实现一个输入dom元素数组返回元素子元素数组的函数。

  1. 正常
// Object.prototype.toString.call(elements) === "[object Array]"
function getChildren(elements){
    return elements.map(i => i.childNodes)
}
  1. curry
// 先假设我们实现了柯里化函数 curry, 下面会讲curry的实现细节
// curry: (* → a) → (* → a)
//function curry(fn: (any)=> any):(any)=>any{}
//var map = curry(function(f, ary) {
//  return ary.map(f);
//});
let getChildren = map(i => i.childNodes)

上面的2种实现方式的效果是一样的,第一种方式直接定义了操作数组的函数,第二种方式不直接定义操作数组的函数,而是调用map函数,内联i=>i.childNodes返回新的函数。

柯里化实现

下面介绍2种方式,第一种实现方式相比于第二种最明显的缺陷是经过柯里化后不能获取函数的参数的个数, 比如:

function a(b1, b2){}
console.log(a.length) // 2
// 柯里化后
let curryA = curry(a)
console.log(curryA.length) // 等下如果使用第一种实现,length = 0

第一种实现

function curry(fn){
    return function f(){
      const args = [].slice.call(arguments)
      if(args.length < fn.length){
          return function(){
          // 下面的arguments与上面的arguments不同
              return f.apply(this, args.concat([].slice.call(arguments)))
          }
      }else{
          return fn.apply(this, args)
      }
    }
}

第二种实现

为了弥补不能获取函数参数的长度需要借助一个辅助函数,辅助函数包裹目标函数,下面的实现参考了ramda的源码,剔除了占位符的功能,该辅助函数的参数只适用于 0 - 10 个,多于10个会报错。

// n为还需接收的参数
var _arity = function (n, fn) {
  /* eslint-disable no-unused-vars */
  switch (n) {
    case 0: return function() { return fn.apply(this, arguments); };
    case 1: return function(a0) { return fn.apply(this, arguments); };
    case 2: return function(a0, a1) { return fn.apply(this, arguments); };
    case 3: return function(a0, a1, a2) { return fn.apply(this, arguments); };
    case 4: return function(a0, a1, a2, a3) { return fn.apply(this, arguments); };
    case 5: return function(a0, a1, a2, a3, a4) { return fn.apply(this, arguments); };
    case 6: return function(a0, a1, a2, a3, a4, a5) { return fn.apply(this, arguments); };
    case 7: return function(a0, a1, a2, a3, a4, a5, a6) { return fn.apply(this, arguments); };
    case 8: return function(a0, a1, a2, a3, a4, a5, a6, a7) { return fn.apply(this, arguments); };
    case 9: return function(a0, a1, a2, a3, a4, a5, a6, a7, a8) { return fn.apply(this, arguments); };
    case 10: return function(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) { return fn.apply(this, arguments); };
    default: throw new Error('First argument to _arity must be a non-negative integer no greater than ten');
  }
}

下面是实现,具体可以看注释:

// curry使用_curry1柯里化,接收参数fn, 默认调用curryN, 
// curry = (fn) => curryN(fn.length, fn)
var curry = _curry1(function(fn) {
  return curryN(fn.length, fn);
});
// 如果是一个参数,使用_curry1, ramda里面的内部函数,接收的参数大多为1-3个
var _curry1 = function (fn) {
  return function f1(a) {
    if (arguments.length === 0) {
      return f1;
    } else {
      return fn.apply(this, arguments);
    }
  };
}
// 如果参数是2个
_curry2 = function(fn) {
    return function f2(a, b){
        switch(arguments.length){
            // 返回本身
            case 0: 
                return f2;
            // 返回_curry1的结果, 
            case 1: 
                return _curry1(function(_b) { return fn(a, _b); })
            // 直接调用
            case 2:
                return fn(a, b) 
        }
    }
}
// curryN本身也是一个柯里化的函数
var curryN = _curry2(function(n, fn){
  if (length === 1) {
    return _curry1(fn);
  }
  // 使用_artity包裹函数_curryN, _curryN包括3个参数,具体看下面的实现
  return _arity(length, _curryN(length, [], fn));
})
// 内部_curryN的实现, 在上面第一种方式,的基础上添加包裹函数
var _curryN = function (length, recived, fn) {
        return function() {
            // 获取函数调用的参数
            var args = [].slice.call(arguments);
            // 已传的参数
            var combined = recived.concat(args);
            
            if(combined.length < length ) {
                return _arity(length - combined.length, _curryN(length, combined, fn));
                
            } else {
                return fn.apply(this, combined);
            }
        }
    }

柯里化优点

优点:只传给函数一部分参数通常叫做局部调用(partial application),这样做的好处缓存参数,能够大量减少样板文件代码(boilerplate code)。(下面的例子中可以看到的)

在实际的项目中,我们调用后端的接口通常是一个函数对于一个接口,通过执行函数获得后端结果的返回数据,其中的问题就是每个函数的内容差不多,也就是样板代码多,比如下面这张图:

我们完成可以通过柯里化做成这样:

参考文档

  1. 基础知识,我参考了这里 js函数式编程指南
  2. ramda的源码解析,我参考了这篇掘金文章Ramda.js中的柯里化实现
  3. ramda的源码链接curry.js