Erlang 从零 · 第 23 课(展开目录)
01 · 先打开 erl02 · 先认常见 term03 · 分清两种文字04 · 变量只接一次05 · 拆开一列,再筛一次06 · 把文字安全变成整数07 · 用 case 选一条路08 · 让函数继续走09 · 把代码放进模块10 · 条件必须说真话11 · 一层一层取数据12 · 文字先说清编码13 · 让函数子句排好队14 · 带着累加器往前走15 · 先看值,再选路16 · 给模块立规矩17 · 让 rebar3 管项目18 · 让结果有固定形状19 · 把磁盘当成会失败的邻居20 · 把函数交给 list21 · record 留在院里22 · 先写模块之间的约定23 · 把 term 形状写出来24 · 小测试与整段路25 · 把应用磨成命令26 · 先把小项目圈住27 · 把真实文件洗干净28 · 公开入口越少越稳29 · 把约定留在源码旁30 · 打包,也留下线索31 · 按最初任务交卷
ERLANG · 进阶 · LESSON 2340 分钟
把 term 形状写出来
用 `-type` 与 `-spec` 记录约定,让 Dialyzer 寻找矛盾。
01 · 先看完整例子
为查分结果写类型
放进 rebar3 项目,先编译与测试,再运行 rebar3 dialyzer。
Erlang
-module(scores).
-export([fetch/2]).
-type student_id() :: pos_integer().
-type result() :: {ok, non_neg_integer()} | {error, not_found}.
%% spec 与两个返回分支保持一致
-spec fetch(#{student_id() => non_neg_integer()}, student_id()) -> result().
fetch(Scores, Id) ->
case maps:find(Id, Scores) of
{ok, Score} -> {ok, Score};
error -> {error, not_found}
end.先对照结果
scores:fetch(#{7 => 92}, 7).得到{ok,92}。
02 · 回头拆代码
从第一行往下读
- 学生编号限定为正整数。
- 结果类型列出成功和失败两种 tuple。
- Dialyzer 建立 PLT 首次会较慢,后续可复用分析资料。
03 · 认清新符号
符号不是暗号
-type name() :: ... .声明可复用类型。
-spec fun(Type) -> Type.声明函数输入输出。
term()任意 Erlang term。
04 · 代码里的概念
把名字和意思对上
typespec
Erlang 模块中的类型与函数规格声明。
Dialyzer
根据 success typing 分析 BEAM 代码矛盾的工具。
05 · 再说清楚
这些写法为什么有用
Erlang 是动态语言,但 typespec 能把复杂 term 形状写给读者和工具。
-type 给数据形状命名,-spec 描述函数输入与输出。它们必须如实覆盖所有分支。
Dialyzer 分析代码可能产生的值,寻找永远匹配不到或返回约定冲突等问题。
设计选择
为什么这样写
既然能直接运行,为什么还写 spec?
spec 给人和 Dialyzer 留下函数契约。它善于发现不可能成功的调用,不负责在运行时拦住每个错误值。
spec 写得越宽,帮助越少;写错也不会自动变成运行时验证。系统边界仍要解析和校验。
换一把尺子
从 Java、Python、JavaScript 看过来
Java
- 熟悉的起点
- 编译器在构建时做静态类型检查。
- BEAM 在意什么
- Dialyzer 使用 success typing,目标不是证明所有输入都安全。
- 别带错直觉
- 通过 Dialyzer 不代表运行时不会收到坏消息。
Python
- 熟悉的起点
- type hints 配合 mypy/pyright。
- BEAM 在意什么
- spec 也主要服务工具与读者,但分析模型不同。
- 别带错直觉
- 它不是输入验证库。
JavaScript
- 熟悉的起点
- TypeScript 在输出 JS 前检查类型。
- BEAM 在意什么
- BEAM 运行 term;Dialyzer 从可能成功的调用推断矛盾。
- 别带错直觉
- 跨网络与文件边界仍要检查真实数据。
06 · 自己改一次
合上答案,动手
为 double/1 写整数输入和输出 spec。
练习起点
-spec double(____) -> ____.
double(Number) -> Number * 2.目标结果: 写成 -spec double(integer()) -> integer().。
卡住了,再看提示
输入与输出都使用 integer()。
运行过以后,再看一种答案
一种答案
%% 规格如实描述函数行为
-spec double(integer()) -> integer().
double(Number) -> Number * 2.想一想:spec 会在每次运行时拦住所有错误类型吗?
不会。spec 服务文档与静态分析;运行时仍由模式、guard 和函数正文处理 term。
带走
记住三句
- 1
类型约定要覆盖真实分支。
- 2
-type能给领域 term 命名。 - 3
Dialyzer 寻找代码与约定的矛盾。
本课结束代码也跑过一次,就把这一课收好。