强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

函数式编程艺术 / 15 函数式错误处理

15 函数式错误处理

“异常破坏了函数的纯粹性——函数式编程用类型系统来表达可能的失败。”


15.1 传统错误处理的问题

15.1.1 异常的缺点

问题 说明
隐式控制流 异常跳转难以追踪
不安全 未捕获异常导致程序崩溃
非类型安全 函数签名不反映可能的异常
性能开销 异常创建和栈展开有代价
不可组合 异常不能像值一样组合

15.1.2 函数式错误处理的优势

特性 异常 函数式
错误信息 堆栈跟踪 类型签名
组合性 优秀(Monad)
编译检查
纯函数兼容 不兼容 完全兼容
错误恢复 try/catch flatMap/map

15.2 Option/Maybe

用于表示可能缺失的值。

15.2.1 基本用法

Haskell:

-- Maybe:可能缺失的值
safeHead :: [a] -> Maybe a
safeHead []    = Nothing
safeHead (x:_) = Just x

safeDivide :: Int -> Int -> Maybe Int
safeDivide _ 0 = Nothing
safeDivide a b = Just (a `div` b)

-- 链式调用
calculation :: Int -> Maybe Int
calculation x = do
  a <- safeDivide x 2
  b <- safeDivide a 3
  return (a + b)

-- 或使用 >>= 链式
calculation' :: Int -> Maybe Int
calculation' x =
  safeDivide x 2 >>= \a ->
  safeDivide a 3 >>= \b ->
  return (a + b)

Rust:

fn safe_head<T>(items: &[T]) -> Option<&T> {
    items.first()
}

fn safe_divide(a: i32, b: i32) -> Option<i32> {
    if b == 0 { None } else { Some(a / b) }
}

// 使用 ? 运算符链接
fn calculation(x: i32) -> Option<i32> {
    let a = safe_divide(x, 2)?;
    let b = safe_divide(a, 3)?;
    Some(a + b)
}

// 组合操作
let result = Some(10)
    .filter(|&x| x > 5)
    .map(|x| x * 2)
    .unwrap_or(0);
// result = 20

JavaScript:

class Option {
  static some(value) { return new Some(value); }
  static none() { return new None(); }
  static from(value) {
    return value === null || value === undefined ? Option.none() : Option.some(value);
  }
}

class Some extends Option {
  constructor(value) { super(); this.value = value; }
  map(fn) { return Option.some(fn(this.value)); }
  flatMap(fn) { return fn(this.value); }
  filter(pred) { return pred(this.value) ? this : Option.none(); }
  getOrElse(defaultValue) { return this.value; }
  isSome() { return true; }
}

class None extends Option {
  map(fn) { return this; }
  flatMap(fn) { return this; }
  filter(pred) { return this; }
  getOrElse(defaultValue) { return defaultValue; }
  isSome() { return false; }
}

15.3 Either/Result

用于表示成功或失败,附带错误信息。

15.3.1 Either 类型

Haskell:

data Either a b = Left a | Right b

-- Right 表示成功,Left 表示失败
type Result = Either String

validateAge :: Int -> Result Int
validateAge age
  | age < 0   = Left "Age cannot be negative"
  | age > 150 = Left "Age cannot exceed 150"
  | otherwise = Right age

validateEmail :: String -> Result String
validateEmail email
  | '@' `elem` email = Right email
  | otherwise        = Left "Invalid email"

-- 组合验证
validateUser :: String -> Int -> Either String (String, Int)
validateUser email age = do
  validEmail <- validateEmail email
  validAge   <- validateAge age
  return (validEmail, validAge)

-- 结果
validateUser "[email protected]" 30  -- Right ("[email protected]", 30)
validateUser "invalid" (-1)          -- Left "Invalid email"(短路)

Rust:

fn validate_age(age: i32) -> Result<i32, String> {
    if age < 0 { Err("Age cannot be negative".into()) }
    else if age > 150 { Err("Age cannot exceed 150".into()) }
    else { Ok(age) }
}

