Row的四种写法

Row的四种写法,你可知道?

1. 历史考古

Row polymorphism是上个世纪70年代左右发明的一项技术, 直到最近(2019年的POPL)都有持续研究。 最早,Row polymorphism是作为subtyping的竞争对手, 用于解决polymorphic record operation的问题的。 考虑函数

\[ \lambda r \rightarrow r.x \]

在C之类的语言里,r的类型必须被手动标注出来。 在ML这样有full type inference的语言里, 则要求x只能从属于惟一的一个type, 否则,如果t1、t2都有x这个field, 我们就无法知道r的类型应该是哪一个。 但要求每一个field都只从属于惟一的类型有时是不方便的。 比如Haskell就因为这个问题而弄了一个language extension。 在那个大量学者热火朝天地formalizing OO的年代, 这个问题就有了更为重大的意义。 为了解决这个问题,最早的手段是subtyping。 上面那个函数会拥有类型 \(\forall \alpha. \{ x : \alpha \} \rightarrow \alpha\) 。 当我们给这个函数喂 \(\{ x = 1; y = 2 \}\) 时, type checker会检查 \(\{ x : int; y : int \} \leq \{ x : \alpha  \}\) 是否得到满足。 但这个方式并不完美,考虑如下的例子:

\[ \lambda r \rightarrow (r, r.x) \]

它应该有什么类型呢? 为了解决这个问题,我们需要bounded quantification:

 \[ \forall \alpha, \beta \leq \{ x : \alpha \} \rightarrow \beta \times \alpha \]

但是众所周知,bounded quantification是undecidable的。 因此,row polymorphism作为另外一种方法被提出来了。 Row polymorphism的本质, 就是把一个record中未知、待定的部分用一个variable表示。 一个record的类型不再是一串concrete的label,type二元组, 而是一个抽象的,名为“row”的东西。 而row自己一般长这样:

 \[\begin{align} \rho ::=&\ \cdot          \\        |&\ \xi            \\        |&\ \rho, l : \tau \\ \end{align}\]

用人类的语言来说,就是: 一个row要么是空的(对于没有label的空record), 要么是一个row variable \(\xi\) (因为row不是type,用的是另外的字母), 要么是另一个row,加上一个label \(l\) ,和它对应的类型 \(\tau\) 。 row的核心就是row variable, 通过substitution这一通用手段, 就可以用同一个type表示多种不同的concrete record。 上面那个例子的类型就可以写成:

\[ \forall \xi \alpha. \{ \xi, x : \alpha \} \rightarrow \{ \xi, x : \alpha \} \times \alpha \]

通过对 \(\xi\) 的不同substitution,我们就可以得到无数中不同的record, 但这些record都会包含x这个field。

