mcpField
P
Pragmatic Functional Programming

Pragmatic Functional Programming

github.com/whatiskadudoing开发1460 次安装39 周前更新
代码仓库

关于 Pragmatic Functional Programming

此指南面向希望在适当场景采用函数式编程的开发者,以 fp-ts 示例讲解 pipe、Option、Either、map 和 flatMap。它强调可读性优先,并说明何时应避免函数式写法。

主要功能

  1. 01用 pipe 串联数据转换
  2. 02用 Option 处理缺失值
  3. 03用 Either 显式表达错误
  4. 04用 map 与 flatMap 转换和串联
  5. 05按可读性判断何时使用函数式编程

适用场景

  1. 01整理多步数据转换
  2. 02表达可选值及其默认处理
  3. 03返回可检查的预期错误
  4. 04串联可能失败的操作

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/whatiskadudoing/fp-ts-skills --skill pragmatic-functional-programming

安装后在 Claude Code 中输入/pragmatic-functional-programming确认已加载。

详细介绍

实用函数式编程

先读这里。 本指南拨开学术术语的迷雾,告诉你真正重要的内容。没有范畴论,没有抽象空谈,只有能让代码更好的模式。

黄金法则

如果函数式编程让你的代码更难读,就不要用它。

函数式编程是一种工具,不是信仰。能帮上忙时就用,不适用时就跳过。


函数式编程的二八法则

掌握这五种模式,就能获得大部分好处。先学好它们,再探索其他内容。

1. Pipe:清晰地串联操作

不必嵌套函数调用或创建中间变量,按阅读顺序串联操作。

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
)

适合使用 pipe 的情况:

  • 对同一数据进行 3 次或更多转换
  • 你发现自己总在给临时变量命名
  • 从上到下读逻辑更清晰

不适合使用 pipe 的情况:

  • 只有 1~2 个操作(直接调用就可以)
  • 这些操作之间并不适合自然串联
2. Option:处理缺失值,无需检查 null

别再到处写 if (x !== null && x !== undefined) 了。

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')
  )

通俗解释:

  • O.fromNullable(x) = “包装这个值,将 null/undefined 视为‘没有值’”
  • O.flatMap(fn) = “如果有值,就应用这个函数”
  • O.getOrElse(() => default) = “取出值;如果没有值,就使用这个默认值”
3. Either:明确表示错误

对于预期中的失败,不要再抛出异常,而是将错误作为值返回。

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}`)
}

通俗解释:

  • E.right(value) = “成功,值就是这个”
  • E.left(error) = “失败,错误就是这个”
  • E.isRight(x) = “是否成功?”
4. Map:无需拆出值即可转换

在容器内转换值,无需先将它们取出。

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)
)
5. FlatMap:串联可能失败的操作

如果每个步骤都可能失败,就将它们串联起来。

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('[email protected]')
// Any failure: Left('specific error message')

通俗解释:flatMap 的意思是“如果这一步成功了,就尝试下一步”


何时不该使用函数式编程

函数式编程并非总是最佳选择。以下情况,保持简单即可。

简单的 null 检查
// 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')
)
简单循环
// 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
)
对性能要求严格的代码
// 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))
团队不了解函数式编程时

如果只有你一个人看得懂这段代码,那它就不是好代码。

// 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))
  )

快速改进:今天就能让代码更好的简单改动

1. 用 pipe + fold 替代嵌套三元表达式
// 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}`
  )
)
2. 用 tryCatch 替代 try-catch
// 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)
)
3. 用 Option 替代返回 undefined
// 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))
}
4. 用类型化错误替代错误字符串
// 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' })
}
5. 使用 const 断言表示错误类型
// 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(', ')
  }
}

常见重构:前后对比

将回调地狱改为 Pipe
// 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))
)
将多重 null 检查改为 Option 链
// 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)
  )
)
多重检查的验证
// 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 }))
  )
将 Promise 链改为 TaskEither
// 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)
  )

可读性法则

使用任何函数式编程模式之前,先问问自己:“初级开发者能看懂吗?”

过于巧妙(避免)
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))
)
恰到好处(推荐)
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.none
折中方案(通常最佳)
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
)

速查表

你想做什么通俗说法fp-ts
处理 null/undefined“包装这个可空值”O.fromNullable(x)
为缺失值提供默认值“没有值时就用这个”O.getOrElse(() => default)
有值时进行转换“有值就转换它”O.map(fn)
串联可空操作“有值的话,就试试这个”O.flatMap(fn)
返回成功“成功了,值在这里”E.right(value)
返回失败“失败了,原因在这里”E.left(error)
包装可能抛出异常的函数“试着运行,捕获错误”E.tryCatch(fn, onError)
处理两种情况“失败时这样做,成功时那样做”E.fold(onLeft, onRight)
串联操作“接着做这个,再做那个”pipe(x, fn1, fn2, fn3)

何时进阶

熟悉这些模式后,可以继续探索:

  1. TaskEither - 可能失败的异步操作(替代 Promise + try/catch)
  2. Validation - 收集所有错误,而不是遇到第一个错误就停止
  3. Reader - 无需类即可实现依赖注入
  4. Do notation - 多个绑定的语法更简洁

但不要急于求成。这里的基础知识足以应对 80% 的实际场景。先熟练掌握这些,再添置更多工具。


总结

  1. 使用 pipe 处理 3 个或更多操作
  2. 使用 Option 处理可空链
  3. 使用 Either 处理可能失败的操作
  4. 使用 map 转换包装后的值
  5. 使用 flatMap 链接可能失败的操作
  6. 如果影响可读性,就跳过 FP
  7. 保持简单——如果团队看不懂,那就不是好代码

评论

支持 Markdown