MTProto移动协议的TL依赖类型

主条目:TL 语言

在某些情况下,MTProto 移动协议的TL类型不仅可能依赖于其他类型(多态),还可能依赖于另一个类型的参数(依赖类型)。TL 语言对此功能的支持非常有限:仅允许依赖于使用 `#` 指定的自然参数#(别名 `#` nat,但这是私有的——TL 目前不支持此别名)。类型为 `#` 的值被序列化为 0 到 2^31-1 之间的 32 位有符号数。

例如:整数元组(向量)

假设我们想用归纳法定义“一个整数”、“两个整数”和“三个整数”这三种类型。我们可以尝试这样定义它们:

empty = Empty;
single x:int = Single;
pair x:int y:int = Pair;
triple x:int y:int z:int = Triple;
quadruple x:int y:int z:int t:int = Quadruple;
...

或者:

empty = Empty;
single x:int empty = Single;
pair x:int y:single = Pair;
triple x:int yz:pair = Triple;
quadruple x:int yzt:triple = Quadruple;

或者:

tnil = Tuple0;
tcons0 hd:int tl:Tuple0 = Tuple1;
tcons1 hd:int tl:Tuple1 = Tuple2;
tcons2 hd:int tl:Tuple2 = Tuple3;
...
tcons_n hd:int tl:Tuple_n = Tuple_(n+1)

前两种变体导致相同的序列化结果。例如,(2 3 9):%triple它们都会(2 (3 9)):%triple序列化为三个 32 位数字:2 3 9。最后一种变体更好地强调了归纳定义的版本,但它使用了装箱类型。从理论角度来看,这很好,但会导致序列化过程中出现“多余的”构造函数名称。

