【函数式编程】柯里化和偏函数

柯里化和偏函数

写在前面

本文的目的在于使用函数式编程解决常见问题,本文仅代表个人对函数式编程的一些粗浅认识,仅供参考,如有错漏,欢迎指出。

什么是柯里化

柯里化是把一个多参数函数转换为嵌套的单参数函数的过程

举个栗子
一个相加函数add = (x, y) => x + y,如果写成柯里化函数可以这样写:

function curryAdd(x) {
  return function (y) {
    return x + y;
  };
}
console.log("curryAdd(4)(5) :>> ", curryAdd(4)(5)); // 9

这里使用ES5的语法,是为了能更明显的体现函数嵌套的关系,当x=4时,返回一个匿名函数:

function (y) {
    return 4 + y
}
// 和curryAdd(4)是等价的

柯里化函数的通用定义

第一步 判断参数是否为函数

const curry = (fn) => {
  if (typeof fn !== "function") {
    throw Error("no function");
  }
};

第二步 传入参数并使用apply调用函数

const curry = (fn) => {
  if (typeof fn !== "function") {
    throw Error("no function");
  }
  return function curriedFn(...args) {
    return fn.apply(null, args);
  };
};

const multiply = (x, y, z) => x * y * z;

console.log(curry(multiply)(1,2,3)); // 6

相比第一步,增加了

  return function curriedFn(...args) {
    return fn.apply(null, args);
  };

通过curry(multiply)(1,2,3), args会指向[1,2,3],相当于multiply(1,2,3),结果为6

第三步 将多参数函数转换为单参数函数的柯里化函数

const curry = (fn) => {
  if (typeof fn !== "function") {
    throw Error("no function");
  }
  return function curriedFn(...args) {
    if (args.length < fn.length) {
      return function () {
        const as = args.concat([].slice.call(arguments));
        return curriedFn.apply(null, as);
      };
    }
    return fn.apply(null, args);
  };
};

相比第二步增加了

    if (args.length < fn.length) {
      return function () {
        const as = args.concat([].slice.call(arguments));
        return curriedFn.apply(null, as);
      };
    }
  };

说明一下:

  1. args.length < fn.length 判断传入参数的个数与函数参数个数是否一致,一致则直接调用函数
  2. args起到一个存储参数的作用,const as = args.concat([].slice.call(arguments)); 每进入一层嵌套函数,args都把arguments加进来,作为下一次嵌套函数的参数,直到传入参数的个数与函数参数个数是否一致
  3. curriedFn.apply(null, as),把前面的参数作为下一次嵌套函数的参数
const multiply = (x, y, z) => x * y * z;

console.log(curry(multiply)(1)(2)(3)); // 6

最后得到的结果就是每个嵌套函数的结果

柯里化在开发中应用

由已知函数创建新的函数

在数组中查找数字:

const arr = ["hello", "world", "123666"];
const hasNumber = (str) => str.match(/[0-9]+/); // 是否匹配数字
const fn = (fn, ary) => ary.filter(fn); // 筛选函数

const filter = curry(fn);
const match = curry(hasNumber);
const findNumInArr = filter(match);

console.log(findNumInArr(arr));

可以由简单的两个函数组合成一个新的函数findNumInArr,这里把数组作为最后一个参数传入,实际上是有意为之,开发中程序员经常处理数组一类的数据结构,把数组作为最后一个参数传入,能够很方便的创建如findNumInArr/findEvenInArr等可复用的函数。

固定部分函数参数

开发中可能会遇到这样的函数:

const logHelper = (type, position, message) => {
  console.log(type, position, message);
};

logHelper("ERROR", "Error at State.js", "this is a error.");
logHelper("WARN", "Warn at State.js", "this is a warn.");
logHelper("INFO", "Info at State.js", "this is a info.");

可以通过柯里化固定前两个参数,起到简洁代码的效果。

const errorLogger = curry(logHelper)("ERROR")("Error at State.js");
const warnLogger = curry(logHelper)("WARN")("Warn at State.js");
const infoLogger = curry(logHelper)("INFO")("Info at State.js");

errorLogger("this is a error");
warnLogger("this is a warn");
infoLogger("this is a info");
// ERROR Error at State.js this is a error.
// WARN Warn at State.js this is a warn.
// INFO Info at State.js this is a info.

偏函数

假如一个setTimeout函数:

