Skip to content

定义Definition

行多态

Row polymorphism · Row types

以开放行变量保留记录的未知字段,并用标签无序相等和lacks约束维持唯一标签。

形式陈述 ​

行多态用一个行变量表示记录中尚未列出的字段,并在输入输出之间保留这部分字段的信息。固定不可变、无重复标签的记录系统。行语法为

ρ::=∅∣ℓ:T,ρ∣r.

r是行变量,不是普通值类型变量。记录类型记为 Rec(ρ),也简写为 {ℓ1:T1,…∣r}。记录是带标签的积;行是描述标签到类型对应关系的类型层对象。

两行若包含相同标签及对应类型,就视为相等,书写顺序无关。本页要求每个标签至多出现一次,因此形成 ℓ:T,ρ 时须有 lacks约束 ℓ∉ρ。替换行变量后仍须保持这个约束,不能把重复字段藏进未知尾部。

例如

∀(r:Row). name∉r⇒{name:String∣r}→{name:String∣r}

是带种类与约束的行方案,选择该页独立于普通 HM 情形给出的扩展。量化的r在一次调用的输入输出中是同一个行,而不是两个互不相关的“任意其他字段”。

本页不采用允许同名字段按作用域遮蔽的scoped-label系统。后者有另一套字段选择和行相等规则,不能一边允许重复标签、一边沿用这里的唯一标签判定。

直觉

“我只需要name字段”常有两种接口需求。读取name并返回字符串时,其他字段可以不出现在结果里;修改name并返回整条记录时,调用者还希望知道age、active等字段仍在。

行变量像一张原样传递的字段清单。函数知道name的类型,r记住剩余清单;函数返回时再次使用r,所以调用者能恢复完整结果类型。

例子与边界

更新之后为什么仍能读age ​

设

text
updateName r = replace r.name with "Lin"
p = { name="Mei", age=31, active=true }
q = updateName p

replace表示产生一个新记录,保留其他字段,并不通过别名修改原记录。类型检查把r实例化为

{age:Int,active:Bool},

它确实不含name。于是q的类型为

{name:String,age:Int,active:Bool}.

运行结果为 {name="Lin",age=31,active=true},因此 q.age+1仍能检查为Int,结果32。函数只检查name,不需要为每一种额外字段组合重新写定义。

一般的字段替换甚至可以改变该字段的类型:先移除旧字段,再扩展新字段,得到

∀a,b,r. ℓ∉r⇒b→{ℓ:a∣r}→{ℓ:b∣r}.

这里是不可变记录的结构操作。若把它解释为给共享可变对象原位改字段类型,旧别名可能仍按a读取,就需要另外的存储类型规则,不能直接套用此方案。

与闭合记录统一 ​

把开放类型 {name:α∣r} 与闭合类型 {age:Int,name:String}统一。先按标签找到共同字段name,得到 α=String;再把剩余字段交给r,得到

r={age:Int}.

最后检查继承来的约束name不在r中,成立。字段在右侧写成name、age或age、name,不影响这个解。若只是把行编码成普通有序列表,再用未经修改的一阶合一,便会把本来相等的行误判成不一致。

两边都有未知尾部 ​

考虑

{name:String∣r}={age:Int∣s},

已有name不在r、age不在s。因为左边也必须有age,右边也必须有name,引入新鲜行变量t,得到

r={age:Int∣t},s={name:String∣t},

并要求t同时缺少name和age。代回后,两边都包含name:String、age:Int及相同的剩余t。这比把两个尾部直接设为相等更准确;直接令r=s会遗漏需要插入的字段。

共同标签下的普通有限类型等式可调用一阶合一求解;标签交换与开放行尾则需要专门的行规则。行统一器先处理共同标签的类型等式,再把各边独有标签插入另一边的开放尾部,必要时引入共享新尾。若某边闭合却缺字段,就失败;给行变量绑定新行时,仍须检查occurs条件和lacks条件。不能只做一次集合并集,因为两个同名字段可能提出不同类型要求。

重复与缺失各有明确失败点 ​

  • {name:Int,name:String}不是合法行,即使两个字段改成同型也违反本页唯一标签约定
  • 已知 name∉r,却令 r={name:Bool},违反lacks约束
  • 把 {name:String|r}用于闭合记录 {age:Int},对方缺少name且没有尾部可补,拒绝
  • {name:Int|r}与闭合 {name:String,age:Int}共同字段类型冲突,拒绝

这些都不是运行时“找不到字段再抛异常”的策略,而是类型检查期间已具备的结构矛盾。

推论与应用

行多态与宽度子类型可以提供相似的输入灵活性,但保存的信息不同。若函数只有闭合签名 {name:String}→{name:String},客户端可能通过宽度子类型传入更多字段,却无法仅由该返回类型知道age仍在。共享r的签名则把剩余字段的对应关系写进接口。更丰富的有界多态系统也可表达保留关系,所以不能笼统说“子类型系统一定会丢字段”。

一套正确的行检查至少维持三个不变量:已知标签唯一;行替换保留所有lacks条件;每次统一后的两边表示同一个标签到类型的映射。初学实现可以用有限映射保存已知字段、另存一个可选尾变量,先把字段顺序规范化,再求类型和尾部约束。规范化只是表示步骤,不能取代尾变量作用域和递归出现检查。

相同思想可用于开放变体、配置记录,以及效应行中保留未知效果。不过效应标签是否允许重复、合并是否幂等、顺序是否有意义,都要重新声明;记录行的唯一标签约定不是所有行系统通用的事实。

参考资料
关系图谱9 个相邻概念 · 2 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组
类型化关系

使用的工具