跳到主要内容

代数数据类型

代数数据类型(Algebraic Data Types,ADT)是函数式编程中用于建模复杂数据结构的强大工具。理解 ADT 可以帮助你设计更安全、更表达性的程序。

什么是代数数据类型?

代数数据类型这个术语来源于类型理论,它将数据类型视为可以像代数一样组合的构造。"代数"指的是类型可以通过两种基本方式组合:

  1. 积类型(Product Types):多个类型同时存在,类似于"与"的关系
  2. 和类型(Sum Types):多种类型之一存在,类似于"或"的关系

为什么叫"积"和"和"?

这个命名来自类型的可能值数量:

  • 积类型 A × B 的可能值数量是 |A| × |B|(类型A的值数量乘以类型B的值数量)
  • 和类型 A + B 的可能值数量是 |A| + |B|(类型A的值数量加类型B的值数量)

积类型

积类型表示一个值同时包含多个类型的成员。最常见的积类型是元组和记录(对象)。

元组 - 有序积类型

元组是固定长度、有序的值的集合,每个位置可以是不同的类型:

// 二维坐标点:同时包含 x 和 y
const point = [10, 20]; // [x, y]

// 三维坐标点
const point3D = [10, 20, 30]; // [x, y, z]

// 人名和年龄
const person = ['张三', 25]; // [name, age]

// 使用解构访问
const [x, y] = point;
console.log(`坐标: (${x}, ${y})`); // 坐标: (10, 20)

元组的问题在于位置的含义不明确。比如 [10, 20] 是坐标还是长宽?这需要约定或文档说明。

记录 - 带标签的积类型

记录(在 JavaScript 中是对象)给每个字段命名,解决了元组的可读性问题:

// 带标签的坐标点
const point = {
x: 10,
y: 20
};

// 用户信息
const user = {
name: '张三',
email: '[email protected]',
age: 25,
active: true
};

// 访问字段更清晰
console.log(`X: ${point.x}, Y: ${point.y}`);

积类型的关键特性是它包含所有组成部分。一个点既有 x 坐标也有 y 坐标,一个用户既有姓名也有年龄。

和类型

和类型表示一个值可能是多种类型之一。这是处理"这个或那个"场景的关键工具。

JavaScript 中的和类型

JavaScript 没有原生的和类型支持,但可以通过多种方式模拟:

使用类和继承

// 定义形状类型的基类
class Shape {}

// 圆形:只需要半径
class Circle extends Shape {
constructor(radius) {
super();
this.type = 'Circle';
this.radius = radius;
}
}

// 矩形:需要宽和高
class Rectangle extends Shape {
constructor(width, height) {
super();
this.type = 'Rectangle';
this.width = width;
this.height = height;
}
}

// 三角形:需要底和高
class Triangle extends Shape {
constructor(base, height) {
super();
this.type = 'Triangle';
this.base = base;
this.height = height;
}
}

// 使用模式匹配处理不同类型
function getArea(shape) {
switch (shape.type) {
case 'Circle':
return Math.PI * shape.radius ** 2;
case 'Rectangle':
return shape.width * shape.height;
case 'Triangle':
return (shape.base * shape.height) / 2;
default:
throw new Error(`未知形状类型: ${shape.type}`);
}
}

// 使用示例
const circle = new Circle(5);
const rectangle = new Rectangle(4, 6);
const triangle = new Triangle(3, 4);

console.log(getArea(circle)); // 78.54...
console.log(getArea(rectangle)); // 24
console.log(getArea(triangle)); // 6

使用对象字面量

// 使用对象字面量表示和类型
const Circle = radius => ({ type: 'Circle', radius });
const Rectangle = (width, height) => ({ type: 'Rectangle', width, height });
const Triangle = (base, height) => ({ type: 'Triangle', base, height });

// 模式匹配函数
const match = (shape, cases) => {
const handler = cases[shape.type];
if (!handler) {
throw new Error(`未处理类型: ${shape.type}`);
}
return handler(shape);
};

// 使用
const area = shape => match(shape, {
Circle: ({ radius }) => Math.PI * radius ** 2,
Rectangle: ({ width, height }) => width * height,
Triangle: ({ base, height }) => (base * height) / 2
});

