函数式编程艺术 / 06 模式匹配
06 模式匹配
“模式匹配是函数式编程中最接近魔法的特性——它让你直接描述数据的形状,然后按形状分派逻辑。”
6.1 模式匹配概述
模式匹配(Pattern Matching) 是一种根据数据的结构进行解构和分支的强大机制。它远比传统的 if-else 和 switch 更具表达力。
6.1.1 模式匹配 vs 传统分支
| 特性 | switch/case | 模式匹配 |
|---|---|---|
| 解构数据 | ❌ 仅值比较 | ✅ 解构嵌套数据 |
| 绑定变量 | ❌ | ✅ 匹配时绑定 |
| 穷尽性检查 | ❌ | ✅ 编译时检查 |
| 嵌套匹配 | ❌ | ✅ 多层嵌套 |
| 守卫条件 | ❌ | ✅ where/guard |
| 可读性 | 一般 | 高 |
6.2 基本模式匹配
6.2.1 值匹配
Haskell:
dayType :: String -> String
dayType "Saturday" = "Weekend"
dayType "Sunday" = "Weekend"
dayType "Monday" = "Start of work week"
dayType _ = "Weekday" -- _ 是通配符
-- 更推荐用代数数据类型
data Day = Mon | Tue | Wed | Thu | Fri | Sat | Sun
dayType :: Day -> String
dayType Sat = "Weekend"
dayType Sun = "Weekend"
dayType Mon = "Start of work week"
dayType _ = "Weekday"
Rust:
fn day_type(day: &str) -> &str {
match day {
"Saturday" | "Sunday" => "Weekend",
"Monday" => "Start of work week",
_ => "Weekday",
}
}
Python(3.10+ 结构化模式匹配):
def day_type(day: str) -> str:
match day:
case "Saturday" | "Sunday":
return "Weekend"
case "Monday":
return "Start of work week"
case _:
return "Weekday"
Clojure:
;; 使用 core.match
(require '[clojure.core.match :refer [match]])
(defn day-type [day]
(match [day]
["Saturday"] "Weekend"
["Sunday"] "Weekend"
["Monday"] "Start of work week"
:else "Weekday"))
6.2.2 类型匹配
Haskell:
data Shape = Circle Double
| Rectangle Double Double
| Triangle Double Double Double
area :: Shape -> Double
area (Circle r) = pi * r * r
area (Rectangle w h) = w * h
area (Triangle a b c) = let s = (a + b + c) / 2
in sqrt (s * (s-a) * (s-b) * (s-c))
Rust:
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle(f64, f64, f64),
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rectangle(w, h) => w * h,
Shape::Triangle(a, b, c) => {
let s = (a + b + c) / 2.0;
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
}
}
Python:
from dataclasses import dataclass
from typing import Union
import math
@dataclass
class Circle:
radius: float
@dataclass
class Rectangle:
width: float
height: float
Shape = Union[Circle, Rectangle]
def area(shape: Shape) -> float:
match shape:
case Circle(r):
return math.pi * r * r
case Rectangle(w, h):
return w * h
6.3 解构(Destructuring)
解构是模式匹配的简化形式,用于从数据结构中提取值。
6.3.1 元组解构
Haskell:
-- 元组解构
fst' :: (a, b) -> a
fst' (x, _) = x
snd' :: (a, b) -> b
snd' (_, y) = y
-- 嵌套解构
nested :: ((Int, Int), (Int, Int)) -> Int
nested ((a, b), (c, d)) = a + b + c + d
JavaScript:
// 数组解构
const [first, second, ...rest] = [1, 2, 3, 4, 5];
// first=1, second=2, rest=[3,4,5]
// 嵌套解构
const { user: { name, address: { city } } } = data;
// name = data.user.name
// city = data.user.address.city
// 函数参数解构
const greet = ({ name, age }) => `Hello ${name}, you are ${age}`;
// 交换变量
let a = 1, b = 2;
[a, b] = [b, a]; // a=2, b=1
Python:
# 元组解构
first, *rest = [1, 2, 3, 4, 5]
# first=1, rest=[2,3,4,5]
# 嵌套解构
(a, b), (c, d) = (1, 2), (3, 4)
# 字典解构
def greet(user):
name, age = user['name'], user['age']
return f"Hello {name}, you are {age}"
# match-case 解构
def process(data):
match data:
case {'type': 'user', 'name': str(name), 'age': int(age)}:
return f"User {name}, age {age}"
case {'type': 'product', 'title': str(title)}:
return f"Product {title}"
Rust:
// 元组解构
let (x, y, z) = (1, 2, 3);
// 结构体解构
struct Point { x: i32, y: i32 }
let Point { x, y } = Point { x: 1, y: 2 };
// 枚举解构
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
}
fn process(msg: Message) {
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Text: {}", text),
}
}
// if let 简化匹配
if let Some(value) = some_option {
println!("Got: {}", value);
}
Clojure:
;; 向量解构
(let [[first second & rest] [1 2 3 4 5]]
[first second rest])
;; [1 2 (3 4 5)]
;; map 解构
(let [{:keys [name age]} {:name "Alice" :age 30}]
(str name " is " age " years old"))
;; 嵌套解构
(let [{:keys [name address]} {:name "Alice" :address {:city "Beijing"}}]
(str name " lives in " (:city address)))
6.4 代数数据类型(Algebraic Data Types, ADT)
代数数据类型是函数式编程中构建数据模型的核心工具。
6.4.1 两种 ADT
| 类型 | 英文 | 构造方式 | 示例 |
|---|---|---|---|
| 积类型 | Product Type | 所有字段同时存在 | data User = User String Int |
| 和类型 | Sum Type | 多种可能取其一 | data Bool = True | False |
6.4.2 积类型(Product Type)
-- Haskell
data User = User
{ userName :: String
, userAge :: Int
, userEmail :: String
}
-- User 的值 = String × Int × String 的所有可能组合
-- 这就是"积"类型名字的来源
// Rust
struct User {
name: String,
age: u32,
email: String,
}
6.4.3 和类型(Sum Type)
-- Haskell
data PaymentMethod
= CreditCard String String String -- cardNumber, expiry, cvv
| PayPal String -- email
| Bitcoin String -- walletAddress
| Cash
-- PaymentMethod 的值 = CreditCard 所有可能 | PayPal 所有可能 | ...
-- 这就是"和"类型名字的来源
-- 更多和类型示例
data Maybe a = Nothing | Just a
data Either a b = Left a | Right b
data Ordering = LT | EQ | GT
data List a = Nil | Cons a (List a)
// Rust
enum PaymentMethod {
CreditCard { number: String, expiry: String, cvv: String },
PayPal(String),
Bitcoin(String),
Cash,
}
// Rust 枚举比 C/Java 枚举强大得多——它就是和类型
enum Result<T, E> {
Ok(T),
Err(E),
}
enum Option<T> {
Some(T),
None,
}
# Python 使用 Union type + dataclass
from dataclasses import dataclass
from typing import Union
@dataclass
class CreditCard:
number: str
expiry: str
cvv: str
@dataclass
class PayPal:
email: str
@dataclass
class Bitcoin:
wallet_address: str
PaymentMethod = Union[CreditCard, PayPal, Bitcoin, None]
6.4.4 ADT 的代数性质
| 类型 | 计数公式 | 示例 |
|---|---|---|
| 积类型 | |A × B| = |A| × |B| | (Bool, Bool) 有 4 个值 |
| 和类型 | |A + B| = |A| + |B| | Maybe Bool 有 3 个值 |
| 函数类型 | |A → B| = |B|^|A| | Bool → Bool 有 4 个函数 |
-- Bool × Bool 有 4 个值
(True, True), (True, False), (False, True), (False, False)
-- Maybe Bool 有 3 个值
Nothing, Just True, Just False
-- Bool → Bool 有 4 个函数
-- f1: True→True, False→True
-- f2: True→True, False→False
-- f3: True→False, False→True
-- f4: True→False, False→False
6.5 穷尽性检查(Exhaustiveness Check)
穷尽性检查确保模式匹配覆盖了所有可能的情况。
6.5.1 编译时保障
Haskell:
data Color = Red | Green | Blue
-- 编译器会警告不完整的模式匹配
showColor :: Color -> String
showColor Red = "Red"
showColor Green = "Green"
-- Warning: Pattern match(es) are non-exhaustive
-- In an equation for 'showColor': Patterns not matched: Blue
-- 正确的穷尽匹配
showColor :: Color -> String
showColor Red = "Red"
showColor Green = "Green"
showColor Blue = "Blue"
Rust:
enum Color { Red, Green, Blue }
fn show(color: Color) -> &'static str {
match color {
Color::Red => "Red",
Color::Green => "Green",
// 如果省略 Blue,编译器报错:
// non-exhaustive patterns: `Blue` not covered
Color::Blue => "Blue",
}
}
// 使用通配符(不推荐,失去穷尽性检查)
fn show_limited(color: Color) -> &'static str {
match color {
Color::Red => "Red",
_ => "Other",
}
}
6.5.2 嵌套穷尽性
// Rust 确保嵌套模式也是穷尽的
enum Shape {
Circle(f64),
Rectangle(f64, f64),
}
enum Color {
Red,
Blue,
}
struct ColoredShape {
color: Color,
shape: Shape,
}
fn describe(cs: ColoredShape) -> String {
match (cs.color, cs.shape) {
(Color::Red, Shape::Circle(r)) => format!("Red circle, r={}", r),
(Color::Red, Shape::Rectangle(w, h)) => format!("Red rect, {}x{}", w, h),
(Color::Blue, Shape::Circle(r)) => format!("Blue circle, r={}", r),
(Color::Blue, Shape::Rectangle(w, h)) => format!("Blue rect, {}x{}", w, h),
}
}
6.6 守卫(Guard)
守卫为模式匹配提供额外的条件约束。
6.6.1 Haskell 中的守卫
-- 基本守卫
bmiCategory :: Double -> String
bmiCategory bmi
| bmi < 18.5 = "Underweight"
| bmi < 25.0 = "Normal"
| bmi < 30.0 = "Overweight"
| otherwise = "Obese"
-- 模式 + 守卫
describeAge :: Int -> String
describeAge age
| age < 0 = "Invalid"
| age < 13 = "Child"
| age < 18 = "Teenager"
| age < 65 = "Adult"
| otherwise = "Senior"
-- 复杂示例:分类三角形
classifyTriangle :: Int -> Int -> Int -> String
classifyTriangle a b c
| a + b <= c || a + c <= b || b + c <= a = "Not a triangle"
| a == b && b == c = "Equilateral"
| a == b || b == c || a == c = "Isosceles"
| otherwise = "Scalene"
Rust:
fn bmi_category(bmi: f64) -> &'static str {
match bmi {
bmi if bmi < 18.5 => "Underweight",
bmi if bmi < 25.0 => "Normal",
bmi if bmi < 30.0 => "Overweight",
_ => "Obese",
}
}
fn classify_triangle(a: i32, b: i32, c: i32) -> &'static str {
match (a, b, c) {
_ if a + b <= c || a + c <= b || b + c <= a => "Not a triangle",
_ if a == b && b == c => "Equilateral",
_ if a == b || b == c || a == c => "Isosceles",
_ => "Scalene",
}
}
Python:
def bmi_category(bmi: float) -> str:
match bmi:
case b if b < 18.5: return "Underweight"
case b if b < 25.0: return "Normal"
case b if b < 30.0: return "Overweight"
case _: return "Obese"
6.7 嵌套模式匹配
6.7.1 深层嵌套解构
Haskell:
data Address = Address { city :: String, zipCode :: String }
data Company = Company { companyName :: String, address :: Address }
data Employee = Employee
{ empName :: String
, company :: Company
, role :: String
}
-- 嵌套模式匹配
getWorkCity :: Employee -> String
getWorkCity (Employee _ (Company _ (Address city _)) _) = city
-- 更可读的 Record 语法
getWorkCity' :: Employee -> String
getWorkCity' Employee { company = Company { address = Address { city = c } } } = c
JavaScript:
// 深层解构
const {
name: empName,
company: {
name: companyName,
address: { city }
},
role
} = employee;
// 函数参数深层解构
const getWorkCity = ({ company: { address: { city } } }) => city;
Python:
def get_work_city(employee: dict) -> str:
match employee:
case {'company': {'address': {'city': str(city)}}}:
return city
case _:
return "Unknown"
6.8 业务场景
6.8.1 AST 解释器
-- 用代数数据类型表示算术表达式
data Expr
= Num Double
| Add Expr Expr
| Mul Expr Expr
| Neg Expr
| Var String
-- 求值
eval :: Map.Map String Double -> Expr -> Double
eval _ (Num x) = x
eval env (Add a b) = eval env a + eval env b
eval env (Mul a b) = eval env a * eval env b
eval env (Neg e) = negate (eval env e)
eval env (Var name) = case Map.lookup name env of
Just v -> v
Nothing -> error $ "Undefined variable: " ++ name
-- 化简
simplify :: Expr -> Expr
simplify (Add (Num 0) e) = simplify e
simplify (Add e (Num 0)) = simplify e
simplify (Mul (Num 1) e) = simplify e
simplify (Mul e (Num 1)) = simplify e
simplify (Mul (Num 0) _) = Num 0
simplify (Neg (Neg e)) = simplify e
simplify (Add a b) = Add (simplify a) (simplify b)
simplify (Mul a b) = Mul (simplify a) (simplify b)
simplify e = e
Rust:
#[derive(Debug, Clone)]
enum Expr {
Num(f64),
Add(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Neg(Box<Expr>),
Var(String),
}
impl Expr {
fn eval(&self, env: &std::collections::HashMap<String, f64>) -> f64 {
match self {
Expr::Num(x) => *x,
Expr::Add(a, b) => a.eval(env) + b.eval(env),
Expr::Mul(a, b) => a.eval(env) * b.eval(env),
Expr::Neg(e) => -e.eval(env),
Expr::Var(name) => *env.get(name).expect(&format!("Undefined: {}", name)),
}
}
fn simplify(&self) -> Expr {
match self {
Expr::Add(a, b) => match (a.simplify(), b.simplify()) {
(Expr::Num(0.0), e) | (e, Expr::Num(0.0)) => e,
(a, b) => Expr::Add(Box::new(a), Box::new(b)),
},
// ... 其他简化规则
_ => self.clone(),
}
}
}
6.8.2 状态机
-- 用和类型表示订单状态
data OrderStatus
= Pending
| Paid Double
| Shipped String String -- carrier, trackingNumber
| Delivered String -- signature
| Cancelled String -- reason
nextStatus :: OrderStatus -> String -> OrderStatus
nextStatus Pending "pay" = Paid 0.0
nextStatus (Paid _) "ship" = Shipped "" ""
nextStatus (Shipped _ _) "deliver" = Delivered ""
nextStatus _ "cancel" = Cancelled "User cancelled"
nextStatus status _ = status -- 无效转换,保持原状态
6.9 注意事项
| 注意事项 | 说明 |
|---|---|
| 通配符使用 | 避免过多 _ 导致丢失穷尽性检查的收益 |
| 模式顺序 | 模式从上到下匹配,第一个匹配的被执行 |
| 性能 | 编译器通常将模式匹配优化为跳转表 |
| 嵌套深度 | 过深嵌套可读性差,考虑拆分函数 |
| 守卫副作用 | 守卫应该是纯函数 |
6.10 小结
| 要点 | 说明 |
|---|---|
| 模式匹配 | 根据数据结构进行解构和分支 |
| 解构 | 从数据结构中提取值的语法糖 |
| 代数数据类型 | 积类型(AND)+ 和类型(OR) |
| 穷尽性检查 | 编译器确保覆盖所有情况 |
| 守卫 | 模式匹配的条件约束 |
扩展阅读
- Algebraic Data Types - Haskell Wiki
- Pattern Matching - Rust Book
- Structural Pattern Matching - Python PEP 634
- 《Types and Programming Languages》 — Benjamin C. Pierce
下一章:07 递归与不动点 — 函数式编程的循环替代方案