因此,我们将使用 ` %Type-Ident%` 来表示与装箱类型对应的裸类型,该类型Type-Ident只有一个构造函数。如果此构造函数名为`% constructor`,则根据定义,`%` Type-Ident= %` constructor。现在我们可以这样定义:

tnil = Tuple0;
tcons_n hd:int tl:%Tuple_n = Tuple_(n+1)

如果我们现在从类型名称中抽象出n ,并将其作为多态(更准确地说是依赖)类型的参数,那么就可以用合适的函数式语言编写类似下面的代码:

NewType Tuple (n : #) :=
| tnil = Tuple 0
| tcons n:# hd:int tl:%(Tuple n) = Tuple (S n)
EndType;

用目标语言来说,它看起来是这样的:

tnil = Tuple 0;
tcons {n:#} hd:int tl:%(Tuple n) = Tuple (S n);

函数S : # -> #和常量O : #(它是0)分别是下一个自然数的函数(S n = n + 1)和常量 null。因此,该类型#(别名nat)的行为就像是在 TL 中使用构造函数定义一样。

O = nat;
S nat = nat;

或者,使用其他函数式语言更典型的语法,

NewType nat :=
| O
| S nat
EndType;

所有已定义组合子的类型:

O : #
S : # -> #
Tuple : # -> Type
tnil : Tuple 0
tcons : forall n : #, int -> Tuple n -> Tuple (S n)

或者

Tuple : forall n : #, Type;
tcons : forall n : #, forall hd : int, forall tl : Tuple n, Tuple (S n)

请注意,在这种情况下,构造函数tnil不依赖于参数n,而tcons则依赖于参数 n。

类似地,也可以定义一个高度为h 的完全二叉树,其叶节点为字符串:

tleaf value:string = BinTree 0;
tnode {h:#} left:(BinTree h) right:(BinTree h) = BinTree (S h);

或者一棵随机树,其所有叶节点到根节点的距离均为h ,并且所有节点都用整数标记:

hleaf value:int = Tree 0;
hnode {n:#} left:(Tree n) next:(Tree (S n)) = Tree (S n)
hnil {n:#} = Tree (S n)

另一个版本:

hleaf' value:int = Tree' 0;
hnode' {n:#} children:(list (Tree' n)) = Tree' (S n)

多态依赖类型

让我们尝试定义一个类型,Tuple X n它的值是n 个类型X值的元组。这样,Tuple它将同时具有多态性和依赖性:

Tuple : Type -> # -> Type;

在函数式语言的常见语法中:

NewType Tuple {X : Type} {n : #} :=
| vnil : Tuple X 0
| vcons {n:#} hd:X tl:%(Tuple X n) : Tuple X (S n)
EndType

或者,用 TL 语法来说,

vnil {X:Type} = Tuple X 0;
vcons {X:Type} {n:#} tl:(%Tuple X n) = Tuple X S n

最终我们得到以下几种类型的项:

vnil : forall X : Type, Tuple X 0
vcons : forall X : Type, forall n : #, X -> Tuple X n -> Tuple X (S n)

或者

vnil : forall X : Type, Tuple X 0
vcons : forall X : Type, forall n : #, forall hd : X, forall tl : Tuple X n, Tuple X (S n)

相关和

我们刚才定义的类型Tuple与内置Vector类型不同。具体来说,内置Vector类型形式上依赖于单个参数(类型),而我们的类型Tuple依赖于两个参数(类型和数字):

Tuple : Type -> # -> Type;
Vector : Type -> Type;

内置函数Vector可以用我们Tuple使用“对所有n : # 求和”来定义:

vector {X:Type} n:# v:(%Tuple X n) = Vector X;

然而,我们的方法Tuple也有其优势。例如,我们可以定义如下数据类型:

matrix_10x10 a:(%Tuple (%Tuple double 10) 10) = Matrix_10x10;

总之,请记住,在计算组合子数时,必须删除所有括号,并计算matrix_10x10字符串的 CRC32 值。matrix_10x10 a:%Tuple %Tuple double 10 10 = Matrix_10x10

此外,我们可以定义任意大小的矩阵:

matrix {X:Type} m:# n:# a:(%Tuple (%Tuple X m) n) = Matrix X;

在这种情况下,使用向量会导致在每一行中存储一行的长度(m ),例如n次。

请注意,当n > 0%Tuple X n时,类型为 ` None` 和`None` vector X(也称为 `None`和%vector X`None` )的值的序列化结果几乎相同:两种情况下,我们都会得到一个 32 位数字(等于n-1n ,具体取决于版本),后面跟着n 个类型为`X`的对象序列化结果。(这略有不准确:只有当n是常量或已知值时,类型为 `None` 的值才能被序列化;但此时n不会被显式地序列化。)%Vector X%Tuple X n

重复的特殊语法

鉴于上述构造的重要性,它以如下方式内置于 TL 语言中。任何组合子的声明中都可以使用形如 [数组字段名":" ] [ nat-ident * ] "["字段描述... "]” 的子结构,其中nat-ident是之前遇到的任何 # 类型字段的名称(如果未显式指定,则使用最近一次遇到的字段)。抽象而言,此子结构等价于:

aux_type *field-descr* ... = AuxType;
*current_constructor* ... [ *array-field-name* ":" ] (%Tuple aux_type *nat-ident*)

例如,10x10矩阵、向量和任意矩阵可以按如下方式定义:

matrix {X:Type} m:# n:# a:n*[ m*[ X ] ] = Matrix X;
matrix_10x10 a:10*[ 10*[ double ]] = Matrix_10x10;
vector {X:Type} # [ X ] = Vector X;

我们已经遇到过最后一个版本,它是“内置类型”的“定义” Vector。

当然,重复部分可以包含多个字段,其复杂程度可根据需要而定。此外,除了使用n作为重复计数器之外,还可以使用形如(n+const)(const+n)的表达式,其中const是一个较小的非负常数,它们是S (S ( ... (S n) ... ))的简写:

repeat_np1 n:# a:(S n)*[ key:string value:string ] = Dictionary;

为了计算 CRC32 值,这些表达式会被转换为不带内部空格的表达式(const+X)。此外,*在这种情况下,表达式左右两侧也不用空格隔开。

依赖类型的序列化

序列化依赖类型和多态类型并非根本性的挑战:我们有具有非零元数和类型值的组合子。例如,类型Tuple double 10 : Type可以序列化为'Tuple' '%double' 10。需要注意的是,目前在实践中几乎没有必要序列化类型,无论它们是否依赖。

TL 中的可选组合器参数

TL 中的可选组合器参数必须具备以下属性:

例如,cons {X:Type} hd:X tl:(list X) = list X参数X可以设为可选,因为它位于参数列表的最开头,并且由list X结果类型明确决定。类似地,tcons {X:Type} {n:#} hd:X tl:(%Tuple X n) = Tuple X (S n)X 和 n 的值也完全取决于Tuple X (S n)结果类型,因此它们也可以设为可选参数。

通常情况下,将满足第二个条件的所有构造函数参数移到列表开头,并按照它们在结果类型参数中出现的顺序排列,然后将它们设为可选参数,这样是合理的。采用这种方法,构造函数的完整版本很少需要——只有当我们想将多态类型或依赖类型的值作为 Object 类型的值传递时才需要。在所有其他情况下,上下文中预期值的类型已经已知,这意味着所有可选参数都可以在分解过程中恢复。