Go Wiki: Go 2 Generics 反饋

此頁面旨在收集和整理關於 Go 2 Contracts (Generics) 草案設計 的反饋。

可以在 https://golang.org.tw/cl/149638 找到該語法的原型實現,該實現可能已針對 Go 倉庫的最新版本進行修補。

請在您的部落格、Medium、GitHub Gists、郵件列表、Google Docs 等地方釋出您的反饋。然後請在此處連結。

隨著反饋的增多,請隨時按反饋型別對本頁面進行組織或重新組織。

支援

補充(有修改的支援意見)

反向提案

反對

新增您的反饋

請按以下格式編寫所有條目。

  • 您的姓名,“標題”,月 年

為了更容易檢視新反饋,請建立一個 Gist。另外,請將新條目放在類別列表的頂部,以幫助保持列表按時間倒序排序。

快速評論

  • Chester Gould:此提案唯一的問題在於,顯式 Contracts 似乎只會增加程式碼的冗長性,這違背了程式碼簡潔易讀的目標。與其編寫顯式 Contracts,不如使用我們實際編寫的程式碼作為一種“隱式 Contract”,這樣會更簡潔優雅。這裡提供了一個示例:這裡。我承認這在此處得到了解決,但我不同意顯式 Contracts 是解決問題的辦法。在我看來,Contracts 與介面提供的功能非常相似,因此應該擴充套件介面的功能,使其行為更接近 Contracts,而不是向語言新增一種全新的語句型別。

  • Izaak Weiss:大部分討論都集中在如何實現 Contracts 或類似功能。然而,大多數“有用示例”並不需要 Contracts;它們只需要引數化多型。在沒有 Contracts 的情況下,編寫型別安全的 MergeSortSlice 是可能的。對於更簡單的 Contracts,我們可以透過高階函式來實現。泛型雜湊表可以引數化一個具有 Hash 方法的型別,或者在構造時接受一個 func(K) int64 來對其鍵進行雜湊。如果需要更多函式,可以宣告包含這些函式的結構體作為偽 Contracts,然後將它們傳遞給泛型函式。這使得 Go 的多型性變得簡單、顯式,併為未來關於 Contracts 或其他機制的進一步創新留下了空間,同時允許現在實現泛型型別的大部分好處。

  • Christoph Hack:我剛剛觀看了 Alexandrescu 的最新演講The next big Thing。他聲稱“Concepts 是在浪費時間”,並提出了一種完全不同、功能更強大的方向(甚至與 C++ 今天所能實現的一切相比)。Go 已經擁有了大多數所需的功能,例如反射和檢查型別是否實現了可選介面。唯一缺失的是程式碼生成。例如,json.Marshal 透過反射工作得很好,但如果它還能(可選地)透過實現一個由編譯器自動呼叫的 Go 函式來生成程式碼,那麼我們就擁有了一切。這可能一開始聽起來很瘋狂,並且示例可能看起來很冗長,但我認為 Alexandrescu 說得有道理。想想 gqlgen 與其他基於反射的 graphql-libs 的對比就知道了。請觀看他的演講!

  • Bodie Solomon:我發現泛型設計有點令人困惑和不透明。請考慮整合一些Zig 的漂亮 comptime 函式中的概念!Go 2 泛型的設計很巧妙,但我認為它違背了 Go 傳統上將簡單執行時語義與簡單語法緊密耦合的原則。此外,Go 的一個主要問題,阻礙了它在我希望使用的所有地方成為一個可行的競爭者,是我無法擺脫 GC 和執行時。我曾強烈希望 Go 2 會引入僅限編譯時的泛型,這樣我就可以可靠地避免在不想使用的地方使用動態介面,而無需依賴程式碼生成。不幸的是,這似乎將由編譯器決定,而無需我的輸入。請至少考慮允許使用者將泛型約束為僅限編譯時解析,也許作為 Contract 的一種屬性,拒絕編譯動態型別以滿足 Contract。

  • Dag Sverre Seljebotn:C++ 在人們濫用超程式設計(“泛型”)進行編譯時超程式設計方面存在巨大問題。我真希望 Go 能像 Julia 一樣,提供衛生宏。即使嚴格限制在編譯時邊界內,並且不進行執行時程式碼生成,這至少也能避免 C++ 世界中因其模板系統而產生的各種糟糕傾向。通常,你可以透過宏實現泛型可以做到的事情(例如,SortSliceOfInts = MakeSliceSorterFunctionMacro!(int) 可以生成一個新的函式來對整數切片進行排序)。連結:https://docs.julialang.org/en/v0.6.1/manual/metaprogramming/

  • Maxwell Corbin:討論和開放問題部分提出的問題都可以透過在包級別而不是函式或型別級別定義泛型來避免。原因很簡單:型別可以引用自身,但包不能匯入自身,雖然有許多方法可以演算法化地生成更多型別簽名,但不能用匯入語句做同樣的事情。一個快速的語法示例可能如下:

    \\ list
    package list[T]
    
    type T interface{}
    
    type List struct {
        Val T
        Next *List
    }
    
    // main
    package main
    
    import (
        il "list"[int]
        sl "list"[string]
    )
    
    var iList = il.List{3}
    var sList = sl.List{"hello"}
    
    // etc...
    

    示例中的語法可能過於冗長,但重點是部落格文章中的不幸程式碼示例都不是合法的構造。包級別泛型避免了超程式設計中最糟糕的問題,同時保留了其大部分有用性。

  • Andrew Gwozdziewycz:contract 這個詞讓我猶豫,因為它會與“Design by Contract”(DbC)中的“contract”概念混淆。雖然泛型的使用場景與 DbC 中的“Contracts”有一些相似之處(如果你仔細看的話),但這兩個概念差異很大。由於“Contracts”是計算機科學中的一個既定概念,我認為使用一個不同的名稱,如 behaviortrait,會更不容易引起混淆。設計文件還解釋了為什麼使用 interface 不是理想選擇,然而,Go 的 Contract 機制似乎是介面的顯而易見擴充套件,不應如此輕易地忽略……如果可以做到 interface setter(x T) { x.Set(string) error }interface addable(x T, y U) { x + y },它們看起來會非常自然易讀。

    • Russell Johnston:同意合併 Contracts 和介面會很棒。繞過運算子命名問題的另一種方法是提供一些標準的運算子介面,其主體無法在普通 Go 程式碼中表達。例如,一個標準的 Multipliable 介面將允許 **= 運算子,而一個標準的 Comparable 介面將允許 ==!=<<=>=>。為了表達具有多種型別的運算子,這些介面可能需要自身帶有型別引數,例如:type Multipliable(s Self /* this exists implicitly on all interfaces */, t Other) interface { /* provided by the language */ }。然後,使用者編寫的介面/Contracts 可以使用這些標準的基於識別符號的名稱,從而巧妙地避開設計文件中關於語法和型別的問題。
    • Roberto (empijei) Clapis:我同意這一點,也同意應該更清楚在哪裡使用介面和在哪裡使用 Contracts。統一兩者會很棒,因為它們都試圖解決重疊的問題。
    • Kurnia D Win:我認為 constraintcontract 是個更好的關鍵字。我個人喜歡 type addable constraint(x T, y U) { x + y } 而不是與介面合併。
  • Hajime Hoshi:我覺得這個提議對於解決我們在https://go.googlesource.com/proposal/+/master/design/go2draft-generics-overview.md 列出的問題來說太龐大了。我擔心這個功能會被濫用並降低程式碼的可讀性。抱歉,如果我遺漏了什麼,但該提案沒有提及 go generatego generate 對於解決這些問題難道不夠嗎?

  • Stephen Rowles:我覺得方法語法很難解析,作為人類閱讀者,使用不同型別的括號來包圍型別部分可能會更清晰,例如:我也是 👍 +1。還有 👍 +1(Pasha Osipyants)。

    func Sum<type T Addable>(x []T) T {
        var total T
        for _, v := range x {
            total += v
        }
        return total
    }
    
  • yesuu:在這個例子中,將 T 視為引數名,將 type 視為引數型別。顯然,將 type 放在後面更合理,然後是 contract,如 chan int

    func Sum(T type Addable)(x []T) T
    
    • Roberto Clapis:請閱讀這一部分
      • 這似乎有點敷衍了事。它說“通常”,這意味著一定已經有例外了。Go 具有漂亮的清晰語法,使程式碼易於閱讀,並且團隊易於協作。我認為為了提高程式碼的可讀性,即使使解析器更復雜一點也是值得的。對於大型和長期專案來說,程式碼的可讀性(以及因此的可維護性)至關重要。
      • 那麼這個怎麼樣?
  • Seebs:反饋太長不適合在此處引用,2018 年 8 月。總結基本上是“我希望有一種方法為兩種型別分別指定一個 contract,而不是一個 contract 針對兩種型別”,以及“我更喜歡 map[T1]T2 而不是 t1var == t1var 作為‘T1 必須是允許的 map 鍵’的規範形式”。

  • Seebs:“如果 contracts 只是型別引數函式呢?”(2018 年 9 月 1 日)

  • Sean Quinlan:我覺得 contract 語法非常混亂。對於一個本應精確定義 api 所需內容並作為其文件一部分的語言特性,它卻可以包含各種不影響 contract 的雜亂內容。而且,引用設計文件:“我們不需要解釋 contract 主體中每個語句的含義”。這似乎與我從 contract 中期望的相反。一個函式的主體可以複製到 contract 中並正常工作,這對我來說似乎是個 bug,而不是一個特性。我個人更傾向於一個統一介面和 contracts 的模型。介面感覺更接近我想要的 contract 的樣子,而且它們之間有很大的重疊。許多 contracts 也會是介面嗎?

  • Nodir Turakulov:請詳細說明。

    像 container/list 和 container/ring 這樣的包,以及 sync.Map 這樣的型別將被更新為編譯時型別安全。

    math 包將擴充套件,提供一組適用於所有數值型別的簡單標準演算法,例如常用的 Min 和 Max 函式。

    或者最好是新增一個關於現有型別/函式遷移/更新以使用型別多型的部分。據我所知,向現有型別/函式新增型別引數最有可能破壞使用該型別/函式的現有程式。math.Max 具體會如何更改?意圖是進行向後不相容的更改,並編寫工具來自動將程式碼轉換為 Go2 嗎?其他提供當前使用 interface{} 的函式和型別的庫的作者的一般建議是什麼?是否考慮了型別引數的預設值?例如,math.Max 的型別引數將預設為 float64"container/list".List 的型別引數將預設為 interface{}

  • Ward Harold:為了完整起見,應將Modula-3 的泛型設計納入其他語言設計部分。Modula-3 是一種優美的語言,可惜上市時機不對。

    • Matt Holiday:同樣,提及Alphard 語言,該語言大約與 CLU 同時開發,也影響了 Ada 的設計。請參閱 Alphard: Form and Content, Mary Shaw, ed., Springer 1991,其中收集了各種論文和一些輔助材料。Alphard 和 Ada 是我接觸泛型程式設計的入門。Go 能否在等待 40 年後,擊敗 C++ 最終交付 Contracts?
  • Ole Begemann:您在Generics Overview 頁面上寫道:“Swift 在 2017 年釋出的 Swift 4 中添加了泛型。”這是不正確的。Swift 自 2014 年首次公開發布以來就支援泛型。證據(僅舉一例):2014 年 WWDC 的一次 Apple 開發者談話記錄詳細介紹了 Swift 的泛型功能。

    這也不正確:“Equatable 似乎是 Swift 的內建型別,無法以其他方式定義。”Equatable 協議定義在 Swift 標準庫中,但它沒有什麼特別之處。在“普通”程式碼中定義相同的東西是完全可能的。

  • Kevin Gillette:對“Contracts”草案的修正,截至 2018 年 8 月 30 日

    check.Convert(int, interface{})(0, 0) 的例項應改為 check.Convert(int, interface{})(0),或提供為什麼該函式應接受兩個零而不是一個的解釋。

  • Adam Ierymenko:我有一個關於在 Go 中實現有限運算子過載的想法,這可能會使此提案對數值程式碼更有用。它內容較多,所以我放在了 Gist 中

    • DeedleFake:我完全同意反對運算子過載的論點,並且總的來說我很高興 Go 沒有它,但我同時也認為,無法透過 contract 來解決 a == ba.Equals(b) 之間的區別是當前草案設計最大的問題。這意味著對於相當一部分事情,您仍然會編寫多個函式。例如,嘗試編寫一個二叉樹。您應該使用帶有 t < tt.Less(t) 的 contract 嗎?對於求和函式,您應該使用 t + t 還是 t.Plus(t)?我肯定需要一個不涉及運算子過載的解決方案。也許有一種方法可以指定一個介面卡,基本上說“如果使用型別 T,它滿足 contract A 但不滿足 B,並且 T 被用於一個受 contract B 約束的引數,則應用此介面卡以使其滿足 contract B”。例如,Contract B 可能需要一個 Plus() 方法,而 Contract A 需要使用 +,因此介面卡會自動將使用者指定的 Plus() 方法附加到 T,以使其在該 contract 下使用。
      • 一個可能對該提案有用的內建函式是 equal(a, b),如果存在 a.Equals(b) 則使用它,否則使用 a == b,如果型別不可比較(同樣適用於其他運算子)則編譯失敗。這太奇怪了,不能認真考慮,但它將與 contracts 一起使用,並允許在不引入運算子過載的情況下解決具有運算子的型別和無法使用的型別之間的不對稱性 —jimmyfrasche
      • 另一個想法是顯式可過載的運算子:a + b 不可過載,但 a [+] b 可過載。對於基本型別,它將使用普通 +,但對於物件(如果存在),它將使用 Operator+() 等。我確實認為,沒有某種合理的運算子過載或類似機制的泛型用處不大,以至於你不如不進行泛型。-Adam Ierymenko(原始釋出者)
    • Ian Denhardt:DeedleFake 很好地闡述了沒有運算子過載的問題。我認為涉及使過載“顯式”的提案是錯誤的;相反,我們應該限制哪些運算子可以被過載,以滿足這些標準:
      1. 運算子的語義可以理解為方法呼叫。大多數數字上的運算子都通過了這項測試;big.Add 仍然是加法,從 int32、uint64 等的角度來看。未透過此測試的運算子示例是 &&||;它們是短路運算子,任何函式或方法都無法複製。無論你怎麼看,它們本質上都不是方法,不應被方法覆蓋。我認為運算子過載之所以聲名狼藉,部分原因在於 C++ 允許你覆蓋所有內容,包括像逗號運算子這樣的瘋狂東西。
      2. 應該有明確的覆蓋它們的使用場景。例如,算術運算子通過了這項測試,以及 < 及其同類運算子。指標解引用通過了第一項測試,但我很難想出“其他型別的指標”的有用的使用場景。它們在 C++ 中有一定的道理,但垃圾回收指標基本上已經涵蓋了您的情況。
      3. 運算子的正常含義應該是容易理解的。例如,指標是錯誤的常見來源,而 *foo 可能執行除了從記憶體地址讀取之外的操作,這使得本來就很困難的除錯會話更加困難。另一方面,+ 可能呼叫 big.Add 的可能性相對獨立,不太可能造成很大的混淆。
      4. 最後,標準庫必須樹立一個好榜樣;覆蓋 + 的方法在概念上應該是加法,例如。C++ 在定義實際上是 os.Stdout.ShiftLeft("Hello, World!") 的內容時,就走上了一條完全錯誤的道路。
  • Eltjon Metko:如何在函式引數內的型別識別符號之後指定 contract?這樣就可以推斷出 T 是什麼,我們就可以消除第一組括號。

    func Sum(x []T:Addable) T {
        var total T
        for _, v := range x {
            total += v
        }
        return total
    }
    
  • Tristan Colgate-McFarlane:經過反覆權衡,我最終支援該提案(大致按原樣)。有限的 contract 語法可能更可取,但我認為它應該允許引用特定的欄位(而不僅僅是方法,如一些人提出的)。如果能讓相容的介面和 contracts 之間的互用更容易,那也很好(儘管我認為可能不需要額外的規範)。最後,我認為考慮棄用介面型別是值得的。雖然很激進,但 contracts 基本上也允許指定行為。任何限制這些的 contract 限制(例如引用包內的其他型別),都應該被解除。Contracts 似乎是介面的嚴格超集,我通常反對存在兩個重疊的功能。還應考慮一個輔助編寫介面的工具。

  • Patrick Smith:我們可以考慮在定義泛型型別的方法時要求 type 關鍵字。這會使程式碼稍微冗長一些,但更清晰、更一致(現在型別引數始終以 type 關鍵字開頭)。

    func (x Foo(type T)) Bar()
    
  • Patrick Smith:在此示例中,Foo(T) 是否嵌入在 Bar(T) 中,還是 Bar(T) 有一個名為 Foo 的方法?

    type Foo(type T) interface {}
    type Bar(type T) interface {
            Foo(T)
    }
    
  • Xingtao Zhao:提案中有太多的圓括號。在提案中,據說“[]”在某些情況下存在歧義。而如果我們使用 [type T, S contract],則不再有歧義。

  • Dave Cheney:早期的 Type Functions 提案表明,型別宣告可以支援引數。如果這是正確的,那麼提議的 contract 宣告可以重寫為:

    contract stringer(x T) {
        var s string = x.String()
    }
    

    轉換為

    type stringer(x T) contract {
        var s string = x.String()
    }
    

    這支援 Roger 的觀察,即 contract 是介面的超集。type stringer(x T) contract { ... } 以與 type stringer interface { ... } 引入新介面型別相同的方式引入了新的 contract 型別。

    • jimmyfrasche:Contract 不是型別。你不能有一個 stringer 值。你有一個是 stringer 的型別的變數。它是一種元型別。型別是對值的謂詞。你問編譯器“這個值是 string 嗎?”它回答是(允許編譯繼續)或否(停止告訴你哪裡出錯了)。Contract 是對型別向量的謂詞。你問編譯器兩個問題。這些型別滿足這個 contract 嗎?然後:這些值滿足這些型別嗎?介面在某種程度上模糊了這些界限,透過儲存一個 (type, value) 對,前提是該型別具有適當的方法。它同時是型別和元型別。任何不使用介面作為元型別的泛型系統都將不可避免地包含介面的超集。雖然完全有可能定義一個僅使用介面作為元型別的泛型系統,但這意味著失去了編寫使用介面無法描述的事物(如運算子)的泛型函式的能力。你必須將你可以詢問型別的型別限制為它們的介面集。(我對此沒意見)。
  • btj:草案設計文件的“其他語言設計”部分缺少兩個非常重要的條目:Haskell(帶有型別類)和 Scala(帶有隱式引數)。

  • iamgoroot:難道不應該自然地提供更好的類型別名支援,並讓使用者選擇泛型嗎?而且你不需要太多語法來實現這一點。

