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 课的测试不必模拟终端。
分离失败与显示
- 将示例中的
Types|many改成Types|45,对应行应变为成功。 - 增加
" |25",得到empty topic;再改为"OCaml|1441",得到范围错误。 - 恢复主示例。思考为什么不能用
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,让调用者按类型区分原因。