fn validate_email(email: &str) -> Result<String, String> {
    if email.contains('@') { Ok(email.to_string()) }
    else { Err("Invalid email".into()) }
}

fn validate_user(email: &str, age: i32) -> Result<(String, i32), String> {
    let valid_email = validate_email(email)?;
    let valid_age = validate_age(age)?;
    Ok((valid_email, valid_age))
}

// 使用
match validate_user("[email protected]", 30) {
    Ok((email, age)) => println!("Valid: {} ({})", email, age),
    Err(err) => println!("Error: {}", err),
}

JavaScript:

class Left {
  constructor(value) { this.value = value; }
  map(fn) { return this; }
  flatMap(fn) { return this; }
  fold(leftFn, rightFn) { return leftFn(this.value); }
  swap() { return new Right(this.value); }
  getOr(defaultValue) { return defaultValue; }
}

class Right {
  constructor(value) { this.value = value; }
  map(fn) { return new Right(fn(this.value)); }
  flatMap(fn) { return fn(this.value); }
  fold(leftFn, rightFn) { return rightFn(this.value); }
  swap() { return new Left(this.value); }
  getOr() { return this.value; }
}

const either = { left: v => new Left(v), right: v => new Right(v) };

// 使用
const validateAge = (age) =>
  age < 0 ? either.left("Negative age") :
  age > 150 ? either.left("Too old") :
  either.right(age);

const validateEmail = (email) =>
  email.includes('@') ? either.right(email) : either.left("Invalid email");

const result = validateEmail("[email protected]")
  .flatMap(email => validateAge(30).map(age => ({ email, age })));

result.fold(
  error => console.log("Error:", error),
  user => console.log("Valid:", user)
);

15.4 Validation

Validation 与 Either 类似,但会收集所有错误而非短路。

15.4.1 实现

data Validation a b = Failure a | Success b

instance Semigroup a => Applicative (Validation a) where
  pure = Success
  Failure e1 <*> Failure e2 = Failure (e1 <> e2)  -- 收集所有错误
  Failure e1 <*> Success _  = Failure e1
  Success _  <*> Failure e2 = Failure e2
  Success f  <*> Success x  = Success (f x)

-- 使用
data User = User { name :: String, email :: String, age :: Int }

validateUser :: String -> String -> Int -> Validation [String] User
validateUser name email age =
  User <$> validateName name <*> validateEmail email <*> validateAge age
  where
    validateName n
      | null n    = Failure ["Name required"]
      | otherwise = Success n
    validateEmail e
      | '@' `elem` e = Success e
      | otherwise    = Failure ["Invalid email"]
    validateAge a
      | a >= 0 && a <= 150 = Success a
      | otherwise          = Failure ["Invalid age"]

-- 结果
validateUser "" "invalid" (-1)
-- Failure ["Name required", "Invalid email", "Invalid age"]

JavaScript:

class Validation {
  static success(value) { return new Success(value); }
  static failure(errors) { return new Failure(errors); }
}

class Success {
  constructor(value) { this.value = value; }
  map(fn) { return new Success(fn(this.value)); }
  ap(other) {
    if (other instanceof Failure) return other;
    return new Success(this.value(other.value));
  }
}

class Failure {
  constructor(errors) { this.errors = errors; }
  map(fn) { return this; }
  ap(other) {
    if (other instanceof Failure) return new Failure([...this.errors, ...other.errors]);
    return this;
  }
}

// 使用
const validateUser = (name, email, age) => {
  const validateName = name.length > 0
    ? Validation.success(name)
    : Validation.failure(["Name required"]);

  const validateEmail = email.includes('@')
    ? Validation.success(email)
    : Validation.failure(["Invalid email"]);

  const validateAge = age >= 0 && age <= 150
    ? Validation.success(age)
    : Validation.failure(["Invalid age"]);

  return Validation.success(
    name => email => age => ({ name, email, age })
  ).ap(validateName).ap(validateEmail).ap(validateAge);
};

