函数式编程

275 阅读8分钟

概述

  1. 介绍函数式编程的基本概念、优势以及在项目中的应用
  2. 目标:带领大家认识函数式编程,了解函数式编程的基本使用规则,能够在项目中应用,拓展大家思考问题的方向
  3. 问题:
    1. 论证用函数式编程的好处在哪儿
    2. 比封装普通函数优势在哪儿
    3. 对加入新功能和修改bug的提效是否有帮助
    4. 函数式编程 - 代码逻辑复用相关
    5. react中的函数式编程

函数式编程介绍

我们常见的编程范式有哪些

💡 特征,优缺点,在项目中的应用程度

面向过程编程

  1. Demo

    const a = 1
    const b = 2
    const add = a + b
    
  2. 优点

    1. 一目了然,容易理解
    2. 命令式编程是一种编程风格,程序员通过告诉计算机他们想要什么来告诉计算机该做什么。然后计算机必须弄清楚如何产生结果。
    3. 命令式编程产生了我们每天使用的许多结构:控制流(if-else)、算术运算符(+-*/)、比较运算符(><)和逻辑运算符(===、!==)
  3. 缺点

    1. 编写大型项目和复杂的逻辑时,代码会变得非常冗余,容易产生bug,且维护成本高
    2. 复用性差

面向对象编程

  1. Demo

    class Calculator{
        add(a,b){
            return a + b
        }
    }
    const c = new Calculator()
    c.add(1,2)
    
  2. 优点

    1. OOP(Object Oriented Programming)
  3. 缺点

    1. 更多的代码量,对于简单的函数实现来说不够简洁

什么是函数式编程

💡 特征,优缺点,对函数式编程的印象

定义

wiki

函数式编程是一种编程范式,它将电脑运算视为函数运算,并且避免使用程序状态以及易变对象。其中,λ演算是该语言最重要的基础。而且λ演算的函数可以接受函数作为输入的参数和输出的返回值。

转换为我们自己的理解

  1. 纯函数
  2. 函数可以作为参数传递
  3. 数据不可变

概念

函数的元

  1. 不论使用何种方式去构造一个函数,这个函数都有两个固定的信息可以获取:

    • name 表示当前标识符指向的函数的名字。
    • length 表示当前标识符指向的函数定义时的参数列表长度。
    // 一元函数 x
    const one = a => a
    // 二元函数 x+y
    const two = (a,b) => a+b
    // 三元函数 x+y+z
    const three = (a,b,c) => a+b+c
    
  2. 定义函数的元的意义在于,我们可以对函数进行归类,并且可以明确一个函数需要的确切参数个数。函数的元在编译期(类型检查、重载)和运行时(异常处理、动态生成代码)都有重要作用。

柯里化(函数元的降维技术)

  1. 柯里化(Currying)是一种将接受多个参数的函数转换成一系列接受一个参数的函数的技术。是利用closure把函数的直接调用变成延迟调用的偏函数(不完全执行)

  2. demo

    const add = (a,b,c) => a+b+c
    const curryAdd = curry(add)
    curryAdd(1)(2)(3)
    
  3. 作用:

    1. 更加灵活
    2. 更加可组合
    3. 更加方便调试

函子(Functor)

  1. 函子(Functor)是一种范畴论中的概念,它是一个封装了值(value)的容器,并提供了一些操作来对容器内的值进行变换的抽象。函子通过一个 map 方法来对容器内的值进行变换并返回一个新的函子
  2. 函子的特点是具有可组合性和纯函数式的性质,它们可以在不改变原有的结构的情况下对值进行操作和转换,从而方便地组合多个变换操作,达到更加复杂的操作目的。
  3. 一些常用的函子
    1. Maybe 函子用于表示可能为空的值,它通过封装一个值来避免出现空指针异常。Maybe 函子的 map 方法会将传入的函数应用于封装的值,如果值存在,则返回变换后的值的 Maybe 函子,如果值不存在,则返回一个空的 Maybe 函子。

      class Maybe {
        constructor(val) {
          this.val = val;
        }
        
        map(f) {
          return this.val != null ? new Maybe(f(this.val)) : new Maybe(null);
        }
      }
      
      const maybeValue = new Maybe(5);
      const newValue = maybeValue.map(x => x * 2); // 返回封装了新值的 Maybe 函子
      const nullValue = new Maybe(null);
      const nullNewValue = nullValue.map(x => x * 2); // 返回一个空的 Maybe 函子
      
    2. Either 函子用于表示两个可能的值中的任意一个。Either 函子的实例有两种情况,一种是 Left 函子,表示错误或异常情况,另一种是 Right 函子,表示正常情况。Either 函子的 map 方法会对 Right 函子的值应用传入的函数,对 Left 函子的值不做任何操作。

      class Either {
        constructor(val) {
          this.val = val;
        }
        
        static left(val) {
          return new Left(val);
        }
        
        static right(val) {
          return new Right(val);
        }
        
        map(f) {
          return this.val instanceof Left ? this : Either.right(f(this.val));
        }
      }
      
      class Left extends Either {
        map() {
          return this;
        }
      }
      
      class Right extends Either {}
      
      const eitherValue = Either.right(5);
      const newValue = eitherValue.map(x => x * 2); // 返回封装了新值的 Right 函子
      const eitherError = Either.left("error");
      const newEitherError = eitherError.map(x => x * 2); // 返回原始的 Left 函子
      
    3. IO 函子用于封装具有副作用(如读取文件、发送请求等)的函数,使得这些函数的执行时机可以被延迟,并且可以方便地组合和重用。IO 函子的 map 方法会对函数进行包装,而不会立即执行。

      class IO {
        constructor(f) {
          this.f = f;
        }
        
        static of(x) {
          return new IO(() => x);
        }
        
        map(f) {
          return new IO(() => f(this.f()));
        }
        
        chain(f) {
          return f(this.f());
        }
      }
      
      const ioValue = IO.of(5);
      const newValue = ioValue.map(x => x * 2); // 返回封装了新值的 IO 函子
      const ioAction = new IO(() => console.log("Hello, world!"));
      const newIoAction = ioAction.map(x => x.toUpperCase()); // 返回封装了函数的 IO 函子
      
    The Principles of Functional Programming

