函数式编程范式(三)
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请求、setTimeout、fs.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 方法,还必须满足:
- 提供
of方法(纯函数,创建 Monad 实例); - 提供
flatMap方法(也叫bind):接收一个返回 Monad 的函数f,先执行map(f),再「扁平化」嵌套的 Monad; - 满足三条「单子律」(保证行为一致性,类似数学公理):
- 左单位律:
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