聊聊柯里化

仅以此文献给我的学弟 誅诺_弥 ,并将逐风者的祝福送给他:
英雄,愿你有一份无悔的爱情!

什么是柯里化

维基百科中有如下定义:

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

举个例子,如果我们实现一个三个数的加法函数,需要这么实现:

function add(a, b, c) {
    return a + b + c;
}
add(1, 2, 3);   // 6

如果我们将其柯里化(变换成接受一个单一参数的函数,并且返回接受余下的参数而且返回结果的新函数),我们的调用方式应该是这样的。

add(1)(2)(3);   // 6

注意到在接受最后一个参数前,柯里化后的函数返回值都是函数,因此我们实现如下:

function add(a) {
    return function (b) {
        return function (c) {
            return a + b + c;
        }
    }
}
add(1)(2)(3);   // 6

这样我们就实现了一个柯里化的add函数。由于ES6中引入了箭头函数,我们可以将上面的add实现成这样:

const add = a => b => c => a + b + c;

我想你大概知道为什么ES6要引入箭头函数了。

柯里化的用途

就目前我们知道的来看,柯里化仅仅是修改了一下函数参数的传入方式或者说函数的调用方式,那么有什么用呢?

考虑下面这个求两数相除余数的函数:

const modulo = divisor => dividend => dividend % divisor;
modulo(3)(9);   // 0

有个这个函数,我们现在能够很轻松的写出判断一个数是奇数还是偶数的函数:

const isOdd = modulo(2);

isOdd(6);   // 0
isOdd(5);   // 1

如果你没有布尔值一定要用true、false的强迫症的话,这是一个很不错的方案:)

接下来我们来实现下面这个需求:给定一个由数字构成的数组,获取里面所有的奇数,怎么办呢?

先准备一个filter函数:

const filter = condition => arr => arr.filter(condition);

然后实现我们的getTheOdd:

const getTheOdd = filter(isOdd);

getTheOdd([1, 2, 3, 4, 5]);     // [1, 3, 5]

到这里我说一下我的理解,柯里化的函数有这样一种能力:组合。将简单的函数组合起来实现更复杂的功能,一方面能够更好的复用你的代码,另一方面能够培养一种对代码拆解的直觉。

就像我们在实现函数节流的时候(比如onscroll事件处理函数频繁调用问题这样的场景),常常会使用throttle包一下处理函数一样,拆解代码并进行组合往往能给我们带来更多的价值。

接下来我们来看这个例子:

// 该函数接收一个数组,返回该数组元素倒序后的数组
const reverse = arr => arr.slice().reverse();
// 该函数接收一个数组,返回数组的第一个元素
const first = arr => arr[0];
// 基于上面两个函数我们可以轻松实现获取数组最后一个元素的函数
const last = arr => {
    const reversed = reverse(arr);
    return first(arr);
};

在提供函数式编程能力的JavaScript库中,通常都会有一个用于组合的实现组合的函数:compose。我们可以用它来让前一个例子更加函数式:

const compose = (...funcs) => {
    if (funcs.length === 0) {
        return arg => arg;
    }

    if (funcs.length === 1) {
        return funcs[0];
    }

    return funcs.reduce((a, b) => (...args) => a(b(...args)));
};

const last = compose(first, reverse);

从组合到传播

考虑以下场景,我们有一个执行很慢的函数(记为slowFunc),我们希望能够对它的值进行缓存,以此提高性能,怎么办呢?相信很多人都会想到memoize函数,我们在下面给一个相对简洁的实现,参考:https://github.com/reactjs/re...

const slowFuncWithCache = memoize(slowFunc);

function memoize(fn) {
    let lastArgs = null;
    let lastResult = null;

    return (...args) => {
        if (!isAllArgsEqual(args, lastArgs)) {
            lastArgs = args;
            lastResult = fn(...args);
        }
        return lastResult;
    };
}

function isAllArgsEqual(prev, next) {
    if (prev === null || next === null || prev.length !== next.length) {
        return false;
    }

    for (let i = 0; i < prev.length; i++) {
        if (prev[i] !== next[i]) {
            return false;
        }
    }

    return true;
}

回看我们前面介绍compose时使用的例子。

const reverse = arr => arr.slice().reverse();

const first = arr => arr[0];

const last = compose(first, reverse);

如果我们现在有一个需求,需要给last加一个缓存,怎么办?你可能直接就想到了这样:

const last = memoize(compose(first, reverse));

不过其实我们还可以这样:

const last = compose(memoize(first), memoize(reverse));

在last内层的first和reverse,在经过memoize处理获得缓存的能力后,也让last获得了缓存的能力。这就是组合的传播

绝不觉得这很熟悉?让我们回顾一下小学数学的知识:

a * (b + c) = a * b + a * c

组合的这种传播特性给了我们一种新的思路:如果我们要实现一个大系统的数据缓存功能,不妨试着将系统中的每一步计算都加上缓存进行处理,如果每一步都进行了计算上的缓存,那么最终这个系统一定是带有缓存能力的。

结语

从认识柯里化,到利用柯里化的能力去组合我们的更复杂的逻辑,再到把内部组合的出功能传播到外层,这是一种化繁为简,以简解繁的方法。在此借用otakustay前辈的一句话:尝试始终将你的逻辑拆解到最简,藉由组合和传播,你会获得更多的可能性。

参考链接

  • Hey Underscore, You're Doing It Wrong!

你可能感兴趣的:(currying,javascript)