来聊聊函数式

仅个人观点,欢迎讨论。

先用一句话描绘fp的轮廓:使用(纯)函数抽象作用在数据之上的控制流。

概念

什么是状态

  • 程序执行过程中生存了一定时间的值。
  • 这些值可能存在于内存、硬盘、显示器等任何程序可以到达的能存储数据的地方。

变量是状态,屏幕上的一个像素点也是状态。

什么是变化

  • 状态的改变,包括值的改变、引用的改变和存储介质的改变。
  • 本质上是一种隐式通信。

程序生成文件,数据从内存到磁盘,即是状态存储介质的改变。

什么是副作用

  • 引起了外部状态的变化。

如何区分外部状态和内部状态

  • 对函数而言,外部状态大致可对应公有状态,内部状态大致可对应私有状态。即与其他函数共享的是外部状态,该函数独有的是内部状态。
  • 内部状态不一定在函数内。
js 复制代码
const useCounter = () => {
  let count = 0;

  const counter = () => count++;

  return counter;
};

count虽然在counter外,但无法被其他函数访问,所以它是counter的内部状态。

什么是无状态函数

  • 不依赖外部状态也不影响外部状态的函数。

什么是纯函数

  • 首先是无状态函数。
  • 对相同的输入总有相同的输出。

内部状态也会对输出产生影响,比如上面的counter,是无状态函数但不是纯函数。

函数式编程,什么是"函数"

  • 数学上的函数,即一种映射关系。
  • 本身没有状态,只是将一种状态映射到另一种状态。

形式

  • 通过封装抽象操作。
  • 通过组合构建控制流。
  • 通过柯里化等手段在操作过程中注入依赖。

以下两个案例均使用fx-flow

  • 文件分片上传
ts 复制代码
import { concurrent, consume, delay, filter, map, pipe, toAsync } from 'fx-flow'

type FileChunk = number

const fileChunks: FileChunk[] = [1, 2, 3, 4, 5]

const notUploaded = (chunk: FileChunk) => delay(100, chunk > 1)

const uploadChunk = (chunk: FileChunk) => delay(1000, chunk).then(() => console.log(`${chunk} has been uploaded`))

// 一边滤除已上传的分片,一边上传文件分片,并发数为3
// 在惰性求值的作用下并不是全部过滤完再上传,而是过滤一部分,凑齐3个,同时上传,完成后再循环
pipe(fileChunks, toAsync, filter(notUploaded), map(uploadChunk), concurrent(3), consume)
  • 转换参数->调用接口->转换返回值->统一错误信息
ts 复制代码
const queryCompanyStatisticsInfoBiz = async (args) =>
  flow(
    ok(args),
    into((data) => ({ companyCode: data.company })),
    andThen(queryCompanyStatisticsInfoApi),
    andThen((data) =>
      ok(
        format([
          ['场站', data.stationCount, '个'],
          ['气瓶', data.cylinderCount, '支'],
          ['车辆', data.carCount, '辆'],
          ['从业人员', data.employeeCount, '人'],
          ['用户', data.customerCount, '户']
        ])
      )
    ),
    mapErr(() => '查询企业统计信息失败')
  )

灵魂

为什么用纯函数来抽象操作

  • 纯函数既不依赖外部状态,也不改变外部状态,对相同的输入总保持相同的输出。意味着可测试,意味着调用时通常符合直觉。
  • 与外部无联系,意味着易理解,易排查错误。

有状态的各种"子"

函子、单子、半群和幺半群等映射或代数结构在此统称为"子"。

  • 在控制流中前后操作之间通常需要通信,无状态的纯函数无法胜任工作。于是"子"挺身而出,作为前后操作交互的媒介,推动控制流。
  • "子"通过映射进行状态流转,该步骤对上层的操作屏蔽。
  • "子"脱胎于范畴论。

以最简单的函子为例。fn为操作,Identify为推动控制流的"子"。Identify保存当次操作的结果,并在map时传递给下一个操作。

ts 复制代码
class Identify<T> {
  private constructor(private value: T) {}

  static of<U>(x: U) {
    return new Identify(x);
  }

  public map<R>(fn: (x: T) => R) {
    return new Identify(fn(this.value));
  }
}

const fn1 = (x: number) => x * Math.random();
const fn2 = (x: number) => x.toFixed(0);

Identify.of(100).map(fn1).map(fn2);

为什么通过映射进行状态流转

  • 传统的状态流转方式一般是直接修改状态(特指外部状态),则可能存在未知的第三方通过监听该状态的变化执行不可预测的操作。此为隐式通信。
  • 若产生一个新的状态,则无法在旧状态上观测到变化。必须应用显式通信。这让程序的行为变得可预测,就能减少错误。

是否有极致的"纯"

  • 没有极致的"纯"。
  • 抽象建立在具象之上,封装之内,尽可按传统方式进行状态流转,注意控制隐式通信作用域大小即可。
  • "纯"的操作干不了实事,不与外界交互就只是内存里的一堆数据。带副作用的操作同样重要,但在条件允许的情况下最好放到流的尾部(或支流的尾部)。

探索实践

  • fx-flow是一个承载函数式编程理念的工具库,以实用为目标,支持js/ts,是本人探索函数式编程实践的产物。该库具备流式编程、错误处理、并发控制、惰性求值和类型提示等功能。并结合ts的特点,对不可变性和柯里化进行了创新,有更强的运行时性能。测试覆盖率也达到90%以上。
  • fp-ts是一个高度符合函数式理念的工具库,提供了各种常用的"子"。但对不熟悉fp的开发者来说前期学习成本略高,实际应用中可能会因逻辑组合方式的颠覆式变化而感到代码可读性降低。

扩展

响应式编程正与函数式编程一起快速成长为现代前端开发的主流范式。其核心是实现对异步数据流变化的自动反馈。关于frp,请听下回分解。

相关推荐
王解18 小时前
Jest项目实战(2): 项目开发与测试
前端·javascript·react.js·arcgis·typescript·单元测试
鸿蒙开天组●21 小时前
鸿蒙进阶篇-网格布局 Grid/GridItem(二)
前端·华为·typescript·harmonyos·grid·mate70
zhizhiqiuya21 小时前
第二章 TypeScript 函数详解
前端·javascript·typescript
初遇你时动了情1 天前
react 18 react-router-dom V6 路由传参的几种方式
react.js·typescript·react-router
王解1 天前
Jest进阶知识:深入测试 React Hooks-确保自定义逻辑的可靠性
前端·javascript·react.js·typescript·单元测试·前端框架
_jiang2 天前
nestjs 入门实战最强篇
redis·typescript·nestjs
大福是小强2 天前
002-Kotlin界面开发之Kotlin旋风之旅
kotlin·函数式编程·lambda·语法·运算符重载·扩展函数
清清ww2 天前
【TS】九天学会TS语法---计划篇
前端·typescript
努力变厉害的小超超3 天前
TypeScript中的类型注解、Interface接口、泛型
javascript·typescript
王解3 天前
Jest进阶知识:整合 TypeScript - 提升单元测试的类型安全与可靠性
前端·javascript·typescript·单元测试