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

OCaml 教程 / 模式匹配

模式匹配

概述

模式匹配(Pattern Matching)是 OCaml 最强大和最常用的特性之一。它不仅是 switch 语句的增强版,更是一种结构化数据解构和条件分支的统一机制。编译器会对模式匹配进行穷尽性检查(exhaustiveness check),确保所有可能的情况都被处理。

match 表达式基础

(* 基本 match 语法 *)
let describe_number n =
  match n with
  | 0 -> "零"
  | 1 -> "一"
  | 2 -> "二"
  | _ -> "其他"

let _ = describe_number 2    (* => "二" *)
let _ = describe_number 5    (* => "其他" *)

match 表达式由三部分组成:

  1. match ... with — 指定要匹配的值
  2. | pattern -> expression — 模式分支
  3. 返回第一个匹配分支的结果

⚠️ 注意:每个分支用 | 分隔(第一个 | 可选)。每个分支必须返回相同类型的值。

字面量匹配

(* 整数字面量 *)
let classify n =
  match n with
  | 0 -> "零"
  | 1 -> "一"
  | -1 -> "负一"
  | _ -> "其他"

(* 字符串字面量 *)
let color_name c =
  match c with
  | "red" -> "红色"
  | "green" -> "绿色"
  | "blue" -> "蓝色"
  | _ -> "未知颜色"

(* 字符字面量 *)
let is_vowel c =
  match c with
  | 'a' | 'e' | 'i' | 'o' | 'u' -> true
  | 'A' | 'E' | 'I' | 'O' | 'U' -> true
  | _ -> false

(* 布尔字面量 *)
let bool_to_string b =
  match b with
  | true -> "是"
  | false -> "否"

💡 提示:使用 | 分隔多个模式可以实现 “或” 逻辑,匹配其中任意一个。

通配符 _

_ 匹配任何值且不绑定变量:

(* 忽略不关心的值 *)
let get_first (a, _, _) = a

let _ = get_first (1, "hello", true)  (* => 1 *)

(* 忽略列表的尾部 *)
let head_or_default lst default =
  match lst with
  | x :: _ -> x
  | [] -> default

let _ = head_or_default [1; 2; 3] 0  (* => 1 *)
let _ = head_or_default [] 0          (* => 0 *)

⚠️ 注意:虽然 _ 很方便,但在关键逻辑中应尽量使用具名变量,使代码更具自文档性。

when 守卫

when 守卫为模式添加额外条件:

(* 使用 when 添加条件 *)
let classify_age age =
  match age with
  | n when n < 0 -> "无效年龄"
  | n when n < 13 -> "儿童"
  | n when n < 18 -> "青少年"
  | n when n < 65 -> "成年人"
  | _ -> "老年人"

(* 列表匹配中的 when *)
let rec find_first_even lst =
  match lst with
  | [] -> None
  | x :: _ when x mod 2 = 0 -> Some x
  | _ :: rest -> find_first_even rest

let _ = find_first_even [1; 3; 4; 5; 6]  (* => Some 4 *)

(* 实际应用:处理 HTTP 状态码 *)
let handle_status code =
  match code with
  | n when n >= 200 && n < 300 -> "成功"
  | n when n >= 300 && n < 400 -> "重定向"
  | n when n >= 400 && n < 500 -> "客户端错误"
  | n when n >= 500 -> "服务器错误"
  | _ -> "未知状态码"

let _ = handle_status 404  (* => "客户端错误" *)

⚠️ 注意when 守卫会影响穷尽性检查。编译器可能无法验证带 when 分支的穷尽性,谨慎使用。

嵌套模式

模式可以嵌套,实现深层数据结构的解构:

(* 嵌套元组 *)
let describe_pair pair =
  match pair with
  | (0, 0) -> "原点"
  | (0, _) -> "y 轴上"
  | (_, 0) -> "x 轴上"
  | (x, y) when x = y -> "对角线上"
  | _ -> "普通点"

(* 嵌套列表 *)
let rec describe_list lst =
  match lst with
  | [] -> "空列表"
  | [x] -> Printf.sprintf "只有一个元素: %d" x
  | [x; y] -> Printf.sprintf "两个元素: %d 和 %d" x y
  | x :: _ -> Printf.sprintf "首元素: %d,共多个元素" x

(* 深层嵌套 *)
type shape =
  | Circle of float
  | Rect of float * float
  | Point

