函数式编程艺术 / 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 "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 组合 |
| 类型安全 | 编译器强制处理错误情况 |
扩展阅读
- Railway Oriented Programming — Scott Wlaschin
- Error Handling Guide - Rust
- Validation applicative functor — Cats 库
下一章:16 函数式测试