函数式编程范式(三)

函数式编程范式(三)

Functor(函子)

Functor 是函数式编程中最基础、最核心的概念之一,本质是:一个包含值(或多个值)的容器,并且实现了一个名为 map 的方法,这个方法可以将容器内的每个值传递给一个函数,并返回一个包含新值的同类型容器

  • 容器:可以把 Functor 理解成一个 "盒子",里面装着数据(比如数字、字符串、对象);
  • map 方法:这个盒子提供了一个接口,让你可以 "不打开盒子" 就对里面的数据做变换,且变换后返回的还是一个同类型的盒子(保持容器结构不变)。
typescript 复制代码
// Functor 函子
class Container {
  #value: any
  constructor (value: any) {
    this.#value = value
  }
  
  map<T extends (...args: any[]) => any>(fn: T) {
    return new Container(fn(this.#value))
  }
}

new Container(5).map(x => x + 1).map(x => x * 2).map(x => { console.log(x) }) // 12

这里的 new 关键字实例化对象看起来有点像面向对象,我可以创建一个 of 静态方法,在里面实例化函子

typescript 复制代码
class Container {
  #value: any
  constructor (value: any) {
    this.#value = value
  }

  static of (value: any) {
    return new Container(value)
  }
  
  map<T extends (...args: any[]) => any>(fn: T) {
    return Container.of(fn(this.#value))
  }
}

Container.of(5).map(x => x + 1).map(x => x * 2).map(x => { console.log(x) }) // 12

我们这里的函子还存在一些问题,比如我们传入了 null ,处理过程中就可能会出现一些问题。

typescript 复制代码
Container.of(5).map(x => x.toUpperCase()) // 报错

我们可以通过 MayBe 函子解决这个问题

MayBe函子

基础 Box 函子如果遇到 null/undefined 会报错,Maybe 函子是对 Box 的增强,专门处理空值安全

typescript 复制代码
class MayBe {
  #value: any
  constructor (value: any) {
    this.#value = value
  }

  static of (value: any) {
    return new Container(value)
  }

  isNothing () {
    return this.#value === null || this.#value === undefined
  }
  
  map<T extends (...args: any[]) => any>(fn: T) {
    return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this.#value))
  }
}

Maybe 函子还会有一些问题,当我们调用多次调用 map 的过程出现报错,我们不知道到底是哪个地方出先的问题,我们可以通过 Either函子来解决这个问题。

Either 函子

Either 函子分为 Left(异常分支)和 Right(正常分支),适合处理可能抛出错误的逻辑。

typescript 复制代码
// Either 函子
class Left {
  #value: any
  constructor (value: any) {
    this.#value = value
  }

  static of (value: any) {
    return new Container(value)
  }
  // Left 的 map 不执行函数,直接返回自身(跳过处理)
  map<T extends (...args: any[]) => any>(fn: T) {
    return this
  }
}

class Right {
  #value: any
  constructor (value: any) {
    this.#value = value
  }

  static of (value: any) {
    return new Container(value)
  }
  
  map<T extends (...args: any[]) => any>(fn: T) {
    return Right.of(fn(this.#value))
  }
}

// 示例:解析 JSON,失败则返回错误信息
const parseJSON = (str: string) => {
  try {
    return Right.of(JSON.parse(str))
  } catch (e: any) {
    return Left.of(`解析错误:${e.message}`)
  }
}
IO函子

IO(Input/Output,输入输出)函子是函数式编程中专门处理有副作用操作的函子(比如读写文件、操作 DOM、发起网络请求、读取环境变量等)。

它的核心设计思想是:把有副作用的操作 "包裹" 起来,延迟执行,让副作用从 "立即发生" 变成 "按需触发" ,同时保持函子的核心特性(map 方法、组合能力)。

typescript 复制代码
import { flowRight } from "lodash-es"

class IO {
  #value: any
  constructor (fn: any) {
    // 核心:IO 内部存储的是函数,而非直接的结果
    this.#value = fn
  }
	
  // of 静态方法创建 IO 实例传入 初始数据
  static of (value: any) {
    return new IO(function() {
      return value
    })
  }
	// map组合新的函数,返回新的 IO 函子
  // map 不执行函数,只是组合
  map (fn: any) {
    // 先执行原函数(获取副作用结果),再执行传入的 fn
    return new IO(flowRight(fn, this.#value))
  }
	// 触发副作用执行(命名加 unsafe 提醒:这是唯一触发副作用的入口)
  unsafePerformIO() {
    return this.#value();
  }
}

const a = IO.of(process).map((p: any) => p.execPath)

console.log(a.unsafePerformIO())
TasK 函子

Task 函子(也常被称为 Future 函子)是函数式编程中专门处理异步副作用 的函子,是 IO 函子的异步版本。它的核心目标是:将异步操作(如网络请求、异步文件读写)包裹成纯函数式的容器,支持异步操作的组合、错误处理,同时保持延迟执行的特性

简单来说:

  • IO 函子处理同步副作用(如读取 localStorage、操作 DOM);
  • Task 函子处理异步副作用 (如 fetch 请求、setTimeoutfs.readFile

在真实项目中,我们不会手动实现 Task 函子,而是使用成熟的函数式库:

  • folktale/data.task:经典的 Task 实现;
  • ramda-fantasy:包含 Task 等常用函子;
  • fluture:功能更强大的异步函子(兼容 Task 特性)。

以 folktale 为例:

typescript 复制代码
const { task } = require('folktale/concurrency/task');

function delay(ms) {
  return task(resolver => {
    const timerId = setTimeout(
      () => { resolver.resolve() },
      ms
    );
    resolver.cleanup(() => {
      clearTimeout(timerId);
    });
  });
}

delay(100).run().listen({
  onCancelled: () => { console.log('the task was cancelled') },
  onRejected: (error) => { console.log('something went wrong') },
  onResolved: (value) => { console.log(`The value is ${value}`) }
});;
Monad 函子

Monad 函子主要用于解决容器嵌套的问题

javascript 复制代码
// 普通 Functor(Box)
class Box {
  constructor(value) { this.value = value }
  static of(value) { return new Box(value) }
  map(fn) { return Box.of(fn(this.value)) }
}

// 一个返回 Box 的函数
const addOne = x => Box.of(x + 1)

// 用 map 调用:产生嵌套 Box(Box(3))
const nestedBox = Box.of(2).map(addOne)
console.log(nestedBox); // Box { value: Box { value: 3 } }

Monad 是满足更严格规则的函子 ,除了拥有 map 方法,还必须满足:

  1. 提供 of 方法(纯函数,创建 Monad 实例);
  2. 提供 flatMap 方法(也叫 bind):接收一个返回 Monad 的函数 f,先执行 map(f),再「扁平化」嵌套的 Monad;
  3. 满足三条「单子律」(保证行为一致性,类似数学公理):
    • 左单位律:Monad.of(x).flatMap(f) === f(x)
    • 右单位律:monad.flatMap(Monad.of) === monad
    • 结合律:monad.flatMap(f).flatMap(g) === monad.flatMap(x => f(x).flatMap(g))

这里实现一个 Maybe Monad:

javascript 复制代码
class Maybe {
  constructor(value) {
    this._value = value;
  }

  static of(value) {
    return new Maybe(value);
  }

  map(f) {
    return this._value == null ? Maybe.of(null) : Maybe.of(f(this._value));
  }

  // 核心:flatMap 方法(map + 扁平化)
  flatMap(f) {
    // 先执行 map(f) 得到嵌套的 Maybe,再取内部值作为新 Maybe 的值
    return this.map(f).getValue();
  }

  getValue() {
    return this._value;
  }
}

// 测试解决嵌套问题
function addOneMaybe(n) {
  return Maybe.of(n + 1);
}

// 使用 flatMap 而非 map
const flatMaybe = Maybe.of(5).flatMap(addOneMaybe);
console.log(flatMaybe.getValue()); // 输出 6(无嵌套)

// 链式调用 flatMap
Maybe.of(5)
  .flatMap(n => Maybe.of(n + 3))
  .flatMap(n => Maybe.of(n * 4))
  .getValue(); // 输出 32
相关推荐
ruoyusixian2 小时前
chrome二维码识别查插件
前端·chrome
fengfuyao9852 小时前
一个改进的MATLAB CVA(Change Vector Analysis)变化检测程序
前端·算法·matlab
yuhaiqiang2 小时前
为什么这道初中数学题击溃了所有 AI
前端·后端·面试
djk88882 小时前
支持手机屏幕的layui后台html模板
前端·html·layui
紫_龙2 小时前
最新版vue3+TypeScript开发入门到实战教程之watch详解
前端·javascript·typescript
默默学前端3 小时前
ES6模板语法与字符串处理详解
前端·ecmascript·es6
lxh01133 小时前
记忆函数 II 题解
前端·javascript
我不吃饼干3 小时前
TypeScript 类型体操练习笔记(三)
前端·typescript
华仔啊3 小时前
除了防抖和节流,还有哪些 JS 性能优化手段?
前端·javascript·vue.js