十、详解函数柯里【上】

简介: 柯里化是函数的一个高级应用,想要理解它并不简单。因此我一直在思考应该如何更加表达才能让大家理解起来更加容易。通过上一个章节的学习我们知道,接收函数作为参数的函数,都可以叫做高阶函数。我们常常利用高阶函数来封装一些公共的逻辑。这一章我们要学习的柯里化,其实就是高阶函数的一种特殊用法。

柯里化是函数的一个高级应用,想要理解它并不简单。因此我一直在思考应该如何更加表达才能让大家理解起来更加容易。


通过上一个章节的学习我们知道,接收函数作为参数的函数,都可以叫做高阶函数。我们常常利用高阶函数来封装一些公共的逻辑。


这一章我们要学习的柯里化,其实就是高阶函数的一种特殊用法。


柯里化是指这样一个函数(假设叫做createCurry),他接收函数A作为参数,运行后能够返回一个新的函数。并且这个新的函数能够处理函数A的剩余参数。


这样的定义不太好理解,我们可以通过下面的例子配合解释。


有一个接收三个参数的函数A。


function A(a, b, c) {
    // do something
}


假如,我们有一个已经封装好了的柯里化通用函数createCurry。他接收bar作为参数,能够将A转化为柯里化函数,返回结果就是这个被转化之后的函数。


var _A = createCurry(A);


那么_A作为createCurry运行的返回函数,他能够处理A的剩余参数。因此下面的运行结果都是等价的。


_A(1, 2, 3);
_A(1, 2)(3);
_A(1)(2, 3);
_A(1)(2)(3);
A(1, 2, 3);


函数A被createCurry转化之后得到柯里化函数_A,_A能够处理A的所有剩余参数。因此柯里化也被称为部分求值。


在简单的场景下,可以不用借助柯里化通用式来转化得到柯里化函数,我们凭借眼力自己封装。


例如有一个简单的加法函数,他能够将自身的三个参数加起来并返回计算结果。


function add(a, b, c) {
    return a + b + c;
}


那么add函数的柯里化函数_add则可以如下:


function _add(a) {
    return function(b) {
        return function(c) {
            return a + b + c;
        }
    }
}


下面的运算方式是等价的。


add(1, 2, 3);
_add(1)(2)(3);


当然,靠眼力封装的柯里化函数自由度偏低,柯里化通用式具备更加强大的能力。因此我们需要知道如何去封装这样一个柯里化的通用式。


首先通过_add可以看出,柯里化函数的运行过程其实是一个参数的收集过程,我们将每一次传入的参数收集起来,并在最里层里面处理。在实现createCurry时,可以借助这个思路来进行封装。


封装如下:


// 简单实现,参数只能从右到左传递
function createCurry(func, args) {
    var arity = func.length;
    var args = args || [];
    return function() {
        var _args = [].slice.call(arguments);
        [].push.apply(_args, args);
        // 如果参数个数小于最初的func.length,则递归调用,继续收集参数
        if (_args.length < arity) {
            return createCurry.call(this, func, _args);
        }
        // 参数收集完毕,则执行func
        return func.apply(this, _args);
    }
}


尽管我已经做了足够详细的注解,但是我想理解起来也并不是那么容易,因此建议大家用点耐心多阅读几遍。这个createCurry函数的封装借助闭包与递归,实现了一个参数收集,并在收集完毕之后执行所有参数的一个过程。


聪明的读者可能已经发现,把函数经过createCurry转化为一个柯里化函数,最后执行的结果,不是正好相当于执行函数自身吗?柯里化是不是把简单的问题复杂化了?


如果你能够提出这样的问题,那么说明你确实已经对柯里化有了一定的了解。柯里化确实是把简答的问题复杂化了,但是复杂化的同时,我们使用函数拥有了更加多的自由度。而这里对于函数参数的自由处理,正是柯里化的核心所在。


举一个非常常见的例子。


如果我们想要验证一串数字是否是正确的手机号,按照普通的思路来做,大家可能是这样封装,如下:


function checkPhone(phoneNumber) {
    return /^1[34578]\d{9}$/.test(phoneNumber);
}


而如果想要验证是否是邮箱呢?这么封装:


function checkEmail(email) {
    return /^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/.test(email);
}


我们还可能会遇到验证身份证号,验证密码等各种验证信息,因此在实践中,为了统一逻辑,我们就会封装一个更为通用的函数,将用于验证的正则与将要被验证的字符串作为参数传入。


function check(targetString, reg) {
    return reg.test(targetString);
}


但是这样封装之后,在使用时又会稍微麻烦一点,因为会总是输入一串正则,这样就导致了使用时的效率低下。


check(/^1[34578]\d{9}$/, '14900000088');
check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, 'test@163.com');


这个时候,我们就可以借助柯里化,在check的基础上再做一层封装,以简化使用。


var _check = createCurry(check);
var checkPhone = _check(/^1[34578]\d{9}$/);
var checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);


最后在使用的时候就会变得更加直观与简洁了。


checkPhone('183888888');
checkEmail('xxxxx@test.com');


经过这个过程我们发现,柯里化能够应对更加复杂的逻辑封装。当情况变得多变,柯里化依然能够应付自如。


虽然柯里化确实在一定程度上将问题复杂化了,也让代码更加不容易理解,但是柯里化在面对复杂情况下的灵活性却让我们不得不爱。