console.log(area(Circle(5))); // 78.54...
console.log(area(Rectangle(4, 6))); // 24

TypeScript 中的和类型

TypeScript 提供了原生的联合类型支持:

// 联合类型
type Shape =
| { type: 'Circle'; radius: number }
| { type: 'Rectangle'; width: number; height: number }
| { type: 'Triangle'; base: number; height: number };

// 类型窄化(Type Narrowing)
function getArea(shape: Shape): number {
switch (shape.type) {
case 'Circle':
return Math.PI * shape.radius ** 2;
case 'Rectangle':
return shape.width * shape.height;
case 'Triangle':
return (shape.base * shape.height) / 2;
}
}

Maybe 类型(Option 类型)

Maybe 类型是函数式编程中最常用的和类型之一,它表示一个值可能存在也可能不存在。

为什么需要 Maybe?

在传统编程中,我们使用 nullundefined 表示缺失的值,但这容易导致运行时错误:

// 传统方式 - 容易出错
function getUserName(user) {
// 如果 user 或 user.profile 或 user.profile.name 不存在,会抛出错误
return user.profile.name.toUpperCase();
}

// 防御式编程 - 繁琐且容易遗漏
function getUserNameSafe(user) {
if (user && user.profile && user.profile.name) {
return user.profile.name.toUpperCase();
}
return 'UNKNOWN';
}

Maybe 类型提供了一种更安全、更组合的方式来处理可能缺失的值。

Maybe 实现

// Maybe 类型的实现
class Maybe {
// 工厂方法
static just(value) {
return new Just(value);
}

static nothing() {
return new Nothing();
}

static of(value) {
return value == null ? Maybe.nothing() : Maybe.just(value);
}

// 从可空链中安全获取值
static fromNullable(value) {
return Maybe.of(value);
}
}

// Just - 有值的情况
class Just extends Maybe {
constructor(value) {
super();
this.value = value;
}

// map: 转换值
map(fn) {
return Maybe.just(fn(this.value));
}

// flatMap: 转换并展平(用于链式调用可能返回 Maybe 的函数)
flatMap(fn) {
return fn(this.value);
}

// filter: 条件过滤
filter(predicate) {
return predicate(this.value) ? this : Maybe.nothing();
}

// getOrElse: 获取值或默认值
getOrElse(defaultValue) {
return this.value;
}

// 类型检查
isJust() { return true; }
isNothing() { return false; }

// 折叠:根据类型执行不同操作
fold(onNothing, onJust) {
return onJust(this.value);
}
}

// Nothing - 无值的情况
class Nothing extends Maybe {
map(fn) {
return this; // 无值时保持 Nothing
}

flatMap(fn) {
return this; // 无值时保持 Nothing
}

filter(predicate) {
return this;
}

getOrElse(defaultValue) {
return defaultValue;
}

isJust() { return false; }
isNothing() { return true; }

fold(onNothing, onJust) {
return onNothing();
}
}

// 使用示例
const user = {
profile: {
name: '张三',
email: '[email protected]'
}
};

// 安全获取嵌套属性
function getUserName(user) {
return Maybe.of(user)
.flatMap(u => Maybe.of(u.profile))
.flatMap(p => Maybe.of(p.name))
.map(name => name.toUpperCase())
.getOrElse('未知用户');
}

console.log(getUserName(user)); // '张三'
console.log(getUserName({})); // '未知用户'
console.log(getUserName(null)); // '未知用户'

实际应用

// 安全的数组查找
function safeFind(array, predicate) {
const result = array.find(predicate);
return Maybe.of(result);
}

const users = [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
];

const user = safeFind(users, u => u.id === 1);
user.map(u => console.log(`找到用户: ${u.name}`)); // 找到用户: 张三

const notFound = safeFind(users, u => u.id === 999);
notFound.map(u => console.log(`找到用户: ${u.name}`)); // 不会执行
console.log(notFound.getOrElse('未找到')); // '未找到'

// 安全的 JSON 解析
function safeParseJSON(json) {
try {
return Maybe.just(JSON.parse(json));
} catch (e) {
return Maybe.nothing();
}
}

const valid = safeParseJSON('{"name": "张三"}');
console.log(valid.map(obj => obj.name).getOrElse('解析失败')); // '张三'

