immut 设计

设计目标

immut 门面包让 luna-poly 中面向值的那一半只需一次导入。把多项式当作值来使用的用户,不应需要知道稠密、项、稀疏和上下文多项式分布在四个包中,也不应需要知道单项式和 trait 来自 core 和 luna-generic。

数学背景

每个不可变类型都建模多项式环中的一个元素:DensePolynomial 对应 R[x]R[x],TermPolynomial 和 SparsePolynomial 对应 R[x0,x1,… ]R[x_0, x_1, \dots],ContextPolynomial 对应 R[Γ]R[\Gamma](参见 core 设计)。环元素是值:f+gf + g 是一个新元素,ff 不会改变。不可变层正是如此:没有任何操作会改变已有的多项式,因此多项式可以被共享、存放在多处,并在任何计算之后继续使用,就像整数一样。

设计决策

实现包之上的门面包

问题。把实现拆分为 immut/dense、immut/term、immut/sparse 和 immut/context,可以让每种表示保持精简,并让各包只依赖其实际使用的部分(dense 不依赖多变量代码)。但四个导入,加上 core,再加上 luna-generic,作为入口并不友好。

选择。immut 仅由 pub using 重新导出组成。这些别名就是同一类型,而非包装类型,因此值可以在导入门面包的代码与导入子包的代码之间流转而无需转换。重新导出 luna-generic 的 trait,使得 A : @immut.Ring 这类约束也能用同一个导入写出。

处处是值语义

四种表示都把内容保存在持久向量中,或保存在构造后从不修改的私有字段之后,并且每个构造器都会复制调用者拥有的数组。因此:

  • 没有任何公开函数会修改其接收者或参数;
  • 对同一个多项式观察两次,得到的答案相同;
  • 在数据结构之间共享多项式总是安全的。

代价是增量更新(SparsePolynomial::add_term)会重建结果。增量算法应放在 mutable 层。

表示之间的显式转换

表示之间不会隐式转换。TermPolynomial 与 SparsePolynomial 通过 to_terms() 和 from_terms 互相转换,后者会重新进行规范化;ContextPolynomial 把二者之一与上下文绑定,并通过 to_term_polynomial 或 to_sparse_polynomial 转换回去。保持转换显式,可以让各实现包彼此独立,并使每一项开销都在代码中清晰可见。

与 mutable 的对称性

该门面包导出的 trait 集合和类型名与 mutable 门面包 相同,且每个不可变类型都有一个具有相同构造器、查询、运算符和带检查变体的可变对应类型。基于共享 trait 或操作记录编写的代码可以在两者上运行。差异列于 mutable 设计。

正确性 / 不变量

  • 每个导出的类型都与其在实现包或 core 中的定义完全相同。
  • 每次公开操作之后,每个不可变多项式都处于规范形式(修剪过的稠密向量、已排序并合并的项数组、不含零的稀疏映射),因此凡是提供了 Eq 的地方,== 都是多项式相等。
  • 没有任何公开操作会修改已有的值。

被否决的替代方案

  • 单一根包。0.1 版的布局把所有内容放在一个包中。拆分使依赖关系变得显式;门面包保留了单一导入。
  • 在门面包中使用包装类型。包装类型在每个包边界都需要转换;别名则完全不需要。
  • 隐式表示转换。背着用户选择存储方式会隐藏开销;只有 ContextPolynomial 会选择存储方式,而且仅针对混合操作数。

边界

  • 门面包不添加任何自己的函数、类型或行为。
  • 构造器 Scalar 和 Polynomial 可通过重新导出的 ContextSubstitutionValue 类型访问,而不是作为独立的门面包值。
  • 持久性通过复制和封装实现;多项式与其更新结果之间没有结构共享。
  • 原地更新不在本包范围内;参见 mutable。