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)
⚠️ 注意:fst 和 snd 仅适用于二元组。对于三元组或更大的元组,使用模式匹配或自己定义访问函数。
(* 自定义三元组访问函数 *)
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 会使用最近定义的类型。这可能造成困惑。解决方案是:
- 使用不同的字段名(如
user_id、admin_id) - 使用模块(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 } |