const invalid = safeParseJSON('invalid json');
console.log(invalid.map(obj => obj.name).getOrElse('解析失败')); // '解析失败'

Either 类型

Either 类型表示两种可能之一:通常是成功(Right)或失败(Left)。与 Maybe 不同,Either 可以携带失败的原因。

Either 实现

class Either {
static left(value) {
return new Left(value);
}

static right(value) {
return new Right(value);
}

// 从可能抛出异常的函数创建
static tryCatch(fn) {
try {
return Either.right(fn());
} catch (e) {
return Either.left(e);
}
}
}

class Left extends Either {
constructor(value) {
super();
this.value = value;
}

map(fn) {
return this; // Left 不执行转换
}

flatMap(fn) {
return this;
}

// 折叠:执行左侧函数
fold(onLeft, onRight) {
return onLeft(this.value);
}

isLeft() { return true; }
isRight() { return false; }

// 获取值
getOrElse(defaultValue) {
return defaultValue;
}

// 交换左右
swap() {
return Either.right(this.value);
}
}

class Right extends Either {
constructor(value) {
super();
this.value = value;
}

map(fn) {
return Either.right(fn(this.value));
}

flatMap(fn) {
return fn(this.value);
}

fold(onLeft, onRight) {
return onRight(this.value);
}

isLeft() { return false; }
isRight() { return true; }

getOrElse(defaultValue) {
return this.value;
}

swap() {
return Either.left(this.value);
}
}

实际应用

// 验证函数返回 Either
function validateAge(age) {
if (typeof age !== 'number') {
return Either.left('年龄必须是数字');
}
if (age < 0) {
return Either.left('年龄不能为负数');
}
if (age > 150) {
return Either.left('年龄不合理');
}
return Either.right(age);
}

// 解析并验证
function parseAndValidate(input) {
const parsed = parseInt(input, 10);
if (isNaN(parsed)) {
return Either.left(`"${input}" 不是有效的数字`);
}
return validateAge(parsed);
}

// 使用
const results = ['25', '-5', 'abc', '200', '30'];

results.forEach(input => {
const result = parseAndValidate(input);
result.fold(
error => console.log(`输入 "${input}" 无效: ${error}`),
age => console.log(`输入 "${input}" 有效,年龄: ${age}`)
);
});
// 输入 "25" 有效,年龄: 25
// 输入 "-5" 无效: 年龄不能为负数
// 输入 "abc" 无效: "abc" 不是有效的数字
// 输入 "200" 无效: 年龄不合理
// 输入 "30" 有效,年龄: 30

// 链式验证
function validateUser(data) {
return validateName(data.name)
.flatMap(() => validateEmail(data.email))
.flatMap(() => validateAge(data.age))
.map(() => ({ ...data, validated: true }));
}

function validateName(name) {
return (!name || name.trim() === '')
? Either.left('姓名不能为空')
: Either.right(name);
}

function validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return (!email || !emailRegex.test(email))
? Either.left('邮箱格式不正确')
: Either.right(email);
}

Result 类型

Result 类型是 Either 的一个特化版本,专门用于成功/失败的二元结果:

// Result = Success | Failure
const Result = {
ok: value => ({ ok: true, value }),
err: error => ({ ok: false, error }),

// 从可能抛出异常的函数创建
from(fn) {
try {
return Result.ok(fn());
} catch (e) {
return Result.err(e);
}
},

// map: 转换成功的值
map: (result, fn) => result.ok ? Result.ok(fn(result.value)) : result,

// mapErr: 转换错误
mapErr: (result, fn) => result.ok ? result : Result.err(fn(result.error)),

// andThen: 链式操作
andThen: (result, fn) => result.ok ? fn(result.value) : result,

// unwrap: 获取值或抛出错误
unwrap: result => {
if (result.ok) return result.value;
throw result.error;
},

// unwrapOr: 获取值或默认值
unwrapOr: (result, defaultValue) => result.ok ? result.value : defaultValue
};

// 使用示例
function divide(a, b) {
if (b === 0) {
return Result.err(new Error('除数不能为零'));
}
return Result.ok(a / b);
}

const result1 = divide(10, 2);
console.log(Result.unwrapOr(result1, 0)); // 5

