Go 泛型語法又出 “么蛾子”:引入 type set 概念和移除 type list 中的 type 關鍵字

bigwhite-github發表於2021-04-08

近日,Go 泛型語法負責人之一的Ian Lance Taylor 釋出了一個 issue,說明 go 團隊想引入新的 type set 概念,並去除原 Go 泛型方案中置於 interface 定義中的 type list 中的 type 關鍵字。

對於 Go 泛型來龍去脈不是很瞭解的童鞋,可以先去看看我看看我之前的文章:《能力越大,責任越大” - Go 語言之父詳解將於 Go 1.18 釋出的 Go 泛型》。在那篇文章的結尾,Go 設計團隊對自己的 Go 泛型設計方案中的幾個方面給出了自己的滿意度評價,其中唯一讓團隊感覺還不是很完美的就是 “Type lists in interfaces”:

1. 何為 Type lists in interfaces

我們先來說說何為 Type lists in interfaces!當前 Go 泛型方案使用 interface 型別用於表達對型別引數 (type parameters) 的約束 (constraints),比如:

type MyC1 interface {
    M1()
}

func F1[T MyC1](t T) {

}

在上述程式碼中,我們使用 interface MyC1 作為型別引數 (type parameters) 的約束,對於 F1 函式而言,所有滿足 MyC1 介面的型別都可以作為其型別引數的實參傳入:

type MyT1 string
func(t1 *MyT1) M1() {}

var t1 = new(MyT1)
F1(t1)

*MyT1 實現了 MyC1 介面,於是我們可以將其例項 (t1) 傳給 F1。Go 泛型的自動型別推導會將 T 的實參置為 *MyT1。

完整程式如下:

// https://go2goplay.golang.org/p/WPCvmwkxcEL
package main

import (
    "fmt"
)

type MyC1 interface {
    M1()
}

func F1[T MyC1](t T) {
    fmt.Printf("%T\n", t)
}

type MyT1 string

func (t1 *MyT1) M1() {

}

func main() {
    var t1 = new(MyT1)
    F1(t1) // *main.MyT1
}

對於自定義型別,通過實現介面的方法集合即可滿足介面,對於型別引數可以是原生型別的情況,我們無法通過這種方式實現,於是 Go 團隊將 type list 加入到 interface 介面中,僅用作泛型型別引數的約束檢查

type MyC2 interface {
    type int, int32, int64
}

func F2[T MyC2](t T) {
    fmt.Printf("%T\n", t)
}

func main() {
    var t2 string
    F2(t2) // string
}

而 MyMC2 中的:

type int, int32, int64

就是所謂的"type list"。

如果一個 interface 定義中既有 method 也有 type list,那麼要滿足這個 interface 型別,則作為型別引數實參的型別既必須在 type list 中(或其 underlying type 在 type list 中),又必須實現介面型別的所有方法:

// https://go2goplay.golang.org/p/rE8mGH0lHWm
package main

import (
    "fmt"
)

type MyC3 interface {
    M3()
    type int, string, float64
}


func F3[T MyC3](t T) {
    fmt.Printf("%T\n", t)
}

type MyT3 string

func (t3 MyT3) M3() {

}

func main() {
    t3 := MyT3("hello")
    F3(t3) // main.MyT3
}

細心的童鞋會發現:擁有 type list 的 interface 僅能用於做為型別引數的約束,而不能像普通 interface 型別那樣使用:

// https://go2goplay.golang.org/p/mJoEYrceBSL
package main

type MyC3 interface {
    M3()
    type int, string, float64
}

func main() {
    var i3 MyC3 // type checking failed for main 
                    // prog.go2:9:9: interface contains type constraints (int, string, float64)
    _ = i3
}

這種 gap(縫隙) 始終讓 Go 核心團隊的開發人員感到 “不爽”,那麼能否將兩者融合在一起呢?即放開對包含 type list 的 interface 型別僅能做 constraint 的限制,讓其和普通 interface 一樣使用。這次引入的 type set 應該是解決這個問題的一個前提。但在這個新 proposal 中,核心團隊還沒有將這個問題作為重點,只能算作是為以後留個作業吧。