validateUser("", "invalid", -1);
// Failure(["Name required", "Invalid email", "Invalid age"])

15.5 错误组合

15.5.1 错误处理链

// 组合多个可能失败的操作
const fetchAndProcess = (url) =>
  Task.fromPromise(fetch(url))
    .mapError(err => `Network error: ${err.message}`)
    .flatMap(response =>
      response.ok
        ? Task.fromPromise(response.json())
        : Task.rejected(`HTTP ${response.status}`)
    )
    .map(data => data.items)
    .mapError(err => `Processing error: ${err}`);

// 链式错误处理
fetchAndProcess('/api/data')
  .fork(
    error => console.error(error),
    items => renderItems(items)
  );

15.5.2 重试与错误恢复

-- 使用 EitherT 进行错误恢复
withRetry :: Int -> IO (Either String a) -> IO (Either String a)
withRetry 0 action = action
withRetry n action = do
  result <- action
  case result of
    Right x -> return (Right x)
    Left _  -> withRetry (n - 1) action

-- 错误回退
withFallback :: IO (Either String a) -> IO (Either String a) -> IO (Either String a)
withFallback primary fallback = do
  result <- primary
  case result of
    Right x -> return (Right x)
    Left _  -> fallback

15.6 业务场景

15.6.1 表单验证

// TypeScript 完整的表单验证
type ValidationErrors = Record<string, string[]>;

interface FormData {
  name: string;
  email: string;
  age: number;
  password: string;
}

const validators: Record<string, (value: any) => Either<string[], any>> = {
  name: (name: string) =>
    name.length >= 2 ? right(name) : left(["Name must be at least 2 characters"]),

  email: (email: string) =>
    /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) ? right(email) : left(["Invalid email"]),

  age: (age: number) =>
    age >= 18 ? right(age) : left(["Must be 18+"]),
};

const validateForm = (data: FormData): Either<ValidationErrors, FormData> => {
  const results = Object.entries(data).map(([key, value]) =>
    validators[key] ? validators[key](value).mapError(errors => [key, errors]) : right(value)
  );

  // 收集所有错误
  const errors = results
    .filter(r => r.isLeft)
    .map(r => r.value);

  if (errors.length > 0) {
    return left(Object.fromEntries(errors));
  }

  return right(data);
};

15.6.2 API 响应处理

// Rust:结构化 API 错误处理
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
enum ApiError {
    NotFound { resource: String, id: String },
    Validation { field: String, message: String },
    Unauthorized,
    Internal { message: String },
}

impl ApiError {
    fn status_code(&self) -> u16 {
        match self {
            ApiError::NotFound { .. } => 404,
            ApiError::Validation { .. } => 400,
            ApiError::Unauthorized => 401,
            ApiError::Internal { .. } => 500,
        }
    }

    fn to_response(&self) -> serde_json::Value {
        serde_json::json!({
            "error": self,
            "status": self.status_code()
        })
    }
}

fn get_user(id: &str) -> Result<User, ApiError> {
    db::find_user(id)
        .map_err(|_| ApiError::NotFound {
            resource: "user".into(),
            id: id.into(),
        })
}

15.7 注意事项

注意事项 说明
错误类型设计 使用枚举/ADT 统一错误类型
错误信息 提供有用的错误信息,便于调试
性能 Result/Option 通常零成本抽象
混合使用 可以在边界使用异常,核心使用 Result
过度抽象 简单场景不需要复杂的错误处理

15.8 小结

要点 说明
Option/Maybe 表示可能缺失的值
Either/Result 表示成功或失败,附带错误信息
Validation 收集所有错误,非短路
错误组合 通过 Monad/Applicative 组合
类型安全 编译器强制处理错误情况

扩展阅读

  1. Railway Oriented Programming — Scott Wlaschin
  2. Error Handling Guide - Rust
  3. Validation applicative functor — Cats 库

下一章16 函数式测试