const result2 = divide(10, 0);
console.log(Result.unwrapOr(result2, 0)); // 0

// 链式操作
const final = Result.andThen(
divide(10, 2),
x => divide(x, 2)
);
console.log(Result.unwrap(final)); // 2.5

模式匹配

模式匹配是处理代数数据类型的核心技术。虽然 JavaScript 没有原生的模式匹配,但我们可以模拟:

// 简单的模式匹配实现
const match = (value, patterns) => {
// 检查是否有匹配的类型
if (value.type && patterns[value.type]) {
return patterns[value.type](value);
}

// 检查是否有默认处理
if (patterns._) {
return patterns._(value);
}

throw new Error(`未匹配的模式: ${JSON.stringify(value)}`);
};

// 定义数据类型
const Payment = {
Cash: amount => ({ type: 'Cash', amount }),
CreditCard: (number, amount) => ({ type: 'CreditCard', number, amount }),
PayPal: (email, amount) => ({ type: 'PayPal', email, amount })
};

// 使用模式匹配处理
const describe = payment => match(payment, {
Cash: ({ amount }) => `现金支付: ¥${amount}`,
CreditCard: ({ number, amount }) => `信用卡(${number.slice(-4)})支付: ¥${amount}`,
PayPal: ({ email, amount }) => `PayPal(${email})支付: ¥${amount}`
});

console.log(describe(Payment.Cash(100)));
// 现金支付: ¥100

console.log(describe(Payment.CreditCard('1234567890123456', 200)));
// 信用卡(3456)支付: ¥200

console.log(describe(Payment.PayPal('[email protected]', 300)));
// PayPal([email protected])支付: ¥300

ADT 的优势

1. 穷尽性检查

使用和类型时,编译器(如 TypeScript)可以检查你是否处理了所有情况:

type Result = Success | Failure | Pending;

// TypeScript 会警告缺少 Pending 的处理
function getStatus(result: Result) {
switch (result.type) {
case 'Success': return '成功';
case 'Failure': return '失败';
// 缺少 'Pending' 分支!
}
}

2. 不可能状态不可表示

正确设计的 ADT 可以消除无效状态:

// 不好的设计:可能产生无效组合
type User = {
isLoggedIn: boolean;
loginError?: string; // 只在未登录时有意义
userData?: object; // 只在已登录时有意义
};

// 好的设计:每个状态只包含相关数据
type User =
| { type: 'Anonymous' }
| { type: 'LoggedIn'; data: object }
| { type: 'LoginFailed'; error: string };

3. 组合性

ADT 可以自由组合,构建复杂类型:

// 积类型组合
type Point = { x: number; y: number };

// 和类型组合
type Shape = Circle | Rectangle;

// 嵌套组合
type Drawing = {
shapes: Shape[];
background: Color;
selected: Maybe<Shape>; // 和类型嵌套在积类型中
};

最佳实践

  1. 优先使用和类型代替布尔标志
// 不好的设计
function process(data, isValid, hasError) {
// 三个布尔值有 8 种组合,但很多组合是无效的
}

// 好的设计
function process(data) {
return match(data.status, {
Valid: () => /* 处理有效数据 */,
Invalid: () => /* 处理无效数据 */,
Error: () => /* 处理错误 */
});
}
  1. 让非法状态不可表示
// 不要这样
const order = {
status: 'cancelled',
cancelledAt: null // 矛盾!
};

// 应该这样
const cancelledOrder = {
status: 'cancelled',
cancelledAt: new Date() // 取消时必须有取消时间
};
  1. 使用类型保护数据完整性
// 使用 Maybe 避免 null 检查
function findUser(id) {
return Maybe.of(users.find(u => u.id === id));
}

// 使用 Either 携带错误信息
function validate(input) {
return input.valid
? Either.right(input.data)
: Either.left(input.errorMessage);
}

小结

代数数据类型是函数式编程的强大工具:

  • 积类型:同时包含多个值,如元组和对象
  • 和类型:表示多种可能之一,如联合类型
  • Maybe:安全处理可能缺失的值
  • Either:携带错误信息的二元结果
  • 模式匹配:安全地处理不同类型

正确使用 ADT 可以让代码更安全、更表达性强、更容易推理。