2. 引入 type set 概念

Ian Lance Taylor 釋出的這個 issue主要就是想引入 type set 概念,並用新語法等價替代原泛型 proposal中的 type list,新語法去除了原 type list 中的 type 關鍵字

於是 go 團隊試圖這樣來做:


// 當前的type list
type SignedInteger interface {
    type int, int8, int16, int32, int64
}


// type set理念下的新語法
type SignedInteger interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

我們看到新語法中去掉了原先 type list 中的 type 關鍵字,型別間的間隔也由逗號改為了管道符 |。按該 proposal 的原意,管道符 (在布林代數中也表示或) 更接近於 type list 的原意,即可以是 int,或 int8 或....。如果僅僅是變成了如下改進的語法:

type SignedInteger interface {
    int | int8 | int16 | int32 | int64
}

估計大家也沒多大意見。但是偏偏引入了 “~” 這個字首。~int 與 int 有什麼區別呢?要搞清楚區別就要先來看看 Ian 新引入的 type set 概念了。

什麼是 type set(型別集合)?Ian 給出了此概念的定義:

  • 每個型別都有一個 type set。
  • 非介面型別的型別的 type set 中僅包含其自身。比如非介面型別 T,它的 type set 中唯一的元素就是它自身:{T};
  • 對於一個普通的、沒有 type list 的普通介面型別來說,它的 type set 是一個無限集合。所有實現了該介面型別所有方法的型別都是該集合的一個元素,另外由於該介面型別本身也宣告瞭其所有方法,因此介面型別自身也是其 Type set 的一員。
  • 空介面型別 interface{}的 type set 中則是囊括了所有可能的型別;
  • 這樣一來我們來試試用 type set 概念重新陳述一下一個型別 T 實現一個介面型別 I:即當型別 T 是介面型別 I 的 type set 的一員時,T 便實現了介面 I;
  • 對於使用嵌入介面型別組合而成的介面型別,其 type set 就是其所有的嵌入的介面型別的 type set 的交集。proposal 中的舉例:type O2 interface{ E1; E2 } ,則 02 這個介面型別的 type set 是 E1 和 E2 兩個介面型別的 type set 的交集。
  • 一個擁有一個 method 的介面型別,比如:
type MyInterface1 interface {
    MyMethod()
}

可以看成嵌入一個僅包含 MyMethod 的介面型別的介面型別:

type MyInterface interface {
    MyMethod()
}
type MyInterface1 interface {
    MyInterface
}
  • 因此,一個帶有自身 Method 的嵌入其他介面型別的介面型別,比如:
type 03 interface {
    E1
    E2
    MyMethod03()
}

它的 type set 可以看成 E1、E2 和 E3(type E3 interface { MyMethod03}) 的 type set 的交集。

3. 替換 type list 的新語法方案

我們再回到前面提到的新語法方案:

// type set 新語法
type SignedInteger interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64
}

Go 開發團隊給那些用於作為約束或被嵌入到作為約束的介面型別中的介面型別的定義做了重新描述,稱這類介面型別的定義中可以嵌入一些額外的結構,被稱為interface elements,其組成如下圖:

  • 圖中 MyInterface 是一個僅用於約束或嵌入到作為約束的介面型別中的型別;
  • MyInterface 除了擁有自己的方法列表 (M1、M2) 外,還可以嵌入額外的結構:interface elements,就是 T1|T2|~T3|T4...|Tn 那一行,這一行即替代了原先方案中的 type list;
  • interface elements 這一行有三個值得關注的事情:
    • T1、T2、T4、Tn 這些僅代表 type set 僅為自身的型別;
    • ~T3 的 type set 為所有 underlying type 為 T3 的型別,~T3 被稱為 approximation elements;
    • 管道符將這些型別連線在一起,共同構成一個 union element,該 union element 的 type set 為所有這些型別的 type set 的並集。

好了現在一切都建立在 type set 這個概念上。那麼當上述介面型別作為型別引數的約束時,要想滿足該約束,可以作為型別引數的實參,那麼傳入的型別應該在作為約束的介面型別的 type set 中。

