资讯专栏INFORMATION COLUMN

聊聊柯里化

yankeys / 3501人阅读

摘要:举个例子,如果我们实现一个三个数的加法函数,需要这么实现如果我们将其柯里化变换成接受一个单一参数的函数,并且返回接受余下的参数而且返回结果的新函数,我们的调用方式应该是这样的。

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

什么是柯里化

维基百科中有如下定义:

在计算机科学中,柯里化(英语: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!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/89447.html

相关文章

  • Javascript中的柯里

    摘要:三使用场景场景性能优化可以将一些模板代码通过柯里化的形式预先定义好,例如这段代码的作用就是根据浏览器的类型决定事件添加的方式。场景扩展能力中的方法,就是通过柯里化实现的四总结通过本文的介绍,相信你对柯里化已经有一个全新的认识了。 欢迎关注我的公众号睿Talk,获取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 柯里化...

    HtmlCssJs 评论0 收藏0
  • 函数式 js 接口实现原理,以及 lodash/fp 模块

    摘要:函数式接口之前在上看到一个技术视频,讲的接口为什么不好用,以及什么样的接口更好用。演讲者是的作者,他提出了一种全面函数式的接口设计模式。言归正传,今天聊聊这样的接口如何实现,以及中的模块。 函数式 js 接口 之前在 youtube 上看到一个技术视频,讲underscore.js的接口为什么不好用,以及什么样的接口更好用。演讲者是 lodash.js 的作者,他提出了一种全面函数式的...

    asce1885 评论0 收藏0
  • 【进阶 6-2 期】深入高阶函数应用之柯里

    摘要:引言上一节介绍了高阶函数的定义,并结合实例说明了使用高阶函数和不使用高阶函数的情况。我们期望函数输出,但是实际上调用柯里化函数时,所以调用时就已经执行并输出了,而不是理想中的返回闭包函数,所以后续调用将会报错。引言 上一节介绍了高阶函数的定义,并结合实例说明了使用高阶函数和不使用高阶函数的情况。后面几部分将结合实际应用场景介绍高阶函数的应用,本节先来聊聊函数柯里化,通过介绍其定义、比较常见的...

    stackvoid 评论0 收藏0
  • 开开心心做几道JavaScript机试题 - 01

    摘要:碰到这种面试官,你只有是个题霸,再加上眼缘够才能顺利入围。只要按照我题目的思路,甚至打出来测试用例看看,就能实现这个题目了。答案根据的,对答案做出修正。另我的答案绝不敢称最佳,随时欢迎优化修正。但了解总归是好的。 我们在长期的面试过程中,经历了种种苦不堪言,不诉苦感觉不过瘾(我尽量控制),然后主要聊聊常见JavaScript面试题的解法,以及面试注意事项 忆苦 面试第一苦,面试官的土 ...

    liujs 评论0 收藏0
  • JavaScript专题系列20篇正式完结!

    摘要:写在前面专题系列是我写的第二个系列,第一个系列是深入系列。专题系列自月日发布第一篇文章,到月日发布最后一篇,感谢各位朋友的收藏点赞,鼓励指正。 写在前面 JavaScript 专题系列是我写的第二个系列,第一个系列是 JavaScript 深入系列。 JavaScript 专题系列共计 20 篇,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里...

    sixleaves 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<