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 · 回头拆代码

从第一行往下读

  1. 学生编号限定为正整数。
  2. 结果类型列出成功和失败两种 tuple。
  3. Dialyzer 建立 PLT 首次会较慢,后续可复用分析资料。
03 · 认清新符号

符号不是暗号

-type name() :: ... .

声明可复用类型。

-spec fun(Type) -> Type.

声明函数输入输出。

term()

任意 Erlang term。

04 · 代码里的概念

把名字和意思对上

01

typespec

Erlang 模块中的类型与函数规格声明。

02

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. 1

    类型约定要覆盖真实分支。

  2. 2

    -type 能给领域 term 命名。

  3. 3

    Dialyzer 寻找代码与约定的矛盾。

本课结束代码也跑过一次,就把这一课收好。