编程语言
首页 > 编程语言> > 详解JavaScript函数式编程中的curry函数

详解JavaScript函数式编程中的curry函数

作者:互联网

curry函数在JavaScript函数式编程中十分重要。在网上搜索该函数,现有的基本上都不是我想要的;分析ramda,lodash等JavaScript函数式库,发现该函数的实现十分复杂,一时半会摸不清头绪。于是昨天晚上花了几个小时,自己实现了该函数,今天把它拿出来让大家参考一下。

柯里化函数

柯里化概念

所谓的柯里化就是把一个多参数的函数转换为一个嵌套的单参数函数的过程,它要求使用部分参数时返回一个新的函数,
在真正运行之前等待外部提供剩余参数,当参数准备完备后执行最初的多参数函数。

柯里化函数应该满足的要求

  1. 从柯里化的概念我们可以得出柯里化函数(即curry函数)应该满足以下要求:
    • curry函数接受一个多参数函数作为参数
    • curry调用后返回一个被柯里化的函数
    • 被柯里化的函数调用时一次可以传入一个参数,也可以传入多个参数
    • 被柯里化的函数调用后如果没有得到足够的参数,那么会返回一个新的函数
    • 被柯里化的函数调用后如果得到了足够的参数,那会就会执行最初的那个多参数函数。

一个简单的柯里化函数实例

  1. 知道柯里化的概念,和柯里化函数应满足的要求后,我们实现一个简单的柯里化函数,来为后面真正curry函数的实现打下基础。
  2. 假设我们有一个函数sum,它的作用是求两个数字的和。sum函数的实现如下
       const sum = (x, y) => x + y;
    
  3. 实现简单的curry函数:
        const curry = (fn) => {
            return function recursive(...args) {
                // 如果args.length >= fn.length则表明传入了足够的参数,此时调用fn并返回
                if (args.length >= fn.length) {
                    return fn(...args);
                }
        
                // 否则表明没有传入足够的参数,此时返回一个函数,用这个函数接受后面传递的新参数
                return (...newArgs) => {
                    // 递归调用recursive函数,并返回
                    return recursive(...args.concat(newArgs));
                };
            };
        };
    
  4. 将sum函数柯里化,并测试
        const cSum = curry(sum); // => [Function]
        const cSum1 = cSum(1); //=> [Function]
        cSum1(2);  // => 3
    
  5. 到此为止我们实现了一个简单的柯里化实例。那么我们用来柯里化sum的curry函数在执行过程中到底做了什么呢?
    • 先让我们打印输出一下console.log(cSum.toString());发现得到下面的结果:
         function recursive(...args) {
             // 如果args.length >= fn.length则表明传入了足够的参数,此时调用fn并返回
             if (args.length >= fn.length) {
                 return fn(...args);
             }
      
             // 否则表明没有传入足够的参数,此时返回一个函数,用这个函数接受后面传递的新的参数
             return (...newArgs) => {
                 // 递归调用recursive函数,并返回
                 return recursive(...args.concat(newArgs));
             };
         }
      
      于是我们可以知道执行curry(sum)返回了一个函数
    • 再让我们打印输出一下console.log(cSum(1).toString());发现得到下面的结果:
          (...newArgs) => {
                  // 递归调用recursive函数,并返回
                  return recursive(...args.concat(newArgs));
              }
      
      于是我们可以知道调用cSum(1)时参数没有准备完备,返回了一个函数,这个函数内部递归调用了recursive函数,并且recursive函数的参数是1和我们调用cSum1时传入的参数。
    • 当我们执行cSum1(2)得到了结果 3,这表明参数准备完备了,这时执行了下面的代码,从而得到了我们想要的结果。
          // 如果args.length >= fn.length则表明传入了足够的参数,此时调用fn并返回
          if (args.length >= fn.length) {
              return fn(...args);
          }
      

柯里化函数的实现

我们要实现什么?

上面我们实现了一个简单的curry函数,用来柯里化sum函数;但实际情况要复杂的多。我们要柯里化的函数的参数是不确定的,有时也希望能够通过占位符表示将要传递的参数;因此我们真正要实现的curry函数将满足以下两个条件:

实现真正的curry函数

一、占位符

  1. 这里我们通过 __(双下划线)来表示占位符,它的值是一个对象,具体如下所示:
    const __ = {'@@/placeHolder': true}; // curry  函数的占位符
    
  2. 判断一个标识符是否是占位符,当一个标识符不为null,且是一个对象,且对象中的'@@/placeHolder'属性全等于 true则认为它是一个占位符。通过函数isPlaceHolder来实现该功能:
    const isPlaceHolder = (p) => p !== null && typeof p === 'object' && p['@@/placeHolder'] === true;
    
  3. 测试isPlaceHolder函数:
    const __ = {'@@/placeHolder': true}; // curry  函数的占位符
    //----- 判断是否是占位符 -----
    const isPlaceHolder = (p) => p !== null && typeof p === 'object' && p['@@/placeHolder'] === true;
    
    isPlaceHolder(0);  // =>false
    isPlaceHolder('a'); // =>false
    isPlaceHolder('__'); // =>false
    isPlaceHolder(__); // =>true
    isPlaceHolder({
        name:'李四',
        age: 18,
        '@@/placeHolder': true,
    }); // => true
    
  4. 从上面的测试中可以看到当某个对象包含'@@/placeHolder': true, 时会被isPlaceHolder函数认为占位符,因此实际开发中需要注意

