TypeScript 中的函数式编程:进阶篇
函数式编程(FP)是一种近年来在 JavaScript 和 TypeScript 领域风靡的编程范式。对于已经熟悉基础概念(纯函数、数据不可变性)的开发者来说,一个新的可能性层正在打开:与单子(Monad)、函子(Functor)、应用函子(Applicative)以及复杂组合的协作。在本文中,我们将通过实践示例,在 TypeScript 中解析这些进阶 FP 概念。
1. 不可变性与结构共享
不可变性是 FP 的基石。在 TypeScript 中,我们可以通过 readonly 和 ReadonlyArray 在类型层面保证不可变性。然而,为了高效处理大型数据结构(例如 Redux 存储),我们会使用结构共享(Structural Sharing)——像 Immer 或 Immutable.js 这样的库。
考虑一个使用 readonly 以及一个不通过突变来更新嵌套对象的函数的例子:
type User = { readonly id: number; readonly name: string; readonly address: { readonly city: string; readonly zip: string; };};
const updateUserCity = (user: User, newCity: string): User => ({ ...user, address: { ...user.address, city: newCity, },});
const user: User = { id: 1, name: 'Alice', address: { city: 'NYC', zip: '10001' } };const updatedUser = updateUserCity(user, 'LA');console.log(user.address.city); // NYC (原始对象未改变)console.log(updatedUser.address.city); // LA注意:我们创建了一个新对象,但复用了未改变的部分(如 id 和 name)。这是手动实现的结构共享。对于深层嵌套,最好使用库。
2. 函子及其实现
函子(Functor)是一个实现了 map 方法的容器,它允许将一个函数应用于容器内的值,同时不破坏其结构。在 TypeScript 中,一个典型的函子是 Array。但我们也可以创建自己的函子,例如 Maybe,来避免 null 和 undefined 导致的错误。
让我们实现一个简单的 Maybe 函子:
class Maybe<T> { private constructor(private value: T | null) {}
static of<U>(val: U | null | undefined): Maybe<U> { return new Maybe(val ?? null); }
map<U>(fn: (val: T) => U): Maybe<U> { if (this.value === null) { return Maybe.of<U>(null); } return Maybe.of(fn(this.value)); }
getOrElse(defaultValue: T): T { return this.value !== null ? this.value : defaultValue; }}
// 使用示例const result = Maybe.of('hello') .map(s => s.toUpperCase()) .map(s => s + ' world') .getOrElse('fallback');console.log(result); // "HELLO WORLD"
const empty = Maybe.of<string>(null) .map(s => s.toUpperCase()) .getOrElse('default');console.log(empty); // "default"Maybe 函子允许安全地链式调用函数,而无需担心 null。每个 map 都返回一个新的 Maybe,从而保留了上下文。
3. 单子:带有副作用的操作链
单子(Monad)是一个同时实现了 flatMap 方法(通常称为 bind 或 chain)的函子。它允许“解包”嵌套的容器。单子的一个经典例子是 Promise 或 Array。让我们看看用于错误处理(不使用异常)的 Either 单子。
type Either<L, R> = Left<L> | Right<R>;
class Left<L> { readonly value: L; constructor(value: L) { this.value = value; } isLeft(): this is Left<L> { return true; } isRight(): this is Right<ne