fp-ts-pragmatic
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePragmatic Functional Programming
实用函数式编程
Read this first. This guide cuts through the academic jargon and shows you what actually matters. No category theory. No abstract nonsense. Just patterns that make your code better.
请先阅读这部分。本指南摒弃学术术语,只展示真正实用的内容。无需范畴论,无需抽象概念,只提供能让代码更优秀的模式。
When to Use This Skill
何时使用本技能
- When starting with fp-ts and need practical guidance
- When writing TypeScript code that handles nullable values, errors, or async operations
- When you want cleaner, more maintainable functional code without the academic overhead
- When refactoring imperative code to functional style
- 刚开始使用fp-ts,需要实用指导时
- 编写处理可空值、错误或异步操作的TypeScript代码时
- 希望编写更简洁、更易维护的函数式代码,且不想承担学术理论负担时
- 将命令式代码重构为函数式风格时
The Golden Rule
黄金法则
If functional programming makes your code harder to read, don't use it.
FP is a tool, not a religion. Use it when it helps. Skip it when it doesn't.
如果函数式编程让你的代码更难读,那就不要用它。
函数式编程是工具,不是宗教。有用就用,没用就跳过。
The 80/20 of FP
函数式编程的80/20核心内容
These five patterns give you most of the benefits. Master these before exploring anything else.
掌握这五个模式,就能获得函数式编程的大部分收益。在探索其他内容之前,先精通这些模式。
1. Pipe: Chain Operations Clearly
1. Pipe:清晰地链式调用操作
Instead of nesting function calls or creating intermediate variables, chain operations in reading order.
typescript
import { pipe } from 'fp-ts/function'
// Before: Hard to read (inside-out)
const result = format(validate(parse(input)))
// Before: Too many variables
const parsed = parse(input)
const validated = validate(parsed)
const result = format(validated)
// After: Clear, linear flow
const result = pipe(
input,
parse,
validate,
format
)When to use pipe:
- 3+ transformations on the same data
- You find yourself naming throwaway variables
- Logic reads better top-to-bottom
When to skip pipe:
- Just 1-2 operations (direct call is fine)
- The operations don't naturally chain
无需嵌套函数调用或创建中间变量,按阅读顺序链式调用操作。
typescript
import { pipe } from 'fp-ts/function'
// 之前:难以阅读(从内到外)
const result = format(validate(parse(input)))
// 之前:过多中间变量
const parsed = parse(input)
const validated = validate(parsed)
const result = format(validated)
// 之后:清晰的线性流程
const result = pipe(
input,
parse,
validate,
format
)何时使用pipe:
- 对同一数据进行3次及以上转换时
- 你发现自己在命名临时变量时
- 逻辑从上到下阅读更清晰时
何时不使用pipe:
- 仅1-2次操作(直接调用即可)
- 操作之间不适合链式调用时
2. Option: Handle Missing Values Without null Checks
2. Option:无需空值检查即可处理缺失值
Stop writing everywhere.
if (x !== null && x !== undefined)typescript
import * as O from 'fp-ts/Option'
import { pipe } from 'fp-ts/function'
// Before: Defensive null checking
function getUserCity(user: User | null): string {
if (user === null) return 'Unknown'
if (user.address === null) return 'Unknown'
if (user.address.city === null) return 'Unknown'
return user.address.city
}
// After: Chain through potential missing values
const getUserCity = (user: User | null): string =>
pipe(
O.fromNullable(user),
O.flatMap(u => O.fromNullable(u.address)),
O.flatMap(a => O.fromNullable(a.city)),
O.getOrElse(() => 'Unknown')
)Plain language translation:
- = "wrap this value, treating null/undefined as 'nothing'"
O.fromNullable(x) - = "if we have something, apply this function"
O.flatMap(fn) - = "unwrap, or use this default if nothing"
O.getOrElse(() => default)
不要再到处写 。
if (x !== null && x !== undefined)typescript
import * as O from 'fp-ts/Option'
import { pipe } from 'fp-ts/function'
// 之前:防御性空值检查
function getUserCity(user: User | null): string {
if (user === null) return 'Unknown'
if (user.address === null) return 'Unknown'
if (user.address.city === null) return 'Unknown'
return user.address.city
}
// 之后:链式处理可能的缺失值
const getUserCity = (user: User | null): string =>
pipe(
O.fromNullable(user),
O.flatMap(u => O.fromNullable(u.address)),
O.flatMap(a => O.fromNullable(a.city)),
O.getOrElse(() => 'Unknown')
)通俗解释:
- = "包装这个值,将null/undefined视为'无'"
O.fromNullable(x) - = "如果有值,就应用这个函数"
O.flatMap(fn) - = "解包,如果无值则使用默认值"
O.getOrElse(() => default)
3. Either: Make Errors Explicit
3. Either:显式处理错误
Stop throwing exceptions for expected failures. Return errors as values.
typescript
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
// Before: Hidden failure mode
function parseAge(input: string): number {
const age = parseInt(input, 10)
if (isNaN(age)) throw new Error('Invalid age')
if (age < 0) throw new Error('Age cannot be negative')
return age
}
// After: Errors are visible in the type
function parseAge(input: string): E.Either<string, number> {
const age = parseInt(input, 10)
if (isNaN(age)) return E.left('Invalid age')
if (age < 0) return E.left('Age cannot be negative')
return E.right(age)
}
// Using it
const result = parseAge(userInput)
if (E.isRight(result)) {
console.log(`Age is ${result.right}`)
} else {
console.log(`Error: ${result.left}`)
}Plain language translation:
- = "success with this value"
E.right(value) - = "failure with this error"
E.left(error) - = "did it succeed?"
E.isRight(x)
不要再为预期的失败抛出异常,将错误作为值返回。
typescript
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
// 之前:隐藏的失败模式
function parseAge(input: string): number {
const age = parseInt(input, 10)
if (isNaN(age)) throw new Error('Invalid age')
if (age < 0) throw new Error('Age cannot be negative')
return age
}
// 之后:错误在类型中可见
function parseAge(input: string): E.Either<string, number> {
const age = parseInt(input, 10)
if (isNaN(age)) return E.left('Invalid age')
if (age < 0) return E.left('Age cannot be negative')
return E.right(age)
}
// 使用示例
const result = parseAge(userInput)
if (E.isRight(result)) {
console.log(`Age is ${result.right}`)
} else {
console.log(`Error: ${result.left}`)
}通俗解释:
- = "成功,返回该值"
E.right(value) - = "失败,返回该错误"
E.left(error) - = "是否成功?"
E.isRight(x)
4. Map: Transform Without Unpacking
4. Map:无需解包即可转换值
Transform values inside containers without extracting them first.
typescript
import * as O from 'fp-ts/Option'
import * as E from 'fp-ts/Either'
import * as A from 'fp-ts/Array'
import { pipe } from 'fp-ts/function'
// Transform inside Option
const maybeUser: O.Option<User> = O.some({ name: 'Alice', age: 30 })
const maybeName: O.Option<string> = pipe(
maybeUser,
O.map(user => user.name)
)
// Transform inside Either
const result: E.Either<Error, number> = E.right(5)
const doubled: E.Either<Error, number> = pipe(
result,
E.map(n => n * 2)
)
// Transform arrays (same concept!)
const numbers = [1, 2, 3]
const doubled = pipe(
numbers,
A.map(n => n * 2)
)无需先提取容器内的值,直接转换容器内的值。
typescript
import * as O from 'fp-ts/Option'
import * as E from 'fp-ts/Either'
import * as A from 'fp-ts/Array'
import { pipe } from 'fp-ts/function'
// 转换Option内的值
const maybeUser: O.Option<User> = O.some({ name: 'Alice', age: 30 })
const maybeName: O.Option<string> = pipe(
maybeUser,
O.map(user => user.name)
)
// 转换Either内的值
const result: E.Either<Error, number> = E.right(5)
const doubled: E.Either<Error, number> = pipe(
result,
E.map(n => n * 2)
)
// 转换数组(概念相同!)
const numbers = [1, 2, 3]
const doubled = pipe(
numbers,
A.map(n => n * 2)
)5. FlatMap: Chain Operations That Might Fail
5. FlatMap:链式调用可能失败的操作
When each step might fail, chain them together.
typescript
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
const parseJSON = (s: string): E.Either<string, unknown> =>
E.tryCatch(() => JSON.parse(s), () => 'Invalid JSON')
const extractEmail = (data: unknown): E.Either<string, string> => {
if (typeof data === 'object' && data !== null && 'email' in data) {
return E.right((data as { email: string }).email)
}
return E.left('No email field')
}
const validateEmail = (email: string): E.Either<string, string> =>
email.includes('@') ? E.right(email) : E.left('Invalid email format')
// Chain all steps - if any fails, the whole thing fails
const getValidEmail = (input: string): E.Either<string, string> =>
pipe(
parseJSON(input),
E.flatMap(extractEmail),
E.flatMap(validateEmail)
)
// Success path: Right('user@example.com')
// Any failure: Left('specific error message')Plain language: means "if this succeeded, try the next thing"
flatMap当每个步骤都可能失败时,将它们链式调用。
typescript
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
const parseJSON = (s: string): E.Either<string, unknown> =>
E.tryCatch(() => JSON.parse(s), () => 'Invalid JSON')
const extractEmail = (data: unknown): E.Either<string, string> => {
if (typeof data === 'object' && data !== null && 'email' in data) {
return E.right((data as { email: string }).email)
}
return E.left('No email field')
}
const validateEmail = (email: string): E.Either<string, string> =>
email.includes('@') ? E.right(email) : E.left('Invalid email format')
// 链式调用所有步骤——任何一步失败,整个流程都会失败
const getValidEmail = (input: string): E.Either<string, string> =>
pipe(
parseJSON(input),
E.flatMap(extractEmail),
E.flatMap(validateEmail)
)
// 成功路径:Right('user@example.com')
// 任何失败:Left('具体错误信息')通俗解释: 意为 "如果这一步成功,就尝试下一步"
flatMapWhen NOT to Use FP
何时不使用函数式编程
Functional programming is not always the answer. Here's when to keep it simple.
函数式编程并非万能解。以下场景请保持简单。
Simple Null Checks
简单空值检查
typescript
// Just use optional chaining - it's built into the language
const city = user?.address?.city ?? 'Unknown'
// DON'T overcomplicate it
const city = pipe(
O.fromNullable(user),
O.flatMap(u => O.fromNullable(u.address)),
O.flatMap(a => O.fromNullable(a.city)),
O.getOrElse(() => 'Unknown')
)typescript
// 直接使用可选链——这是语言内置的功能
const city = user?.address?.city ?? 'Unknown'
// 不要过度复杂化
const city = pipe(
O.fromNullable(user),
O.flatMap(u => O.fromNullable(u.address)),
O.flatMap(a => O.fromNullable(a.city)),
O.getOrElse(() => 'Unknown')
)Simple Loops
简单循环
typescript
// A for loop is fine when you need early exit or complex logic
function findFirst(items: Item[], predicate: (i: Item) => boolean): Item | null {
for (const item of items) {
if (predicate(item)) return item
}
return null
}
// DON'T force FP when it doesn't help
const result = pipe(
items,
A.findFirst(predicate),
O.toNullable
)typescript
// 当你需要提前退出或复杂逻辑时,for循环就足够了
function findFirst(items: Item[], predicate: (i: Item) => boolean): Item | null {
for (const item of items) {
if (predicate(item)) return item
}
return null
}
// 不要强行使用函数式编程
const result = pipe(
items,
A.findFirst(predicate),
O.toNullable
)Performance-Critical Code
性能关键代码
typescript
// For hot paths, imperative is faster (no intermediate arrays)
function sumLarge(numbers: number[]): number {
let sum = 0
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i]
}
return sum
}
// fp-ts creates intermediate structures
const sum = pipe(numbers, A.reduce(0, (acc, n) => acc + n))typescript
// 对于热点路径,命令式代码更快(无中间数组)
function sumLarge(numbers: number[]): number {
let sum = 0
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i]
}
return sum
}
// fp-ts会创建中间结构
const sum = pipe(numbers, A.reduce(0, (acc, n) => acc + n))When Your Team Doesn't Know FP
团队不熟悉函数式编程时
If you're the only one who can read the code, it's not good code.
typescript
// If your team knows this pattern
async function getUser(id: string): Promise<User | null> {
try {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) return null
return await response.json()
} catch {
return null
}
}
// Don't force this on them
const getUser = (id: string): TE.TaskEither<Error, User> =>
pipe(
TE.tryCatch(() => fetch(`/api/users/${id}`), E.toError),
TE.flatMap(r => r.ok ? TE.right(r) : TE.left(new Error('Not found'))),
TE.flatMap(r => TE.tryCatch(() => r.json(), E.toError))
)如果只有你能读懂代码,那它不是好代码。
typescript
// 如果你的团队熟悉这种模式
async function getUser(id: string): Promise<User | null> {
try {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) return null
return await response.json()
} catch {
return null
}
}
// 不要强行使用这种写法
const getUser = (id: string): TE.TaskEither<Error, User> =>
pipe(
TE.tryCatch(() => fetch(`/api/users/${id}`), E.toError),
TE.flatMap(r => r.ok ? TE.right(r) : TE.left(new Error('Not found'))),
TE.flatMap(r => TE.tryCatch(() => r.json(), E.toError))
)Quick Wins: Easy Changes That Improve Code Today
快速优化:今天就能提升代码的简单改动
1. Replace Nested Ternaries with pipe + fold
1. 用pipe + fold替换嵌套三元表达式
typescript
// Before: Nested ternary nightmare
const message = user === null
? 'No user'
: user.isAdmin
? `Admin: ${user.name}`
: `User: ${user.name}`
// After: Clear case handling
const message = pipe(
O.fromNullable(user),
O.fold(
() => 'No user',
(u) => u.isAdmin ? `Admin: ${u.name}` : `User: ${u.name}`
)
)typescript
// 之前:嵌套三元表达式噩梦
const message = user === null
? 'No user'
: user.isAdmin
? `Admin: ${user.name}`
: `User: ${user.name}`
// 之后:清晰的分支处理
const message = pipe(
O.fromNullable(user),
O.fold(
() => 'No user',
(u) => u.isAdmin ? `Admin: ${u.name}` : `User: ${u.name}`
)
)2. Replace try-catch with tryCatch
2. 用tryCatch替换try-catch
typescript
// Before: try-catch everywhere
let config
try {
config = JSON.parse(rawConfig)
} catch {
config = defaultConfig
}
// After: One-liner
const config = pipe(
E.tryCatch(() => JSON.parse(rawConfig), () => 'parse error'),
E.getOrElse(() => defaultConfig)
)typescript
// 之前:到处都是try-catch
let config
try {
config = JSON.parse(rawConfig)
} catch {
config = defaultConfig
}
// 之后:一行代码搞定
const config = pipe(
E.tryCatch(() => JSON.parse(rawConfig), () => 'parse error'),
E.getOrElse(() => defaultConfig)
)3. Replace undefined Returns with Option
3. 用Option替换返回undefined
typescript
// Before: Caller might forget to check
function findUser(id: string): User | undefined {
return users.find(u => u.id === id)
}
// After: Type forces caller to handle missing case
function findUser(id: string): O.Option<User> {
return O.fromNullable(users.find(u => u.id === id))
}typescript
// 之前:调用者可能会忘记检查
function findUser(id: string): User | undefined {
return users.find(u => u.id === id)
}
// 之后:类型强制要求调用者处理缺失情况
function findUser(id: string): O.Option<User> {
return O.fromNullable(users.find(u => u.id === id))
}4. Replace Error Strings with Typed Errors
4. 用类型化错误替换错误字符串
typescript
// Before: Just strings
function validate(data: unknown): E.Either<string, User> {
// ...
return E.left('validation failed')
}
// After: Structured errors
type ValidationError = {
field: string
message: string
}
function validate(data: unknown): E.Either<ValidationError, User> {
// ...
return E.left({ field: 'email', message: 'Invalid format' })
}typescript
// 之前:仅返回字符串
function validate(data: unknown): E.Either<string, User> {
// ...
return E.left('validation failed')
}
// 之后:结构化错误
type ValidationError = {
field: string
message: string
}
function validate(data: unknown): E.Either<ValidationError, User> {
// ...
return E.left({ field: 'email', message: 'Invalid format' })
}5. Use const Assertions for Error Types
5. 用const断言定义错误类型
typescript
// Create specific error types without classes
const NotFound = (id: string) => ({ _tag: 'NotFound' as const, id })
const Unauthorized = { _tag: 'Unauthorized' as const }
const ValidationFailed = (errors: string[]) =>
({ _tag: 'ValidationFailed' as const, errors })
type AppError =
| ReturnType<typeof NotFound>
| typeof Unauthorized
| ReturnType<typeof ValidationFailed>
// Now you can pattern match
const handleError = (error: AppError): string => {
switch (error._tag) {
case 'NotFound': return `Item ${error.id} not found`
case 'Unauthorized': return 'Please log in'
case 'ValidationFailed': return error.errors.join(', ')
}
}typescript
// 无需类即可创建特定错误类型
const NotFound = (id: string) => ({ _tag: 'NotFound' as const, id })
const Unauthorized = { _tag: 'Unauthorized' as const }
const ValidationFailed = (errors: string[]) =>
({ _tag: 'ValidationFailed' as const, errors })
type AppError =
| ReturnType<typeof NotFound>
| typeof Unauthorized
| ReturnType<typeof ValidationFailed>
// 现在可以进行模式匹配
const handleError = (error: AppError): string => {
switch (error._tag) {
case 'NotFound': return `Item ${error.id} not found`
case 'Unauthorized': return 'Please log in'
case 'ValidationFailed': return error.errors.join(', ')
}
}Common Refactors: Before and After
常见重构:前后对比
Callback Hell to Pipe
回调地狱转pipe
typescript
// Before
fetchUser(id, (user) => {
if (!user) return handleNoUser()
fetchPosts(user.id, (posts) => {
if (!posts) return handleNoPosts()
fetchComments(posts[0].id, (comments) => {
render(user, posts, comments)
})
})
})
// After (with TaskEither for async)
import * as TE from 'fp-ts/TaskEither'
const loadData = (id: string) =>
pipe(
fetchUser(id),
TE.flatMap(user => pipe(
fetchPosts(user.id),
TE.map(posts => ({ user, posts }))
)),
TE.flatMap(({ user, posts }) => pipe(
fetchComments(posts[0].id),
TE.map(comments => ({ user, posts, comments }))
))
)
// Execute
const result = await loadData('123')()
pipe(
result,
E.fold(handleError, ({ user, posts, comments }) => render(user, posts, comments))
)typescript
// 之前
fetchUser(id, (user) => {
if (!user) return handleNoUser()
fetchPosts(user.id, (posts) => {
if (!posts) return handleNoPosts()
fetchComments(posts[0].id, (comments) => {
render(user, posts, comments)
})
})
})
// 之后(使用TaskEither处理异步)
import * as TE from 'fp-ts/TaskEither'
const loadData = (id: string) =>
pipe(
fetchUser(id),
TE.flatMap(user => pipe(
fetchPosts(user.id),
TE.map(posts => ({ user, posts }))
)),
TE.flatMap(({ user, posts }) => pipe(
fetchComments(posts[0].id),
TE.map(comments => ({ user, posts, comments }))
))
)
// 执行
const result = await loadData('123')()
pipe(
result,
E.fold(handleError, ({ user, posts, comments }) => render(user, posts, comments))
)Multiple null Checks to Option Chain
多重空值检查转Option链式调用
typescript
// Before
function getManagerEmail(employee: Employee): string | null {
if (!employee.department) return null
if (!employee.department.manager) return null
if (!employee.department.manager.email) return null
return employee.department.manager.email
}
// After
const getManagerEmail = (employee: Employee): O.Option<string> =>
pipe(
O.fromNullable(employee.department),
O.flatMap(d => O.fromNullable(d.manager)),
O.flatMap(m => O.fromNullable(m.email))
)
// Use it
pipe(
getManagerEmail(employee),
O.fold(
() => sendToDefault(),
(email) => sendTo(email)
)
)typescript
// 之前
function getManagerEmail(employee: Employee): string | null {
if (!employee.department) return null
if (!employee.department.manager) return null
if (!employee.department.manager.email) return null
return employee.department.manager.email
}
// 之后
const getManagerEmail = (employee: Employee): O.Option<string> =>
pipe(
O.fromNullable(employee.department),
O.flatMap(d => O.fromNullable(d.manager)),
O.flatMap(m => O.fromNullable(m.email))
)
// 使用示例
pipe(
getManagerEmail(employee),
O.fold(
() => sendToDefault(),
(email) => sendTo(email)
)
)Validation with Multiple Checks
多步骤验证
typescript
// Before: Throws on first error
function validateUser(data: unknown): User {
if (!data || typeof data !== 'object') throw new Error('Must be object')
const obj = data as Record<string, unknown>
if (typeof obj.email !== 'string') throw new Error('Email required')
if (!obj.email.includes('@')) throw new Error('Invalid email')
if (typeof obj.age !== 'number') throw new Error('Age required')
if (obj.age < 0) throw new Error('Age must be positive')
return obj as User
}
// After: Returns first error, type-safe
const validateUser = (data: unknown): E.Either<string, User> =>
pipe(
E.Do,
E.bind('obj', () =>
typeof data === 'object' && data !== null
? E.right(data as Record<string, unknown>)
: E.left('Must be object')
),
E.bind('email', ({ obj }) =>
typeof obj.email === 'string' && obj.email.includes('@')
? E.right(obj.email)
: E.left('Valid email required')
),
E.bind('age', ({ obj }) =>
typeof obj.age === 'number' && obj.age >= 0
? E.right(obj.age)
: E.left('Valid age required')
),
E.map(({ email, age }) => ({ email, age }))
)typescript
// 之前:第一个错误就抛出
function validateUser(data: unknown): User {
if (!data || typeof data !== 'object') throw new Error('Must be object')
const obj = data as Record<string, unknown>
if (typeof obj.email !== 'string') throw new Error('Email required')
if (!obj.email.includes('@')) throw new Error('Invalid email')
if (typeof obj.age !== 'number') throw new Error('Age required')
if (obj.age < 0) throw new Error('Age must be positive')
return obj as User
}
// 之后:返回第一个错误,类型安全
const validateUser = (data: unknown): E.Either<string, User> =>
pipe(
E.Do,
E.bind('obj', () =>
typeof data === 'object' && data !== null
? E.right(data as Record<string, unknown>)
: E.left('Must be object')
),
E.bind('email', ({ obj }) =>
typeof obj.email === 'string' && obj.email.includes('@')
? E.right(obj.email)
: E.left('Valid email required')
),
E.bind('age', ({ obj }) =>
typeof obj.age === 'number' && obj.age >= 0
? E.right(obj.age)
: E.left('Valid age required')
),
E.map(({ email, age }) => ({ email, age }))
)Promise Chain to TaskEither
Promise链式调用转TaskEither
typescript
// Before
async function processOrder(orderId: string): Promise<Receipt> {
const order = await fetchOrder(orderId)
if (!order) throw new Error('Order not found')
const validated = await validateOrder(order)
if (!validated.success) throw new Error(validated.error)
const payment = await processPayment(validated.order)
if (!payment.success) throw new Error('Payment failed')
return generateReceipt(payment)
}
// After
const processOrder = (orderId: string): TE.TaskEither<string, Receipt> =>
pipe(
fetchOrderTE(orderId),
TE.flatMap(order =>
order ? TE.right(order) : TE.left('Order not found')
),
TE.flatMap(validateOrderTE),
TE.flatMap(processPaymentTE),
TE.map(generateReceipt)
)typescript
// 之前
async function processOrder(orderId: string): Promise<Receipt> {
const order = await fetchOrder(orderId)
if (!order) throw new Error('Order not found')
const validated = await validateOrder(order)
if (!validated.success) throw new Error(validated.error)
const payment = await processPayment(validated.order)
if (!payment.success) throw new Error('Payment failed')
return generateReceipt(payment)
}
// 之后
const processOrder = (orderId: string): TE.TaskEither<string, Receipt> =>
pipe(
fetchOrderTE(orderId),
TE.flatMap(order =>
order ? TE.right(order) : TE.left('Order not found')
),
TE.flatMap(validateOrderTE),
TE.flatMap(processPaymentTE),
TE.map(generateReceipt)
)The Readability Rule
可读性原则
Before using any FP pattern, ask: "Would a junior developer understand this?"
使用任何函数式编程模式之前,请自问:"初级开发者能读懂吗?"
Too Clever (Avoid)
过于复杂(避免)
typescript
const result = pipe(
data,
A.filter(flow(prop('status'), equals('active'))),
A.map(flow(prop('value'), multiply(2))),
A.reduce(monoid.concat, monoid.empty),
O.fromPredicate(gt(threshold))
)typescript
const result = pipe(
data,
A.filter(flow(prop('status'), equals('active'))),
A.map(flow(prop('value'), multiply(2))),
A.reduce(monoid.concat, monoid.empty),
O.fromPredicate(gt(threshold))
)Just Right (Prefer)
恰到好处(推荐)
typescript
const activeItems = data.filter(item => item.status === 'active')
const doubledValues = activeItems.map(item => item.value * 2)
const total = doubledValues.reduce((sum, val) => sum + val, 0)
const result = total > threshold ? O.some(total) : O.nonetypescript
const activeItems = data.filter(item => item.status === 'active')
const doubledValues = activeItems.map(item => item.value * 2)
const total = doubledValues.reduce((sum, val) => sum + val, 0)
const result = total > threshold ? O.some(total) : O.noneThe Middle Ground (Often Best)
折中方案(通常最佳)
typescript
const result = pipe(
data,
A.filter(item => item.status === 'active'),
A.map(item => item.value * 2),
A.reduce(0, (sum, val) => sum + val),
total => total > threshold ? O.some(total) : O.none
)typescript
const result = pipe(
data,
A.filter(item => item.status === 'active'),
A.map(item => item.value * 2),
A.reduce(0, (sum, val) => sum + val),
total => total > threshold ? O.some(total) : O.none
)Cheat Sheet
速查表
| What you want | Plain language | fp-ts |
|---|---|---|
| Handle null/undefined | "Wrap this nullable" | |
| Default for missing | "Use this if nothing" | |
| Transform if present | "If something, change it" | |
| Chain nullable operations | "If something, try this" | |
| Return success | "Worked, here's the value" | |
| Return failure | "Failed, here's why" | |
| Wrap throwing function | "Try this, catch errors" | |
| Handle both cases | "Do this for error, that for success" | |
| Chain operations | "Then do this, then that" | |
| 需求 | 通俗描述 | fp-ts实现 |
|---|---|---|
| 处理null/undefined | "包装可空值" | |
| 缺失值的默认值 | "无值时使用默认值" | |
| 转换存在的值 | "如果有值,就转换它" | |
| 链式处理可空操作 | "如果有值,就尝试下一步" | |
| 返回成功结果 | "成功,返回该值" | |
| 返回失败结果 | "失败,返回原因" | |
| 包装可能抛出异常的函数 | "尝试执行,捕获错误" | |
| 处理成功/失败两种情况 | "错误时执行这个,成功时执行那个" | |
| 链式调用操作 | "先执行这个,再执行那个" | |
When to Level Up
何时进阶
Once comfortable with these patterns, explore:
- TaskEither - Async operations that can fail (replaces Promise + try/catch)
- Validation - Collect ALL errors instead of stopping at first
- Reader - Dependency injection without classes
- Do notation - Cleaner syntax for multiple bindings
But don't rush. The basics here will handle 80% of real-world scenarios. Get comfortable with these before adding more tools to your belt.
熟练掌握这些模式后,可以探索:
- TaskEither - 处理可能失败的异步操作(替代Promise + try/catch)
- Validation - 收集所有错误而非在第一个错误时停止
- Reader - 无需类的依赖注入
- Do表示法 - 更简洁的多绑定语法
但不要急于求成。这里的基础内容可以处理80%的实际场景。在添加更多工具之前,先熟练掌握这些。
Summary
总结
- Use pipe for 3+ operations
- Use Option for nullable chains
- Use Either for operations that can fail
- Use map to transform wrapped values
- Use flatMap to chain operations that might fail
- Skip FP when it hurts readability
- Keep it simple - if your team can't read it, it's not good code
- 使用pipe处理3次及以上操作
- 使用Option处理可空值链式调用
- 使用Either处理可能失败的操作
- 使用map转换包装内的值
- 使用flatMap链式调用可能失败的操作
- 避免函数式编程如果它降低了可读性
- 保持简单 - 如果你的团队读不懂,那它就不是好代码