let describe_shape s =
  match s with
  | Circle r when r > 0.0 -> Printf.sprintf "圆,半径 %.2f" r
  | Circle _ -> "无效圆"
  | Rect (w, h) when w > 0.0 && h > 0.0 ->
    Printf.sprintf "矩形 %.2fx%.2f (面积=%.2f)" w h (w *. h)
  | Rect _ -> "无效矩形"
  | Point -> "点"

元组匹配

(* 元组解构 *)
let (x, y) = (1, 2)
(* x = 1, y = 2 *)

(* 交换 *)
let swap (a, b) = (b, a)

(* 三元组 *)
let (a, b, c) = (1, "hello", true)

(* match 中的元组模式 *)
let classify_point point =
  match point with
  | (0, 0) -> "原点"
  | (x, 0) -> Printf.sprintf "x=%d 轴上" x
  | (0, y) -> Printf.sprintf "y=%d 轴上" y
  | (x, y) -> Printf.sprintf "(%d, %d)" x y

(* 函数参数中的模式匹配 *)
let distance (x1, y1) (x2, y2) =
  let dx = float_of_int (x2 - x1) in
  let dy = float_of_int (y2 - y1) in
  sqrt (dx *. dx +. dy *. dy)

let _ = distance (0, 0) (3, 4)  (* => 5.0 *)

记录匹配

type person = {
  name : string;
  age : int;
  email : string option;
}

let describe_person p =
  match p with
  | { name; age; email = Some e } ->
    Printf.sprintf "%s (%d岁, %s)" name age e
  | { name; age; email = None } ->
    Printf.sprintf "%s (%d岁, 无邮箱)" name age

(* 部分匹配 — 只关心某些字段 *)
let is_adult p =
  match p with
  | { age; _ } -> age >= 18

(* 使用 record punning(字段名与变量名相同时可省略) *)
let get_name { name; _ } = name

💡 提示:在记录模式中使用 _ 来忽略不关心的字段。

变体匹配

变体(Variant)类型的模式匹配是 OCaml 最强大的特性之一:

type color = Red | Green | Blue | RGB of int * int * int

let color_to_hex c =
  match c with
  | Red -> "#FF0000"
  | Green -> "#00FF00"
  | Blue -> "#0000FF"
  | RGB (r, g, b) -> Printf.sprintf "#%02X%02X%02X" r g b

(* 递归变体 — 表达式树 *)
type expr =
  | Num of float
  | Add of expr * expr
  | Mul of expr * expr
  | Neg of expr

let rec eval e =
  match e with
  | Num n -> n
  | Add (a, b) -> eval a +. eval b
  | Mul (a, b) -> eval a *. eval b
  | Neg e -> ~-.(eval e)

let expression = Add (Num 1.0, Mul (Num 2.0, Num 3.0))
let _ = eval expression  (* => 7.0 *)

(* 带守卫的变体匹配 *)
type response =
  | Success of string
  | Error of int * string

let handle_response r =
  match r with
  | Success body -> "成功: " ^ body
  | Error (code, _) when code >= 500 -> "服务器错误,请稍后重试"
  | Error (code, msg) -> Printf.sprintf "错误 %d: %s" code msg

异常匹配

try ... with 使用与 match 相同的模式匹配语法:

(* 基本异常处理 *)
let safe_divide a b =
  try
    Some (a / b)
  with
  | Division_by_zero -> None

let _ = safe_divide 10 3   (* => Some 3 *)
let _ = safe_divide 10 0   (* => None *)

(* 匹配多种异常 *)
let read_file_safe path =
  try
    let ic = open_in path in
    let content = really_input_string ic (in_channel_length ic) in
    close_in ic;
    Ok content
  with
  | Sys_error msg -> Error ("文件错误: " ^ msg)
  | End_of_file -> Error "意外的文件结束"
  | e -> Error ("未知错误: " ^ Printexc.to_string e)

(* 自定义异常 *)
exception Parse_error of int * string

let parse_line line_num line =
  match String.split_on_char ',' line with
  | [name; age_str] ->
    (name, int_of_string age_str)
  | _ ->
    raise (Parse_error (line_num, "格式错误: " ^ line))

let parse_csv content =
  let lines = String.split_on_char '\n' content in
  List.mapi (fun i line ->
    try parse_line (i + 1) line
    with
    | Parse_error (n, msg) ->
      Printf.eprintf "第 %d 行: %s\n" n msg;
      ("unknown", 0)
    | Failure _ ->
      Printf.eprintf "第 %d 行: 数字解析失败\n" (i + 1);
      ("unknown", 0)
  ) lines

⚠️ 注意try ... with 中的 with 部分使用与 match 完全相同的模式匹配语法。

