Go Wiki: Go 2 ジェネリクスに関するフィードバック

このページは、Go 2 のコントラクト (ジェネリクス) ドラフト設計に関するフィードバックを収集・整理することを目的としています。

構文のプロトタイプ実装はhttps://go.dokyumento.jp/cl/149638にあります。これは Go リポジトリの最新版にパッチを適用できます。

ブログ、Medium、GitHub Gists、メーリングリスト、Google ドキュメントなどにフィードバックを投稿し、そのリンクをここに掲載してください。

フィードバックの量が増えるにつれて、フィードバックの種類ごとにこのページを自由に整理したり、再編成したりしてください。

支持

補足(修正を伴う支持)

対案

反対

フィードバックの追加

すべてのエントリは以下のようにフォーマットしてください。

新しいフィードバックを見つけやすくするため、Gistを作成してください。また、新しいエントリをカテゴリリストの**一番上**に含めることで、リストを逆時系列順に並べ続けるのに役立ててください。

簡単なコメント

  • Chester Gould: この提案の唯一の問題は、明示的な契約がコードを冗長にするように思われることであり、これはシンプルで読みやすいコードという目標に反します。明示的な契約を記述する代わりに、記述する実際のコードを一種の「暗黙の契約」として使用する方がはるかにシンプルでエレガントでしょう。この例はこちらに示されています。これがこちらで述べられていることは承知していますが、明示的な契約が問題の解決策であるという意見には同意しません。契約はインターフェースが提供するものに非常に近いように思えるので、言語に新しいステートメントの種類を追加するよりも、インターフェースの動作を拡張して契約に近い動作を可能にするべきだと思います。

  • Izaak Weiss: 多くの議論は、契約、あるいはそれに類するものをどのように実装するかに焦点を当ててきました。しかし、ほとんどの「役立つ例」は契約を必要としません。それらはパラメトリック多相性のみを必要とします。型安全なMergeSortSliceの記述は、契約なしで可能です。そして、より単純な契約については、高階関数を介して実装できます。汎用ハッシュマップは、Hashメソッドを持つ型に対してパラメトリックであるか、または構築時にfunc(K) int64を取り、それを使用してキーをハッシュできます。より多くの関数が必要な場合は、これらの関数を保持する構造体を擬似契約として宣言し、それらを汎用関数に渡すことができます。これにより、Goの多相性はシンプルかつ明示的になり、将来の契約やその他のメカニズムに関するさらなる革新の余地を残しながら、ジェネリック型のほとんどの利点をすぐに実現できます。

  • Christoph Hack: Alexandrescuの最新の講演「The next big Thing」をちょうど見ました。彼は「コンセプトは時間の無駄」と述べ、全く異なる、はるかに強力な方向性(今日のC++で可能なあらゆるものと比較しても)を提案しています。Goは、リフレクションや型がオプションのインターフェースを実装しているかどうかのテストなど、必要な機能のほとんどをすでに持っています。唯一欠けているのはコード生成です。たとえば、json.Marshalはリフレクションを使用することでうまく機能しますが、コンパイラによって自動的に呼び出され、通常のGoコードを実行するGo関数を実装することで(オプションで)コードを生成できれば、すべてが揃います。最初はクレイジーに聞こえるかもしれませんが、おもちゃの例は冗長に見えるかもしれませんが、Alexandrescuには一理あると思います。たとえば、gqlgenと他のリフレクションベースのgraphql-libsを考えてみてください。彼の講演をぜひ見てください!

  • Bodie Solomon: ジェネリクスの設計は少し分かりにくく、不透明だと感じています。Zigの美しいコンパイル時関数のコンセプトをいくつか取り入れることを検討してください! Go 2 ジェネリクスの設計は賢いですが、Goの伝統的な、シンプルな実行時セマンティクスとシンプルな構文の間の密接な結合に反していると感じます。さらに、Goの最大の問題点の1つは、私が使いたいと思うあらゆる場所でGoを競争力のあるものにすることを妨げていることですが、GCとランタイムを排除できないことです。Go 2がコンパイル時のみのジェネリクスを導入し、コード生成に頼ることなく、動的インターフェースを使いたくない場合に確実に回避できるようになることを強く期待していました。残念ながら、それは私の意見なしにコンパイラによって決定されるようです。少なくとも、ユーザーにジェネリクスをコンパイル時のみの解決に制限する機能を与えることを検討してください。おそらく、契約のプロパティとして、動的な型のコンパイルを拒否して契約を満たすようにするようなものです。

  • 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の「契約」といくつかの類似点がありますが、コンセプトはかなり異なります。「契約」はコンピュータサイエンスにおける確立された概念であるため、behaviortraitのような異なる名前を使用する方がはるかに混乱が少ないと思います。設計ドキュメントでは、interfaceの使用が理想的ではない理由も示唆されていますが、Goの契約メカニズムはインターフェースのあまりにも明白な拡張であるため、すぐに無視すべきではありません…もしできるのであれば、interface setter(x T) { x.Set(string) error }interface addable(x T, y U) { x + y }は非常に自然に読んで理解できるように思えます。

    • Russell Johnston: 契約とインターフェースを統合できれば素晴らしいという点に同意します。演算子の命名問題を回避する別の方法は、通常のGoコードでは表現できない本体を持つ、演算子用のいくつかの標準インターフェースを提供することかもしれません。例えば、標準のMultipliableインターフェースは**=演算子を許可し、標準のComparableインターフェースは==!=<<=>=>を許可します。複数の型を持つ演算子を表現するには、これらのインターフェース自体に型パラメータが必要になるでしょう。例えば:type Multipliable(s Self /* これはすべてのインターフェースに暗黙的に存在する */, t Other) interface { /* 言語によって提供される */ }。そうすれば、ユーザーが記述するインターフェース/契約は、これらの標準の識別子ベースの名前を使用でき、設計ドキュメントで言及されている構文と型に関する問題をきれいに回避できます。
    • Roberto (empijei) Clapis: この点と、インターフェースとコントラクトをどこで使うべきかをもっと明確にするべきだという点に同意します。重複する問題に対処しようとしているため、両者を統合できれば素晴らしいでしょう。
    • Kurnia D Win: contractよりもconstraintの方が良いキーワードだと思います。個人的には、インターフェースとの統合よりもtype addable constraint(x T, y U) { x + y }の方が好きです。
  • Hajime Hoshi: 提案されている内容は、https://go.googlesource.com/proposal/+/master/design/go2draft-generics-overview.mdに挙げられている解決したい問題に対して、あまりにも巨大だと感じます。この機能が悪用され、コードの可読性が低下することを懸念しています。もし私の見落としでしたら申し訳ありませんが、提案にはgo generateについて何も書かれていません。go 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の後にtypeが続く形、例えばchan intのようになるでしょう。

    func Sum(T type Addable)(x []T) T
    
    • Roberto Clapis: このセクションを読んでください
      • 正直なところ、少し言い訳に聞こえます。「一般に」と言っていますが、それはすでに例外があるはずだということです。Goには明確な構文があり、コードを読みやすく、チームでの共同作業を容易にしています。コードの可読性を向上させるために、パーサーをより複雑にすることは価値があると思います。大規模で長期にわたるプロジェクトでは、コードの可読性、ひいては保守性が最も重要です。
      • これはどうでしょうか
  • Seebs: フィードバックは長すぎてインラインにはできない、2018年8月。要約すると、「2つの型に1つの契約ではなく、それぞれの型に1つの契約を指定する方法が欲しい」ということと、「『T1はマップのキーとして許容される必要がある』という規範的な形式として、t1var == t1varよりもmap[T1]T2の方が良い」ということです。

  • Seebs: もし契約が型パラメータ関数そのものだったら?。(2018年9月1日)

  • Sean Quinlan: 契約構文はかなり混乱すると感じています。何が必要かを正確に定義し、APIのドキュメントの一部となるはずのものであるのに、契約に影響を与えないあらゆる種類の不要な情報を含む可能性があります。さらに、設計から引用すると、「契約本体に現れる可能性のあるすべてのステートメントの意味を説明する必要はない」とあります。これは、私が契約に期待するものとは逆のように思えます。関数の本体を契約にコピーして機能することが、機能ではなくバグのように思えます。個人的には、インターフェースと契約を統合するモデルの方がはるかに望ましいです。インターフェースは、私が契約に求めるものにはるかに近く、多くの重複があります。多くの契約がインターフェースでもある可能性が高いように思えますか?

  • 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の設計にも影響を与えました。様々な論文といくつかの補足資料がまとめられたMary Shaw編のSpringer 1991年刊「Alphard: Form and Content」を参照してください。AlphardとAdaは私にとってジェネリックプログラミングとの出会いでした。Goは40年間待ち望まれた契約を最終的に提供することでC++に勝てるでしょうか?
  • Ole Begemann: ジェネリクスの概要ページに、「Swiftは2017年にリリースされたSwift 4でジェネリクスを追加した。」と書かれています。これは真実ではありません。Swiftは2014年の最初の公開リリース以来、ジェネリクスを持っています。証拠(多くの例の1つ):WWDC 2014でのSwiftに関するApple開発者講演の議事録では、Swiftのジェネリクス機能について詳しく語られています。

    これも不正確です。「EquatableはSwiftでは組み込みのようで、それ以外に定義することはできない。」EquatableプロトコルはSwift標準ライブラリで定義されていますが、特別なことは何もありません。「通常の」コードで同じものを定義することは完全に可能です。

  • Kevin Gillette: 2018年8月30日現在の「Contracts」ドラフトの修正

    check.Convert(int, interface{})(0, 0)のインスタンスは、check.Convert(int, interface{})(0)とするか、なぜ関数が1つではなく2つのゼロを取るべきなのかについて説明を提供すべきです。

  • Adam Ierymenko: Goで限定的な演算子オーバーロードを行うアイデアがあり、これが数値計算コードでこの提案をより有用にするかもしれません。長いので、ここにGistにまとめました

    • DeedleFake: 演算子オーバーロードに対する議論には完全に同意しますし、Goにそれがないことには全体的に非常に満足していますが、a == ba.Equals(b)の違いを契約によって解決できないことが、現在のドラフト設計の最大の問題点だと考えています。これは、かなりの数のことについて複数の関数を作成することになることを意味します。例えば、二分木を記述してみてください。t < tを含む契約とt.Less(t)を含む契約のどちらを使用すべきでしょうか?合計関数では、t + tt.Plus(t)のどちらを使用すべきでしょうか?ただし、演算子オーバーロードを伴わない解決策を強く望んでいます。おそらく、基本的に「契約Aを満たすが契約Bを満たさない型Tが契約Bによって制約されたパラメータに使用される場合、契約Bを満たすようにこれを適用する」というアダプターを指定する方法があるかもしれません。例えば、契約BがPlus()メソッドを要求し、契約Aが+の使用を要求する場合、アダプターは、その契約の下での使用期間中、ユーザー指定のPlus()メソッドをTに自動的にアタッチします。
      • この提案と機能するかもしれないのは、equal(a, b)という組み込み関数です。これは、a.Equals(b)が存在すればそれを使用し、そうでなければa == bを使用し、型が比較不可能であればコンパイルに失敗します(他の演算子も同様です)。真剣に検討するには奇妙すぎますが、契約と連携し、演算子オーバーロードを導入することなく、演算子を持つ型と持たない型の間の非対称性を回避できるでしょう。—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: 関数のパラメータ内の型識別子の後に契約を指定するのはどうでしょうか?これにより、Tが何であるかを推測でき、最初の括弧のグループを排除できます。

    func Sum(x []T:Addable) T {
        var total T
        for _, v := range x {
            total += v
        }
        return total
    }
    
  • Tristan Colgate-McFarlane: しばらく行ったり来たりした後、私はこの提案におおむね賛成です。契約の構文は限定的である方が望ましいかもしれませんが、特定のフィールド(一部が提案しているようなメソッドだけでなく)を参照できるべきだと考えます。互換性のあるインターフェースと契約の相互利用を容易にするために何かできることがあれば、それも素晴らしいでしょう(ただし、おそらく追加の仕様は不要だと思います)。最後に、インターフェース型の非推奨化を検討する価値があると思います。劇的ではありますが、契約も本質的に動作を指定することを可能にします。それを制限する契約の制約(パッケージ内の他の型を参照するなど)は、おそらく解除されるべきです。契約はインターフェースの厳密なスーパーセットであるように思われ、私は一般的に2つの重複する機能を持つことに反対です。インターフェースの記述を助けるツールも検討されるべきです。

  • 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: 以前の型関数提案では、型宣言がパラメータをサポートできることが示されていました。これが正しければ、提案されている契約宣言は、

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

    から

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

    これにより、ロジャーの「契約はインターフェースのスーパーセットである」という観察が裏付けられます。type stringer(x T) contract { ... }は、type stringer interface { ... }が新しいインターフェース型を導入するのと同じ方法で、新しい契約型を導入します。

    • jimmyfrasche: しかし、契約は型ではありません。stringerである値を持つことはできません。stringerである型の値を持つことはできます。それはメタ型です。型は値に対する述語の一種です。コンパイラに「この値はstringですか?」と尋ねると、はい(コンパイルを続行する)またはいいえ(何が問題だったかを伝えるために停止する)と答えます。契約は、型のベクトルに対する述語です。コンパイラに2つの質問をします。これらの型はこの契約を満たしますか?次に、これらの値はこれらの型を満たしますか?インターフェースは、型が適切なメソッドを持っている場合に(type, value)のペアを格納することで、これらの境界を曖昧にします。それは型であると同時にメタ型でもあります。インターフェースをメタ型として使用しない汎用システムは、必然的にインターフェースのスーパーセットを含むことになります。インターフェースをメタ型としてのみ使用する汎用システムを定義することは完全に可能ですが、それは、演算子のようにインターフェースでは扱えないものを使用する汎用関数を記述する能力を失うことを意味します。型のメソッドセットに質問を限定する必要があります。(私はこれで構いません)。
  • btj: ドラフト設計文書の「他の言語の設計」セクションには、非常に重要な2つのエントリが欠けています。それは、型クラスを持つ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: ドラフトではFF[T]、非ASCII(ここには入力できません)のF<>がなぜ却下されたのかが明確に説明されていますが、F{T}は、時々3つ連続する()よりも人間が読みやすいと感じます。また、そのような状況ではブロックを開くことができないため、パーサーを無制限の先読みで複雑にすることもないでしょう。

  • aprice2704: 通常の丸括弧(を使うのは本当に嫌です。2文字のシーケンスでは無制限の先読みによるコンパイラのオーバーヘッドが発生しますか?<| と |> はどうでしょうか?機能しますか?それらには、(とはかなり異なる、ASCIIで視覚的に意味がある、そして私がVSCodeで使っている「Fira Code」フォント(強くお勧めします)では、これらが小さな右または左を指す三角形としてレンダリングされるリガチャーがある、という利点があります。

  • leaxoy: まず、ページフッターを編集してしまい申し訳ありませんが、フッターの内容を削除できません。私の意見としては、多くの()がGoを非常に乱雑に見せています。他の言語のように<>の方が良く、他の言語から来た人々にとってより親切です。

  • Hajime Hoshi: aprice2704の構文に関する懸念に完全に同意します。たとえば、[[ / ]]ではうまくいかないでしょうか?


このコンテンツはGo Wikiの一部です。