如何理解柯里化|函数式编程

2,312 阅读5分钟

这里我们来谈论下函数式编程中另一个重要的概念,柯里化

首先,我们先通过下面的方式将上节代码中不纯的函数变成纯函数。就是将mini拿到函数内部去。

function checkAge (age) {
    let mini = 18;
    return age >= mini;
}

但是当我们把这个mini拿到函数内部的时候还有一个问题,因为这个变量的值等于一个具体的数字,就出现了硬编码,我们都知道,在写程序的时候要尽量避免硬编码。我们要解决硬编码也比较简单,只是需要把18提取到参数位置就可以了。

function checkAge (min, age) {
    return age >= min;
}

checkAge(18, 20);
checkAge(18, 21);
checkAge(18, 22);

这里我们就改造完了,我们根据输入始终会得到相同的输出,因为他不在依赖于外部的变量,并且它里面也没有硬编码。

可以发现,当我们经常使用18这个基准值的时候,这个18就会经常重复,我们想要避免这个18重复,我们可以使用闭包来解决这个问题。比如我们重新定义chekAge函数,他接收一个基准值min,返回一个函数。

返回的函数中接收一个age参数, 在函数体中我们返回age大于等于min,定义完之后,我们可以通过checkAge返回一个新的函数checkAge18,checkAge调用的时候就可以传入18。这个18就记录到了函数中。

function checkAge (min) {
    return function (age) {
        return age >= min;
    }
}
let checkAge18 = checkAge(18);

checkAge18(20);
checkAge18(21);
checkAge18(22);

这里可以发现我们在调用的时候不会让基准值重复,因为我们在第一个函数中已经确定下来了。

以上函数调用的方式就是柯里化,那我们这里简单说明一下什么是柯里化。

当我们的函数有多个参数的时候我们可以对这个函数进行改造,我们可以调用一个函数,只传递部分参数,并且让这个函数返回一个新的函数,这个新的函数去接收剩余的参数,并且返回相应的结果,这就是函数的柯里化。

上面的代码不够通用,我们这里介绍一下lodash中提供的通用柯里化方法,lodash中柯里化的方法叫做curry,这个方法的参数是一个函数,返回值是柯里化之后的函数。curry本身是一个纯函数,如果我们传入的参数是个纯函数的话,返回的函数也会是一个纯函数。

我们这里演示一下lodash中curry方法的使用。

我们这里定义一个求三个数和的函数, 柯里化可以将一个多元(参数个数)函数转换为一元函数。我们使用curried来接收柯里化之后的getSum方法。这个新得到的curried被调用时,当他判断传入的参数个数已经是需要的个数了,就会执行。我们可以一次性全部传入,也可以从前到后部分传入。当传入部分参数时,他会返回一个新的函数。

const _ = require('lodash');

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

const curried = _curry(getSum);

// curried(1, 2, 3);
// curried(1)(2, 3);
curried(1, 2)(3);

所以我们这里发现,我们通过柯里化过后的函数使用起来非常方便,他可以传递一个参数,可以传入多个参数。

下面我们来模拟一下lodash中柯里化的实现,在模拟之前我们先来回顾一下curry方法是如何调用的,当我们调用这个方法的时候,我们需要给他传入一个参数,这个参数是一个纯函数,当调用完成之后他会返回一个函数,那这个函数是柯里化之后的函数。

返回的柯里化函数在执行的时候,可以传递全部参数,也可以传递部分参数,当传递全部参数的时候,这个函数就要立即执行,当传递是部分参数的时候,会返回一个新的函数,然后等待接收剩余的参数。

这里我们知道,传递的参数是不固定的,所以我们在函数的内部就要判断一下传入的参数和形参的个数是否相同。我们可以通过ES6的reset剩余参数(..args)来实现。

然后我们需要把形参个数和实参个数进行对比,判断是否相同。实参就是args,形参可以通过函数名的长度获取,也就是func.length。

function curry (func) {
    return function curriedFn(...args) {
        if (args.length >= func.length) {
           return func(...args);
        } else {
            return function () {
                
            }
        }
    }
}

当传入部分参数的时候,我们需要将当前传入的参数和之前传入的参数合并到一起,然后与原函数的参数进行对比。

新传入的参数我们用...newArgs获取,以前传入的参数在...args中。这里我们可以将args和newArgs进行合并,然后手动调用curriedFn,让curriedFn帮我们判断参数是否相等的逻辑。

function curry (func) {
    return function curriedFn(...args) {
        if (args.length >= func.length) {
           return func(...args);
        } else {
            return function (...newArgs) {
                return curriedFn(...args.cancat(newArgs));
            }
        }
    }
}

这里我们就写完了,最后我们来总结一下函数的柯里化。

函数的柯里化可以让我们给一个函数传递较少的参数,得到一个已经记住了某些固定参数的新函数。也就是柯里化可以实现函数的参数分步传递,如果传递的参数不满足函数的参数要求,就会返回一个新的函数,可以在新的函数中继续传递后面的参数。前面传递的参数已经被记录在新函数里面了。

柯里化的内部使用了闭包对函数的参数进行了缓存,柯里化可以让函数变得更灵活,因为可以生成一些粒度更小的函数。我们这么做的目的是为了后续学习组合的时候使用。

使用柯里化可以把多元的函数转化成一元的函数,可以把这些一元函数组合成功能更强大的函数。