根据来自@rsc的原始类型别名设计文档,必须能够"展开"类型别名声明;对于type T = *T来说,如果不无限展开,这是不可能的。该规范在这方面没有提及任何内容;并且在循环主题上通常模糊或保持沉默。
type T = *T
ar7v8xwq1#
术语:我将“类型别名循环”用于诸如 type T = *T 之类的事物,并将“接口循环”用于诸如 type U interface { F() interface { U } } 之类的事物。在这里,我看到了三个选项:
type U interface { F() interface { U } }
$N
type T = *$1
type U interface { F() $1 }
$2
type V = map[V][]V
type V = map[$1][]$2
我们需要确保有一个规范Map。例如,[]V 可以写成 []map[$1]$2 或 []map[$1][]$2,但它必须始终是其中一个。(我倾向于前者;即,始终使用最短的类型描述符。)
[]V
[]map[$1]$2
[]map[$1][]$2
cgfeq70w2#
@mdempsky 由于这种循环只能通过名称首先创建,为什么不保留名称呢?具体来说,这可能意味着在编译器中我们需要保留一个Alias类型节点(在使用之前总是先访问别名类型)。但它也允许更好的错误信息(如果有的话,可能会引用错误场景中使用的别名名称)。
ttvkxqim3#
我认为在编译时保留额外的类型信息是可以的,但我担心我们在链接时和运行时会做什么。我们有很多方法依赖于相同的类型Map到相同的运行时类型描述符。例如,类型Assert、接口比较或使用 == 比较 reflect.Type 值。所以如果我们在不同的包中有 type t1 = *t1 和 type t2 = *t2,它们需要重用相同的运行时类型描述符。对于这个共享的运行时类型描述符,runtime.Type.String() 应该返回什么?我们可以任意选择一个,但这对我来说似乎并不比使用合成占位符(如 $1、$2 等)创建一个人工类型描述符更好。或者,我们可能修改运行时代码,使其不依赖于类型标识与类型描述符唯一性之间的对应关系,但这似乎具有侵入性,并可能(轻微地,但负面地)影响甚至不使用匿名循环的 Go 程序。我甚至不确定如果我们去掉类型描述符唯一性,是否还能在 reflect.Type 上保持 == 的功能。
type t1 = *t1
type t2 = *t2
$1
a1o7rhls4#
@mdempsky 很好的观点。编号方案似乎是一个更好的选择。我们不仅可以从左到右(写出来时)对引用的类型进行编号,还可以从左到右(写出来时)对引用的类型进行编号;这可能更容易理解。所以 T = *T 仍然是 *$1 (如前所述); V = map[V][]V 变为 map[$1][]$1 。这样做的好处是,给定的数字总是在给定的类型字符串中引用相同的类型。对于 T 和 M ,T = *T; M = map[T]M 仍然是 *$1 ,而 map[*$2]$1 是 M 。
T = *T
*$1
V = map[V][]V
map[$1][]$1
T
M
T = *T; M = map[T]M
map[*$2]$1
oo7oh9g95#
另一方面,从内到外的编号可能允许更简单的组合:在上面的最后一个示例中,我的外部类型(M)将以其内部类型的max + 1个数字开始编号(大致上说)。
xqnpmsa86#
我看到了4种可能的编码方案。对于每一种,我都将以map[T][]T为例进行编码。
map[T][]T
map[*$1][]*$1
map[*$2][]$2
map[*$2][]*$4
map[*$2][]*$3
我认为这些都可以工作,所以似乎取决于个人喜好选择使用哪一个。
我想说我们不应该使用2,因为这与我们处理非循环有向无环图(DAG)类型的方式不一致。例如,我不认为我们会将map[*int][]*int编码为map[*int][]$2。这使得正确实现变得更加困难(但并非不可能)。
map[*int][]*int
map[*int][]$2
我也想说我们不应该使用3,因为这使得人们很容易意外地实现2,它在大多数时候都能正常工作,但并非总是如此。(同样,虽然更复杂/微妙,但并非不可能。)
最后,我想说1比4更好,因为它具有这样一个属性:每次T发生时,都使用相同的描述(*$1),而4使用了两个不同的描述(*$2和*$3)。
*$2
*$3
bwleehnv7#
"inside out"编码类似于de Bruijn indices,它们同样保持术语局部化和可组合。(我不确定它们是否对应于上面的(1)或(4),但这并不重要。)
7条答案
按热度按时间ar7v8xwq1#
术语:我将“类型别名循环”用于诸如
type T = *T
之类的事物,并将“接口循环”用于诸如type U interface { F() interface { U } }
之类的事物。在这里,我看到了三个选项:
我认为,在类型别名提案中,我们已经决定采用选项1(随后基本上实现了它),但我现在认为当时没有考虑到更大的范围。据我所知,类型别名循环引入的问题不应该比接口循环更难解决。例如,我们已经有不能扩展的类型:https://play.golang.org/p/8KQfNRbGwRV。
为了一致性,我倾向于支持选项2或3而不是选项1。
选项2无疑是最简单的(除了如何在规范中解释它之外),但可能会破坏向后兼容性。
我认为,如果我们对接受 #8082 感兴趣,那么选择3是必要的。
--
我认为,我们可以通过定义一个避免需要“展开”类型的类型序列化格式来追求选项3。例如,如果我们使用
$N
表示匿名循环(其中 N 衡量了类型字面量中“向外”的步骤数),那么一些示例如下:type T = *T
可以描述为type T = *$1
type U interface { F() interface { U } }
可以描述为type U interface { F() $1 }
(或者也许可以描述为$2
,如果我们将方法签名视为单独的类型)。type V = map[V][]V
可以描述为type V = map[$1][]$2
我们需要确保有一个规范Map。例如,
[]V
可以写成[]map[$1]$2
或[]map[$1][]$2
,但它必须始终是其中一个。(我倾向于前者;即,始终使用最短的类型描述符。)cgfeq70w2#
@mdempsky 由于这种循环只能通过名称首先创建,为什么不保留名称呢?具体来说,这可能意味着在编译器中我们需要保留一个Alias类型节点(在使用之前总是先访问别名类型)。但它也允许更好的错误信息(如果有的话,可能会引用错误场景中使用的别名名称)。
ttvkxqim3#
我认为在编译时保留额外的类型信息是可以的,但我担心我们在链接时和运行时会做什么。
我们有很多方法依赖于相同的类型Map到相同的运行时类型描述符。例如,类型Assert、接口比较或使用 == 比较 reflect.Type 值。
所以如果我们在不同的包中有
type t1 = *t1
和type t2 = *t2
,它们需要重用相同的运行时类型描述符。对于这个共享的运行时类型描述符,runtime.Type.String() 应该返回什么?我们可以任意选择一个,但这对我来说似乎并不比使用合成占位符(如
$1
、$2
等)创建一个人工类型描述符更好。或者,我们可能修改运行时代码,使其不依赖于类型标识与类型描述符唯一性之间的对应关系,但这似乎具有侵入性,并可能(轻微地,但负面地)影响甚至不使用匿名循环的 Go 程序。我甚至不确定如果我们去掉类型描述符唯一性,是否还能在 reflect.Type 上保持 == 的功能。
a1o7rhls4#
@mdempsky 很好的观点。编号方案似乎是一个更好的选择。我们不仅可以从左到右(写出来时)对引用的类型进行编号,还可以从左到右(写出来时)对引用的类型进行编号;这可能更容易理解。所以
T = *T
仍然是*$1
(如前所述);V = map[V][]V
变为map[$1][]$1
。这样做的好处是,给定的数字总是在给定的类型字符串中引用相同的类型。对于T
和M
,T = *T; M = map[T]M
仍然是*$1
,而map[*$2]$1
是M
。oo7oh9g95#
另一方面,从内到外的编号可能允许更简单的组合:在上面的最后一个示例中,我的外部类型(
M
)将以其内部类型的max + 1个数字开始编号(大致上说)。xqnpmsa86#
我看到了4种可能的编码方案。对于每一种,我都将以
map[T][]T
为例进行编码。map[*$1][]*$1
。这是上面我建议的“向外”的方案。map[*$2][]$2
。这是简单地从左到右对类型进行编号,参考第一次出现的相同类型的出现。(我认为这就是你建议的。)map[*$2][]*$4
。这是2,但不允许除了打破循环之外的引用。map[*$2][]*$3
。这是3,但每次我们“弹出”类型上下文时都会重置编号。(或者,可以将其视为1,但从根类型内部而不是从循环外部计数。)我认为这些都可以工作,所以似乎取决于个人喜好选择使用哪一个。
我想说我们不应该使用2,因为这与我们处理非循环有向无环图(DAG)类型的方式不一致。例如,我不认为我们会将
map[*int][]*int
编码为map[*int][]$2
。这使得正确实现变得更加困难(但并非不可能)。我也想说我们不应该使用3,因为这使得人们很容易意外地实现2,它在大多数时候都能正常工作,但并非总是如此。(同样,虽然更复杂/微妙,但并非不可能。)
最后,我想说1比4更好,因为它具有这样一个属性:每次
T
发生时,都使用相同的描述(*$1
),而4使用了两个不同的描述(*$2
和*$3
)。bwleehnv7#
"inside out"编码类似于de Bruijn indices,它们同样保持术语局部化和可组合。(我不确定它们是否对应于上面的(1)或(4),但这并不重要。)