穷尽性检查

OCaml 编译器会检查模式匹配是否覆盖了所有可能的情况:

type direction = North | South | East | West

(* 完整匹配 — 编译通过 *)
let dir_to_string d =
  match d with
  | North -> "北"
  | South -> "南"
  | East -> "东"
  | West -> "西"

(* 不完整匹配 — 编译器警告 *)
let dir_to_string_bad d =
  match d with
  | North -> "北"
  | South -> "南"
(* 编译器发出警告: this pattern-matching is not exhaustive.
   Here is an example of a case that is not matched:
   (East|West) *)

(* 使用通配符消除警告 *)
let dir_to_string_safe d =
  match d with
  | North -> "北"
  | South -> "南"
  | _ -> "其他方向"

or-patterns(或模式)

let is_cardinal d =
  match d with
  | North | South | East | West -> true

(* 列表中的或模式 *)
let is_special_char c =
  match c with
  | '!' | '@' | '#' | '$' | '%' -> true
  | _ -> false

💡 提示:穷尽性检查是 OCaml 类型系统的一大安全特性。当修改变体类型(增加新的构造器)时,编译器会自动提醒你所有需要更新的匹配表达式,避免遗漏。

模式匹配编译优化

OCaml 编译器对模式匹配进行了多种优化:

决策树编译

编译器将模式匹配编译为高效的决策树,而不是简单的逐个比较:

(* 编译器可能优化为跳转表 *)
let int_to_roman n =
  match n with
  | 1 -> "I"
  | 2 -> "II"
  | 3 -> "III"
  | 4 -> "IV"
  | 5 -> "V"
  | 6 -> "VI"
  | 7 -> "VII"
  | 8 -> "VIII"
  | 9 -> "IX"
  | _ -> "不支持"
(* 编译器可能生成类似 switch 的高效代码 *)

嵌套扁平化

(* 编译器会优化嵌套的模式匹配 *)
let classify lst =
  match lst with
  | [] -> "空"
  | [x] when x > 0 -> "单个正数"
  | [x] -> "单个非正数"
  | _ -> "多个元素"
(* 编译器会将 when 条件优化到决策树中 *)

实用示例:JSON 解析器

(* 简化的 JSON AST *)
type json =
  | JNull
  | JBool of bool
  | JNum of float
  | JStr of string
  | JArr of json list
  | JObj of (string * json) list

(* JSON 值转字符串 *)
let rec json_to_string indent j =
  let pad = String.make (indent * 2) ' ' in
  match j with
  | JNull -> "null"
  | JBool b -> string_of_bool b
  | JNum n ->
    if Float.is_integer n then string_of_int (int_of_float n)
    else Printf.sprintf "%.2f" n
  | JStr s -> Printf.sprintf "\"%s\"" s
  | JArr [] -> "[]"
  | JArr items ->
    let inner = List.map (fun v ->
      pad ^ "  " ^ json_to_string (indent + 1) v
    ) items in
    "[\n" ^ String.concat ",\n" inner ^ "\n" ^ pad ^ "]"
  | JObj [] -> "{}"
  | JObj pairs ->
    let inner = List.map (fun (k, v) ->
      pad ^ "  \"" ^ k ^ "\": " ^ json_to_string (indent + 1) v
    ) pairs in
    "{\n" ^ String.concat ",\n" inner ^ "\n" ^ pad ^ "}"

(* 从 JSON 提取值 — 带错误处理 *)
let rec get_nested j keys =
  match keys, j with
  | [], _ -> Some j
  | key :: rest, JObj pairs ->
    (match List.assoc_opt key pairs with
     | Some v -> get_nested v rest
     | None -> None)
  | _, _ -> None

(* 使用示例 *)
let () =
  let data = JObj [
    ("name", JStr "Alice");
    ("age", JNum 30.0);
    ("scores", JArr [JNum 95.0; JNum 87.0; JNum 92.0]);
    ("address", JObj [
      ("city", JStr "北京");
      ("zip", JStr "100000")
    ]);
  ] in
  print_endline (json_to_string 0 data);
  match get_nested data ["address"; "city"] with
  | Some (JStr city) -> Printf.printf "城市: %s\n" city
  | _ -> print_endline "未找到"

业务场景

场景模式匹配用法
状态机对状态变体进行匹配
命令解析匹配命令字符串
错误处理try ... with + 异常模式
AST 遍历递归匹配表达式树
配置解析记录模式 + 默认值
API 响应处理变体匹配(Success/Error)

扩展阅读