container/adapters 设计

设计目标

container 包定义能力而不依赖任何具体类型。总得有人为仓库自己的类型提供字典,而这部分代码既要看到能力记录,也要看到每个具体包。container/adapters 就是这样的位置:一个依赖于它所适配的一切的叶子包,从而其他包都不必如此。

数学背景

适配器是具体类型表示了容器模型中抽象对象的证据(见 container 设计)。对类型 V,它通过 length 和 get 提供指称 ⟦⋅⟧:V→([n]→T)\llbracket\cdot\rrbracket : V \to ([n] \to T),并在可能时通过 tabulate 提供它的一个截面:

⟦tabulate(n,f)⟧=f∣[n].\llbracket \mathtt{tabulate}(n, f) \rrbracket = f|_{[n]} .

对视图而言,指称由形状为 (r,c)(r, c) 的底层矩阵 AA 计算得到:

⟦rowi(A)⟧(j)=Aij,⟦colj(A)⟧(i)=Aij,⟦AviewT⟧(i,j)=Aji.\llbracket \mathrm{row}_i(A) \rrbracket(j) = A_{ij}, \qquad \llbracket \mathrm{col}_j(A) \rrbracket(i) = A_{ij}, \qquad \llbracket A^{\mathsf T}_{\text{view}} \rrbracket(i, j) = A_{ji} .

视图没有 tabulate:构造必须创建一个底层矩阵,而结果将不再是调用者所持有的任何对象的视图。视图的可变编辑会写穿到 AA,这正是视图的用途。

设计决策

独立的叶子包

问题。 如果 container 包含这些适配器,它就会依赖 immut、mutable 和 backends/default,而每个只需要能力记录的外部库都会引入所有具体类型。

决定。 记录和算法留在 container 中,它只依赖 error。适配器位于 container/adapters,它依赖 container、immut、mutable 和 backends/default。仓库中没有包依赖这些适配器。

理由。 这样依赖就从具体指向一般。外部库只需针对 container 发布自己的字典,参见集成指南。

每个类型与能力一个工厂函数

每个工厂函数的名称形如 <type>_<capability>_ops,并返回一个新记录。带有自由类型参数的泛型函数不能作为值存储,因此用工厂函数来提供“对每个 T,@immut.Matrix[T] 的读取字典”。工厂函数对 T 没有约束,因为读取、构造和编辑从不检查元素。

编辑模型遵循所有权

不可变类型及其包装类型获得持久化编辑;可变类型、视图和可变包装类型获得可变编辑。为可变矩阵提供持久化编辑,每次 set 都需要完整复制,这会把 O(rc)O(rc) 的开销藏在一个看似 O(1)O(1) 的调用后面;为不可变矩阵提供可变编辑则不可能不破坏其值语义。

先校验再委托

具体类型在索引非法时中止。每个适配器先检查索引或形状并返回错误值,因此即使它们调用的方法会中止,这些字典仍满足 container 的无 panic 契约。

正确性与不变量

  • 每个读取适配器在合法索引上满足 get(v,i)=Ok(v[i])\mathtt{get}(v, i) = \mathrm{Ok}(v[i]),在其他索引上返回 IndexOutOfBounds;构造器满足上述 tabulate/get 定律。
  • 构造器按行主序对每个元素恰好调用一次初始化函数。
  • 持久化编辑从不修改其参数:@immut 的更新是对持久化向量的路径复制。
  • 视图的可变编辑只修改底层矩阵,不影响其他任何东西。
  • 每个工厂函数都在 O(1)O(1) 内运行;字典调用的开销与底层方法相同(@mutable 读取为 O(1)O(1),@immut 为 O(log⁡32n)O(\log_{32} n))。

被否决的方案

  • 在每个具体包内部提供适配器。 这样 immut 和 mutable 就会依赖实验性的 container 层,使稳定的具体 API 继承其不稳定性。
  • 为外部类型提供适配器。 本仓库只为自己维护的类型提供适配器;外部库自行负责其类型的适配器。

边界

该包不新增能力,也不新增算法,只为现有类型提供证据。它不为视图提供构造字典,不为可变类型提供持久化编辑,也不为本仓库之外的类型提供适配器。已撤下的 OpenBLAS 后端的适配器与该后端一起保存在 contrib/openblas_backend 中,不属于本包。