当然这个案例本身情况还算简单,所以还不能够特别明显的凸显柯里化的优势,我们的主要目的在于借助这个案例帮助大家了解柯里化在实践中的用途。


继续来思考一个例子。这个例子与map有关。在高阶函数的章节中,我们分析了封装map方法的思考过程。由于我们没有办法确认一个数组在遍历时会执行什么操作,因此我们只能将调用for循环的这个统一逻辑封装起来,而具体的操作则通过参数传入的形式让使用者自定义。这就是map函数。


但是,这是针对了所有的情况我们才会这样想。


实践中我们常常会发现,在我们的某个项目中,针对于某一个数组的操作其实是固定的,也就是说,同样的操作,可能会在项目的不同地方调用很多次。


于是,这个时候,我们就可以在map函数的基础上,进行二次封装,以简化我们在项目中的使用。假如这个在我们项目中会调用多次的操作是将数组的每一项都转化为百分比 1 --> 100%。


普通思维下我们可以这样来封装。


function getNewArray(array) {
    return array.map(function(item) {
        return item * 100 + '%'
    })
}
getNewArray([1, 2, 3, 0.12]);   // ['100%', '200%', '300%', '12%'];


而如果借助柯里化来二次封装这样的逻辑,则会如下实现:


function _map(func, array) {
    return array.map(func);
}
var _getNewArray = createCurry(_map);
var getNewArray = _getNewArray(function(item) {
    return item * 100 + '%'
})
getNewArray([1, 2, 3, 0.12]);   // ['100%', '200%', '300%', '12%'];
getNewArray([0.01, 1]); // ['1%', '100%']


如果我们的项目中的固定操作是希望对数组进行一个过滤,找出数组中的所有Number类型的数据。借助柯里化思维我们可以这样做。


function _filter(func, array) {
    return array.filter(func);
}
var _find = createCurry(_filter);
var findNumber = _find(function(item) {
    if (typeof item == 'number') {
        return item;
    }
})
findNumber([1, 2, 3, '2', '3', 4]); // [1, 2, 3, 4]
// 当我们继续封装另外的过滤操作时就会变得非常简单
// 找出数字为20的子项
var find20 = _find(function(item, i) {
    if (typeof item === 20) {
        return i;
    }
})
find20([1, 2, 3, 30, 20, 100]);  // 4
// 找出数组中大于100的所有数据
var findGreater100 = _find(function(item) {
    if (item > 100) {
        return item;
    }
})
findGreater100([1, 2, 101, 300, 2, 122]); // [101, 300, 122]


我采用了与check例子不一样的思维方向来想大家展示我们在使用柯里化时的想法。目的是想告诉大家,柯里化能够帮助我们应对更多更复杂的场景。


当然不得不承认,这些例子都太简单了,简单到如果使用柯里化的思维来处理他们显得有一点多此一举,而且变得难以理解。因此我想读者朋友们也很难从这些例子中感受到柯里化的魅力。不过没关系,如果我们能够通过这些例子掌握到柯里化的思维,那就是最好的结果了。在未来你的实践中,如果你发现用普通的思维封装一些逻辑慢慢变得困难,不妨想一想在这里学到的柯里化思维,应用起来,柯里化足够强大的自由度一定能给你一个惊喜。


当然也并不建议在任何情况下以炫技为目的的去使用柯里化,在柯里化的实现中,我们知道柯里化虽然具有了更多的自由度,但同时柯里化通用式里调用了arguments对象,使用了递归与闭包,因此柯里化的自由度是以牺牲了一定的性能为代价换来的。只有在情况变得复杂时,才是柯里化大显身手的时候。


相关文章
|
5月前
|
机器学习/深度学习
函数的使用
任务1 统计小组一门课程的总分及平均分。
28 1
|
7月前
|
程序员
函数
一、函数 函数是一段封装了特定功能的可重复使用的代码块。它接受输入参数,执行特定的操作,并返回一个结果。函数可以在程序中被多次调用,避免了重复编写相同的代码,提高了代码的复用性和可维护性。 函数通常具有以下几个特点: 1. 输入参数:函数可以接受零个或多个输入参数,用于传递数据给函数。输入参数可以是任意类型的数据,如整数、浮点数、字符串、数组等。函数可以使用输入参数来执行特定的操作。 2. 函数体:函数体是函数的核心部分,包含了函数要执行的操作。函数体是由一系列的语句组成的代码块,可以包含各种控制语句、变量声明、表达式等。函数体定义了函数的具体功能。 3. 返回值:函数可以返回一个结果给调用者
34 0
|
9月前
|
存储 编译器 C语言
C语言知识点之 函数
C语言知识点之 函数
32 0
|
自然语言处理 C++
C/C++ 中的 atol()、atoll() 和 atof() 函数
1.atol(): 此函数将作为参数传递给函数调用的 C 类型字符串转换为长整数。它解析 C 字符串 str 并将其内容解释为整数,该整数作为 long int 类型的值返回。该函数会丢弃字符串开头的空白字符,直到找到非空白字符。如果 C 字符串 str 中的非空白字符序列不是有效的整数,或者如果因为 str 为空或仅包含空白字符而不存在这样的序列,则不执行任何转换并返回零。
148 0
|
算法 编译器
函数(二)
函数(二)
58 0
函数(二)
|
编译器
【C++Primer】第6章:函数
【C++Primer】第6章:函数
【C++Primer】第6章:函数