• 函数式编程


    什么是函数式编程

    函数式编程是一种编程范式。

    编程范式又是什么?
    编程范式是一种解决问题的思路。

    • 命令式编程 把程序看作 一系列改变状态的指令
    • 函数式编程 把程序看作 一系列数学函数映射的组合
    i++; // 命令式 关心指令步骤
    [i].map(x => x + 1); // 函数式 关心映射关系
    

    函数式编程有什么好处

    • 易写易读:聚焦重要逻辑,摆脱了循环之类的底层工作。
    • 易复用:面向对象可复用单位是类,函数式可复用单位是函数,更小更灵活。
    • 易测:纯函数【后面有写】不依赖外部环境,测试起来准备工作少。

    函数式编程怎么学

    方法不难,念个数学博士,搞清楚范畴论、幺半群什么的就可以了。

    ../images/laugh.gif

    人生苦短,还是来点实际的吧。

    1. filtermapreduce:三板斧用好,从循环中解放出来。
    2. Pure Function:多写纯函数。
    3. composepipelinecurry:三个工具利用好,把纯函数像搭积木一样搭成想要的功能。

    1. 三板斧 filter,map,reduce

    例子:找出集合中的素数,求它们的平方和。

    命令式

    const isPrimeNumber = x => {
      if (x <= 1) return false;
    
      let testRangStart = 2,
        testRangeEnd = Math.floor(Math.sqrt(x));
    
      let i = testRangStart;
      while (i <= testRangeEnd) {
        if (x % i == 0) return false;
        i++;
      }
    
      return true;
    };
    
    const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    let sum = 0;
    
    for (let i = 0; i < arr.length; i++) {
      if (isPrimeNumber(arr[i])) {
        sum += arr[i] * arr[i];
      }
    }
    
    console.log(sum);
    

    函数式

    const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    const sum = arr
      .filter(isPrimeNumber)
      .map(x => x * x)
      .reduce((acc, cur) => acc + cur, 0);
    
    console.log(sum);
    

    for 循环没了,代码意图也更明显了。

    1. filter(isPrimeNumber) 找出素数。
    2. map(x => x * x) 变成平方。
    3. reduce((acc, cur) => acc + cur, 0) 求和。

    看着 是不是比命令式更清晰?

    isPrimeNumber 函数式写法也放出,没有了循环:

    // 输入范围,获得一个数组,例如 输入 1和5,返回 [1, 2, 3, 4, 5]
    const range = (start, end) =>
      start <= end
        ? [start, ...range(start + 1, end)]
        : [];
    const isPrimeNumber = x =>
      x >= 2 &&
      range(2, Math.floor(Math.sqrt(x))).every(
        cur => x % cur !== 0
      );
    

    有人说函数式效率不高。filtermapreduce 每次调用,内部都会遍历一次集合。而命令式只遍历了一次。

    函数式是更高级的抽象。它只声明解决问题的步骤,把性能优化的事情交给框架或者 runtime 来解决。

    • 框架

      • transducer 可以让集合只遍历一次。【TODO 写篇博客来介绍】
      • memoize 记录算过的,下次再算,直接拿上次的结果。【后面写纯函数的部分会给出实现】
    • runtime
      有的语言 map 是多线程运行的。代码不变,runtime 一优化,性能就大幅的提升了。而前面的命令式,就做不到这一点。


    2. 纯函数 - Pure Function

    一个函数满足下面 2 点要求就称为纯函数:

    1. 相同传参,返回值一定相同
    2. 函数调用不会对外界造成影响

    看个例子

    let name = "apolis";
    const greet = () => console.log("Hello " + name);
    
    greet();
    name = "kzhang";
    greet();
    

    greet 函数依赖外部变量 name。两次调用,相同传参【都不传参也是相同传参】输出不一样,所以它不是纯函数。

    const greet = name =>
      console.log("Hello " + name);
    

    这样能满足相同传参输出一样了。但再严格点,这个函数造影响了控制台 console,所以它还不是纯函数。

    const greet = name => "Hello " + name;
    

    这样才够纯,同时 greet 也摆脱了对控制台的依赖,可以适用的范围更广了。

    纯函数同样传参返回值一定相同,因此可以把算过的结果保存下来,下次调时,发现传的参数算过了,直接返回之前算的结果,提升效率。

    const memoize = fn => {
      let cache = {};
      return x => {
        if (cache.hasOwnProperty(x)) return cache[x];
        else {
          const result = fn(x);
          cache[x] = result;
          return result;
        }
      };
    };
    

    利用 memoize 函数,我们可以缓存纯函数的计算结果。

    三板斧的例子 filter 改一下就可以了。

    const sum = arr
      .filter(memoize(isPrimeNumber))
      .map(x => x * x)
      .reduce((acc, cur) => acc + cur, 0);
    
    console.log(sum);
    

    如果数组中包含重复元素,这样就能减少计算次数了。命令式写法要达到这个效果,改动就大得多了。


    三个工具 compose,pipeline,curry

    写了一堆 Pure Function,怎么把他们组合成想要的功能呢?

    composepipelinecurry 这三位该出场了。

    compose

    举个例子。

    const upperCase = str => str.toUpperCase();
    const exclaim = str => str + "!";
    const holify = str => "Holy " + str;
    

    现在需要一个 amaze 函数,字符串前面添加 Holy,后面添加叹号,全部转为大写。

    const amaze = str =>
      upperCase(exclaim(holify(str)));
    

    很不优雅对不对?

    看看 compose 怎么帮我们解决这个问题。

    const compose = (...fns) => x =>
      fns.reduceRight((acc, cur) => cur(acc), x);
    const amaze = compose(upperCase, exclaim, holify);
    console.log(amaze("functional programing"));
    

    这里用到了 reduceRight。它和 reduce 的区别就是数组是从后往前遍历的。compose 内的函数是从右往左运行的,也就是先 holifyexclaimupperCase

    看不惯从右往左运行?没事,还有一个 pipeline

    pipeline

    pipelinecompose 的区别就是换个方向:

    • composereduceRight
    • pipelinereduce
    const pipeline = (...fns) => x =>
      fns.reduce((acc, cur) => cur(acc), x);
    const amaze = pipeline(
      holify,
      exclaim,
      upperCase
    );
    console.log(amaze("functional programing"));
    

    curry

    上面 composepipeline 里的函数参数都只是一个,如果函数要传多个参数怎么办?

    解决办法就是用 curry,又叫做柯里化,把多参函数变成单参函数。

    const add = (x, y) => x + y;
    const multiply = (x, y) => x * y;
    

    这两个函数都需要传 2 个参数,现在我需要一个把数字先加 5 再乘 2 的函数。

    const add5ThenMultiplyBy2 = x =>
      multiply(add(x, 5), 2);
    

    很不好看,我们来 curry 一下再 compose 看看。

    怎么 curry
    把括号去掉,逗号变箭头就可以了。这样传入参数 x 时,返回一个函数,等待接收参数 y

    const add = x => y => x + y;
    const multiply = x => y => x * y;
    

    接下来,我们又可以用 compose 了。

    const add5ThenMultiplyBy2 = x =>
      compose(multiply(2), add(5));
    

    不过 curry 之后的 add 要这么调用了:

    add(2)(3);
    

    原先的调用方式 add(2, 3) 都得改掉了。

    不喜欢这个副作用?再奉上一个工具函数 curry

    const curry = fn => {
      const inner = (...args) => {
        if (args.length >= fn.length)
          return fn(...args);
        else
          return (...newArgs) =>
            inner(...args, ...newArgs);
      };
      return inner;
    };
    

    传入 fn 返回一个新函数,新函数调用时判断传入的参数个数有没有达到 fn 的要求。

    • 达到了,直接返回 fn 调用的结果;
    • 没达到,继续返回一个新新函数,记录着之前已传入的参数。
    const add = (x, y) => x + y;
    const curriedAdd = curry(add);
    

    这样两种调用方式都支持了。

    curriedAdd(2)(3);
    curriedAdd(2, 3);
    

    总结

    函数式是一种编程思维,声明式、更抽象。

    想入门可以从下面 3 点开始:

    1. filtermapreduce:三板斧用好,从循环中解放出来。
    2. Pure Function:多写纯函数。
    3. composepipelinecurry:三个工具利用好,把纯函数像搭积木一样搭成想要的功能。
  • 相关阅读:
    设计模式--17、建造者模式
    设计模式--16、原型模式
    面向对象的几大设计原则
    设计模式--15、模板方法模式
    设计模式--14、中介者模式
    设计模式--13、享元模式
    设计模式--12、外观模式
    设计模式--11、命令模式
    消息中间件ActiveMQ、RabbitMQ、RocketMQ、ZeroMQ、Kafka如何选型?
    Kafka,Mq,Redis作为消息队列有何差异?
  • 原文地址:https://www.cnblogs.com/apolis/p/9370847.html
Copyright © 2020-2023  润新知