函数式编程范式(三)

函数式编程范式(三)

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
相关推荐
mldong3 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排4 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60614 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角5 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!5 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰6 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi7 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen8 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马8 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒9 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端