单子(Monad)

Functors, Applicatives, And Monads In Pictures - adit.io

pointFree(无参风格)

  1. Pointfree 的本质就是使用一些通用的函数组合出各种复杂运算。上层运算不要直接操作数据,而是通过底层函数去处理。这就要求,将一些常用的操作封装成函数。

  2. 例子可以看文章

    Pointfree 编程风格指南 - 阮一峰的网络日志

  3. demo

    functional programming - CodeSandbox

  4. 作用

    • 它使程序更简单更简洁
    • 它使算法更清晰。通过只关注被组合的函数,我们可以更好地了解正在发生的事情,而不会受到数据参数的阻碍。
    • 它迫使我们更多地考虑正在完成的转换,而不是正在转换的数据
    • 它帮助我们将函数视为可以处理不同类型数据的通用构建块,而不是将它们视为对特定类型数据的操作。通过给数据命名,我们可以确定我们可以在哪里使用我们的函数的想法。通过将数据参数排除在外,它使我们更具创造力。

React中的函数式编程

  • 函数式组件

    React 中的函数式组件是纯函数的体现,通过使用纯函数的方式来构建 UI,可以更容易实现代码的可测试性和可维护性。

  • Hooks

    React 的 Hooks 是一种函数式编程的方式来管理组件的状态和副作用等逻辑,通过使用 hooks,我们可以将组件中的状态和副作用等逻辑提取到一个单独的函数中,并使用函数调用的方式在组件中共享使用。这种方式更符合函数式编程的思想,避免了类组件中 this 和生命周期等特性的使用,提高了代码的可读性和可维护性。

  • 数据不可变性

    React 推崇的数据更新方式是不可变性,即通过创建新的对象来更新数据,而不是直接修改原始数据。这种方式避免了许多错误,提高了代码的可维护性,并且更容易实现代码的优化。

  • 组合、HoC

    React 中的组件是可以组合的,通过组合不同的组件,可以构建出复杂的 UI。这种方式与函数式编程中的组合思想非常相似,可以更好地实现代码的可重用性和可维护性。

在项目中的实践

  1. 主要还是结合ramda使用,我们可以先把ramda用好用熟练,后面再探索更高级的用法

    Thinking in Ramda

总结

函数式编程的优缺点

优点

  • 更好的管理状态:因为它的宗旨是无状态,或者说更少的状态,能最大化的减少未知代码、减少出错情况
  • 更简单的复用:固定输入->固定输出,没有其他外部变量影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响
  • 更优雅的组合:往大的说,网页是由各个组件组成的。往小的说,一个函数也可能是由多个小函数组成的。更强的复用性,带来更强大的组合性
  • 隐性好处。减少代码量,提高维护性

缺点

  • 性能:函数式编程相对于指令式编程,性能绝对是一个短板,因为它往往会对一个方法进行过度包装,从而产生上下文切换的性能开销
  • 资源占用:在 JS 中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式
  • 递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作

思考

函数式编程只是万千编程范式的一种,它可以拓宽我们思考问题和编写代码的思路,它的很多设计理念值得我们借鉴和思考,但并不是解决问题的万能药剂,不光函数式编程不是,其他任何一种编程范式都不是,我们在编程时应该结合实际业务使用最合理的范式。

参考链接

深入理解函数式编程(上)

Functional Components with React stateless functions and Ramda

Why Ramda?