type Key _
type Value _

type IntStringHolder Holder<Key:int, Value:string>

type Holder struct {
    K Key
    V Value
}

func (h *Holder) Set(k Key, v Value) {
    h.K = k
    h.V = v
}

func main() {
    v:= IntStringHolder{}
    v.Set(7,"Lucky")
}
  • antoniomo:雖然草案清楚地解釋了為什麼 F<T>F[T] 和非 ASCII(無法在此處輸入)F<<T>> 被丟棄,但感覺 F{T} 比有時連續三個 () 更具人類可讀性,同時又不會因無界前瞻而使解析器複雜化,因為在那些情況下你無法開啟一個塊。

  • aprice2704:我非常不喜歡使用普通括號 (。一個兩字元序列會導致因無界前瞻而產生的編譯器開銷嗎?<||> 怎麼樣?它們可行嗎?它們具有與 ( 截然不同的優勢,在 ASCII 中具有一定的視覺效果,並且在我在 VSCode 中使用的“Fira Code”字型(強烈推薦)中,存在使其渲染為小向右或向左三角形的連字。

  • leaxoy:首先,我很抱歉編輯了頁面底部,但我無法刪除底部內容。這是我的觀點:大量的 () 使 Go 看起來非常混亂,<> 就像其他語言一樣更好,並且對那些來自其他語言的人更友好。

  • Hajime Hoshi:我完全同意 aprice2704 關於語法的擔憂。例如,[[ / ]] 不可行嗎?


此內容是 Go Wiki 的一部分。