人们发现row的威力不止于此。 Row polymorphism还可以用于表示可扩展的record和variant。 例如,你可以写这样的程序:

 \[\begin{align} &let\ r\ =\ \{ x = 1; y = 2 \}\ in \\ &let\ r'\ =\ \{ r\ with\ z = 1 \} \end{align}\]

注意这里r本来不包含z这个field, 但它可以被加上z这个field,或者其它任何x、y以外的field。

2. 大致分类

Row是一个很强大的功能。 它最吸引人的地方是,row可以享受和原版Hindley-Milner一样的soundness和completeness。 要知道HM是如此精致,以至于快50年过去了, 能够维持sound + complete + full type inference的HM扩展也屈指可数。 不过由于row的概念的简洁性,row的表示法可以有很多种。 大部分row system都禁止record中有重复的label, 而所有row system都会禁止索引一个不存在的label。 表达并检查上面这些限制就是一个row system的目标。 现存的row system有几个大致的派别, 但首先所有row system要面临的第一个选择是primitive operation的选取。 第一种是基于extension,也就是给一个现有record加上单一的一个label的操作。 另一种是concatenation,就是把任意的两个record拼接到一起的操作。 一个很现象的比喻是,把row看成一个label,type的association list的话, 那么extension就是cons,而concatenation就是append。 基于extension的系统比基于concatenation的系统好处理许多。 concatenation的难点可以通过一个例子说明:

 \[ \lambda r1\ r2 \rightarrow (r1 \bowtie r2).x \]

上面这个函数的类型应该是什么? 问题在于,x可能来自r1,也可能来自r2, 但它必须在r1或r2中。 如何表达这个复杂的约束条件就是concatenation的难点所在。

3. lack constraint

首先介绍的是一个基于extension的系统。 当我们要给一个record r加上一个label x时, 我们必须检查r中并不包含x。 一个很自然的思路就是把这个约束表示成对r的类型的一个constraint。 这时就可以直接利用含有constraint,支持full,sound and complete type inference的系统, 例如qualified typesHM(X)等。 当然也可以直接手动做,比如这里

lack constraint的核心就是一个名为lack的constraint,可以写成 \(\rho/l\) 。 表示 \(\rho\) 这个row里不包含 \(l\) 这个label。 接下来就只需要生成并解一系列constraint就可以了。 例如

\[ \lambda r \rightarrow \{ r\ with\ x = 1 \} \]

的类型就会是:

 \[ \forall \rho. \rho/x \Rightarrow \{ \rho \} \rightarrow \{ \rho, x : int \} \]

4. absent/presence flag

lack constraint使用constraint来表达约束。 但有constraint的system比起普通的HM毕竟复杂度要高一个级别。 所以就有了一种完全constraint free的row system。 这里首先要引入关于row的information的概念。 关于一个row,可以有positive和negative两种information。 positive information就是“这个row有那个label,对应那个类型”; negative information就是“这个row没有那个label”。 在lack的方式里,positive information是体现在type本身里面的, 而negative information是用constraint表达的。 那么,我们能不能把negative information也通过type本身直接表达呢? 这就是这种constraint free方法的原理了。 每一个label对应的不再是一个type,而是一种抽象的叫作field的东西, 定义如下:

\[\begin{align} f::=&\ Abs       \\    |&\ Pre(\tau) \\    |&\ \phi      \\ \end{align}\]

翻译成自然语言,就是说: 如果我们在一个row里看到了 \(l : f\) , f这个field可以是 Abs[sent] ,表达 \(l\) 不在row中; 可以是 Pre[sent](\tau) ,表达 \(l\) 在row中,而且有类型 \(\tau\) ; 还可以是一个variable \(\phi\) ,表示我们暂时没有关于 \(l\) 的信息。 当我们得到了关于 l 的信息后,只需要把 \(\phi\) 替换掉就可以了。

5. scoped label

这是相对比较新的一个思路,但也是个非常精彩的思路。 上面我说过“大部分row system都禁止record中有重复的label”, 而scoped label就是那个“小部分”。 思路很简单: 从一个record里拿一个不存在的label会出错,当然要禁止。 但是给一个record加上已有的field会有什么问题呢? 考虑如下的代码:

 \[\begin{align} &let\ r\ =\ \{ x = 1 \}\ in \\ &let\ r'\ =\ \{ r\ with\ x = "1" \} \end{align}\]

r'有类型 \(\{ x : int; x : string \}\) 。 这里有重复的label,但又何妨呢? 我们知道r'中第一个x有类型string,第二个x才有类型int,那么就不会出错。 如果我们想要第二个x,只要支持一个“从record中删除label”的操作就可以了。 于是这下事情就变得简单了: 根本不需要任何形式,explicit的或者写在type里的constraint了。 事实上,原论文里根本没有定义一个新的type system。 作者只是简单的把所有record相关的东西做成constructor class里面的primitive, 然后定义了一个针对row的unification,就得到了sound的type system,加上sound and complete full type inference。

6. 杂谈

说好的四种写法呢? 其实可以看到,上面给出的都是基于extension的系统。 基于concatenation的系统也有不少,而且extension几十年前就做得差不多了, 所以最近新一点的都是做concatenation的。 那么为什么不写concatenation呢? 一方面我对concatenation的了解不如extension, 一方面这些系统都比较复杂,很难几句话讲清楚, 最重要的是,我懒XD。 所以这里就丢几个链接跑路:

用extension表达concatenation

用conditional constraint + subtyping

BC Pierce和Robert Harper,但好像没法type上面那个例子

2019年的POPL

这篇论文有一个观点我非常认同, 就是哪怕同一个row system,其表达能力也可能随与其集成的类型系统的不同而不同。 想象一下,如果在Simply Typed Lambda Calculus里加上row,会发生什么呢? 答案是,row的优异特性会全部消失。extensibility就跟没有存在过一样。 这说明,row的能力是基于ML polymorphism之上的。 这也是为什么row的性质如此之好,如此简单:因为它依托于ML的优良性质。 在scoped label的论文里,作者的row system甚至不是一个type system, 而只是一个现有type system上的一套primitive。 因此,随着基础系统表达能力的不同,row system的表达能力也会随之改变。 在这点上,我更欣赏absent/present flag和scoped label的做法, 因为它们没有提出新的机制,完全利用现有type system的构造。 当然,constraint based的系统也可以利用现有的type system, 但它们重要的constraint却是独立的。 因此给它们的基础系统增强并不一定能使它们的表达能力也随之增强。

Reference

ittc.ku.edu/~garrett/pu

web.cecs.pdx.edu/~mpj/p

cs.ox.ac.uk/files/3432/

cs.tufts.edu/~nr/cs257/

people.cs.uchicago.edu/

repository.upenn.edu/cg

microsoft.com/en-us/res

citeseerx.ist.psu.edu/v

citeseerx.ist.psu.edu/v

citeseerx.ist.psu.edu/v

citeseerx.ist.psu.edu/v

(知乎你不支持markdown里有tex math dollar你好歹让我直接上传html可以吗?非要我手动用公式编辑器粘贴写好的tex有意思?)

发布于 2020-02-23