跳到内容

第 9 卷 / 第 1 章 / 第 3 课

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

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

Product 与 Sum 对应乘法和加法 ​

text
record Point(int x, int y)

若 x 有 X 种值、y 有 Y 种值,且两者可独立选择,这个数对有 X × Y 种组合。字段之间若还有约束,合法状态数可能更少。

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

不同变体的可能值数量相加。每个变体只携带自己的字段,因此 Approved 不再同时带有拒绝原因。但在允许空引用的语言里,和类型不会自动拒绝 null receipt,这条约束仍需构造函数检查。

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

Exhaustive matching 让新增状态显式传播 ​

把下面代码保存为 PaymentDemo.java,用 JDK 21 或更高版本运行 java PaymentDemo.java。示例使用已正式定稿的 record pattern 与 switch 模式匹配,不需要预览开关。

java
import java.util.Objects;

class PaymentDemo {
    sealed interface PaymentResult permits Approved, Declined, Pending {}
    record Approved(String receipt) implements PaymentResult {
        Approved { Objects.requireNonNull(receipt); }
    }
    record Declined(String reason) implements PaymentResult {
        Declined { Objects.requireNonNull(reason); }
    }
    record Pending(String reviewId) implements PaymentResult {
        Pending { Objects.requireNonNull(reviewId); }
    }
    static String describe(PaymentResult result) {
        Objects.requireNonNull(result);
        return switch (result) {
            case Approved(var receipt) -> "receipt:" + receipt;
            case Declined(var reason) -> "declined:" + reason;
            case Pending(var id) -> "pending:" + id;
        };
    }
    public static void main(String[] args) {
        System.out.println(describe(new Approved("r-1")));
        System.out.println(describe(new Declined("limit")));
        System.out.println(describe(new Pending("p-1")));
    }
}

输出依次是 receipt:r-1、declined:limit、pending:p-1。调用 new Approved(null) 或 describe(null) 都会立即抛出 NullPointerException。空字符串仍能通过这里的构造函数;若业务要求非空白文本,还需增加对应校验。

新增一个实现 PaymentResult 的 Cancelled record,并把它加入 permits 列表,原 switch 就应因不再穷尽而编译失败。统一加 default -> null 会掩盖遗漏。对开放的外部协议,可以显式设计 unknown 变体,保留原始输入并有意识地处理。

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

Optional<T> 表达可能没有值,Result<T,E> 这类抽象表达成功或有类型的失败;Java 标准库没有同名的通用 Result 类型。它们不必替代所有异常:参数校验失败、业务拒绝和基础设施故障可能需要不同的恢复策略。

避免 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