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

OCaml 教程 / 元组与记录类型

元组与记录类型

概述

元组(Tuple)和记录(Record)是 OCaml 中两种基本的复合类型。元组用于将少量值组合在一起,记录则为每个字段命名,提供更清晰的数据结构。

元组定义与解构

元组定义

(* 二元组 *)
let pair = (1, "hello")

(* 三元组 *)
let triple = (1, "hello", true)

(* 空元组 = unit *)
let unit_val = ()

(* 嵌套元组 *)
let nested = ((1, 2), (3, 4))

元组类型

(* 二元组类型 *)
let pair : int * string = (1, "hello")

(* 三元组类型 *)
let triple : int * string * bool = (1, "hello", true)

(* 嵌套元组类型 *)
let nested : (int * int) * (int * int) = ((1, 2), (3, 4))

💡 提示:元组类型用 * 分隔各分量类型,如 int * string 表示一个包含 int 和 string 的二元组。

元组解构

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

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

(* 模式匹配解构 *)
let describe pair =
  match pair with
  | (0, 0) -> "原点"
  | (x, y) -> Printf.sprintf "(%d, %d)" x y

(* 函数参数中的解构 *)
let add (a, b) = a + b
let _ = add (3, 5)  (* => 8 *)

(* 交换元组 *)
let swap (a, b) = (b, a)
let _ = swap (1, 2)  (* => (2, 1) *)

(* 忽略某些分量 *)
let (_, y, _) = (1, 2, 3)
(* y = 2 *)

fst 和 snd

OCaml 提供了访问二元组的标准函数:

let pair = (42, "answer")

let first = fst pair    (* => 42 *)
let second = snd pair   (* => "answer" *)

(* fst 和 snd 仅适用于二元组 *)
(* 对三元组没有标准函数,需要解构 *)
let (a, b, c) = (1, 2, 3)

⚠️ 注意fstsnd 仅适用于二元组。对于三元组或更大的元组,使用模式匹配或自己定义访问函数。

(* 自定义三元组访问函数 *)
let fst3 (a, _, _) = a
let snd3 (_, b, _) = b
let thd3 (_, _, c) = c

记录类型定义

记录为每个字段提供名称,比元组更可读:

(* 定义记录类型 *)
type point = {
  x : float;
  y : float;
}

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

type config = {
  host : string;
  port : int;
  debug : bool;
  max_connections : int;
}

⚠️ 注意:记录类型定义中的最后一个分号可以省略,但保留它可以使添加新字段时的 diff 更清晰。

创建记录值

(* 创建记录 *)
let origin = { x = 0.0; y = 0.0 }
let alice = { name = "Alice"; age = 30; email = "[email protected]" }

(* 字段顺序不重要 *)
let same_alice = { age = 30; email = "[email protected]"; name = "Alice" }

字段访问

(* 点号访问 *)
let alice_age = alice.age    (* => 30 *)
let origin_x = origin.x      (* => 0.0 *)

(* 函数中访问记录字段 *)
let get_name p = p.name
let is_adult p = p.age >= 18

(* 嵌套记录 *)
type address = {
  street : string;
  city : string;
  zip : string;
}

type contact = {
  person : person;
  address : address;
  phone : string;
}

let get_city c = c.address.city

记录更新语法

OCaml 提供了便捷的记录更新语法,基于现有记录创建新记录:

let alice = { name = "Alice"; age = 30; email = "[email protected]" }

(* 更新一个字段 *)
let alice_older = { alice with age = 31 }

(* 更新多个字段 *)
let alice_new_email = { alice with
  age = 31;
  email = "[email protected]"
}

(* 原始记录不变 *)
let _ = alice.age    (* => 30,仍然是 30 *)
let _ = alice_older.age  (* => 31 *)

💡 提示{ record with field = value } 创建一个记录,原记录不变。这是不可变数据的核心特性。

实际应用:配置覆盖

type server_config = {
  host : string;
  port : int;
  max_connections : int;
  timeout : int;
  debug : bool;
}

(* 默认配置 *)
let default_config = {
  host = "localhost";
  port = 8080;
  max_connections = 100;
  timeout = 30;
  debug = false;
}

(* 通过覆盖创建自定义配置 *)
let dev_config = { default_config with
  debug = true;
  port = 3000;
}

let prod_config = { default_config with
  host = "0.0.0.0";
  port = 80;
  max_connections = 1000;
}

记录与模块

记录类型定义实际上创建了一个隐式模块:

type user = {
  id : int;
  name : string;
  role : [`Admin | `User | `Guest];
}

(* 当多个记录类型有相同字段名时,会发生遮蔽 *)
type admin = {
  id : int;
  name : string;
  permissions : string list;
}

(* 最后定义的类型会遮蔽之前的同名字段 *)
(* 使用模块限定访问 *)
let user_id (u : user) = u.id
let admin_id (a : admin) = a.id

⚠️ 注意:当多个记录类型有同名字段时,OCaml 会使用最近定义的类型。这可能造成困惑。解决方案是:

  1. 使用不同的字段名(如 user_idadmin_id
  2. 使用模块(Module)或 OCaml 5 的命名记录字段(named record fields)

模块中定义记录

module User = struct
  type t = {
    id : int;
    name : string;
    email : string;
  }

  let create ~id ~name ~email = { id; name; email }
  let get_id t = t.id
  let to_string t = Printf.sprintf "%s <%s>" t.name t.email
end

module Admin = struct
  type t = {
    id : int;
    name : string;
    permissions : string list;
  }

  let create ~id ~name ~permissions = { id; name; permissions }
  let get_id t = t.id
  let has_permission t perm = List.mem perm t.permissions
end

(* 使用 *)
let user = User.create ~id:1 ~name:"Alice" ~email:"[email protected]"
let admin = Admin.create ~id:2 ~name:"Bob" ~permissions:["read"; "write"; "admin"]

带类型注解的记录

type 'a result = {
  value : 'a;
  status : int;
  message : string;
}

(* 多态记录 *)
let int_result : int result = {
  value = 42;
  status = 200;
  message = "OK";
}

let string_result : string result = {
  value = "hello";
  status = 200;
  message = "OK";
}

(* 记录中使用变体类型 *)
type error_type = Network | Parse | Validation

type 'a response =
  | Success of 'a
  | Failure of {
    error_type : error_type;
    code : int;
    message : string;
  }

let handle_response r =
  match r with
  | Success data -> data
  | Failure { error_type; code; message } ->
    Printf.sprintf "Error [%d] %s: %s" code
      (match error_type with
       | Network -> "NETWORK"
       | Parse -> "PARSE"
       | Validation -> "VALIDATION")
      message

记录的相等性比较

type point = { x : int; y : int }

let p1 = { x = 1; y = 2 }
let p2 = { x = 1; y = 2 }
let p3 = { x = 3; y = 4 }

(* 结构相等 — 值相同即相等 *)
let _ = (p1 = p2)    (* => true *)
let _ = (p1 = p3)    (* => false *)

(* 物理相等 — 必须是同一个对象 *)
let _ = (p1 == p2)   (* => false *)
let _ = (p1 == p1)   (* => true *)

(* 自定义比较函数 *)
let compare_point a b =
  match compare a.x b.x with
  | 0 -> compare a.y b.y
  | c -> c

(* 排序点 *)
let points = [{ x = 3; y = 1 }; { x = 1; y = 2 }; { x = 2; y = 3 }]
let sorted = List.sort compare_point points
(* => [{ x = 1; y = 2 }; { x = 2; y = 3 }; { x = 3; y = 1 }] *)

⚠️ 注意:记录的结构相等比较会递归比较所有字段。如果记录包含函数字段,比较会失败(函数不支持相等比较)。

实用示例

坐标系统

type point = { x : float; y : float }
type line = { start_p : point; end_p : point }

let distance a b =
  let dx = b.x -. a.x in
  let dy = b.y -. a.y in
  sqrt (dx *. dx +. dy *. dy)

let midpoint a b = {
  x = (a.x +. b.x) /. 2.0;
  y = (a.y +. b.y) /. 2.0;
}

let line_length line = distance line.start_p line.end_p

let p1 = { x = 0.0; y = 0.0 }
let p2 = { x = 3.0; y = 4.0 }
let line = { start_p = p1; end_p = p2 }

let _ = distance p1 p2       (* => 5.0 *)
let _ = midpoint p1 p2       (* => { x = 1.5; y = 2.0 } *)
let _ = line_length line      (* => 5.0 *)

配置管理

type db_config = {
  host : string;
  port : int;
  database : string;
  username : string;
  password : string;
  pool_size : int;
}

let default_db = {
  host = "localhost";
  port = 5432;
  database = "myapp";
  username = "postgres";
  password = "";
  pool_size = 10;
}

let production_db = { default_db with
  host = "db.production.com";
  database = "myapp_prod";
  pool_size = 50;
}

let connection_string db =
  Printf.sprintf "postgresql://%s:%s@%s:%d/%s"
    db.username db.password db.host db.port db.database

API 响应

type 'a api_response = {
  data : 'a;
  status_code : int;
  timestamp : float;
  request_id : string;
}

type user_data = {
  user_id : int;
  username : string;
  roles : string list;
}

let process_response (resp : user_data api_response) =
  if resp.status_code = 200 then
    Ok (Printf.sprintf "User %s (ID: %d)"
      resp.data.username resp.data.user_id)
  else
    Error (Printf.sprintf "Request %s failed with code %d"
      resp.request_id resp.status_code)

元组 vs 记录选择指南

场景推荐使用原因
2-3 个值的临时组合元组简洁,不需要定义类型
函数返回多个值元组解构方便
具有语义含义的数据结构记录字段名自文档化
超过 3 个字段记录元组字段位置难以记忆
作为公共 API记录更稳定,添加字段不破坏调用方
模式匹配都支持元组更简洁,记录更清晰

业务场景

场景数据类型示例
坐标记录{ x: float; y: float }
像素记录{ r: int; g: int; b: int }
函数返回错误码+消息元组(int, string)
键值对元组列表(string * 'a) list
用户配置记录带默认值的配置
HTTP 响应记录{ status; headers; body }

扩展阅读