函数式编程

33 阅读7分钟

一、什么是函数式编程

函数式编程是一种编程方式,通过对数据流操作的抽象,来减少数据状态的改变和副作用的消除。通过声明式的方式抽象数据流操作,减少数据状态的改变,清晰化程序的执行步骤。通过纯函数来消除副作用。

什么是副作用?

副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互

  • 更改文件系统
  • 往数据库插入记录
  • 发送一个 http 请求
  • 可变数据
  • 打印/log
  • 获取用户输入
  • DOM 查询
  • 访问系统状态
  • ......

1.1纯函数

什么是纯函数:

  1. 仅取决于提供的输入,而不依赖于任何在函数求值期间或调用间隔时可能变化的隐藏状态和外部状态。
  2. 不会造成超出其作用域的变化,例如修改全局对象或引用传递的参数。 特性:引用透明,输入和输出明确。便于整条函数链的推理;存储不可变数据。

// 代码举例

纯函数的好处:

  1. 不改变和破坏全局状态,便于代码的测试和维护

简洁的说,函数式编程是指为创建不可变的程序,通过消除外部可见的副作用,来对纯函数的声明式的求值过程。

1.2函数式编程有什么好处

  1. 可以将复杂任务分解成简单函数,再将其组合起来,通过一等高阶函数的组合方式来提高代码的模块化和可重用性。
  2. 通过流式的调用链直观的处理数据流。
  3. 可以通过响应式编程降低事件驱动代码的复杂性。

1.3 函数式与面向对象的异同

面向对象是行为和数据,即函数和属性紧耦合,注重考虑对象继承层级的抽象,着重于数据及数据之间的关系;函数式编程是行为和数据松耦合,抽离具体数据类型对行为的影响,注重函数组合的抽象,关注于操作如何执行。

  1. 特性比较
函数式面向对象
编程风格声明式命令式
数据和行为独立且松耦合的纯函数与方法紧耦合的类
状态管理将对象视为不可变的值主张通过实例方法改变对象
程序流控制函数和递归循环和条件
线程安全可并发编程难以实现
封装性因为一切都是不可变的,所以没有必要需要保护数据的完整性
  1. 对象的不可变如何实现
  • 简单的方式是返回函数;也可以通过Object.freeze()方法实现完全不可变设置writable属性为false,但这些方式只能在最外层不可变,如果属性是一个引用类型,此属性内容还是可变的,可以递归使用Object.freeze()方法实现完全不可变。
  • 采用一个能够合理管理和赋值状态的内部存储部件,类似于写时复制,书中以Ramda.js,一个函数式JavaScript库举例

1.4 函数 (消灭this)

有值函数对函数式编程才有意义,因为无值函数会修改外部数据,产生副作用。函数是一个基于输入的且尚未求值的不可变的值,这一原则将贯穿整个函数式编程的学习。

  1. 一等函数和高阶函数;this的使用取决于函数如何使用,需要开发者关注函数内部,函数式代码中应该尽量避免使用this
  2. 函数上下文,闭包和作用域
    • 闭包是一种能够在函数声明过程中将环境信息与所属函数绑定在一起的数据结构。它是基于函数声明的文本位置的,因此也被称为围绕函数定义的静态作用域或词法作用域。闭包能够使函数访问其环境状态,使得代码更清晰可读。你很快就会看到,闭包不仅应用于函数式编程的高阶函数中,也可用于事件处理和回调、模拟私有成员变量,还能用于弥补一些 JavaScript 的不足。
    • 闭包包含了在外部(全局)作用域中声明的 变量 、在父函数内部作用域中声明的变 量 、 父函数的参数以及在函数声明之后声明的变量。
  3. 闭包的实际应用
    • 模拟私有变量
    • 异步服务端调用
    • 创建人工块作用域变量
    • 综合应用,信息隐藏、模块化开发,并能够将参数化的行为跨 数据类型地应用于粗粒度的函数之上

二、函数式基础

2.1 注重函数的操作

  1. 使用map, reduce, filter, some 等抽象函数进行数据处理,通过控制链创建控制流与数据交换明确分离的程序。
  2. 使用是声明式的函数式编程能够构建出更易理解的程序。
  3. 将高阶抽象映射到 SQL 语句,从而深刻地认识数据。
  4. 递归能够解决自相似问题,并解析递归定义的数据结构。

2.2 代码的可复用和模块化

  1. 函数链与函数管道。函数链需要在一个对象上进行衍生,是紧耦合的级联方法,比如Array对象的map, reduce, filter,some 等等;函数管道是函数组合,通过高阶函数的方式组合,为了提高函数输入和输出参数的可维护性,需要尽量减少参数个数,同时明确参数类型,明确参数类型可以交给typescript,减少入参个数可以使用curry柯里化,减少出参个数可以利用元组。
  2. 部分应用和函数绑定,和柯里化类似,但部分应用函数,会拿到一个剩余所有入参的函数,柯里化是一个一个的一元函数。功能是扩展了语言的能力,绑定了惰性函数,业务部分应用函数也是返回一个函数
  3. 函数组合。(redux和koa的compose函数举例)
  4. 组合子
  5. Hindley-Milner类型签名 能够明确函数入参出参及函数功能

2.3 Functor(函子)

Functor是一个集合到另一个集合的映射。在程序里表示从一个数据类型集合到另一个数据类型集合,如字符串的集合到数的集合,集合的转换就通过map方法拿到集合的值,再配合一个转换函数来实现,集合的转换。

实践:最常见的就是Array的map和filter,可以把Array当作一个容器,通过map方法拿到容器的值,配合转换函数计算后,再放入容器。 如果要做更复杂的处理,更优雅的错误处理,需要Monad

2.4 Monad(单子)

Monad也是一个集合到另一个集合的映射,他和Functor的区别在于,被Functor包裹的值,如果有多个被包裹的函数进行组合,会发生Functor嵌套,需要扁平化处理,这就需要Monad。因此,Functor的map 方法接收一个仅仅变换容器内值的函数,而Monad的flatMap 接收一个返回Box类型的函数,实现了flatMap的数据结构就是Monad。

介绍一个Either Monad用来处理异常,为什么处理异常需要一个Functor包裹呢,因为异常意味着另外一种输出,与正常函数的输出不同,使得代码不再可预测,同时函数的组合也变得困难。 注:Promise类似于Monad,但不是Monad,因为Promise不满足Monad的数学定义。