setTimeout(() => {
  console.log("Hello World"), 1000;
});

按上面所讲,我们能隐藏1000这个时间吗?能用curry解决吗?答案是否定的,因为curry应用参数的方式是从左往右,一次处理一个参数,所以不能做到。
虽然我们可以像这样调换函数参数的顺序来变相实现,但实际不得不创建warpper这样的封装器,而这正是可以使用偏函数的地方。

const warpper = (time, fn) => {
  setTimeout(fn, time);
};

const delay = curry(warpper)(1000);
delay(() => console.log("Hello World"));

偏函数的通用定义

const partial = (fn, ...partialArgs) => {
  let args = partialArgs;

  return function (...fullArgs) {
    let arg = 0;

    for (let i = 0; i < args.length && arg < fullArgs.length; i++) {
      if (args[i] === undefined) {
        args[i] = fullArgs[arg++];
      }
    }
    return fn.apply(null, args);
  };
};

const delayOne = partial(setTimeout, undefined, 1000);
delayOne(() => console.log("Hello World"));

简单说明一下,通过闭包,我们获取到传入的参数args = [undefine, 1000],返回函数会记住args的值(是的,我们再次使用了闭包),fullArgs的值就是delayOne函数的参数

比较不好理解的就是以下这部分:

    for (let i = 0; i < args.length && arg < fullArgs.length; i++) {
      if (args[i] === undefined) {
        args[i] = fullArgs[arg++];
      }
    }

我们来逐步分析这段代码
当 i= 0时
args = [undefined, 1000]
fullArgs = [() => console.log(‘Hello World’)]

在if循环内
args[0] = fullArgs[0]
args = [() => console.log(‘Hello World’), 1000]

此时我们已经得到足够的参数执行方法
fn.apply(null, args);

setTimeout(() => console.log(‘Hello World’), 1000)

注意
partial函数有个bug,用不同的参数再次调用,都只会返回第一次的结果,原因是用参数替换undefined值从而修改partialArgs,而数组传递的只是引用。这里仅仅是讨论这种仅使用部分函数参数的思想,“原来可以这么玩”,实际开发中推荐使用lodash.partial方法。

总结

柯里化函数将多参数函数转换为嵌套的单参数函数,可以用来固定较多参数函数的一部分,用于简洁代码,有时候只需要用到前面参数和最后一个参数,中间参数处于未知状态,这正是偏函数应用的地方。

相关推荐

  1. 函数编程函数

    2024-03-23 17:38:03       40 阅读
  2. 函数 剖析

    2024-03-23 17:38:03       60 阅读
  3. 手写函数示例

    2024-03-23 17:38:03       23 阅读
  4. 函数(function currying)及部分求值

    2024-03-23 17:38:03       47 阅读
  5. Python之函数进阶-

    2024-03-23 17:38:03       37 阅读
  6. 一文了解什么是函数

    2024-03-23 17:38:03       41 阅读
  7. react diffing算法及函数

    2024-03-23 17:38:03       42 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-03-23 17:38:03       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-03-23 17:38:03       100 阅读
  3. 在Django里面运行非项目文件

    2024-03-23 17:38:03       82 阅读
  4. Python语言-面向对象

    2024-03-23 17:38:03       91 阅读

热门阅读

  1. 腾讯面试准备-2024.3.21

    2024-03-23 17:38:03       31 阅读
  2. Vanilla Transformer

    2024-03-23 17:38:03       30 阅读
  3. 【Docker】在 Ubuntu 上安装 Docker 的步骤

    2024-03-23 17:38:03       38 阅读
  4. Python从入门到精通秘籍十五

    2024-03-23 17:38:03       41 阅读
  5. C语言可变参函数

    2024-03-23 17:38:03       36 阅读
  6. jquery如何请求用ajax请求假数据

    2024-03-23 17:38:03       34 阅读
  7. SQL server 里对多行数据进行循环处理

    2024-03-23 17:38:03       42 阅读
  8. MySQL内存表和临时表的区别

    2024-03-23 17:38:03       37 阅读
  9. 最大中位数(c++题解)

    2024-03-23 17:38:03       41 阅读
  10. MySQL常用的聚合函数(比较常用滴~)

    2024-03-23 17:38:03       37 阅读
  11. 哈夫曼de树

    2024-03-23 17:38:03       47 阅读