二、判断参数中是否含有占位符

  1. 因为有多个参数所以使用数组来存储这些参数,现在我们通过hasPlaceHolder来判断参数中是否含有占位符:
    const hasPlaceHolder = (args) => args.some((el) => isPlaceHolder(el));
    
  2. 分析hasPlaceHolder的实现可以发现,只要存储参数的args数组中有某个参数是占位符,则hasPlaceHolder调用后返回true,否则返回false。

三、判断参数是否准备完备

  1. 在上面柯里化const sum = (x, y) => x + y;的curry函数判断参数准备完备的条件是args.length >= fn.length即传入的参数个数大于等于fn.length
  2. 在这里判断参数是否准备完备要满足两个条件
    • 传入的参数前面的fn.length个参数中没有占位符
    • 传入的参数个数大于等于fn.length
  3. 通过函数isParametersReady来判断参数是否准备完备:
    const isParametersReady = (fn, args) => {
        const fnParametersLength = fn.length;
        const usefulParameters = args.slice(0, fnParametersLength);
        return hasPlaceHolder(usefulParameters) ? false : usefulParameters.length >= fnParametersLength;
    };
    

四、获取参数

  1. 多参数函数被柯里化后,实际的参数是多次传入的,同时可能会传入占位符,因此我们需要对传入的参数进行处理得到可用的参数。
  2. 传入的参数有两种情况
    • 前面传入的参数有占位符,这种情况下要用后面传入的参数中的第一个参数替换第一占位符,第二个参数替换第二个占位符,依次类推。如果还有多余的参数就将它拼接到最后面。
    • 前面传入的参数没有占位符,这种情况直接用前面的参数拼接上后面的参数即可
  3. 通过getParameters来获取参数,下面是它的实现:
    const getParameters = (oldArgs, newArgs) => {
        let params = []; // 存储经过处理后的参数
    
        // oldArgs中有占位符
        if (hasPlaceHolder(oldArgs)) {
            // 替换占位符
            let index = 0;  // 占位符的在所有占位符中的索引
            params = oldArgs.map((el) => {
                if (isPlaceHolder(el) && index < newArgs.length) {
                    index += 1;
                    return newArgs[index - 1];
                } else {
                    return el;
                }
            });
    
            // 将newArgs中多余的参数添加到params后面
            params.push(...newArgs.slice(index));
            return params;
        }
    
        // oldArgs中没有占位符
        params = oldArgs.concat(newArgs);
        return params;
    };
    
    

五、curry函数的实现

有了占位符,isPlaceHolderhasPlaceHolderisParametersReadygetParameters等函数我们就可以实现想要的curry函数了,下面是它的实现:

const curry = (fn) => {
    return function recursive(...args) {
        // 参数准备好了,调用fn并返回
        if (isParametersReady(fn, args)) {
            return fn(...args);
        }

        // 参数没有准备好,返回一个函数
        return (...newArgs) => recursive(...getParameters(args, newArgs));
    };
};

完整的代码及测试

const __ = {'@@/placeHolder': true}; // curry  函数的占位符

//----- 判断是否是占位符 -----
const isPlaceHolder = (p) => p !== null && typeof p === 'object' && p['@@/placeHolder'] === true;

//----- 判断参数中是否存在占位符 -----
const hasPlaceHolder = (args) => args.some((el) => isPlaceHolder(el));

//----- 判断参数是否准备好了 -----
const isParametersReady = (fn, args) => {
    const fnParametersLength = fn.length;
    const usefulParameters = args.slice(0, fnParametersLength);
    return hasPlaceHolder(usefulParameters) ? false : usefulParameters.length >= fnParametersLength;
};

//----- 获取参数 -----
const getParameters = (oldArgs, newArgs) => {
    let params = []; // 存储经过处理后的参数

    // oldArgs中有占位符
    if (hasPlaceHolder(oldArgs)) {
        // 替换占位符
        let index = 0;  // 占位符的在所有占位符中的索引
        params = oldArgs.map((el) => {
            if (isPlaceHolder(el) && index < newArgs.length) {
                index += 1;
                return newArgs[index - 1];
            } else {
                return el;
            }
        });

        // 将newArgs中多余的参数添加到params后面
        params.push(...newArgs.slice(index));
        return params;
    }

    // oldArgs中没有占位符
    params = oldArgs.concat(newArgs);
    return params;
};


//----- curry化函数 -----
const curry = (fn) => {
    return function recursive(...args) {
        // 参数准备好了,调用fn并返回
        if (isParametersReady(fn, args)) {
            return fn(...args);
        }

        // 参数没有准备好,返回一个函数
        return (...newArgs) => recursive(...getParameters(args, newArgs));
    };
};


//----- 测试curry -----
const sum = (x, y, z) => x + y + z;
const cSum = curry(sum);
cSum(1, __, __, 5)(2, __)(__, 3)(4);   // => 7
cSum('a')(__)('b')('c', 'd');          // 'abc'

const division = (a, b, c) => a / b / c;
const cDivision = curry(division);
cDivision(12, 2, 3);          // => 2              
cDivision(12, __, 3)(2);      // => 2          
cDivision(__, 2, 3)(12);      // => 2          
cDivision(12)(2)(3);          // => 2      
[子鱼] 发布了9 篇原创文章 · 获赞 0 · 访问量 143 私信 关注

标签:args,const,函数,JavaScript,占位,参数,curry,fn
来源: https://blog.csdn.net/weixin_43379230/article/details/104071842