Course Builder个人学习工作台 · 本地模式
OCaml:从类型推导到可测试的命令行工具

08 · Option 与 Result:验证一行输入

目标:把 主题|分钟数 解析为记录,使用 result 返回可解释的失败原因。

输入来自用户时,失败是接口的一部分。int_of_string_opt 不会把无法转换的字符串假装成 0,而是返回 None;成功时返回 Some n。'a option 表示“有一个值或没有值”。需要告诉用户失败原因时,用 ('a, 'e) result:成功是 Ok value,失败是 Error reason。它们都是已经定义好的 variant。

从 Go 的 (value, error) 迁移时,最值得注意的是组合方式:Go 返回两个结果,并由接口约定说明它们的关系;OCaml 的 result 在一次返回中只属于 Ok 或 Error 一种情况,取正常值时需要处理这个外层结构。从 Python/JS 的异常习惯迁移时,则要注意:Error message 是普通返回值,不会自动跳出调用栈,也不会自动进入 try 的异常处理分支。第 9 课会显式传递这个失败。Go 规范:返回语句

StudyLog 规定一条记录恰有两个字段;主题去除首尾空白后非空;分钟数为 1–1440。转换采用 OCaml 的整数文本规则,因此 0x19 也可表示 25;这不是只允许十进制数字的格式。超出机器整数范围的文本无法转换。若将来需要严格十进制,应该另加字符规则与测试。

实验:先检查形状,再检查内容

创建 results.ml:

type session = { topic : string; minutes : int }

let parse_line line =
  match String.split_on_char '|' line with
  | [topic; raw_minutes] ->
      let topic = String.trim topic in
      let raw_minutes = String.trim raw_minutes in
      if topic = "" then Error "empty topic"
      else
        (match int_of_string_opt raw_minutes with
        | None -> Error "minutes must be an integer"
        | Some minutes when minutes < 1 || minutes > 1440 ->
            Error "minutes must be between 1 and 1440"
        | Some minutes -> Ok { topic; minutes })
  | _ -> Error "expected topic|minutes"

let show input =
  match parse_line input with
  | Ok s -> Printf.printf "ok: %s %d\n" s.topic s.minutes
  | Error message -> Printf.printf "error: %s\n" message

let () =
  List.iter show [" OCaml |25"; "Types|many"; "OCaml|0"; "x|2|3"]

String.split_on_char 返回字符串列表,模式 [topic; raw_minutes] 要求长度恰好为 2。单引号 '|' 是字符,双引号 "|" 是字符串。String.trim 去除首尾的标准空白字符;|| 是逻辑或。嵌套 match 外的括号明确它的边界,避免下面的 | _ 被解析成内层分支。

先预测每条输入会在哪一步失败,再运行:

ocaml results.ml

预期输出:

ok: OCaml 25
error: minutes must be an integer
error: minutes must be between 1 and 1440
error: expected topic|minutes

parse_line 没有打印,也没有退出程序。它只从输入计算结果,让调用者决定怎么呈现错误。这会让第 14 课的测试不必模拟终端。

分离失败与显示

  1. 将示例中的 Types|many 改成 Types|45,对应行应变为成功。
  2. 增加 " |25",得到 empty topic;再改为 "OCaml|1441",得到范围错误。
  3. 恢复主示例。思考为什么不能用 Error "..." 替代 None:它们属于不同的类型构造。

独立练习:增加测试,明确 1 和 1440 都合法,-1 和空分钟数都失败。至少有一个测试检查去空白后得到的主题,而不是只检查是否成功。

可执行参考检查

追加到主文件:

let () =
  assert (parse_line " X | 1 " = Ok { topic = "X"; minutes = 1 });
  assert (parse_line "X|1440" = Ok { topic = "X"; minutes = 1440 });
  assert (parse_line "X|-1" = Error "minutes must be between 1 and 1440");
  assert (parse_line "X|" = Error "minutes must be an integer");
  print_endline "parse checks passed"

这些值只包含字符串和整数,可以用结构相等比较。不要据此认为任意值都适合 =,例如函数值不适合这样比较。

若坏输入使程序抛出 Failure,检查是否误用了 int_of_string。若合法输入被判断为三个字段,检查主题里是否包含额外的 |;本格式没有转义机制。错误原因用字符串便于入门,复杂应用可以另建错误 variant,让调用者按类型区分原因。

参考资料