name: effect-patterns-value-handling description: Effect-TS 值处理模式。在 Effect-TS 应用程序中处理值时使用。
Effect-TS 模式:值处理
这个技能提供了 2 个精选的 Effect-TS 值处理模式。 在以下任务中使用此技能:
- 值处理
- Effect-TS 应用程序中的最佳实践
- 现实世界中的模式和解决方案
🟡 中级模式
可选模式 1:处理 None 和 Some 值
规则: 使用 Option 表示可能不存在的值,用类型安全的 Option 替换 null/undefined,强制显式处理。
好例子:
这个例子演示了 Option 处理模式。
import { Effect, Option } from "effect";
interface User {
id: string;
name: string;
email: string;
}
interface Profile {
bio: string;
website?: string;
location?: string;
}
const program = Effect.gen(function* () {
console.log(
`
[选项处理] None/Some 值和模式匹配
`
);
// 示例 1:创建 Options
console.log(`[1] 创建 Option 值:
`);
const someValue: Option.Option<string> = Option.some("数据");
const noneValue: Option.Option<string> = Option.none();
const displayOption = <T,>(opt: Option.Option<T>, label: string) =>
Effect.gen(function* () {
if (Option.isSome(opt)) {
yield* Effect.log(`${label}: Some(${opt.value})`);
} else {
yield* Effect.log(`${label}: None`);
}
});
yield* displayOption(someValue, "someValue");
yield* displayOption(noneValue, "noneValue");
// 示例 2:从可空值创建
console.log(`
[2] 将可空值转换为 Option:
`);
const possiblyNull = (shouldExist: boolean): string | null =>
shouldExist ? "找到" : null;
const toOption = (value: string | null | undefined): Option.Option<string> =>
value ? Option.some(value) : Option.none();
const opt1 = toOption(possiblyNull(true));
const opt2 = toOption(possiblyNull(false));
yield* displayOption(opt1, "toOption(找到)");
yield* displayOption(opt2, "toOption(null)");
// 示例 3:在 Option 上进行模式匹配
console.log(`
[3] 使用 match() 进行模式匹配:
`);
const userId: Option.Option<string> = Option.some("user-123");
const message = Option.match(userId, {
onSome: (id) => `用户 ID: ${id}`,
onNone: () => "未找到用户",
});
yield* Effect.log(`[匹配] ${message}`);
const emptyUserId: Option.Option<string> = Option.none();
const emptyMessage = Option.match(emptyUserId, {
onSome: (id) => `用户 ID: ${id}`,
onNone: () => "未找到用户",
});
yield* Effect.log(`[匹配] ${emptyMessage}
`);
// 示例 4:使用 map 转换值
console.log(`[4] 使用 map() 转换值:
`);
const userCount: Option.Option<number> = Option.some(42);
const doubled = Option.map(userCount, (count) => count * 2);
yield* displayOption(doubled, "加倍");
// 链式映射
const email: Option.Option<string> = Option.some("user@example.com");
const domain = Option.map(email, (e) =>
e.split("@")[1] ?? "未知"
);
yield* displayOption(domain, "邮箱域名");
// 示例 5:使用 flatMap 链式操作
console.log(`
[5] 使用 flatMap() 链式操作:
`);
const findUser = (id: string): Option.Option<User> =>
id === "user-1"
? Option.some({ id, name: "Alice", email: "alice@example.com" })
: Option.none();
const getProfile = (userId: string): Option.Option<Profile> =>
userId === "user-1"
? Option.some({ bio: "开发者", website: "alice.dev" })
: Option.none();
const userId2 = Option.some("user-1");
// 链式操作:userId -> user -> profile
const profileChain = Option.flatMap(userId2, (id) =>
Option.flatMap(findUser(id), (user) =>
getProfile(user.id)
)
);
const profileResult = Option.match(profileChain, {
onSome: (profile) => `简介: ${profile.bio}, 网站: ${profile.website}`,
onNone: () => "未找到简介",
});
yield* Effect.log(`[链] ${profileResult}
`);
// 示例 6:使用 getOrElse 提供后备值
console.log(`[6] 使用 getOrElse() 设置默认值:
`);
const optionalStatus: Option.Option<string> = Option.none();
const status = Option.getOrElse(optionalStatus, () => "未知");
yield* Effect.log(`[默认] 状态: ${status}`);
// 实际值
const knownStatus: Option.Option<string> = Option.some("活跃");
const realStatus = Option.getOrElse(knownStatus, () => "未知");
yield* Effect.log(`[值] 状态: ${realStatus}
`);
// 示例 7:使用谓词过滤
console.log(`[7] 使用条件过滤:
`);
const ageOption: Option.Option<number> = Option.some(25);
const isAdult = Option.filter(ageOption, (age) => age >= 18);
yield* displayOption(isAdult, "成人检查 (25)");
const ageOption2: Option.Option<number> = Option.some(15);
const isAdult2 = Option.filter(ageOption2, (age) => age >= 18);
yield* displayOption(isAdult2, "成人检查 (15)");
// 示例 8:多个 Options(所有都存在?)
console.log(`
[8] 组合多个 Options:
`);
const firstName: Option.Option<string> = Option.some("John");
const lastName: Option.Option<string> = Option.some("Doe");
const middleName: Option.Option<string> = Option.none();
// 所有三个都存在?
const allPresent = Option.all([firstName, lastName, middleName]);
yield* displayOption(allPresent, "所有存在");
// 仅两个
const twoPresent = Option.all([firstName, lastName]);
yield* displayOption(twoPresent, "两个存在");
// 示例 9:将 Option 转换为错误
console.log(`
[9] 将 Option 转换为结果/错误:
`);
const optionalConfig: Option.Option<{ apiKey: string }> = Option.none();
const configOrError = Option.match(optionalConfig, {
onSome: (config) => config,
onNone: () => {
throw new Error("未找到配置");
},
});
// 在实际代码中,会捕获错误
const result = Option.match(optionalConfig, {
onSome: (config) => ({ success: true, value: config }),
onNone: () => ({ success: false, error: "config-not-found" }),
});
yield* Effect.log(`[转换] ${JSON.stringify(result)}
`);
// 示例 10:在业务逻辑中使用 Option
console.log(`[10] 实践:可选用户设置:
`);
const userSettings: Option.Option<{
theme: string;
notifications: boolean;
}> = Option.some({
theme: "深色",
notifications: true,
});
const getTheme = Option.map(userSettings, (s) => s.theme);
const theme = Option.getOrElse(getTheme, () => "浅色"); // 默认
yield* Effect.log(`[设置] 主题: ${theme}`);
// 无设置
const noSettings: Option.Option<{ theme: string; notifications: boolean }> =
Option.none();
const noTheme = Option.map(noSettings, (s) => s.theme);
const defaultTheme = Option.getOrElse(noTheme, () => "浅色");
yield* Effect.log(`[默认] 主题: ${defaultTheme}`);
});
Effect.runPromise(program);
原理:
Option 实现空值安全编程:
- Some(value):值存在
- None:值不存在
- 模式匹配:显式处理两种情况
- 链式操作:安全组合操作
- 后备值:默认值
- 转换:Option ↔ 错误
模式:使用 Option.isSome()、Option.isNone()、match()、map()、flatMap()
空值/未定义导致广泛错误:
问题 1:十亿美元错误
- Tony Hoare 在 1965 年于 ALGOL 中发明了 null
- 创造了“十亿美元错误”
- 90% 的安全漏洞涉及空值处理
问题 2:未定义行为
user.profile.name- 任何属性都可能为 null- 运行时错误:“无法读取未定义的属性 ‘name’”
- 无编译时警告
- 生产环境崩溃
问题 3:静默失败
- 函数在失败时返回 null
- 调用者未检查
- 将 null 当作值使用
- 下游状态损坏
问题 4:条件地狱
if (user !== null && user.profile !== null && user.profile.name !== null) {
// 执行操作
}
解决方案:
Option 类型:
Some(value)= 值存在None= 值不存在- 类型系统强制检查
- 无法静默空值检查
模式匹配:
Option.match()- 显式处理两种情况
- 如果遗漏,编译器会警告
链式操作:
option.map().flatMap().match()- 操作管道
- 设计上安全处理空值
🟠 高级模式
可选模式 2:可选链与组合
规则: 使用 Option 组合器(map、flatMap、ap)组合可能失败的操作,创建可读且可维护的管道。
好例子:
这个例子演示了可选链模式。
import { Effect, Option, pipe } from "effect";
interface User {
id: string;
name: string;
email: string;
}
interface Profile {
bio: string;
website?: string;
avatar?: string;
}
interface Settings {
theme: "light" | "dark";
notifications: boolean;
language: string;
}
const program = Effect.gen(function* () {
console.log(`
[可选链] 组合 Option 操作
`);
// 示例 1:使用 map 的简单链
console.log(`[1] 使用 map() 链式转换:
`);
const userId: Option.Option<string> = Option.some("user-42");
const userDisplayId = Option.map(userId, (id) => `User#${id}`);
const idMessage = Option.match(userDisplayId, {
onSome: (display) => display,
onNone: () => "无用户 ID",
});
yield* Effect.log(`[链 1] ${idMessage}`);
// 链式映射
const email: Option.Option<string> = Option.some("alice@example.com");
const emailParts = pipe(
email,
Option.map((e) => e.toLowerCase()),
Option.map((e) => e.split("@")),
Option.map((parts) => parts[0]) // 用户名
);
const username = Option.getOrElse(emailParts, () => "未知");
yield* Effect.log(`[用户名] ${username}
`);
// 示例 2:使用 flatMap 链式返回 Option 的操作
console.log(`[2] 使用 flatMap() 链式操作:
`);
const findUser = (id: string): Option.Option<User> =>
id === "user-42"
? Option.some({
id,
name: "Alice",
email: "alice@example.com",
})
: Option.none();
const getProfile = (userId: string): Option.Option<Profile> =>
userId === "user-42"
? Option.some({
bio: "软件工程师",
website: "alice.dev",
avatar: "https://example.com/avatar.jpg",
})
: Option.none();
const userProfile = pipe(
Option.some("user-42"),
Option.flatMap((id) => findUser(id)),
Option.flatMap((user) => getProfile(user.id))
);
const profileInfo = Option.match(userProfile, {
onSome: (profile) => `简介: ${profile.bio}, 网站: ${profile.website}`,
onNone: () => "未找到简介",
});
yield* Effect.log(`[简介] ${profileInfo}
`);
// 示例 3:复杂管道
console.log(`[3] 复杂管道(用户 → 简介 → 设置 → 主题):
`);
const getSettings = (userId: string): Option.Option<Settings> =>
userId === "user-42"
? Option.some({
theme: "深色",
notifications: true,
language: "en",
})
: Option.none();
const userTheme = pipe(
Option.some("user-42"),
Option.flatMap((id) => findUser(id)),
Option.flatMap((user) => getSettings(user.id)),
Option.map((settings) => settings.theme)
);
const theme = Option.getOrElse(userTheme, () => "浅色");
yield* Effect.log(`[主题] ${theme}`);
// 即使任何步骤为 None,结果也为 None
const invalidUserTheme = pipe(
Option.some("invalid-user"),
Option.flatMap((id) => findUser(id)),
Option.flatMap((user) => getSettings(user.id)),
Option.map((settings) => settings.theme)
);
const invalidTheme = Option.getOrElse(invalidUserTheme, () => "浅色");
yield* Effect.log(`[默认主题] ${invalidTheme}
`);
// 示例 4:使用 ap() 组合独立的 Options
console.log(`[4] 使用 ap() 组合值:
`);
const firstName: Option.Option<string> = Option.some("John");
const lastName: Option.Option<string> = Option.some("Doe");
// 创建一个包裹在 Option 中的函数
const combineNames = (first: string) => (last: string) =>
`${first} ${last}`;
const fullName = pipe(
Option.some(combineNames),
Option.ap(firstName),
Option.ap(lastName)
);
const name = Option.getOrElse(fullName, () => "未知");
yield* Effect.log(`[组合] ${name}`);
// 如果任何为 None
const noLastName: Option.Option<string> = Option.none();
const incompleteName = pipe(
Option.some(combineNames),
Option.ap(firstName),
Option.ap(noLastName)
);
const incompleteFull = Option.getOrElse(incompleteName, () => "不完整");
yield* Effect.log(`[不完整] ${incompleteFull}
`);
// 示例 5:使用 traverse 处理集合
console.log(`[5] 处理集合(traverse):
`);
const userIds: string[] = ["user-42", "user-99", "user-1"];
// 尝试加载所有用户
const allUsers = Option.all(
userIds.map((id) => findUser(id))
);
const usersMessage = Option.match(allUsers, {
onSome: (users) => `加载了 ${users.length} 个用户`,
onNone: () => "未找到某些用户",
});
yield* Effect.log(`[遍历] ${usersMessage}
`);
// 示例 6:使用 orElse 进行多选项回退
console.log(`[6] 使用 orElse() 的回退链:
`);
const getPrimaryEmail = (): Option.Option<string> => Option.none();
const getSecondaryEmail = (): Option.Option<string> =>
Option.some("backup@example.com");
const getTertiaryEmail = (): Option.Option<string> =>
Option.some("tertiary@example.com");
const email1 = pipe(
getPrimaryEmail(),
Option.orElse(() => getSecondaryEmail()),
Option.orElse(() => getTertiaryEmail())
);
const contactEmail = Option.getOrElse(email1, () => "no-email@example.com");
yield* Effect.log(`[回退] 使用邮箱: ${contactEmail}
`);
// 示例 7:过滤选项
console.log(`[7] 使用谓词过滤:
`);
const age: Option.Option<number> = Option.some(25);
const canVote = pipe(
age,
Option.filter((a) => a >= 18)
);
const voteStatus = Option.match(canVote, {
onSome: () => "可以投票",
onNone: () => "太年轻无法投票",
});
yield* Effect.log(`[过滤] ${voteStatus}`);
// 链式多个过滤器
const score: Option.Option<number> = Option.some(85);
const isAGrade = pipe(
score,
Option.filter((s) => s >= 80),
Option.filter((s) => s < 90)
);
const grade = Option.match(isAGrade, {
onSome: () => "等级 A",
onNone: () => "不在 A 范围内",
});
yield* Effect.log(`[等级] ${grade}
`);
// 示例 8:实践:数据库查询链
console.log(`[8] 现实世界:数据库记录链:
`);
const getRecord = (id: string): Option.Option<{ data: string; nested: { value: number } }> =>
id === "rec-1"
? Option.some({
data: "内容",
nested: { value: 42 },
})
: Option.none();
const recordValue = pipe(
Option.some("rec-1"),
Option.flatMap((id) => getRecord(id)),
Option.map((rec) => rec.nested),
Option.map((nested) => nested.value),
Option.map((value) => value * 2)
);
const finalValue = Option.getOrElse(recordValue, () => 0);
yield* Effect.log(`[值] ${finalValue}`);
// 缺失记录
const missingValue = pipe(
Option.some("rec-999"),
Option.flatMap((id) => getRecord(id)),
Option.map((rec) => rec.nested),
Option.map((nested) => nested.value),
Option.map((value) => value * 2)
);
const defaultValue = Option.getOrElse(missingValue, () => 0);
yield* Effect.log(`[默认] ${defaultValue}
`);
// 示例 9:条件链
console.log(`[9] 条件路径:
`);
const loadUserWithFallback = (id: string) =>
pipe(
findUser(id),
Option.flatMap((user) =>
// 仅当用户存在时获取高级功能
user.name.includes("Alice")
? Option.some({ ...user, isPremium: true })
: Option.none()
),
Option.orElse(() =>
// 回退:返回基本用户
findUser(id)
)
);
const result1 = loadUserWithFallback("user-42");
const result2 = loadUserWithFallback("user-99");
yield* Effect.log(
`[条件 1] ${Option.match(result1, { onSome: (u) => `${u.name}`, onNone: () => "未找到" })}`
);
yield* Effect.log(
`[条件 2] ${Option.match(result2, { onSome: (u) => `${u.name}`, onNone: () => "未找到" })}`
);
});
Effect.runPromise(program);
原理:
Option 链式操作实现优雅数据流:
- map:如果值存在则转换
- flatMap:链式返回 Option 的操作
- ap:应用包裹在 Option 中的函数
- traverse:在集合上映射 Option
- 组合:组合多个链
- 恢复:提供回退
模式:使用 Option.map()、flatMap()、ap()、pipe 运算符
嵌套 Option 处理变得复杂:
问题 1:末日金字塔
if (user !== null) {
if (user.profile !== null) {
if (user.profile.preferences !== null) {
if (user.profile.preferences.theme !== null) {
// 最终执行操作
}
}
}
}
问题 2:重复空值检查
- 每个步骤都需要自己的检查
- 代码重复
- 难以重构
- 容易引入错误
问题 3:逻辑分散
- 转换逻辑与空值检查混合
- 难以理解意图
- 易出错
解决方案:
Option 链式:
None自动流过- 仅在
Some时转换 - 无需中间检查
组合:
- 清洁组合函数
- 分离关注点
- 可重用部件
回退:
orElse()用于恢复- 链式多个备选方案
- 优雅降级