有了前面關於 type set 以及介面嵌入的 type set 的鋪墊,作為約束的介面型別的理解就容易多了。無論是單純的介面型別還是使用嵌入其他介面組合而成的介面型別,亦或是既包括嵌入也擁有自己的 method list 的介面型別。

4. 問題

Ian 的 issue 一發出就得到了社群的重點關注,並引來的激烈的討論,但從頭看到尾,似乎大家都有些 “跑題”,關於這個 proposal 的真正疑問在於 approximation elements 身上:

  • 是否有必要單獨拿出 approximation elements 這個概念

我們回顧一下當前泛型語法作為約束的介面定義所使用的 type list 語法,看看當前的 type list 語法中各個型別是否是僅代表自身?

// https://go2goplay.golang.org/p/5VbaSCQ8-Dq
package main

import (
    "fmt"
)

type S1 struct {
    Name string
    Age  int
}

type S2 S1

type MyC4 interface {
    type struct {
        Name string
        Age  int
    }, int
}

func F4[T MyC4](t T) {
    fmt.Printf("%T\n", t)
}

type MyInt int

func main() {
    var t1 = S1{"tony", 17}
    F4(t1) // main.S1
    var t2 = S2{"tony", 17}
    F4(t2) // main.S2
    var n MyInt = 3
    F4(n) // main.MyInt
}

我們看到作為約束的介面型別 MyC4 的 type list 中有兩個型別:一個匿名 struct 和 int。之後我們分別使用 S1、S2 和 MyInt 作為型別引數的實參,居然都通過了!也就是說當前的 type list 中的型別按照 type set 的概念解釋,都屬於 approximation element,只要是 underlying type 在 type list 中,那麼就可以作為型別引數的實參,通過約束檢查。

那就是說:

我們是否可以只將:

type I1 interface {
    type int, string, float64
    ... ...
}

換成:

type I1 interface {
    int | string | float64
    ... ...
}

而無需~這個符號呢?

  • 如果~符號是必要的,可否不用~符號?

Go 語言中沒有使用~運算子,但這個符號在其他主流語言,比如 C 中是位運算子,而且代表的 “非” 這個運算子。因此將其用在型別 T 前面,打眼一看,以為其含義是 “不是型別 T 的型別”。而新 proposal 則將其用於表示 approximation element。這讓很多 gopher 提出異議,希望換一個符號,比如 T+ 等。但目前尚無定論。

5. 小結

能力有限,以上一些對該 proposal 的理解可能有誤,歡迎交流指正。

type set 並沒有改變什麼,只是完成了對 interface 與實現 interface 的重新解釋。 但是對於後續將 interface element 用於普通 interface 型別定義可能有重大的意義。當前的帶有 interface element 的 interface 型別僅能用於作為泛型型別引數的約束,這與普通 interface 之間的 gap 早晚要 “填上”,不過這已經不是這個 proposal 要解決的事情。

從泛型提出到如今,我已經感到泛型的引入極大增加了複雜性 ,即便沒有濫用泛型,沒有耍奇技淫巧,泛型的引入也讓 go 複雜性陡增。就像這個 proposal,認真閱讀並理解還是需要花費不少時間和精力的。


Go 技術專欄 “改善 Go 語⾔程式設計質量的 50 個有效實踐” 正在慕課網火熱熱銷中!本專欄主要滿足>廣大 gopher 關於 Go 語言進階的需求,圍繞如何寫出地道且高質量 Go 程式碼給出 50 條有效實踐建議,上線後收到一致好評!歡迎大家訂 閱!

img{512x368}

我的網課 “Kubernetes 實戰:高可用叢集搭建、配置、運維與應用” 在慕課網熱賣>中,歡迎小夥伴們訂閱學習!

img{512x368}

Gopher Daily(Gopher 每日新聞) 歸檔倉庫 - https://github.com/bigwhite/gopherdaily

我的聯絡方式:

更多原創文章乾貨分享,請關注公眾號
  • Go 泛型語法又出 “么蛾子”:引入 type set 概念和移除 type list 中的 type 關鍵字
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章