Go: 提案:仕様:合計タイプ/識別された共用体を追加する

作成日 2017ĺš´03月06日  Âˇ  320コメント  Âˇ  ソース: golang/go

これは、識別された共用体としても知られる合計タイプの提案です。 Goの合計型は、次の点を除いて、基本的にインターフェイスのように機能する必要があります。

  • それらは構造体のような値型です
  • それらに含まれる型はコンパイル時に修正されます

合計タイプは、switchステートメントと照合できます。 コンパイラは、すべてのバリアントが一致することを確認します。 switchステートメントの内部では、値は、一致したバリアントのものであるかのように使用できます。

Go2 LanguageChange NeedsInvestigation Proposal

最も参考になるコメント

この提案を作成していただきありがとうございます。 私はこのアイデアを1年ほどいじっています。
具体的な提案は以下のとおりです。 私が思うに
「選択タイプ」は実際には「合計タイプ」よりも適切な名前かもしれませんが、YMMVです。

Goの合計タイプ

合計タイプは、「|」と組み合わせた2つ以上のタイプで表されます。
オペレーター。

type: type1 | type2 ...

結果のタイプの値は、指定されたタイプの1つのみを保持できます。 NS
タイプはインターフェイスタイプとして扱われます-その動的タイプは
それに割り当てられている値。

特別な場合として、「nil」を使用して、値が可能かどうかを示すことができます
ゼロになります。

例えば:

type maybeInt nil | int

合計タイプのメソッドセットは、メソッドセットの共通部分を保持します
同じメソッドを除く、すべてのコンポーネントタイプの
名前が異なる署名。

他のインターフェイスタイプと同様に、合計タイプは動的な対象となる可能性があります
型変換。 タイプスイッチでは、スイッチの最初のアームは
保存されているタイプに一致するものが選択されます。

合計タイプのゼロ値は、の最初のタイプのゼロ値です。
合計。

合計タイプに値を割り当てるとき、値がより多くに収まるかどうか
可能なタイプの1つよりも、最初のタイプが選択されます。

例えば:

var x int|float64 = 13

動的型intの値になりますが、

var x int|float64 = 3.13

動的タイプfloat64の値になります。

実装

ナイーブな実装では、合計型をインターフェイスとまったく同じように実装できます
値。 より洗練されたアプローチでは、表現を使用できます
可能な値のセットに適しています。

たとえば、ポインタのない具象型のみで構成される合計型
追加の値を使用して、非ポインタ型で実装できます。
実際のタイプを覚えておいてください。

構造体タイプの合計の場合、予備のパディングを使用することも可能かもしれません
その目的のための構造体に共通のバイト。

全てのコメント320件

これは、オープンソースのリリース前から、過去に何度か議論されてきました。 過去のコンセンサスは、合計型はインターフェイス型にあまり追加されないというものでした。 すべてを整理すると、最終的に得られるのは、型スイッチのすべてのケースに入力したことをコンパイラーがチェックするインターフェース型です。 これは、新しい言語の変更にとってはかなり小さなメリットです。

この提案をさらに推し進めたい場合は、次のような、より完全な提案ドキュメントを作成する必要があります。構文は何ですか。 正確にはどのように機能しますか? (あなたはそれらが「値型」であると言いますが、インターフェース型も値型です)。 トレードオフは何ですか?

知っておくべき過去の議論については、 https://www.reddit.com/r/golang/comments/46bd5h/ama_we_are_the_go_contributors_ask_us_anything/d03t6ji/?st = ixp2gf04&sh = 7d6920dbを参照して

これはGo1の型システムの変更としてはあまりにも重要であり、差し迫った必要はないと思います。
Go2のより大きな文脈でこれを再検討することをお勧めします。

この提案を作成していただきありがとうございます。 私はこのアイデアを1年ほどいじっています。
具体的な提案は以下のとおりです。 私が思うに
「選択タイプ」は実際には「合計タイプ」よりも適切な名前かもしれませんが、YMMVです。

Goの合計タイプ

合計タイプは、「|」と組み合わせた2つ以上のタイプで表されます。
オペレーター。

type: type1 | type2 ...

結果のタイプの値は、指定されたタイプの1つのみを保持できます。 NS
タイプはインターフェイスタイプとして扱われます-その動的タイプは
それに割り当てられている値。

特別な場合として、「nil」を使用して、値が可能かどうかを示すことができます
ゼロになります。

例えば:

type maybeInt nil | int

合計タイプのメソッドセットは、メソッドセットの共通部分を保持します
同じメソッドを除く、すべてのコンポーネントタイプの
名前が異なる署名。

他のインターフェイスタイプと同様に、合計タイプは動的な対象となる可能性があります
型変換。 タイプスイッチでは、スイッチの最初のアームは
保存されているタイプに一致するものが選択されます。

合計タイプのゼロ値は、の最初のタイプのゼロ値です。
合計。

合計タイプに値を割り当てるとき、値がより多くに収まるかどうか
可能なタイプの1つよりも、最初のタイプが選択されます。

例えば:

var x int|float64 = 13

動的型intの値になりますが、

var x int|float64 = 3.13

動的タイプfloat64の値になります。

実装

ナイーブな実装では、合計型をインターフェイスとまったく同じように実装できます
値。 より洗練されたアプローチでは、表現を使用できます
可能な値のセットに適しています。

たとえば、ポインタのない具象型のみで構成される合計型
追加の値を使用して、非ポインタ型で実装できます。
実際のタイプを覚えておいてください。

構造体タイプの合計の場合、予備のパディングを使用することも可能かもしれません
その目的のための構造体に共通のバイト。

@rogpeppeそれはタイプアサーションとタイプスイッチとどのように相互作用しますか? おそらく、合計のメンバーではない型(または型へのアサーション)にcaseがあると、コンパイル時エラーになります。 そのようなタイプで非網羅的なスイッチを使用することもエラーになりますか?

タイプスイッチの場合、

type T int | interface{}

そしてあなたはする:

switch t := t.(type) {
  case int:
    // ...

tにはintを含むinterface {}が含まれていますが、最初のケースと一致しますか? 最初のケースがcase interface{}場合はどうなりますか?

または、合計タイプに具象タイプのみを含めることはできますか?

type T interface{} | nilどうですか? あなたが書くなら

var t T = nil

tのタイプは何ですか? それともその建設は禁止されていますか? type T []int | nilについても同様の質問が発生するため、インターフェイスだけではありません。

はい、コンパイル時エラーが発生するのは合理的だと思います
一致することができないケースを持っていること。 それがそうであるかどうかわからない
そのようなタイプで非網羅的なスイッチを許可することをお勧めします-私たちは
他の場所で網羅性を必要としないでください。 かもしれない1つのこと
ただし、良いことです。スイッチが完全な場合、デフォルトを要求することはできません。
それを終了ステートメントにするために。

つまり、次の場合にコンパイラでエラーが発生する可能性があります。

func addOne(x int|float64) int|float64 {
    switch x := x.(type) {
    case int:
        return x + 1
    case float64:
         return x + 1
    }
}

合計タイプを変更して、ケースを追加します。

タイプスイッチの場合、

タイプTint | インターフェース{}

そしてあなたはする:

スイッチt:= t。(タイプ){
ケースint:
//..。
tにはintを含むinterface {}が含まれていますが、最初のケースと一致しますか? 最初のケースがケースインターフェイス{}の場合はどうなりますか?

tには、intを含むインターフェイス{}を含めることはできません。 tはインターフェースです
他のインターフェイスタイプと同じようにタイプしますが、
列挙されたタイプのセットが含まれています。
インターフェイス{}がintを含むインターフェイス{}を含むことができないのと同じように。

合計型はインターフェイス型と一致する可能性がありますが、それでも具体的なものになります
動的値のタイプ。 たとえば、次のようにすると問題ありません。

type R io.Reader | io.ReadCloser

タイプTインターフェースはどうですか{} | なし? あなたが書くなら

var t T = nil

tのタイプは何ですか? それともその建設は禁止されていますか? タイプT [] int |についても同様の質問が発生します。 nilなので、インターフェイスだけではありません。

上記の提案によると、あなたは最初のアイテムを手に入れます
値を割り当てることができる合計で、
あなたはnilインターフェースを手に入れるでしょう。

実際、インターフェース{} | nilは技術的に冗長です。これは、任意のインターフェイス{}
ゼロにすることができます。

[] intの場合| nil、nil [] intはnilインターフェイスと同じではないため、
具体的な値は([]int|nil)(nil)なり[]int(nil)ではない型なしnil 。

[]int | nil場合は興味深いです。 型宣言のnilは、常に「nilインターフェイス値」を意味すると思います。その場合

type T []int | nil
var x T = nil

xがnilインターフェースであり、nil []intではないことを意味します。

その値は、同じタイプでエンコードされたnil []intとは異なります。

var y T = []int(nil)  // y != x

合計がすべての値型であっても、常にnilは必要ではないでしょうか。 そうでなければ、 var x int64 | float64はどうなるでしょうか? 他のルールから外挿した私の最初の考えは、最初のタイプのゼロ値ですが、 var x interface{} | intどうでしょうか? @bcmillsが指摘しているように、それは明確な合計nilでなければなりません。

微妙すぎるようです。

網羅的なタイプのスイッチがいいでしょう。 望ましい動作でない場合は、いつでも空のdefault:追加できます。

提案には、「合計タイプに値を割り当てるときに、値がより多くに収まる場合
可能なタイプの1つよりも、最初のタイプが選択されます。」

だから、と:

type T []int | nil
var x T = nil

nilは[] intに割り当て可能であり、[] intは型の最初の要素であるため、xは具象型[] intになります。 他の[] int(nil)値と同じになります。

合計がすべての値型であっても、常にnilは必要ではないでしょうか。 そうでなければ、var x int64 | float64 be?

提案には、「合計タイプのゼロ値は、の最初のタイプのゼロ値です。
合計。」なので、答えはint64(0)です。

他のルールから外挿した私の最初の考えは、最初のタイプのゼロ値ですが、次にvar x interface {} | int? @bcmillsが指摘しているように、それは明確な合計nilでなければなりません。

いいえ、その場合は通常のインターフェイスのnil値になります。 そのタイプ(interface {} | nil)は冗長です。 おそらく、ある要素が別の要素のスーパーセットである合計型を指定するコンパイラにすることをお勧めします。現在、そのような型を定義する意味がわかりません。

合計タイプのゼロ値は、合計の最初のタイプのゼロ値です。

これは興味深い提案ですが、sumタイプは現在保持している値のタイプをどこかに記録する必要があるため、sumタイプのゼロ値がall-bytes-zeroではないことを意味すると思います。 Goの他のすべてのタイプ。 または、タイプ情報が存在しない場合、値はリストされている最初のタイプのゼロ値であるという例外を追加することもできますが、そうでない場合はnilを表す方法がわかりませんリストされている最初のタイプ。

したがって、 (stuff) | nilは、(もの)の何もnilにできない場合にのみ意味があり、 nil | (stuff)は、ものの何かがnilにできるかどうかによって異なる意味を持ちますか? nilはどのような価値を追加しますか?

@ianlancetaylor多くの関数型言語は、基本的にCの場合と同じように(閉じた)合計型を実装していると思います

struct {
    int which;
    union {
         A a;
         B b;
         C c;
    } summands;
}

whichがユニオンのフィールドに順番にインデックスを付ける場合(0 = a、1 = b、2 = c)、ゼロ値の定義はすべてのバイトがゼロになるように機能します。 また、インターフェイスとは異なり、タイプを別の場所に保存する必要があります。 また、タイプ情報を保存する場所では、ある種のnilタグを特別に処理する必要があります。

これにより、特別なインターフェイスではなく、ユニオンの値型が作成されます。これも興味深いことです。

タイプを記録するフィールドに最初のタイプを表すゼロ値がある場合、すべてゼロの値を機能させる方法はありますか? これを表現するための1つの可能な方法は次のようになると思います。

type A = B|C
struct A {
  choice byte // value 0 or 1
  value ?// (thing big enough to store B | C)
}

[編集]

申し訳ありませんが、 @ jimmyfrascheは私を殴りました。

nilによって追加されたもので実行できなかったものはありますか

type S int | string | struct{}
var None struct{}

?

それは(少なくとも私が持っている)多くの混乱を回避するようです

またはそれ以上

type (
     None struct{}
     S int | string | None
)

あなたは上のスイッチを入力することができ、そのようNoneとして割り当てるNone{}

@jimmyfrasche struct{}はnilと等しくありません。 細かい部分ですが、合計のタイプスイッチが他のタイプのタイプスイッチと不必要に(?)分岐することになります。

@bcmills他のことを主張するつもりはありませんでした。つまり、合計のどのタイプでもnilの意味と重複することなく、値の欠如を区別するのと同じ目的で使用できるということです。

@rogpeppeこれは何を印刷しますか?

// r is an io.Reader interface value holding a type that also implements io.Closer
var v io.ReadCloser | io.Reader = r
switch v.(type) {
case io.ReadCloser: fmt.Println("ReadCloser")
case io.Reader: fmt.Println("Reader")
}

私は「読者」だと思います

@jimmyfrasche他のインターフェイスのタイプスイッチから取得するのと同じように、 ReadCloserを想定します。

(また、インターフェイスタイプのみを含む合計では、通常のインターフェイスよりも多くのスペースを使用しないと予想されますが、明示的なタグを使用すると、タイプスイッチのルックアップオーバーヘッドを少し節約できると思います。)

@bcmills興味深いのは割り当てです、検討してください: //play.golang.org/p/PzmWCYex6R

@ianlancetaylorそれは、提起するのに最適なポイントです、ありがとう。 私の「素朴な実装」の提案自体が素朴すぎることを意味しますが、回避するのは難しいとは思いません。 合計型は、インターフェイス型として扱われますが、実際には型とそのメソッドセットへの直接ポインタを含む必要はありません。代わりに、必要に応じて、型を暗示する整数タグを含めることができます。 タイプ自体がnilの場合でも、そのタグはゼロ以外になる可能性があります。

与えられた:

 var x int | nil = nil

xの実行時の値はすべてゼロである必要はありません。 xのタイプをオンにするとき、または変換するとき
別のインターフェイスタイプに変換すると、タグは次のような小さなテーブルを介して間接化される可能性があります。
実際の型ポインタ。

別の可能性は、それが最初の要素である場合にのみnilタイプを許可することですが、
それは次のような構造を排除します:

var t nil | int
var u float64 | t

@jimmyfrasche他のインターフェイスのタイプスイッチから取得するのと同じように、

はい。

@bcmills興味深いのは割り当てです、検討してください: //play.golang.org/p/PzmWCYex6R

わかりません。 「タイプスイッチがReadCloserを出力するには、この[...]が有効である必要がある」のはなぜですか。
他のインターフェースタイプと同様に、合計タイプはその中にあるものの具体的な値しか格納しません。

合計に複数のインターフェイスタイプがある場合、ランタイム表現は単なるインターフェイス値です。基になる値が宣言された可能性の1つ以上を実装する必要があることがわかっているだけです。

つまり、I1とI2の両方がインターフェイスタイプであるタイプ(I1 | I2)に何かを割り当てると、入力した値がその時点でI1またはI2を実装することがわかっていたかどうかを後で判断することはできません。

io.ReadCloserであるタイプがある場合| io.Reader io.Readerでswitchまたはassertを入力したときに、sumタイプへの割り当てによってインターフェイスのボックスが解除され、再ボックス化されない限り、io.ReadCloserではないことを確認できません。

io.Readerを使用している場合は、逆になります。 io.ReadCloserは、厳密に右から左に移動するため、io.ReadCloserを受け入れることはありません。または、実装は、合計内のすべてのインターフェイスから「最適な」インターフェイスを検索する必要がありますが、それを適切に定義することはできません。

@rogpeppe提案では、実装における最適化の可能性とゼロ値の微妙さを無視して、手動で作成されたインターフェイスタイプ(関連するメソッドの共通部分を含む)よりも合計タイプを使用する主な利点は、タイプチェッカーがエラーを指摘できることです。実行時ではなくコンパイル時に。 2番目の利点は、型の値がより識別され、プログラムの可読性/理解に役立つ可能性があることです。 他に大きなメリットはありますか?

(私は決して提案を減らそうとはしていません。直感を正しくしようとしているだけです。特に、構文と意味の複雑さが「適度に小さい」場合は、それが何を意味するかにかかわらず、コンパイラを使用することの利点を明確に理解できます。エラーを早期にキャッチします。)

@griesemerはい、その通りです。

特にチャネルやネットワークを介してメッセージを伝達する場合、利用可能な可能性を正確に表現するタイプを持つことができると、読みやすさと正確さが役立つと思います。 現在、インターフェイスタイプにエクスポートされていないメソッドを含めることでこれを行うのは中途半端な試みですが、これはa)埋め込みによって回避可能であり、b)エクスポートされていないメソッドが非表示になっているため、考えられるすべてのタイプを確認するのは困難です。

@jimmyfrasche

io.ReadCloserであるタイプがある場合| io.Reader io.Readerでswitchまたはassertを入力したときに、sumタイプへの割り当てによってインターフェイスのボックスが解除され、再ボックス化されない限り、io.ReadCloserではないことを確認できません。

そのタイプを使用している場合は、常にio.Reader(または、任意のio.Readerもnilになる可能性があるためnil)であることがわかります。 2つの選択肢は排他的ではありません。提案されている合計型は、「排他的論理和」ではなく「包括的論理和」です。

io.Readerを使用している場合は、逆になります。 io.ReadCloserは、厳密に右から左に移動するため、io.ReadCloserを受け入れることはありません。または、実装は、合計内のすべてのインターフェイスから「最適な」インターフェイスを検索する必要がありますが、それを適切に定義することはできません。

「反対方向に進む」とは、そのタイプに割り当てることを意味する場合、提案は次のように述べています。

「合計タイプに値を割り当てるとき、値がより多くに収まるかどうか
可能なタイプの1つよりも、最初のタイプが選択されます。」

この場合、io.ReadCloserはio.Readerとio.ReadCloserの両方に収まるため、io.Readerを選択しますが、実際には後で判断する方法はありません。 タイプio.Readerとタイプio.Readerの間に検出可能な違いはありません。 io.Readerは、io.Readerを実装するすべてのインターフェイスタイプも保持できるため、io.ReadCloser。 そのため、コンパイラにこのような型を拒否させるのは良い考えかもしれないと私は思います。 たとえば、interface {}にはすでに任意の型が含まれている可能性があるため、interface {}に関連するすべての合計型を拒否する可能性があります。そのため、追加の条件によって情報が追加されることはありません。

@rogpeppeあなたの提案について私が好きなことがたくさんあります。 左から右への割り当てセマンティクスとゼロ値は、左端の型ルールのゼロ値であり、非常に明確で単純です。 非常に行きます。

私が心配しているのは、インターフェイスですでにボックス化されている値を、合計型の変数に割り当てることです。

とりあえず、前の例を使用して、RCはio.ReadCloserに割り当てることができる構造体であるとしましょう。

あなたがこれをするなら

var v io.ReadCloser | io.Reader = RC{}

結果は明白で明確です。

ただし、これを行うと

var r io.Reader = RC{}
var v io.ReadCloser | io.Reader = r

唯一の賢明なことは、vストアrをio.Readerとして使用することですが、スイッチをオンにすると、io.Readerケースを押したときに、実際には存在しないことを確認できません。 io.ReadCloser。 あなたはこのようなものを持っている必要があるでしょう:

switch v := v.(type) {
case io.ReadCloser: useReadCloser(v)
case io.Reader:
  if rc, ok := v.(io.ReadCloser); ok {
    useReadCloser(rc)
  } else {
    useReader(v)
  }
}

さて、io.ReadCloser <:io.Readerという感覚があり、あなたが提案したように、それらを許可しないこともできますが、問題はより根本的であり、Go†の任意の合計タイプの提案に当てはまると思います。

それぞれメソッドA()、B()、およびC()を持つ3つのインターフェイスA、B、およびCと、3つのメソッドすべてを持つ構造体ABCがあるとします。 A、B、およびCは互いに素であるため、A | B | Cとその順列はすべて有効なタイプです。 しかし、あなたはまだ次のようなケースがあります

var c C = ABC{}
var v A | B | C = c

それを再配置する方法はたくさんありますが、インターフェイスが関係しているときにvが何であるかについて意味のある保証はまだ得られません。 合計を箱から出した後、順序が重要な場合はインターフェースの箱を開ける必要があります。

たぶん、制限は、どの被加数もインターフェースになることができないということである必要がありますか?

私が考えることができる他の唯一の解決策は、sum型変数へのインターフェースの割り当てを禁止することですが、それ自体がより厳しいように思われます。

†それは曖昧さを解消するために合計の型の型構築子を必要としません(多分型の値を構築するためにJust vと言わなければならないHaskellのように)—しかし私はそれをまったく支持していません。

@jimmyfrasche注文された開開のユースケースは実際に重要ですか? それは私には明らかではありませんし、それが重要な場合のためには、明示的なボックス構造体を回避するのは簡単です:

type ReadCloser struct {  io.ReadCloser }
type Reader struct { io.Reader }

var v ReadCloser | Reader = Reader{r}

@bcmills結果が明白で

提供する明示的なボックス構造体の例は、合計タイプでインターフェースを禁止しても、合計タイプの能力がまったく制限されないことを示しています。 脚注で述べたように、曖昧性解消のための型構築子を効果的に作成しています。 確かに、それは少し面倒で余分なステップですが、それは単純であり、言語構造を可能な限り直交させるというGoの哲学と非常に一致しているように感じます。

合計タイプで必要なすべての保証

それはあなたが期待する保証に依存します。 合計タイプは次のようになることを期待していると思います
厳密にタグ付けされた値なので、任意のタイプA | B | Cが与えられると、静的なものが正確にわかります
割り当てたタイプ。 私はそれをコンクリートの単一の値に対する型制限として見ています
type-制限は、値が(少なくとも)A、B、およびCのいずれかと型互換であるということです。
結局、それはの値を持つ単なるインターフェースです。

つまり、割り当てと互換性があるために値を合計型に割り当てることができる場合
合計タイプのメンバーの1つでは、それらのメンバーのどれがされたかは記録されません。
「選択済み」-値自体を記録するだけです。 io.Readerを割り当てる場合と同じ
インターフェイス{}に対しては、静的io.Readerタイプが失われ、値自体が含まれるだけです。
これはio.Readerと互換性がありますが、それが発生する他のインターフェイスタイプとも互換性があります
実装する。

あなたの例では:

var c C = ABC{}
var v A | B | C = c

A、B、およびCのいずれかに対するvの型アサーションは成功します。 それは私には理にかなっているようです。

@rogpeppeこれらのセマンティクスは、私が想像していたよりも理にかなっています。 インターフェースと合計がうまく混ざり合っているとはまだ完全には確信していませんが、そうでないかどうかはもうわかりません。 進捗!

type U I | *Tがあるとします。ここで、 Iはインターフェイスタイプで、 *TはIを実装するタイプです。

与えられた

var i I = new(T)
var u U = i

uの動的タイプは*Tであり、

var u U = new(T)

タイプアサーションを使用して、その*T Iとしてアクセスできます。 あれは正しいですか?

つまり、有効なインターフェイス値から合計への割り当てでは、合計で最初に一致するタイプを検索する必要があります。

また、のようなものとは多少異なるだろうvar v uint8 | int32 | int64 = iだけで、常にこれらの3つのタイプのいずれかで行く、私は想像することになるiあってもであるiいたint64 uint8収まる

進捗!

わーい!

タイプアサーションを持つIとしてその* Tにアクセスできます。 あれは正しいですか?

はい。

つまり、有効なインターフェイス値から合計への割り当てでは、合計で最初に一致するタイプを検索する必要があります。

うん、提案が言うように(もちろん、コンパイラはどちらを選択するかを静的に知っているので、実行時に検索する必要はありません)。

また、var v uint8 |のようなものとは多少異なります。 int32 | int64 = iは、私がuint8に収まるint64であったとしても、これら3つのタイプのいずれかと常に一致すると思います。

はい。iが定数でない限り、これらの選択肢の1つにのみ割り当てることができるためです。

はい。iが定数でない限り、これらの選択肢の1つにのみ割り当てることができるためです。

名前のない型を名前の付いた型に割り当てることを許可する規則があるため、それは完全には真実ではありません。 しかし、それがあまり違いを生むとは思いません。 ルールは同じままです。

したがって、前回の投稿のI | *Tタイプは、タイプIと実質的に同じであり、 io.ReadCloser | io.Readerは、実質的にio.Readerと同じタイプですか?

それは正しい。 どちらの型も、コンパイラが合計型を拒否するという私の提案されたルールでカバーされます。この場合、1つの型は、別の型によって実装されるインターフェイスです。 同じまたは類似のルールで、 int|intような重複タイプの合計タイプをカバーできます。

1つの考え: int|byteがbyte|intと同じではないことはおそらく直感的ではありませんが、実際にはおそらく問題ありません。

つまり、有効なインターフェイス値から合計への割り当てでは、合計で最初に一致するタイプを検索する必要があります。

うん、提案が言うように(もちろん、コンパイラはどちらを選択するかを静的に知っているので、実行時に検索する必要はありません)。

私はこれをフォローしていません。 私がそれを読む方法(意図したものとは異なる可能性があります)には、IとT実装のユニオンUに対処する方法が少なくとも2つあります-I。

1a) U u = t割り当て時に、タグはTに設定されます。タグがTであるため、後で選択するとTになります。
1b) U u = i (iは実際にはT)の割り当て時に、タグはIに設定されます。タグがIであるため、後で選択するとTになりますが、2番目のチェック(TはIとTを実装するため実行されます) Uのメンバーです)Tを発見します。

2a)1aのように
2b) U u = i (iは実際にはT)の割り当て時に、生成されたコードは値(i)をチェックして、実際にTであるかどうかを確認します。これは、TがIを実装し、TもUのメンバーであるためです。つまり、タグはTに設定されます。後で選択すると、直接Tになります。

T、V、WがすべてIとU = *T | *V | *W | I実装している場合、割り当てU u = iは(最大)3つのタイプのテストが必要です。

しかし、インターフェースとポインターは、共用体型の元々のユースケースではありませんでしたか?

「素敵な」実装がビットバンギングを実行する特定の種類のハッカーを想像できます。たとえば、すべての指示対象が4バイトに整列されている4つ以下のポインター型の和集合がある場合は、タグを下の2に格納します。値のビット。 これは、組合のメンバーのアドレスを取得するのは良くないことを意味します(そのアドレスは、タグを調整せずに「古い」タイプを復元するために使用できるため、とにかく良くありません)。

または、50ビットのアドレス空間があり、NaNを自由に使用できる場合は、整数、ポインター、および倍精度浮動小数点数をすべて64ビットの和集合にスラップすることができます。

どちらのサブ提案も大まかなものであり、どちらも熱狂的な支持者の数が少ない(?)と確信しています。

これは、組合員の住所を取るのは良くないことを意味します

正しい。 しかし、タイプアサーションの結果は、とにかく今日は対処可能ではないと思いますか?

U u = i(iは実際にはT)の割り当て時に、タグはIに設定されます。

これが核心だと思います-タグIはありません。

ランタイム表現をしばらく無視し、合計型をインターフェースと見なします。 他のインターフェースと同様に、動的型(それに格納されている型)があります。 あなたが参照する「タグ」はまさにその動的タイプです。

あなたが提案するように(そして私は提案の最後の段落で暗示しようとしました)、実行時型へのポインタを使用するよりも効率的な方法で型タグを格納する方法があるかもしれませんが、最終的には常に動的をエンコードするだけです合計タイプ値のタイプ。作成時にどの選択肢が「選択」されたかではありません。

しかし、インターフェースとポインターは、共用体型の元々のユースケースではありませんでしたか?

そうではありませんでしたが、私の見解では、提案は他の言語機能に関して可能な限り直交する必要があります。

@ dr2chaseこれまでの私の理解では、sum型の定義にインターフェイス型が含まれている場合、実行時の実装はインターフェイス(メソッドセットの共通部分を含む)と同じですが、許容される型に関するコンパイル時の不変条件は変わりません。強制。

合計型に具象型のみが含まれ、Cスタイルの識別された共用体のように実装された場合でも、合計型の値は、取得後に別の型(およびサイズ)になる可能性があるため、アドレス指定できません。住所・アドレス。 ただし、入力された合計値自体のアドレスを取得することもできます。

合計型がこのように動作することが望ましいですか? 選択/アサートされたタイプは、値がユニオンに割り当てられたときにプログラマーが言った/暗示したものと同じであると簡単に宣言できます。 そうしないと、int8対int16対int32などに関して興味深い場所に導かれる可能性があります。または、たとえば、 int8 | uint8 。

合計型がこのように動作することが望ましいですか?

それは判断の問題です。 私たちはすでに言語のインターフェースの概念を持っているので、そうだと思います-静的型と動的型の両方の値。 提案されている合計型は、場合によってはインターフェイス型を指定するためのより正確な方法を提供するだけです。 また、合計型は他の型に制限されることなく機能できることも意味します。 そうしないと、インターフェースタイプを除外する必要があり、その場合、機能は完全に直交しません。

そうしないと、int8対int16対int32などに関して興味深い場所に導かれる可能性があります。または、たとえば、int8 | uint8。

ここであなたの懸念は何ですか?

関数タイプをマップのキータイプとして使用することはできません。 それが同等だと言っているのではなく、他の種類の型を制限する型の前例があるというだけです。 インターフェースを許可することはまだ可能ですが、まだ販売されていません。

他の方法では作成できないインターフェイスを含む合計型を使用して、どのような種類のプログラムを作成できますか?

対案。

共用体型は、0個以上の型をリストする型です。

union {
  T0
  T1
  //...
  Tn
}

ユニオンにリストされているすべてのタイプ(T0、T1、...、Tn)は異なっている必要があり、インターフェイスタイプにすることはできません。

メソッドは、通常のルールによって、定義された(名前の付いた)共用体タイプで宣言できます。 リストされたタイプからプロモートされるメソッドはありません。

共用体型の埋め込みはありません。 ある共用体型を別の共用体型にリストすることは、他の有効な型をリストすることと同じです。 ただし、 type S struct { S }が無効であるのと同じ理由で、ユニオンは自身の型を再帰的にリストすることはできません。

ユニオンは構造体に埋め込むことができます。

共用体型の値は動的型であり、リストされた型の1つに限定され、動的型の値(格納された値と呼ばれます)です。 リストされているタイプの1つは、常に動的タイプです。

空の共用体のゼロ値は一意です。 空でないユニオンのゼロ値は、ユニオンにリストされている最初のタイプのゼロ値です。

共用体型の値U U{}は、ゼロ値としてUに1つ以上の型があり、 vがリストされた型の1つの値である場合、 T 、 U{v}はv格納する和集合値を作成しますT 。 vがUリストされていないタイプであり、リストされている複数のタイプに割り当てることができる場合は、曖昧さを解消するために明示的な変換が必要です。

共用体型Uの値は、 Uの型のセットがサブセットである場合、 V(U{})ように別の共用体型V変換できます。 Vの型のセット。 つまり、順序を無視すると、 UはVと同じタイプである必要があり、 UはVなくVタイプを持つことはできません。 Uないタイプを持つことができます。

共用体タイプ間の割り当て可能性は、多くても1つの共用体タイプが定義(名前付き)されている限り、兌換性として定義されます。

共用体型Uのリストされた型の1つTの値を、共用体型U変数に割り当てることができます。 これにより、動的タイプがTに設定され、値が格納されます。 割り当て互換性のある値は上記のように機能します。

リストされているすべてのタイプが等式演算子をサポートしている場合:

  • 等式演算子は、同じ共用体タイプの2つの値で使用できます。 動的型が異なる場合、共用体型の2つの値が等しくなることはありません。
  • そのユニオンの値は、リストされているタイプのいずれかの値と比較できます。 和集合の動的型が他のオペランドの型でない場合、格納されている値に関係なく、 ==はfalseであり、 !=はtrueです。 割り当て互換性のある値は上記のように機能します。
  • ユニオンはマップキーとして使用できます

共用体型の値では、他の演算子はサポートされていません。

アサートされた型が動的型である場合、リストされた型の1つの共用体型に対する型アサーションが保持されます。

動的型がそのインターフェイスを実装している場合、インターフェイス型の共用体型に対する型アサーションが保持されます。 (特に、リストされているすべてのタイプがこのインターフェースを実装している場合、アサーションは常に保持されます)。

タイプスイッチは、リストされているすべてのタイプを含めて網羅的であるか、デフォルトのケースを含む必要があります。

タイプアサーションとタイプスイッチは、保存された値のコピーを返します。

パッケージreflectには、反映された共用体値の動的型と格納された値を取得する方法と、反映された共用体型のリストされた型を取得する方法が必要です。

ノート:

union{...}構文は、このスレッドの合計型の提案と区別するために、主にGo文法の優れたプロパティを保持するために、そして偶然にもこれが識別された共用体であることを強調するために部分的に選択されました。 結果として、これにより、 union{}やunion{ int }などのやや奇妙な結合が可能になります。 1つ目は、多くの意味でstruct{}と同等であるため(定義上は異なる型ですが)、別の空の型を追加する以外は言語に追加されません。 2番目はおそらくもっと便利です。 たとえば、 type Id union { int }はtype Id struct { int } type Id union { int }と非常によく似ていますが、ユニオンバージョンでは、 idValue.intを指定しなくても直接割り当てできるため、組み込み型のように見えます。

割り当て互換タイプを処理するときに必要なあいまいさを解消する変換は少し厳しいですが、ダウンストリームコードが準備されていないあいまいさを導入するためにユニオンが更新されると、エラーが発生します。

埋め込みの欠如は、ユニオンでメソッドを許可し、タイプスイッチで徹底的なマッチングを必要とする結果です。

リストされたタイプのメソッドの有効な共通部分を取得するのではなく、ユニオン自体でメソッドを許可することで、誤って不要なメソッドを取得することを回避できます。 格納された値を共通のインターフェースにアサートするタイプにより、昇格が必要な場合に単純で明示的なラッパーメソッドが可能になります。 たとえば、共用体型U 、リストされているすべての型がfmt.Stringer実装します。

func (u U) String() string {
  return u.(fmt.Stringer).String()
}

リンクされたredditスレッドで、rscは次のように述べています。

sum {X;の値がゼロの場合は、奇妙になります。 Y}はsum {Y;のそれとは異なります。 NS }。 これは、合計が通常どのように機能するかではありません。

どんな提案にも当てはまるので、ずっと考えていました。

これはバグではありません。機能です。

検討

type (
  Undefined = struct{}
  UndefinedOrInt union { Undefined; int }
)

対。

type (
  Illegal = struct{}
  IntOrIllegal union { int; Illegal }
)

UndefinedOrIntは、デフォルトではまだ定義されていないことを示していますが、定義されている場合は、 int値になります。 これは*int類似しており、合計タイプ(1 + int)をGo nowで表す必要があり、ゼロ値も類似しています。

一方、 IntOrIllegalは、デフォルトではint 0であると言いますが、ある時点で違法としてマークされる可能性があります。 これはまだ*int似ていますが、ゼロ値は、デフォルトでnew(int)強制するなど、意図をより表現します。

これは、構造体のブールフィールドを負の値で表現できるようなものなので、デフォルトとしてゼロ値が必要です。

合計の両方のゼロ値は、それ自体が有用で意味があり、プログラマーは状況に最も適したものを選択できます。

合計が曜日の列挙型(各日は定義されたstruct{} )の場合、最初にリストされている方が週の最初の日であり、 iotaスタイルの列挙型でも同じです。

また、ゼロ値の概念を持つ合計型または識別/タグ付き共用体を持つ言語を私は知りません。 Cが最も近いですが、ゼロ値は初期化されていないメモリであり、従うことはほとんどありません。 Javaのデフォルトはnullだと思いますが、それはすべてが参照であるためです。 私が知っている他のすべての言語には、被加数の必須の型構築子があるため、実際にはゼロ値の概念はありません。 そのような言語はありますか? それは何をするためのものか?

「合計」と「和集合」の数学的概念との違いが問題である場合、いつでもそれらを別の何か(たとえば「バリアント」)と呼ぶことができます。

名前の場合:Unionはc / c ++の純粋主義者を混乱させます。 Variantは、主にCOBRAおよびCOMプログラマーによく知られていますが、関数型言語では、識別された共用体が好まれているようです。 セットは動詞と名詞です。 キーワード_pick_が好きです。 Limboは_pick_を使用しました。 これは短く、有限の型のセットから選択する型の意図を説明しています。

名前/構文はほとんど関係ありません。 ピックは大丈夫でしょう。

このスレッドのどちらの提案も、設定された理論的定義に適合します。

型理論の合計が通勤するため、ゼロ値に特別な最初の型は無関係です。したがって、順序は無関係です(A + B = B + A)。 私の提案はその特性を維持していますが、製品タイプも理論的には通勤しており、ほとんどの言語(Goを含む)では実際には異なると見なされているため、おそらく必須ではありません。

@jimmyfrasche

私は個人的に、「ピック」メンバーとしてインターフェースを許可しないことは非常に大きな欠点であると信じています。 まず、「pick」タイプの優れたユースケースの1つを完全に打ち負かします。エラーが発生すると、メンバーの1つになります。 または、ユーザーに事前にStringReaderを使用させたくない場合は、io.Readerまたは文字列のいずれかを持つピックタイプを処理する必要があります。 しかし、全体として、インターフェースは単なる別のタイプであり、「pick」メンバーにタイプ制限があるべきではないと私は信じています。 その場合、ピックタイプに2つのインターフェイスメンバーがあり、一方が他方で完全に囲まれている場合、前述のように、コンパイル時エラーになるはずです。

あなたの反対の提案から私が好きなのは、メソッドがピックタイプで定義できるという事実です。 メソッドがすべてのメンバーに属する場合は多くないと思うので、メンバーのメソッドの断面を提供する必要はないと思います(とにかくそのためのインターフェイスがあります)。 そして、徹底的なスイッチ+デフォルトのケースは非常に良い考えです。

@rogpeppe @jimmyfrascheあなたの提案に私が見ない何かが、私たちがこれをしなければならない理由です。 新しい種類のタイプを追加することには明らかな欠点があります。それは、Goに傾倒するすべての人が学ばなければならない新しい概念です。 補償の利点は何ですか? 特に、新しい種類のタイプは、インターフェイスタイプからは得られないものを私たちに与えますか?

@ianlancetaylor Robertはここでそれをうまく要約しました: https :

@ianlancetaylor
結局のところ、コードが読みやすくなり、それがGoの主要なディレクティブです。 json.Tokenについて考えてみましょう。現在、インターフェース{}として定義されていますが、ドキュメントには、実際には特定の数のタイプの1つにしかできないと記載されています。 一方、次のように書かれている場合

type Token Delim | bool | float64 | Number | string | nil

ユーザーはすべての可能性をすぐに見ることができ、ツールは完全なスイッチを自動的に作成することができます。 さらに、コンパイラーは、予期しない型をそこに貼り付けることも防ぎます。

結局のところ、コードが読みやすくなり、それがGoの主要なディレクティブです。

より多くの機能は、コードを理解するためにもっと知る必要があることを意味します。 言語の平均的な知識しか持たない人にとって、その読みやすさは必然的に[新しく追加された]機能の数に反比例します。

@cznic

より多くの機能は、コードを理解するためにもっと知る必要があることを意味します。

常にではない。 「コード内の文書化が不十分または一貫性のない不変条件についてもっと知る」の代わりに「言語についてもっと知る」ことができれば、それでも正味の勝利になる可能性があります。 (つまり、グローバルな知識は、ローカルな知識の必要性に取って代わる可能性があります。)

より良いコンパイル時の型チェックが確かに唯一の利点である場合、獣医によってチェックされたコメントを導入することにより、言語を変更せずに非常に類似した利点を得ることができます。 何かのようなもの

//vet:types Delim | bool | float64 | Number | string | nil
type Token interface{}

現在、獣医のコメントはありませんので、これは完全に深刻な提案ではありません。 しかし、私は基本的な考え方に真剣に取り組んでいます。静的分析ツールで完全に実行できる唯一の利点がある場合、言語に複雑な新しい概念を追加する価値は本当にあるのでしょうか。

cmd / vetによって実行されるテストの多く、おそらくすべては、個別の静的分析ツールではなくコンパイラーによってチェックできるという意味で、言語に追加できます。 しかし、さまざまな理由から、vetをコンパイラから分離すると便利です。 なぜこの概念は獣医側ではなく言語側にあるのですか?

@ianlancetaylorはコメントを再確認しました: https :

@ianlancetaylor変更が正当化されるかどうかに関しては、私は積極的にそれを無視してきました。 要約でそれについて話すことは曖昧で、私には役に立ちません。それはすべて、私には「良いことは良いこと、悪いことは悪いこと」のように聞こえます。 タイプが実際にどのようなものになるか(制限とは何か、それが持つ意味、長所とは何か、短所は何か)を知りたかったので、言語にどのように適合するか(または適合しないか)を確認できました。 )そして、私がそれをプログラムでどのように使用するか/使用できるかについての考えを持っています。 少なくとも私の観点からは、Goで合計タイプが何を意味するのかについては良い考えがあると思います。 私は彼らがそれだけの価値があると完全に確信しているわけではありませんが(私が本当に悪いことを望んでいるとしても)、今では私が推論できる明確に定義されたプロパティで分析するための確かなものがあります。 それ自体は実際には答えではないことを私は知っていますが、少なくとも私がこれに取り組んでいるところです。

より良いコンパイル時の型チェックが確かに唯一の利点である場合、獣医によってチェックされたコメントを導入することにより、言語を変更せずに非常に類似した利点を得ることができます。

これは、新しいことを学ぶ必要があるという批判に対して依然として脆弱です。 コードをデバッグ/理解/使用するためにこれらの魔法の獣医のコメントについて学ぶ必要がある場合、それをGo言語の予算に割り当てるか、技術的にGo言語ではない予算に割り当てるかに関係なく、それは精神的な負担です。 どちらかといえば、私が言語を学んだと思ったときに私がそれらを学ぶ必要があることを知らなかったので、魔法のコメントはより高価です。

@cznic
同意しません。 あなたの現在の仮定では、人がチャネルが何であるか、あるいは機能が何であるかさえ理解するだろうと確信することはできません。 しかし、これらのものは言語に存在します。 また、新機能は、言語を難しくすることを自動的に意味するわけではありません。 この場合、ブラックボックスインターフェイス{}タイプを使用するのではなく、タイプが何であるかが読者にすぐに明らかになるため、実際には理解しやすくなると私は主張します。

@ianlancetaylor
個人的には、この機能はコードを読みやすくし、推論することと関係があると思います。 コンパイル時の安全性は非常に優れた機能ですが、主要な機能ではありません。 型署名がすぐにわかりやすくなるだけでなく、その後の使用法も理解しやすく、書きやすくなります。 予期しないタイプを受け取った場合、人々はもはやパニックに訴える必要はありません-それは標準ライブラリでも現在の動作ですが、未知のものに邪魔されることなく、使用法について考えるのが簡単になります。 そして、コメントや他のツール(ファーストパーティであっても)に頼るのは良い考えではないと思います。なぜなら、そのようなコメントよりもクリーンな構文の方が読みやすいからです。 また、コメントは構造がなく、混乱しやすくなっています。

@ianlancetaylor

なぜこの概念は獣医側ではなく言語側にあるのですか?

同じ質問をチューリング完全コア以外の機能に適用することもできますが、間違いなく、Goを「チューリング陥穷」にしたくないのです。 一方、我々は一般的な「拡張子」構文にオフ実際の言語の重要なサブセットを押し込んだている言語の例を持っています。 (たとえば、Rust、C ++、およびGNU Cの「属性」。)

機能をコア言語ではなく拡張機能または属性に配置する主な理由は、新機能を認識しないツールとの互換性など、構文の互換性を維持するためです。 (「ツールとの互換性」が実際に機能するかどうかは、機能が実際に何をするかに大きく依存します。)

Goのコンテキストでは、機能をvetに配置する主な理由は、言語自体に適用した場合にGo1の互換性を維持しない変更を実装することであるように思われます。 ここではそれを問題とは考えていません。

機能をvetに入れない理由のひとつは、コンパイル中にそれらを伝播する必要がある場合です。 たとえば、私が書く場合:

switch x := somepkg.SomeFunc().(type) {
…
}

パッケージの境界を越えて、合計に含まれないタイプに対して適切な警告が表示されますか? vetが他動詞の深い分析を実行できることは私には明らかではないので、おそらくそれがコア言語に入る必要がある理由です。

@ dr2chase一般的に、もちろんあなたは正しいですが、この特定の例については正しいですか? 魔法のコメントが何を意味するのかを知らなくても、コードは完全に理解できます。 魔法のコメントは、コードの動作をまったく変更しません。 獣医からのエラーメッセージは明確でなければなりません。

@bcmills

なぜこの概念は獣医側ではなく言語側にあるのですか?

同じ質問をチューリング完全コア以外の機能に適用できます。

同意しません。 議論中の機能がコンパイルされたコードに影響を与える場合、それを支持する自動引数があります。 この場合、この機能はコンパイルされたコードに影響を与えないようです。

(そして、はい、vetはインポートされたパッケージのソースを解析できます。)

私は獣医についての私の議論が決定的であると主張しようとはしていません。 しかし、すべての言語の変更は否定的な立場から始まります。単純な言語が非常に望ましく、このような重要な新機能により、必然的に言語がより複雑になります。 言語の変更を支持する強力な議論が必要です。 そして、私の観点からは、それらの強力な議論はまだ現れていません。 結局のところ、私たちはこの問題について長い間考えてきました、そしてそれはFAQ(https://golang.org/doc/faq#variant_types)です。

@ianlancetaylor

この場合、この機能はコンパイルされたコードに影響を与えないようです。

それは具体的な詳細に依存すると思いますか? 上記の@jimmyfrasche (https://github.com/golang/go/issues/19412#issuecomment-289319916)が確かに行う「合計のゼロ値は、最初のタイプのゼロ値」の動作です。

@urandom明示的な型コンストラクターなしでインターフェイス型と共用体型が混在しない理由について長い説明を書いていましたが、それを行うためのある種の賢明な方法があることに気づきました。

私の反対提案に対する迅速で汚い反対提案。 (明示的に言及されていないものはすべて、私の以前の提案と同じです)。 ある提案が他の提案よりも優れているかどうかはわかりませんが、これはインターフェースを許可し、より明確です。

ユニオンには、以降「タグ名」と呼ばれる明示的な「フィールド名」があります。

union { //or whatever
  None, invalid struct{} //None is zero value
  Good, Bad int
  Err error //okay because it's explicitly named
}

まだ埋め込みはありません。 タグ名のない型があると常にエラーになります。

ユニオン値には、動的タイプではなく動的タグがあります。

リテラル値の作成: U{v}は、完全に明確な場合にのみ有効です。それ以外の場合は、 U{Tag: v}ます。

変換可能性と割り当ての互換性では、タグ名も考慮されます。

組合への割り当ては魔法ではありません。 これは常に、互換性のあるユニオン値を割り当てることを意味します。 保存された値を設定するには、目的のタグ名を明示的に使用する必要があります。 v.Good = 1は、動的タグをGoodに設定し、保存された値を1に設定します。

保存された値にアクセスするには、タイプアサーションではなくタグアサーションを使用します。

g := v.[Tag] //may panic
g, ok := v.[Tag] //no panic but could return zero-value, false

v.Tagはあいまいなため、rhsのエラーです。

タグスイッチは、ケースがユニオンのタグであることを除いて、 switch v.[type]と書かれたタイプスイッチに似ています。

タイプアサーションは、動的タグのタイプに関して保持されます。 タイプスイッチも同様に機能します。

ある共用体タイプの値a、bが与えられた場合、それらの動的タグが同じであり、格納されている値が同じである場合、a == bです。

保存された値が特定の値であるかどうかを確認するには、タグアサーションが必要です。

タグ名がエクスポートされていない場合、ユニオンを定義するパッケージでのみ設定およびアクセスできます。 これは、エクスポートされたタグとエクスポートされていないタグが混在するユニオンのタグスイッチが、デフォルトのケースなしで定義パッケージの外で網羅的になることは決してないことを意味します。 すべてのタグがエクスポートされていない場合、それはブラックボックスです。

リフレクションでは、タグ名も処理する必要があります。

e:ネストされた共用体の明確化。 与えられた

type U union {
  A union {
    A1 T1
    A2 T2
  }
  B union {
    B1 T3
    B2 T4
  }
}
var u U

uの値は動的タグAであり、格納された値は動的タグA1との匿名共用体であり、その格納された値はT1のゼロ値です。

u.B.B2 = returnsSomeT3()

uはすべて1つのメモリ位置に格納されているため、ネストされたユニオンの1つから別のユニオンに移動しても、uをゼロ値から切り替えるために必要なのはこれだけです。 しかし

v := u.[A].[A2]

2つのユニオン値でタグアサーションを実行し、複数行に分割しないと2値バージョンのタグアサーションを使用できないため、パニックになる可能性が2つあります。 この場合、ネストされたタグスイッチはよりクリーンになります。

edit2:タイプassertの明確化。

与えられた

type U union {
  Exported, unexported int
}
var u U

u.(int)ようなタイプアサーションは完全に合理的です。 定義パッケージ内では、それは常に成り立ちます。 ただし、 uが定義パッケージの外にある場合、動的タグがunexportedときにu.(int)はパニックになり、実装の詳細が漏洩するのを防ぎます。 インターフェイスタイプへのアサーションについても同様です。

@ianlancetaylorこの機能がどのように役立つかのいくつかの例を次に示します。

  1. 一部のパッケージ(たとえばgo/ast )の中心には、1つ以上の大きな合計型があります。 これらのタイプを理解せずにこれらのパッケージをナビゲートすることは困難です。 さらに紛らわしいことに、合計型がメソッドを持つインターフェース( go/ast.Node )で表される場合もあれば、空のインターフェース( go/ast.Object.Decl )で表される場合もあります。

  2. protobuf oneof機能をGoにコンパイルすると、エクスポートされていないインターフェイスタイプが生成されます。このインターフェイスタイプの唯一の目的は、oneofフィールドへの割り当てがタイプセーフであることを確認することです。 そのためには、oneofのブランチごとにタイプを生成する必要があります。 最終製品の型リテラルは、読み取りと書き込みが困難です。

    &sppb.Mutation{
               Operation: &sppb.Mutation_Delete_{
                   Delete: &sppb.Mutation_Delete{
                       Table:  m.table,
                       KeySet: keySetProto,
                   },
               },
    }
    

    一部の(すべてではありませんが)oneofは、合計型で表すことができます。

  3. 「たぶん」タイプがまさに必要なものである場合があります。 たとえば、多くのGoogle APIリソース更新操作では、リソースのフィールドのサブセットを変更できます。 Goでこれを表現する自然な方法の1つは、各フィールドに「多分」タイプを持つリソース構造体のバリアントによるものです。 たとえば、Google Cloud StorageObjectAttrsリソースは次のようになります。

    type ObjectAttrs struct {
       ContentType string
       ...
    }
    

    部分的な更新をサポートするために、パッケージは

    type ObjectAttrsToUpdate struct {
       ContentType optional.String
       ...
    }
    

    optional.Stringは次のようになります( godoc ):

    // String is either a string or nil.
    type String interface{}
    

    これは説明が難しく、タイプが安全ではありませんが、プレゼンスをエンコードしている間、 ObjectAttrsToUpdateリテラルはObjectAttrsリテラルとまったく同じように見えるため、実際には便利であることがわかります。 書けたらよかったのに

    type ObjectAttrsToUpdate struct {
       ContentType string | nil
       ...
    }
    
  4. 多くの関数(T, error) 、xorセマンティクスでT | errorと書くと、セマンティクスが明確になり、安全性が高まり、構成の機会が増えます。 (互換性の理由で)関数の戻り値を変更できない、または変更したくない場合でも、sum型は、チャネルへの書き込みなど、その値を持ち運ぶのに役立ちます。

go vetアノテーションは確かにこれらのケースの多くに役立ちますが、匿名タイプが意味をなすケースには役立ちません。 合計型があれば、たくさん見られると思います

chan *Response | error

そのタイプは、複数回書き出すのに十分短いです。

@ianlancetaylorこれはおそらく素晴らしいスタートではありませんが、これらの議論を認めて要約することは公正であると私が考えたので、Go1ですでに実行できるユニオンで実行できるすべてがここにあります。

(以下の構文/セマンティクスのタグを使用した最新の提案を使用します。また、発行されたコードは基本的に、スレッドのかなり前に投稿したCコードのようであると想定しています。)

合計型は、iota、ポインター、およびインターフェースと重複しています。

iota

これらの2つのタイプはほぼ同等です。

type Stoplight union {
  Green, Yellow, Red struct {}
}

func (s Stoplight) String() string {
  switch s.[type] {
  case Green: return "green" //etc
  }
}

と

type Stoplight int

const (
  Green Stoplight = iota
  Yellow
  Red
)

func (s Stoplight) String() string {
  switch s {
  case Green: return "green" //etc
  }
}

コンパイラーは、両方に対してまったく同じコードを出力する可能性があります。

ユニオンバージョンでは、intは非表示の実装詳細に変換されます。 iotaバージョンでは、Yellow / Redとは何かを尋ねたり、Stoplight値を-42に設定したりできますが、unionバージョンではできません。これらはすべてコンパイラエラーであり、最適化中に考慮することができる不変条件です。 同様に、黄色のライトを考慮しない(値)スイッチを作成できますが、タグスイッチを使用すると、それを明示的にするためにデフォルトのケースが必要になります。

もちろん、iotaでできることと、共用体タイプではできないことがあります。

ポインタ

これらの2つのタイプはほぼ同等です

type MaybeInt64 union {
  None struct{}
  Int64 int64
}

と

type MaybeInt64 *int64

ポインタバージョンはよりコンパクトです。 ユニオンバージョンでは、動的タグを格納するために追加のビットが必要になるため(ワードサイズになる可能性があります)、値のサイズはhttps://golang.org/pkg/database/sql/と同じになる可能性があり

ユニオンバージョンは、その意図をより明確に文書化しています。

もちろん、共用体型では実行できないポインタで実行できることがあります。

インターフェース

これらの2つのタイプはほぼ同等です

type AB union {
  A A
  B B
}

と

type AB interface {
  secret()
}
func (A) secret() {}
func (B) secret() {}

ユニオンバージョンは、埋め込みで回避することはできません。 AとBは共通のメソッドを必要としません。実際には、プリミティブ型であるか、json.Tokenの例@urandomが投稿したように完全に互いに素なメソッドセットを持っている可能性があります。

ABユニオンとABインターフェースに何を入れることができるかを確認するのは本当に簡単です。定義はドキュメントです(何かが何であるかを理解するために、go / astソースを何度も読む必要がありました)。

ABユニオンをゼロにすることはできず、その構成要素の共通部分の外側にメソッドを与えることができます(これは、構造体にインターフェイスを埋め込むことでシミュレートできますが、構造体はより繊細でエラーが発生しやすくなります)。

もちろん、共用体型では実行できないインターフェースで実行できることがあります。

概要

多分そのオーバーラップはあまりにも多くのオーバーラップです。

いずれの場合も、ユニオンバージョンの主な利点は、コンパイル時のチェックがより厳密になることです。 できないことは、できることよりも重要です。 より強力な不変条件に変換するコンパイラーの場合、コードを最適化するために使用できます。 別のことに変換するプログラマーにとっては、コンパイラーに心配させることができます。それは、あなたが間違っているかどうかを教えてくれるだけです。 インターフェイスバージョンでは、少なくとも、ドキュメントに関する重要な利点があります。

不格好なバージョンのiotaとポインターの例は、「エクスポートされていないメソッドとのインターフェース」戦略を使用して構築できます。 ただし、そのことについては、構造体はmap[string]interface{}と、関数型とメソッド値を持つ(空でない)インターフェイスを使用してシミュレートできます。 それはより難しく、安全性が低いため、誰もそうしません。

これらの機能はすべて言語に何かを追加しますが、それらの欠如は回避される可能性があります(痛々しいほど、そして抗議中)。

したがって、この基準は、Goで近似することさえできないプログラムを示すことではなく、Goでユニオンを使用しない場合よりもはるかに簡単かつクリーンに記述できるプログラムを示すことであると想定しています。 ですから、まだ示されていないのはそれです。

@jimmyfrasche

共用体型に名前付きフィールドが必要な理由はわかりません。 名前は、同じタイプの異なるフィールドを区別する場合にのみ役立ちます。 ただし、ユニオンに同じタイプの複数のフィールドを含めることはできません。これはまったく意味がないためです。 したがって、名前を持つことは冗長であり、混乱とより多くの入力につながります。

本質的に、共用体のタイプは次のようになります。

union {
    struct{}
    int
    err
}

型自体は、構造体に埋め込まれた型が識別子として使用される方法と非常によく似た、共用体に割り当てるために使用できる一意の識別子を提供します。

ただし、明示的な割り当てが機能するためには、名前のない型をメンバーとして指定して共用体型を作成することはできません。これは、構文でそのような式が許可されるためです。 例: v.struct{} = struct{}

したがって、raw struct、unions、funcsなどの型は、unionの一部になり、割り当て可能になるために、事前に名前を付ける必要があります。 これを念頭に置いて、ネストされた共用体は特別なものではありません。内部の共用体は別のメンバータイプにすぎないからです。

さて、どちらの構文が良いかわかりません。

[union|sum|pick|oneof] {
    type1
    package1.type2
    ....
}

上記はもっと似ているように見えますが、そのようなタイプには少し冗長です。

一方、 type1 | package1.type2は、通常のgoタイプのようには見えない場合がありますが、「|」を使用する利点があります。 主にORとして認識される記号。 そして、それは不可解になることなく冗長性を減らします。

@urandomは、「タグ名」がないがインターフェースを許可している場合、追加のチェックで合計がinterface{}折りたたまれます。 1つのものを入れることができるが、複数の方法でそれを取り出すことができるので、それらは合計型ではなくなります。 タグ名を使用すると、それらは合計型になり、あいまいさのないインターフェイスを保持できます。

ただし、タグ名は、インターフェイス{}の問題だけでなく多くの問題を修復します。 それらはタイプをはるかに魔法のように少なくし、区別するためだけにタイプの束を発明する必要なしにすべてを華やかに明示的にします。 ご指摘のとおり、明示的な割り当てと型リテラルを使用できます。

1つのタイプに複数のタグを付けることができるのが特徴です。 連続して発生した成功または失敗の数を測定するタイプを検討してください(1回の成功でN回の失敗がキャンセルされ、その逆も同様です)。

type Counter union {
  Successes, Failures uint 
}

必要なタグ名なし

type (
  Success uint
  Failures uint
  Counter Successes | Failures
)

そして割り当ては次のようになりますc = Successes(1)の代わりにc.Successes = 1 。 あなたはあまり得ません。

別の例は、ローカルまたはリモートの障害を表すタイプです。 タグ名を使用すると、これをモデル化するのは簡単です。

type Failure union {
  Local, Remote error
}

エラーの摂理は、実際のエラーが何であるかに関係なく、タグ名で指定できます。 タグ名がないと、合計で直接インターフェースを許可する場合でも、リモートでも同じtype Local { error }必要になります。

タグ名は、ユニオン内でローカルにエイリアスタイプも名前付きタイプも作成しないようなものです。 同じタイプの複数の「タグ」を持つことは、私の提案に固有のものではありません。それは、(私が知っている)すべての関数型言語が行うことです。

エクスポートされたタイプに対してエクスポートされていないタグを作成する機能、およびその逆の機能も興味深い工夫です。

また、タグとタイプのアサーションを個別に設定すると、1行のラッパーを使用して共有メソッドをユニオンにプロモートできるなど、いくつかの興味深いコードが可能になります。

それが引き起こすよりも多くの問題を解決し、すべてをよりうまく組み合わせるように思われます。 正直なところ、それを書いたときはよくわかりませんでしたが、合計をGoに統合する際のすべての問題を解決する唯一の方法であるとますます確信するようになりました。

それをいくらか拡張するために、私にとってのやる気を起こさせる例はio.Reader | io.ReadCloser 。 タグのないインターフェースを許可します。これはio.Readerと同じタイプです。

ReadCloserを挿入し、リーダーとして引き出すことができます。 あなたはAを失います| Bは、合計タイプのAまたはBプロパティを意味します。

あなたは時々取り扱いについて特定する必要がある場合はio.ReadCloserとio.Readerラッパー構造体@bcmillsが指摘したように、作成する必要がありますtype Reader struct { io.Reader }などと種類があること持っていますReader | ReadCloser 。

合計を互いに素なメソッドセットを持つインターフェイスに制限した場合でも、1つのタイプで複数のインターフェイスを実装できるため、この問題が発生します。 合計タイプの明示性が失われます。「AまたはB」ではありません。「AまたはB、または場合によってはどちらか好きな方」です。

さらに悪いことに、これらのタイプが他のパッケージからのものである場合、AがBと同じように扱われないようにプログラムを非常に注意深く構築したとしても、更新後に突然異なる動作をする可能性があります。

もともと私は問題を解決するためにインターフェースを禁止することを検討しました。 誰もそれに満足していませんでした! しかし、それはまた、 a = bような問題を取り除くことはできませんでした。これは、aとbのタイプによって異なることを意味します。 また、タイプの割り当て可能性が機能するときに、ピックでどのタイプを選択するかについて多くのルールが必要でした。 それはたくさんの魔法です。

タグを追加すると、それはすべてなくなります。

union { R io.Reader | RC io.ReadCloser }を使用すると、それが理にかなっている場合は、このReadCloserをリーダーと見なしてほしいと明示的に言うことができます。 ラッパータイプは必要ありません。 それは定義に暗黙のうちに含まれています。 タグのタイプに関係なく、どちらかのタグです。

欠点は、他の場所からio.Readerを取得した場合、chan receiveまたはfunc呼び出しを言うと、それがio.ReadCloserである可能性があり、ioでassertと入力する必要がある適切なタグに割り当てる必要があることです。 ReadCloserとテスト。 しかし、それによってプログラムの意図がはるかに明確になります。つまり、正確に言うと、コードにあります。

また、タグアサーションはタイプアサーションとは異なるため、本当に気にせず、io.Readerが必要な場合は、タグに関係なく、タイプアサーションを使用してそれを引き出すことができます。

これは、おもちゃの例をユニオン/合計などなしでGoに音訳するためのベストエフォートです。 これはおそらく最良の例ではありませんが、これがどのように見えるかを確認するために使用したものです。

これは、より運用的な方法でセマンティクスを示します。これは、提案の簡潔な箇条書きよりも理解しやすいでしょう。

文字変換にはかなりの定型文があるので、私は通常、繰り返しについてのメモを付けて、いくつかのメソッドの最初のインスタンスのみを記述しました。

組合提案と一緒に行く:

type fail union { //zero value: (Local, nil)
  Local, Remote error
}

func (f fail) Error() string {
  //Could panic if local/remote nil, but assuming
  //it will be constructed purposefully
  return f.(error).Error()
}

type U union { //zero value: (A, "")
  A, B, C string
  D, E    int
  F       fail
}

//in a different package

func create() pkg.U {
  return pkg.U{D: 7}
}

func process(u pkg.U) {
  switch u := u.[type] {
  case A:
    handleA(u) //undefined here, just doing something with unboxed value
  case B:
    handleB(u)
  case C:
    handleC(u)
  case D:
    handleD(u)
  case E:
    handleE(u)
  case F:
    switch u := u.[type] {
    case Local:
      log.Fatal(u)
    case Remote:
      log.Printf("remote error %s", u)
      retry()
    } 
  }
}

現在のGoに音訳:

(音訳と上記の違いについての注記が含まれています)

const ( //simulates tags, namespaced so other packages can see them without overlap
  Fail_Local = iota
  Fail_Remote
)

//since there are only two tags with a single type this can
//be represented precisely and safely
//the error method on the full version of fail can be
//put more succinctly with type embedding in this case

type fail struct { //zero value (Fail_Local, nil) :)
  remote bool
  error
}

// e, ok := f.[Local]
func (f *fail) TagAssertLocal2() (error, bool) { //same for TagAssertRemote2
  if !f.remote {
    return nil, false
  }
  return f.error, true
}

// e := f.[Local]
func (f *fail) TagAssertLocal() error { //same for TagAssertRemote
  if !f.remote {
    panic("invalid tag assert")
  }
  return f.error
}

// f.Local = err
func (f *fail) SetLocal(err error) { //same for SetRemote
  f.remote = false
  f.error = err
}

// simulate tag switch
func (f *fail) TagSwitch() int {
  if f.remote {
    return Fail_Remote
  }
  return Fail_Local
}

// f.(someType) needs to be written as f.TypeAssert().(someType)
func (f *fail) TypeAssert() interface{} {
  return f.error
}

const (
  U_A = iota
  U_B
  // ...
  U_F
)

type U struct { //zero value (U_A, "", 0, fail{}) :(
  kind int //more than two types, need an int
  s string //these would all occupy the same space
  i int
  f fail
}

//s, ok := u.[A]
func (u *U) TagAssertA2() (string, bool) { //similar for B, etc.
  if u.kind == U_A {
    return u.s, true
  }
  return "", false
}

//s := u.[A]
func (u *U) TagAssertA() string { //similar for B, etc.
  if u.kind != U_A {
    panic("invalid tag assert")
  }
  return u.s
}

// u.A = s
func (u *U) SetA(s string) { //similar for B, etc.
  //if there were any pointers or reference types
  //in the union, they'd have to be nil'd out here,
  //since the space isn't shared
  u.kind = U_A
  u.s = s
}

// special case of u.F.Local = err
func (u *U) SetF_Local(err error) { //same for SetF_Remote
  u.kind = U_F
  u.f.SetLocal(err)
}

func (u *U) TagSwitch() int {
  return u.kind
}

func (u *U) TypeAssert() interface{} {
  switch u.kind {
  case U_A, U_B, U_C:
    return u.s
  case U_D, U_E:
    return u.i
  }
  return u.f
}

//in a different package

func create() pkg.U {
  var u pkg.U
  u.SetD(7)
  return u
}

func process(u pkg.U) {
  switch u.TagSwitch() {
  case U_A:
    handleA(u.TagAssertA())
  case U_B:
    handleB(u.TagAssertB())
  case U_C:
    handleC(u.TagAssertC())
  case U_D:
    handleD(u.TagAssertD())
  case U_E:
    handleE(u.TagAssertE())
  case U_F:
    switch u := u.TagAssertF(); u.TagSwitch() {
    case Fail_Local:
      log.Fatal(u.TagAssertLocal())
    case Fail_Remote:
      log.Printf("remote error %s", u.TagAssertRemote())
    }
  }
}

@jimmyfrasche

ユニオンには同じタイプのタグが含まれているため、次の構文の方が適しているとは限りません。

func process(u pkg.U) {
  switch v := u {
  case A:
    handleA(v) //undefined here, just doing something with unboxed value
  case B:
    handleB(v)
  case C:
    handleC(v)
  case D:
    handleD(v)
  case E:
    handleE(v)
  case F:
    switch w := v {
    case Local:
      log.Fatal(w)
    case Remote:
      log.Printf("remote error %s", w)
      retry()
    } 
  }
}

私の見方では、スイッチと一緒に使用すると、共用体はintやstringなどの型と非常によく似ています。 主な違いは、前者のタイプとは対照的に、それに割り当てることができるのは有限の「値」のみであり、スイッチ自体が網羅的であるということです。 したがって、この場合、特別な構文の必要性は実際にはわかりません。これにより、開発者の精神的な作業が軽減されます。

また、この提案の下では、そのようなコードは有効でしょうか?

type Foo union {
    // Completely different types, no ambiguity
    A string
    B int
}

func Bar(f Foo) {
    switch v := f {
        ....
    }
}

....

func main() {
    // No need for Bar(Foo{A: "hello world"})
    Bar("hello world")
    Bar(1)
}

@urandom可能な限り、既存のGo構文との類似性を使用してセマンティクスを反映する構文を選択しました。

あなたがすることができるインターフェースタイプで

var i someInterface = someValue //where someValue implements someInterface.
var j someInterface = i //this assignment is different from the last one.

契約が満たされている限り、 someValueのタイプは関係ないので、これは問題なく明確です。

ユニオンにタグ†を導入すると、あいまいになることが

特にコードの変更によってその特殊なケースが簡単に無効になり、とにかく戻ってすべてのコードを更新する必要がある場合は特に、ステップをスキップできることには意味がありません。 C intがFoo追加された場合、Foo / Barの例を使用するには、 Bar(1)を変更する必要がありますが、 Bar("hello world")は変更しません。 それほど一般的ではない状況でいくつかのキーストロークを節約するためにすべてが複雑になり、概念がこのように見えることもあれば、そのように見えることもあるため、概念が理解しにくくなります。この便利なフローチャートを参照して、どちらが当てはまるかを確認してください。

†私はそれらのより良い名前があればいいのにと思います。 すでにstructタグがあります。 私はそれらをラベルと呼んでいたでしょうが、Goにもそれらがあります。 それらをフィールドと呼ぶことは、より適切であり、最も混乱しているように思われます。 誰かがバイクシェッドをしたいなら、これは本当に新鮮なコートを使うことができます。

ある意味で、タグ付き共用体はインターフェースというよりも構造体に似ています。 これらは、一度に1つのフィールドしか設定できない特別な種類の構造体です。 その観点から見ると、Foo / Barの例は次のようになります。

type Foo struct {
  A string
  B int
}

func Bar(f Foo) {...}

func main() {
  Bar("hello world") //same as Bar(Foo{A: "hello world", B: 0})
  Bar(1) //same as Bar(Foo{A: "", B: 1})
}

この場合は明確ですが、それは良い考えではないと思います。

また、提案では、本当にキーストロークを保存したい場合は、明確な場合にBar(Foo{1})が許可されます。 ユニオンへのポインタを持つこともできるので、 &Foo{"hello world"}は複合リテラル構文が引き続き必要です。

とは言うものの、ユニオンは、「フィールド」が現在設定されている動的タグを持っているという点で、インターフェースと類似しています。

switch v := u.[type] {... switch v := i.(type) {... 、インターフェイスのu.[union]にする必要があるかもしれませんが、どちらの方法でも構文はそれほど重くなく、意味が明確です。

.(type)は不要であるという同じ議論をすることもできますが、私の意見では、何が起こっているのかを常に正確に把握しており、それが完全に正当化されていることがわかります。

それがこれらの選択の背後にある私の理由でした。

@jimmyfrasche
あなたの説明の後でも、switch構文は私には少し直感に反しているようです。 インターフェイスを使用すると、 switch v := i.(type) {...は、スイッチケースでリストされ、 .(type)示されるように、可能なタイプを切り替えます。
ただし、ユニオンを使用すると、スイッチは可能なタイプではなく値を切り替えます。 それぞれのケースは異なる可能な値を表しており、値は実際には同じタイプを共有している可能性があります。 これは文字列やintスイッチに似ており、ケースにも値がリストされ、それらの構文は単純なswitch v := u {...です。 そのことから、intやstringの場合よりもケースは似ていますが、より制限的であるため、ユニオンの値を切り替えるのがswitch v := u { ...になるのは自然なことのように思えます。

@urandomは、構文の非常に良い点です。 真実は、それがラベルのない私の以前の提案からの引き継ぎであるということです、それでそれは当時のタイプでした。 何も考えずにやみくもにコピーしました。 ご指摘いただきありがとうございます。

switch u {...働くだろうが、との問題switch v := u {... 、それはあまりにも多くのように見えるでswitch v := f(); v {... (エラー報告がより困難-ない明確な意図された作ることになります)。

@asによって提案されているようにunionキーワードの名前がpickに変更された場合、タグスイッチはswitch u.[pick] {...またはswitch v := u.[pick] {...と記述でき、対称性が保たれます。タイプスイッチ付きですが、混乱を失い、かなり見栄えがします。

実装がintをオンに切り替えている場合でも、動的タグとプリペイドへのピックの暗黙的な分解があります。これは、文法規則に関係なく、明示的である必要があると思います。

ご存知のとおり、タグフィールドを呼び出して、フィールドアサートとフィールドスイッチにするだけで、非常に理にかなっています。

編集:それはピックでリフレクトを使用するのは厄介ですが

[応答が遅れてすみません-私は休暇で不在でした]

@ianlancetaylorは書いた:

私があなたの提案に見ない何かが、私たちがこれをしなければならない理由です。 新しい種類のタイプを追加することには明らかな欠点があります。それは、Goに傾倒するすべての人が学ばなければならない新しい概念です。 補償の利点は何ですか? 特に、新しい種類のタイプは、インターフェイスタイプからは得られないものを私たちに与えますか?

私が見る2つの主な利点があります。 1つ目は、言語の利点です。 2つ目は、パフォーマンス上の利点です。

  • メッセージを処理するとき、特に並行プロセスから読み取る場合、各メッセージには関連するプロトコル要件が伴う可能性があるため、受信できるメッセージの完全なセットを知ることができると非常に便利です。 特定のプロトコルでは、可能なメッセージタイプの数は非常に少ない場合がありますが、メッセージを表すためにオープンエンドインターフェイスを使用する場合、その不変条件は明確ではありません。 多くの場合、これを回避するためにメッセージタイプごとに異なるチャネルを使用しますが、それには独自のコストが伴います。

  • 考えられる既知のメッセージタイプが少数あり、いずれにもポインタが含まれていない場合があります。 オープンエンドのインターフェースを使用してそれらを表す場合、インターフェース値を作成するための割り当てを行う必要があります。 可能なメッセージタイプを制限するタイプを使用することは、回避できることを意味し、GCのプレッシャーを軽減し、キャッシュの局所性を高めます。

合計タイプが解決できる私にとっての特定の苦痛はgodocです。 ast.Specを例にとってみましょう: //golang.org/pkg/go/ast/#Spec

多くのパッケージは、名前付きインターフェイスタイプの可能な基になるタイプを手動でリストするため、ユーザーはコードを見たり、名前のサフィックスやプレフィックスに依存したりすることなく、すばやくアイデアを得ることができます。

言語がすべての可能な値をすでに知っている場合、これはiotasを使用した列挙型のようにgodocで自動化できます。 また、単なる平文ではなく、実際に型にリンクすることもできます。

編集:別の例: https :

@mvdanは、言語を変更せずにGo1のストーリーを改善するための優れた実用的なポイントです。 そのために別の問題を提出し、これを参照できますか?

申し訳ありませんが、godocページ内の他の名前へのリンクだけを参照していますが、それでも手動でリストしていますか?

申し訳ありませんが、もっと明確にすべきでした。

godocの現在のパッケージで定義されているインターフェースを実装するタイプを自動的に処理するための機能リクエストを意味しました。

(手動でリストされた名前をリンクするための機能要求がどこかにあると思いますが、現時点ではそれを探す時間がありません)。

この(すでに非常に長い)スレッドを引き継ぐことは望まないので、別の問題を作成しました-上記を参照してください。

@Merovius ASTのものは列挙型よりも合計タイプに多く適用されるため、この号ではhttps://github.com/golang/go/issues/19814#issuecomment-298833986に返信してい

まず、合計タイプがGoに属しているかどうかわからないことを繰り返し述べたいと思います。 私はまだ彼らが絶対に属していないことを自分自身に納得させていません。 私は、彼らがアイデアを探求し、それらが適合するかどうかを確認するためにそうするという仮定の下で働いています。 しかし、私はどちらの方法でも確信するつもりです。

次に、コメントで段階的なコード修復について言及しました。 合計タイプに新しい用語を追加することは、定義上、インターフェイスに新しいメソッドを追加したり、構造体からフィールドを削除したりするのと同じように、重大な変更です。 しかし、これは正しくて望ましい動作です。

新しい種類のノードを追加する、ノードインターフェイスで実装されたASTの例を考えてみましょう。 ASTが外部プロジェクトで定義されており、それをプロジェクト内のパッケージにインポートするとします。これにより、ASTがウォークされます。

いくつかのケースがあります:

  1. あなたのコードはすべてのノードを歩くことを期待しています:
    1.1。 デフォルトのステートメントがありません、あなたのコードは黙って間違っています
    1.2。 パニックを伴うデフォルトのステートメントがあり、コードはコンパイル時ではなく実行時に失敗します(テストは、テストを作成したときに存在したノードについてのみ認識しているため、役に立ちません)
  2. コードは、ノードタイプのサブセットのみを検査します。
    2.1。 この新しい種類のノードは、とにかくサブセットに含まれていなかったでしょう
    2.1.1。 この新しいノードに関心のあるノードが含まれていない限り、すべてがうまくいきます
    2.1.2。 それ以外の場合は、コードがすべてのノードをウォークすることを期待しているのと同じ状況になります
    2.2。 この新しい種類のノードは、あなたがそれについて知っていれば、あなたが興味を持っているサブセットに含まれていたでしょう。

インターフェイスベースのASTでは、ケース2.1.1のみが正しく機能します。 これは何よりも偶然です。 段階的なコード修復は機能しません。 ASTはそのバージョンをバンプする必要があり、コードはそのバージョンをバンプする必要があります。

徹底的なリンターは役に立ちますが、リンターはすべてのインターフェイスタイプを検査できるわけではないため、特定のインターフェイスをチェックする必要があることを何らかの方法で伝える必要があります。 これは、ソース内のコメントまたはリポジトリ内のある種の設定ファイルを意味します。 ソース内のコメントの場合、定義上、ASTは別のプロジェクトで定義されているため、網羅性チェックのためにインターフェイスにタグを付けるのはそのプロジェクトに翻弄されます。 これは、コミュニティ全体が同意し、常に使用する単一の網羅性リンターがある場合にのみ大規模に機能します。

合計ベースのASTでは、バージョン管理を使用する必要があります。 この場合の唯一の違いは、網羅性リンターがコンパイラーに組み込まれていることです。

どちらも2.2には役立ちませんが、何ができるでしょうか?

合計タイプが役立つ、より単純なAST隣接のケースがあります:トークン。 より単純な電卓用の字句解析プログラムを作成しているとします。 値が関連付けられていない*ようなトークンと、名前を表す文字列を持つVarようなトークン、およびfloat64を保持するValようなトークンがあります。 。

これはインターフェースで実装できますが、面倒です。 ただし、おそらく次のようなことをします。

package token
type Type int
const (
  Times Type = iota
  // ...
  Var
  Val
)
type Value struct {
  Type
  Name string // only valid if Type == Var
  Number float64 // only valid if Type == Val
}

iotaベースの列挙型の徹底的なリンターは、不正なTypeが使用されないようにすることができますが、Type == Timesの場合はNameに割り当て、Type == Varの場合はNumberを使用する人に対してはあまりうまく機能しません。 トークンの数と種類が増えるにつれて、それは悪化するだけです。 ここでできる最善のことは、すべての制約をチェックするメソッドValid() errorと、いつ何ができるかを説明する一連のドキュメントを追加することです。

合計タイプはこれらすべての制約を簡単にエンコードし、定義は必要なすべてのドキュメントになります。 新しい種類のトークンを追加することは重大な変更になりますが、ASTについて私が言ったことはすべてここでも当てはまります。

もっと工具が必要だと思います。 私はそれで十分だとは確信していません。

@jimmyfrasche

次に、コメントで段階的なコード修復について言及しました。 合計タイプに新しい用語を追加することは、定義上、インターフェイスに新しいメソッドを追加したり、構造体からフィールドを削除したりするのと同じように、重大な変更です。

いいえ、同等ではありません。 段階的修復モデルでこれらの変更の両方を行うことができます(インターフェイスの場合:1。すべての実装に新しいメソッドを追加します。2。インターフェイスにメソッドを追加します。構造体フィールドの場合:1。フィールドのすべての使用法を削除します。2。フィールドを削除します)。 合計タイプでケースを追加することは、段階的修復モデルでません。 最初にlibを追加すると、すべてのユーザーが完全にチェックしなくなるため、すべてのユーザーが破損しますが、新しいケースがまだ存在しないため、最初にユーザーに追加することはできません。 取り外しについても同じことが言えます。

それが重大な変更であるかどうかではなく、最小限の中断で調整できる重大な変更であるかどうかです。

しかし、これは正しくて望ましい動作です。

丁度。 合計タイプは、その定義と人々がそれらを必要とするすべての理由により、段階的なコード修復のアイデアと根本的に互換性がありません。

インターフェイスベースのASTでは、ケース2.1.1のみが正しく機能します。

いいえ、それはケース1.2(認識されない文法のために実行時に失敗することは完全に問題ありません。私は恐らくパニックになりたくないでしょうが、エラーを返すだけです)と2.1の多くのケースでも正しく機能します。 残りは、ソフトウェアのアップグレードに関する基本的な問題です。 ライブラリに新しい機能を追加する場合、ライブラリのユーザーはそれを利用するためにコードを変更する必要があります。 ただし、ソフトウェアが正しくないという意味ではありません。

ASTはそのバージョンをバンプする必要があり、コードはそのバージョンをバンプする必要があります。

あなたが言っていることから、これがどのように続くのか、私にはまったくわかりません。 私にとって、「この新しい文法はまだすべてのツールで機能するわけではありませんが、コンパイラーは利用できます」と言っても問題ありません。 「この新しい文法でこのツールを実行すると、実行時に失敗する」のと同じように問題ありません。 最悪の場合、これは段階的な修復プロセスに別のステップを追加するだけです。a)新しいノードをASTパッケージとパーサーに追加します。 b)ASTパッケージを使用してツールを修正し、新しいノードを利用します。 c)新しいノードを使用するようにコードを更新します。 はい、新しいノードは、a)とb)が完了した後にのみ使用可能になります。 ただし、このプロセスのすべてのステップで、破損することなく、すべてが正常にコンパイルおよび動作します。

漸進的なコード修復と徹底的なコンパイラチェックのない世界で、あなたが自動的に元気になると言っているのではありません。 それでも注意深い計画と実行が必要であり、おそらく維持されていない逆依存関係を壊し、まったくできない変更があるかもしれません(私は何も考えられませんが)。 ただし、少なくともa)段階的なアップグレードパスがあり、b)これによって実行時にツールが破損するかどうかの決定は、ツールの作成者に任されています。 彼らは未知の場合に何をすべきかを決めることができます。

徹底的なリンターは役に立ちますが、リンターはすべてのインターフェイスタイプを検査できるわけではないため、特定のインターフェイスをチェックする必要があることを何らかの方法で伝える必要があります。

どうして? 私はswitchlint™のためのそれの罰金は、デフォルト・ケースなしのいずれかのタイプのスイッチ文句を言うことを主張するだろう。 結局のところ、コードはどのインターフェイス定義でも機能することが期待されるため、未知の実装で機能するコードがないことは、とにかく問題になる可能性があります。 はい、このルールには例外がありますが、例外はすでに手動で無視できます。

私はおそらく、実際の合計型よりも、コンパイラーで「すべての型スイッチはデフォルトのケースを必要とするはずです」をコンパイラーで強制することにもっと乗り込んでいるでしょう。 それは人々が未知の選択に直面したときに彼らのコードが何をすべきかを決定することを可能にしそして強制するでしょう。

これはインターフェースで実装できますが、面倒です。

肩をすくめるのは、めったに起こらない場合の1回限りの作業です。 私には問題ないようです。

そしてFWIW、私は現在、合計型の徹底的なチェックの概念に反対しているだけです。 「これらの構造的に定義されたタイプのいずれか」と言うことの追加された便利さについて、私はまだ強い意見を持っていません。

@Merovius段階的なコード修復についてのあなたの優れた点についてさらに考える必要があります。 その間:

徹底チェック

私は現在、合計型の徹底的なチェックの概念に反対しているだけです。

デフォルトのケースを使用して、網羅性チェックを明示的にオプトアウトできます(まあ、事実上、デフォルトでは、「その他すべて、それが何であれ」をカバーするケースを追加することで、網羅的チェックになります)。 まだ選択肢はありますが、明示的に行う必要があります。

switchlint™がデフォルトケースのないタイプスイッチについて文句を言うのは問題ないと私は主張します。 結局のところ、コードはどのインターフェイス定義でも機能することが期待されるため、未知の実装で機能するコードがないことは、とにかく問題になる可能性があります。 はい、このルールには例外がありますが、例外はすでに手動で無視できます。

それは興味深い考えです。 インターフェイスでシミュレートされた合計型とconst / iotaでシミュレートされた列挙型にヒットしますが、既知のケースを見逃したことはわかりません。未知のケースを処理しなかっただけです。 とにかく、うるさいようです。 検討:

switch {
case n < 0:
case n == 0:
case n > 0:
}

nが整数の場合(floatの場合はn != nが欠落しています)、これは網羅的ですが、型に関する多くの情報をエンコードしないと、デフォルトが欠落していることを示すフラグを立てる方が簡単です。 次のような場合:

switch {
case p[0](a, b):
case p[1](a, b):
//...
case p[N](a, b):
}

p[i]がaとbのタイプで同値関係を形成している場合でも、それを証明することはできないため、スイッチにデフォルトがないことを示すフラグを立てる必要があります。ケース。これは、マニフェスト、ソース内の注釈、ホワイトリストからegrep -v除外するラッパースクリプト、またはp[i]を誤って暗示するスイッチの不要なデフォルトでそれを沈黙させる方法を意味します。

いずれにせよ、「すべての状況でデフォルトがないことについて常に不平を言う」ルートが取られた場合、実装するのは簡単なリンターです。 そうしてgo-corpusで実行し、実際にどれほどノイズが多いか、および/または有用であるかを確認するのは興味深いことです。

トークン

代替トークンの実装:

//Type defined as before
type SimpleToken { Type }
type StringToken { Type; Value string }
type NumberToken { Type; Value float64 }
type Interface interface {
  //some method common to all these types, maybe just token() Interface
}

これにより、文字列と数値の両方を持つものが、 SimpleTokenまたはその逆の型でStringTokenを作成することを許可しない、不正なトークン状態を定義する可能性がなくなります。逆もまた同様です。

インターフェイスでこれを行うには、トークンごとに1つのタイプ( type Plus struct{} 、 type Mul struct{}など)を定義する必要があり、ほとんどの定義はタイプ名とまったく同じです。 一度の努力であろうとなかろうと、それは多くの作業です(ただし、この場合のコード生成には適しています)。

トークンインターフェイスの「階層」を使用して、許容値に基づいてトークンの種類を分割できると思います(この例では、数値や文字列などを含むことができるトークンの種類が複数あると想定しています)。

type SimpleToken int //implements token.Interface
const (
  Plus SimpleToken = iota
  // ...
}
type NumericToken interface {
  Interface
  Value() float64
  nt() NumericToken
}
type IntToken struct { //implements NumericToken, and a FloatToken
type StringToken interface { // for Var and Func and Const, etc.
  Interface
  Value() string
  st() StringToken
}

とにかく、文字列が含まれる場合にのみポインタを必要とする構造体または合計型とは異なり、各トークンはその値にアクセスするためにポインタの違いを必要とすることを意味します。 したがって、適切なリンターとgodocの改善により、この場合の合計タイプの大きなメリットは、違法な状態と(キーボードの意味での)入力の量を禁止しながら割り当てを最小限に抑えることに関連していますが、これは重要ではないようです。

デフォルトのケースを使用して、網羅性チェックを明示的にオプトアウトできます(まあ、事実上、デフォルトでは、「その他すべて、それが何であれ」をカバーするケースを追加することで、網羅的チェックになります)。 まだ選択肢はありますが、明示的に行う必要があります。

したがって、どちらの方法でも、徹底的なチェックをオプトインまたはオプトアウトするかを選択できます:)

既知のケースを見逃したことを示すのではなく、未知のケースを処理しなかっただけです。

事実上、コンパイラはすでにプログラム全体の分析を行って、どのインターフェイスでどの具象タイプが使用されているかを判断していると思いますか? 少なくとも、少なくとも非インターフェース型アサーション(つまり、インターフェース型ではなく具象型を表明する型アサーション)の場合、コンパイル時にインターフェースで使用される関数テーブルを生成することを期待します。
しかし、正直なところ、これは第一原理から議論されており、実際の実装についてはわかりません。

いずれにせよ、a)プログラム全体で定義された具体的な型をリストし、b)型スイッチについて、そのインターフェイスを実装しているかどうかでフィルタリングするのは非常に簡単です。 このようなものを使用すると、信頼できるリストになります。 私が思うに。

私は実際に明示的にオプションを記述として信頼性の高いようであるツールを書き込むことができると確信して100%ではないんだけど、私はあなたが例の90%をカバーすることができると確信しているとあなたは間違いなく、この外を行うツールを書くことができます正しい注釈が与えられたコンパイラー(つまり、sum-typesを実際の型ではなく、プラグマのようなコメントにします)。 確かに、素晴らしい解決策ではありません。

とにかく、うるさいようです。 検討:

これは不公平だと思います。 あなたが言及しているケースは、合計型とはまったく関係がありません。 そのようなツールをどこに書くかというと、タイプスイッチと式付きのスイッチに制限します。これらは合計タイプも処理される方法のようです。

代替トークンの実装:

なぜマーカー法ではないのですか? タイプフィールドは必要ありません。インターフェイス表現から無料で取得できます。 マーカーメソッドを何度も繰り返すことを心配している場合; エクスポートされていない構造体{}を定義し、そのマーカーメソッドを指定して各実装に埋め込みます。追加コストがゼロで、オプションごとの入力がメソッドよりも少なくなります。

とにかく、それは各トークンがその値にアクセスするためにポインタの違いを必要とすることを意味します

はい。 それは本当のコストだが、私はそれが基本的に他の引数を上回るとは思いません。

これは不公平だと思います。

それは本当だ。

私は手っ取り早いバージョンを作成し、それをstdlibで実行しました。 switchステートメントをチェックすると1956ヒットが発生し、 switch {フォームをスキップするように制限すると、そのカウントは1677に減少しました。結果が意味があるかどうかを確認するためにこれらの場所を調べていません。

https://github.com/jimmyfrasche/switchlint

確かに改善の余地はたくさんあります。 それほど洗練されていません。 プルリクエストは大歓迎です。

(残りは後で返信します)

編集:間違ったマークアップ形式

これはこれまでのすべての(かなり偏った)要約だと思います(そして私の2番目の提案を麻薬的に仮定します)

長所

  • 簡潔で、自己文書化された方法で簡潔に多くの制約を書くのは簡単
  • 割り当てのより良い制御
  • 最適化が容易(コンパイラーに知られているすべての可能性)
  • 徹底的なチェック(必要に応じてオプトアウトできます)

短所

  • 合計タイプのメンバーへの変更は重大な変更であり、すべての外部パッケージが網羅性チェックをオプトアウトしない限り、段階的なコード修復は許可されません。
  • 学ぶべき言語のもう1つのこと、既存の機能とのいくつかの概念的な重複
  • ガベージコレクターは、どのメンバーがポインターであるかを知る必要があります
  • 1 + 1 + ⋯ + 1形式の合計には扱いにくい

代替案

  • 1 + 1 + ⋯ + 1形式の合計のiota「列挙型」
  • より複雑な合計(おそらく生成される)のためのエクスポートされていないタグメソッドとのインターフェース
  • または、iota列挙型と、列挙型の値に応じて設定されるフィールドに関する言語外ルールを使用して構造体を作成します

関係なく

  • より良いツーリング、常により良いツーリング

段階的な修理の場合、それは大きな問題ですが、唯一の選択肢は、外部パッケージが網羅性チェックをオプトアウトすることだと思います。 これは、他のすべてに一致する場合でも、将来の校正のみに関係する「不要な」デフォルトのケースを持つことが合法でなければならないことを意味します。 私はそれが今暗黙のうちに真実であると信じています、そして特定するのに十分簡単ではないにしても。

パッケージメンテナから、「次のバージョンでこの合計タイプに新しいメンバーを追加する予定です。処理できることを確認してください」というアナウンスがあり、switchlintツールが必要なケースを見つけることができます。オプトアウトされます。

他の場合ほど単純ではありませんが、それでもかなり実行可能です。

外部で定義された合計型を使用するプログラムを作成する場合、デフォルトをコメントアウトして既知のケースを見逃していないことを確認し、コミットする前にコメントを外すことができます。 または、デフォルトが「不要」であることを通知するツールがある可能性があります。これにより、すべてが既知になり、未知のものに対して将来にわたって使用できるようになります。

合計型をシミュレートするインターフェイス型を使用するときに、それらが定義されているパッケージに関係なく、リンターによる網羅性チェックをオプトインしたいとします。

@MeroviusあなたのbetterSumType() BetterSumTypeトリックはとてもクールですが、それは切り替えが定義パッケージで行われなければならないことを意味します(またはあなたは次のようなものを公開します

func CallBeforeSwitches(b BetterSumType) (BetterSumType, bool) {
    if b == nil {
        return nil, false
    }
    b = b.betterSumType()
    if b == nil {
        return nil, false
    }
    return b, true
}

また、毎回呼び出されるリント)。

プログラム内のすべてのスイッチが網羅的であることを確認するために必要な基準は何ですか?

それは何でもゲームなので、空のインターフェースにすることはできません。 したがって、少なくとも1つのメソッドが必要です。

インターフェイスにエクスポートされていないメソッドがない場合は、どのタイプでも実装できるため、網羅性は各スイッチのコールグラフまでのすべてのパッケージに依存します。 パッケージをインポートし、そのインターフェイスを実装してから、その値をパッケージの関数の1つに送信することができます。 そのため、その関数のスイッチは、インポートサイクルを作成せずに網羅することはできません。 したがって、少なくとも1つのエクスポートされていないメソッドが必要です。 (これには前の基準が含まれます)。

埋め込むと、探しているプロパティが台無しになるため、パッケージのインポーターがインターフェイスやそれを実装するタイプを埋め込まないようにする必要があります。 本当に派手なリンターは、エンベディッドバリューを作成する特定の関数を呼び出さない場合、またはエンベディッドインターフェイスがパッケージのAPI境界を「エスケープ」しない場合、埋め込みが問題ない場合があることを認識できる場合があります。

徹底的に行うには、インターフェイスのゼロ値が渡されないことを確認するか、徹底的なスイッチでcase nilも確認する必要があります。 (後者の方が簡単ですが、nilを含めると、「タイプAまたはタイプBまたはタイプC」の合計が「nilまたはタイプAまたはタイプBまたはタイプC」の合計に変わるため、前者が推奨されます)。

インポートのツリーとそのツリー内の特定のインターフェイスに対してこれらのセマンティクスを検証できる、オプションの機能も含め、すべての機能を備えたリンターがあるとします。

ここで、依存関係Dを持つプロジェクトがあるとします。Dのパッケージの1つで定義されたインターフェースが、プロジェクトで網羅的であることを確認したいと思います。 そうだとしましょう。

次に、プロジェクトD 'に新しい依存関係を追加する必要があります。 D 'が問題のインターフェイスタイプを定義したパッケージをDにインポートするが、このリンターを使用しない場合、完全なスイッチを使用するために保持する必要のある不変条件を簡単に破棄できます。

さらに言えば、メンテナがリンターを実行しているからではなく、Dが偶然にリンターを通過したとしましょう。 Dにアップグレードすると、D 'と同じくらい簡単に不変条件を破壊できます。

リンターが「今は100%網羅的です👍」と言っても、何もしなくても変更できます。

「iotaenums」の網羅性チェッカーの方が簡単なようです。

すべてのtype t u 、 uは整数であり、 tは、個別に指定された値またはiotaいずれかでconstとして使用されます。 u値は、これらの定数に含まれています。

ノート:

  • 重複する値はエイリアスとして扱われ、この分析では無視されます。 名前付き定数はすべて異なる値を持っていると想定します。
  • 1 << iotaはパワーセットとして扱われる可能性があります。少なくともほとんどの場合はそう思いますが、特にビット単位の補数を取り巻く追加の条件が必要になる可能性があります。 当分の間、それらは考慮されません

いくつかの省略形については、 min(t)定数と呼び、他の定数についてはC 、 min(t) <= Cと呼び、同様に、 max(t)そのような定数と呼びましょう。他の定数の場合、 C 、 C <= max(t) 。

tが徹底的に使用されるようにするには、次のことを確認する必要があります。

  • t値は、常に名前付き定数です(または、関数呼び出しなどの特定の慣用的な位置では0)。
  • min(t) <= v <= max(t)以外では、 t 、 v値の不平等比較はありません。
  • t値は、算術演算+ 、 /などでは使用されません。結果がmin(t)とmax(t)間にクランプされる場合、例外が発生する可能性があります。直後にtを定義するパッケージに制限する必要があります。
  • スイッチには、 tまたはデフォルトのケースのすべての定数が含まれています。

これでも、インポートツリー内のすべてのパッケージを確認する必要があり、慣用的なコードで無効になる可能性は低くなりますが、簡単に無効にすることができます。

私の理解では、これはタイプエイリアスと同様に、変更を壊すことはないので、なぜGo2のためにそれを保持するのですか?

型エイリアスは、明確な重大な変更である新しいキーワードを導入しません。 マイナーな言語変更でさえモラトリアムがあるようで、これは大きな変更になるでしょう。 反映された合計値を処理するためにすべてのマーシャル/アンマーシャルルーチンを改良するだけでも、大きな試練になります。

タイプエイリアスは、回避策がなかった問題を修正しています。 合計型は型の安全性に利点がありますが、それがないことはショーストッパーではありません。

@rogpeppeの最初の提案のようなものを支持する1つの(マイナーな)ポイント。 パッケージhttpには、インターフェイスタイプHandlerと、それを実装する関数タイプHandlerFuncます。 現在、関数をhttp.Handleに渡すには、明示的にHandlerFuncに変換する必要があります。 http.Handle代わりにタイプHandlerFunc | Handler引数を受け入れた場合、 HandlerFunc直接割り当て可能な任意の関数/クロージャを受け入れることができます。 ユニオンは、名前のない型の値をインターフェイス型に変換する方法をコンパイラに伝える型ヒントとして効果的に機能します。 HandlerFuncはHandler HandlerFunc実装しているため、それ以外の場合、共用体型はHandlerまったく同じように動作します。

@griesemerは、列挙型スレッドhttps://github.com/golang/go/issues/19814#issuecomment -322752526でのコメントに応えて、このスレッドの前半の私の提案https://github.com/golang/ go / issues / 19412#issuecomment -289588569は、合計タイプ(「スウィフトスタイルの列挙型」)がGoでどのように機能する必要があるかという問題に対処します。 彼らが行くのに必要な追加になる場合、私はそれらをしたいと思います限り、私は知らないが、私は彼らが追加された場合、彼らは見ているだろうと思います/そのように多くのことを操作します。

その投稿は完全ではなく、このスレッドの前後で、このスレッド全体に説明がありますが、このスレッドは非常に長いので、これらの点を繰り返したり要約したりしてもかまいません。

タイプタグ付きのインターフェイスによってシミュレートされた合計タイプがあり、埋め込みによってそれを回避することが絶対にできない場合、これは私が思いついた最高の防御です: https :

@jimmyfrasche私はしばらく前にこれを書き

別の可能なアプローチはこれです: https :

@rogpeppeリフレクションを使用する場合は、リフレクションを使用しないのはなぜですか?

ここや他の問題のコメントに基づいて、2番目の提案の改訂版を作成しました。

特に、徹底的なチェックを削除しました。 ただし、合計タイプをシミュレートするために使用される他のGoタイプ用に作成できるとは思わないものの、外部の網羅性チェッカーを以下の提案に書き込むのは簡単です。

編集:ピック値の動的な値に対してassertと入力する機能を削除しました。 それはあまりにも魔法であり、それを許可する理由はコード生成によっても同様に提供されます。

Edit2:ピックが別のパッケージで定義されている場合に、フィールド名がアサーションとスイッチでどのように機能するかを明確にしました。

Edit3:埋め込みの制限と暗黙のフィールド名の明確化

Edit4:スイッチのデフォルトを明確にする

タイプを選ぶ

ピックは、構文的に構造体に似た複合型です。

pick {
  A, B S
  C, D T
  E U "a pick tag"
}

上記では、 A 、 B 、 C 、 D 、およびEがピックのフィールド名であり、 S 、 T 、およびUは、これらのフィールドのそれぞれのタイプです。 フィールド名はエクスポートされる場合とされない場合があります。

ピックは、間接なしでは再帰的ではない場合があります。

法的

type p pick {
    //...
    p *p
}

違法

type p pick {
    //...
    p p
}

ピックの埋め込みはありませんが、ピックを構造体に埋め込むことができます。 ピックが構造体に埋め込まれている場合、ピックのメソッドは構造体にプロモートされますが、ピックのフィールドはプロモートされません。

フィールド名のないタイプは、タイプと同じ名前のフィールドを定義するための省略形です。 (これは、タイプに名前が付いていない場合のエラーです。ただし、名前がTである*Tは例外です)。

例えば、

type p pick {
    io.Reader
    io.Writer
    string
}

3つのフィールドReader 、 Writer 、およびstringがあり、それぞれのタイプがあります。 フィールドstringは、ユニバーススコープ内にある場合でも、エクスポートされないことに注意してください。

ピックタイプの値は、動的フィールドとそのフィールドの値で構成されます。

ピックタイプのゼロ値は、ソース順の最初のフィールドであり、そのフィールドのゼロ値です。

同じピックタイプの2つの値、 aとbが与えられた場合、ピック値は他の値として割り当てることができます。

a = b

ピック内のフィールドの1つのタイプの1つであっても、ピック以外の値を割り当てることは違法です。

ピックタイプには、常に1つの動的フィールドしかありません。

複合リテラル構文は構造体に似ていますが、追加の制限があります。 つまり、キーレスリテラルは常に無効であり、指定できるキーは1つだけです。

以下が有効です

pick{A string; B int}{A: "string"} //value is (B, "string")
pick{A, B int}{B: 1} //value is (B, 1)
pick{A, B string}{} //value is (A, "")

コンパイル時のエラーは次のとおりです。

pick{A int; B string}{A: 1, B: "string"} //a pick can only have one value at a time
pick{A int; B uint}{1} //pick composite literals must be keyed

タイプpick {A int; B string}の値pが与えられた場合、次の割り当て

p.B = "hi"

pの動的フィールドをBし、 Bを「hi」に設定します。

現在の動的フィールドに割り当てると、そのフィールドの値が更新されます。 新しい動的フィールドを設定する割り当てでは、指定されていないメモリ位置をゼロにする必要があります。 ピックフィールドのピックフィールドまたは構造体フィールドへの割り当ては、必要に応じて動的フィールドを更新または設定します。

type P pick {
    A, B image.Point
}

var p P
fmt.Println(P) //{A: {0 0}}

p.A.X = 1 //A is the dynamic field, update
fmt.Println(P) //{A: {1 0}}

p.B.Y = 2 //B is not the dynamic value, create zero image.Point first
fmt.Println(P) //{B: {0 2}}

ピックに保持されている値には、フィールドアサートまたはフィールドスイッチでのみアクセスできます。

x := p.[X] //panics if X is not the dynamic field of p
x, ok := p.[X] //returns the zero value of X and false if X is not the dynamic field of p

switch v := p.[var] {
case A:
case B, C: // v is only defined in this case if fields B and C have identical type names
case D:
default: // always legal even if all fields are exhaustively listed above
}

フィールドアサーションおよびフィールドスイッチのフィールド名は、タイプのプロパティであり、定義されたパッケージではありません。 pickを定義するパッケージ名で修飾することはできません。また、修飾することもできません。

これは有効です:

_, ok := externalPackage.ReturnsPick().[Field]

これは無効です:

_, ok := externalPackage.ReturnsPick().[externalPackage.Field]

フィールドアサーションとフィールドスイッチは、常に動的フィールドの値のコピーを返します。

エクスポートされていないフィールド名は、定義パッケージでのみアサートできます。

タイプアサーションとタイプスイッチもピックで機能します。

//removed, see note at top
//v, ok := p.(fmt.Stringer) //holds if the type of the dynamic field implements fmt.Stringer
//v, ok := p.(int) //holds if the type of the dynamic field is an int

タイプアサーションとタイプスイッチは、常に動的フィールドの値のコピーを返します。

ピックがインターフェイスに格納されている場合、インターフェイスの型アサーションは、ピック自体のメソッドセットとのみ一致します。 [上記は削除されたため、まだ真実ですが冗長です]

ピックのすべてのタイプが等式演算子をサポートしている場合、次のようになります。

  • そのピックの値は、マップキーとして使用できます
  • 同じ動的フィールドがあり、その値が==場合、同じピックの2つの値は==です。
  • 異なる動的フィールドを持つ2つの値は、 !=の値がある場合でも、 == 。

ピックタイプの値では、他の演算子はサポートされていません。

P内のフィールド名とそのタイプのセットがフィールド名とそのタイプのサブセットである場合、ピックタイプPの値を別のピックタイプQ変換できます。 Qます。

PとQが異なるパッケージで定義されていて、エクスポートされていないフィールドがある場合、それらのフィールドは名前とタイプに関係なく異なると見なされます。

例:

type P pick {A int; B string}
type Q pick {B string; A int; C float64}

//legal
var p P
q := Q(p)

//illegal
var q Q
p := P(Q) //cannot handle field C

2つのピックタイプ間の割り当て可能性は、1つだけのタイプが定義されている限り、兌換性として定義されます。

メソッドは、定義されたピックタイプで宣言できます。

エクスペリエンスレポートを作成(およびwikiに追加)しましたhttps://gist.github.com/jimmyfrasche/ba2b709cdc390585ba8c43c989797325

編集:と:heart: @mewmewに、その要点への返信として、はるかに優れた詳細なレポートを残しました

特定の型Tについて、型T変換したり、型T変数に割り当てたりできる型のリストを言う方法があるとしたらどうでしょうか。 例えば

type T interface{} restrict { string, error }

Tという名前の空のインターフェイスタイプを定義し、それに割り当てられる可能性のあるタイプはstringまたはerrorます。 他のタイプの値を割り当てようとすると、コンパイル時エラーが発生します。 今私は言うことができます

func FindOrFail(m map[int]string, key int) T {
    if v, ok := m[key]; ok {
        return v
    }
    return errors.New("no such key")
}

func Lookup() {
    v := FindOrFail(m, key)
    if err, ok := v.(error); ok {
        log.Fatal(err)
    }
    s := v.(string) // This type assertion must succeed.
}

この種のアプローチでは、合計タイプ(またはピックタイプ)のどの重要な要素が満たされないでしょうか?

s := v.(string) // This type assertion must succeed.

vもnilなる可能性があるため、これは厳密には当てはまりません。 この可能性を排除するには、言語にかなり大きな変更を加える必要があります。これは、ゼロ値を持たない型とそれに伴うすべてのものを導入することを意味するためです。 ゼロ値は言語の一部を単純化しますが、これらの種類の機能の設計をより困難にします。

興味深いことに、このアプローチは@rogpeppeの最初の提案とかなり似ています。 それが持っていないのは、リストされたタイプへの強制です。これは、前に指摘したような状況( http.Handler )で役立つ可能性があります。 もう1つのことは、バリアントは個別のタグではなくタイプによって区別されるため、各バリアントが個別のタイプである必要があることです。 これは厳密に表現力があると思いますが、バリアントタグとタイプを区別することを好む人もいます。

@ianlancetaylor

プロ

  • 閉じたタイプのセットに制限することが可能です—そしてそれは間違いなく主なことです
  • 正確な網羅性チェッカーを書くことが可能
  • 「このコントラクトを満たす値を割り当てることができます」プロパティを取得します。 (私はこれを気にしませんが、他の人は気にしていると思います)。

短所

  • それらは単なる利点とのインターフェースであり、実際には異なる種類のタイプではありません(ただし、素晴らしい利点です!)
  • あなたはまだnilを持っているので、型理論的な意味での合計型ではありません。 指定するA + B + Cは、実際には1 + A + B + Cであり、選択の余地はありません。 @stevenblenkinsopが私がこれに取り組んでいる間に指摘したように。
  • さらに重要なことに、その暗黙のポインターのために、常に間接参照があります。 ピック提案を使用すると、 pまたは*pを選択して、メモリのトレードオフをより細かく制御できます。 最適化として(Cの意味で)識別された共用体としてそれらを実装することはできませんでした。
  • ゼロ値の選択はありません。これは、Goで可能な限り有用なゼロ値を持つことが非常に重要であるため、非常に優れたプロパティです。
  • おそらく、 Tメソッドを定義できませんtype T restrict {string, error}だけではありません)
  • fields / summands / what-have-youのラベルを失うと、インターフェイスタイプと相互作用するときに混乱します。 合計型の強力な「正確にこれまたは正確にそれ」のプロパティが失われます。 io.Readerを入れて、 io.Writer引き出すことができます。 これは(制限のない)インターフェースには意味がありますが、合計型には意味がありません。
  • 2つの同一の型が異なることを意味するようにしたい場合は、ラッパー型を使用して曖昧さを解消する必要があります。 このようなタグは、構造体フィールドのように型に限定されるのではなく、外部の名前空間にある必要があります。
  • これはあなたの特定の言葉遣いを読みすぎているかもしれませんが、それは譲受人のタイプに基づいて割り当て可能性のルールを変更するようです(私はあなたがerror割り当て可能なものを割り当てることができないと言っていると読んでいますTへの変換は正確にエラーである必要があります)。

そうは言っても、それは主要なボックス(私がリストした最初の2つのプロ)をチェックします、そしてそれが私が得ることができるすべてであるならば、私はそれをハートビートで受け取ります。 しかし、私はもっと良くなることを願っています。

型アサーションルールが適用されていると仮定しました。 したがって、タイプは具象タイプと同一であるか、インターフェースタイプに割り当て可能である必要があります。 基本的には、インターフェイスとまったく同じように機能しますが、値( nil )は、リストされているタイプの少なくとも1つに対してアサート可能である必要があります。

@jimmyfrasche
更新された提案では、タイプのすべての要素が異なるタイプである場合、次の割り当てが可能でしょうか。

type p pick {
    A int
    B string
}

func Foo(P p) {
}

var P p = 42
var Q p = "foo"

Foo(42)
Foo("foo")

このような割り当てが可能な場合の合計タイプの使いやすさははるかに大きくなります。

ピック提案を使用すると、 pまたは*pを選択して、メモリのトレードオフをより細かく制御できます。

インターフェイスがスカラー値を格納するために割り当てる理由は、他のワードがポインタであるかどうかを判断するためにタイプワードを読み取る必要がないためです。 議論については#8405を参照してください。 同じ実装上の考慮事項がピックタイプにも当てはまる可能性があります。これは、実際には、 p割り当てられ、いずれにせよ非ローカルになることを意味する場合があります。

@urandomいいえ、あなたの定義を考えると、それは書かれなければならないでしょう

var p P = P{A: 42} // p := P{A: 42}
var q P = P{B: "foo")
Foo(P{A: 42}) // or Foo({A: 42}) if types can be elided here
Foo(P{B: "foo"})

それらは、一度に1つのフィールドしか設定できない構造体と考えるのが最善です。

それがなく、 C uintをp追加すると、 p = 42どうなりますか?

順序と割り当て可能性に基づいて多くのルールを作成できますが、それらは常に、型定義への変更が型を使用するすべてのコードに微妙で劇的な影響を与える可能性があることを意味します。

最良の場合、変更はあいまいさの欠如に依存してすべてのコードを壊し、再度コンパイルする前にp = int(42)またはp = uint(42)に変更する必要があることを示します。 1行の変更では、100行を修正する必要はありません。 特に、それらの行がコードに応じて人々のパッケージに含まれている場合。

100%明示的であるか、すべてを壊す可能性があるため誰も触れることができない非常に壊れやすいタイプである必要があります。

これはすべての合計タイプの提案に適用されますが、明示的なラベルがある場合でも、ラベルはどのタイプに割り当てられているかについて明示的であるため、割り当て可能です。

@josharianだから、私がそれを正しく読んでいるなら、Goが以前行ったように2番目のフィールドに単語サイズの値を隠さずにifaceが常に(*type, *value)になる理由は、同時GCが両方を検査する必要がないようにするためです2番目がポインタであるかどうかを確認するためのフィールド—常にそうであると想定できます。 私はそれを正しく理解しましたか?

言い換えると、ピックタイプが(C表記を使用して)実装された場合、

struct {
    int which;
    union {
         A a;
         B b;
         C c;
    } summands;
}

GCはwhichを検査して、 summandsをスキャンする必要があるかどうかを判断するために、ロック(または何か凝ったが同等のもの)を取得する必要がありますか?

Goが以前行ったように2番目のフィールドにワードサイズの値を格納するのではなく、ifaceが常に(* type、* value)になる理由は、並行GCが両方のフィールドを検査して2番目がポインターであるかどうかを確認する必要がないようにするためです。 —常にそうであると想定することができます。

それは正しい。

もちろん、ピックタイプの性質が限られているため、いくつかの代替実装が可能になります。 ピックタイプは、ポインター/非ポインターの一貫したパターンが常に存在するようにレイアウトできます。 たとえば、すべてのスカラー型がオーバーラップする可能性があり、文字列フィールドがスライスフィールドの先頭とオーバーラップする可能性があります(両方とも「ポインター、非ポインター」を開始するため)。 そう

pick {
  a uintptr
  b string
  c []byte
}

大まかにレイアウトすることができます:

[ word 1 (ptr) ] [ word 2 (non-ptr) ] [ word 3 (non-ptr) ]
[    <nil>         ] [                 a           ] [                              ]
[       b.ptr      ] [            b.len          ] [                              ]
[       c.ptr      ] [             c.len         ] [        c.cap             ]

ただし、他のピックタイプでは、このような最適なパッキングができない場合があります。 (ASCIIが壊れていることをお詫びします。GitHubに正しくレンダリングさせることができないようです。要点はわかりますが、願っています。)

静的レイアウトを実行するこの機能は、ピックタイプを含めることを支持するパフォーマンスの議論でさえあるかもしれません。 ここでの私の目標は、単に関連する実装の詳細にフラグを立てることです。

@josharianそしてそうしてくれてありがとう。 私はそのことを考えていませんでした(正直なところ、GCで識別された共用体をGCする方法に関する研究があったかどうかをググっただけで、そうすることができます。それを1日と呼びました。何らかの理由で、私の脳は「並行性」を関連付けませんでした。その日の「Go」で:facepalm!)。

タイプの1つが、すでにレイアウトを持っている定義済みの構造体である場合、選択肢は少なくなります。

1つのオプションは、サイズが同等の構造体と同じであることを意味するポインターが含まれている場合、被加数を「圧縮」しないことです(識別子intの場合は+1)。 可能であれば、ハイブリッドアプローチを採用して、レイアウトを共有できるすべてのタイプが実行できるようにします。

素敵なサイズのプロパティを失うのは残念ですが、それは実際には単なる最適化です。

ポインタが含まれていなくても、常に1 +同等の構造体のサイズであったとしても、割り当ての制御など、型自体の他のすべての優れたプロパティがあります。 追加の最適化は時間の経過とともに追加される可能性があり、少なくともあなたが指摘するように可能です。

type p pick {
    A int
    B string
}

AとBがそこにいる必要がありますか? ピックは一連のタイプから選択するので、識別子名を完全に破棄してみませんか。

type p pick {
    int
    string
}
q := p{string: "hello"}

このフォームはすでに構造体に有効であると思います。 ピックに必要な制約がある場合があります。

@フィールド名が省略されているかのように、タイプと同じであるため、例は機能しますが、これらのフィールド名はエクスポートされないため、定義パッケージ内からのみ設定/アクセスできます。

タイプ名に基づいて暗黙的に生成された場合でも、フィールド名が存在する必要があります。そうしないと、割り当て可能性やインターフェイスタイプとの相互作用が悪くなります。 フィールド名は、Goの他の部分で機能するようにするものです。

@お詫びとして、私はあなたが私が読んだものとは異なる何かを意味していることに気づきました。

公式化は機能しますが、構造体フィールドのように見えますが、通常のエクスポート/非エクスポートのもののために動作が異なります。

宇宙にあるので、 p定義するパッケージの外部から文字列にアクセスできますか?

どうですか

type t struct {}
type P pick {
  t
  //other stuff
}

?

フィールド名をタイプ名から分離することにより、次のようなことができます。

pick {
  unexported Exported
  Exported unexported
}

あるいは

pick { Recoverable, Fatal error }

ピックフィールドが構造体フィールドのように動作する場合は、構造体フィールドについてすでに知っていることの多くを使用して、ピックフィールドについて考えることができます。 唯一の本当の違いは、一度に設定できるフィールドを選択できるのは1つだけであるということです。

@jimmyfrasche
Goはすでに構造体への匿名型の埋め込みをサポートしているため、スコープの制限は言語にすでに存在するものであり、問​​題は型エイリアスによって解決されていると思います。 しかし、考えられるすべてのユースケースについて考えたわけではないことを認めてください。 このイディオムがGoで一般的であるかどうかにかかっているようです。

package p
type T struct{
    Exported t
}
type t struct{}

小さな_t_は、大きなTに埋め込まれているパッケージに存在し、そのようなエクスポートされたタイプを介してのみ公開されます。

@なぎで

ただし、完全にフォローしているのかどうかはわかりません。

//with the option to have field names
pick { //T is in the namespace of the pick and the type isn't exposed to other packages
  T t
  //...
}

//without
type T = t //T has to be defined in the outer scope and now t is exposed to other packages
pick {
  T
  //...
}

また、ラベルのタイプ名しかない場合、たとえば[]stringを含めるには、 type Strings = []stringを実行する必要があります。

これは、ピックタイプが実装されていることを確認したい方法です。 の
特に、RustとC ++(パフォーマンスのゴールドスタンダード)がどのように機能するかです。
それ。

徹底的なチェックが必要な場合は、チェッカーを使用できます。 私が欲しい
パフォーマンスが勝ちます。 つまり、ピックタイプもnilにすることはできません。

ピック要素のメンバーのアドレスを取得することは許可されるべきではありません(
でよく知られているように、シングルスレッドの場合でもメモリセーフではありません
Rustコミュニティ。)。 ピックタイプに他の制限が必要な場合は、
それならそうです。 しかし、私にとっては、ピックタイプを常にヒープに割り当てる必要があります
悪いでしょう。

2017ĺš´8月18日12:01 PMに、「jimmyfrasche」 [email protected]は次のように書いています。

@josharian https://github.com/josharianだから、私がそれを正しく読んでいるなら
ifaceが隠しではなく常に(* type、* value)になっている理由
Goが以前に行ったように、2番目のフィールドのワードサイズの値は、
並行GCは、2番目のフィールドかどうかを確認するために両方のフィールドを検査する必要はありません。
はポインタです。常にそうであると想定できます。 私はそれを正しく理解しましたか?

言い換えると、ピックタイプが(C表記を使用して)実装された場合、

構造体{
int which;
ユニオン{
A a;
B b;
C c;
}被加数;
}

GCは、次のようにロック(または派手だが同等のもの)を取得する必要があります。
被加数をスキャンする必要があるかどうかを判断するためにどちらを検査しますか?

—
スレッドを作成したため、これを受け取っています。
このメールに直接返信し、GitHubで表示してください
https://github.com/golang/go/issues/19412#issuecomment-323393003 、またはミュート
スレッド
https://github.com/notifications/unsubscribe-auth/AGGWB3Ayi31dYwotewcfgmCQL-XVrfxIks5sZbVrgaJpZM4MTmSr
。

@DemiMarie

pick要素のメンバーのアドレスを取得することは許可されるべきではありません(Rustコミュニティでよく知られているように、シングルスレッドの場合でもメモリセーフではありません)。 ピックタイプに他の制限が必要な場合は、そうしてください。

それは良い点です。 私はそこにそれを持っていました、しかしそれは編集で失われたに違いありません。 ピックから値にアクセスすると、同じ理由で常にコピーが返されることを含めました。

それが真実である理由の例として、後世のために、考えてみてください

v := pick{ A int; B bool }{A: 5}
p := &v.[A] //(this would be illegal but pretending it's not for a second)
v.B = true

vが最適化され、フィールドAとBがメモリ内で同じ位置を占めるようになっている場合、 pはintを指していません。つまり、指しているのです。ブール値に。 メモリの安全性が侵害されました。

@jimmyfrasche

コンテンツをアドレス指定可能にしたくない2つ目の理由は、ミューテーションセマンティクスです。 特定の状況下で値が間接的に保存される場合、

v := pick{ A int; ... }{A: 5}
v2 := v

v2.[A] = 6 // (this would be illegal under the proposal, but would be 
           // permitted if `v2.[A]` were addressable)

fmt.Println(v.[A]) // would print 6 if the contents of the pick are stored indirectly

pickがインターフェースに似ている可変アドレス指定可能ですが、現在Goには区別がありません)。これにより、エイリアシングを観察できなくなります。 。

編集:おっと(以下を参照)

@jimmyfrasche

ピックタイプのゼロ値は、ソース順の最初のフィールドであり、そのフィールドのゼロ値です。

v.[A]とv.(error)が正しいことを行うようにゼロ値を特別な場合を除いて、最初のフィールドを間接的に格納する必要がある場合、これは機能しないことに注意してください。

@stevenblenkinsop 「最初のフィールドは間接的に保存する必要がある」とはどういう意味か

与えられた

var p pick { A error; B int }

ゼロ値pには、動的フィールドAあり、 Aはnilです。

@josharianで説明されているように、ガベージコレクターによって課せられたレイアウトの制約のために、ポインターが含まれている/含まれているpickに格納されている値を参照していません

あなたの例では、 p.Bポインターではない)は、2つのポインターで構成されるp.Aと重複するストレージを共有できません。 ほとんどの場合、間接的に保存する必要があります(つまり、 intとしてではなく、アクセス時に自動的に逆参照される*intとして表されます)。 p.Bが最初のフィールドである場合、 pickのゼロ値はnew(int)になります。これは、初期化が必要なため、許容できるゼロ値ではありません。 nil *intがnew(int)として扱われるように、特殊なケースにする必要があります。

@jimmyfrasche
あ、ごめんなさい。 会話を振り返ると、非ポインター型の間接ストレージのインターフェースメカニズムをコピーするのではなく、隣接するストレージを使用して互換性のないレイアウトのバリアントを保存

編集:おっと、競合状態。 投稿してからあなたのコメントを見ました。

@stevenblenkinsopああ、わかりました、あなたが何を意味するのかわかります。 しかし、それは問題ではありません。

重複するストレージの共有は最適化です。 それは決してそれをすることができませんでした:タイプのセマンティクスは重要なビットです。

コンパイラーがストレージを最適化でき、そうすることを選択した場合、それは素晴らしいボーナスです。

あなたの例では、コンパイラはそれを同等の構造体とまったく同じように格納できます(アクティブフィールドがどれであるかを知るためにタグを追加します)。 これは

struct {
  which_field int // 0 = A, 1 = B
  A error
  B int
}

ゼロ値は依然としてすべてバイト0であり、特別な場合として密かに割り当てる必要はありません。

重要なのは、一度に1つのフィールドだけが機能するようにすることです。

ピックでタイプアサーション/スイッチを許可する動機は、たとえば、ピック内のすべてのタイプがfmt.Stringerを満たした場合、ピックで次のようなメソッドを記述できるようにすることでした。

func (p P) String() string {
  return p.(fmt.Stringer).String()
}

ただし、選択フィールドのタイプはインターフェースである可能性があるため、これは微妙な点を生み出します。

前の例のピックPに、それ自体がfmt.Stringerあるフィールドがある場合、それが動的フィールドであり、その値がnil場合、 Stringメソッドはパニックになります。 nil 。 nilインターフェースを、それ自体でさえも、何に対してもassertと入力することはできません。 https://play.golang.org/p/HMYglwyVblこれは常に真実ですが、定期的に発生するわけではありませんが、ピックを使用するとより定期的に発生する可能性があります。

ただし、合計タイプのクローズドな性質により、網羅性のリンターは、これが発生するすべての場所を見つけて(場合によっては誤検知が発生する可能性があります)、処理が必要なケースを報告できます。

ピックにメソッドを実装できる場合、それらのメソッドが型アサーションを満たすために使用されないことも驚くべきことです。

type Num pick { A int; B float32 }

func (n Num) String() string {
      switch v := n.[var] {
      case A:
          return fmt.Sprint(v)
      case B:
          return fmt.Sprint(v)
      }
}
...
n := Num{A: 5}
s1, ok := p.(fmt.Stringer) // ok == false
var i interface{} = p
s2, ok := i.(fmt.Stringer) // ok == true

タイプアサーションがインターフェイスを満たしている場合、現在のフィールドからメソッドをプロモートすることもできますが、これには、インターフェイス自体で定義されていないインターフェイスフィールドの値からメソッドをプロモートするかどうかなどの独自の問題が発生します(またはこれを効率的に実装する方法も)。 また、すべてのフィールドに共通のメソッドがピック自体にプロモートされることを期待するかもしれませんが、ピックがインターフェイスに格納されている場合は仮想ディスパッチに加えて、呼び出しごとにバリアント選択を介してディスパッチする必要があります、および/またはフィールドがインターフェースの場合は仮想ディスパッチへ。

編集:ちなみに、ピックを最適にパックすることは、最も短い一般的なスーパーストリング問題のインスタンスであり、NP完全ですが、一般的に使用される貪欲な近似があります。

ルールは、それがピック値である場合、タイプアサーションがピック値の動的フィールドにアサートしますが、ピック値がインターフェイスに格納されている場合、タイプアサーションはピックタイプのメソッドセットにあります。 最初は意外かもしれませんが、かなり一貫しています。

ピック値でタイプアサーションを許可するだけでドロップすることは問題ではありません。 ただし、すべてのケースを書き出したり、リフレクションを使用したりすることなく、ピック内のすべてのタイプが共有するメソッドを非常に簡単に宣伝できるので、残念です。

ただし、コード生成を使用して

func (p Pick) String() string {
  switch v := p.[var] {
  case A:
    return v.String()
  case B:
    return v.String()
  //etc
  }
}

先に進んで、型アサーションを削除しました。 たぶんそれらは追加されるべきですが、それらは提案の必要な部分ではありません。

@ianlancetaylorの以前のコメントに戻りたいと思います。エラー処理についてもう少し考えた後、新しい視点が得られたからです(具体的には、https://github.com/golang/go/issues/21161# issuecomment-320294933)。

特に、新しい種類のタイプは、インターフェイスタイプからは得られないものを私たちに与えますか?

私が見ているように、合計タイプの主な利点は、複数の値を返すことと、いくつかの値の1つを返すことを区別できることです。特に、これらの値の1つがエラーインターフェイスのインスタンスである場合はそうです。

現在、フォームの機能がたくさんあります

func F(…) (T, error) {
    …
}

io.Reader.Readやio.Reader.Writeなどの一部はTとerror返しますが、その他はTまたはerrorが、両方になることはありません。 以前のスタイルのAPIの場合、エラーが発生した場合にTを無視することは、多くの場合バグです(たとえば、エラーがio.EOF )。 後者のスタイルでは、ゼロ以外のT返すことがバグです。

lintを含む自動化ツールは、特定の関数の使用法をチェックして、エラーがnilでない場合に値が正しく無視される(または無視されない)ことを確認できますが、そのようなチェックは当然任意の関数に拡張されません。

たとえば、エラーがRequiredNotSetError場合、 proto.Marshalは「値とエラー」スタイルであることが意図されていますが、それ以外の場合は「値またはエラー」スタイルのようです。 型システムは2つを区別しないため、誤って回帰を導入する可能性があります。つまり、必要なときに値を返さないか、すべきでないときに値を返すかのどちらかです。 そして、 proto.Marshaler実装は、問題をさらに複雑にします。

一方、型を和集合として表現できれば、それについてもっと明確にすることができます。

type PartialMarshal struct {
    Data []byte // The marshalled value, ignoring unset required fields.
    MissingFields []string
}

func Marshal(pb Message) []byte | PartialMarshal | error

@ianlancetaylor 、私はあなたの提案を紙で遊んでいます。 以下の何かが間違っているかどうか教えていただけますか?

与えられた

var r interface{} restrict { uint, int } = 1

rの動的タイプはintであり、

var _ interface{} restrict { uint32, int32 } = 1

違法です。

与えられた

type R interface{} restrict { struct { n int }, etc }
type S struct { n int }

その場合、 var _ R = S{}は違法になります。

しかし与えられた

type R interface{} restrict { int, error }
type A interface {
  error
  foo()
}
type C struct { error }
func (C) foo() {}

var _ R = C{}とvar _ R = A(C{})両方が合法です。

両方

interface{} restrict { io.Reader, io.Writer }

と

interface{} restrict { io.Reader, io.Writer, *bytes.Buffer }

同等です。

同じく、

interface{} restrict { error, net.Error }

と同等です

interface { Error() string }

与えられた

type IO interface{} restrict { io.Reader, io.Writer }
type R interface{} restrict {
  interface{} restrict { int, uint },
  IO,
}

その場合、基になるタイプのRは次のようになります。

interface{} restrict { io.Writer, uint, io.Reader, int }

編集:イタリック体の小さな修正

@jimmyfrasche私が上で書いたのは提案だったとまでは言いません。 それはアイデアのようなものでした。 私はあなたのコメントについて考えなければならないでしょう、しかし一見彼らはもっともらしいように見えます。

@jimmyfrascheの提案は、ピックタイプがGoで動作することを直感的に期待する方法とほぼ同じです。 タグ値がゼロから始まる場合、ピックのゼロ値に最初のフィールドのゼロ値を使用するという彼の提案は、「ゼロ値はバイトをゼロにすることを意味する」と直感的であることに特に注意する価値があると思います(おそらくこれすでに注目されています;このスレッドは今では非常に長いです...)。 また、パフォーマンスへの影響(不要な割り当てがない)と、ピックがインターフェイスに完全に直交していること(インターフェイスを含むピックで驚くような動作が切り替わることはありません)も気に入っています。

変更を検討する唯一のことは、タグを変更することです。 foo.X = 0は、 foo = Foo{X: 0}です。 さらに数文字ですが、タグをリセットして値をゼロにすることをより明確に示しています。 これはマイナーな点であり、彼の提案がそのまま受け入れられれば、私はまだ非常に嬉しいです。

@ ns-cweberありがとうございますが、ゼロ値の動作を信用することはできません。 アイデアはしばらくの間浮かんでいて、このスレッドの前半で出された@rogpeppeの提案にありました(あなたがかなり長い間指摘しているように)。 私の正当化はあなたが与えたものと同じでした。

foo.X = 0対foo = Foo{X: 0} 、私の提案では実際には両方が許可されています。 あなたが行うことができますので、ピックのフィールドが構造体の場合は後者が便利ですfoo.X.Y = 0の代わりにfoo = Foo{X: image.Point{X: foo.[X].X, 0}}冗長であることに加えて、実行時に失敗する可能性があります。

また、セマンティクスのエレベータピッチを強化するため、そのままにしておくと役立つと思います。これは、一度に1つのフィールドしか設定できない構造体です。

それがそのまま受け入れられるのを妨げる可能性があることの1つは、構造体にピックを埋め込むことがどのように機能するかです。 先日、構造体の使用に及ぼすさまざまな影響について詳しく説明しました。 私はそれが修理可能だと思いますが、最良の修理が何であるか完全にはわかりません。 最も単純なのは、メソッドのみを継承し、埋め込まれたピックを名前で直接参照してフィールドにアクセスする必要があることです。構造体が構造体フィールドとピックフィールドの両方を持つことを避けるために、私はそれに傾倒しています。

@jimmyfrascheゼロ値の動作について修正していただきありがとうございます。 私はあなたの提案が両方のミューテーターを考慮に入れていることに同意します、そしてあなたのエレベーターピッチポイントは良いものだと思います。 あなたの提案に対するあなたの説明は理にかなっていますが、私はfoo.XYを設定しているのを見ることができましたが、それが自動的に選択フィールドを変更することに気づいていませんでした。 あなたの提案が成功すれば、そのわずかな予約があっても、私はまだ前向きに喜んでいます。

最後に、ピック埋め込みの簡単な提案は、私が直感的に理解できるもののようです。 気が変わっても、既存のコードを壊さずに単純な提案から複雑な提案に移行することはできますが、その逆は当てはまりません。

@ ns-cweber

自分がfoo.XYを設定しているのを見ることができましたが、ピックフィールドが自動的に変更されることに気づいていませんでした。

それは公正な点ですが、そのことに関しては、その言語、または任意の言語で多くのことについてそれを成し遂げることができます。 一般的に、囲碁には安全レールがありますが、安全はさみはありません。

あなたがそれらを破壊するためにあなたの邪魔にならないならば、それがあなたを一般的に保護する多くの大きなものがあります、しかしあなたはそれでもあなたが何をしているのかを知る必要があります。

このように間違えると煩わしいかもしれませんが、おお、「 bar.X = 0を設定しましたが、 bar.Y = 0を設定するつもりでした」と大差ありません。そのfooはピックタイプです。

同様に、 i.Foo() 、 p.Foo() 、およびv.Foo()すべて同じように見えますが、 iがnilインターフェイスの場合、 pはnilポインターであり、 Fooはそのケースを処理しません。最初の2つはパニックになる可能性がありますが、 vがvalueメソッドレシーバーを使用する場合はできませんでした(少なくとも呼び出し自体からではありません)。 。

—

埋め込みに関しては、後で緩めやすいのが良い点なので、先に進んで提案を編集しました。

合計タイプには、多くの場合、値のないフィールドがあります。 たとえば、 database/sqlパッケージには、次のものがあります。

type NullString struct {
    String string
    Valid  bool // Valid is true if String is not NULL
}

合計タイプ/ピック/共用体がある場合、これは次のように表されます。

type NullString pick {
  Null   struct{}
  String string
}

この場合、sumタイプには構造体よりも明らかな利点があります。 これは十分に一般的な使用法であるため、提案に例として含める価値があると思います。

Bikeshedding(申し訳ありません)、これは構文サポートと構造体フィールド埋め込み構文との不一致の価値があると私は主張します:

type NullString union {
  Null
  String string
}

@neild

最初に最後のポイントを打つ:投稿する前の最後の変更として(厳密には必須ではありません)、フィールド名のない名前付きタイプ(または名前付きタイプへのポインター)がある場合、ピックは暗黙のフィールドを作成することを追加しましたタイプと同じ名前です。 それは最善のアイデアではないかもしれませんが、大騒ぎせずに「これらのタイプのいずれか」の一般的なケースの1つをカバーするように見えました。 あなたの最後の例を書くことができるとすると:

type Null = struct{} //though this puts Null in the same scope as NullString
type NullString pick {
  Null
  String string
}

ただし、要点に戻ると、それは優れた使用法です。 実際、これを使用して列挙型を作成できます: type Stoplight pick { Stop, Slow, Go struct{} } 。 これは、const / iotafaux-enumによく似ています。 同じ出力にコンパイルすることもできます。 この場合の主な利点は、状態を表す数値が完全にカプセル化されており、リストされている3つ以外の状態にすることができないことです。

残念ながら、 Stoplight値を作成および設定するための構文がやや厄介であり、この場合は悪化します。

light := Stoplight{Slow: struct{}{}}
light.Go = struct{}{}

他の場所で提案されているように、 {}または_をstruct{}{}省略形にすることを許可すると役立ちます。

多くの言語、特に関数型言語は、ラベルを型と同じスコープに配置することでこれを回避します。 これにより、多くの複雑さが生じ、同じスコープで定義された2つのピックがフィールド名を共有できなくなります。

ただし、フィールドの型を引数として取るピック内の各フィールドの同じ名前で関数を作成するコードジェネレーターを使用すると、これを回避するのは簡単です。 また、特別な場合として、型のサイズがゼロの場合に引数を取らなかった場合、 Stoplight例の出力は次のようになります。

func Stop() Stoplight {
  return Stoplight{Stop: struct{}{}}
}
func Slow() Stoplight {
  return Stoplight{Slow: struct{}{}}
}
func Go() Stoplight {
  return Stoplight{Go: struct{}{}}
}

NullString例では、次のようになります。

func Null() NullString {
  return NullString{Null: struct{}{}}
}
func String(s string) NullString {
  return NullString{String: s}
}

きれいではありませんが、 go generate離れており、非常に簡単にインライン化できます。

タイプ名に基づいて暗黙のフィールドを作成した場合(タイプが他のパッケージからのものでない限り)、またはフィールド名を共有する同じパッケージ内の2つのピックで実行された場合は機能しませんが、問題ありません。 この提案は、箱から出してすべてを行うわけではありませんが、多くのことを可能にし、プログラマーに特定の状況に最適なものを決定する柔軟性を提供します。

より多くの構文bikeshedding:

type NullString union {
  Null
  Value string
}

var _ = NullString{Null}
var _ = NullString{Value: "some value"}
var _ = NullString{Value} // equivalent to NullString{Value: ""}.

具体的には、キーを含まない要素リストを持つリテラルは、設定するフィールドに名前を付けるものとして解釈されます。

これは、複合リテラルの他の使用法と構文的に矛盾します。 一方、キーのない共用体初期化子の賢明な解釈はないため、(少なくとも私には)共用体/ピック/合計型のコンテキストでは賢明で直感的に見える使用法です。

@neild

これは、複合リテラルの他の使用法と構文的に矛盾します。

それは文脈上は理にかなっていますが、それは私には大きなネガティブのように思えます。

また、注意してください

var ns NullString // == NullString{Null: struct{}{}} == NullString{}
ns.String = "" // == NullString{String: ""}

map[T]struct{}を使用しているときにstruct{}{}するために

var set struct{}

どこかでtheMap[k] = setを使用すると、同様にピックで機能します

さらなるバイクシェディング:(合計タイプのコンテキストでの)空のタイプは、従来、「null」ではなく「unit」と呼ばれていました。

@bcmillsSorta 。

関数型言語では、sum型を作成すると、そのラベルは実際にはその型の値を作成する関数になります(ただし、コンパイラがパターンマッチングを可能にするために知っている「型構築子」または「タイコン」と呼ばれる特別な関数です)。

data Bool = False | True

データ型Boolと、同じスコープ内に2つの関数TrueとFalse 、それぞれに署名() -> Boolます。

ここで、 ()は、タイプ発音単位(単一の値のみを持つタイプ)の記述方法です。 Goでは、このタイプはさまざまな方法で記述できますが、慣例的にstruct{}記述されます。

したがって、コンストラクターの引数の型はユニットと呼ばれます。 コンストラクターの名前の規則は、このようなオプションタイプとして使用される場合、通常Noneが、ドメインに合わせて変更できます。 たとえば、値がデータベースからのものである場合、 Nullは適切な名前になります。

@bcmills

私が見ているように、合計タイプの主な利点は、複数の値を返すことと、いくつかの値の1つを返すことを区別できることです。特に、これらの値の1つがエラーインターフェイスのインスタンスである場合はそうです。

別の見方をすれば、これはGoの合計型の主な欠点だと思います。

もちろん、多くの言語は、値やエラーを返す場合に正確に合計型を使用します。これは、それらの言語に適しています。 合計タイプがGoに追加された場合、同じ方法でそれらを使用したいという大きな誘惑があります。

ただし、Goには、この目的のために複数の値を使用するコードの大規模なエコシステムがすでにあります。 新しいコードが合計型を使用して(値、エラー)タプルを返す場合、そのエコシステムは断片化されます。 一部の作成者は、既存のコードとの一貫性を保つために、引き続き複数のリターンを使用します。 一部の作成者は合計タイプを使用します。 既存のAPIを変換しようとする人もいます。 何らかの理由で古いGoバージョンに固執した作成者は、新しいAPIからロックアウトされます。 それは混乱するでしょう、そして私は利益がコストの価値があるようになり始めるとは思いません。

新しいコードが合計型を使用して(値、エラー)タプルを返す場合、そのエコシステムは断片化されます。

Go 2で合計型を追加し、それらを均一に使用すると、問題は断片化ではなく移行の1つになります。つまり、Go 1(値、エラー)APIをGo 2(値|エラー)に変換できる必要があります。 )APIおよびその逆。ただし、プログラムのGo2部分では異なるタイプである可能性があります。

Go 2で合計型を追加し、それらを均一に使用する場合

これは、これまでに見たものとはまったく異なる提案であることに注意してください。標準ライブラリを大幅にリファクタリングする必要があり、APIスタイル間の変換を定義する必要があります。このルートに進むと、これは非常に大きくなります。合計タイプの設計に関するマイナーなコディシルを使用したAPI移行の複雑な提案。

Go1とGo2が同じプロジェクトでシームレスに共存できるようにすることを目的としているため、誰かが「何らかの理由で」Go 1コンパイラでスタックし、 2ライブラリに移動します。 ただし、依存関係があればAに順番に依存するB 、およびBのような新機能を使用するためにアップデートpickそのAPIには、その新しいバージョンのBを使用するように更新しない限り、依存関係Aが壊れます。 Aは、ベンダーBで古いバージョンを使い続けることができますが、セキュリティバグなどのために古いバージョンが維持されていない場合、または新しいバージョンを使用する必要がある場合B直接使用しているため、何らかの理由でプロジェクトに2つのバージョンを含めることができないため、問題が発生する可能性があります。

最終的に、ここでの問題は言語バージョンとはほとんど関係がなく、既存のエクスポートされた関数の署名の変更と関係があります。 それが推進力を提供する新しい機能になるという事実は、それから少し気を散らすものです。 下位互換性を損なうことなく既存のAPIを変更してpickを使用できるようにすることが目的の場合は、何らかのブリッジ構文が必要になる可能性があります。 例(完全にストローマンとして):

type ReadResult pick(N int, Err error) {
    N
    PartialResult struct { N; Err }
    Err
}

コンパイラーは、レガシーコードからアクセスされたときに、特定のバリアントにフィールドが存在しない場合はゼロ値を使用して、 ReadResultスプラットすることができます。 逆の方法や、それだけの価値があるかどうかはわかりません。 template.MustようなAPIは、 pickではなく、複数の値を受け入れ続け、違いを補うためにスプラッティングに依存する必要がある場合があります。 または、次のようなものを使用できます。

type ReadResult pick(N int, Err error) {
case Err == nil:
    N
default:
    PartialResult struct { N; Err }
case N == 0:
    Err
}

これは、複雑なことを行いますが、私はAPIが書かれるべき方法を変更機能を導入することは、世界を壊すことなく移行する方法についての物語を必要とどのように見ることができます。 ブリッジ構文を必要としない方法があるかもしれません。

合計型から製品型(構造体、複数の戻り値)に移行するのは簡単です。値以外のすべてをゼロに設定するだけです。 製品タイプから合計タイプへの移行は、一般的に明確に定義されていません。

APIが製品タイプベースの実装から合計タイプベースの実装にシームレスかつ段階的に移行する場合、最も簡単なルートは、合計タイプバージョンに実際の実装があり、製品タイプバージョンが合計型バージョン。ランタイムチェックに必要なものと、製品スペースへの投影を実行します。

それは本当に抽象的なので、ここに例があります

合計なしのバージョン1

func Take(i interface{}) error {
  switch i.(type) {
  case int: //do something
  case string:
  default: return fmt.Errorf("invalid %T", i)
  }
}
func Give() (interface{}, error) {
   i := f() //something
   if i == nil {
     return nil, errors.New("whoops v:)v")
  }
  return i
}

合計のあるバージョン2

type Value pick {
  I int
  S string
}
func TakeSum(v Value) {
  // do something
}
// Deprecated: use TakeSum
func Take(i interface{}) error {
  switch x := i.(type) {
  case int: TakeSum(Value{I: x})
  case string: TakeSum(Value{S: x})
  default: return fmt.Errorf("invalid %T", i)
  }
}
type ErrValue pick {
  Value
  Err error
}
func GiveSum() ErrValue { //though honestly (Value, error) is fine
  return f()
}
// Deprecated: use GiveSum
func Give() (interface{}, error) {
  switch v := GiveSum().(var) {
  case Value:
    switch v := v.(var) {
    case I: return v, nil
    case S: return v, nil
    }
  case Err:
    return nil, v
  }
}

バージョン3はGive / Takeを削除します

バージョン4は、GiveSum / TakeSumの実装をGive / Takeに移動し、GiveSum / TakeSumをGive / Takeを呼び出すだけで、GiveSum / TakeSumを廃止します。

バージョン5はGiveSum / TakeSumを削除します

それはきれいでも速くもありませんが、同様の性質の他の大規模な混乱と同じであり、言語から余分なものを必要としません

合計型の(ほとんどの)ユーティリティは、コンパイル時に型インターフェイス{}の型への代入を制約するメカニズムで実現できると思います。

私の夢では、次のようになります。

type T1 switch {T2,T3} // only nil, T2 and T3 may be assigned to T1
type T2 struct{}
type U switch {} // only nil may be assigned to U
type V switch{interface{} /* ,... */} // V equivalent to interface{}
type Invalid switch {T2,T2} // only uniquely named types
type T3 switch {int,uint} // switches can contain switches but... 

...スイッチタイプが明示的に定義されていないタイプであるとアサートすると、コンパイル時エラーにもなります。

var t1 T1
i,ok := t1.(int) // T1 can't be int, only T2 or T3 (but T3 could be int)
switch t := t1.(type) {
    case int: // compile error, T1 is just nil, T2 or T3
}

そして、獣医はT3のようなタイプへのあいまいな定数の割り当てについて鯉を振るうでしょうが、すべての意図と目的のために(実行時) var x T3 = 32はvar x interface{} = 32ます。 たぶん、スイッチやポニーのような名前のパッケージに組み込まれているビルトインのいくつかの事前定義されたスイッチタイプもかっこいいでしょう。

@ j7b、@ianlancetaylorはで同様のアイデアを提供しhttps://github.com/golang/go/issues/19412#issuecomment -323256891

後でこれの論理的帰結になると思うものをhttps://github.com/golang/go/issues/19412#issuecomment-325048452に投稿しました。

それらの多くは、類似性を考えると等しく適用されるようです。

そのようなことがうまくいけば本当に素晴らしいでしょう。 インターフェイスからインターフェイス+制限に簡単に移行できます(特にIanの構文では、インターフェイスで構築された既存の疑似合計の最後にrestrictを追加するだけです)。 実行時にそれらは基本的にインターフェースと同一であり、ほとんどの作業は、不変条件が壊れたときにコンパイラーに追加のエラーを発行させるだけなので、実装は簡単です。

しかし、それを機能させることは不可能だと思います。

とても近く、それはフィットのように見えますが、あなたはズームインしてあなたがずれて、それを少しプッシュして、何か他のポップを与えるので、それだけで、かなり右でないことをすべてラインアップ。 あなたはそれを修復しようとすることができますが、それからあなたはインターフェースによく似ているが奇妙な場合には異なった振る舞いをする何かを手に入れます。

多分私は何かが欠けています。

ケースが必ずしも互いに素である必要がない限り、制限されたインターフェイスの提案に問題はありません。 2つのインターフェースタイプ( io.Reader / io.Writer )間の結合が互いに素でないことは、あなたほど驚くべきことではないと思います。 interface{}割り当てられた値が、両方を実装している場合、 io.Readerまたはio.Writerとして格納されているかどうかを判断できないという事実と完全に一致しています。 それぞれのケースが具体的なタイプである限り、非交和を構築できるという事実は完全に適切であるように思われます。

トレードオフは、ユニオンが制限されたインターフェースである場合、それらに直接メソッドを定義できないことです。 また、インターフェースタイプが制限されている場合、 pickタイプが提供する保証された直接ストレージを取得できません。 これらの追加の利点を得るために言語に別の種類のものを追加する価値があるかどうかはわかりません。

@jimmyfrasche for type T switch {io.Reader,io.Writer} ReadWriterをTに割り当てることは問題ありませんが、Tがio.ReaderまたはIo.Writerであるとしかアサートできません。io.Readerまたはio.Writerをアサートするには別のアサーションが必要です。有用なアサーションである場合は、スイッチタイプに追加することを推奨するReadWriter。

@stevenblenkinsopメソッドなしでピック提案を定義できます。 実際、メソッドと暗黙のフィールド名を削除すると、ピックの埋め込みを許可できます。 (明らかに私は、メソッドと、それほどではないが暗黙のフィールド名が、そこでのより有用なトレードオフであると思います)。

そして、その一方で、 @ ianlancetaylorの構文は

type IR interface {
  Foo()
  Bar()
} restrict { A, B, C }

A 、 B 、およびCそれぞれFooおよびBarメソッドを持っている限りコンパイルされます(ただし、心配する必要があります)約nil値)。

編集:斜体での説明

何らかの形式の_restrictedinterface_が役立つと思いますが、構文に同意しません。 これが私が提案していることです。 これは代数的データ型と同じように機能し、必ずしも共通の動作をする必要のないドメイン関連のオブジェクトをグループ化します。

//MyGroup can be any of these. It can contain other groups, interfaces, structs, or primitive types
type MyGroup group {
   MyOtherGroup
   MyInterface
   MyStruct
   int
   string
   //..possibly some other types as well
}

//type definitions..
type MyInterface interface{}
type MyStruct struct{}
//etc..

func DoWork(item MyGroup) {
   switch t:=item.(type) {
      //do work here..
   }
}

このアプローチには、従来の空のインターフェイスinterface{}アプローチに比べていくつかの利点があります。

  • 関数使用時の静的型チェック
  • ユーザーは、関数の実装を見なくても、関数のシグネチャだけからどのタイプの引数が必要かを推測できます。

空のインターフェースinterface{}は、関係するタイプの数が不明な場合に役立ちます。 ここでは、実行時の検証に依存する以外に選択肢はありません。 一方、型の数が限られていて、コンパイル時にわかっている場合は、コンパイラに支援してもらいませんか?

@henryasより有用な比較は、合計タイプを(開く)行うために現在推奨されている方法だと思います:空でないインターフェース(明確なインターフェースを抽出できない場合は、エクスポートされていないマーカー関数を使用)。
私はあなたの議論がそれに重要な意味で当てはまるとは思わない。

Goprotobufsに関するエクスペリエンスレポートは次のとおりです。

  • proto2構文では、「オプション」フィールドを使用できます。これは、ゼロ値と未設定値が区別されるタイプです。 現在の解決策は、ポインター(たとえば、 *int )を使用することです。ここで、nilポインターは未設定を示し、setポインターは実際の値を指します。 欲求は、値にアクセスするだけでよいという一般的なケースを複雑にすることなく、ゼロと未設定の区別を可能にするアプローチです(ゼロ値は未設定の場合は問題ありません)。

    • これは、追加の割り当てのためにパフォーマンスが低下します(ただし、実装によっては、組合が同じ運命に苦しむ可能性があります)。
    • ポインタを常にチェックする必要があると読みやすさが損なわれるため、これはユーザーにとって苦痛です(ただし、protosのデフォルト値がゼロ以外の場合は、チェックする必要があることを意味する場合があります...)。
  • 祖語は、合計型の祖語バージョンである「oneofs」を許可します。 現在採用されているアプローチは次のとおりです(大まかな例)。

    • 非表示のメソッドを使用してインターフェースタイプを定義します(例: type Communique_Union interface { isCommunique_Union() } )
    • ユニオンで許可されている可能なGoタイプごとに、ラッパー構造体を定義します。ラッパー構造体は、許可されている各タイプ( type Communique_Number struct { Number int32 } )をラップすることだけを目的としています。各タイプにはisCommunique_Unionメソッドがあります。
    • ラッパーが割り当てを引き起こすため、これもパフォーマンスが低下します。 最大値(スライス)が占めるのは24B以下であることがわかっているため、合計タイプが役立ちます。

@henryasより有用な比較は、合計タイプを(開く)行うために現在推奨されている方法だと思います:空でないインターフェース(明確なインターフェースを抽出できない場合は、エクスポートされていないマーカー関数を使用)。
私はあなたの議論がそれに重要な意味で当てはまるとは思わない。

次のように、オブジェクトにダミーのエクスポートされていないメソッドを追加して、オブジェクトをインターフェイスとして渡すことができるようにすることを意味しますか?

type MyInterface interface {
   belongToMyInterface() //dummy method definition
}

type MyObject struct{}
func (MyObject) belongToMyInterface(){} //dummy method

私はそれがまったく推奨されるべきではないと思います。 これは、解決策というよりは回避策のようなものです。 私は個人的に、空のメソッドや不要なメソッド定義をあちこちに置くのではなく、静的型の検証をやめたいと思っています。

_ダミーメソッド_アプローチの問題は次のとおりです。

  • 不要なメソッドとメソッド定義がオブジェクトとインターフェースを乱雑にします。
  • 新しい_group_が追加されるたびに、オブジェクトの実装を変更する必要があります(たとえば、ダミーメソッドの追加)。 これは間違っています(次のポイントを参照)。
  • 代数的データ型(または動作ではなく_domain_に基づくグループ化)はドメイン固有です。 ドメインによっては、オブジェクトの関係を異なる方法で表示する必要がある場合があります。 会計士は、倉庫管理者とは異なる方法でドキュメントをグループ化します。 このグループ化は、オブジェクト自体ではなく、オブジェクトのコンシューマーに関係します。 オブジェクトは、コンシューマーの問題について何も知る必要はなく、知る必要もありません。 請求書は会計について何か知る必要がありますか? そうでない場合、会計規則に変更があるたびに(たとえば、新しいドキュメントのグループ化を適用する)_、なぜ請求書はその実装を変更する必要があるのですか? _dummy method_アプローチを使用することにより、オブジェクトをコンシューマーのドメインに結合し、コンシューマーのドメインについて重要な仮定を立てます。 これを行う必要はありません。 これは、空のインターフェイスinterface{}アプローチよりもさらに悪いです。 利用可能なより良いアプローチがあります。

@henryas

私はあなたの3番目のポイントを強い議論とは見ていません。 会計士がオブジェクトの関係を別の方法で表示したい場合、会計士は仕様に合った独自のインターフェースを作成できます。 プライベートメソッドをインターフェイスに追加しても、それを満たす具象型が他の場所で定義されているインターフェイスのサブセットと互換性がないという意味ではありません。

Goパーサーはこの手法を多用しており、正直なところ、ピックを言語で実装する必要があるほどパッケージを改善するピックを想像することはできません。

@as私のポイントは、新しい_relationshipビュー_が作成されるたびに、関連する具象オブジェクトを更新して、このビューに確実に対応できるようにする必要があるということです。 それを行うために、オブジェクトはしばしば消費者のドメインについて特定の仮定をしなければならないので、それは間違っているように思われます。 Goパーサーの場合のように、オブジェクトとコンシューマーが密接に関連しているか、同じドメイン内に存在する場合、それはそれほど重要ではない可能性があります。 ただし、オブジェクトが他のいくつかのドメインによって消費される基本的な機能を提供する場合、それは問題になります。 オブジェクトは、_dummy method_アプローチが機能するために、他のすべてのドメインについて少し知る必要があります。

オブジェクトに多くの空のメソッドがアタッチされてしまいますが、それらを必要とするインターフェイスは別のドメイン/パッケージ/レイヤーに存在するため、なぜこれらのメソッドが必要なのかは読者にはわかりません。

インターフェースを介したオープンサムアプローチでは簡単に合計を使用できないという点は十分に公平です¹。 明示的なsum-typeを使用すると、合計が簡単になります。 ただし、「合計型は型安全性を提供する」とは非常に異なる議論です。必要に応じて、今日でも型安全性を取得できます。

ただし、他の言語で実装されているクローズドサムには2つの欠点があります。1つは、大規模な分散開発プロセスでそれらを進化させることの難しさです。 そして2つ目は、型システムに力を加えると思います。Goにはそれほど強力な型システムがないのが好きです。それは、型のコーディングを思いとどまらせ、代わりにプログラムをコーディングするからです。より強力な型システムでは、より強力な言語(HaskellやRustなど)に移行します。

そうは言っても、少なくとも2つ目は間違いなく好みのひとつであり、同意したとしても、マイナス面がプラス面を上回っていると見なされるかどうかは、個人的な好み次第です。 ただ指摘したかったのですが、閉じた合計型がないと型安全な合計を取得できないということは、実際には真実ではありません:)

[1]特に、それは簡単ではありませんが、それでも可能です。

type Node interface {
    node()
}

type Foo struct {
    bar.Baz
}

func (foo) node() {}

@Merovius
私はあなたの2番目の欠点に同意しません。 標準ライブラリには、合計型から多大な恩恵を受ける場所がたくさんありますが、現在は空のインターフェイスとパニックを使用して実装されているという事実は、この欠如がコーディングに悪影響を及ぼしていることを示しています。 もちろん、そのようなコードはそもそも書かれているので問題はなく、合計型は必要ないと言う人もいるかもしれませんが、その論理の愚かさは、関数に他の型は必要ないということです。署名。代わりに空のインターフェイスを使用する必要があります。

現在、合計タイプを表すためにいくつかのメソッドでインターフェースを使用することに関して、1つの大きな欠点があります。 それらは暗黙的に実装されているため、そのインターフェイスに使用できるタイプがわかりません。 適切な合計型を使用すると、型自体が実際に使用できる型を正確に記述します。

私はあなたの2番目の欠点に同意しません。

「合計型は型を使用したプログラミングを促進する」というステートメントに同意しませんか、それとも欠点であることに同意しませんか? あなたが最初のものに同意していないように思われるので(あなたのコメントは基本的にそれの単なる再主張です)そして2番目に関しては、私はそれが上記の好み次第であることを認めました。

標準ライブラリには、合計型から多大な恩恵を受ける場所がたくさんありますが、現在は空のインターフェイスとパニックを使用して実装されているという事実は、この欠如がコーディングに悪影響を及ぼしていることを示しています。 もちろん、そのようなコードはそもそも書かれているので問題はなく、合計型は必要ないと言う人もいるかもしれませんが、その論理の愚かさは、関数に他の型は必要ないということです。署名。代わりに空のインターフェイスを使用する必要があります。

このタイプの白黒の議論は実際には役に立ちません。 私は、合計タイプが場合によっては痛みを軽減することに同意します。 型システムをより強力にするすべての変更は、場合によっては痛みを軽減しますが、場合によっては痛みも

議論は、Python風の型システム(型なし)またはcoq風の型システム(すべての正しさの証明)が必要かどうかについてではありません。 議論は「合計型の利点が欠点を上回っているか」である必要があり、両方を認めることは役に立ちます。


FTR、個人的には、オープンサムタイプ(つまり、すべてのサムタイプに暗黙的または明示的な「SomethingElse」ケースがある)に反対することはないことを強調したいと思います。これにより、の技術的な欠点のほとんどが軽減されます。それら(ほとんどの場合、それらは進化するのが難しい)でありながら、それらの技術的な利点のほとんどを提供します(静的型チェック、あなたが言及したドキュメント、他のパッケージから型を列挙できます…)。

ただし、オープンサムは、a)通常、合計タイプを要求する人々にとって満足のいく妥協ではなく、b)Goチームによる包含を保証するのに十分な大きなメリットとは見なされない可能性があると思います。 しかし、私はこれらの仮定のいずれかまたは両方で間違っていることが証明される準備ができています:)

もう1つの質問:

標準ライブラリには、合計型から多大な恩恵を受ける場所がたくさんあるという事実

私は標準ライブラリの2つの場所しか考えられません。そこでは、それらに重要な利点があると思います。それは、リフレクトとゴー/アストです。 そして、そこにさえ、パッケージはそれらなしでうまく機能するようです。 この基準点から、「たっぷり」と「非常に」という言葉は誇張されているように見えますが、もちろん、正当な場所がたくさんあるとは思えないかもしれません。

database/sql/driver.Valueは、合計型であることが有益な場合があります(#23077に記載されています)。
https://godoc.corp.google.com/pkg/database/sql/driver#Value

ただし、 database/sql.Rows.Scanのよりパブリックなインターフェイスでは、機能が失われることはありません。 スキャンは、基になる型がint値に読み込むことができます。 宛先パラメーターを合計型に変更するには、入力を有限の型のセットに制限する必要があります。
https://godoc.corp.google.com/pkg/database/sql#Rows.Scan

@Merovius

オープンサムタイプ(つまり、すべてのサムタイプには暗黙的または明示的な「SomethingElse」ケースがあります)とは対照的ではありません。技術的な欠点のほとんどが軽減されるためです(ほとんどの場合、進化が困難です)。

クローズドサムの「進化するのが難しい」問題を軽減する他のオプションが少なくとも2つあります。

1つは、実際には合計の一部ではないタイプでの一致を許可することです。 次に、合計にメンバーを追加するには、最初にそのコンシューマーを更新して新しいメンバーと照合し、コンシューマーが更新された後でのみ実際にそのメンバーを追加します。

もう1つは、「不可能な」メンバーを許可することです。つまり、一致では明示的に許可されているが、実際の値では明示的に許可されていないメンバーです。 合計にメンバーを追加するには、最初にそれを不可能なメンバーとして追加し、次にコンシューマーを更新し、最後に新しいメンバーを可能なように変更します。

database/sql/driver.Valueは、合計型であることが有益な場合があります

同意しました、それについて知りませんでした。 ありがとう :)

1つは、実際には合計の一部ではないタイプでの一致を許可することです。 次に、合計にメンバーを追加するには、最初にそのコンシューマーを更新して新しいメンバーと照合し、コンシューマーが更新された後でのみ実際にそのメンバーを追加します。

興味深いソリューション。

@Meroviusインターフェースは、本質的に無限和型のファミリーです。 すべての合計タイプは、無限であろうとなかろうと、 default:場合があります。 ただし、有限合計タイプがない場合、 default:は、それについて知らなかった有効なケース、またはプログラムのどこかにバグがある無効なケースのいずれかを意味します。有限合計の場合、前者のみであり、後者はありません。

json.Tokenおよびsql.Null *タイプは、他の標準的な例です。 go / typesは、go / astと同じようにメリットがあります。 内部状態のドメインを制限することで、複雑な配管のデバッグとテストが簡単になる、エクスポートされたAPIにはない例がたくさんあると思います。 これらは、一般的なライブラリのパブリックAPIではあまり発生しない内部状態やアプリケーションの制約に最も役立つと思いますが、そこでも時々使用されます。

個人的には、合計タイプはGoに十分な追加のパワーを与えますが、あまり多くはないと思います。 Go型システムには欠点がありますが、すでに非常に優れていて柔軟性があります。 型システムへのGo2の追加は、すでに存在するものほど多くの電力を提供することはありません。必要なものの80〜90%はすでに配置されています。 つまり、ジェネリックスでさえ、基本的に新しいことを実行できるわけではありません。つまり、すでに実行していることを、より安全に、より簡単に、よりパフォーマンス的に、より優れたツールを可能にする方法で実行できるようになります。 合計型は似ていますが、imoです(ただし、それが1つまたは他のジェネリックである場合は、明らかに優先されます(そして、それらはかなりうまくペアになります))。

合計タイプのスイッチで無関係なデフォルトを許可し(すべてのケース+デフォルトを許可)、コンパイラーに網羅性を強制させない場合(リンターは可能ですが)、合計にケースを追加するのは同じくらい簡単です(そして同じくらい難しいです) )他のパブリックAPIを変更する場合。

json.Tokenおよびsql.Null *タイプは、他の標準的な例です。

トークン-確かに。 AST問題の別のインスタンス(基本的に、すべてのパーサーは合計タイプの恩恵を受けます)。

ただし、sql.Null *のメリットはわかりません。 ジェネリックがない場合(または「魔法の」ジェネリックオプションビルトインを追加する場合)でも、型が必要であり、 type NullBool enum { Invalid struct{}; Value Int }とtype NullBool struct { Valid bool; Value Int }間に大きな違いはないようです。 はい、違いがあることは承知していますが、それはほとんどありません。

合計タイプのスイッチで無関係なデフォルトを許可し(すべてのケース+デフォルトを許可)、コンパイラーに網羅性を強制させない場合(リンターは可能ですが)、合計にケースを追加するのは同じくらい簡単です(そして同じくらい難しいです) )他のパブリックAPIを変更する場合。

上記を参照。 これらは私がオープンサムと呼んでいるものであり、私はそれらに反対していません。

これらは私がオープンサムと呼んでいるものであり、私はそれらに反対していません。

私の具体的な提案はhttps://github.com/golang/go/issues/19412#issuecomment-323208336であり、オープンの定義を満たす可能性があると思いますが、まだ少しラフであり、まだまだあると確信しています。取り外して磨きます。 特に、すべてのケースがリストされていても、デフォルトのケースが許容可能であることが明確でないことに気づいたので、それを更新しました。

オプション型は合計型のキラーアプリではないことに同意しました。 しかし、それらは非常に優れており、ジェネリックスで指摘しているように、

type Nullable(T) pick { // or whatever syntax (on all counts)
  Null struct{}
  Value T
}

一度、すべてのケースをカバーするのは素晴らしいことです。 しかし、あなたも指摘しているように、一般的な製品(構造体)でも同じことができます。 Valid = false、Value!= 0の無効な状態があります。そのシナリオでは、1 + Tほど小さくなくても、2⨯Tが小さいため、問題が発生している場合は簡単に根絶できます。

もちろん、多くのケースと多くの重複する不変条件を含むより複雑な合計である場合、防御的なプログラミングでも間違いを犯しやすく、間違いを発見するのが難しくなります。したがって、不可能なものをまったくコンパイルしないようにすると、多くの髪を節約できます。引っ張る。

トークン-確かに。 AST問題の別のインスタンス(基本的に、すべてのパーサーは合計タイプの恩恵を受けます)。

私はいくつかの入力を受け取り、いくつかの処理を行い、いくつかの出力を生成する多くのプログラムを作成します。通常、これを再帰的に多くのパスに分割し、入力をケースに分割し、それらのケースに基づいて変換します。必要な出力。 私は文字通りパーサーを書いているわけではないかもしれませんが(確かにそれが楽しいからです!)、ASTの問題は、多くのコードに当てはまります。特に、奇妙なものが多すぎる厄介なビジネスロジックを扱う場合はそうです。私の小さな頭に収まる要件とエッジケース。

一般的なライブラリを作成しているときは、ETLを実行したり、空想的なレポートを作成したり、状態XのユーザーがZとマークされていない場合にアクションYが発生することを確認したりするほど、APIに表示されません。一般的なライブラリですが、10分のデバッグを1秒に短縮しただけでも、内部状態を制限できると役立つ場所が見つかりました。「コンパイラが間違っていると言った」。

特にGoの場合、合計タイプを使用する1つの場所は、1つのゴルーチンに3つのチャンを、別のゴルーチンに2つのチャンを与える必要があるチャネルの束を選択するゴルーチンです。 chan stuct { kind MsgKind; a A; b B; c C }はできますが、 chan A 、 chan B 、 chan C超えてchan pick { a A; b B; c C }を使用できるようになるために何が起こっているのかを追跡するのに役立ちます余分なスペースと少ない検証を犠牲にして、ピンチで仕事をします。

新しいタイプの代わりに、既存のインターフェイスタイプスイッチ機能への追加としてのコンパイル時のタイプリストチェックはどうですか?

func main() {
    if FlipCoin() == false {
        printCertainTypes(FlipCoin(), int(5))
    } else {
        printCertainTypes(FlipCoin(), string("5"))
    }
}
// this function compiles with main
func printCertainTypes(flip bool, in interface{}) {
    if flip == false {
        switch v := in.(type) {
        case int:
            fmt.Printf(“integer %v\n”, v)
        default:
            fmt.Println(v)
        }
    } else {
        switch v := in.(type) {
        case int:
            fmt.Printf(“integer %v\n”, v)
        case string:
            fmt.Printf(“string %v\n”, v)
        }
    }
}
// this function compiles with main
func printCertainTypes(flip bool, in interface{}) {
    switch v := in.(type) {
    case int:
        fmt.Printf(“integer %v\n”, v)   
    case bool:
        fmt.Printf(“bool %v\n”, v)
    }
    fmt.Println(flip)
    switch v := in.(type) {
    case string:
        fmt.Printf(“string %v\n”, v)
    case bool:
        fmt.Printf(“bool 2 %v\n”, v)
    }
}
// this function emits a type switch not complete error when compiled with main
func printCertainTypes(flip bool, in interface{}) {
    if flip == false {
        switch v := in.(type) {
        case int:
            fmt.Printf(“integer %v\n”, v)
        case bool:
            fmt.Printf(“bool %v\n”, v)
        }
    } else {
        switch v := in.(type) {
        case string:
            fmt.Printf(“string %v\n”, v)
        case bool:
            fmt.Printf(“bool %v\n”, v)
        }
    }
}
// this function emits a type switch not complete error when compiled with main
func printCertainTypes(flip bool, in interface{}) {
    fmt.Println(flip)
    switch v := in.(type) {
    case int:
        fmt.Printf(“integer %v\n”, v)
    case bool:
        fmt.Printf(“bool %v\n”, v)
    }
}

公平を期すために、現在の型システムで合計型を概算する方法を検討し、それらの長所と短所を比較検討する必要があります。 他に何もない場合、それは比較のためのベースラインを提供します。

標準的な手段は、タグとしてエクスポートされていない、何もしないメソッドを持つインターフェースです。

これに対する1つの議論は、合計の各タイプにこのタグを定義する必要があるということです。 これは厳密には真実ではありません。少なくとも構造体であるメンバーについては、次のことができます。

type Sum interface { sum() }
type sum struct{}
func (sum) sum() {}

その0幅のタグを構造体に埋め込むだけです。

ラッパーを導入することで、合計に外部タイプを追加できます

type External struct {
  sum
  *pkg.SomeType
}

これは少し不格好ですが。

合計のすべてのメンバーが共通の動作を共有する場合、それらのメソッドをインターフェース定義に含めることができます。

このような構成では、型が合計に含まれているとは言えますが、その合計に含まれていないものはわかりません。 必須のnil場合に加えて、同じ埋め込みトリックを次のような外部パッケージで使用できます。

import "p"
var member struct {
  p.Sum
}

パッケージ内では、コンパイルされても違法な値を検証するように注意する必要があります。

実行時にいくつかの型安全性を回復するには、さまざまな方法があります。 合計インターフェースの定義にvalid() errorメソッドを含め、次のような関数を組み合わせていることがわかりました。

func valid(s Sum) error {
  switch s.(type) {
  case nil:
    return errors.New("pkg: Sum must be non-nil")
  case A, B, C, ...: // listing each valid member
    return s.valid()
  }
  return fmt.Errorf("pkg: %T is not a valid member of Sum")
}

2種類の検証を同時に行うことができるので便利です。 たまたま常に有効なメンバーの場合、次のような定型文を回避できます。

type alwaysValid struct{}
func (alwaysValid) valid() error { return nil }

このパターンに関するより一般的な不満の1つは、godocで合計のメンバーシップが明確にならないことです。 また、メンバーを除外することはできず、とにかく検証する必要があるため、これを回避する簡単な方法があります。ダミーメソッドをエクスポートすることです。
それ以外の、

//A Node is one of (list of types).
type Node interface { node() }

書きます

//A Node is only valid if it is defined in this package.
type Node interface { 
  //Node is a dummy method that signifies that a type is a Node.
  Node()
}

誰かがNodeを満たすのを止めることはできないので、何をするのかを知らせたほうがよいでしょう。 これにより、どのタイプがNode (中央リストなし)を満たすかが一目でわかりませんが、現在表示している特定のタイプがNode満たすかどうかはわかります。

このパターンは、合計のタイプの大部分が同じパッケージで定義されている場合に役立ちます。 ない場合、一般的な手段は、 json.Tokenやdriver.Valueように、 interface{}にフォールバックすることです。 前のパターンをそれぞれのラッパータイプで使用することもできますが、最終的にはinterface{}なるため、ほとんど意味がありません。 そのような値がパッケージの外部から来ると予想される場合は、礼儀正しく、ファクトリを定義できます。

//Sum is one of int64, float64, or bool.
type Sum interface{}
func New(v interface{}) (Sum, error) {
  switch v.(type) {
  case nil:
    return errors.New("pkg: Sum must be non-nil")
  case int64, float64, bool:
     return v
  }
  return fmt.Printf("pkg: %T is not a valid member of Sum")
}

合計の一般的な使用法は、「値なし」と「ゼロの可能性がある値」を区別する必要があるオプション型です。 これを行うには2つの方法があります。

*Tは、 nilポインターとして値がないことを示し、nil以外のポインターの参照を解除した結果として(おそらく)ゼロ値を示します。

以前のインターフェースベースの近似、および制限付きのインターフェースとして合計タイプを実装するためのさまざまな提案と同様に、これには追加のポインター逆参照と可能なヒープ割り当てが必要です。

オプションの場合、これはsqlパッケージの手法を使用して回避できます。

type OptionalT struct {
  Valid bool
  Value T
}

これの主な欠点は、無効な状態をエンコードできることです。有効はfalseであり、値はゼロ以外である可能性があります。 Validがfalseの場合にValueを取得することもできます(ただし、指定されていない場合にゼロTが必要な場合は便利です)。 値をゼロにせずにValidをfalseに設定した後、Valueを割り当てずにValidをtrueに設定(または無視)すると、以前に破棄された値が誤って再表示されます。 これは、タイプの不変条件を保護するためのセッターとゲッターを提供することで回避できます。

合計型の最も単純な形式は、値ではなくIDを気にする場合です:列挙型。

Goでこれを処理する従来の方法は、const / iotaです。

type Enum int
const (
  A Enum = iota
  B
  C
)

OptionalTタイプと同様に、これには不要な間接

このタイプの基本的な数の問題もあります。 A+B == C 。 型なしの積分定数をこの型に変換するのは少し簡単すぎます。 それが望ましい場所はたくさんありますが、私たちは何があってもこれを手に入れます。 少し余分な作業を行うだけで、これをIDだけに制限できます。

type Enum struct { v int }
var (
  A = Enum{0}
  B = Enum{1}
  C = Enum{2}
)

現在、これらは不透明なラベルです。 それらを比較することはできますが、それだけです。 残念ながら、今では恒常性を失いましたが、もう少し作業を行うことでそれを取り戻すことができました。

func A() Enum { return Enum{0} }
func B() Enum { return Enum{1} }
func C() Enum { return Enum{2} }

ボイラープレートと非常にインライン化可能な関数呼び出しを犠牲にして、外部ユーザーが名前を変更できないことを取り戻しました。

ただし、型をほぼ完全に閉じているため、これはインターフェイスの合計よりもいくつかの点で優れています。 外部コードはA() 、 B() 、またはC()のみを使用できます。 varの例のようにラベルを入れ替えることはできず、 A() + B()を実行することもできません。また、 Enum必要なメソッドを自由に定義できます。 同じパッケージ内のコードが誤って値を作成または変更する可能性はありますが、それが起こらないように注意すれば、これは検証コードを必要としない最初の合計タイプです。存在する場合は有効です。 。

多くのラベルがあり、それらのいくつかには追加の日付があり、ラベルには同じ種類のデータがある場合があります。 3つの値のない状態(A、B、C)、2つは文字列値(D、E)、もう1つは文字列値とint値(F)を持つ値があるとします。 上記の戦術をいくつか組み合わせて使用​​することもできますが、最も簡単な方法は次のとおりです。

type Value struct {
  Which int // could have consts for A, B, C, D, E, F
  String string
  Int int
}

これは上記のOptionalTタイプによく似ていますが、ブール値の代わりに列挙型があり、 Which値に応じて設定できる(または設定できない)複数のフィールドがあります。 検証では、これらが適切に設定されている(または設定されていない)ことに注意する必要があります。

Goで「次のいずれか」を表現する方法はたくさんあります。 他の人よりも注意が必要な人もいます。 多くの場合、実行時に不変条件の「1つ」または無関係な間接参照を検証する必要があります。 彼ら全員が共有する主な欠点は、言語の一部ではなく言語でシミュレートされているため、「の1つ」の不変条件がreflectまたはgo / typesに表示されず、メタプログラミングが困難になることです。彼ら。 メタプログラミングでそれらを使用するには、合計の正しいフレーバーを認識して検証できる必要があります。また、「いずれか」の不変条件がない有効なコードによく似ているため、それが探しているものであると言われる必要があります。

合計タイプが言語の一部である場合、それらはソースコードに反映され、簡単に引き出される可能性があり、その結果、ライブラリとツールが改善されます。 コンパイラーは、その「1つ」の不変条件を認識していれば、いくつかの最適化を行うことができます。 プログラマーは、値が実際に正しいドメインにあることを確認するという些細なメンテナンスではなく、重要な検証コードに集中することができます。

このような構成では、型が合計に含まれているとは言えますが、その合計に含まれていないものはわかりません。 必須のnilの場合に加えて、同じ埋め込みトリックを次のような外部パッケージで使用できます。
[…]
パッケージ内では、コンパイルされても違法な値を検証するように注意する必要があります。

どうして? パッケージの作者として、これは私には「あなたの問題」の領域にしっかりと見えます。 io.Readerを渡して、そのReadメソッドがパニックになった場合、私はそれから回復するつもりはなく、ただパニックに陥らせます。 同様に、私が宣言したタイプの無効な値を作成するために邪魔になった場合、私は誰と議論しますか? つまり、「エミュレートされたクローズドサムを埋め込んだ」という問題は、偶然に発生することはめったにありません。

そうは言っても、インターフェースをtype Sum interface { sum() Sum }変更し、すべての値がそれ自体を返すようにすることで、その問題を防ぐことができます。 そうすれば、 sum()のリターンを使用できます。これは、埋め込みの下でも正常に動作します。

このパターンに関するより一般的な不満の1つは、godocで合計のメンバーシップが明確にならないことです。

これはあなたを助けるかもしれません。

これの主な欠点は、無効な状態をエンコードできることです。有効はfalseであり、値はゼロ以外である可能性があります。

これは私にとって無効な状態ではありません。 ゼロ値は魔法ではありません。 sql.NullInt64{false,0}とNullInt64{false,42}間にIMOの違いはありません。 どちらもSQLNULLの有効で同等の表現です。 値を使用する前にすべてのコードが有効であるとチェックした場合、その違いはプログラムでは観察できません。

コンパイラがこのチェックの実行を強制しないことは公正で正しい批判であり(「実際の」オプション/合計タイプの場合はおそらくそうなるでしょう)、実行しないほうが簡単です。 しかし、それを忘れた場合、誤ってゼロ以外の値を使用するよりも、誤ってゼロの値を使用する方が良いとは思いません(ポインター型の型を除いて、使用するとパニックになる可能性があります。大声で失敗しますが、それらの場合は、とにかく裸のポインタ型を使用し、 nilを「未設定」として使用する必要があります。

このタイプの基本的な数の問題もあります。 A + B == C。型なしの積分定数をこの型に変換するのは少し簡単すぎます。

これは理論上の懸念ですか、それとも実際に発生しましたか?

プログラマーは、値が実際に正しいドメインにあることを確認するという些細なメンテナンスではなく、重要な検証コードに集中することができます。

FTRだけで、sum-types-as-sum-typesを使用する場合(つまり、問題をゴールデンバラエティインターフェイスを介してよりエレガントにモデル化することはできません)、検証コードを作成することはありません。 レシーバーまたは引数として渡されたポインターがゼロかどうかをチェックしないのと同じように(有効なバリアントとして文書化されている場合を除く)。 コンパイラが私にそれを処理するように強制する場所(つまり、「関数の終わりに戻らない」スタイルの問題)では、デフォルトの場合にパニックになります。

個人的には、Goは実用的な言語だと思います。これは、自分のために、または「誰もが自分の方が優れていることを知っている」という理由で安全機能を追加するだけでなく、実証されたニーズに基づいています。 したがって、実用的な方法で使用することは問題ないと思います。

標準的な手段は、タグとしてエクスポートされていない、何もしないメソッドを持つインターフェースです。

インターフェイスと合計タイプには根本的な違いがあります(あなたの投稿で言及されていませんでした)。 インターフェイスを介して合計タイプを概算する場合、値を処理する方法は実際にはありません。 消費者として、あなたはそれが実際に何を保持しているのか見当がつかず、推測することしかできません。 これは、単に空のインターフェイスを使用するよりも優れています。 唯一の有用性は、インターフェイスを定義する同じパッケージからのみ実装を取得できる場合です。その場合にのみ、取得できるものを制御できます。

一方、次のようなものがあります。

func foo(val string|int|error) {
    switch v:= val.(type) {
    case string:
        ...
    }
}

合計タイプの値を使用する際に、消費者にフルパワーを提供します。 その価値は具体的であり、解釈の余地はありません。

@Merovius
あなたが言及するこれらの「オープンサム」には、「フィーチャークリープ」のためにそれらを悪用することを許可するという点で、一部の人々が重大な欠点として分類する可能性があるものがあります。 これは、オプションの関数の引数が機能として拒否された理由です。

あなたが言及するこれらの「オープンサム」には、「フィーチャークリープ」のためにそれらを悪用することを許可するという点で、一部の人々が重大な欠点として分類する可能性があるものがあります。 これは、オプションの関数の引数が機能として拒否された理由です。

それは私にはかなり弱い議論のように思えます-他に何もないとしても、それらが存在するので、あなたはすでにそれらが可能にするものは何でも許可しています。 確かに、すべての意図と目的のために、すでにオプションの引数があります(私がそのパターンが

インターフェイスと合計タイプには根本的な違いがあります(あなたの投稿で言及されていませんでした)。 インターフェイスを介して合計タイプを概算する場合、値を処理する方法は実際にはありません。 消費者として、あなたはそれが実際に何を保持しているのか見当がつかず、推測することしかできません。

これをもう一度解析しようとしましたが、まだできません。 なぜあなたはそれらを使うことができないのですか? それらは通常のエクスポートされたタイプにすることができます。 はい、それらは(明らかに)パッケージで作成されたタイプである必要がありますが、それを除けば、実際のクローズドサムと比較して、それらの使用方法に制限はないようです。

これをもう一度解析しようとしましたが、まだできません。 なぜあなたはそれらを使うことができないのですか? それらは通常のエクスポートされたタイプにすることができます。 はい、それらは(明らかに)パッケージで作成されたタイプである必要がありますが、それを除けば、実際のクローズドサムと比較して、それらの使用方法に制限はないようです。

ダミーメソッドがエクスポートされ、サードパーティが「合計タイプ」を実装できる場合はどうなりますか? または、チームメンバーがインターフェイスのさまざまなコンシューマーに精通しておらず、同じパッケージに別の実装を追加することを決定し、その実装のインスタンスがコードのさまざまな手段を介してこれらのコンシューマーに渡されるという非常に現実的なシナリオですか? 私の明白な「解析不可能な」ステートメントを繰り返すリスクがあります。「消費者として、あなたは[合計値]が実際に何を保持しているのかわからず、推測することしかできません。」 それはインターフェースであり、誰がそれを実装しているのかを教えてくれないので、あなたは知っています。

@Merovius

FTRだけで、sum-types-as-sum-typesを使用する場合(つまり、問題をゴールデンバラエティインターフェイスを介してよりエレガントにモデル化することはできません)、検証コードを作成することはありません。 レシーバーまたは引数として渡されたポインターがゼロかどうかをチェックしないのと同じように(有効なバリアントとして文書化されている場合を除く)。 コンパイラが私にそれを処理するように強制する場所(つまり、「関数の終わりに戻らない」スタイルの問題)では、デフォルトの場合にパニックになります。

私はこれを常にまたは決して物として扱いません。

悪い入力を渡した人がすぐに爆発する場合、私は検証コードを気にしません。

しかし、誰かが悪い入力を渡すと最終的にパニックが発生する可能性があるが、しばらく表示されない場合は、検証コードを記述して、悪い入力にできるだけ早くフラグを付け、エラーが発生したことを誰も理解する必要がないようにします150コールスタックでフレームを上げます(特に、その悪い値がどこに導入されたかを把握するために、コールスタックでさらに150フレームを上げる必要がある場合があるため)。

後でデバッグの30分を節約できる可能性があるため、今30分を費やすのは実用的です。 特に私にとっては、いつもばかげた間違いを犯しているので、学校に通うのが早ければ早いほど、次のばかげた間違いを犯すことができます。

リーダーを取得してすぐに使用を開始する関数がある場合、nilをチェックしませんが、関数が特定のメソッドが呼び出されるまでリーダーを呼び出さない構造体のファクトリである場合は、 nilをチェックしてパニックになるか、「リーダーはnilであってはなりません」などのエラーを返し、エラーの原因がエラーの原因にできるだけ近くなるようにします。

godoc -analysis

私は知っていますが、それは役に立たないと思います。 ^ Cを押す前に、ワークスペースで40分間実行されました。これは、パッケージをインストールまたは変更するたびに更新する必要があります。 ただし、#20131(このスレッドから分岐しています!)があります。

そうは言っても、インターフェースをtype Sum interface { sum() Sum }変更し、すべての値がそれ自体を返すようにすることで、その問題を防ぐことができます。 そうすれば、 sum()のリターンを使用できます。これは、埋め込みの下でも正常に動作します。

私はそれが役に立つとは思いませんでした。 明示的な検証よりも多くの利点はなく、検証も少なくなります。

[const / iota列挙型のメンバーを追加できるという事実]は理論的な懸念ですか、それとも実際に出てきましたか?

その特定のものは理論的でした:私は私が考えることができるすべての賛否両論、理論的および実用的をリストしようとしていました。 しかし、私のより大きなポイントは、言語で不変条件の「1つ」を表現しようとする方法はたくさんあり、かなり一般的に使用されていますが、言語の一種の型にするほど単純ではないということでした。

[型なし積分をconst / iota列挙型に割り当てることができるという事実]は理論的な懸念ですか、それとも実際に出てきましたか?

それは実際に出てきました。 何が悪かったのかを理解するのにそれほど時間はかかりませんでしたが、コンパイラが「あの行、それが間違っている」と言っていたら、さらに時間がかからなかったでしょう。 その特定のケースを処理する他の方法の話がありますが、それらがどのように一般的に使用されるかはわかりません。

これは私にとって無効な状態ではありません。 ゼロ値は魔法ではありません。 sql.NullInt64{false,0}とNullInt64{false,42}間にIMOの違いはありません。 どちらもSQLNULLの有効で同等の表現です。 値を使用する前にすべてのコードが有効であるとチェックした場合、その違いはプログラムでは観察できません。

コンパイラがこのチェックの実行を強制しないことは公正で正しい批判であり(「実際の」オプション/合計タイプの場合はおそらくそうなるでしょう)、実行しないほうが簡単です。 しかし、それを忘れた場合、誤ってゼロ以外の値を使用するよりも、誤ってゼロの値を使用する方が良いとは思いません(ポインター型の型を除いて、使用するとパニックになる可能性があります。大声で失敗しますが、それらの場合は、とにかく裸のポインタ型を使用し、「未設定」としてnilを使用する必要があります。

「すべてのコードが値を使用する前に有効であるとチェックした場合」は、バグが入り込んだ場所であり、コンパイラーが強制できることです。 私はそのようなバグが発生しました(ただし、そのパターンのより大きなバージョンでは、ディスクリミネーターに複数の値フィールドと3つ以上の状態がありました)。 私は開発とテスト中にこれらすべてを見つけ、誰も野生に逃げられなかったと信じていますが、私がその間違いを犯したときにコンパイラがちょうど私に言ってくれればいいのですが、これらの1つが唯一の方法であると確信できましたコンパイラにバグがあった場合、int型の変数に文字列を割り当てようとした場合と同じように、過去にすり抜けました。

そして、確かに、私はオプション型には*Tを好みますが、実行時空とコードの可読性の両方でゼロ以外のコストが関連付けられています。

(その特定の例では、ピック提案で実際の値または正しいゼロ値を取得するコードはv, _ := nullable.[Value]あり、これは簡潔で安全です。)

それは私が望んでいることではありません。 ピックタイプは値タイプである必要があります。
Rustのように。 それらの最初の単語は、必要に応じて、GCメタデータへのポインターである必要があります。

それ以外の場合、それらの使用にはパフォーマンスの低下が伴います。
受け入れられない。 私にとって、パス10:41 AM、「JoshBleecherSnyder」<
[email protected]>は次のように書いています:

ピックプロポーザルを使用すると、apまたは* pを選択してより多くの情報を得ることができます
メモリのトレードオフをより細かく制御できます。

インターフェイスがスカラー値を格納するために割り当てる理由は、
他の単語が
ポインタ; #8405 https://github.com/golang/go/issues/8405を参照してください
議論。 同じ実装上の考慮事項が、
ピックタイプ。これは、実際には、pが割り当てられて最終的になることを意味する場合があります。
とにかく非ローカル。

—
スレッドを作成したため、これを受け取っています。
このメールに直接返信し、GitHubで表示してください
https://github.com/golang/go/issues/19412#issuecomment-323371837 、またはミュート
スレッド
https://github.com/notifications/unsubscribe-auth/AGGWB-wQD75N44TGoU6LWQhjED_uhKGUks5sZaKbgaJpZM4MTmSr
。

@urandom

ダミーメソッドがエクスポートされ、サードパーティが「合計タイプ」を実装できる場合はどうなりますか?

エクスポートされるメソッドとエクスポートされるタイプには違いがあります。 私たちはお互いを超えて話しているようです。 私には、これはオープンサムとクローズドサムの間に違いがなく、問題なく機能しているように見えます。

type X interface { x() X }
type IntX int
func (v IntX) x() X { return v }
type StringX string
func (v StringX) x() X { return v }
type StructX struct{
    Foo bool
    Bar int
}
func (v StructX) x() X { return v }

パッケージの外部に拡張機能はありませんが、パッケージのコンシューマーは、他の値と同じように値を使用、作成、および渡すことができます。

X、またはそれを満たすローカル型の1つを外部に埋め込んでから、Xを受け取るパッケージ内の関数に渡すことができます。

その関数がxを呼び出すと、パニックになるか(X自体が埋め込まれていて、何にも設定されていない場合)、コードが操作できる値を返しますが、呼び出し元から渡された値ではないため、呼び出し元には少し驚きます。 (そして、彼らがドキュメントを読んでいないので、彼らがこのようなことを試みているかどうか、彼らのコードはすでに疑わしいです)。

「それをしないでください」というメッセージでパニックになるバリデーターを呼び出すことは、それを処理するための最も驚くべき方法のように思われ、呼び出し元にコードを修正させます。

その関数がxを呼び出すと、パニックになるか[…]、コードが操作できる値を返しますが、呼び出し元から渡された値ではないため、呼び出し元には少し驚きます。

上で述べたように、無効な値の意図的な構築が無効であることに驚いた場合は、期待を再考する必要があります。 しかし、いずれにせよ、それはこの特定の議論の系統が何であったかではなく、別々の議論を別々に保つことは役に立ちます。 これは@urandomに関するもので、タグメソッドを使用したインターフェイスを介したオープンサムは、他のパッケージでは内省できず、使用できないと言っていました。 疑わしい主張だと思いますが、それを明確にできれば素晴らしいと思います。

問題は、誰かがコンパイルしてパッケージに渡すことができる合計に含まれない型を作成する可能性があることです。

言語に適切な合計タイプを追加せずに、それを処理するための3つのオプションがあります

  1. 状況を無視する
  2. 検証してパニック/エラーを返す
  3. エンベディッドバリューを暗黙的に抽出して使用することにより、「意味することを実行」してみてください

3は私には1と2の奇妙な組み合わせのように思えます:それが何を買うのかわかりません。

「驚いたら、意図的に無効な値を作成したのが無効だと思ったら、期待を考え直す必要がある」とは思いますが、3では何かがおかしくなったことに気づきにくくなります。理由を理解するのは難しいでしょう。

2は、コードが無効な状態に陥るのを防ぎ、誰かが混乱した場合にフレアを送信して、なぜ間違っているのか、そしてそれを修正する方法を知らせるので、最良のようです。

私はパターンの意図を誤解していますか、それとも異なる哲学からこれにアプローチしているだけですか?

@urandom説明も

問題は、誰かがコンパイルしてパッケージに渡すことができる合計に含まれない型を作成する可能性があることです。

あなたはいつでもそれをすることができます。 疑わしい場合は、コンパイラでチェックされた合計型を使用しても、常に安全でないものを使用できます(そして、明らかに合計として意図されたものを埋め込み、それを初期化しないことから無効な値を構築する質的に異なる方法としては見ていません有効な値)。 問題は、「これが実際に問題を引き起こす頻度と、その問題がどれほど深刻になるか」です。 私の意見では、上記の解決策では、答えは「ほとんど決して、非常に低い」です-あなたは明らかに同意しません、それは問題ありません。 しかし、いずれにせよ、これについて苦労している点はあまりないようです-この特定の点の両側の議論と見解は十分に明確である必要があり、私はあまりにも騒々しい繰り返しを避け、真に焦点を当てようとしています新しい引数。 私は上記の構造を取り上げて、ファーストクラスの合計タイプとエミュレートされた合計-経由のインターフェイスの間にエクスポート可能性に違いがないことを示しました。 それらがあらゆる点で厳密に優れていることを示すものではありません。

疑わしい場合は、コンパイラでチェックされた合計型を使用しても、常に安全でないものを使用できます(そして、明らかに合計として意図されたものを埋め込み、それを初期化しないことから無効な値を構築する質的に異なる方法としては見ていません有効な値)。

私はそれが質的に異なると思います:人々がこのように埋め込みを誤用するとき(少なくともproto.Messageとそれを実装する具体的なタイプで)、彼らは一般的にそれが安全であるかどうか、そしてそれが壊すかもしれない不変量について考えていません。 (ユーザーは、インターフェースが必要な動作を完全に記述していると想定しますが、インターフェースが共用体または合計型として使用される場合、多くの場合、そうではありません。https://github.com/golang/protobuf/issues/364も参照してください。)

対照的に、誰かがパッケージunsafeを使用して、通常は参照できない型に変数を設定する場合、少なくとも何が壊れるか、そしてその理由について考えたことがあると明示的に主張しています。

@Meroviusおそらく私ははっきりしていません

安全機能の最大の利点は、それが反映によって尊重され、go / typesで表されることです。 これにより、ツールとライブラリで作業するためのより多くの情報が得られます。 Goで合計型をシミュレートする方法はたくさんありますが、それらはすべて非合計型コードと同じであるため、ツールとライブラリは、それが合計型であり、特定のパターンを認識できる必要があることを知るために帯域外情報を必要とします使用されていますが、それらのパターンでさえ大幅な変動が可能です。

また、無効な値を作成する唯一の方法は安全ではありません。通常のコード、生成されたコード、リフレクトがあります。後者の2つは、ドキュメントを読むことができない人とは異なり、問題を引き起こす可能性が高くなります。

安全性のもう1つの副次的な利点は、コンパイラーがより多くの情報を持ち、より高速なコードを生成できることを意味します。

疑似合計をインターフェースに置き換えることができることに加えて、 json.Tokenやdriver.Valueような「これらの通常のタイプの1つ」の疑似合計を置き換えることができるという事実もあります。 それらはほとんどありませんが、 interface{}が必要な場所は1つ少なくなります。

また、無効な値を作成する唯一の方法は安全ではありません

この発言につながる「無効な価値」の定義を理解していないと思います。

@neildもしあれば

var v pick {
  None struct{}
  A struct { X int; Y *T}
  B int
}

それは次のようにメモリに配置されます

struct {
  activeField int //which of None (0), A (1), or B (2) is the current field
  theInt int // If None always 0
  thePtr *T // If None or B, always nil
}

安全でない場合は、 activeFieldが0または2の場合でもthePtr設定したり、 activeFieldが0の場合でもtheInt値を設定したりできます。

どちらの場合でも、これはコンパイラーが行うであろう仮定を無効にし、今日私たちが持つことができるのと同じ種類の理論上のバグを許します。

しかし、 @ bcmillsが指摘したように、安全でないものを使用している場合は、それが核の選択肢であるため、何をしているのかをよく知っているはずです。

私が理解していないのは、なぜ安全でないことが無効な値を作成する唯一の方法であるかということです。

var t time.Timer

tは無効な値です。 t.Cが設定されておらず、 t.Stopを呼び出すとパニックになります。安全でない必要はありません。

一部の言語には、「無効な」値の作成を防ぐために非常に長い型システムがあります。 Goはそれらの1つではありません。 組合がその針をどのように大きく動かすのかわかりません。 (もちろん、組合を支持する理由は他にもあります。)

@neildはい申し訳ありませんが、私は自分の定義に甘んじています。

合計型の不変量に​​関しては無効でした。

もちろん、合計の個々のタイプは無効な状態になる可能性があります。

ただし、合計型の不変条件を維持することは、プログラマーだけでなく、反映および移動/型にもアクセスできることを意味するため、ライブラリやツールでそれらを操作すると、その安全性が維持され、メタプログラマーにより多くの情報が提供されます。

@jimmyfrasche 、可能なすべての型を示す合計型とは異なり、インターフェイスは不透明で、型のリストがわからないか、少なくとも使用できません。インターフェイスを実装するのはです。 これにより、コードのswitch部分の記述が少し推測になります。

func F(sum SumInterface) {
    switch v := sum {
    case Screwdriver:
             ...
    default:
           panic ("Someone implementing a new type which gets passed to F and causes a runtime panic 3 weeks into production")
    }
}

したがって、インターフェイスベースの合計型エミュレーションで人々が抱えている問題のほとんどは、料金や慣習によって解決できるように思われます。 たとえば、インターフェイスにエクスポートされていないメソッドが含まれている場合、考えられるすべての(はい、意図的な回避)実装を理解するのは簡単です。 同様に、iotaベースの列挙型に関するほとんどの問題に対処するために、「列挙型はtype Foo intであり、 const ( FooA Foo = iota; FooB; FooC )形式で宣言されている」という単純な規則により、広範囲で正確なツールを記述できます。彼らにとっても。

はい、これは実際の合計タイプと同等ではありません(とにかくそれがどれほど重要であるかはよくわかりませんが、とりわけ、ファーストクラスのリフレクトサポートは得られません)が、それは既存のソリューションを意味します私のPOVからは、よく描かれているよりも優れているように見えます。 そしてIMOは、実際にGo 2に入れる前に、そのデザインスペースを探索する価値があります。少なくとも、それらが本当に人々にとってそれほど重要である場合は。

(そして、合計タイプの利点を認識していることを強調したいので、利益のためにそれらを言い換える必要はありません。私は他の人ほど重くはしませんし、欠点も見て、したがって同じデータで異なる結論に達する)

@Meroviusそれは素晴らしいポジションです。

リフレクトのサポートにより、ライブラリとオフラインツール(リンター、コードジェネレーターなど)が情報にアクセスし、静的に正確に検出できない情報を不適切に変更することを禁止できます。

とにかく、探索するのは公正な考えなので、探索してみましょう。

Goで最も一般的な疑似和のファミリを要約すると、次のようになります。(おおよそ発生順に)

  • const / iota列挙型。
  • 同じパッケージで定義されたタイプを合計するためのタグメソッドとのインターフェース。
  • *TオプションのためのT
  • 設定できるフィールドを決定する値を持つ列挙型を使用した構造体(列挙型がブール値で、他にフィールドが1つしかない場合、これは別の種類のオプションのT )
  • interface{}は、有限のタイプのセットのグラブバッグに制限されています。

これらはすべて、合計タイプと非合計タイプの両方に使用できます。 最初の2つは他の目的で使用されることはめったにないので、それらが合計タイプを表し、時折の誤検知を受け入れると仮定するのは理にかなっているかもしれません。 インターフェイスの合計の場合、パラメータやリターンがなく、メンバーに本文がない、エクスポートされていないメソッドに制限される可能性があります。 列挙型の場合、それは、彼らはただいるときだけそれらを認識するために理にかなってType = iotaイオタは、式の一部として使用されている場合、それがアップつまずいていないので。

オプションのT *Tは、通常のポインターと区別するのが非常に難しいでしょう。 これには、 type O = *Tという規則を与えることができます。 エイリアス名は型の一部ではないため、少し難しいですが、それを検出することは可能です。 type O *Tは検出が簡単ですが、コードでの操作は困難です。 一方、実行する必要のあるものはすべて基本的に型に組み込まれているため、これを認識することでツールから得られるものはほとんどありません。 これは無視しましょう。 (ジェネリックスはtype Optional(T) *T沿った何かを許可する可能性が高く、これらの「タグ付け」を単純化します)。

列挙型の構造体は、ツールで推論するのが難しいでしょう。どのフィールドが列挙型のどの値に対応するのでしょうか。 これを単純化して、列挙型のメンバーごとに1つのフィールドが必要であり、列挙型の値とフィールドの値が同じである必要があるという規則に単純化できます。次に例を示します。

type Which int
const (
  A Which = iota
  B
  C
)
type Sum struct {
  Which
  A struct{} // has to be included to line up with the value of Which
  B struct { X int; Y float64 }
  C struct { X int; Y int } 
}

オプションの型は取得できませんが、レコグナイザーで「2つのフィールド、最初はブール値」という特殊なケースを使用できます。

グラブバッグの合計にinterface{}を使用すると、 //gosum: int, float64, string, Fooような魔法のコメントがないと検出できません。

あるいは、次の定義を持つ特別なパッケージが存在する可能性があります。

package sum
type (
  Type struct{}
  Enum int
  OneOf interface{}
)

列挙型はtype MyEnum sum.Enumの形式の場合にのみ認識され、インターフェースと構造体はsum.Type埋め込まれている場合にのみ認識され、 interface{} type GrabBag sum.OneOfようなグラブバッグのみを認識します。
長所

  • コードで明示的:そのようにマークされている場合、100%合計タイプであり、誤検知はありません。
  • これらの定義には、その意味を説明するドキュメントが含まれている可能性があり、パッケージドキュメントは、これらのタイプで使用できるツールにリンクされている可能性があります。
  • いくつかは反映にある程度の可視性を持っているでしょう
    短所
  • 古いコードとstdlib(それらを使用しない)からの多くの偽陰性。
  • それらは有用であるために使用されなければならないので、採用は遅く、100%に達することはないでしょう。この特別なパッケージを認識したツールの有効性は採用の関数であり、実験ではありますが非現実的である可能性があります。

これらの2つの方法のどちらを使用して合計タイプを識別するかに関係なく、それらが認識されたと想定し、その情報を使用して、構築できるツールの種類を確認します。

ツールを大まかに生成(ストリンガーなど)と内省(ゴリントなど)にグループ化できます。

最も単純な生成コードは、switchステートメントに欠落しているケースを入力するためのツールです。 これは編集者が使用できます。 合計タイプが合計タイプとして識別されると、これは簡単です(少し面倒ですが、実際の生成ロジックは言語サポートの有無にかかわらず同じになります)。

すべての場合において、不変条件の「1つ」を検証する関数を生成することが可能です。

列挙型の場合、ストリンガーのようなツールがもっとある可能性があります。 https://github.com/golang/go/issues/19814#issuecomment -291002852で、いくつかの可能性について言及しました。

最大の生成ツールは、この情報を使用してより優れたマシンコードを生成できるコンパイラですが、まあまあです。

現時点では他のことは考えられません。 誰かのウィッシュリストに何かありますか?

内省の場合、明らかな候補は徹底的なリンティングです。 言語サポートがない場合、実際には2種類のリンティングが必要です。

  1. 考えられるすべての状態が処理されることを確認する
  2. 無効な状態が作成されていないことを確認します(これにより、1によって実行された作業が無効になります)

1は些細なことですが、2は100%検証できず(安全でないことを無視しても)、コードを使用するすべてのコードがこのリンターを実行することを期待できないため、すべての可能な状態とデフォルトのケースが必要になります。

2は、合計に対して無効な状態を生成する可能性のあるすべてのコードを反映または識別することによって値を実際に追跡することはできませんでしたが、合計タイプを埋め込んでからそれを使用して関数を呼び出す場合など、多くの単純なエラーをキャッチする可能性があります。 「pkg.F(v)を作成しましたが、pkg.F(v.EmbeddedField)を意味しました」または「2をpkg.Fに渡したので、pkg.Bを使用してください」。 構造体の場合、「どちらをオンに切り替え、Xの場合はフィールドFをゼロ以外の値に設定する」などの非常に明白な場合を除いて、一度に1つのフィールドが設定されるという不変条件を強制することはあまりできませんでした。 "。 パッケージの外部から値を受け入れる場合は、生成された検証関数を使用するように要求される可能性があります。

もう1つの大きなことは、godocに表示されることです。 godocはすでにconst / iotaをグループ化しており、#20131はインターフェイスの疑似合計に役立ちます。 不変条件を指定する以外に、定義で明示されていない構造体バージョンとは実際には何の関係もありません。

オフラインツール(リンター、コードジェネレーターなど)も同様です。

いいえ。静的な情報が存在します。そのために型システム(または反映)は必要ありません。規則は正常に機能します。 インターフェイスにエクスポートされていないメソッドが含まれている場合、静的ツールはそれをクローズドサムとして扱い(効果的にそうであるため)、必要な分析/コード生成を実行することを選択できます。 同様に、iota-enumsの規則もあります。

リフレクトは実行時型情報用です-そしてある意味で、コンパイラーはここで慣例による合計を機能させるために必要な情報を消去します(関数または宣言された型または宣言された定数のリストへのアクセスを提供しないため)。実際の合計がこれを可能にすることに同意する理由です。

(また、FTRは、ユースケースによっては、静的に既知の情報を使用して必要なランタイム情報を生成するツールを使用することもできます。たとえば、必要なタグメソッドを持つタイプを列挙し、ルックアップテーブルを生成することができます。しかし、ユースケースがどうなるかわからないので、これの実用性を評価するのは難しいです)。

それで、私の質問は意図的にでした:実行時にこの情報を利用できるようにすることのユースケースは何でしょうか?

とにかく、探索するのは公正な考えなので、探索してみましょう。

私が「それを探求する」と言ったとき、私は「それらを列挙し、真空中でそれらについて議論する」という意味ではなく、「これらの規則を使用し、それらがどれほど有用/必要/実用的であるかを確認するツールを実装する」という意味でした。

経験レポートの利点は、

「そのために既存のメカニズムを使用しようとしている」部分をスキップしています。 あなたは静的な網羅性を持ちたい-合計のチェック(問題)。 エクスポートされていないメソッドとのインターフェースを見つけ、網羅性を実行するツールを作成します-使用されているタイプスイッチをチェックし、しばらくの間そのツールを使用します(既存のメカニズムを使用します)。 それが失敗したところを書きなさい。

私は大声で考えていて、ツールが使用する可能性のあるそれらの考えに基づいて静的認識機能の作業を開始しました。 私は、暗黙のうちにフィードバックやより多くのアイデアを探していたと思います(そして、それは反映に必要な情報を再生成することで報われました)。

FWIW、もしあなたが複雑なケースを単に無視して、機能するものに焦点を当てるなら:a)インターフェースのエクスポートされていないメソッドとb)基礎となる型としてintを持ち、単一の定数を持つ単純なconst-iota-enum-期待されるフォーマットの宣言。 ツールを使用するには、これら2つの回避策のいずれかを使用する必要がありますが、IMOは問題ありません(コンパイラツールを使用するには、合計も明示的に使用する必要があるため、問題ないようです)。

これは間違いなく開始するのに適した場所であり、多数のパッケージセットで実行し、誤検知/誤検知がいくつあるかを確認した後でダイヤルインできます。

https://godoc.org/github.com/jimmyfrasche/closed

まだ非常に進行中の作業です。 コンストラクターにパラメーターを追加する必要がないことを約束することはできません。 おそらくテストよりも多くのバグがあります。 しかし、それで遊ぶには十分です。

cmds / closed-exporerでの使用例があり、インポートパスで指定されたパッケージで検出されたすべての閉じたタイプも一覧表示されます。

エクスポートされていないメソッドを使用してすべてのインターフェイスを検出し始めましたが、それらはかなり一般的であり、明らかに合計型であるものもあれば、そうでないものもあります。 空のタグメソッドの規則に限定すると、多くの合計タイプが失われたため、両方を別々に記録し、合計タイプを少し超えて閉じたタイプにパッケージを一般化することにしました。

列挙型を使用して、逆に進み、定義された型のすべての非ビットセット定数を記録しました。 発見されたビットセットも公開する予定です。

ある種のマーカーコメントが必要になるため、オプションの構造体や定義済みの空のインターフェイスはまだ検出されませんが、stdlibにあるものは特別な場合に使用されます。

エクスポートされていないメソッドを使用してすべてのインターフェイスを検出し始めましたが、それらはかなり一般的であり、明らかに合計型であるものもあれば、そうでないものもあります。

提供されなかった例をいくつか提供していただければ助かります。

@Merovius申し訳ありませんが、リストを保持していませんでした。 stdlib.sh(cmds / closed-explorer内)を実行してそれらを見つけました。 次回これで遊ぶときに良い例に出くわしたら、それを投稿します。

合計型として私が考慮していないのは、いくつかの実装の1つをプラグインするために使用されていたすべてのエクスポートされていないインターフェイスでした。インターフェイスの内容は何も気にせず、満足するものがあっただけです。 それらは合計ではなくインターフェースとして非常に使用されていましたが、エクスポートされなかったためにたまたま閉じられました。 おそらくそれは違いのない区別ですが、私はさらに調査した後、いつでも考えを変えることができます。

@jimmyfrascheこれらはクローズドサムとして適切に扱われるべきだと私は主張します。 彼らが動的タイプを気にしない場合(つまり、インターフェイスのメソッドを呼び出すだけの場合)、静的リンターは「すべてのスイッチが網羅的である」ため、文句を言わないだろうと私は主張します-したがって、それらを扱うことにマイナス面はありませんクローズドサムとして。 OTOHが時々タイプを切り替えてケースを省略した場合、不平を言うのは正しいでしょう-それはまさにリンターが捕まえることになっている種類のことです。

共用体タイプがメモリ使用量をどのように削減できるかを探求するための良い言葉を述べたいと思います。 私はGoでインタープリターを作成していますが、Valuesはさまざまな型へのポインターになる可能性があるため、必ずインターフェイスとして実装されるValue型があります。 これはおそらく、[]値がCでできるように小さなビットタグでポインタをパックするのに比べて2倍のメモリを消費することを意味します。

言語仕様ではこれについて言及する必要はありませんが、一部の小さな共用体タイプでは配列のメモリ使用量を半分に削減することは、共用体にとってかなり説得力のある議論になる可能性がありますか? 私の知る限り、今日の囲碁では不可能なことをすることができます。 対照的に、インターフェイスの上にユニオンを実装すると、プログラムの正確性と理解性に役立つ可能性がありますが、マシンレベルでは何も新しいことはありません。

パフォーマンステストは行っていません。 研究の方向性を指摘するだけです。

代わりに、値をunsafe.Pointerとして実装できます。

2018ĺš´2月6日15:54、「BrianSlesinsky」 [email protected]は次のように書いています。

共用体タイプがどのように削減できるかを探求するための良い言葉を述べたいと思います
メモリ使用量。 私はGoでインタプリタを書いていて、その値型を持っています
値はポインタになる可能性があるため、必ずインターフェイスとして実装されます
さまざまなタイプに。 これはおそらく、[]値が2倍の量を占めることを意味します
あなたができるように小さなビットタグでポインタをパックすることと比較したメモリ
Cで。それはたくさんのようですか?

言語仕様はこれについて言及する必要はありませんが、メモリを削減しているようです
一部の小さな共用体タイプで配列を半分に使用すると、かなりの可能性があります
組合に対する説得力のある議論? それはあなたに私がする限り何かをすることを可能にします
今日の囲碁では不可能だと知っています。 対照的に、
インターフェースの上部は、プログラムの正確性と
理解しやすいですが、マシンレベルでは何も新しいことはしません。

パフォーマンステストは行っていません。 方向性を指摘するだけ
リサーチ。

—
あなたが言及されたのであなたはこれを受け取っています。
このメールに直接返信し、GitHubで表示してください
https://github.com/golang/go/issues/19412#issuecomment-363561070 、またはミュート
スレッド
https://github.com/notifications/unsubscribe-auth/AGGWBz-L3t0YosVIJmYNyf2iQ-YgIXLGks5tSLv9gaJpZM4MTmSr
。

@skybrianこれは、合計型の実装に関してはかなり

合計タイプはおそらくタグ付き共用体になり、スライス内で現在と同じくらいのスペースを占める可能性があります。 スライスが均質でない限り、今はより具体的なスライスタイプを使用することもできます。

そうそう。 非常に特殊なケースでは、特に最適化すればメモリを少し節約できるかもしれませんが、実際に必要な場合は手動で最適化することもできるようです。

@ DemiMarieunsafe.PointerはAppEngineで機能しません。いずれの場合も、ガベージコレクターを台無しにせずにビットをパックすることはできません。 たとえそれが可能であったとしても、それは持ち運びできないでしょう。

@Meroviusはい、パックされたメモリレイアウトを理解するには、ランタイムとガベージコレクターを変更する必要があります。 それがポイントです。 ポインターはGoランタイムによって管理されるため、インターフェースよりも安全な方法で実行したい場合は、ライブラリーやコンパイラーで実行することはできません。

しかし、高速インタプリタを作成することはまれなユースケースであることを容易に認めます。 おそらく他にもありますか? 言語機能をやる気にさせる良い方法は、今日のGoでは簡単にできないことを見つけることだと思われます。

それは本当です。

私の考えでは、Goは通訳を書くのに最適な言語ではありません。
そのようなソフトウェアの非常に動的なため。 高性能が必要な場合は、
ホットループはアセンブリで作成する必要があります。 何か理由はありますか
App Engineで動作するインタプリタを作成する必要がありますか?

2018ĺš´2月6日午後6時15分、「BrianSlesinsky」 [email protected]は次のように書いています。

@DemiMarie https://github.com/demimarieunsafe.Pointerがアプリで機能しない
エンジン、そしていずれにせよ、それなしではビットをパックすることはできません
ガベージコレクターを台無しにします。 それが可能であったとしても、そうではないでしょう
ポータブル。

@metroviusはい、ランタイムとガベージコレクターを変更する必要があります
パックされたメモリのレイアウトを理解する。 それがポイントです。 ポインタは
Goランタイムによって管理されるため、
安全な方法として、ライブラリやコンパイラでそれを行うことはできません。

しかし、私はすぐに高速インタプリタを書くことは珍しい使用法であることを認めます
場合。 おそらく他にもありますか? やる気を起こさせる良い方法のようです
言語機能は、今日のGoでは簡単に実行できないことを見つけることです。

—
あなたが言及されたのであなたはこれを受け取っています。
このメールに直接返信し、GitHubで表示してください
https://github.com/golang/go/issues/19412#issuecomment-363598572 、またはミュート
スレッド
https://github.com/notifications/unsubscribe-auth/AGGWB65jRKg_qVPWTiq8LbGk3YM1RUasks5tSN0tgaJpZM4MTmSr
。

@rogpeppeの提案は非常に魅力的だと思います。 また、@ griesemerによってすでに特定されているメリットに加えて、追加のメリットを引き出す可能性があるのではないかと思います。

提案は次のように述べています。「合計タイプのメソッドセットは、メソッドセットの共通部分を保持します。
同じメソッドを除く、すべてのコンポーネントタイプの
名前が異なる署名。」

しかし、型は単なるメソッドセットではありません。 合計タイプが、そのコンポーネントタイプでサポートされている操作の共通部分をサポートしている場合はどうなりますか?

たとえば、次のことを考慮してください。

var x int|float64

次のことがうまくいくという考えです。

x += 5

これは、フルタイプのスイッチを書き出すのと同じです。

switch i := x.(type) {
case int:
    x = i + 5
case float64:
    x = i + 5
}

別のバリアントには、コンポーネントタイプ自体が合計タイプであるタイプスイッチが含まれます。

type Num int | float64
type StringOrNum string | Num 
var x StringOrNum

switch i := x.(type) {
case string:
    // Do string stuff.
case Num:
    // Would be nice if we could use i as a Num here.
}

また、合計型と型制約を使用するジェネリックシステムの間には、本当に素晴らしい相乗効果がある可能性があると思います。

var x int|float64

var x, y int | float64どうですか? これらを追加するときのここでのルールは何ですか? どの損失のある変換が行われるのですか(そしてその理由)? 結果のタイプはどうなりますか?

Goは、(Cのように)式の自動変換を意図的に行いません。これらの質問に答えるのは簡単ではなく、バグにつながります。

そしてさらに楽しいために:

var x, y, z int|string|rune
x = 42
y = 'a'
z = "b"
fmt.Println(x + y + z)
fmt.Println(x + z + y)
fmt.Println(y + x + z)
fmt.Println(y + z + x)
fmt.Println(z + x + y)
fmt.Println(z + y + x)

int 、 string 、およびruneすべてに、 +演算子があります。 上記の印刷とは何ですか、なぜそして何よりも、結果が完全に混乱しないようにするにはどうすればよいですか?

var x, y int | float64どうですか? これらを追加するときのここでのルールは何ですか? どの損失のある変換が行われるのですか(そしてその理由)? 結果のタイプはどうなりますか?

@Meroviusは、損失の多い変換が暗黙的に行われることはありませんが、私の言葉遣いがその印象をどのように与えることができるかはわかります。 ここでは、単純なx + yは、暗黙的な変換の可能性を示唆しているため、コンパイルされません。 ただし、次のいずれかがコンパイルされます。

z = int(x) + int(y)
z = float64(x) + float64(y)

同様に、xyzの例は、暗黙的な変換が必要になる可能性があるため、コンパイルされません。

「サポートされている操作の交差点をサポートしている」というのはいいことのように聞こえますが、私が意図していたことを完全には伝えていません。 「すべてのコンポーネントタイプ用にコンパイルする」のようなものを追加すると、それがどのように機能すると思うかを説明するのに役立ちます。

別の例は、すべてのコンポーネントタイプがスライスとマップである場合です。 タイプスイッチを必要とせずに、合計タイプでlenを呼び出すことができると便利です。

int、string、runeのすべてに+演算子があります。 上記の印刷とは何ですか、なぜそして何よりも、結果が完全に混乱しないようにするにはどうすればよいですか?

「合計型がそのコンポーネント型でサポートされている操作の共通部分をサポートしている場合はどうなりますか?」ということを追加したかっただけです。 「タイプは、値のセットを、それらの値に固有の操作およびメソッドとともに決定する」というGoSpecのタイプの説明に触発されました。

私が言いたかったのは、型は単なる値やメソッドではないため、sum型は、そのコンポーネント型から他のものの共通性を捉えようとする可能性があるということです。 この「その他のもの」は、単なる演算子のセットよりも微妙な違いがあります。

別の例は、nilとの比較です。

var x []int | []string
fmt.Println(x == nil)  // Prints true
x = []string(nil)
fmt.Println(x == nil)  // Still prints true

両方のコンポーネントタイプは少なくとも1つのタイプがnilに相当するため、タイプスイッチなしで合計タイプをnilと比較できます。 もちろん、これは現在のインターフェースの動作とは多少矛盾していますが、 https://github.com/golang/go/issues/22729によると悪いことではないかもしれません

編集:同等性テストは、より寛容であり、1つ以上のコンポーネントタイプからの潜在的な一致のみを必要とするため、ここでは悪い例です。 その点で割り当てをミラーリングします。

問題は、結果がa)自動変換と同じ問題を抱えているか、b)範囲が極端に(そしてIMOが混乱して)制限されることです。つまり、すべての演算子は、せいぜい型なしリテラルでしか機能しません。

また、別の問題があります。これを許可すると、構成タイプの進化に対する堅牢性がさらに制限されます。下位互換性を維持しながら追加できるタイプは、構成タイプのすべての操作を許可するタイプだけです。

これらすべては、(もしあれば)非常に小さな具体的な利益のために、私には本当に厄介に思えます。

現在、下位互換性を維持しながら追加できるタイプは、構成タイプのすべての操作を許可するタイプのみです。

ああ、これについても明確に言うと、これは、パラメーターを拡張するか、型または変数を返すか、または…シングルトン型から合計に拡張するかどうかを決定できないことを意味します。 新しいタイプを追加すると、一部の操作(割り当てなど)がコンパイルに失敗するためです。l

@Meroviusは、「合計タイプのメソッドセットは、メソッドセットの共通部分を保持しているため、元の提案には互換性の問題のバリアントがすでに存在することに注意してください。
したがって、そのメソッドセットを実装しない新しいコンポーネントタイプを追加すると、下位互換性のない変更になります。

ああ、これについても明確に言うと、これは、パラメーターを拡張するか、型または変数を返すか、または…シングルトン型から合計に拡張するかどうかを決定できないことを意味します。 新しいタイプを追加すると、一部の操作(割り当てなど)がコンパイルに失敗するためです。l

割り当ての動作は@rogpeppeで説明されている

何といっても、タイプスイッチの外での合計タイプの動作に関して、元のrogpeppe提案を明確にする必要があると思います。 割り当てとメソッドセットについては説明しますが、それだけです。 平等はどうですか? インターフェース{}よりもうまくやれると思います。

var x int | float64
fmt.Println(x == "hello")  // compilation error?
x = 0.0
fmt.Println(x == 0) // true or false?  I vote true :-)

したがって、そのメソッドセットを実装しない新しいコンポーネントタイプを追加すると、下位互換性のない変更になります。

メソッドはいつでも追加できますが、演算子をオーバーロードして新しい型を処理することはできません。 これはまさに違いです-彼らの提案では、type-assertion / -switchでラップを解除しない限り、sum-valueで一般的なメソッドを呼び出す(または割り当てる)ことしかできません。 したがって、追加する型に必要なメソッドがある限り、それは重大な変更にはなりません。 あなたの提案では、ユーザーがオーバーロードできない演算子を使用する可能性があるため、それでも重大な変更になります。

(タイプ・スイッチはそれらの新しいタイプを持っていないので、私はどちらかの当初の提案に賛成していないよ、正確に理由であるあなたは、合計に種類を追加すると、まだ互換性に影響する変更になることを指摘したい場合があります- 。Iまさにその理由でクローズドサムを望まない)

割り当ての動作は、@ rogpeppeで説明されているとおりのままになります

彼らの提案は、合計値への割り当てについてのみ説明しています。私は、合計値から(その構成要素の1つへの)割り当てについて説明しています。 彼らの提案もこれを許可していないことに同意しますが、違いは、彼らの提案はこの可能性を追加することではないということです。 つまり、私の主張は、実際には、それらが取得する使用法が厳しく制限されているため、あなたが提案するセマンティクスは特に有益ではないということです。

fmt.Println(x == "hello") // compilation error?

これはおそらく彼らの提案にも追加されるでしょう。 インターフェースには、同等の特殊なケースがすでにあり

非インターフェースタイプXの値xとインターフェースタイプTの値tは、タイプXの値が比較可能であり、XがTを実装している場合に比較可能です。tの動的タイプがXと同一であり、tの動的値がxに等しい場合、これらは等しくなります。 。

fmt.Println(x == 0) // true or false? I vote true :-)

おそらく間違っています。 与えられて、それは似ています

var x int|float64 = 0.0
y := 0
fmt.Println(x == y)

(上記で結論付けたように)コンパイルエラーである必要があります。この質問は、型指定されていない数値定数と比較する場合にのみ実際に意味があります。 その時点で、これが仕様にどのように追加されるかによって異なります。 これは、インターフェイスタイプに定数を割り当てるのと似ているため、デフォルトのタイプにする必要があると主張することができます(その場合、比較はfalseになります)。 どのIMOが問題ないのか、私たちは今日すでに受け入れています。 ただし、型なし定数の仕様にケースを追加して、それらを合計に割り当て/比較する場合をカバーし、その方法で問題を解決することもできます。

ただし、どちらの方法でもこの質問に答えるには、構成要素にとって意味のある合計タイプを使用するすべての式を許可する必要はありません。

しかし、繰り返しになりますが、私は合計について別の提案を支持することを主張しているわけではありません。 私はこれに反対している。

fmt.Println(x == "hello") // compilation error?

これはおそらく彼らの提案にも追加されるでしょう。

訂正:仕様には、ステートメントが含まれているため、このコンパイルエラーはすでにカバーされています

いずれの比較においても、第1オペランドは第2オペランドのタイプに割り当て可能である必要があり、その逆も同様です。

@Meroviusあなたは私の提案の変種についていくつかの良い点を述べています。 これ以上の議論は控えますが、元の提案にも同様に当てはまるので、0の質問との比較についてもう少し詳しく説明したいと思います。

fmt.Println(x == 0) // true or false? I vote true :-)

おそらく間違っています。 与えられて、それは似ています

var x int|float64 = 0.0
y := 0
fmt.Println(x == y)
コンパイルエラーである必要があります(上記で結論付けたように)、

最初の行をvar x float64 = 0.0変更すると、同じ理由を使用して、float64を0と比較することは誤りであると主張できるため、この例はあまり説得力がありません。 (マイナーポイント:(a)0.0はintに割り当て可能であるため、最初の行でfloat64(0)を意味していると思います。(b)x == yは、この例ではコンパイルエラーではありません。ただし、falseと出力されます。)

「これはインターフェイス型に定数を割り当てるのと似ているので、デフォルトの型にする必要がある」というあなたの考えはより説得力があると思います(合計型を意味すると仮定します)。したがって、例は次のようになります。

var x,y int|float64 = float64(0), 0
fmt.Println(x == y) // false

しかし、私はまだx == 0が真実であるべきだと主張します。 私のメンタルモデルは、タイプができるだけ遅く0に与えられるというものです。 私はこれがインターフェースの現在の振る舞いに反していることを理解しています。それがまさに私がそれを提起した理由です。 これが「多くのファズ」につながっていないことに同意しますが、インターフェイスをnilと比較するという同様の問題により、かなりの混乱が生じています。 合計タイプが存在し、古い等式セマンティクスが保持されている場合、0と比較するために同様の混乱が見られると思います。

最初の行をvarx float64 = 0.0に変更すると、同じ理由を使用して、float64を0と比較することは誤りであると主張できるため、この例はあまり説得力がありません。

私はそうすべきだとが、彼らの提案がどのように実行されるかについての単純さと有用性の間の最も可能性の高いトレードオフとして私が認識していることを考えると、おそらくそう

float64(0)をint(0)と比較すること(つまり、合計がvar x float64 = 0.0置き換えられた例)はfalseではないことに注意してください。ただし、これはコンパイル時です。エラー(あるべきです)。 これはまさに私のポイントです; あなたの提案は、型なしの定数と組み合わせた場合にのみ本当に役立ちます。それ以外の場合はコンパイルされないからです。

(a)0.0はintに割り当て可能であるため、最初の行でfloat64(0)を意味していると思います。

確かに(定数式の現在の「デフォルトタイプ」に近いセマンティクスを想定していましたが、現在の表現がそれを意味しないことに同意します)。

(b)x == yは、あなたの例ではコンパイルエラーであってはなりません。 ただし、falseと出力されるはずです。)

いいえ、コンパイル時エラーである必要があります。 e1がsum型式である操作e1 == yは、式が構成要素型の任意の選択でコンパイルされる場合にのみ許可されるべきであるとあなたは言いました。 私の例では、 xタイプはint|float64 、 yタイプはintで、 float64とintは比較できません。この条件は、明らかに違反しています。

このコンパイルを行うには、構成要素の型付き式を置き換えることもコンパイルする必要があるという条件を削除する必要があります。 この時点で、これらの式で使用されたときに型がどのようにプロモートまたは変換されるかについてのルールを設定する必要があります(「C混乱」とも呼ばれます)。

過去のコンセンサスは、合計型はインターフェイス型にあまり追加されないというものでした。

ささいなネットワークサービスやユーティリティなど、Goのほとんどのユースケースには当てはまりません。 しかし、システムが大きくなると、それらが役立つ可能性が高くなります。
私は現在、多くのロジックを介して実装されたデータ整合性保証を備えた高度に分散されたサービスを作成しており、それらが便利な状況に乗り込みました。 これらのNPDは、サービスが大きくなり、それを分割するための適切な方法が見当たらないため、煩わしくなりすぎました。
つまり、Goの型システムの保証は、一般的なプリミティブネットワークサービスよりも複雑なものには少し弱すぎます。

しかし、Rustの話は、Haskellの場合と同じように、NPDとエラー処理に合計型を使用することは悪い考えであることを示しています。典型的な自然な命令ワークフローがあり、Haskellishのアプローチはそれにうまく適合しません。

例

擬似コードのiotuils.WriteFileような関数を検討してください。 命令フローは次のようになります

file = open(name, os.write)
if file is error
    return error("cannot open " + name + " writing: " + file.error)
if file.write(data) is error:
    return error("cannot write into " + name + " : " + file.error)
return ok

そしてそれがRustでどのように見えるか

match open(name, os.write)
    file
        match file.write(data, os.write)
            err
                return error("cannot open " + name + " writing: " + err)
            ok
                return ok
    err
        return error("cannot write into " + name + " : " + err)

安全ですが醜いです。

そして私の提案:

type result[T, Err] oneof {
    default T
    Error Err
}

プログラムがどのように見えるか( result[void, string] = !void )

file := os.Open(name, ...)
if !file {
    return result.Error("cannot open " + name + " writing: " + file.Error)
}
if res := file.Write(data); !res {
    return result.Error("cannot write into " + name + " : " + res.Error)
}
return ok

ここで、デフォルトのブランチは匿名であり、エラーブランチには.Errorでアクセスできます(結果がエラーであることがわかったら)。 ファイルが正常に開かれたことがわかると、ユーザーは変数自体を介してファイルにアクセスできます。 最初に、 fileが正常に開かれたことを確認するか、そうでない場合は終了します(したがって、以降のステートメントでは、ファイルがエラーではないことがわかります)。

ご覧のとおり、このアプローチは命令型のフローを維持し、型の安全性を提供します。 NPDの処理は、同様の方法で実行できます。

type Reference[T] oneof {
    default T
    nil
}
// Reference[T] = *T

処理は結果と同様です

@sirkon 、あなたのRustの例は、Rustのような単純な合計型に何か問題があることを私に納得させません。 むしろ、合計タイプのパターンマッチングは、 ifステートメントを使用してよりGoに似たものになる可能性があることを示唆しています。 何かのようなもの:

ferr := os.Open(name, ...)
if err(e) := ferr {           // conditional match and unpack, initializing e
  return fmt.Errorf("cannot open %v: %v", name, e)
}
ok(f) := ferr                  // unconditional match and unpack, initializing f
werr := f.Write(data)
...

(合計タイプの精神では、ケースが1つだけ残っているため、コンパイラーが無条件の一致が常に成功することを証明できない場合、コンパイルエラーになります。)

基本的なエラーチェックの場合、これは1行長く、もう1つのローカル変数を宣言するため、複数の戻り値よりも改善されているようには見えません。 ただし、(ifステートメントを追加することで)複数のケースに拡張する方が適切であり、コンパイラーはすべてのケースが処理されていることを確認できます。

@sirkon

ささいなネットワークサービスやユーティリティなど、Goのほとんどのユースケースには当てはまりません。 しかし、システムが大きくなると、それらが役立つ可能性が高くなります。
[…]
つまり、Goの型システムの保証は、一般的なプリミティブネットワークサービスよりも複雑なものには少し弱すぎます。

このような発言は、不必要に対立的で蔑称的です。 Goで書かれた非常に大規模で重要なサービスがあるため、これらはちょっと恥ずかしいTBHでもあります。 また、開発者のかなりの部分がGoogleで働いていることを考えると、大規模で重要なサービスを作成するのに適している場合は、開発者があなたよりもよく知っていると想定する必要があります。 Goはすべてのユースケースをカバーして

NPDの処理も同様の方法で行うことができます

これは、あなたのアプローチが実際には重要な価値をもたらさないことを本当に示していると思います。 ご指摘のとおり、間接参照用に異なる構文を追加するだけです。 しかし、AFAICTは、プログラマーがnil値でその構文を使用することを妨げるものは何もありません(おそらくまだパニックになります)。 つまり、 *pを使用して有効なすべてのプログラムは、 p.Tを使用しても有効です(またはp.defaultですか?あなたのアイデアが具体的に何であるかを判断するのは難しいです)。

合計タイプがエラー処理とnil-dereferencesに追加できる1つの利点は、コンパイラーが、パターンマッチングによって操作が安全であることを証明する必要があることを強制できることです。 その施行を省略した提案は、テーブルに重要な新しいものをもたらさないようです(おそらく、インターフェースを介してオープンサムを使用するよりも悪いです)が、それを含む提案はまさにあなたが「醜い」と表現するものです。

@Merovius

そして、その開発者のかなりの部分がグーグルで働いていることを考えると、あなたは彼らがあなたよりよく知っていると仮定するべきです、

信者は幸いです。

ご指摘のとおり、間接参照用に異なる構文を追加するだけです。

また

var written int64
...
res := os.Stdout.Write(data) // Write([]byte) -> Result[int64, string] ≈ !int64
written += res // Will not compile as res is a packed result type
if !res {
    // we are living on non-default res branch thus the only choice left is the default
    return Result.Error(...)
}
written += res // is OK

@skybrian

ferr := os.Open(...)

この中間変数は、私がこの考えを離れることを強いるものです。 ご覧のとおり、私のアプローチは特にエラーとゼロ処理のためのものです。 これらの小さなタスクは非常に重要であり、IMOに特別な注意を払う必要があります。

@sirkonあなたはどうやら人と目と目で話すことにほとんど興味がないようです。 そのままにしておきます。

会話を礼儀正しく保ち、非建設的なコメントを避けましょう。 私たちは物事について意見を異にすることができますが、それでも立派な言説を維持します。 https://golang.org/conduct。

そして、その開発者のかなりの部分がグーグルで働いていることを考えると、あなたは彼らがあなたよりよく知っていると仮定するべきです

グーグルでそのような議論をすることができるとは思えない。

@hasufellその男はドイツ出身で、面接官のエゴと巨大な管理を

@sirkon同じことがあなたにも

いいえ、ありません。 知的権威はありません。 ただ決定権があります。 それを乗り越えなさい。

会話をリセットするためにいくつかのコメントを非表示にします(そして、それをレールに戻そうとしてくれた@agnivadeに感謝します)。

皆さん、私たちのGopherの価値観に照らして、これらの議論におけるあなたの役割を考慮してください。コミュニティの誰もがもたらす視点を持っており、私たちはお互いをどのように解釈し、対応するかについて敬意と慈善に努めるべきです。

このディスカッションに私の2セントを追加さ​​せてください。

メソッドセット以外の機能(インターフェイスの場合など)によって、さまざまなタイプをグループ化する方法が必要です。 新しいグループ化機能では、メソッドを持たないプリミティブ(または基本)タイプと、インターフェイスタイプを関連する類似として分類できるようにする必要があります。 プリミティブ型(ブール、数値、文字列、さらには[] byte、[] intなど)をそのまま保持できますが、型定義によってファミリーにグループ化されている型間の違いから抽象化することができます。

_family_型のようなものを言語に追加することをお勧めします。

構文

型族は、他の型と同じように定義できます。

type theFamilyName family {
    someType
    anotherType
}

正式な構文は次のようになります。
FamilyType = "family" "{" { TypeName ";" } "}" .

型族は、関数シグニチャ内で定義できます。

func Display(s family{string; fmt.Stringer}) { /* function body */ }

つまり、1行の定義では、タイプ名の間にセミコロンが必要です。

ファミリタイプのゼロ値は、nilインターフェイスの場合と同様にnilです。

(内部では、ファミリの抽象化の背後にある値は、インターフェイスのように実装されます。)

推論

関数の引数として、または関数の戻り値として有効な型を指定する空のインターフェイスよりも正確なものが必要です。

提案されたソリューションは、コンパイル時に完全にチェックされ、実行時に追加のオーバーヘッドを追加することなく、より優れた型安全性を可能にします。

重要なのは、_Goコードはより自己文書化する必要があるということです_。 関数が引数として取ることができるものは、コード自体に組み込まれている必要があります。

コードが多すぎると、「interface {}は何も言わない」という事実を誤って悪用します。 Goでこのように広く使用されている(そして悪用されている)構造がなければ、多くのことを行うことができないのは少し恥ずかしいことです、と_nothing_は言います。

いくつかの例

sql.Rows.Scan関数のドキュメントには、関数に渡される可能性のあるタイプの詳細を示す大きなブロックが含まれています。

Scan converts columns read from the database into the following common Go types and special types provided by the sql package:
 *string
 *[]byte
 *int, *int8, *int16, *int32, *int64
 *uint, *uint8, *uint16, *uint32, *uint64
 *bool
 *float32, *float64
 *interface{}
 *RawBytes
 any type implementing Scanner (see Scanner docs)

また、 sql.Row.Scan関数の場合、ドキュメントには「詳細についてはRows.Scanのドキュメントを参照してください」という文が含まれています。 詳細については、_その他の関数_のドキュメントを参照してください。 これはGoに似たものではありません。実際、 Rows.Scanは*RawBytes値を取ることができますが、 Row.Scanは取ることができないため、この文は正しくありません。

問題は、コンパイラが強制できない保証や動作コントラクトについてコメントに頼らざるを得ないことが多いことです。

関数のドキュメントに、その関数が他の関数と同じように機能すると記載されている場合(「他の関数のドキュメントを参照してください」)、その関数が時々誤用されることはほぼ間違いありません。 私のように、ほとんどの人は、 Row.Scanからエラーを受け取った後でのみ、 *RawBytesがRow.Scan引数として許可されていないことに気付いたに違いありません。 「sql:RawBytesはRow.Scanでは許可されていません」と言っています)。 型システムがそのような間違いを許すのは悲しいことです。

代わりに、次のものを使用できます。

type Value family {
    *string
    *[]byte
    *int; *int8; *int16; *int32; *int64
    *uint; *uint8; *uint16; *uint32; *uint64
    *bool
    *float32; *float64
    *interface{}
    *RawBytes
    Scanner
}

このように、渡される値は指定されたファミリのタイプの1つである必要があり、 Rows.Scan関数内のタイプスイッチは予期しないケースやデフォルトのケースを処理する必要はありません。 Row.Scan関数には別のファミリがあります。

cloud.google.com/go/datastore.Property構造体にタイプinterface{} 「値」フィールドがあり、次のすべてのドキュメントが必要なことも考慮してください。

// Value is the property value. The valid types are:
// - int64
// - bool
// - string
// - float64
// - *Key
// - time.Time
// - GeoPoint
// - []byte (up to 1 megabyte in length)
// - *Entity (representing a nested struct)
// Value can also be:
// - []interface{} where each element is one of the above types
// This set is smaller than the set of valid struct field types that the
// datastore can load and save. A Value's type must be explicitly on
// the list above; it is not sufficient for the underlying type to be
// on that list. For example, a Value of "type myInt64 int64" is
// invalid. Smaller-width integers and floats are also invalid. Again,
// this is more restrictive than the set of valid struct field types.
//
// A Value will have an opaque type when loading entities from an index,
// such as via a projection query. Load entities into a struct instead
// of a PropertyLoadSaver when using a projection query.
//
// A Value may also be the nil interface value; this is equivalent to
// Python's None but not directly representable by a Go struct. Loading
// a nil-valued property into a struct will set that field to the zero
// value.

これは次のようになります。

type PropertyVal family {
  int64
  bool
  string
  float64
  *Key
  time.Time
  GeoPoint
  []byte
  *Entity
  nil
  []int64; []bool; []string; []float64; []*Key; []time.Time; []GeoPoint; [][]byte; []*Entity
}

(これがどのようにクリーナーを2つのファミリーに分割できるか想像できます。)

json.Tokenタイプは上記のとおりです。 タイプ定義は次のようになります。

type Token family {
    Delim
    bool
    float64
    Number
    string
    nil
}

私が最近少しずつ得た別の例:
sql.DB.Exec 、 sql.DB.Queryなどの関数、またはinterface{}可変個引数リストを受け取る関数を呼び出す場合、各要素は特定のセット内の型を持っている必要があり、それ自体はそうではありません。スライス_、 []interface{}からそのような関数に引数を渡すときは、「spread」演算子を使用することを忘れないでください。 DB.Exec("some query with placeholders", emptyInterfaceSlice)と言うのは間違っています。 正しい方法は次のとおりです。 DB.Exec("the query...", emptyInterfaceSlice...)ここで、 emptyInterfaceSliceタイプは[]interface{}です。 このような間違いを不可能にするエレガントな方法は、この関数にValue可変引数をとらせることです。ここで、 Valueは上記のようにファミリとして定義されます。

これらの例のポイントは、 interface{}の不正確さのために、_実際の間違いが行われている_ということです。

var x int | float64 | string | rune
z = int(x) + int(y)
z = float64(x) + float64(y)

xのタイプは、 int()渡すことができるものと実際には互換性がないため、これは間違いなくコンパイラエラーであるはずです。

私はfamilyを持つという考えが好きです。 これは基本的に、リストされた型に制約された(制約された?)インターフェイスであり、コンパイラーは常に一致していることを確認し、対応するcaseのローカルコンテキスト内で変数の型を変更できます。

問題は、保証のためにコメントに頼らざるを得ないことです。
コンパイラが強制できない動作コントラクト。

それが私が少し嫌いになり始めた理由です

func foo() (..., error) 

どんな種類のエラーが返されるのかわからないからです。

具象型の代わりにインターフェースを返す他のいくつかのもの。 いくつかの機能
net.Addrを返すと、ソースコードを調べて、実際に返されるnet.Addr種類を特定し、適切に使用するのが少し難しい場合があります。 具体的な型を返すことにはそれほどマイナス面はありません(インターフェイスを実装しているため、インターフェイスを使用できる場所ならどこでも使用できるため)。
後で、メソッドを拡張して別の種類のnet.Addrを返すように計画します。 しかし、あなたの場合
APIは、 OpError返すと述べています。それなら、その部分を「コンパイル時」の仕様に入れてみませんか?

例えば:

 OpError is the error type usually returned by functions in the net package. It describes the operation, network type, and address of an error. 

いつもの? どの関数がこのエラーを返すかを正確に教えてくれません。 そして、これはタイプのドキュメントであり、関数ではありません。 Readのドキュメントには、OpErrorが返されるとはどこにも記載されていません。 また、あなたがそうするなら

err := blabla.(*OpError)

別の種類のエラーが返されるとクラッシュします。 だからこそ、これを関数宣言の一部として見たいのです。 少なくとも*OpError | errorは、
このようなエラーが発生すると、コンパイラは、チェックされていない型アサーションを実行しないようにして、将来プログラムをクラッシュさせないようにします。

ところで:ハスケルの型多型のようなシステムはまだ考慮されていましたか? または、「特性」ベースの型システム、すなわち:

func calc(a < add(a, a) a >, b a) a {
   return add(a, b)
}

func drawWidgets(widgets []< widgets.draw() error >) error {
  for _, widgets := range widgets {
    err := widgets.draw()
    if err != nil {
      return err
    }
  }
  return nil
}

a < add(a, a) aは、「aのタイプが何であれ、関数add(typeof a、typeof a)typeof a)が存在する必要があることを意味します。 < widgets.draw() error>は、「ウィジェットのタイプが何であれ、エラーを返すメソッド描画を提供する必要がある」ことを意味します。 これにより、より一般的な関数を作成できるようになります。

func Sum(a []< add(a,a) a >) a {
  sum := a[0]
  for i := 1; i < len(a); i++ {
    sum = add(sum,a[i])
  }
  return sum
}

(これは従来の「ジェネリック」と同じではないことに注意してください)。

後で別の種類のnet.Addrを返すようにメソッドを拡張する予定がある場合を除いて、具象型を返すことにはそれほどマイナス面はありません(インターフェイスを実装しているため、インターフェイスを使用できる場所ならどこでも使用できます)。 。

また、Goにはバリアントのサブタイプがないため、必要に応じてfunc() *FooErrorをfunc() errorとして使用することはできません。 これは、インターフェイスの満足度にとって特に重要です。 そして最後に、これはコンパイルされません:

func Foo() (FooVal, FooError) {
    // ...
}

func Bar(f FooVal) (BarVal, BarError) {
    // ...
}

func main() {
    foo, err := Foo()
    if err != nil {
        log.Fatal(err)
    }
    bar, err := Bar(foo) // Type error: Can not assign BarError to err (type FooError)
    if err != nil {
        log.Fatal(err)
    }
}

つまり、これを機能させるには(どういうわけか可能であれば)、はるかに高度な型推論が必要

また、Goにはバリアントのサブタイプがないため、必要に応じてfunc()* FooErrorをfunc()エラーとして使用することはできません。 これは、インターフェイスの満足度にとって特に重要です。 そして最後に、これはコンパイルされません:

これはGoで正常に機能すると思っていましたが、現在の方法ではerror使用するだけなので、これに遭遇したことはありません。 しかし、はい、この場合、これらの制限により、実際には、戻り型としてerrorを使用する必要があります。

func main() {
    foo, err := Foo()
    if err != nil {
        log.Fatal(err)
    }
    bar, err := Bar(foo) // Type error: Can not assign BarError to err (type FooError)
    if err != nil {
        log.Fatal(err)
    }
}

私はこれを可能にする言語を知りませんが(エソランを除いて)、あなたがしなければならないのは「タイプワールド」(基本的にはvariable -> typeマップです)を維持することだけです。 -「タイプワールド」でタイプを更新するだけの変数を割り当てます。

これを行うために複雑な型推論は必要ないと思いますが、変数の型を追跡する必要がありますが、とにかくそうする必要があると思います。

var int i = 0;
i = "hi";

確かに、どの変数/宣言にどのタイプがあるかを覚えておく必要があります。 i = "hi"場合は、 i 「タイプルックアップ」を行って、文字列を割り当てることができるかどうかを確認する必要があります。

タイプチェッカーがサポートしていない以外に、 func () *ConcreteErrorをfunc() error割り当てることを複雑にする実際的な問題はありますか(実行時の理由/コンパイルされたコードの理由など)? 現在、次のような関数でラップする必要があると思います。

type MyFunc func() error

type A struct {
}

func (_ *A) Error() string { return "" }

func NewA() *A {
    return &A{}
}

func main() {
    var err error = &A{}
    fmt.Println(err.Error())
    var mf MyFunc = MyFunc(func() error { return NewA() }) // type checks fine
        //var mf MyFunc = MyFunc(NewA) // doesn't type check
    _ = mf
}

func (a, b) c直面しているが、 func (x, y) z取得した場合、実行する必要があるのは、 zがc (およびa 、 bはx 、 yに割り当て可能である必要があります。これは、少なくとも型レベルでは複雑な型推論を必要としません(タイプは、別のタイプに割り当て可能/互換性があります)。 もちろん、これがランタイム/コンパイルで問題を引き起こすかどうか...わかりませんが、少なくとも型レベルを厳密に見ると、なぜこれが複雑な型推論を伴うのかわかりません。 タイプチェッカーは、 xをaに割り当てることができるかどうかをすでに知っているため、 x func () xをfunc () a割り当てることができるかどうかも簡単にわかります。 もちろん、これが簡単に不可能になる実際的な理由(ランタイム表現について考える)があるかもしれません。 (これが実際のタイプチェックではなく、ここでの本当の核心だと思います)。

理論的には、(上記のスニペットのように)関数を自動的にラップすることでランタイムの問題を回避できます(ラップされたfuncはfuncと等しくないため)_潜在的に巨大な_欠点があります。ラップします)。

私はこれを可能にする言語を知りません(まあ、esolangsを除いて)

正確ではありませんが、強力な型システムを備えた言語は通常、実際には変数をためです(したがって、識別子を再利用する機能は実際には必要ありません)。 FWIW、たとえばHaskellの型システムはこれをうまく処理できると私は主張します-少なくともFooErrorまたはBarError他のプロパティを使用していない限り、そうすべきですerrがerror型であると推測し、それを処理することができます。 もちろん、繰り返しますが、これは架空のものです。なぜなら、この正確な状況は関数型言語に簡単に移行できないからです。

とにかくそうする必要があると思います

違いは、あなたの例では、 iの最初の行の後に明確でよく理解されているタイプ( intあり、 stringを割り当てるとタイプエラーが発生することです。それに

タイプチェッカーがサポートしていない以外に、 func () *ConcreteErrorをfunc() error割り当てることを複雑にする実際的な問題はありますか(実行時の理由/コンパイルされたコードの理由など)?

実用的な問題はありますが、 func場合、おそらく解決できると思います(インターフェイスの受け渡しの仕組みと同様に、アン/ラッピングコードを発行することで)。 Goの差異について少し書き、下部に表示される実際的な問題のいくつかを説明します。 ただし、追加する価値があるとは完全には確信していません。 つまり、それ自体で重要な問題を解決できるかどうかはわかりません。

ファンクとファンクの比較を台無しにする可能性のある大きな欠点があります(ラップされたファンクはラップされたファンクと等しくないため)。

funcsは比較できません。

とにかく、TBH、これはすべてこの問題のトピックから少し外れているようです:)

参考:私はこれをやっただけです。 それは良くありませんが、それは確かにタイプセーフです。 (#19814 FWIWでも同じことができます)

私はパーティーに少し遅れていますが、私も4年間のGoの後で私の気持ちをあなたと共有したいと思います:

  • マルチバリューリターンは大きな間違いでした。
  • ゼロ可能なインターフェースは間違いでした。
  • ポインタは「オプション」の同義語ではないため、代わりに識別された共用体を使用する必要があります。
  • 必須フィールドがJSONドキュメントに含まれていない場合、JSONアンマーシャラーはエラーを返すはずでした。

過去4年間で、私はそれに関連する多くの問題を発見しました。

  • エラーが発生した場合、ガベージデータが返されます。
  • 構文の乱雑さ(エラーの場合はゼロ値を返します)。
  • マルチエラーリターン(紛らわしいAPIをお願いします、そうしないでください!)。
  • nil指すポインターを指すnil以外のインターフェース(「Goは簡単な言語です」というステートメントを悪い冗談のように聞こえさせる人々の地獄を混乱させます)。
  • チェックされていないJSONフィールドは、サーバーをクラッシュさせます(そうです!)。
  • チェックされていない戻りポインタはサーバーをクラッシュさせますが、返されたポインタがオプション(多分型)を表していることを文書化した人は誰もいないため、 nil (はい!)

ただし、これらすべての問題を修正するために必要な変更には、真に後方互換性のないGo 2.0.0(Go2ではない)バージョンが必要であり、これは実現されないと思います。 ともかく...

エラー処理は次のようになります。

// Divide returns either a float64 or an arbitrary error
func Divide(dividend, divisor float64) float64 | error {
  if dividend == 0 {
    return errors.New("dividend is zero")
  }
  if divisor == 0 {
    return errors.New("divisor is zero")
  }
  return dividend / divisor
}

func main() {
  // type-switch statements enforce completeness:
  switch v := Divide(1, 0).(type) {
  case float64:
    log.Print("1/0 = ", v)
  case error:
    log.Print("1/0 = error: ", v)
  }

  // type-assertions, however, do not:
  divisionResult := Divide(3, 1)
  if v, ok := divisionResult.(float64); ok {
    log.Print("3/1 = ", v)
  }
  if v, ok := divisionResult.(error); ok {
    log.Print("3/1 = error: ", v.Error())
  }
  // yet they don't allow asserting types not included in the union:
  if v, ok := divisionResult.(string); ok { // compile-time error!
    log.Print("3/1 = string: ", v)
  }
}

インターフェースは、識別された共用体の代わりではなく、 2つの完全に異なる動物です。 コンパイラーは、識別された共用体の型スイッチが完全であることを確認します。つまり、ケースはすべての可能な型をカバーします。これが不要な場合は、型アサーションステートメントを使用できます。

_nil値へのnil以外のインターフェース_について人々が完全に混乱しているのをよく見かけます: https : errorインターフェイスがnil指すカスタムエラータイプのポインタを指していると、人々は混乱します。これが、インターフェイスを無効にすることが間違いだったと思う理由の1つです。

インターフェースは受付係のようなもので、話しているときは人間だと知っていますが、Goでは段ボールのフィギュアである可能性があり、話しかけようとすると突然世界が崩壊します。

識別ユニオンはオプション(多分タイプ)に使用されるべきであり、インターフェースにnilポインターを渡すとパニックが発生するはず

type CustomErr struct {}
func (err *CustomErr) Error() string { return "custom error" }

func CouldFail(foo int) error | nil {
  var err *customErr
  if foo > 10 {
    // you can't return a nil pointer as an interface value
    return err // this will panic!
  }
  // no error
  return nil
}

func main() {
  // assume no error
  if err, ok := CouldFail().(error); ok {
    log.Fatalf("it failed, Jim! %s", err)
  }
}

ポインタと多分タイプは互換性がありません。 オプション型にポインターを使用すると、APIが混乱するため、不適切です。

// P returns a pointer to T, but it's not clear whether or not the pointer
// will always reference a T instance. It might be an optional T,
// but the documentation usually doesn't tell you.
func P() *T {}

// O returns either a pointer to T or nothing, this implies (but still doesn't guarantee)
// that the pointer is always expected to not be nil, in any other case nil is returned.
func O() *T | nil {}

次に、JSONもあります。 ただし、コンパイラは使用前にユニオンをチェックするように強制するため、これはユニオンでは発生しません。 必須フィールド(ポインター型のフィールドを含む)がJSONドキュメントに含まれていない場合、JSONアンマーシャラーは失敗するはずです。

type DataModel struct {
  // Optional needs to be type-checked before use
  // and is therefore allowed to no be included in the JSON document
  Optional string | nil `json:"optional,omitempty"`
  // Required won't ever be nil
  // If the JSON document doesn't include it then unmarshalling will return an error
  Required *T `json:"required"`
}

PS
現在、関数型言語の設計にも取り組んでいます。これが、そこでのエラー処理に識別された共用体を使用する方法です。

read = (s String) -> (Array<Byte> or Error) => match s {
  "A" then Error<NotFound>
  "B" then Error<AccessDenied>
  "C" then Error<MemoryLimitExceeded>
  else Array<Byte>("this is fine")
}

main = () -> ?Error => {
  // assume the result is a byte array
  // otherwise throw the error up the stack wrapped in a "type-assertion-failure" error
  r = read("D") as Array<Byte>
  log::print("data: %s", r)
}

いつかこれが実現するのを楽しみにしています。 だから私が少し助けることができるかどうか見てみましょう:

たぶん問題は、私たちが提案でカバーしすぎていることです。 価値の大部分をもたらす単純化されたバージョンを使用して、短期的に言語に追加するのがはるかに簡単になるようにすることができます。

私の見解では、この簡略化されたバージョンはnil関連しているだけです。 主なアイデアは次のとおりです(ほとんどすべてがコメントですでに言及されています):

  1. |のみを許可する 「識別された共用体」の
    <any pointer type> | nil
    ポインターの種類は次のとおりです。ポインター、関数、チャネル、スライス、およびマップ(Goポインターの種類)
  2. nilをベアポインタ型に割り当てることを禁止します。 nilを割り当てる場合、タイプは<pointer type> | nil必要があります。 例えば:
var n *int       = nil // Does not compile, wrong type
var n *int | nil = nil // Ok!

var set map[string] bool       = nil // Does not compile
var set map[string] bool | nil = nil // Ok!

var myFunc func(int) err       = nil // Nope!
var myFunc func(int) err | nil = nil // All right.

これらが主なアイデアです。 以下は、主要なものから派生したアイデアです。

  1. ベアポインタ型の変数を宣言して、初期化しないままにすることはできません。 それをしたい場合は、 | nil識別タイプを追加する必要があります
var maybeAString *string       // Wrong: invalid initial value
var maybeAString *string | nil // Good
  1. ベアポインタ型を「nilable」ポインタ型に割り当てることはできますが、その逆はできません。
var value int = 42
var barePointer *int = &value          // Valid
var nilablePointer *int | nil = &value // Valid

nilablePointer = barePointer // Valid
barePointer = nilablePointer // Invalid: Incompatible types
  1. 他の人が指摘しているように、「nilable」ポインタ型から値を取得する唯一の方法は、型スイッチを使用することです。 例えば、上記の例を次のよう、私たちは本当にの値割り当てたい場合はnilablePointerにbarePointer 、私たちは何をする必要があります:
switch val := nilablePointer.(type) {
  case *int:
    barePointer = val // Yeah! Types are compatible now. It is imposible that "val = nil"
  case nil:
    // Do what you need to do when nilablePointer is nil
}

以上です。 識別された共用体はもっと多くの用途に使用できることを知っていますが(特にエラーを返す場合)、上記の内容に固執することで、少ない労力で言語に大きな価値をもたらすことができます。必要以上に複雑にします。
この単純な提案で私が見る利点:

  • a) nilポインタエラーはありません。 さて、4つの単語がそれほど意味することはありません。 そのため、別の観点から言う必要があると感じています。NoGoプログラムでは、 nil pointer dereferenceエラーが再び発生することはありません。 💥
  • b) 「パフォーマンスvs意図」をトレードせずに、関数パラメーターへのポインターを渡すことができます。
    これが意味するのは、関数へのポインタではなく、構造体を関数に渡したい場合があるということです。なぜなら、その関数がnullを心配して、パラメータをチェックするように強制したくないからです。 。 ただし、コピーのオーバーヘッドを回避するために、通常はポインタを渡すことになります。
  • c)これ以上nilマップはありません! うん! 「安全なnil-slices」と「安全でないnil-maps」(それらに書き込もうとするとパニックになります)についての矛盾で終わります。 マップは初期化されるか、タイプmap | nilになります。この場合、タイプスイッチを使用する必要があります😃

しかし、ここには多くの価値をもたらすもう1つの無形のものがあります。それは、開発者の安心です。 ポインター、関数、チャンネル、マップなどを、ゼロであることを心配する必要がないリラックスした感覚で作業したり遊んだりすることができます。 _私はこれにお金を払うでしょう!_😂

この単純なバージョンの提案から始めることの利点は、将来的に完全な提案に進むこと、または段階的に進むことさえも妨げないことです(私にとっては、識別されたエラーリターンを可能にする次の自然なステップです) 、しかし今はそれを忘れましょう)。

1つの問題は、この単純なバージョンの提案でさえ後方互換性がないことですが、 gofixで簡単に修正できます。すべてのポインター型宣言を<pointer type> | nil置き換えるだけです。

どう思いますか? これが光を当て、言語へのnil-safetyの組み込みをスピードアップできることを願っています。 この方法(「識別された共用体」を介して)は、それを達成するためのより単純でより直交する方法であるように思われます。

@alvaroloes

ベアポインタ型の変数を宣言して、初期化しないままにすることはできません。

これが問題の核心です。 それはGoが行うことではありません-すべてのタイプにはゼロ値、ピリオドがあります。 そうでなければ、あなたは何に答えなければならないでしょう、例えばmake([]T, 100)は何をしますか? あなたが言及する他のこと(例えば、書き込みでパニックになるnilマップ)は、この基本的なルールの結果です。 (余談ですが、nil-slicesがマップよりも安全であると言うのは本当ではないと思います-nil-sliceへの書き込みはnil-mapへの書き込みと同じくらいパニックになります)。

言い換えれば、あなたの提案は、Go言語のかなり基本的な設計上の決定からかなり大きく逸脱しているため、実際にはそれほど単純ではありません。

Goが行うより重要なことは、単にすべてにゼロ値を与えるのではなく、ゼロ値を有用にすることだと思います。 ゼロマップはゼロ値ですが、役に立ちません。 実際、それは有害です。 したがって、役に立たない場合にゼロ値を許可しないのはなぜですか。 この点でGoを変更することは有益ですが、提案は確かにそれほど単純ではありません。

上記の提案は、Swiftなどのオプション/非オプションのようなもののように見えます。 かっこいいですが、次の点を除きます。

  1. それはそこにあるほとんどすべてのプログラムを壊し、修正はgofixにとって簡単ではありません。 提案ごとに、値を解凍するにはタイプスイッチが必要になるため、すべてを<pointer type> | nil置き換えることはできません。
  2. これが実際に使用可能で耐えられるようにするには、Goはこれらのオプションの周りにはるかに多くのシンタックスシュガーを持っている必要があります。 Swiftを例にとってみましょう。 この言語には、特にオプションを操作するための多くの機能があります。ガード、オプションのバインディング、オプションのチェーン、nil合体などです。Goがその方向に進むとは思いませんが、オプションを操作しないと面倒です。

したがって、役に立たない場合にゼロ値を許可しないのはなぜですか。

上記を参照。 それは、安く見えるものの中には、それに関連する非常に重要なコストがあることを意味します。

この点でGoを変更することは有益です

それには利点がありますが、それは有益であることと同じではありません。 また、害もあります。 どちらが重いかは、好みとトレードオフ次第です。 Goのデザイナーはこれを選びました。

FTR、これはこのスレッドの一般的なパターンであり、合計タイプの概念に対する主な反論の1つです。つまり、ゼロ値が何であるかを言う必要があります。 そのため、新しいアイデアは明示的に対処する必要があります。 しかし、ややイライラすることに、最近ここに投稿しているほとんどの人は、スレッドの残りの部分を読んでおらず、その部分を無視する傾向があります。

🤔ああ! 私は自分が見逃している明らかな何かがあることを知っていました。 ドー! 「シンプル」という言葉には複雑な意味があります。 さて、私の前のコメントから「単純な」単語を自由に削除してください。

イライラさせられたらごめんなさい。 私の意図は少し助けようとすることでした。 私はスレッドについていくように努めていますが、これに費やす余暇はあまりありません。

問題に戻ると、これを妨げている主な理由はゼロ値であるように思われます。
しばらく考えて多くのオプションを破棄した後、付加価値があり、言及する価値があると私が思う唯一のことは次のとおりです。

正しく思い出せば、どのタイプのゼロ値も、そのメモリ空間を0で埋めることで構成されます。
すでにご存知のように、これは非ポインター型には問題ありませんが、ポインター型のバグの原因です。

type S struct {
    n int
}
var s S 
s.n  // Fine

var s *S
s.n // runtime error

var f func(int)
f() // runtime error

だから、もし私たちが:

  • すべてのポインタタイプに役立つゼロ値を定義します
  • 初めて使用するときにのみ初期化します(遅延初期化)。

これは別の問題で示唆されていると思いますが、確かではありません。 この提案の主な抑制点に対処しているので、ここに書きます。

以下は、ポインタ型のゼロ値のリストである可能性があります。 これらのゼロ値は、値にアクセスした場合にのみ使用されることに注意してください。 これを「動的ゼロ値」と呼ぶことができ、これはポインタ型のプロパティにすぎません。

| ポインタ型| ゼロ値| 動的ゼロ値| コメント|
| --- | --- | --- | --- |
| * T | nil | new(T)|
| [] T | nil | [] T {} |
| map [T] U | nil | map [T] U {} |
| func | nil | noop | したがって、関数の動的ゼロ値は何もせず、ゼロ値を返します。 戻り値リストがerrorで終了すると、関数が「操作なし」であるというデフォルトのエラーが返されます。
| chan T | nil | make(chan T)|
| interface | nil | -| 上記のnoop関数ですべてのメソッドが初期化されるデフォルトの実装|
| 識別された共用体| nil | 最初のタイプの動的ゼロ値| |

現在、これらのタイプが初期化されると、現在のようにnilになります。 違いは、 nilにアクセスした瞬間です。 その時点で、動的ゼロ値が使用されます。 いくつかの例:

type S struct {
    n int
}
var s *S
if s == nil { // true. Nothing different happens here
...
}
s.n = 1       // At this moment the go runtime would check if it is nil, and if it is, 
              // do "s = new(S)". We could say the code would be replaced by:
/*
if s == nil {
    s = new(S)
}
s.n = 1
*/

// -------------
var pointers []*S = make([]*S, 100) // Everything as usual
for _,p := range pointers {
    p.n = 1 // This is translated to:
    /*
        if p == nil {
            p = new(S)
        }
        p.n = 1
    */
}

// ------------
type I interface {
    Add(string) (int, error)
}

var i I
n, err := i.Add("yup!") // This method returns 0, and the default error "Noop"
if err != nil { // This condition is true and the error is returned
    return err
}

私はおそらく実装の詳細と起こりうる問題を見逃していますが、最初にそのアイデアに焦点を合わせたかったのです。

主な欠点は、ポインタ型の値にアクセスするたびにnil-checkが追加されることです。 しかし、私は言うでしょう:

  • これは、私たちが得る利益との良いトレードオフです。 配列/スライスアクセスの境界チェックでも同じ状況が発生し、それがもたらす安全性に対してパフォーマンスペナルティを支払うことを受け入れます。
  • nil-checkは、配列境界チェックと同じ方法で回避できます。ポインタ型が現在のスコープで初期化されている場合、コンパイラはそれを認識し、nil-checkの追加を回避できます。

これにより、前のコメントで説明したすべての利点が得られます。さらに、値にアクセスするために型スイッチを使用する必要がなく(識別された共用体のみ)、goコードを次のようにクリーンに保つことができます。今です。

どう思いますか? これがすでに議論されている場合はお詫び申し上げます。 また、このコメント提案は、識別された共用体よりもnil関連していることを認識しています。 これをnil関連の問題に移すかもしれませんが、私が言ったように、識別された共用体の主な問題である有用なゼロ値を修正しようとするため、ここに投稿しました。

問題に戻ると、これを妨げている主な理由はゼロ値であるように思われます。

これは、対処する必要のある重要な技術的理由の1つです。 私にとっての主な理由は、段階的な修復が断固として不可能になることです(上記を参照)。 つまり、私個人としては、それらをどのように実装するかという問題ではなく、その概念に根本的に反対しているということです。
いずれにせよ、どちらの理由が「メイン」であるかは、実際には好みと好みの問題です。

だから、もし私たちが:

  • すべてのポインタタイプに役立つゼロ値を定義します
  • 初めて使用するときにのみ初期化します(遅延初期化)。

ポインタ型を渡すと失敗します。 例えば

func F(p *T) {
    *p = 42 // same as if p == nil { p = new(T) } *p = 42
}

func G() {
    var p *T
    F(p)
    fmt.Println(p == nil) // Has to be true, as F can't modify p. But now F is silently misbehaving
}

この議論はまったく新しいものではありません。 参照型がそのように動作するのには理由があり、Go開発者がそれについて考えていなかったわけではありません:)

これが問題の核心です。 それはGoが行うことではありません-すべてのタイプにはゼロ値、ピリオドがあります。 そうでなければ、例えばmake([] T、100)が何をするのか答える必要がありますか?

Tゼロ値がない場合、これ(およびnew(T) )は許可されない必要があります。 make([]T, 0, 100)から、 appendを使用してスライスにデータを入力する必要があります。 より大きく( v[:0][:100] )を再スライスすることもエラーである必要があります。 [10]Tは基本的に不可能なタイプです(配列ポインターにスライスをアサートする機能が言語に追加されていない限り)。 また、下位互換性を維持するために、既存のnilableタイプをnilable以外としてマークする方法が必要になります。

これは、ジェネリックスが追加された場合に問題を引き起こします。つまり、ある範囲を満たさない限り、すべての型パラメーターをゼロ値を持たないものとして扱う必要があります。 タイプのサブセットも、基本的にどこでも初期化追跡が必要になります。 これは、その上に合計タイプを追加しなくても、それ自体でかなり大きな変更になります。 それは確かに実行可能ですが、費用便益分析の費用面に大きく貢献します。 代わりに、初期化を単純に保つ(「常にゼロ値がある」)という意図的な選択は、初期化の追跡が1日目からの言語で行われた場合よりも、初期化を複雑にする影響があります。

これは、対処する必要のある重要な技術的理由の1つです。 私にとっての主な理由は、段階的な修復が断固として不可能になることです(上記を参照)。 つまり、私個人としては、それらをどのように実装するかという問題ではなく、その概念に根本的に反対しているということです。
いずれにせよ、どちらの理由が「メイン」であるかは、実際には好みと好みの問題です。

はい、わかりました。 他の人の視点も見る必要があります(あなたがそうしていないと言っているのではなく、私はただポイントを作っているだけです:wink :)彼らはこれを彼らのプログラムを書くための強力な何かとして見ています。 Goに適合しますか? それはアイデアがどのように実行され、言語に統合されるかに依存します、そしてそれは私たち全員がこのスレッドでやろうとしていることです(私は推測します)

ポインタ型を渡すと失敗します。 例(...)

よくわかりません。 なぜこれが失敗なのですか? 関数パラメーターに値を渡すだけです。これはたまたまnil値を持つポインターです。 次に、関数内でその値を変更します。 関数の外ではこれらの効果は見られないことが予想されます。 いくつかの例についてコメントさせてください。

// Augmenting your example with more comments:
func FCurrentGo(p *T) {
    // Here "p" is just a value, which happens to be a pointer type. Doing...
    *p = 42
    // ...without checking first for "nil" is the recipe for hiding a bug that will crash the entire program, 
    // which is exactly what is happening in current Go code bases

    // The correct code would be:
    if p == nil {
        // panic or return error
    }
    *p = 42
}

func FWithDynamicZero(p *T) {
    // Here, again, p is just a value of a pointer type. Doing...
    *p = 42
    // would allocate a new T and assign 42. It is true that this doesn't have any effect on the "outside
    // world", which could be considered "incorrect" because you expected the function to do that.
    // If you really want to be sure "p" is pointing to something valid in the "outside world", then
    // check that:
    if p == nil {
        // panic or return error
    }
    *p = 42
}

func main() {
    var p *T
    FCurrentGo(p) // This will crash the program
        FWithDynamicZero(p) // This won't have any effect on "p". This is expected because "p" is not pointing
                            // to anything. No crash here.
    fmt.Println(p == nil) // It is true, as expected
}

同様の状況が非ポインターレシーバーメソッドでも発生しており、新規参入者がGoを実行するのは混乱しています(ただし、理解してしまえば意味があります)。

type Point struct {
    x, y int
}

func (p Point) SetXY(x, y int) {
    p.x = x
    p.y = y
}

func main() {
    p := Point{x: 1, y: 2}
    p.SetXY(24, 42)

    pointerToP := &Point{x: 1, y: 2}
    pointerToP.SetXY(24, 42)

    fmt.Println(p, pointerToP) // Will print "{1 2} &{1 2}", which could confuse at first
}

したがって、次のいずれかを選択する必要があります。

  • A)クラッシュによる失敗
  • B)ポインターが関数に渡されたときに、ポインターが指す値をサイレントに変更しないことによる失敗。

どちらの場合も修正は同じです。何かを行う前にnilを確認してください。 しかし、私にとっては、A)ははるかに有害です(アプリケーション全体がクラッシュします!)。
B)「サイレントエラー」と見なすことができますが、エラーとは見なしません。 これは、関数へのポインターを渡す場合にのみ発生します。これまでに示したように、構造体が同様に動作する場合があります。 これは、それがもたらす大きなメリットを考慮せずに行います。

注:私は「自分の」アイデアを盲目的に擁護しようとしているのではなく、Goを改善しようとしているのです(これはすでに本当に良いことです)。 アイデアを価値のないものにする他のポイントがある場合、私はそれを捨てて他の方向に考え続けることを気にしません

注2:最終的に、このアイデアは「nil」値のみを対象としており、識別された共用体とは関係ありません。 だから私はこれを汚染しないように別の問題を作成します

はい、わかりました。 他の人の視点も見なければなりません(あなたがそうしていないと言っているのではなく、ただ主張しているだけです)

しかし、その剣は両方の道を切ります。 あなたは「これを抑える主な理由は"。その声明は、この提案の効果が必要かどうかについて私たち全員が同意していることを意味します。これは、行われた特定の提案を妨げる技術的な詳細であることに確かに同意できます(または、少なくとも、提案はその質問について何かを言う必要があります) )。しかし、私は、誰もが実際にそれを望んでいると想定しているパラレルワールドに静かに議論が再構成されるのは好きではありません。

なぜこれが失敗なのですか?

ポインタをとっている関数は、少なくとも頻繁に、ポインタを変更することを約束するからです。 関数がサイレントに何もしない場合、私はそれをバグと見なします。 または、少なくとも、このようにゼロパニックを防ぐことで、新しいクラスのバグが発生するというのは簡単な議論です。

そこに何かを期待する関数にnilポインターを渡すと、それはバグです-そして、そのようなバグのあるソフトウェアを静かに継続させることの実際の価値はわかりません。 私は非nilableポインタのサポートを持つことにより、コンパイル時にそのバグを引くのオリジナルのアイデアの値を見ることができますが、私はそのバグが全くキャッチされないようにできるようにポイントが表示されていません。

つまり、いわば、nilableでないポインタの実際の提案とは異なる問題に取り組んでいます。その提案の場合、ランタイムパニックは問題ではなく、単なる症状です。問題は、誤って渡されるバグです。 nilは、それを予期せず、このバグは実行時にのみキャッチされるものになります。

同様の状況は、非ポインター受信機方式でも発生しています。

私はこのアナロジーを購入しません。 IMOを検討することは完全に合理的です

func Foo(p *int) { *p = 42 }

func main() {
    var v int
    Foo(&v)
    if v != 42 { panic("") }
}

正しいコードであるために。 考えるのは合理的ではないと思います

func Foo(v int) { v = 42 }

func main( ){
    var v int
    Foo(v)
    if v != 42 { panic("") }
}

正しいこと。 たぶん、あなたがGoの完全な初心者であり、すべての値が参照である言語から来ている場合(私は正直にそれを見つけるのに苦労しています-PythonとJavaでさえほとんどの値を参照します)。 しかし、IMOは、その場合の最適化は無駄であり、人々はポインターと値にある程度精通してnilポインターを静的に防ぐための全体的な議論であり、意図せずにポインターをゼロにし、実行時に正しい外観のコードが失敗するのは簡単すぎるということです。

どちらの場合も修正は同じです。何かを行う前にnilを確認してください。

IMOの現在のセマンティクスの修正は、nilをチェックせず、誰かがnilを渡した場合にバグと見なすことです。 あなたの例では、

// The correct code would be:
if p == nil {
    // panic or return error
}
*p = 42

しかし、私はそのコードが正しいとは思いません。 nil逆参照すでにパニックになっているため、 nil checkは何もしません。

しかし、私にとっては、A)ははるかに有害です(アプリケーション全体がクラッシュします!)。

それは問題ありませんが、多くの人がこれに強く反対することを覚えておいてください。 私は個人的に、破損したデータや間違った仮定を続けるよりも、クラッシュが常に望ましいと考えています。 理想的な世界では、私のソフトウェアにはバグがなく、クラッシュすることもありません。 あまり理想的ではない世界では、私のプログラムにはバグがあり、検出されたときにクラッシュすることで安全に失敗します。 最悪の世界では、私のプログラムにはバグがあり、それらが発生したときに大混乱を引き起こし続けます。

しかし、その剣は両方の道を切ります。 あなたは「これを抑える主な理由は」と言いました。 その声明は、私たち全員がこの提案の効果を望むかどうかについて合意していることを意味します。 私は確かに、それが行われた特定の提案を妨げる技術的な詳細であることに同意することができます(または、少なくとも、提案はその質問について何かを言うべきです)。 しかし、私は、誰もが実際にそれを望んでいると想定しているパラレルワールドに静かに議論が再構成されるのは好きではありません。

まあ、私はこれを暗示したくありませんでした。 それが理解されたのなら、私は正しい言葉を選んでいないかもしれません、そして私は謝罪します。 考えられる解決策についていくつかのアイデアを提供したかっただけです。それだけです。

私は_ "...これを妨げている主な理由は...." _あなたの文章に基づいているようです_ "これが問題の核心です" _ゼロ値を参照しています。 そのため、ゼロ値がこれを妨げる主なものであると思いました。 だからそれは私の悪い仮定でした。

nilサイレントに処理するのか、コンパイル時にチェックするのかについて:コンパイル時にチェックする方がよいことに同意します。 「動的ゼロ値」は、all-types-should-have-zero-valueの問題に対処することに焦点を当てたときの、元の提案の単なる反復でした。 追加の動機は、それが差別化された組合の提案の主な抑制でもあると私が考えたということでした。
nil関連の問題のみに焦点を当てる場合は、コンパイル時にnil以外のポインター型をチェックすることをお勧めします。

ある時点で、私たち(「私たち」とは、Goコミュニティ全体を指します)は、ある種の変更を受け入れる必要があると思います。 例: nilエラーを完全に回避するための適切な解決策があり、これを妨げるのが「すべてのタイプの値がゼロで、0で構成されている」という設計上の決定である場合、そのアイデアを検討できます。それが価値をもたらすのであれば、その決定にいくつかの微調整や変更を加えることです。

私がこれを言っている主な理由はあなたの文です_ "すべてのタイプはゼロ値、終止符" _。 私は通常、「終止符を書く」のは好きではありません。 誤解しないでください! 私はあなたがそのように考えることを完全に受け入れます、それは私の考え方です:彼らはより良い解決策につながる可能性のある道を隠すことができるので私は教義を好みません。

最後に、これに関して:

それは問題ありませんが、多くの人がこれに強く反対することを覚えておいてください。 私は個人的に、破損したデータや間違った仮定を続けるよりも、クラッシュが常に望ましいと考えています。 理想的な世界では、私のソフトウェアにはバグがなく、クラッシュすることもありません。 あまり理想的ではない世界では、私のプログラムにはバグがあり、検出されたときにクラッシュすることで安全に失敗します。 最悪の世界では、私のプログラムにはバグがあり、それらが発生したときに大混乱を引き起こし続けます。

私はこれに完全に同意します。 大声で失敗することは、静かに失敗するよりも常に優れています。 ただし、Goには問題があります。

  • 何千ものゴルーチンを含むアプリがある場合、そのうちの1つで処理されないパニックが発生すると、プログラム全体がクラッシュします。 これは、パニックになるスレッドだけがクラッシュする他の言語とは異なります

それはさておき(非常に危険ですが)、考えは、障害のカテゴリ全体( nil関連の障害)を回避することです。

それでは、これを繰り返し続けて、解決策を見つけてみましょう。

時間とエネルギーをありがとう!

haskellの合計型ではなく、rustの識別された共用体構文を確認したいと思います。これにより、バリアントの命名が可能になり、より優れたパターンマッチング構文の提案が可能になります。
実装は、タグフィールド(uintタイプ、バリアントの数によって異なります)とユニオンフィールド(データを保持する)を使用した構造体のように実行できます。
この機能は、バリアントのクローズドセットに必要です(コンパイル時のチェックにより、状態表現がはるかに簡単でクリーンになります)。 インターフェイスとその表現に関する質問によると、インターフェイスはいくつかの要件に適合する任意のタイプに関するものであるため、合計タイプでの実装は、合計タイプの別のケースよりも終わってはならないと思いますが、合計タイプのユースケースは異なります。

構文:

type Type enum {
         Tuple (int,int),
         One int,
         None,
};

上記の例では、サイズはsizeof((int、int))になります。
パターンマッチングは、次のように、新しく作成された一致演算子を使用して、または既存のスイッチ演算子内で実行できます。

var a Type
switch (a) {
         case Tuple{(b,c)}:
                    //do something
         case One{b}:
                    //do something else
         case None:
                    //...
}

作成構文:
var a Type = Type{One=12}
列挙型インスタンスの構築では、指定できるバリアントは1つだけであることに注意してください。

ゼロ値(問題):
名前はアルファベット順に並べ替えることができます。列挙型のゼロ値は、並べ替えられたメンバーリストの最初のメンバーのタイプのゼロ値になります。

ゼロ値問題のPSソリューションは、主に合意によって定義されます。

合計のゼロ値を最初のユーザー定義の合計フィールドのゼロ値として保持することは、おそらく混乱が少ないと思います。

合計のゼロ値を最初のユーザー定義の合計フィールドのゼロ値として保持することは、おそらく混乱が少ないと思います。

しかし、ゼロ値を作成することはフィールド宣言の順序に依存します、私はそれがより悪いと思います。

誰かがデザインドキュメントを書いた?

私は1つを持っている:
19412-discriminated_unions_and_pattern_matching.md.zip

私はこれを変更しました:

合計のゼロ値を最初のユーザー定義の合計フィールドのゼロ値として保持することは、おそらく混乱が少ないと思います。

今、私の提案では、ゼロ値(問題)に関する合意がurandomsの位置に移動しました。

UPD:設計ドキュメントが変更され、マイナーな修正が行われました。

最近、2つのユースケースがあり、組み込みの合計型が必要でした。

  1. 予想通り、ASTツリー表現。 最初は一目で解決策となるライブラリを見つけましたが、彼らのアプローチは、多くのnilableフィールドを持つ大きな構造体を持つことでした。 両方の世界で最悪のIMO。 もちろん型安全性はありません。 代わりに私たち自身を書いた。
  2. 事前定義されたバックグラウンドタスクのキューがありました。現在開発中の検索サービスがあり、検索操作が長すぎる可能性があるなどです。そこで、検索インデックス操作タスクをチャネルに送信して、バックグラウンドで実行することにしました。 次に、ディスパッチャはそれらをさらにどうするかを決定します。 ビジターパターンを使用することもできますが、単純なgRPCリクエストには明らかにやり過ぎです。 そして、それはディスパッチャと訪問者の間に結びつきをもたらすので、少なくとも言うことは特に明確ではありません。

どちらの場合も、次のようなものを実装しました(2番目のタスクの例)。

type Task interface {
    task()
}

type SearchAdd struct {
    Ctx   context.Context
    ID    string
    Attrs Attributes
}

func (SearchAdd) task() {}

type SearchUpdate struct {
    Ctx         context.Context
    ID          string
    UpdateAttrs UpdateAttributes
}

func (SearchUpdate) task() {}

type SearchDelete struct {
    Ctx context.Context
    ID  string
}

func (SearchDelete) task() {}

その後

task := <- taskChannel

switch v := task.(type) {
case tasks.SearchAdd:
    resp, err := search.Add(task.Ctx, &search2.RequestAdd{…}
    if err != nil {
        log.Error().Err(err).Msg("blah-blah-blah")
    } else {
        if resp.GetCode() != search2.StatusCodeSuccess  {
            …
        } 
    }
case tasks.SearchUpdate:
    …
case tasks.SearchDelete:
    …
}

これはほとんど良いです。 悪い点は、Goが完全な型安全性を提供しないことです。つまり、新しい検索インデックス操作タスクが追加された後、コンパイルエラーは発生しません。

合計タイプを使用するIMHOは、通常、訪問者とディスパッチャのセットで解決されるこの種のタスクの最も明確なソリューションです。訪問者の機能は多くなく、小さく、訪問者自体は固定タイプです。

私は本当に何かのようなものを持っていると信じています

type Task oneof {
    // SearchAdd holds a data for a new record in the search index
    SearchAdd {
        Ctx   context.Context
        ID    string
        Attrs Attributes   
    }

    // SearchUpdate update a record
    SearchUpdate struct {
        Ctx         context.Context
        ID          string
        UpdateAttrs UpdateAttributes
    }

    // SearchDelete delete a record
    SearchDelete struct {
        Ctx context.Context
        ID  string
    }
}

+

switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

Goが現在の状態で許可している他のどのアプローチよりも、精神的にははるかにGoishになります。 ハスケルリッシュのパターンマッチングは必要ありません。特定のタイプまでのユイストダイビングで十分です。

痛い、構文提案のポイントを逃した。 修理する。

2つのバージョン。1つは汎用合計タイプ用、もう1つは列挙用の合計タイプです。

一般的な合計タイプ

type Sum oneof {
    T₁ TypeDecl₁
    T₂ TypeDecl₂
    …
    Tₙ TypeDeclₙ
}

ここで、 T₁ … Tₙは、 Sumと同じレベルの型定義であり( oneofはそれらをスコープ外に公開します)、 Sum宣言しますT₁ … Tₙだけが満たすいくつかのインターフェース。

処理は(type)スイッチと同様ですが、 oneofオブジェクトに対して暗黙的に実行され、すべてのバリアントがリストされているかどうかをコンパイラーでチェックする必要があります。

実際の型の安全な列挙

type Enum oneof {
    Value = iota
}

明示的にリストされた値のみが列挙型であり、他のすべてがそうではないことを除いて、constsのiotaと非常に似ています。

switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

Goが現在の状態で許可している他のどのアプローチよりも、精神的にははるかにGoishになります。 ハスケルリッシュのパターンマッチングは必要ありません。特定のタイプまでのユイストダイビングで十分です。

task変数の意味を操作することは、許容できるものの、良い考えではないと思います。

```go
switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

Goが現在の状態で許可している他のどのアプローチよりも、精神的にははるかにGoishになります。 ハスケルリッシュのパターンマッチングは必要ありません。特定のタイプまでのユイストダイビングで十分です。

タスク変数の意味を操作することは、許容できるものの、良い考えではないと思います。
`` `

それでは、訪問者の皆さん、頑張ってください。

@sirkon訪問者についてどういう意味ですか? 私はこの構文が好きでしたが、スイッチは次のように書く必要があります:

switch task {
case Task.SearchAdd:
    // task is Task.SearchAdd in this scope
case Task.SearchUpdate:
case Task.SearchDelete:
}

また、 Task価値がない場合はどうなりますか? 例えば:

var task Task

nilでしょうか? もしそうなら、 switchには追加のcase nilですか?
それとも最初のタイプに初期化されますか? 型宣言の順序が以前とは異なる方法で重要になるため、これは厄介ですが、数値列挙型ではおそらく問題ありません。

これはswitch task.(type)と同等だと思いますが、スイッチではすべてのケースが存在する必要がありますよね? のように..1つのケースを見逃した場合、コンパイルエラー。 また、 default許可されていません。 そうですか?

訪問者とはどういう意味ですか?

私は、それらがそのような種類の機能のためのGoの唯一のタイプセーフオプションであることを意味しました。 特定のケースのセット(事前定義された選択肢の数が限られている)の場合は、さらに悪化します。

また、タスクの価値がないことは何でしょうか? 例えば:

var task Task

私はそれがこのようにGoでnilableタイプであるべきだと思います

それとも最初のタイプに初期化されますか?

特に意図された目的のためにはあまりにも奇妙だろう。

これはスイッチタスク(タイプ)と同等だと思いますが、スイッチではすべてのケースが存在する必要がありますよね? のように..1つのケースを見逃した場合、コンパイルエラー。

はい、そうです。

また、デフォルトは許可されていません。 そうですか?

いいえ、デフォルトは許可されています。 ただし、お勧めしません。

PS私はGo @ ianlancetaylorや他のGoの人々が合計型について持っているという考えを持っているようです。 Goはnil値を制御できないため、nilnessを使用するとNPDが発生しやすくなるようです。

ゼロの場合は、問題ないと思います。 case nilがswitchステートメントの要件であることが望ましいです。 前にif task != nil実行することも問題ありませんが、私はそれがあまり好きではありません:|

これも許可されますか?

type Foo oneof {
  A = 3
  B = "3"
  C = 3.0
  D = struct { E bool }{ true }
}

これも許可されますか?

type Foo oneof {
  A = 3
  B = "3"
  C = 3.0
  D = struct { E bool }{ true }
}

ええと、定数はありません、ただ

type Foo oneof {
    A <type reference>
}

また

type Foo oneof {
    A = iota
    B
    C
}

また

type Foo oneof {
    A = 1
    B = 2
    C = 3
}

iotasと値の組み合わせはありません。 または、値の制御と組み合わせて、それらを繰り返さないでください。

FWIW、最新のジェネリック設計について私が興味深いと思ったのは、ゼロ値に関する落とし穴を避けながら、合計型のユースケースの少なくともいくつかに対処するための別の場所を示したことです。 ある意味で和っぽい論理和コントラクトを定義しますが、型ではなく制約を記述するため、ゼロ値を持つ必要はありません(その型の変数を宣言できないため)。 つまり、コンパイル時にそのセットの型チェックを使用して、可能な型の限られたセットを受け取る関数を作成することは少なくとも可能です。

もちろん、現状のままでは、ここで意図されているユースケースでは設計は実際には機能しません。論理和は、基になる型またはメソッドのみをリストするため、まだ広く開かれています。 そしてもちろん、一般的な考え方としても、一般的な(または総和的な)関数や値をインスタンス化できないため、かなり制限されています。 しかし、IMOは、合計のユースケースのいくつかに対処するための設計スペースが、合計タイプ自体のアイデアよりもはるかに大きいことを示しています。 したがって、合計について考えることは、特定の問題ではなく、特定の解決策に固執することになります。

ともかく。 面白いと思っただけです。

@Meroviusは、合計型のいくつかのユースケースを処理できる最新の汎用設計について優れた点を示しています。 たとえば、スレッドの前半で使用されたこの関数は次のとおりです。

func addOne(x int|float64) int|float64 {
    switch x := x.(type) {
    case int:
        return x + 1
    case float64:
         return x + 1
    }
}

になります:

contract intOrFloat64(T) {
    T int, float64
}

func addOne(type T intOrFloat64) (x T) T {
    return x + 1
}

合計型自体に関する限り、ジェネリックが最終的に着陸した場合、それらを導入することの利点がGoなどの単純な言語のコストを上回るかどうかについて私は今よりもさらに疑わしいでしょう。

ただし、何かが行われる場合、最も単純で混乱の少ないソリューションIMOは、 @ ianlancetaylorの「制限付きインターフェイス」のアイデアです。これは、現在の「制限なし」インターフェイスとまったく同じ方法で実装されますが、満足することしかできません。指定されたタイプによって。 実際、ジェネリックデザインの本からリーフを取り出し、型制約をインターフェイスブロックの最初の行にした場合:

type intOrFloat64 interface{ type int, float64 }    

その場合、新しいキーワード( restrict )はまったく必要ないため、これは完全な下位互換性があります。 それでもインターフェイスにメソッドを追加することはできますが、指定されたすべてのタイプでメソッドがサポートされていない場合は、コンパイル時エラーになります。

制限されたインターフェイスタイプの変数に値を割り当てるのにまったく問題はありません。 RHSの値の型(または型指定されていないリテラルのデフォルトの型)が、指定された型の1つと完全に一致しなかった場合、単にコンパイルされません。 だから私たちは持っているでしょう:

var v1 intOrFloat64 = 1        // compiles, dynamic type int
var v2 intOrFloat64 = 1.0      // compiles, dynamic type float64
var v3 intOrFloat64 = 1 + 2i   // doesn't compile, complex128 is not a specified type

タイプスイッチが指定されたタイプと一致しない場合はコンパイル時エラーになり、網羅性チェックが実装される可能性があります。 ただし、制限されたインターフェイス値を現在のように動的型の値に変換するには、型アサーションが必要です。

ゼロ値は、このアプローチでは問題になりません(または、いずれにせよ、今日の一般的なインターフェイスの場合ほど問題にはなりません)。 制限されたインターフェイスのゼロ値はnil (現在何も含まれていないことを意味します)であり、指定されたタイプはもちろん、内部的に独自のゼロ値を持ち、 nil nilableタイプの場合。

先に述べたように、これはすべて完全に機能しているように見えますが、コンパイル時の安全性は、さらに複雑にするだけの価値があります。自分のプログラミングで合計型の必要性を実際に感じたことがないので、疑問があります。

IIUCジェネリックは動的タイプではないため、この全体的なポイントは成り立ちません。 ただし、インターフェイスがコントラクトとして機能することを許可されている場合(これは疑わしいです)、sumtypeが何であるか(おそらくそうではないか?)である徹底的なチェックと列挙型を解決しません。

@ alanfo 、 @ Meroviusキューをありがとう。 この議論がこの方向に向かっているのは興味深いことです。

私はほんの一瞬視点を変えるのが好きです:コントラクトを上記のタイプ制限を許可するパラメーター化されたインターフェースで完全に置き換えることができない理由を理解しようとしています。 現時点では、このような「合計」インターフェイスタイプを「合計」タイプとして使用すると、可能な動的値をインターフェイスに列挙されているタイプに正確に制限する必要があることを除いて、技術的な理由はわかりません。同じインターフェイスがコントラクト位置で使用されました-インターフェイスの列挙型は、合理的に有用な一般的な制限であるために、基になる型として機能する必要があります。

@Goodwine
@Meroviusが前回の投稿で明確に説明しているように、ジェネリックの設計が合計型でやりたいことすべてに対処することを示唆していませんでした。 特に、ジェネリックスに対して提案された型制約は、組み込み型とそれらから派生した型のみを対象としています。 合計タイプの観点から、前者は狭すぎ、後者は広すぎます。

ただし、ジェネリックスの設計により、コンパイラーが強制する限られたタイプのセットで動作する関数を記述できるようになります。これは、現時点ではまったく実行できないことです。

制限されたインターフェイスに関する限り、コンパイラは使用できる正確な型を知っているため、typeswitchステートメントで徹底的なチェックを行うことが可能になります。

@Griesemer

ドラフトジェネリック設計ドキュメントが(「コントラクトの代わりにインターフェイスを使用しない理由」のセクションで)ジェネリック制約を表現するために前者よりも優れた手段と見なされた理由を非常に明確に説明していると思ったので、あなたの言うことに戸惑います。

特に、コントラクトはタイプパラメータ間の関係を表すことができるため、必要なコントラクトは1つだけです。 そのタイプパラメータのいずれも、コントラクトにリストされているメソッドのレシーバタイプとして使用できます。

パラメータ化されているかどうかに関係なく、インターフェイスについても同じことは言えません。 制約がある場合は、タイプパラメータごとに個別のインターフェイスが必要になります。

これにより、グラフの例が示すように不可能ではありませんが、インターフェイスを使用して型パラメータ間の関係を表現するのがより厄介になります。

ただし、インターフェイスに型制約を追加し、それらを汎用型と合計型の両方の目的で使用することで、「1つの石で2羽の鳥を殺す」ことができると考えている場合は、(あなたが言及した問題は別として)これが技術的に実現可能であることはおそらく正しいでしょう。

ジェネリックスに関する限り、インターフェイスタイプの制約に「非組み込み」タイプを含めることができるかどうかは実際には問題ではないと思いますが、それらを正確なタイプに制限する方法を見つける必要があります(派生タイプも同様ではありません)。したがって、合計タイプに適しています。 現在のキーワードに固執する場合は、後者にconst typeを使用することもできます(またはconstだけを使用することもできます)。

@griesemerパラメータ化されたインターフェースタイプがコントラクトの直接の代替ではない理由はいくつかあります。

  1. 型パラメーターは、他のパラメーター化された型と同じです。
    のようなタイプで

    type C2(type T C1) interface { ... }
    

    タイプパラメータTは、インターフェイス自体の外部に存在します。 Tとして渡される型引数は、コントラクトC1を満たすためにすでに認識されている必要があり、インターフェイスの本体はTさらに制約することはできません。 これは、契約に渡された結果として契約の本体によって制約される契約パラメーターとは異なります。 これは、関数の各型パラメーターを、他の型パラメーターの制約にパラメーターとして渡す前に、個別に制約する必要があることを意味します。

  2. インターフェイスの本体でレシーバータイプに名前を付ける方法はありません。
    インターフェースでは、次のようなものを記述できるようにする必要があります。

    type C3(type U C1) interface(T) {
        Add(T) T
    }
    

    ここで、 Tはレシーバータイプを示します。

  3. 一部のインターフェイスタイプは、一般的な制約としてそれ自体を満たさないでしょう。
    レシーバータイプの複数の値に依存する操作は、動的ディスパッチと互換性がありません。 したがって、これらの操作はインターフェイス値では使用できません。 これは、インターフェイスがそれ自体を満たさないことを意味します(たとえば、同じインターフェイスによって制約された型パラメータへの型引数として)。 これは驚くべきことです。 1つの解決策は、そのようなインターフェイスのインターフェイス値の作成をまったく許可しないことですが、これはとにかくここで想定されているユースケースを許可しません。

基礎となる型制約と型ID制約を区別することに関しては、機能する可能性のある1つの方法があります。 次のようなカスタム制約を定義できると想像してください。

contract (T) indenticalTo(U) {
    *T *U
}

(ここでは、発明された表記法を使用して、単一の型を「レシーバー」として指定しています。レシーバー付きの関数が「メソッド」と発音されるのと同じように、明示的なレシーバー型を持つコントラクトを「制約」と発音します。コントラクト名の後のパラメーターは通常のタイプのパラメーターであり、制約の本体の制約句の左側に表示することはできません。)

リテラルポインタ型の基になる型はそれ自体であるため、この制約はTがUと同一であることを意味します。 これは制約として宣言されているため、 (identicalTo(int)), (identicalTo(uint)), ...を制約論理和として記述できます。

コントラクトはある種の合計タイプを表現するのに役立つかもしれませんが、それらを使用して一般的な合計タイプを表現することはできないと思います。 ドラフトから見たものから、具体的なタイプをリストする必要があるため、次のようなものを書くことはできません。

contract Foo(T, U) {
    T U, int64
}

未知のタイプと1つ以上の既知のタイプの一般的な合計タイプを表現する必要があるのはどれですか。 設計でそのような構成が許可されていたとしても、両方のパラメーターが事実上同じものになるため、使用すると奇妙に見えます。

インターフェイスが型制約を含むように拡張され、デザインのコントラクトを置き換えるために使用された場合、ドラフトジェネリックデザインがどのように変化するかについてもう少し考えていました。

異なる数の型パラメーターを検討する場合、状況を分析するのがおそらく最も簡単です。

パラメータなし

変化なし :)

1つのパラメータ

ここでは実際の問題はありません。 (非ジェネリックなものとは対照的に)パラメーター化されたインターフェースは、インターフェースをインスタンス化するために、それ自体を参照する型パラメーターおよび/または他のいくつかの独立した固定型が必要な場合にのみ必要になります。

2つ以上のパラメーター

前述のように、制約が必要な場合は、各タイプパラメータを個別に制約する必要があります。

パラメータ化されたインターフェイスは、次の場合にのみ必要になります。

  1. typeパラメーターはそれ自体を参照しました。

  2. インターフェイスは、タイプパラメータセクションで_すでに宣言されている_別のタイプパラメータを参照していました(おそらく、ここでバックトラックしたくないでしょう)。

  3. インターフェイスをインスタンス化するには、他のいくつかの独立した固定タイプが必要でした。

これらのうち(2)は、グラフの例のように相互に参照する型パラメーターを除外するため、実際には唯一の厄介なケースです。 一方が最初に「ノード」または「エッジ」を宣言したかどうかにかかわらず、その制約インターフェースでは、もう一方を型パラメーターとして渡す必要があります。

ただし、設計ドキュメントに示されているように、パラメータ化されていない(自分自身を参照していないため)NodeInterfaceとEdgeInterfaceをトップレベルで宣言することで、これを回避できます。宣言の順序。 次に、これらのインターフェイスを使用して、Graph構造体の型パラメーターとそれに関連する「New」メソッドの型パラメーターを制約できます。

ですから、契約のアイデアがもっと良いとしても、ここに克服できない問題があるようには見えません。

おそらく、 comparableは、コントラクトではなく、組み込みのインターフェースになる可能性があります。

もちろん、インターフェースは、すでに可能であるように、相互に埋め込むことができます。

インターフェイスメソッドのレシーバーを指定できないため、ポインターメソッドの問題(コントラクトでこれらを指定する必要がある場合)をどのように処理するかはわかりません。 おそらく、ポインタメソッドを示すために、いくつかの特別な構文(メソッド名の前にアスタリスクを付けるなど)が必要になります。

ここで@stevenblenkinsopの観察に目を向けると、パラメーター化されたインターフェースが独自の型パラメーターをまったく制約できない場合、

個人的には、一部のインターフェイスタイプが一般的な制約としてそれ自体を満たすことができないことは驚くべきことではないと思います。 インターフェイスタイプはどのような場合でも有効なレシーバータイプではないため、メソッドを持つことはできません。

組み込み関数のsidentityTo()というStevenのアイデアは機能しますが、合計型を指定するのに時間がかかる可能性があるように思われます。 型の行全体を正確であると指定できる構文が望ましいです。

もちろん、 @ urandomは正しいです。現在、ジェネリックドラフトが存在するため、具体的な(組み込みまたは集約組み込み)型のみをリストできます。 ただし、ジェネリック型と合計型の両方に制限付きインターフェイスを代わりに使用する場合は、これを明らかに変更する必要があります。 したがって、このようなことが統一された環境で許可されることを排除するつもりはありません。

interface Foo(T) {
    const type T, int64  // 'const' indicates types are exact i.e. no derived types
}

なぜ私たちは、彼らの不在の別の散歩を発明する代わりに、言語に差別化された連合を単に追加することができないのですか?

@griesemer気付いているかもしれないし、気付いていないかもしれませんが、私は最初からインターフェースを使用して制約を指定することに賛成してきました:)その投稿で私が提起した正確なアイデアが進むべき道だとはもはや思いません(特に物事オペレーターに対応することをお勧めします)。 そして、私は契約設計の最新の反復が前のものよりもはるかに好きです。 しかし、一般的に、制約としての(おそらく拡張された)インターフェースは実行可能であり、検討する価値があることに完全に同意します。

@urandom

一般的な合計タイプをそれらで表現できるとは思いません

繰り返しになりますが、私のポイントは「それらを使用して合計型を作成できる」ではなく、「合計型がそれらを使用して解決するいくつかの問題を解決できる」ということでした。 問題の説明が「合計タイプが必要」である場合、合計タイプが唯一の解決策であることは驚くべきことではありません。 あなたが彼らと一緒に解決したい問題に焦点を合わせれば、彼らなしでできるかもしれないことを単に表現したかったのです。

@alanfo

これにより、グラフの例が示すように不可能ではありませんが、インターフェイスを使用して型パラメータ間の関係を表現するのがより厄介になります。

「ぎこちない」は主観的だと思います。 個人的には、パラメーター化されたインターフェースを使用する方が自然であり、グラフの例は非常に良い例だと思います。 私にとって、グラフはエンティティであり、一種のエッジと一種のノードの間の関係ではありません。

しかし、TBH、私はそれらのどちらも実際には多かれ少なかれ厄介だとは思いません-あなたはほとんどまったく同じことを表現するためにほとんどまったく同じコードを書きます。 そしてFWIW、これには先行技術があります。 Haskellの型クラスはインターフェースのように動作し、そのwiki記事が指摘しているように、マルチパラメーター型クラスを使用して型間の関係を表現することはごく普通のことです。

@stevenblenkinsop

インターフェイスの本体でレシーバータイプに名前を付ける方法はありません。

あなたがそれに対処する方法は、usage-siteのtype-argumentsです。 NS

type Adder(type T) interface {
    Add(t T) T
}

func Sum(type T Adder(T)) (vs []T) T {
    var t T
    for _, v := range vs {
        t = t.Add(v)
    }
    return t
}

これには、自己参照型パラメーターを許可できるように、統合がどのように機能するかについて注意が必要ですが、機能させることができると思います。

あなたの1.と3.私は本当に理解していません、私は認めなければなりません。 私はいくつかの具体的な例から利益を得るでしょう。


とにかく、この議論を続けることの終わりにこれを落とすことは少し不誠実です、しかしこれはおそらくジェネリックデザインの細部を通して話すのに正しい問題ではありません。 この問題のデザインスペースを少し広げるためにそれを取り上げただけです:)新しいアイデアが合計タイプに関する議論に持ち込まれてからしばらく経ったように感じたからです。

```go
switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

Goが現在の状態で許可している他のどのアプローチよりも、精神的にははるかにGoishになります。 ハスケルリッシュのパターンマッチングは必要ありません。特定のタイプまでのユイストダイビングで十分です。
タスク変数の意味を操作することは、許容できるものの、良い考えではないと思います。

それでは、訪問者の皆さん、頑張ってください。

Goではパターンマッチングができないと思うのはなぜですか? パッテンマッチングの例がない場合は、たとえばRustを参照してください。

@Merovius re:「私にとって、グラフはエンティティです」

それはコンパイル時のエンティティですか、それとも実行時に表現がありますか? コントラクトとインターフェイスの主な違いの1つは、インターフェイスがランタイムオブジェクトであるということです。 ガベージコレクションに参加し、他のランタイムオブジェクトへのポインタなどを持っています。 コントラクトからインターフェイスへの変換は、含まれるノード/頂点へのポインター(いくつですか?)を持つ新しい一時ランタイムオブジェクトを導入することを意味します。これは、グラフ関数のコレクションがある場合は厄介に思えます。関数のニーズに応じて、グラフのさまざまな部分を独自の方法で指すパラメーターを取得します。

「グラフ」はオブジェクトのように見え、コントラクトは実際には特定のサブグラフを指定していないため、コントラクトに「グラフ」を使用すると直感が誤解される可能性があります。 これは、数学や法律の場合と同様に、後で使用する一連の用語を定義するようなものです。 場合によっては、グラフコントラクトとグラフインターフェイスの両方が必要になることがあり、その結果、迷惑な名前の衝突が発生します。 でも、頭のてっぺんからこれ以上の名前は思いつかない。

対照的に、識別された共用体はランタイムオブジェクトです。 実装を制約することはありませんが、それらの配列がどのようなものかを考える必要があります。 N項目の配列にはN個の識別子とN個の値が必要であり、さまざまな方法で実行できます。 (Juliaには興味深い表現があり、識別子と値を別々の配列に配置することもあります。)

interface{}スキームを使用して、現在至る所で発生しているエラーの削減を提案しますが、 |演算子の連続入力を削除するには、次のことをお勧めします。

type foobar union {
    int
    float64
}

多くのinterface{}をこの種の型安全性に置き換えるというユースケースだけでも、ライブラリにとって大きなメリットになります。 暗号ライブラリの半分を見るだけでこれを使用できます。

次のような問題:ああ、 ecdsa.PrivateKeyではなく*ecdsa.PrivateKey -これは、ecdsa.PrivateKeyのみがサポートされているという一般的なエラーです。 これらが明確な共用体型でなければならないという単純な事実は、型の安全性をかなり向上させるでしょう。

この提案はint|float64と比較してより多くの_スペース_を占有しますが、ユーザーはこれについて考える必要があります。 コードベースをよりクリーンに保つ。

interface{}スキームを使用して、現在至る所で発生しているエラーの削減を提案しますが、 |演算子の連続入力を削除するには、次のことをお勧めします。

type foobar union {
    int
    float64
}

多くのinterface{}をこの種の型安全性に置き換えるというユースケースだけでも、ライブラリにとって大きなメリットになります。 暗号ライブラリの半分を見るだけでこれを使用できます。

次のような問題:ああ、 ecdsa.PrivateKeyではなく*ecdsa.PrivateKey -これは、ecdsa.PrivateKeyのみがサポートされているという一般的なエラーです。 これらが明確な共用体型でなければならないという単純な事実は、型の安全性をかなり向上させるでしょう。

この提案はint|float64と比較してより多くの_スペース_を占有しますが、ユーザーはこれについて考える必要があります。 コードベースをよりクリーンに保つ。

これ(コメント)を見てください、それは私の提案です。

実際、私たちは両方のアイデアを言語に導入することができます。 これにより、ADTを実行するための2つのネイティブな方法が存在しますが、構文は異なります。

機能、特にパターンマッチングについての私の提案は、古いコードベースの機能から利益を得る互換性と能力についてです。

しかし、やり過ぎのように見えますね。

また、合計タイプは、デフォルト値としてnilを持つようにすることができます。 もちろん、すべてのスイッチでnilケースが必要になります。
パターンマッチングは次のように実行できます。
- 宣言

type U enum{
    A(int64),
    B(string),
}

-マッチング

...
var a U
...
switch a {
    case A{b}:
         //process b here
    case B{b}:
         //...
    case nil:
         //...
}
...

パターンマッチングが気に入らない場合は、上記のsirkonの提案を参照してください。

また、合計タイプは、デフォルト値としてnilを持つようにすることができます。 もちろん、すべてのスイッチでnilケースが必要になります。

コンパイル時に開始されていない値を禁止する方が簡単ではないでしょうか。 初期化された値が必要な場合は、それを合計タイプに追加できます。

type U enum {
  None
  A(string)
  B(uint64)
}
...
var a U.None
...
switch a {
  case U.None: ...
  case U.A(str): ...
  case U.B(i): ...
}

また、合計タイプは、デフォルト値としてnilを持つようにすることができます。 もちろん、すべてのスイッチでnilケースが必要になります。

コンパイル時に開始されていない値を禁止する方が簡単ではないでしょうか。 初期化された値が必要な場合は、それを合計タイプに追加できます。

既存のコードを壊します。

また、合計タイプは、デフォルト値としてnilを持つようにすることができます。 もちろん、すべてのスイッチでnilケースが必要になります。

コンパイル時に開始されていない値を禁止する方が簡単ではないでしょうか。 初期化された値が必要な場合は、それを合計タイプに追加できます。

既存のコードを壊します。

合計型の既存のコードはありません。 デフォルト値は型自体で定義されたものでなければならないと思いますが。 最初のエントリ、または最初のアルファベット順、または何か。

合計型の既存のコードはありません。 デフォルト値は型自体で定義されたものでなければならないと思いますが。 最初のエントリ、または最初のアルファベット順、または何か。

私は最初の考えであなたに同意しましたが、いくつかの反省の後、共用体の新しい予約名は以前にいくつかのコードベース(共用体、列挙型など)で使用されていた可能性があります

nilをチェックする義務は、使用するのが非常に面倒だと思います。

Go2.0でしか解決できなかった下位互換性の重大な変更のようです

合計型の既存のコードはありません。 デフォルト値は型自体で定義されたものでなければならないと思いますが。 最初のエントリ、または最初のアルファベット順、または何か。

しかし、すべてをゼロにする既存のgoコードはたくさんあります。 それは間違いなく変化を壊すことになります。 さらに悪いことに、gofixや同様のツールは、変数の型を(同じ型の)Optionsに変更することしかできず、少なくとも醜いコードを生成します。他のすべての場合は、単に世界のすべてを壊してしまいます。

他に何もない場合は、 reflect.Zeroは何かを返す必要があります。 しかし、これらはすべて解決できる技術的なハードルです。たとえば、合計タイプのゼロ値が明確に定義されている場合、このハードルは非常に明白であり、そうでない場合はおそらく「パニック」になります。 より大きな問題は、なぜ特定の選択が正しい選択であるのか、そしてどの選択が言語全体に適合するかどうか、そしてどのように適合するのかということです。 IMO、これらに対処する最善の方法は、合計型が特定の問題に対処する具体的なケース、またはそれらの欠如によって作成された問題について話すことです。 経験レポートの

特に、「ゼロ値があってはならず、初期化されていない値の作成を禁止する必要がある」と「デフォルトは最初のエントリである必要がある」の両方が上記で何度か言及されていることに注意してください。 したがって、このようにすべきだと思うか、それとも実際には新しい情報を追加しないと思うかどうか。 しかし、それはすでに巨大なスレッドをさらに長くし、将来的にその中の関連情報を見つけるのを難しくします。

リフレクトを考えてみましょう。 デフォルトのint値が0であるInvalidKindがあります。reflect.Kindを受け入れる関数があり、そのタイプの初期化されていない変数を渡した場合、それは無効になります。 、reflect.Kindを仮想的に合計タイプに変更できる場合は、nil値に依存するのではなく、名前付きの無効なエントリをデフォルトとして持つ動作を保持する必要があります。

それでは、html /template.contentTypeについて考えてみましょう。 プレーンタイプはデフォルト値であり、フォールバックであるため、stringify関数によって実際にそのように扱われます。 仮の合計の将来では、その動作が必要になるだけでなく、nilはこのタイプのユーザーにとって何の意味もないため、nil値を使用することも不可能です。 ここでは常に名前付きの値を返すことがほぼ必須であり、その値がどうあるべきかについての明確なデフォルトがあります。

代数/可変個引数/合計/任意のデータ型がうまく機能する別の例で再び私です。

そのため、トランザクションなしでnoSQLデータベースを使用しています(分散システム、トランザクションは機能しません)が、明らかな理由でデータの整合性と一貫性が大好きであり、通常は単一の条件付き更新クエリで少し複雑な条件付き更新クエリを使用して、同時アクセスの問題を回避する必要がありますレコード(単一レコードの書き込みはアトミックです)。

挿入、追加、または削除できるエンティティのセットを作成する新しいタスクがあります(これらの操作の1つのみ)。

私たちが次のようなものを持つことができれば

type EntityOp oneof {
    Insert   Reference
    NewState string
    Delete   struct{}
}

方法はただかもしれません

type DB interface {
    …
    Capture(ctx context.Context, processID string, ops map[string]EntityOp) (bool, error)
}

合計時間の素晴らしい使用法の1つは、ASTでノードを表すことです。 もう1つは、 nilをコンパイル時にチェックされるoptionに置き換えることです。

@DemiMarieですが、今日のGoでは、この合計もnilにすることができます。上記で提案したように、nilを各列挙型のバリアントにすることができます。各スイッチにケースnilがありますが、この義務はそれほど悪くありません。既存のすべてのgoコードを壊さずにこの機能が必要です(現在、すべてを列挙できます)

それがここに属しているかどうかはわかりませんが、これはすべてTypescriptのままです。ここには、「文字列リテラルタイプ」と呼ばれる非常に優れた機能があり、次のことができます。

var name: "Peter" | "Consuela"; // string type with compile-time constraint

これは文字列列挙型のようなもので、私の意見では従来の数値列挙型よりもはるかに優れています。

@Merovius
具体的な例は、任意のJSONを使用することです。
Rustでは次のように表すことができます
列挙値{
ヌル、
Bool(bool)、
数(数)、
String(String)、
Array(Vec)、
オブジェクト(マップ)、
}

2つの利点としての共用体タイプ:

  1. コードの自己文書化
  2. コンパイラまたはgo vetが共用体型の誤った使用法をチェックできるようにする
    (例:すべてのタイプがチェックされていないスイッチ)

構文については、タイプエイリアスと同様に、以下はGo1と互換性がある必要があります。

type Token = int | float64 | string

共用体型は、インターフェースとして内部的に実装できます。 重要なのは、共用体型を使用すると、コードが読みやすくなり、次のようなエラーをキャッチできるようになることです。

var tok Token

switch t := tok.(type) {
case int:
    // do something
}

すべてのTokenタイプがスイッチで使用されているわけではないため、コンパイラはエラーを発生させる必要があります。

これに伴う問題は、(私の知る限り)ポインター型(またはstringなどのポインターを含む型)と非ポインター型を一緒に格納する方法がないことです。 レイアウトが異なるタイプでも機能しません。 自由に訂正してください。ただし、問題は、ポインターと単純な変数を同時に使用できる変数では、正確なGCがうまく機能しないことです。

interface{}現在行っているように、暗黙のボクシングの道を進むことができます。 しかし、これが十分な利点を提供するとは思いません-それはまだ栄光のインターフェイスタイプのように見えます。 代わりに、ある種のvetチェックを開発できるのではないでしょうか。

ガベージコレクターは、レイアウトを決定するためにユニオンからタグビットを読み取る必要があります。 これは不可能ではありませんが、gcの速度を低下させる可能性のあるランタイムへの大きな変更になります。

たぶん、ある種の獣医チェックを代わりに開発することができますか?

https://github.com/BurntSushi/go-sumtype

ガベージコレクターは、レイアウトを決定するためにユニオンからタグビットを読み取る必要があります。

これは、インターフェイスに非ポインタが含まれる可能性がある場合に存在したのとまったく同じ競争です。 そのデザインは明示的にから離れました。

go-sumtypeはおもしろいです、ありがとう。 しかし、同じパッケージで2つの共用体型が定義されている場合はどうなりますか?

コンパイラーは、共用体型をインターフェースとして内部的に実装できますが、統一された構文と標準の型チェックを追加します。

共用体タイプを使用するプロジェクトがN個あり、それぞれが異なり、Nが十分に大きい場合は、 1つの方法を導入することが最善の解決策になる可能性があります。

しかし、同じパッケージで2つの共用体型が定義されている場合はどうなりますか?

何もありませんか? ロジックはタイプごとであり、ダミーメソッドを使用して実装者を認識します。 ダミーメソッドには異なる名前を使用するだけです。

型レイアウトを指定@skybrian IIRC現在のビットマップは、現在、一つの場所に保存されています。 オブジェクトごとにそのようなものを追加すると、多くのジャンプが追加され、すべてのオプションのオブジェクトがGCルートになります。

これに伴う問題は、(私の知る限り)ポインタ型(または文字列などのポインタを含む型)と非ポインタ型を一緒に格納する方法がないことです。

これは必要ないと思います。 コンパイラは、ポインタマップが一致する場合は型のレイアウトをオーバーラップする可能性があり、そうでない場合はオーバーラップしません。 それらが一致しない場合、それらを連続してレイアウトするか、現在インターフェースで使用されているポインターアプローチを使用するのは自由です。 構造体メンバーに非連続レイアウトを使用することもできます。

しかし、これが十分な利点を提供するとは思いません-それはまだ栄光のインターフェイスタイプのように見えます。

私の提案では、共用体型は_正確に_栄光のあるインターフェース型です-共用体型は、列挙型のセットのみを格納できるインターフェースのサブセットにすぎません。 これにより、コンパイラは特定のタイプセットに対してより効率的なストレージ方法を自由に選択できるようになる可能性がありますが、これは実装の詳細であり、主な動機ではありません。

@ rogpeppe-好奇心から、sum型を直接使用できますか、それとも何かを行うために既知の型に明示的にキャストする必要がありますか? 常に既知のタイプにキャストする必要がある場合、インターフェイスですでに提供されているものよりも、これがどのようなメリットをもたらすのか本当にわかりません。 私が目にする主な利点は、コンパイル時のエラーチェックです。これは、実行時にアンマーシャリングが引き続き発生するためです。これは、無効な型が渡されるという問題が発生する可能性が高いためです。 もう1つの利点は、インターフェイスがより制約されていることです。これは、言語の変更を保証するものではないと思います。

僕にできる

type FooType int | float64

func AddOne(foo FooType) FooType {
    return foo + 1
}

// if this can be done, what happens here?
type FooType nil | int
func AddOne(foo FooType) FooType {
    return foo + 1
}

これができない場合、私はとの違いはあまり見られません

type FooType interface{}

func AddOne(foo FooType) (FooType, error) {
    switch v := foo.(type) {
        case int:
              return v + 1, nil
        case float64:
              return v + 1.0, nil
    }

    return nil, fmt.Errorf("invalid type %T", foo)
}

// versus
type FooType int | float64

func AddOne(foo FooType) FooType {
    switch v := foo.(type) {
        case int:
              return v + 1
        case float64:
              return v + 1.0
    }

    // assumes the compiler knows that there is no other type is 
    // valid and thus this should always returns a value
    // Would the compiler error out on incomplete switch types?
}

@xibz

好奇心から、sum型を直接使用できますか、それとも何かを行うために既知の型に明示的にキャストする必要がありますか? 常に既知のタイプにキャストする必要がある場合、インターフェイスですでに提供されているものよりも、これがどのようなメリットをもたらすのか本当にわかりません。

@rogpeppe 、私が間違っている場合は私を訂正してください🙏
常にパターンマッチングを実行する必要があること(関数型プログラミング言語で合計型を操作するときに「キャスト」が呼び出される方法)は、実際には合計型を使用する最大の利点の1つです。 合計型のすべての可能な形状を明示的に処理するように開発者に強制することは、開発者が変数を特定の型であると考えて使用するのを防ぐ方法ですが、実際には別の型です。 誇張された例は、JavaScriptでは次のようになります。

const a = "1" // string "1"
const b = a + 5 // string "15" and not number 6

これができない場合、私はとの違いはあまり見られません

自分でいくつかの利点を述べていると思いますか?
↓

私が目にする主な利点は、コンパイル時のエラーチェックです。これは、実行時にアンマーシャリングが引き続き発生するためです。これは、無効な型が渡されるという問題が発生する可能性が高いためです。 もう1つの利点は、インターフェイスがより制約されていることです。これは、言語の変更を保証するものではないと思います。

// Would the compiler error out on incomplete switch types?

関数型プログラミング言語の機能に基づいて、これは可能で構成可能である必要があると思います👍

@xibzは、コンパイル時と実行時で実行できるため、パフォーマンスも向上しますが、できれば、私が死ぬ1日前にジェネリックスがあります。

@xibz

好奇心から、sum型を直接使用できますか、それとも何かを行うために既知の型に明示的にキャストする必要がありますか?

タイプのすべてのメンバーがそのメソッドを共有している場合は、そのメソッドを呼び出すことができます。

int | float64を例にとると、次の結果はどうなりますか。

var x int|float64 = int(2)
var y int|float64 = float64(0.5)
fmt.Println(x * y)

intからfloat64への暗黙の変換を行いますか? またはfloat64からint 。 それともパニックになりますか?

したがって、ほぼ正しいです。ほとんどの場合、使用する前にタイプチェックする必要があります。 それは長所であり、短所ではないと思います。

ところで、実行時の利点は重要かもしれません。 型の例を続けるために、型[](int|float64)スライスにポインタを含める必要はありません。これは、型のすべてのインスタンスを数バイト(おそらく、配置の制限により16バイト)で表すことができるためです。場合によっては、パフォーマンスが大幅に向上します。

@rogpeppe

ほとんどの場合、使用する前にタイプチェックする必要があります。 それは長所であり、短所ではないと思います。

私はそれが不利ではないことに同意します。 私は、これがインターフェースとは対照的にどのような利点をもたらすのかを見ようとしています。

型のスライスは、型のすべてのインスタンスを数バイト(おそらく、配置の制限により16バイト)で表すことができるため、ポインターを含める必要はありません。これにより、場合によってはパフォーマンスが大幅に向上する可能性があります。

うーん、私はこれの重要な部分を購入するかどうかはあまりわかりません。 非常にまれなケースですが、メモリサイズが半分になると確信しています。 そうは言っても、節約されたメモリは言語の変更にとって十分に重要ではないと思います。

@stouf

常にパターンマッチングを実行する必要があること(関数型プログラミング言語で合計型を操作するときに「キャスト」が呼び出される方法)は、実際には合計型を使用する最大の利点の1つです。

しかし、インターフェースでまだ処理されていない言語にどのような利点がありますか? もともと私は完全に合計型に興味がありましたが、それについて考え始めたとき、私はそれがもたらす利益を少し失いました。


とはいえ、sum型を使用することで、よりクリーンで読みやすいコードを提供できるのであれば、私は100%それを望んでいます。 ただし、見た目からは、インターフェイスコードとほぼ同じように見えます。

@xibzパターンマッチングは、ツリーの複数のレベルの深さを調べたいツリーウォーキングコードで役立ちます。 タイプスイッチでは、一度に1レベルしか深く見えないため、ネストする必要があります。

これは少し不自然ですが、たとえば、式の構文ツリーがある場合、2次方程式に一致させるには、次のようにします。

match Add(Add(Mult(Const(a), Power(Var(x), 2)), Mult(Const(b), Var(x))), Const(c)) {
  // here a, b, c are bound to the constants and x is bound to the variable name.
  // x must have been the same in both var expressions or it wouldn't match.
}

深さが1レベルしかない単純な例では大きな違いはありませんが、ここでは深さが5レベルになるため、ネストされたタイプのスイッチを使用するのは非常に複雑になります。 パターンマッチングを備えた言語は、ケースを見逃さないようにしながら、複数のレベルを深く掘り下げることができます。

コンパイラの外でどれだけ出てくるかはわかりませんが。

@xibz
合計型の利点は、あなたとコンパイラの両方が、合計内に存在できる型を正確に知っていることです。 それが本質的に違いです。 インターフェースが空の場合、ユーザーが予期しないタイプを指定したときに回復することを唯一の目的とするブランチを常に持つことで、APIでの誤用を常に心配して防ぐ必要があります。

合計型がコンパイラに実装される見込みはほとんどないように思われるので、少なくとも//go:union A | B | Cような標準のコメントディレクティブが提案され、 go vetによってサポートされることを願っています。

合計型を宣言する標準的な方法では、N年後に、それを使用しているパッケージの数を知ることができます。

ジェネリックスの最近の設計ドラフトでは、おそらく合計タイプがそれらに関連付けられる可能性があります。

ドラフトの1つは、コントラクトの代わりにインターフェイスを使用するというアイデアを浮かび上がらせ、インターフェイスはタイプリストをサポートする必要があります。

type Foo interface { 
     int64, int32, int, uint, uint32, uint64
}

それ自体はメモリパックされたユニオンを生成しませんが、おそらくジェネリック関数または構造体で使用する場合、ボックス化されず、少なくとも型の有限リストを処理するときに型の安全性を提供します。

そして、おそらく、タイプスイッチ内でこれらの特定のインターフェースを使用するには、そのようなスイッチが網羅的である必要があります。

これは理想的な短い構文ではありませんが(例: Foo | int32 | []Bar )、何かです。

ジェネリックスの最近の設計ドラフトでは、おそらく合計タイプがそれらに関連付けられる可能性があります。

ドラフトの1つは、コントラクトの代わりにインターフェイスを使用するというアイデアを浮かび上がらせ、インターフェイスはタイプリストをサポートする必要があります。

type Foo interface { 
     int64, int32, int, uint, uint32, uint64
}

それ自体はメモリパックされたユニオンを生成しませんが、おそらくジェネリック関数または構造体で使用する場合、ボックス化されず、少なくとも型の有限リストを処理するときに型の安全性を提供します。

そして、おそらく、タイプスイッチ内でこれらの特定のインターフェースを使用するには、そのようなスイッチが網羅的である必要があります。

これは理想的な短い構文ではありませんが(例: Foo | int32 | []Bar )、何かです。

私の提案と非常によく似ています: //github.com/golang/go/issues/19412#issuecomment -520306000

type foobar union {
  int
  float
}

@mathieudevosうわー、私は実際にそれがとても好きです。

私にとって、最新のジェネリックスの提案で最大の奇妙な点(実際に残っている唯一の奇妙な点)は、インターフェイスのタイプリストです。 彼らはちょうど完全に適合していません。 次に、型パラメーター制約としてのみ使用できるいくつかのインターフェースなどになります...

union概念は、 unionをinterfaceに埋め込んで、「メソッドとraw型を含む制約」を実現できるため、私の頭の中でうまく機能します。 インターフェースはそのまま機能し続け、ユニオンを中心に定義されたセマンティクスにより、通常のコードで使用でき、奇妙な感覚がなくなります。

// Ordinary interface
type Stringer interface {
    String() string
}

// Type union
type Foobar union {
    int
    float
}

// Equivalent to an interface with a type list
type FoobarStringer interface {
    Stringer
    Foobar
}

// Unions can intersect
type SuperFoo union {
    Foobar
    int
}

// Doesn't compile since these unions can't intersect
type Strange interface {
    Foobar
    union {
        int
        string
    }
}

編集-実際には、このCLを見たばかりです: https :

この変更の主な利点は、一般の人々に門戸を開くことです。
(非制約)タイプリストを使用したインターフェイスの使用

...素晴らしい! インターフェイスは合計型として完全に使用できるようになり、通常の使用と制約の使用の間でセマンティクスが統合されます。 (明らかにまだオンになっていないが、それは向かうべき素晴らしい目的地だと思う。)

合計タイプのバージョンが現在のジェネリック設計ドラフトにどのように表示されるかを説明するために、#41716を開きました。

代数的データ型に関する@henryasからの古い提案を共有したかっただけです。 提供されたユースケースで書かれているのはとても良いです。
https://github.com/golang/go/issues/21154
残念ながら、同じ日に@mvdanによって、作業の評価なしに閉鎖され

私は#21154が本当に好きです。 それは別のことのようですが(したがって@mvdanの)コメントは、完全にヒットしていないのでそれを閉じます。 そこで再開しますか、それともここでの議論に含めますか?

ええ、私は本当に、その号で説明されているのと同様の方法で、より高レベルのビジネスロジックをモデル化する機能が欲しいです。 列挙型のような制限されたオプションの合計タイプ、および他の問題のように提案された受け入れられたタイプは、ツールボックスで素晴らしいでしょう。 Goのビジネス/ドメインコードは、現時点では少し不格好に感じることがあります。

私の唯一のフィードバックは、インターフェイス内のtype foo,barが少しぎこちなく、セカンドクラスに見えるということです。null許容と非null許容のどちらかを選択する必要があることに同意します(可能な場合)。

@ProximaB 「ghアカウントでこれ以上のアクティビティはありません」と言う理由がわかりません。 それ以来、彼らは他の多くの問題も作成してコメントしており、それらの多くはGoプロジェクトに関するものです。 彼らの活動がその問題の影響を受けているという証拠はまったく見当たりません。

さらに、私はダニエルがこの問題の複製としてその問題を閉じることに強く同意します。 @andigが何か違うものを提案していると言っている理由がわかりません。 #21154のテキストを理解できる限り、ここで説明しているのとまったく同じことを提案します。正確な構文でさえ、このメガスレッドのどこかですでに提案されていても、まったく驚かないでしょう(セマンティクス、説明された、最も確かにそうでした。複数回)。 実際、ダニエルズの締めくくりは、この問題の長さによって正しく証明されていると言えます。これは、#21154の非常に詳細で微妙な議論がすでに含まれているため、すべてを繰り返すのは困難で冗長でした。

私は同意し、提案をだましとして閉じるのはおそらく残念なことだと理解しています。 しかし、私はそれを回避する実際的な方法を知りません。 1つの場所で議論することは、関係するすべての人にとって有益であるように思われ、同じことについて複数の問題を開いたままにしておくことは、それらについての議論なしに、明らかに無意味です。

さらに、私はダニエルがこの問題の複製としてその問題を閉じることに強く同意します。 @andigが何か違うものを提案していると言っている理由がわかりません。 #21154のテキストを理解できる限り、ここで説明しているのとまったく同じことを提案しています。

この問題を読み直すことに同意します。 ジェネリック契約でこの問題と混同したようです。 合計型を強くサポートします。 耳障りなことを言うつもりはありませんでした。そのように出くわした場合は、お詫び申し上げます。

私は人間であり、ガーデニングの問題は時々トリッキーになる可能性があるので、間違いを犯したときに必ず指摘してください:)しかし、この場合、特定の合計タイプの提案は、 https:/のようにこのスレッドからフォークする必要があると思います

私は人間であり、ガーデニングの問題は時々トリッキーになる可能性があるので、間違いを犯したときに必ず指摘してください:)しかし、この場合、特定の合計タイプの提案は、 #19412のようにこのスレッドからフォークする必要があると思います

@mvdanは人間ではありません。 私を信じて。 私は彼の隣人です。 冗談だ。

ご清聴ありがとうございました。 私は私の提案にそれほど執着していません。 自由に壊したり、修正したり、それらの任意の部分を撃墜したりしてください。 実生活で忙しいので、積極的に議論する機会がありません。 人々が私の提案を読んでいて、実際にそれを好きな人もいることを知っておくのは良いことです。

本来の目的は、ドメインの関連性によってタイプをグループ化できるようにすることです。タイプは必ずしも共通の動作を共有する必要はなく、コンパイラーにそれを強制させます。 私の意見では、これは単なる静的検証の問題であり、コンパイル中に行われます。 型間の複雑な関係を保持するコードをコンパイラーが生成する必要はありません。 生成されたコードは、これらのドメインタイプを通常のインターフェイス{}タイプであるかのように通常処理する場合があります。 違いは、コンパイラがコンパイル時に追加の静的型チェックを実行するようになったことです。 それが基本的に私の提案の本質です#21154

@henryasはじめまして! 😊
Golangがダックタイピングを使用していなかったとしたら、タイプ間の関係がはるかに厳密になり、提案で説明したように、ドメインの関連性によってオブジェクトをグループ化できるようになるのではないかと思います。

@henryasはじめまして! 😊
Golangがダックタイピングを使用していなかったとしたら、タイプ間の関係がはるかに厳密になり、提案で説明したように、ドメインの関連性によってオブジェクトをグループ化できるようになるのではないかと思います。

そうなるでしょうが、それはGo 1との互換性の約束を破ることになります。明示的なインターフェースがあれば、おそらく合計型は必要ありません。 ただし、ダックタイピングは必ずしも悪いことではありません。 それは特定のものをより軽量で便利にします。 ダックタイピングが好きです。 それは仕事に適切なツールを使用することの問題です。

@henryas同意します。 それは架空の質問でした。 ゴークリエーターは間違いなくすべての浮き沈みを深く考えました。
一方、インターフェイスコンプライアンスの検証などのコーディングGUIDは表示されません。
https://github.com/uber-go/guide/blob/master/style.md#verify -interface-compliance

このトピック外の議論をどこかでお願いできますか? この号を購読している人はたくさんいます。
オープンインターフェースの満足度は、Goの開始以来の一部であり、変わることはありません。

このページは役に立ちましたか?
0 / 5 - 0 評価