聊聊柯里化

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

什么是柯里化

维基百科中有以下定义:git

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

举个例子,若是咱们实现一个三个数的加法函数,须要这么实现:编程

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

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

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要引入箭头函数了。ide

柯里化的用途

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

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

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前辈的一句话:尝试始终将你的逻辑拆解到最简,藉由组合和传播,你会得到更多的可能性。

参考连接

相关文章
相关标签/搜索