《Go 语言原本》

18.3 泛型的未来

本节内容提供一个线上演讲:YouTube 在线 Google Slides 讲稿

TODO: 需要补充并丰富描述

18.3.1 历史性评述:以 C++ 为例

Go 的绝大多数泛型提案均为 Ian Lance Taylor 所设计。Ian Lance Taylor 曾经是 GCC 社区的风云人物;是目前 gccgo 的作者和维护者,也是一名 C++ 程序员。毫不客气的说, C++ 对泛型的设计直接影响了 Go 的泛型设计,若以 Java 等其他后继者对泛型的设计进行对比, 是不合理的。除此之外,参数化多态由 ML 首次实现,但确是 C++ 将其发扬光大,没有 C++ 也就没有现在诸多静态类型的编程语言中广泛存在的泛型这一特性。

在 [Stroustrup 1994] Chapter 15: Templates, 15.2 Templates 中:

『对于与大多数人而言,(在 1988 年)使用 C++ 最大的问题就是缺乏一个扩充的标准库。 要编写这种库,遇到的最主要问题就是,C++ 没有提供一种充分一般的机制,以便与定义容器类。 如:表、向量和关联数组等。』

『回过头看,模板恰好成为精炼一种新语言特征的两种策略之间的分界线。在模板之前, 我(Bjarne Stroustrup)一直通过实现、使用、讨论、再实现的过程去精炼一个语言特征。 而在模板之后,[…] 实现通常是和这些并行讨论的。有关模板的讨论并没有像他所应该做的那样广泛, 我也缺乏批判性的实现经验。这就导致后来基于实现和使用经验又对模板进行了多方面的修订。』

『我确实认为,在开始描述模板机制时自己是过于谨慎和保守了。我们原来就应该把许多特性加进来, […] 这些特性并没有给实现者增加多少负担,但是却对用户特别有帮助。』

阐述语言的设计哲学,没有比语言的发明者更合适的人选了。从这段话中,笔者解读出了以下信息:

  1. C++ 早期并没有现在这么复杂,设计哲学也是实验、化简、发布
  2. 早年的 C++ 也没有泛型

在 Chapter 15: Templates, 15.4 Constraints on Template Arguments 中:

『模板参数并没有提出任何限制。相反,所有类型检查都被推迟到模板实例化的时刻进行(1988 年)。

「模板的用户是否应该要求其使用者说明满足什么样操作的类型,才能用于模板参数吗?例如:

1
2
3
4
5
6
template <class T {
T& operator=(const T&);
int operator==(const T&, const T&);
int operator<=(const T&, const T&);
int operator<(const T&, const T&);
};> class vector { /*…*/ };

不!如此要求用户就会降低参数机制的灵活性,又不会使实现变得简单,或使这种功能更安全……」

(1994 年)回头再看,我明白了这些限制对于可读性和早期错误检测的重要性。』

我们得出了这些信息:

  1. 在设计泛型的过程中,走了很多弯路
  2. 类型检查对泛型设计具有重要的意义

在 Chapter 15: Templates, 15.7 Syntax 中:

『语法总是一个问题。开始时我希望把模板参数直接放在模板名字的后面, 但是这种方式无法很清晰地扩展到函数模板。初看起来,不另外使用关键字的函数语法似乎好一些:

1
2
3
T& index<class T>(vector<T>& v, int i) { /*…*/ }
int i = index(vi, 10);
char* p = index(vpx, 29);

这种 “简洁” 的语法设计非常精巧,很难在程序中识别一个模板的声明, 此外还会对某些函数模板进行语法分析可能非常难。[…] 最后的模板语法被设计为:

1
template<class T> T& index(vector<T>& v, int i) { /*…*/ }

我也严肃的讨论过将返回值放在参数表之后进而很好的解决语法分析问题,

1
2
index<class T>(vector<T>& v, int i) : T& { /*…*/ }
index<class T>(vector<T>& v, int i) -> T& { /*…*/ }

但大部分人宁愿要一个关键字来帮助识别模板,[…] 选择尖括号 <…> 而不是圆括号 (…),是因为用户发现这样更容易阅读,因为圆括号在 C/C++ 里已被过度使用。事实证明,使用圆括号进行语法分析也并不困难,但读者(reader)总是喜欢尖括号 <…>。』

C++ 在早年的一些设计目标与现在的 Go 非常相似。笔者曾经第一次阅读这本书的时候(大概是 2013 年前后,那时正直本科正在学习 C++)并没有意识到这一点, 那时对多门语言理解的我,只觉得此书虚无缥缈,反倒是准备这次分享时重读后才有的感受,可谓醍醐灌顶。

Go 语言泛型的主要设计者 Ian Lance Taylor 称,在早些年进行泛型设计的时候没有参考 Bjarne 的设计决策,只参考了不同语言中泛型的工作方式,但我们现在看来他们都从不同的起点出发,殊途同归,走到了同一个交叉路口。

18.3.2 展望

目前 Go 语言泛型并不支持这些有利于泛型编程的特性:

  • 非类型参数合约
  • 变长参数合约
  • 运算符重载 …

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type Tuple (type Ts ...comparable) struct {
    elements ...Ts
}
func (t *Tuple(Ts...)) Set(es ...Ts) {
    t.elements(Ts...){es...}
}
func (t Tuple) PirntAll() {
    for _, e := range t.elements {
        fmt.Println(e)
    }
}
// func (t Tuple(Ts...)) Get(i int) T?!?
  • 目前的设计不支持变长类型参数
  • 目前的设计未实现变长参数表达式(即 …)
  • 进而无法实现 Tuple
    • 单纯引入 …C 的语法不能够解决多个类型的索引问题
    • 考虑 Tuple 的 Get 方法
  • 单纯从索引的角度来看,但是会产生歧义
    • 可以使用 v1, v2 := t.elements[0], t.elements[1]
    • 可以使用 for _, e := range t.elements
    • 可以使用 reflect
  • 索引的边界检查问题也不简单,考虑
    • 编译期索引
    • 运行时索引

回顾来看,Go 2 中基于合约的泛型设计,是可以理解的,经过多次迭代、吸取了诸多决策失误的经验,目前的实现粗略的说是一种基于特设多态实现的参数化多态。

  • 目前的实现相对完整,但存在一些功能性的缺失,但更像是有意为之(语言更加复杂)
  • 还存在非常多可改进的空间
  • 会像 try proposal 一样被废弃吗?个人看法:形势还不够明朗(例如:社区反馈不够丰富),但被接受的概率很大
  • 会修改语法吗?个人看法:可能不会。
  • 什么时候会正式上线?个人看法:
    • 取决于社区的反馈和大量的实践
    • 以 C++ 的历史经验来看,在模板特性草案被正式定稿时,已经有大量的泛型实现,如 STL
    • Go 也需要这种社区的力量(尽管 Go 团队喜欢「一意孤行」🤷‍♂️,非贬义)
  • 引入泛型会打破向前兼容性吗?
    • 从现在的设计来看,不会
    • 但从 C++ 的历史经验来看,已经积累的代码的迁移过程将是痛苦且漫长的