跳到内容

1.3 代数数据类型与类型推导:让状态空间可见

语言工坊需要表示一份“成功、等待或失败”的结果,阿花不想再靠几个可能互相矛盾的布尔字段拼状态。

Product type 表示“同时拥有这些字段”,sum type 表示“只能是这些变体之一”。它们让业务状态的组合数量显式化。

Product 与 Sum 对应乘法和加法

text
record Point(int x, int y)

若 x 有 X 种值、y 有 Y 种值,Point 有 X × Y 种组合。

text
PaymentResult = Approved(receipt) | Declined(reason) | Pending(reviewId)

变体的可能值数量相加。与 status + nullable receipt + nullable reason 相比,sum type 不允许“Approved 但 receipt 为空、reason 又存在”这种无效组合。

Java 可用 sealed hierarchy + record + pattern matching 表达,Rust 用 enum,TypeScript 用 discriminated union。关键不在语法,而在变体封闭且 payload 与状态绑定。

Exhaustive matching 让新增状态显式传播

java
return switch (result) {
  case Approved(var receipt) -> show(receipt);
  case Declined(var reason)  -> explain(reason);
  case Pending(var id)       -> track(id);
};

加入 Cancelled 后,编译器能指出未处理位置。若加一个 default -> null,就主动丢掉了这种演化帮助。只在确实需要向前兼容的外部开放协议中设计 unknown 分支,并保留原始值与遥测。

Optional/Result 把缺失与失败放进类型

Optional<T> 表达可能没有值,Result<T,E> 表达成功或有类型的失败。它们不应替代所有异常:参数校验失败、业务拒绝和基础设施崩溃的恢复策略不同。

避免 Optional<List<T>> 这类不必要双重空状态,除非“字段未提供”与“提供空列表”确实语义不同。类型应反映业务差异,而不是把所有包装器叠上去。

Type inference 解约束,不读心

编译器从字面量、参数、返回上下文和 bounds 收集相等/子类型约束,再求出满足条件的类型。局部推导减少重复,却不应隐藏公共 API 契约。

text
emptyList() 在不同上下文可能推导为 List<String> 或 List<Order>

若缺乏上下文,编译器只能选择默认/最宽类型或报错。复杂链式表达式出错时,给中间值加显式类型是提供诊断锚点,不是承认推导失败。

类型系统不能替运行时不变量

Email 若只是 String alias,不能保证格式已验证。Smart constructor 可以控制构造:

text
parseEmail(untrusted) -> Result<Email, ValidationError>

但数据库迁移、反序列化、反射和 ORM 仍可能绕过构造路径。关键不变量要在边界验证,必要时在存储层也加约束。

类型系统还无法自动证明余额守恒、权限策略或分布式时序;更强的 refinement/dependent type 可以表达更多性质,也带来证明和工具成本。选择与风险匹配的强度。

建模检查

  • 哪些字段组合其实代表互斥状态?
  • null 是缺失、未知、未加载还是失败?
  • 新增变体后,哪些消费者必须显式更新?
  • 外部开放枚举如何处理未来未知值?
  • 构造函数是否确保内部值合法,边界是否可能绕过?
  • 类型推导是否让公共契约或错误位置变得不清楚?

下一章进入类型背后的运行时:规范中的 heap/frame/reference 与实际 JIT、GC、栈上优化并不是同一层描述。

参考资料

Built with VitePress | Software Systems Atlas