函数柯里化

柯里化是什么

在计算机科学中,柯里化(Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

柯里化不会调用函数,它只是对函数进行转换。

函数柯里化的基本方法和函数绑定 bind() 一样(唯一不同是 bind 强制绑定了 context):使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入的参数。例如:一个函数从可调用的 fn(a, b, c) 转换成可调用的 fn(a)(b)(c)

柯里化的实现

首先看个简单的例子:

function curry (fn) {
    return function (a) {
        return function (b) {
            return fn(a, b)
        }
    }
}

function sum (a, b) {
    return a + b
}

var sumCurry = curry(sum)
console.log(sumCurry(2)(3)) // 5

分析:curry(sum) 执行结果是 function (a),当它被 sumCurry(2) 这样调用时,它的参数会被保存在词法作用域中(闭包),然后返回一个 function (b),接着这个函数被 sumCurry(3) 调用,并且,它将该调用传递给原始的 sum 函数。

其实上面的 curry() 可以优化成如下:

function curry (fn) {
    var args = [].slice.call(arguments, 1)
    return function () {
        var newArgs = args.concat([].slice.call(arguments))
        return fn.apply(this, newArgs)
    }
}

如果 sum(a, b, c) 携带 3 个参数呢?curry() 函数还要再返回 function (c)。如果 sum 携带 4、5 个参数呢?显然上面那种写法会严重影响阅读体验。为了解决这个问题,请看下面代码:

// help_curry() 是上面 curry() 优化后作为协助的函数
function help_curry (fn) {
    var args = [].slice.call(arguments, 1)
    // debugger;
    return function () {
        var newArgs = args.concat([].slice.call(arguments))
        // debugger;
        return fn.apply(this, newArgs)
    }
}

function curry (fn, len) {
    len = len || fn.length
    // debugger;
    return function () {
        if (arguments.length < len) {
            var unite = [fn].concat([].slice.call(arguments))
            // debugger;
            return curry(help_curry.apply(this, unite), len - arguments.length)
        } else {
            // debugger;
            return fn.apply(this, arguments)
        }
    }
}

var fn = curry(function (a, b, c) {
    return [a, b, c]
})

console.log(fn('a', 'b')('c')) // ["a", "b", "c"]

为了更好的理解上面逻辑,建议把我注释的 debugger 放开,在浏览器的调试面板查看。文中涉及到的 callapply 方法,如果不了解的,可以戳这里学习

注意

由于文中须要使用到函数的长度 fn.length,不建议使用 ES6 给参数定义默认值,因为会导致调用函数错误。详见在新窗口打开

柯里化的应用

现在我们已经了解了柯里化的定义以及如何实现,那它到底有什么用呢?

例如,我们有一个用于格式化和输出信息的日志(log)函数 log(date, importance, message),见下面示意代码:

function log (date, importance, message) {
    console.log(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`)
}

var log = curry(log) // 柯里化
var logNow = log(new Date())

logNow("WARN", "this is warn") // HH:mm WARN this is warn

var debugNow = logNow("DEBUG")
debugNow("this is debug") // HH:mm DEBUG this is debug

柯里化的这种用途理解为参数复用。

结语

这篇文章主要是认识函数柯里化的基本概念,代码实现和它的基础用途。这里虽然实现了 curry() 函数,但更像柯里化和偏函数的综合应用。(具体什么是偏函数,下一篇文章介绍)

柯里化是生于函数式编程,也服务于函数式编程,而 JavaScript 并非真正的函数式编程语言,相比 Haskell 等函数式编程语言,JavaScript 使用柯里化等函数式特性有额外的性能开销,也缺乏类型推导。

因此限制了柯里化在 JavaScript 实际项目中的普遍使用。

参考文献