Elixir 从零 · 第 23 课(展开目录)
01 · 先让代码说话02 · 先认六种值03 · 把值装起来04 · 从旧 list 生成新 list05 · 让形状对上06 · 用 case 做选择07 · 把文字安全变成整数08 · 拆开 &1 和管道09 · 把代码放进项目10 · 真假不只靠 true11 · 走进嵌套数据12 · 一字不一定一字节13 · 让函数按形状接活14 · 把一列数收成一个值15 · 给不同问题选不同路16 · 给 map 一张名片17 · 让项目自己验答案18 · 让成功和失败长得一样19 · 读写文件先管好路20 · 只算眼前需要的21 · 同一句话,不同做法22 · 先约好模块会做什么23 · 把数据形状写在门口24 · 测试不只看一条好路25 · 把项目磨成一个命令26 · 定下小项目边界27 · 把真实文字洗干净28 · 只开三扇门29 · 把约定钉在代码旁30 · 打包,也留下脚印31 · 照任务书交卷
ELIXIR · 进阶 · LESSON 2340 分钟

把数据形状写在门口

用 `@type` 与 `@spec` 记录约定,再让 Dialyzer 寻找矛盾。

01 · 先看完整例子

为查分结果写类型

运行结果不因 typespec 改变;它增加的是可读约定与分析信息。

Elixir
defmodule Scores do
  @type student_id :: pos_integer()
  @type result :: {:ok, non_neg_integer()} | {:error, :not_found}

  # 规格与两个返回分支保持一致
  @spec fetch(%{student_id() => non_neg_integer()}, student_id()) :: result()
  def fetch(scores, id) do
    case Map.fetch(scores, id) do
      {:ok, score} -> {:ok, score}
      :error -> {:error, :not_found}
    end
  end
end

Scores.fetch(%{7 => 92}, 7)
先对照结果
  • 得到 {:ok, 92}。
02 · 回头拆代码

从第一行往下读

  1. student_id 排除 0 与负数。
  2. result 明确列出成功和失败两种形状。
  3. 项目可加入 dialyxir 后运行 mix dialyzer;依赖配置按工具当前文档操作。
03 · 认清新符号

符号不是暗号

@type name :: ...

定义一个可复用的类型名。

@spec fun(type) :: type

说明函数参数和返回值。

term()

任意 Elixir/Erlang 值。

04 · 代码里的概念

把名字和意思对上

01

typespec

写给工具与读者看的类型说明,包括自定义类型和函数规格。

02

Dialyzer

分析 BEAM 代码类型矛盾的工具,常通过项目任务运行。

05 · 再说清楚

这些写法为什么有用

Elixir 仍是动态语言。typespec 不会在每次调用前拦住错误,但能让文档、编辑器和分析工具更懂代码。

@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(____) :: ____
def double(number), do: number * 2

目标结果: 规格写成 @spec double(integer()) :: integer()。

卡住了,再看提示

输入与输出都使用 integer()。

运行过以后,再看一种答案
一种答案
# 规格如实描述函数接受和返回的值
@spec double(integer()) :: integer()
def double(number), do: number * 2
想一想:写了 `@spec` 后,Elixir 会拒绝所有类型不符的运行时调用吗?

不会。typespec 主要服务文档和静态分析;运行时仍由函数模式、guard 与代码处理输入。

带走

记住三句

  1. 1

    类型说明必须与真实分支一致。

  2. 2

    @type 可给领域数据形状命名。

  3. 3

    Dialyzer 用来寻找类型约定中的矛盾。

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