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

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

15 函数式错误处理

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


15.1 传统错误处理的问题

15.1.1 异常的缺点

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

15.1.2 函数式错误处理的优势

特性异常函数式
错误信息堆栈跟踪类型签名
组合性优秀(Monad)
编译检查
纯函数兼容不兼容完全兼容
错误恢复try/catchflatMap/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 "alice@example.com" 30  -- Right ("alice@example.com", 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("alice@example.com", 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("alice@example.com")
  .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 函数式测试