Go: Vorschlag: Spezifikation: Summentypen / diskriminierte Gewerkschaften hinzufügen

Erstellt am 6. März 2017  ·  320Kommentare  ·  Quelle: golang/go

Dies ist ein Vorschlag für Summentypen, die auch als diskriminierte Gewerkschaften bezeichnet werden. Summentypen in Go sollten sich im Wesentlichen wie Schnittstellen verhalten, außer dass:

  • sie sind Werttypen, wie structs
  • die darin enthaltenen Typen werden zur Kompilierzeit fixiert

Summentypen können mit einer switch-Anweisung abgeglichen werden. Der Compiler prüft, ob alle Varianten übereinstimmen. Innerhalb der Arme der switch-Anweisung kann der Wert verwendet werden, als ob er von der abgeglichenen Variante wäre.

Go2 LanguageChange NeedsInvestigation Proposal

Hilfreichster Kommentar

Vielen Dank für die Erstellung dieses Vorschlags. Ich spiele jetzt seit einem Jahr mit dieser Idee.
Folgendes ist so weit, wie ich mit einem konkreten Vorschlag habe. Ich denke
"Auswahltyp" könnte tatsächlich ein besserer Name sein als "Summentyp", aber YMMV.

Summentypen in Go

Ein Summentyp wird durch zwei oder mehr Typen kombiniert mit dem "|" dargestellt.
Operator.

type: type1 | type2 ...

Werte des resultierenden Typs können nur einen der angegebenen Typen enthalten. Die
type wird als Schnittstellentyp behandelt - sein dynamischer Typ ist der des
Wert, der ihm zugewiesen ist.

Als Sonderfall kann mit "nil" angegeben werden, ob der Wert
null werden.

Zum Beispiel:

type maybeInt nil | int

Die Methodenmenge vom Typ Summe enthält den Schnittpunkt der Methodenmenge
aller Komponententypen, mit Ausnahme aller Methoden, die die gleichen
Name, aber unterschiedliche Signaturen.

Wie jeder andere Schnittstellentyp kann auch der Summentyp Gegenstand einer dynamischen
Typkonvertierung. Bei Typenschaltern der erste Arm des Schalters, der
Übereinstimmungen wird der gespeicherte Typ ausgewählt.

Der Nullwert eines Summentyps ist der Nullwert des ersten Typs in
die Summe.

Wenn Sie einem Summentyp einen Wert zuweisen, wenn der Wert in mehr passt
als einer der möglichen Typen, dann wird der erste gewählt.

Zum Beispiel:

var x int|float64 = 13

würde zu einem Wert mit dynamischem Typ int führen, aber

var x int|float64 = 3.13

würde einen Wert mit dynamischem Typ float64 ergeben.

Implementierung

Eine naive Implementierung könnte Summentypen genau als Schnittstelle implementieren
Werte. Ein komplexerer Ansatz könnte eine Darstellung verwenden
passend zu den möglichen Werten.

Zum Beispiel ein Summentyp, der nur aus konkreten Typen ohne Zeiger besteht
könnte mit einem Nicht-Zeiger-Typ implementiert werden, mit einem zusätzlichen Wert für
erinnern Sie sich an den tatsächlichen Typ.

Bei Sum-of-Struct-Typen kann es sogar möglich sein, Ersatzpolster zu verwenden
Bytes, die den Strukturen zu diesem Zweck gemeinsam sind.

Alle 320 Kommentare

Dies wurde in der Vergangenheit mehrmals diskutiert, beginnend vor der Open-Source-Veröffentlichung. In der Vergangenheit war man sich einig, dass Summentypen den Schnittstellentypen nicht viel hinzufügen. Wenn Sie alles sortiert haben, erhalten Sie am Ende einen Schnittstellentyp, bei dem der Compiler überprüft, ob Sie alle Fälle eines Typwechsels ausgefüllt haben. Das ist ein ziemlich kleiner Vorteil für eine neue Sprachänderung.

Wenn Sie diesen Vorschlag weiter vorantreiben möchten, müssen Sie ein vollständigeres Vorschlagsdokument schreiben, einschließlich: Wie lautet die Syntax? Wie funktionieren sie genau? (Sie sagen, sie sind "Werttypen", aber Schnittstellentypen sind auch Werttypen). Was sind die Kompromisse?

Ich denke, dies ist eine zu bedeutende Änderung des Typensystems für Go1 und es besteht keine dringende Notwendigkeit.
Ich schlage vor, dass wir dies im größeren Kontext von Go 2 noch einmal betrachten.

Vielen Dank für die Erstellung dieses Vorschlags. Ich spiele jetzt seit einem Jahr mit dieser Idee.
Folgendes ist so weit, wie ich mit einem konkreten Vorschlag habe. Ich denke
"Auswahltyp" könnte tatsächlich ein besserer Name sein als "Summentyp", aber YMMV.

Summentypen in Go

Ein Summentyp wird durch zwei oder mehr Typen kombiniert mit dem "|" dargestellt.
Operator.

type: type1 | type2 ...

Werte des resultierenden Typs können nur einen der angegebenen Typen enthalten. Die
type wird als Schnittstellentyp behandelt - sein dynamischer Typ ist der des
Wert, der ihm zugewiesen ist.

Als Sonderfall kann mit "nil" angegeben werden, ob der Wert
null werden.

Zum Beispiel:

type maybeInt nil | int

Die Methodenmenge vom Typ Summe enthält den Schnittpunkt der Methodenmenge
aller Komponententypen, mit Ausnahme aller Methoden, die die gleichen
Name, aber unterschiedliche Signaturen.

Wie jeder andere Schnittstellentyp kann auch der Summentyp Gegenstand einer dynamischen
Typkonvertierung. Bei Typenschaltern der erste Arm des Schalters, der
Übereinstimmungen wird der gespeicherte Typ ausgewählt.

Der Nullwert eines Summentyps ist der Nullwert des ersten Typs in
die Summe.

Wenn Sie einem Summentyp einen Wert zuweisen, wenn der Wert in mehr passt
als einer der möglichen Typen, dann wird der erste gewählt.

Zum Beispiel:

var x int|float64 = 13

würde zu einem Wert mit dynamischem Typ int führen, aber

var x int|float64 = 3.13

würde einen Wert mit dynamischem Typ float64 ergeben.

Implementierung

Eine naive Implementierung könnte Summentypen genau als Schnittstelle implementieren
Werte. Ein komplexerer Ansatz könnte eine Darstellung verwenden
passend zu den möglichen Werten.

Zum Beispiel ein Summentyp, der nur aus konkreten Typen ohne Zeiger besteht
könnte mit einem Nicht-Zeiger-Typ implementiert werden, mit einem zusätzlichen Wert für
erinnern Sie sich an den tatsächlichen Typ.

Bei Sum-of-Struct-Typen kann es sogar möglich sein, Ersatzpolster zu verwenden
Bytes, die den Strukturen zu diesem Zweck gemeinsam sind.

@rogpeppe Wie würde das mit Typwechseln interagieren? Vermutlich wäre es ein Fehler bei der Kompilierung, ein case für einen Typ (oder eine Assertion für einen Typ) zu haben, der kein Mitglied der Summe ist. Wäre es auch ein Fehler, bei einem solchen Typ einen nicht erschöpfenden Schalter zu haben?

Für Typenschalter, wenn Sie

type T int | interface{}

und du machst:

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

und t enthält eine Schnittstelle{}, die ein int enthält, entspricht dies dem ersten Fall? Was ist, wenn der erste Fall case interface{} ?

Oder können Summentypen nur konkrete Typen enthalten?

Was ist mit type T interface{} | nil ? Wenn du schreibst

var t T = nil

was ist ts typ? Oder ist das Bauen verboten? Eine ähnliche Frage stellt sich für type T []int | nil , es geht also nicht nur um Schnittstellen.

Ja, ich denke, es wäre vernünftig, einen Kompilierzeitfehler zu haben
einen Fall zu haben, der nicht zu vergleichen ist. Ich bin mir nicht sicher, ob es
eine gute Idee, bei einem solchen Typ nicht erschöpfende Schalter zuzulassen - wir
erfordern nirgendwo anders Vollständigkeit. Eine Sache, die könnte
Aber gut: Wenn der Wechsel vollständig ist, können wir keine Vorgabe verlangen
um es zu einer abschließenden Aussage zu machen.

Das bedeutet, dass Sie den Compiler zu einem Fehler bringen können, wenn Sie Folgendes haben:

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

und Sie ändern den Summentyp, um einen zusätzlichen Fall hinzuzufügen.

Für Typenschalter, wenn Sie

Typ T int | Schnittstelle{}

und du machst:

Schalter t := t.(Typ) {
Fall int:
// ...
und t enthält eine Schnittstelle{}, die ein int enthält, entspricht dies dem ersten Fall? Was ist, wenn der erste Fall die Fallschnittstelle{} ist?

t darf keine Schnittstelle{} enthalten, die ein int enthält. t ist eine Schnittstelle
Typ wie jeder andere Schnittstellentyp, außer dass er nur
enthalten den aufgezählten Satz von Typen, aus denen er besteht.
Genauso wie eine Schnittstelle{} keine Schnittstelle{} mit einem int enthalten kann.

Summentypen können mit Schnittstellentypen übereinstimmen, aber sie erhalten immer noch einen konkreten
Typ für den dynamischen Wert. Zum Beispiel wäre es gut zu haben:

type R io.Reader | io.ReadCloser

Was ist mit Typ-T-Schnittstelle{} | Null? Wenn du schreibst

var t T = nil

was ist ts typ? Oder ist das Bauen verboten? Eine ähnliche Frage stellt sich für Typ T []int | nil, es geht also nicht nur um Schnittstellen.

Gemäß obigem Vorschlag erhalten Sie den ersten Artikel
in der Summe, der der Wert zugeordnet werden kann, also
Sie würden die Null-Schnittstelle erhalten.

Tatsächlich Schnittstelle{} | nil ist technisch überflüssig, da jede Schnittstelle{}
kann gleich null sein.

Für []int | nil, ein nil []int ist nicht dasselbe wie eine nil-Schnittstelle, also
Der konkrete Wert von ([]int|nil)(nil) wäre []int(nil) nicht untypisiert nil .

Der Fall []int | nil ist interessant. Ich würde erwarten, dass nil in der Typdeklaration immer "der Null-Schnittstellenwert" bedeutet

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

würde bedeuten, dass x die nil-Schnittstelle ist, nicht die nil []int .

Dieser Wert würde sich von der Null []int , die im gleichen Typ codiert ist:

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

Wäre nicht immer nil erforderlich, selbst wenn die Summe alle Werttypen enthält? Was wäre sonst var x int64 | float64 ? Mein erster Gedanke beim Extrapolieren von den anderen Regeln wäre der Nullwert des ersten Typs, aber was ist dann mit var x interface{} | int ? Es müsste, wie @bcmills betont , eine eindeutige Summe Null sein.

Es wirkt zu subtil.

Umfangreiche Schalter wären schön. Sie können immer ein leeres default: hinzufügen, wenn dies nicht das gewünschte Verhalten ist.

Der Vorschlag lautet: "Wenn einem Summentyp ein Wert zugewiesen wird, wenn der Wert in mehr passt
als einer der möglichen Typen, dann wird der erste gewählt."

Also mit:

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

x hätte den konkreten Typ []int, weil nil []int zuweisbar ist und []int das erste Element des Typs ist. Es wäre gleich jedem anderen []int (nil)-Wert.

Wäre nicht immer nil erforderlich, selbst wenn die Summe alle Werttypen enthält? Was würde sonst var x int64 | float64 sein?

Der Vorschlag lautet: "Der Nullwert eines Summentyps ist der Nullwert des ersten Typs in
die Summe.", die Antwort lautet also int64(0).

Mein erster Gedanke, extrapoliert von den anderen Regeln, wäre der Nullwert des ersten Typs, aber was ist dann mit var x interface{} | int? Es müsste, wie @bcmills betont , eine eindeutige Summe Null sein

Nein, in diesem Fall wäre es nur der übliche Nullwert der Schnittstelle. Dieser Typ (Schnittstelle{} | nil) ist überflüssig. Vielleicht ist es eine gute Idee, es zu einem Compiler zu machen, um Summentypen anzugeben, bei denen ein Element eine Obermenge eines anderen ist, da ich derzeit keinen Sinn darin sehe, einen solchen Typ zu definieren.

Der Nullwert eines Summentyps ist der Nullwert des ersten Typs in der Summe.

Das ist ein interessanter Vorschlag, aber da der Summentyp irgendwo den Typ des Werts aufzeichnen muss, den er derzeit enthält, bedeutet dies meiner Meinung nach, dass der Nullwert des Summentyps nicht alle Bytes-Null ist, was ihn von unterscheiden würde jeder andere Typ in Go. Oder vielleicht könnten wir eine Ausnahme hinzufügen, die besagt, dass, wenn die Typinformationen nicht vorhanden sind, der Wert der Nullwert des ersten aufgelisteten Typs ist, aber dann bin ich mir nicht sicher, wie ich nil wenn dies nicht der Fall ist der erste aufgeführte Typ.

Also macht (stuff) | nil nur Sinn, wenn nichts in (Stuff) Null sein kann und nil | (stuff) etwas anderes bedeutet, je nachdem, ob etwas in Stuff Null sein kann? Welchen Wert fügt null hinzu?

@ianlancetaylor Ich glaube, viele funktionale Sprachen implementieren (geschlossene)

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

Wenn which in der Reihenfolge in die Felder der Union indiziert, 0 = a, 1 = b, 2 = c, ergibt die Nullwertdefinition, dass alle Bytes Null sind. Und Sie müssten die Typen anders als bei Schnittstellen speichern. Sie benötigen auch eine spezielle Behandlung für das nil-Tag, wo immer Sie die Typinformationen speichern.

Das würde die Werttypen von union anstelle von speziellen Interfaces machen, was auch interessant ist.

Gibt es eine Möglichkeit, den gesamten Nullwert zum Laufen zu bringen, wenn das Feld, das den Typ aufzeichnet, einen Nullwert hat, der den ersten Typ darstellt? Ich gehe davon aus, dass dies eine mögliche Darstellung wäre:

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

[bearbeiten]

Sorry @jimmyfrasche hat mich geschlagen.

Gibt es etwas, das von Null hinzugefügt wurde, was nicht getan werden könnte?

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

?

Das scheint eine Menge Verwirrung zu vermeiden (die ich zumindest habe)

Oder besser

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

Auf diese Weise könnten Sie switch on None eingeben und mit None{} zuweisen

@jimmyfrasche struct{} ist nicht gleich nil . Es ist ein kleines Detail, aber es würde dazu führen, dass Typwechsel bei Summen unnötig (?) von Typwechseln bei anderen Typen abweichen.

@bcmills Es war nicht meine Absicht, etwas anderes zu behaupten - ich meinte, dass es für den gleichen Zweck verwendet werden könnte, wie einen Mangel an Wert zu unterscheiden, ohne sich mit der Bedeutung von nil in einem der Typen in der Summe zu überschneiden.

@rogpeppe was druckt das?

// 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")
}

Ich würde "Leser" annehmen

@jimmyfrasche Ich würde ReadCloser annehmen, genauso wie Sie es von einem Typwechsel auf jeder anderen Schnittstelle erhalten würden.

(Und ich würde auch erwarten, dass Summen, die nur Schnittstellentypen enthalten, nicht mehr Platz beanspruchen als eine normale Schnittstelle, obwohl ich vermute, dass ein explizites Tag ein wenig Suchaufwand im Typwechsel sparen könnte.)

@bcmills Interessant ist die Aufgabe: https://play.golang.org/p/PzmWCYex6R

@ianlancetaylor Das ist ein ausgezeichneter Punkt, danke. Ich denke nicht, dass es schwer ist, sich zurechtzufinden, obwohl es bedeutet, dass mein Vorschlag zur "naiven Implementierung" selbst zu naiv ist. Obwohl ein Summentyp als Schnittstellentyp behandelt wird, muss er keinen direkten Zeiger auf den Typ und seinen Methodensatz enthalten – stattdessen könnte er gegebenenfalls ein Integer-Tag enthalten, das den Typ impliziert. Dieses Tag kann auch dann ungleich null sein, wenn der Typ selbst null ist.

Gegeben:

 var x int | nil = nil

der Laufzeitwert von x muss nicht nur Nullen sein. Beim Einschalten des Typs von x oder Umrechnen
es zu einem anderen Schnittstellentyp, könnte das Tag über eine kleine Tabelle mit umgeleitet werden
die eigentlichen Typzeiger.

Eine andere Möglichkeit wäre, einen nil-Typ nur zuzulassen, wenn es das erste Element ist, aber
das schließt Konstruktionen aus wie:

var t nil | int
var u float64 | t

@jimmyfrasche Ich würde ReadCloser annehmen, genauso wie Sie es von einem Typwechsel auf jeder anderen Schnittstelle erhalten würden.

Jawohl.

@bcmills Interessant ist die Aufgabe: https://play.golang.org/p/PzmWCYex6R

Ich verstehe das nicht. Warum sollte "dies [...] gültig sein, damit der Typschalter ReadCloser druckt"
Wie jeder Schnittstellentyp würde ein Summentyp nicht mehr als den konkreten Wert dessen speichern, was darin enthalten ist.

Wenn eine Summe mehrere Schnittstellentypen enthält, ist die Laufzeitdarstellung nur ein Schnittstellenwert - wir wissen nur, dass der zugrunde liegende Wert eine oder mehrere der deklarierten Möglichkeiten implementieren muss.

Das heißt, wenn Sie einem Typ (I1 | I2) etwas zuweisen, bei dem sowohl I1 als auch I2 Schnittstellentypen sind, ist es später nicht möglich zu sagen, ob der von Ihnen eingegebene Wert zu diesem Zeitpunkt bekannt war, um I1 oder I2 zu implementieren.

Wenn Sie einen Typ haben, der io.ReadCloser | . ist io.Reader Sie können nicht sicher sein, wenn Sie switch eingeben oder auf io.Reader bestätigen, dass es kein io.ReadCloser ist, es sei denn, die Zuweisung zu einem Summentyp entpackt und reboxt die Schnittstelle.

In die andere Richtung gehen, wenn Sie io.Reader hätten | io.ReadCloser würde es entweder nie ein io.ReadCloser akzeptieren, weil es strikt von rechts nach links geht, oder die Implementierung müsste aus allen Interfaces in der Summe nach der "am besten passenden" Schnittstelle suchen, die aber nicht genau definiert werden kann.

@rogpeppe In Ihrem Vorschlag, der Optimierungsmöglichkeiten bei der Implementierung und Feinheiten von Nullwerten ignoriert, besteht der Hauptvorteil der Verwendung eines Summentyps gegenüber einem manuell erstellten Schnittstellentyp (der die Schnittmenge der relevanten Methoden enthält) darin, dass die Typprüfung auf Fehler hinweisen kann zur Kompilierzeit und nicht zur Laufzeit. Ein zweiter Vorteil besteht darin, dass der Wert eines Typs differenzierter ist und somit die Lesbarkeit/das Verständnis eines Programms verbessern kann. Gibt es einen anderen großen Vorteil?

(Ich versuche nicht, den Vorschlag in irgendeiner Weise zu schmälern, sondern versuche nur, meine Intuition richtig zu machen. Besonders wenn die zusätzliche syntaktische und semantische Komplexität "vernünftig klein" ist - was auch immer das bedeuten mag - kann ich definitiv den Vorteil des Compilers sehen Fehler frühzeitig erkennen.)

@griesemer Ja, das ist ungefähr richtig.

Gerade bei der Kommunikation von Nachrichten über Kanäle oder das Netzwerk, denke ich, hilft es der Lesbarkeit und Korrektheit, einen Typ haben zu können, der genau die verfügbaren Möglichkeiten ausdrückt. Es ist derzeit üblich, dies halbherzig zu versuchen, indem man eine nicht exportierte Methode in einen Schnittstellentyp einbindet, aber dies ist a) durch Einbetten umgangen und b) es ist schwierig, alle möglichen Typen zu sehen, da die nicht exportierte Methode ausgeblendet ist.

@jimmyfrasche

Wenn Sie einen Typ haben, der io.ReadCloser | . ist io.Reader Sie können nicht sicher sein, wenn Sie switch eingeben oder auf io.Reader bestätigen, dass es kein io.ReadCloser ist, es sei denn, die Zuweisung zu einem Summentyp entpackt und reboxt die Schnittstelle.

Wenn Sie diesen Typ haben, wissen Sie, dass es immer ein io.Reader ist (oder nil, weil jeder io.Reader auch nil sein kann). Die beiden Alternativen sind nicht exklusiv - der vorgeschlagene Summentyp ist ein "inklusive oder" und kein "exklusives oder".

In die andere Richtung gehen, wenn Sie io.Reader hätten | io.ReadCloser würde es entweder nie ein io.ReadCloser akzeptieren, weil es strikt von rechts nach links geht, oder die Implementierung müsste aus allen Interfaces in der Summe nach der "am besten passenden" Schnittstelle suchen, die aber nicht genau definiert werden kann.

Wenn Sie mit "in die andere Richtung gehen" die Zuweisung zu diesem Typ meinen, lautet der Vorschlag:

"Wenn Sie einem Summentyp einen Wert zuweisen, wenn der Wert in mehr passt
als einer der möglichen Typen, dann wird der erste gewählt."

In diesem Fall kann ein io.ReadCloser sowohl in einen io.Reader als auch in einen io.ReadCloser passen, also wählt es io.Reader, aber es gibt eigentlich keine Möglichkeit, dies nachträglich zu sagen. Es ist kein Unterschied zwischen dem Typ io.Reader und dem Typ io.Reader erkennbar | io.ReadCloser, da io.Reader auch alle Schnittstellentypen aufnehmen kann, die io.Reader implementieren. Aus diesem Grund vermute ich, dass es eine gute Idee sein könnte, den Compiler dazu zu bringen, solche Typen abzulehnen. Es könnte beispielsweise jeden Summentyp ablehnen, der die Schnittstelle{} betrifft, da die Schnittstelle{} bereits einen beliebigen Typ enthalten kann, sodass die zusätzlichen Qualifikationen keine Informationen hinzufügen.

@rogpeppe, es gibt viele Dinge, die ich an deinem Vorschlag mag. Die Links-Rechts-Zuordnungssemantik und der Nullwert ist der Nullwert der Typregeln ganz links sind sehr klar und einfach. Sehr gut.

Worüber ich mir Sorgen mache, ist, einer Variablen vom Typ Summe einen Wert zuzuweisen, der bereits in einer Schnittstelle enthalten ist.

Lassen Sie uns für den Moment mein vorheriges Beispiel verwenden und sagen, dass RC eine Struktur ist, die einem io.ReadCloser zugewiesen werden kann.

Wenn du das tust

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

die ergebnisse sind offensichtlich und klar.

Wenn Sie dies jedoch tun

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

das einzig vernünftige ist, v store r als io.Reader zu haben, aber das bedeutet, wenn Sie switch on v eingeben, können Sie nicht sicher sein, dass Sie beim Drücken des io.Reader-Falls tatsächlich keinen haben io.ReadCloser. Sie müssten so etwas haben:

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

Nun, es gibt einen Sinn, in dem io.ReadCloser <:io.Reader ist, und Sie könnten diese einfach verbieten, wie Sie vorgeschlagen haben, aber ich denke, das Problem ist grundlegender und kann für jeden Summentypvorschlag für Go† gelten.

Nehmen wir an, Sie haben drei Interfaces A, B und C mit den Methoden A(), B() bzw. C() und eine Struktur ABC mit allen drei Methoden. A, B und C sind disjunkt, also A | B | C und seine Permutationen sind alle gültige Typen. Aber es gibt immer noch Fälle wie

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

Es gibt eine Reihe von Möglichkeiten, dies neu anzuordnen, und Sie erhalten immer noch keine aussagekräftigen Garantien dafür, was v ist, wenn Schnittstellen beteiligt sind. Nachdem Sie die Summe ausgepackt haben, müssen Sie die Schnittstelle auspacken, wenn die Reihenfolge wichtig ist.

Vielleicht sollte die Einschränkung darin bestehen, dass keiner der Summanden überhaupt Schnittstellen sein kann?

Die einzige andere Lösung, die mir einfällt, besteht darin, die Zuweisung einer Schnittstelle zu einer Variablen vom Typ Summe nicht zuzulassen, aber das scheint auf seine Weise schwerwiegender zu sein.

† Das beinhaltet keine Typkonstruktoren für die Typen in der Summe, die eindeutig gemacht werden sollen (wie in Haskell, wo Sie Just v sagen müssen, um einen Wert vom Typ Vielleicht zu konstruieren) – aber ich bin überhaupt nicht dafür.

@jimmyfrasche Ist der Anwendungsfall für das geordnete Unboxing tatsächlich wichtig? Das ist nicht offensichtlich für mich, und für die Fälle , in denen es wichtig ist es einfach , mit expliziter Box structs umgehen:

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

var v ReadCloser | Reader = Reader{r}

@bcmills Es ist eher so, dass die Ergebnisse nicht offensichtlich und fummelig sind und bedeutet, dass alle Garantien, die Sie mit einem

Das von Ihnen bereitgestellte Beispiel für explizite Box-Strukturen zeigt, dass das Verbieten von Schnittstellen in Summentypen die Leistungsfähigkeit von Summentypen überhaupt nicht einschränkt. Es erstellt effektiv die Typkonstruktoren zur Begriffsklärung, die ich in der Fußnote erwähnt habe. Zugegeben, es ist etwas nervig und ein zusätzlicher Schritt, aber es ist einfach und fühlt sich sehr an Gos Philosophie an, Sprachkonstrukte so orthogonal wie möglich zu lassen.

alle gewünschten Garantien mit einem Summentyp

Es kommt darauf an, welche Garantien Sie erwarten. Ich glaube, du erwartest einen Summentyp
ein streng gekennzeichneter Wert, sodass Sie bei allen Typen A|B|C genau wissen, was statisch ist
Typ, den Sie ihm zugewiesen haben. Ich sehe es als Typbeschränkung für einen einzelnen Wert von konkret
type - Die Einschränkung besteht darin, dass der Wert mit (mindestens) einem von A, B und C typkompatibel ist.
Am Ende ist es nur eine Schnittstelle mit einem Wert in.

Das heißt, wenn einem Summentyp ein Wert zugewiesen werden kann, weil er zuweisungskompatibel ist
Bei einem der Mitglieder des Summentyps erfassen wir nicht, welches dieser Mitglieder war
"chosen" - wir erfassen nur den Wert selbst. Wie bei der Zuweisung eines io.Readers
zu einer Schnittstelle{} verlieren Sie den statischen io.Reader-Typ und haben nur den Wert selbst
die mit io.Reader kompatibel ist, aber auch mit jedem anderen Schnittstellentyp, der passiert
implementieren.

In deinem Beispiel:

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

Eine Typzusicherung von v zu einem von A, B und C wäre erfolgreich. Das erscheint mir vernünftig.

@rogpeppe, diese Semantik macht mehr Sinn, als ich mir vorgestellt habe. Ich bin immer noch nicht ganz davon überzeugt, dass Schnittstellen und Summen gut harmonieren, aber ich bin mir nicht mehr sicher, ob das nicht der Fall ist. Fortschritt!

Angenommen, Sie haben type U I | *T wobei I ein Schnittstellentyp und *T ein Typ ist, der I implementiert.

Gegeben

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

der dynamische Typ von u ist *T und in

var u U = new(T)

Sie können auf das *T als I mit einer Typzusicherung zugreifen. Ist das korrekt?

Das würde bedeuten, dass bei der Zuordnung von einem gültigen Schnittstellenwert zu einer Summe nach dem ersten passenden Typ in der Summe gesucht werden müsste.

Es wäre auch etwas anders als etwas wie var v uint8 | int32 | int64 = i das, wie ich mir vorstelle, einfach immer mit demjenigen dieser drei Typen gehen würde, der i ist, selbst wenn i ein int64 , die in ein uint8 passen könnten.

Fortschritt!

Yay!

Sie können auf dieses *T als I mit einer Typzusicherung zugreifen. Ist das korrekt?

Jawohl.

Das würde bedeuten, dass bei der Zuordnung von einem gültigen Schnittstellenwert zu einer Summe nach dem ersten passenden Typ in der Summe gesucht werden müsste.

Yup, wie der Vorschlag sagt (natürlich weiß der Compiler statisch, welcher zu wählen ist, damit zur Laufzeit nicht gesucht wird).

Es wäre auch etwas anders als so etwas wie var v uint8 | int32 | int64 = i, was, wie ich mir vorstelle, immer mit einem dieser drei Typen i gehen würde, selbst wenn ich ein int64 wäre, der in ein uint8 passen könnte.

Ja, denn wenn i keine Konstante ist, kann es nur einer dieser Alternativen zugewiesen werden.

Ja, denn wenn i keine Konstante ist, kann es nur einer dieser Alternativen zugewiesen werden.

Mir ist klar, dass das nicht ganz stimmt, da die Regel die Zuweisung unbenannter Typen an benannte Typen zulässt. Ich denke aber das macht keinen großen Unterschied. Die Regel bleibt dieselbe.

Also ist der Typ I | *T aus meinem letzten Beitrag effektiv der gleiche wie der Typ I und io.ReadCloser | io.Reader ist effektiv der gleiche Typ wie io.Reader ?

Korrekt. Beide Typen würden von meiner vorgeschlagenen Regel abgedeckt, dass der Compiler Summentypen ablehnt, bei denen ein Typ eine Schnittstelle ist, die von einem anderen der Typen implementiert wird. Dieselbe oder eine ähnliche Regel könnte Summentypen mit doppelten Typen wie int|int abdecken.

Ein Gedanke: Es ist vielleicht nicht intuitiv, dass int|byte nicht dasselbe ist wie byte|int , aber in der Praxis ist es wahrscheinlich in Ordnung.

Das würde bedeuten, dass bei der Zuordnung von einem gültigen Schnittstellenwert zu einer Summe nach dem ersten passenden Typ in der Summe gesucht werden müsste.

Yup, wie der Vorschlag sagt (natürlich weiß der Compiler statisch, welcher zu wählen ist, damit zur Laufzeit nicht gesucht wird).

Ich verfolge das nicht. So wie ich es gelesen habe (was sich von dem beabsichtigten unterscheiden könnte), gibt es mindestens zwei Möglichkeiten, mit einer Vereinigung von U von I und T-Implementen-I umzugehen.

1a) bei Zuweisung von U u = t wird das Tag auf T gesetzt. Spätere Auswahl führt zu einem T, da das Tag ein T ist.
1b) bei Zuweisung von U u = i (i ist wirklich ein T) wird das Tag auf I gesetzt. Spätere Auswahl führt zu einem T, da das Tag ein I ist, aber eine zweite Prüfung (wird durchgeführt, weil T I und T implementiert ist Mitglied von U) entdeckt ein T.

2a) wie 1a
2b) Bei der Zuweisung von U u = i (i ist wirklich ein T) überprüft der generierte Code den Wert (i), um zu sehen, ob es tatsächlich ein T ist, da T I implementiert und T auch ein Mitglied von U ist das heißt, das Tag wird auf T gesetzt. Eine spätere Auswahl führt direkt zu einem T.

Für den Fall, dass T, V, W alle I und U = *T | *V | *W | I implementieren, erfordert die Zuweisung U u = i (bis zu) 3 Typprüfungen.

Schnittstellen und Zeiger waren jedoch nicht der ursprüngliche Anwendungsfall für Unionstypen, oder?

Ich kann mir bestimmte Arten von Hacker-Angriffen vorstellen, bei denen eine "nette" Implementierung ein bisschen hämmern würde - wenn Sie beispielsweise eine Vereinigung von 4 oder weniger Zeigertypen haben, bei der alle Referenten 4-Byte-ausgerichtet sind, speichern Sie das Tag in den unteren 2 Bit des Wertes. Dies wiederum impliziert, dass es nicht gut ist, die Adresse eines Mitglieds einer Gewerkschaft zu verwenden (das wäre sowieso nicht, da diese Adresse verwendet werden könnte, um einen "alten" Typ wiederherzustellen, ohne das Tag anzupassen).

Oder wenn wir einen 50-Bit-Adressraum hätten und bereit wären, uns bei NaNs einige Freiheiten zu nehmen, könnten wir Ganzzahlen, Zeiger und Doubles alle in eine 64-Bit-Union packen, und die möglichen Kosten für einiges Herumfummeln.

Beide Untervorschläge sind eklig, ich bin mir sicher, dass beide eine kleine (?) Anzahl fanatischer Befürworter haben würden.

Dies wiederum impliziert, dass es nicht gut ist, die Adresse eines Mitglieds einer Gewerkschaft zu nehmen

Richtig. Aber ich glaube, das Ergebnis einer Typzusicherung ist heute ohnehin nicht adressierbar, oder?

bei Zuweisung von U u = i (i ist wirklich ein T) wird das Tag auf I gesetzt.

Ich denke, das ist der Knackpunkt - es gibt kein Tag I.

Ignorieren Sie für einen Moment die Laufzeitdarstellung und betrachten Sie einen Summentyp als Schnittstelle. Wie jede Schnittstelle hat sie einen dynamischen Typ (den Typ, der darin gespeichert ist). Das "Tag", auf das Sie sich beziehen, ist genau dieser dynamische Typ.

Wie Sie vorschlagen (und ich habe im letzten Absatz des Vorschlags versucht anzudeuten), gibt es möglicherweise Möglichkeiten, das Typ-Tag effizienter zu speichern als mit einem Zeiger auf den Laufzeittyp, aber am Ende ist es immer nur die Kodierung der Dynamik Typ des Summentypwerts, nicht welche der Alternativen bei der Erstellung "gewählt" wurde.

Schnittstellen und Zeiger waren jedoch nicht der ursprüngliche Anwendungsfall für Unionstypen, oder?

War es nicht, aber meiner Ansicht nach muss jeder Vorschlag in Bezug auf andere Sprachmerkmale so orthogonal wie möglich sein.

@dr2chase Mein Summentyp Schnittstellentypen in seiner Definition enthält, seine Implementierung zur Laufzeit identisch mit einer Schnittstelle ist (die die Schnittmenge von Methodensätzen enthält), aber die Kompilierzeit-Invarianten über zulässige Typen sind immer noch durchgesetzt.

Selbst wenn ein Summentyp nur konkrete Typen enthielt und wie eine diskriminierte Vereinigung im C-Stil implementiert wurde, könnten Sie keinen Wert im Summentyp adressieren, da diese Adresse nach der Übernahme einen anderen Typ (und eine andere Größe) annehmen könnte die Adresse. Sie können jedoch die Adresse des in Summe eingegebenen Werts selbst verwenden.

Ist es wünschenswert, dass sich Summentypen so verhalten? Wir könnten genauso gut erklären, dass der ausgewählte/bestätigte Typ mit dem übereinstimmt, was der Programmierer gesagt/impliziert hat, als der Union ein Wert zugewiesen wurde. Andernfalls könnten wir zu interessanten Stellen in Bezug auf int8 vs. int16 vs. int32 usw. geführt werden. Oder zB int8 | uint8 .

Ist es wünschenswert, dass sich Summentypen so verhalten?

Das ist Ermessenssache. Ich glaube, es ist so, weil wir bereits das Konzept von Schnittstellen in der Sprache haben - Werte sowohl mit statischem als auch mit dynamischem Typ. Die vorgeschlagenen Summentypen bieten in einigen Fällen nur eine genauere Möglichkeit, Schnittstellentypen anzugeben. Es bedeutet auch, dass Summentypen ohne Einschränkung auf andere Typen funktionieren können. Wenn Sie dies nicht tun, müssen Sie Schnittstellentypen ausschließen und das Feature ist dann nicht vollständig orthogonal.

Andernfalls könnten wir zu interessanten Stellen in Bezug auf int8 vs. int16 vs. int32 usw. geführt werden. Oder zB int8 | uint8.

Was ist Ihr Anliegen hier?

Sie können einen Funktionstyp nicht als Schlüsseltyp einer Map verwenden. Ich sage nicht, dass das gleichwertig ist, nur dass es Präzedenzfälle für Typen gibt, die andere Typen von Typen einschränken. Immer noch offen für das Zulassen von Schnittstellen, immer noch nicht verkauft.

Welche Art von Programmen können Sie mit einem Summentyp schreiben, der Schnittstellen enthält, die Sie sonst nicht können?

Gegenvorschlag.

Ein Union-Typ ist ein Typ, der null oder mehr Typen auflistet, geschrieben

union {
  T0
  T1
  //...
  Tn
}

Alle aufgeführten Typen (T0, T1, ..., Tn) in einer Union müssen unterschiedlich sein und keiner darf ein Schnittstellentyp sein.

Methoden können nach den üblichen Regeln für einen definierten (benannten) Unionstyp deklariert werden. Von den aufgelisteten Typen werden keine Methoden heraufgestuft.

Es gibt keine Einbettung für Union-Typen. Das Auflisten eines Unionstyps in einem anderen entspricht dem Auflisten jedes anderen gültigen Typs. Eine Union kann ihren eigenen Typ jedoch nicht rekursiv auflisten, aus dem gleichen Grund, aus dem type S struct { S } ungültig ist.

Unions können in Strukturen eingebettet werden.

Der Wert eines Union-Typs ist ein dynamischer Typ, der auf einen der aufgelisteten Typen beschränkt ist, und ein Wert des dynamischen Typs – angeblich der gespeicherte Wert. Genau einer der aufgeführten Typen ist immer der dynamische Typ.

Der Nullwert der leeren Vereinigung ist eindeutig. Der Nullwert einer nichtleeren Union ist der Nullwert des ersten in der Union aufgelisteten Typs.

Ein Wert für einen Unionstyp, U , kann mit U{} für den Nullwert erstellt werden. Wenn U einen oder mehrere Typen hat und v ein Wert eines der aufgelisteten Typen ist, erzeugt T , U{v} einen Vereinigungswert, der v speichert T . Wenn v von einem Typ ist, der nicht in U ist und mehr als einem der aufgeführten Typen zugeordnet werden kann, ist eine explizite Konvertierung erforderlich, um eine eindeutige Zuordnung vorzunehmen.

Ein Wert eines Unionstyps U kann wie in V(U{}) in einen anderen Unionstyp V umgewandelt werden, wenn die Menge der Typen in U eine Teilmenge der Satz von Typen in V . Das heißt, ohne die Reihenfolge zu beachten, muss U dieselben Typen wie V , und U darf keine Typen haben, die nicht in V sondern in V kann Typen haben, die nicht in U .

Die Zuweisbarkeit zwischen Unionstypen wird als Konvertibilität definiert, solange höchstens einer der Unionstypen definiert (benannt) ist.

Einer Variablen vom Unionstyp U kann ein Wert eines der aufgeführten Typen T eines Unionstyps U zugewiesen werden. Dies setzt den dynamischen Typ auf T und speichert den Wert. Zuweisungskompatible Werte funktionieren wie oben.

Wenn alle aufgeführten Typen die Gleichheitsoperatoren unterstützen:

  • die Gleichheitsoperatoren können für zwei Werte desselben Unionstyps verwendet werden. Zwei Werte eines Unionstyps sind niemals gleich, wenn sich ihre dynamischen Typen unterscheiden.
  • ein Wert dieser Vereinigung kann mit einem Wert eines beliebigen seiner aufgelisteten Typen verglichen werden. Wenn der dynamische Typ der Union nicht der Typ des anderen Operanden ist, ist == falsch und != ist wahr, unabhängig vom gespeicherten Wert. Zuweisungskompatible Werte funktionieren wie oben.
  • die Union kann als Kartenschlüssel verwendet werden

Für Werte eines Unionstyps werden keine anderen Operatoren unterstützt.

Eine Typ-Assertion gegen einen Union-Typ für einen der aufgelisteten Typen gilt, wenn der geltend gemachte Typ der dynamische Typ ist.

Eine Typzusicherung gegen einen Union-Typ für einen Schnittstellentyp gilt, wenn sein dynamischer Typ diese Schnittstelle implementiert. (Bemerkenswert ist, dass die Assertion immer gilt, wenn alle aufgeführten Typen diese Schnittstelle implementieren).

Typwechsel müssen entweder vollständig sein, einschließlich aller aufgelisteten Typen, oder einen Standardfall enthalten.

Typzusicherungen und Typschalter geben eine Kopie des gespeicherten Werts zurück.

Package Reflect würde eine Möglichkeit zum Abrufen des dynamischen Typs und des gespeicherten Werts eines reflektierten Unionswerts sowie eine Möglichkeit zum Abrufen der aufgelisteten Typen eines reflektierten Unionstyps erfordern.

Anmerkungen:

Die Syntax union{...} wurde teilweise gewählt, um sich vom Vorschlag des Summentyps in diesem Thread zu unterscheiden, in erster Linie um die netten Eigenschaften in der Go-Grammatik beizubehalten und nebenbei zu betonen, dass dies eine diskriminierte Vereinigung ist. Als Konsequenz erlaubt dies etwas seltsame Vereinigungen wie union{} und union{ int } . Der erste ist in vielerlei Hinsicht äquivalent zu struct{} (obwohl per Definition ein anderer Typ), so dass er nichts zur Sprache hinzufügt, außer dass ein weiterer leerer Typ hinzugefügt wird. Das zweite ist vielleicht nützlicher. Zum Beispiel type Id union { int } ist sehr viel wie type Id struct { int } , außer dass die Union Version ermöglicht die direkte Zuordnung ohne angeben zu müssen idValue.int ermöglicht es eher wie ein in Art gebaut zu sein scheint.

Die eindeutige Konvertierung, die beim Umgang mit zuweisungskompatiblen Typen erforderlich ist, ist etwas hart, würde jedoch Fehler abfangen, wenn eine Union aktualisiert wird, um eine Mehrdeutigkeit einzuführen, auf die nachgelagerter Code nicht vorbereitet ist.

Das Fehlen einer Einbettung ist eine Folge davon, dass Methoden auf Unions zugelassen werden und eine erschöpfende Übereinstimmung in Typwechseln erforderlich ist.

Wenn Sie Methoden für die Union selbst zulassen, anstatt die gültige Schnittmenge von Methoden der aufgeführten Typen zu verwenden, wird vermieden, dass versehentlich unerwünschte Methoden erhalten werden. Der Typ, der den gespeicherten Wert an allgemeine Schnittstellen bestätigt, ermöglicht einfache, explizite Wrapper-Methoden, wenn eine Heraufstufung gewünscht wird. Beispiel: Bei einem Unionstyp U implementieren alle aufgelisteten Typen fmt.Stringer :

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

Im verlinkten Reddit-Thread sagte rsc:

Es wäre seltsam für den Nullwert von sum { X; Y } von sum { Y; X }. So funktionieren Summen normalerweise nicht.

Ich habe darüber nachgedacht, da es wirklich auf jeden Vorschlag zutrifft.

Das ist kein Fehler, sondern ein Feature.

Erwägen

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

vs.

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

UndefinedOrInt sagt standardmäßig, dass es noch nicht definiert ist, aber wenn es so ist, wird es ein int Wert sein. Dies ist analog zu *int , so dass der Summentyp (1 + int) jetzt in Go dargestellt werden muss und der Nullwert auch analog ist.

IntOrIllegal hingegen sagt standardmäßig, dass es die int 0 ist, aber es kann irgendwann als illegal markiert werden. Dies ist immer noch analog zu *int aber der Nullwert drückt die Absicht aus, wie zum Beispiel, dass er den Standardwert new(int) erzwingt.

Es ist so, als ob Sie ein bool-Feld in einer Struktur negativ formulieren könnten, sodass der Nullwert der Standardwert ist.

Beide Nullwerte der Summen sind für sich genommen sinnvoll und sinnvoll, und der Programmierer kann den für die Situation am besten geeigneten auswählen.

Wenn die Summe eine Wochentage-Enumeration wäre (jeder Tag ist ein definiertes struct{} ), ist der erste Tag der erste Tag der Woche, das gleiche gilt für eine iota -Enumeration.

Außerdem sind mir keine Sprachen mit Summentypen oder diskriminierten/getaggten Vereinigungen bekannt, die das Konzept eines Nullwerts haben. C wäre am nächsten, aber der Nullwert ist nicht initialisierter Speicher – kaum eine Spur, der man folgen kann. Java ist standardmäßig null, glaube ich, aber das liegt daran, dass alles eine Referenz ist. Alle anderen Sprachen, die ich kenne, haben obligatorische Typkonstruktoren für die Summanden, so dass es nicht wirklich einen Nullwert gibt. Gibt es eine solche Sprache? Was tut es?

Wenn der Unterschied zu den mathematischen Konzepten von "Summe" und "Union" das Problem ist, können wir sie immer anders nennen (zB "Variante").

Bei Namen: Union verwirrt c/c++-Puristen. Variant ist vor allem COBRA- und COM-Programmierern vertraut, wobei die diskriminierte Vereinigung von den funktionalen Sprachen bevorzugt wird. Set ist ein Verb und ein Substantiv. Ich mag das Stichwort _pick_. Limbo verwendet _pick_. Es ist kurz und beschreibt die Absicht des Typs, aus einer endlichen Menge von Typen auszuwählen.

Der Name/die Syntax ist weitgehend irrelevant. Auswahl wäre gut.

Jeder Vorschlag in diesem Thread passt zur mengentheoretischen Definition.

Der erste Typ, der speziell für den Nullwert ist, ist irrelevant, da typtheoretische Summen kommutieren, sodass die Reihenfolge irrelevant ist (A + B = B + A). Mein Vorschlag behält diese Eigenschaft bei, aber Produkttypen pendeln auch in der Theorie ein und werden in der Praxis von den meisten Sprachen (einschließlich Go) als unterschiedlich angesehen, sodass dies wahrscheinlich nicht unbedingt erforderlich ist.

@jimmyfrasche

Ich persönlich glaube, dass es ein sehr großer Nachteil ist, Schnittstellen nicht als 'Pick'-Mitglieder zuzulassen. Erstens würde es einen der großen Anwendungsfälle von 'Pick'-Typen vollständig vereiteln - einen Fehler zu haben, der eines der Mitglieder ist. Oder Sie möchten mit einem Auswahltyp arbeiten, der entweder einen io.Reader oder einen String hat, wenn Sie den Benutzer nicht zwingen möchten, zuvor einen StringReader zu verwenden. Aber alles in allem ist eine Schnittstelle nur ein anderer Typ, und ich glaube, es sollte keine Typbeschränkungen für 'pick'-Mitglieder geben. Wenn ein Pick-Typ also zwei Schnittstellenmember hat, von denen einer vollständig vom anderen eingeschlossen ist, sollte dies, wie bereits erwähnt, ein Kompilierzeitfehler sein.

Was mir an Ihrem Gegenvorschlag gefällt, ist die Tatsache, dass Methoden für den Picktyp definiert werden können. Ich denke nicht, dass es einen Querschnitt der Methoden der Member bereitstellen sollte, da es nicht viele Fälle geben würde, in denen Methoden zu allen Membern gehören (und Sie haben sowieso Schnittstellen dafür). Und ein erschöpfender Schalter + Standardfall ist eine sehr gute Idee.

@rogpeppe @jimmyfrasche Etwas, das ich in Ihren Vorschlägen nicht sehe, ist, warum wir dies tun sollten. Das Hinzufügen eines neuen Typs hat einen klaren Nachteil: Es ist ein neues Konzept, das jeder lernen muss, der Go lernt. Was ist der kompensierende Vorteil? Was bringt uns die neue Art von Typ insbesondere, was wir von Schnittstellentypen nicht bekommen?

@ianlancetaylor Robert hat es hier gut zusammengefasst: https://github.com/golang/go/issues/19412#issuecomment -288608089

@ianlancetaylor
Am Ende des Tages macht es Code lesbarer, und das ist die wichtigste Anweisung von Go. Betrachten Sie json.Token, es ist derzeit als Schnittstelle{} definiert, aber die Dokumentation besagt, dass es tatsächlich nur einer einer bestimmten Anzahl von Typen sein kann. Wenn es andererseits geschrieben ist als

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

Der Benutzer sieht sofort alle Möglichkeiten, und die Werkzeuge können automatisch einen vollständigen Schalter erstellen. Darüber hinaus verhindert der Compiler, dass Sie dort auch einen unerwarteten Typ einfügen.

Am Ende des Tages macht es Code lesbarer, und das ist die wichtigste Anweisung von Go.

Mehr Funktionen bedeuten, dass man mehr wissen muss, um den Code zu verstehen. Für eine Person mit nur durchschnittlichen Kenntnissen einer Sprache ist ihre Lesbarkeit notwendigerweise umgekehrt proportional zur Anzahl der [neu hinzugefügten] Merkmale.

@cznic

Mehr Funktionen bedeuten, dass man mehr wissen muss, um den Code zu verstehen.

Nicht immer. Wenn Sie "mehr über die Sprache wissen" durch "mehr über schlecht oder inkonsistent dokumentierte Invarianten im Code wissen" ersetzen können, kann das immer noch ein Nettogewinn sein. (Das heißt, globales Wissen kann den Bedarf an lokalem Wissen verdrängen.)

Wenn eine bessere Typüberprüfung während der Kompilierung tatsächlich der einzige Vorteil ist, können wir einen sehr ähnlichen Vorteil erzielen, ohne die Sprache zu ändern, indem wir einen vom Tierarzt überprüften Kommentar einfügen. Etwas wie

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

Nun, wir haben derzeit keine Tierarzt-Kommentare, daher ist dies kein ganz ernsthafter Vorschlag. Aber ich meine es mit der Grundidee ernst: Wenn der einzige Vorteil, den wir haben, der ist, den wir vollständig mit einem statischen Analysewerkzeug erzielen können, lohnt es sich dann wirklich, der Sprache ein komplexes neues Konzept hinzuzufügen?

Viele, vielleicht alle, von cmd/vet durchgeführten Tests könnten der Sprache hinzugefügt werden, in dem Sinne, dass sie vom Compiler und nicht von einem separaten statischen Analysetool überprüft werden könnten. Aber aus verschiedenen Gründen finden wir es sinnvoll, vet vom Compiler zu trennen. Warum fällt dieses Konzept eher auf die sprachliche als auf die tierärztliche Seite?

@ianlancetaylor hat die Kommentare erneut überprüft: https://github.com/BurntSushi/go-sumtype

@ianlancetaylor Soweit die Änderung gerechtfertigt ist, habe ich das aktiv ignoriert – oder eher zurückgedrängt. Abstrakt darüber zu sprechen, ist vage und hilft mir nicht weiter: Für mich klingt das alles nach "Gutes ist gut und Böses ist schlecht". Ich wollte eine Vorstellung davon bekommen, was der Typ tatsächlich sein würde – was seine Einschränkungen sind, welche Auswirkungen er hat, was die Vor- und Nachteile sind – damit ich sehen konnte, wie er in die Sprache passt (oder nicht! ) und habe eine Idee, wie ich es in Programmen verwenden würde/könnte. Ich glaube, ich habe jetzt, zumindest aus meiner Sicht, eine gute Vorstellung davon, was Summentypen in Go bedeuten müssten. Ich bin nicht ganz davon überzeugt, dass sie es wert sind (auch wenn ich sie wirklich schlecht haben möchte), aber jetzt habe ich etwas Solides zu analysieren mit gut definierten Eigenschaften, über die ich nachdenken kann. Ich weiß, das ist per se nicht wirklich eine Antwort, aber hier bin ich zumindest dabei.

Wenn eine bessere Typüberprüfung während der Kompilierung tatsächlich der einzige Vorteil ist, können wir einen sehr ähnlichen Vorteil erzielen, ohne die Sprache zu ändern, indem wir einen vom Tierarzt überprüften Kommentar einfügen.

Dies ist immer noch anfällig für die Kritik an der Notwendigkeit, neue Dinge zu lernen. Wenn ich etwas über diese magischen Veterinärkommentare lernen muss, um Code zu debuggen/zu verstehen/zu verwenden, ist das eine mentale Belastung, egal ob wir sie dem Go-Sprachbudget oder dem technisch nicht-the-Go-Sprachbudget zuordnen. Wenn überhaupt, sind magische Kommentare teurer, weil ich nicht wusste, dass ich sie lernen muss, als ich dachte, ich hätte die Sprache gelernt.

@cznic
Ich bin nicht einverstanden. Mit Ihrer jetzigen Annahme können Sie nicht sicher sein, dass eine Person dann versteht, was ein Kanal oder gar eine Funktion ist. Doch diese Dinge existieren in der Sprache. Und ein neues Feature bedeutet nicht automatisch, dass es die Sprache erschwert. In diesem Fall würde ich argumentieren, dass dies die Verständlichkeit tatsächlich erleichtert, da dem Leser sofort klar wird, was ein Typ sein soll, im Gegensatz zur Verwendung eines Black-Box-Interface-Typs{}.

@ianlancetaylor
Ich persönlich denke, dass diese Funktion mehr damit zu tun hat, Code leichter zu lesen und zu argumentieren. Compile Time Safety ist ein sehr nettes Feature, aber nicht das wichtigste. Es würde nicht nur eine Typsignatur sofort offensichtlicher machen, sondern ihre spätere Verwendung wäre auch leichter zu verstehen und einfacher zu schreiben. Die Leute müssten nicht mehr in Panik greifen, wenn sie einen Typ erhalten, den sie nicht erwartet haben - das ist das aktuelle Verhalten auch in der Standardbibliothek, sodass sie sich leichter über die Verwendung Gedanken machen können, ohne durch das Unbekannte belastet zu werden . Und ich denke, es ist keine gute Idee, sich dafür auf Kommentare und andere Tools (auch wenn sie Erstanbieter) zu verlassen, da eine sauberere Syntax besser lesbar ist als ein solcher Kommentar. Und Kommentare sind strukturlos und viel einfacher zu vermasseln.

@ianlancetaylor

Warum fällt dieses Konzept eher auf die sprachliche als auf die tierärztliche Seite?

Sie können dieselbe Frage auf jedes Feature außerhalb des Turing-Complete-Kerns anwenden, und wir wollen wohl nicht, dass Go eine "Turing-Plane" ist. Auf der anderen Seite, haben wir Beispiele für Sprachen , die bedeutenden Untergruppen der tatsächlichen Sprache geschoben hat eine generische „Erweiterung“ Syntax aus. (Zum Beispiel "Attribute" in Rust, C++ und GNU C.)

Der Hauptgrund, Funktionen in Erweiterungen oder Attribute statt in eine Kernsprache aufzunehmen, besteht darin, die Syntaxkompatibilität zu wahren, einschließlich der Kompatibilität mit Tools, die die neue Funktion nicht kennen. (Ob "Kompatibilität mit Tools" in der Praxis tatsächlich funktioniert, hängt stark davon ab, was das Feature tatsächlich tut.)

Im Kontext von Go scheint der Hauptgrund für das Einfügen von Funktionen in vet darin zu bestehen, Änderungen zu implementieren, die die Kompatibilität von Go 1 nicht aufrechterhalten würden, wenn sie auf die Sprache selbst angewendet würden. Das sehe ich hier nicht als Problem.

Ein Grund, keine Features in vet besteht darin, dass sie während der Kompilierung weitergegeben werden müssen. Wenn ich zum Beispiel schreibe:

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

Erhalte ich die richtigen Warnungen für Typen, die nicht in der Summe enthalten sind, über Paketgrenzen hinweg? Es ist für mich nicht offensichtlich, dass vet eine so tiefgehende transitive Analyse durchführen kann, also ist das vielleicht ein Grund, warum es in die Kernsprache gehen muss.

@dr2chase Im Allgemeinen haben Sie natürlich Recht ? Der Code ist vollständig verständlich, ohne zu wissen, was der magische Kommentar bedeutet. Der magische Kommentar ändert nichts an dem, was der Code tut. Die Fehlermeldungen vom Tierarzt sollten klar sein.

@bcmills

Warum fällt dieses Konzept eher auf die sprachliche als auf die tierärztliche Seite?

Sie können dieselbe Frage auf jedes Feature außerhalb des Turing-vollständigen Kerns anwenden ....

Ich stimme nicht zu. Wenn das diskutierte Feature den kompilierten Code beeinflusst, gibt es ein automatisches Argument dafür. In diesem Fall wirkt sich die Funktion anscheinend nicht auf den kompilierten Code aus.

(Und ja, der Tierarzt kann die Quelle importierter Pakete analysieren.)

Ich versuche nicht zu behaupten, dass mein Argument über den Tierarzt schlüssig ist. Aber jeder Sprachwechsel geht von einer negativen Position aus: Eine einfache Sprache ist sehr wünschenswert, und eine solche bedeutende Neuerung macht die Sprache unweigerlich komplexer. Sie brauchen starke Argumente für einen Sprachwechsel. Und aus meiner Sicht sind diese starken Argumente noch nicht aufgetaucht. Immerhin haben wir lange über dieses Thema nachgedacht und es ist eine FAQ (https://golang.org/doc/faq#variant_types).

@ianlancetaylor

In diesem Fall wirkt sich die Funktion anscheinend nicht auf den kompilierten Code aus.

Ich denke, das hängt von den konkreten Details ab? Das Verhalten "Nullwert der Summe ist der Nullwert des ersten Typs", das @jimmyfrasche oben erwähnt (https://github.com/golang/go/issues/19412#issuecomment-289319916) sicherlich würde.

@urandom Ich habe eine lange Erklärung geschrieben, warum Interface- und Typkonstruktoren vermischt werden, aber dann wurde mir klar, dass es eine vernünftige Möglichkeit gibt, dies zu tun, also:

Quick and dirty Gegenvorschlag zu meinem Gegenvorschlag. (Alles, was nicht ausdrücklich erwähnt wird, entspricht meinem vorherigen Vorschlag). Ich bin mir nicht sicher, ob ein Vorschlag besser ist als der andere, aber dieser erlaubt Schnittstellen und ist rundherum expliziter:

Die Union hat explizite "Feldnamen", die im Folgenden "Tag-Namen" genannt werden:

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

Es gibt noch keine Einbettung. Es ist immer ein Fehler, einen Typ ohne Tag-Namen zu haben.

Union-Werte haben eher ein dynamisches Tag als einen dynamischen Typ.

Wörtliche Wertschöpfung: U{v} gilt nur bei absoluter Eindeutigkeit, ansonsten muss es U{Tag: v} .

Konvertibilität und Zuweisungskompatibilität berücksichtigen auch Tag-Namen.

Die Zuweisung zu einer Gewerkschaft ist keine Zauberei. Es bedeutet immer, einen kompatiblen Unionswert zuzuweisen. Um den gespeicherten Wert zu setzen, muss explizit der gewünschte Tag-Name verwendet werden: v.Good = 1 setzt den dynamischen Tag auf Good und den gespeicherten Wert auf 1.

Der Zugriff auf den gespeicherten Wert verwendet eine Tag-Assertion anstelle einer Typ-Assertion:

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

v.Tag ist ein Fehler auf der RHS, da es mehrdeutig ist.

Tag-Schalter sind wie Typ-Schalter, geschrieben switch v.[type] , außer dass die Fälle die Tags der Vereinigung sind.

Typzusicherungen gelten in Bezug auf den Typ des dynamischen Tags. Typschalter funktionieren ähnlich.

Bei gegebenen Werten a, b eines Unionstyps ist a == b, wenn ihre dynamischen Tags gleich sind und der gespeicherte Wert gleich ist.

Um zu überprüfen, ob der gespeicherte Wert ein bestimmter Wert ist, ist eine Tag-Assertion erforderlich.

Wenn ein Tag-Name nicht exportiert wird, kann er nur in dem Paket, das die Union definiert, gesetzt und darauf zugegriffen werden. Dies bedeutet, dass ein Tag-Switch einer Union mit gemischt exportierten und nicht exportierten Tags außerhalb des definierenden Pakets ohne einen Standardfall niemals vollständig sein kann. Wenn alle Tags nicht exportiert sind, handelt es sich um eine Blackbox.

Reflection muss auch die Tag-Namen verarbeiten.

e: Klarstellung für verschachtelte Gewerkschaften. Gegeben

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

Der Wert von u ist das dynamische Tag A und der gespeicherte Wert ist die anonyme Vereinigung mit dem dynamischen Tag A1 und sein gespeicherter Wert ist der Nullwert von T1.

u.B.B2 = returnsSomeT3()

ist alles, was erforderlich ist, um u vom Wert Null zu ändern, obwohl es von einer der verschachtelten Vereinigungen zur anderen wechselt, da alles an einem Speicherort gespeichert ist. Aber

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

hat zwei Chancen, in Panik zu geraten, da Tag-Asserts auf zwei Union-Werten ausgeführt werden und die 2-wertige Version der Tag-Assertion nicht verfügbar ist, ohne sich auf mehrere Zeilen aufzuteilen. Verschachtelte Tag-Switches wären in diesem Fall sauberer.

edit2: Klarstellung zu Typzusicherungen.

Gegeben

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

ein Assert-Typ wie u.(int) ist durchaus sinnvoll. Innerhalb des definierenden Pakets würde das immer gelten. Wenn sich u jedoch außerhalb des definierenden Pakets befindet, würde u.(int) in Panik geraten, wenn das dynamische Tag unexported , um ein Durchsickern von Implementierungsdetails zu vermeiden. Ähnlich für Assertionen für einen Schnittstellentyp.

@ianlancetaylor Hier sind ein paar Beispiele dafür, wie diese Funktion helfen würde:

  1. Das Herzstück einiger Pakete (z. B. go/ast ) sind ein oder mehrere große Summentypen. Es ist schwierig, in diesen Paketen zu navigieren, ohne diese Typen zu verstehen. Noch verwirrender ist, dass ein Summentyp manchmal durch ein Interface mit Methoden (zB go/ast.Node ), manchmal durch das leere Interface (zB go/ast.Object.Decl ) repräsentiert wird.

  2. Das Kompilieren des protobuf oneof Features zu Go führt zu einem nicht exportierten Schnittstellentyp, dessen einziger Zweck darin besteht, sicherzustellen, dass die Zuweisung an das oneof-Feld typsicher ist. Das wiederum erfordert die Generierung eines Typs für jeden Zweig des oneof. Typliterale für das Endprodukt sind schwer zu lesen und zu schreiben:

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

    Einige (wenn auch nicht alle) oneofs könnten durch Summentypen ausgedrückt werden.

  3. Manchmal ist ein „Vielleicht“-Typ genau das, was man braucht. Beispielsweise erlauben viele Operationen zur Aktualisierung von Google API-Ressourcen, dass eine Teilmenge der Felder der Ressource geändert wird. Eine natürliche Möglichkeit, dies in Go auszudrücken, ist eine Variante der Ressourcenstruktur mit einem "vielleicht"-Typ für jedes Feld. Die Google Cloud Storage ObjectAttrs- Ressource sieht beispielsweise so aus

    type ObjectAttrs struct {
       ContentType string
       ...
    }
    

    Um Teilaktualisierungen zu unterstützen, definiert das Paket auch

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

    Wobei optional.String aussieht ( godoc ):

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

    Dies ist sowohl schwer zu erklären als auch typunsicher, aber in der Praxis erweist es sich als praktisch, da ein ObjectAttrsToUpdate Literal genau wie ein ObjectAttrs Literal aussieht, während Präsenz kodiert wird. Ich wünschte, wir hätten schreiben können

    type ObjectAttrsToUpdate struct {
       ContentType string | nil
       ...
    }
    
  4. Viele Funktionen geben (T, error) mit xor-Semantik zurück (T ist sinnvoll, wenn error gleich null ist). Das Schreiben des Rückgabetyps als T | error würde die Semantik klären, die Sicherheit erhöhen und mehr Möglichkeiten für die Komposition bieten. Auch wenn wir (aus Kompatibilitätsgründen) den Rückgabewert einer Funktion nicht ändern können oder wollen, ist der Summentyp immer noch nützlich, um diesen Wert herumzutragen, wie zum Beispiel ihn in einen Kanal zu schreiben.

Eine go vet Annotation würde zwar in vielen dieser Fälle helfen, aber nicht in den Fällen, in denen ein anonymer Typ sinnvoll ist. Ich denke, wenn wir Summentypen hätten, würden wir viele sehen

chan *Response | error

Dieser Typ ist kurz genug, um mehrmals auszuschreiben.

@ianlancetaylor das ist wahrscheinlich kein

(Unter Verwendung meines neuesten Vorschlags mit Tags für die Syntax/Semantik unten. Auch unter der Annahme, dass der ausgegebene Code im Grunde wie der C-Code ist, den ich viel früher im Thread gepostet habe.)

Summentypen überschneiden sich mit Iota, Zeigern und Schnittstellen.

Jota

Diese beiden Typen sind ungefähr äquivalent:

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

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

und

type Stoplight int

const (
  Green Stoplight = iota
  Yellow
  Red
)

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

Der Compiler würde wahrscheinlich für beide genau den gleichen Code ausgeben.

In der union-Version wird das int zu einem versteckten Implementierungsdetail. Bei der iota-Version können Sie fragen, was Gelb/Rot ist oder einen Stoplight-Wert auf -42 setzen, aber nicht bei der Union-Version – das sind alles Compilerfehler und Invarianten, die bei der Optimierung berücksichtigt werden können. In ähnlicher Weise können Sie einen (Wert-)Schalter schreiben, der gelbe Lichter nicht berücksichtigt, aber mit einem Tag-Schalter benötigen Sie einen Standardfall, um dies explizit zu machen.

Natürlich gibt es Dinge, die Sie mit iota tun können, die Sie mit Union-Typen nicht tun können.

Zeiger

Diese beiden Typen sind ungefähr gleichwertig

type MaybeInt64 union {
  None struct{}
  Int64 int64
}

und

type MaybeInt64 *int64

Die Zeigerversion ist kompakter. Die Union-Version würde ein zusätzliches Bit benötigen (das wiederum wahrscheinlich eine Wortgröße hat), um das dynamische Tag zu speichern, sodass die Größe des Werts wahrscheinlich die gleiche wäre wie https://golang.org/pkg/database/sql/ #NullInt64

Die Gewerkschaftsversion dokumentiert die Absicht deutlicher.

Natürlich gibt es Dinge, die Sie mit Zeigern tun können, die Sie mit Unionstypen nicht tun können.

Schnittstellen

Diese beiden Typen sind ungefähr gleichwertig

type AB union {
  A A
  B B
}

und

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

Die Union-Version kann durch Einbettung nicht umgangen werden. A und B brauchen keine gemeinsamen Methoden – sie könnten tatsächlich primitive Typen sein oder völlig getrennte Methodensätze haben, wie das json.Token-Beispiel, das @urandom gepostet hat.

Es ist wirklich leicht zu erkennen, was Sie in eine AB-Union im Vergleich zu einer AB-Schnittstelle einfügen können: Die Definition ist die Dokumentation (ich musste die go/ast-Quelle mehrmals lesen, um herauszufinden, was etwas ist).

Die AB-Union kann niemals null sein und kann Methoden außerhalb der Schnittmenge ihrer Konstituenten erhalten (dies könnte durch Einbetten der Schnittstelle in eine Struktur simuliert werden, aber dann wird die Konstruktion komplizierter und fehleranfälliger).

Natürlich gibt es Dinge, die Sie mit Schnittstellen tun können, die Sie mit Union-Typen nicht tun können.

Zusammenfassung

Vielleicht ist diese Überlappung zu viel Überlappung.

In jedem Fall ist der Hauptvorteil der Union-Versionen tatsächlich eine strengere Überprüfung der Kompilierungszeit. Was du nicht kannst, ist wichtiger als das, was du kannst. Für den Compiler, der in stärkere Invarianten übersetzt wird, kann er den Code optimieren. Für den Programmierer, der in etwas anderes übersetzt, können Sie den Compiler kümmern – er wird Ihnen nur sagen, wenn Sie falsch liegen. Zumindest in der Schnittstellenversion gibt es wichtige Dokumentationsvorteile.

Klobige Versionen der Iota- und Pointer-Beispiele können mit der Strategie "Schnittstelle mit einer nicht exportierten Methode" erstellt werden. Allerdings könnten Strukturen mit map[string]interface{} und (nicht leeren) Schnittstellen mit Funktypen und Methodenwerten simuliert werden. Niemand würde es tun, weil es schwieriger und weniger sicher ist.

All diese Funktionen fügen der Sprache etwas hinzu, aber ihr Fehlen könnte (schmerzhaft und unter Protest) umgangen werden.

Ich gehe also davon aus, dass die Messlatte nicht darin besteht, ein Programm zu demonstrieren, das in Go nicht einmal angenähert werden kann, sondern um ein Programm zu demonstrieren, das mit Unions viel einfacher und sauberer in Go geschrieben werden kann als ohne. Was also noch gezeigt werden muss, ist das.

@jimmyfrasche

Ich sehe keinen Grund, warum der Unionstyp benannte Felder haben sollte. Namen sind nur sinnvoll, wenn Sie verschiedene Felder desselben Typs unterscheiden möchten. Eine Union darf jedoch niemals mehrere Felder desselben Typs haben, da dies ziemlich bedeutungslos ist. Daher ist es einfach überflüssig, Namen zu haben und führt zu Verwirrung und mehr Tipparbeit.

Im Wesentlichen sollte Ihr Unionstyp etwa so aussehen:

union {
    struct{}
    int
    err
}

Die Typen selbst stellen die eindeutigen Bezeichner bereit, die zum Zuweisen einer Union verwendet werden können, ganz ähnlich wie eingebettete Typen in Strukturen als Bezeichner verwendet werden.

Damit explizite Zuweisungen funktionieren, kann man jedoch keinen Union-Typ erstellen, indem man einen unbenannten Typ als Member angibt, da die Syntax einen solchen Ausdruck zulassen würde. ZB v.struct{} = struct{}

Daher müssen Typen wie raw struct, unions und funcs vorher benannt werden, um Teil einer Union zu sein und zuweisbar zu werden. Vor diesem Hintergrund ist eine verschachtelte Union nichts Besonderes, da die innere Union nur ein anderer Elementtyp ist.

Jetzt bin ich mir nicht sicher, welche Syntax besser wäre.

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

Das oben Gesagte scheint eher zu sein, ist aber für einen solchen Typ etwas ausführlich.

Auf der anderen Seite sieht type1 | package1.type2 möglicherweise nicht wie Ihr üblicher Go-Typ aus, hat jedoch den Vorteil der Verwendung des '|' Symbol, das überwiegend als ODER erkannt wird. Und es reduziert die Ausführlichkeit, ohne kryptisch zu sein.

@urandom Wenn Sie keine "Tag-Namen" haben, aber Schnittstellen zulassen, fallen die Summen mit zusätzlichen Überprüfungen in ein interface{} . Sie hören auf, Summentypen zu sein, da Sie eine Sache eingeben können, aber auf mehrere Arten wieder herausbekommen. Die Tag-Namen machen sie zu Summentypen und halten Schnittstellen ohne Mehrdeutigkeit.

Die Tag-Namen reparieren jedoch viel mehr als nur das Schnittstellenproblem{}. Sie machen den Typ viel weniger magisch und lassen alles herrlich explizit werden, ohne einen Haufen Typen erfinden zu müssen, nur um zu unterscheiden. Sie können explizite Zuweisungen haben und Literale eingeben, wie Sie darauf hinweisen.

Dass Sie einem Typ mehr als ein Tag geben können, ist eine Funktion. Betrachten Sie einen Typ, um zu messen, wie viele Erfolge oder Misserfolge in Folge aufgetreten sind (1 Erfolg hebt N Misserfolge auf und umgekehrt)

type Counter union {
  Successes, Failures uint 
}

ohne die Tag-Namen, die Sie brauchen würden

type (
  Success uint
  Failures uint
  Counter Successes | Failures
)

und die Zuweisung würde wie c = Successes(1) aussehen: c.Successes = 1 . Du gewinnst nicht viel.

Ein weiteres Beispiel ist ein Typ, der einen lokalen oder entfernten Fehler darstellt. Mit Tag-Namen ist dies einfach zu modellieren:

type Failure union {
  Local, Remote error
}

Die Vorsehung des Fehlers kann mit seinem Tag-Namen angegeben werden, unabhängig vom tatsächlichen Fehler. Ohne Tag-Namen benötigen Sie type Local { error } und dasselbe für Remote, auch wenn Sie Schnittstellen direkt in der Summe zulassen.

Die Tag-Namen erzeugen eine Art spezielle weder Alias- noch benannte Typen lokal in der Union. Mehrere "Tags" mit identischen Typen zu haben, ist für meinen Vorschlag nicht einzigartig: Es ist das, was jede funktionale Sprache (die ich kenne) tut.

Interessant ist auch die Möglichkeit, nicht exportierte Tags für exportierte Typen und umgekehrt zu erstellen.

Auch separate Tag- und Typ-Assertionen ermöglichen interessanten Code, z.

Es scheint, als ob es mehr Probleme löst als es verursacht und alles viel schöner zusammenpasst. Ich war mir ehrlich gesagt nicht so sicher, als ich es geschrieben habe, aber ich bin immer mehr davon überzeugt, dass dies der einzige Weg ist, alle Probleme mit der Integration von Summen in Go zu lösen.

Um das etwas zu erweitern, war das motivierende Beispiel für mich von @rogpeppe io.Reader | io.ReadCloser . Wenn Schnittstellen ohne Tags zugelassen werden, ist dies der gleiche Typ wie io.Reader .

Sie können einen ReadCloser einsetzen und als Reader herausziehen. Du verlierst das A | B bedeutet A- oder B-Eigenschaft von Summentypen.

Wenn Sie spezifisch sein müssen, um io.ReadCloser manchmal als io.Reader , müssen Sie Wrapper-Strukturen erstellen, wie @bcmills darauf hinwies, type Reader struct { io.Reader } usw. und den Typ Reader | ReadCloser .

Auch wenn Sie Summen auf Schnittstellen mit disjunkten Methodensätzen beschränken, besteht dieses Problem immer noch, da ein Typ mehr als eine dieser Schnittstellen implementieren kann. Sie verlieren die Deutlichkeit der Summentypen: Sie sind nicht "A oder B": Sie sind "A oder B oder manchmal wie auch immer Sie möchten".

Schlimmer noch, wenn diese Typen von anderen Paketen stammen, können sie sich nach einem Update plötzlich anders verhalten, selbst wenn Sie Ihr Programm sehr sorgfältig so konstruiert haben, dass A nie gleich B behandelt wird.

Ursprünglich habe ich das Verbieten von Schnittstellen untersucht, um das Problem zu lösen. Damit war keiner zufrieden! Aber es hat auch Probleme wie a = b nicht beseitigt, was je nach Art von a und b unterschiedliche Dinge bedeutet, mit denen ich mich nicht wohl fühle. Es musste auch viele Regeln darüber geben, welcher Typ in der Auswahl ausgewählt wird, wenn die Typzuordnung ins Spiel kommt. Es ist viel Magie.

Sie fügen Tags hinzu und das alles verschwindet.

Mit union { R io.Reader | RC io.ReadCloser } Sie explizit sagen, dass ich möchte, dass dieser ReadCloser als Reader betrachtet wird, wenn dies sinnvoll ist. Keine Wrapper-Typen erforderlich. Es ist in der Definition implizit. Unabhängig vom Typ des Tags ist es entweder das eine oder das andere.

Der Nachteil ist, dass, wenn Sie einen io.Reader von woanders erhalten, sagen wir einen chan-Receive- oder func-Aufruf, und es könnte ein io.ReadCloser sein und Sie müssen ihn dem richtigen Tag zuweisen, den Sie auf io eingeben müssen. ReadCloser und testen. Aber das macht die Absicht des Programms viel klarer – genau das, was Sie meinen, steht im Code.

Da sich die Tag-Assertionen von den Typ-Assertions unterscheiden, können Sie dies unabhängig vom Tag mit einer Typ-Assertion herausziehen, wenn es Ihnen wirklich egal ist und Sie nur einen io.Reader möchten.

Dies ist eine bestmögliche Transliteration eines Spielzeugbeispiels in Go ohne unions/sums/etc. Es ist wahrscheinlich nicht das beste Beispiel, aber ich habe es verwendet, um zu sehen, wie das aussehen würde.

Es zeigt die Semantik auf eine operativere Art und Weise, die wahrscheinlich leichter zu verstehen ist als einige knappe Stichpunkte in einem Vorschlag.

Die Transliteration enthält ziemlich viel Boilerplate, daher habe ich im Allgemeinen nur die erste Instanz mehrerer Methoden mit einem Hinweis auf die Wiederholung geschrieben.

In Go with Gewerkschaftsvorschlag:

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()
    } 
  }
}

In das aktuelle Go transkribiert:

(Hinweise zu den Unterschieden zwischen der Transliteration und den oben genannten sind enthalten)

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

Da die Union Tags enthält, die möglicherweise den gleichen Typ haben, wäre die folgende Syntax nicht besser geeignet:

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()
    } 
  }
}

So wie ich es sehe, ist eine Union, wenn sie mit einem Schalter verwendet wird, Typen wie int oder string ziemlich ähnlich. Der Hauptunterschied besteht darin, dass ihm im Gegensatz zu den vorherigen Typen nur endliche 'Werte' zugewiesen werden können und der Schalter selbst erschöpfend ist. Daher sehe ich in diesem Fall keine Notwendigkeit für eine spezielle Syntax, die die mentale Arbeit des Entwicklers reduziert.

Wäre ein solcher Code nach diesem Vorschlag auch gültig:

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 Ich habe eine Syntax gewählt, die die Semantik widerspiegelt,

Mit Schnittstellentypen können Sie tun

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

Das ist in Ordnung und eindeutig, da es egal ist, welcher Art von someValue ist, solange der Vertrag erfüllt ist.

Wenn Sie Tags† auf Gewerkschaften einführen, kann dies manchmal mehrdeutig sein. Magische Zuweisung wäre nur in bestimmten Fällen gültig. Spezielle Umhüllung bringt Sie nur dazu, manchmal explizit sein zu müssen.

Ich sehe keinen Sinn darin, manchmal einen Schritt überspringen zu können, insbesondere wenn eine Codeänderung diesen Sonderfall leicht ungültig machen kann und Sie dann sowieso zurückgehen und den gesamten Code aktualisieren müssen. Um Ihr Foo/Bar-Beispiel zu verwenden, wenn C int zu Foo hinzugefügt wird, dann muss Bar(1) geändert werden, aber nicht Bar("hello world") . Es verkompliziert alles, um in Situationen, die vielleicht nicht so häufig vorkommen, ein paar Tastenanschläge zu sparen, und macht die Konzepte schwerer zu verstehen, weil sie manchmal so und manchmal so aussehen – konsultieren Sie einfach dieses praktische Flussdiagramm, um zu sehen, was auf Sie zutrifft!

† Ich wünschte, ich hätte einen besseren Namen dafür. Es gibt bereits struct-Tags. Ich hätte sie Labels genannt, aber Go hat diese auch. Sie als Felder zu bezeichnen scheint sowohl angemessener als auch am verwirrendsten zu sein. Wenn jemand Fahrradschuppen möchte, könnte man wirklich einen frischen Mantel gebrauchen.

In gewisser Weise ähneln getaggte Unions eher einer Struktur als einer Schnittstelle. Sie sind eine spezielle Art von Struktur, die jeweils nur ein Feld gleichzeitig haben kann. So gesehen würde Ihr Foo/Bar-Beispiel so aussehen:

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})
}

Obwohl es in diesem Fall eindeutig ist, halte ich es für keine gute Idee.

Auch im Vorschlag ist Bar(Foo{1}) erlaubt, wenn eindeutig ist, ob man wirklich Tastenanschläge speichern möchte. Sie können auch Zeiger auf Unions haben, sodass die zusammengesetzte Literal-Syntax für &Foo{"hello world"} weiterhin erforderlich ist.

Nichtsdestotrotz haben Unions eine Ähnlichkeit mit Schnittstellen, da sie ein dynamisches Tag haben, für das "field" derzeit festgelegt ist.

Das switch v := u.[type] {... spiegelt gut das switch v := i.(type) {... für Interfaces wider und erlaubt dennoch Typwechsel und Assertionen direkt auf Union-Werten. Vielleicht sollte es u.[union] , um es leichter zu erkennen, aber so oder so ist die Syntax nicht so schwer und es ist klar, was gemeint ist.

Sie könnten das gleiche Argument vorbringen, dass .(type) unnötig ist, aber wenn Sie sehen, dass Sie immer genau wissen, was passiert, und dies meiner Meinung nach vollständig rechtfertigt.

Das war meine Argumentation hinter diesen Entscheidungen.

@jimmyfrasche
Die Switch-Syntax erscheint mir auch nach Ihren Erklärungen etwas kontraintuitiv. Bei einer Schnittstelle wechselt switch v := i.(type) {... durch die möglichen Typen, die durch die Schalterfälle aufgelistet und durch .(type) angezeigt werden.
Bei einer Union schaltet ein Schalter jedoch nicht durch mögliche Typen, sondern durch Werte. Jeder Fall stellt einen anderen möglichen Wert dar, wobei Werte tatsächlich denselben Typ haben können. Dies ähnelt eher Strings und int-Schaltern, bei denen Fälle auch Werte auflisten, und ihre Syntax ist ein einfaches switch v := u {... . Daher erscheint es mir natürlicher, dass das Durchschalten der Werte einer Union switch v := u { ... , da die Fälle ähnlich, aber restriktiver sind als die Fälle für Ints und Strings.

@urandom das ist ein sehr guter Punkt in

switch u {... würde funktionieren, aber das Problem mit switch v := u {... ist, dass es zu sehr nach switch v := f(); v {... aussieht (was die Fehlerberichterstattung erschweren würde – nicht klar, was beabsichtigt war).

Wenn das Schlüsselwort union in pick wie von @as vorgeschlagen, switch u.[pick] {... oder switch v := u.[pick] {... was die Symmetrie beibehält mit einem Typenschalter verliert aber die Verwirrung und sieht ganz nett aus.

Selbst wenn die Implementierung ein int einschaltet, gibt es immer noch eine implizite Destrukturierung des Picks in ein dynamisches Tag und einen gespeicherten Wert, der meiner Meinung nach unabhängig von grammatikalischen Regeln explizit sein sollte

Weißt du, es macht sehr viel Sinn, nur die Tag-Felder aufzurufen und es als Feldzusicherung und Feldwechsel zu verwenden.

Bearbeiten: Das würde die Verwendung von Reflect mit Picks jedoch umständlich machen

[Entschuldigung für die verspätete Antwort - ich war im Urlaub]

@ianlancetaylor schrieb:

Was ich in Ihren Vorschlägen nicht sehe, ist, warum wir dies tun sollten. Das Hinzufügen eines neuen Typs hat einen klaren Nachteil: Es ist ein neues Konzept, das jeder lernen muss, der Go lernt. Was ist der kompensierende Vorteil? Was bringt uns die neue Art von Typ insbesondere, was wir von Schnittstellentypen nicht bekommen?

Es gibt zwei Hauptvorteile, die ich sehe. Der erste ist ein Sprachvorteil; der zweite ist ein Leistungsvorteil.

  • Bei der Verarbeitung von Nachrichten, insbesondere beim Lesen aus einem nebenläufigen Prozess, ist es sehr nützlich, den vollständigen Satz von Nachrichten zu kennen, die empfangen werden können, da jede Nachricht mit entsprechenden Protokollanforderungen verbunden sein kann. Für ein gegebenes Protokoll mag die Anzahl möglicher Nachrichtentypen sehr klein sein, aber wenn wir eine offene Schnittstelle verwenden, um die Nachrichten darzustellen, ist diese Invariante nicht klar. Um dies zu vermeiden, verwenden die Leute oft für jeden Nachrichtentyp einen anderen Kanal, aber das ist mit eigenen Kosten verbunden.

  • es gibt Zeiten, in denen eine kleine Anzahl möglicher Nachrichtentypen bekannt ist, von denen keiner Zeiger enthält. Wenn wir eine offene Schnittstelle verwenden, um sie darzustellen, müssen wir eine Zuweisung vornehmen, um Schnittstellenwerte zu erstellen. Die Verwendung eines Typs, der die möglichen Nachrichtentypen einschränkt, bedeutet, dass dies vermieden werden kann und somit der GC-Druck entlastet und die Cache-Lokalität erhöht wird.

Ein besonderer Schmerz für mich, den Summentypen lösen könnten, ist godoc. Nehmen wir zum Beispiel ast.Spec : https://golang.org/pkg/go/ast/#Spec

Viele Pakete listen die möglichen zugrunde liegenden Typen eines benannten Schnittstellentyps manuell auf, sodass sich ein Benutzer schnell ein Bild machen kann, ohne sich den Code ansehen oder sich auf Namenssuffixe oder -präfixe verlassen zu müssen.

Wenn die Sprache bereits alle möglichen Werte kennt, könnte dies in godoc automatisiert werden, ähnlich wie Aufzählungstypen mit iotas. Sie könnten auch tatsächlich auf die Typen verlinken, anstatt nur Klartext zu sein.

Bearbeiten: ein weiteres Beispiel: https://github.com/mvdan/sh/commit/ebbfda50dfe167bee741460a4491ffec1006bdef

@mvdan das ist ein ausgezeichneter, praktischer Punkt, um die Geschichte in Go1 ohne Sprachänderungen zu verbessern. Können Sie dafür ein separates Problem einreichen und auf dieses verweisen?

Entschuldigung, beziehen Sie sich nur auf Links zu anderen Namen auf der godoc-Seite, listen sie aber immer noch manuell auf?

Sorry, hätte klarer sein sollen.

Ich meinte eine Funktionsanfrage für die automatische Behandlung von Typen, die Schnittstellen implementieren, die im aktuellen Paket in godoc definiert sind.

(Ich glaube, es gibt irgendwo eine Feature-Anfrage zum Verknüpfen manuell aufgelisteter Namen, aber ich habe im Moment nicht die Zeit, sie zu suchen).

Ich möchte diesen (schon sehr langen) Thread nicht übernehmen, deshalb habe ich eine separate Ausgabe erstellt - siehe oben.

@Merovius Ich antworte in dieser Ausgabe auf https://github.com/golang/go/issues/19814#issuecomment -298833986, da das AST-Zeug mehr für Summentypen als für Aufzählungen gilt. Entschuldigung, dass ich Sie in ein anderes Problem gezogen habe.

Zunächst möchte ich wiederholen, dass ich nicht sicher bin, ob Summentypen in Go gehören. Ich muss mich noch davon überzeugen, dass sie definitiv nicht dazugehören. Ich gehe davon aus, dass sie es tun, um die Idee zu untersuchen und zu sehen, ob sie passen. Ich lasse mich aber so oder so gerne überzeugen.

Zweitens haben Sie in Ihrem Kommentar die schrittweise Codereparatur erwähnt. Das Hinzufügen eines neuen Begriffs zu einem Summentyp ist per Definition eine grundlegende Änderung, genauso wie das Hinzufügen einer neuen Methode zu einer Schnittstelle oder das Entfernen eines Felds aus einer Struktur. Aber das ist das richtige und erwünschte Verhalten.

Betrachten wir das Beispiel eines mit einer Node-Schnittstelle implementierten AST, der eine neue Art von Knoten hinzufügt. Angenommen, der AST ist in einem externen Projekt definiert und Sie importieren ihn in ein Paket in Ihrem Projekt, das den AST durchläuft.

Es gibt eine Reihe von Fällen:

  1. Ihr Code erwartet, jeden Knoten zu durchlaufen:
    1.1. Sie haben keine Standardanweisung, Ihr Code ist stillschweigend falsch
    1.2. Sie haben eine Standardanweisung mit einer Panik, Ihr Code schlägt zur Laufzeit statt zur Kompilierzeit fehl (Tests helfen nicht, da sie nur die Knoten kennen, die beim Schreiben der Tests vorhanden waren)
  2. Ihr Code untersucht nur eine Teilmenge der Knotentypen:
    2.1. Diese neue Art von Knoten wäre sowieso nicht in der Untermenge gewesen
    2.1.1. Solange dieser neue Knoten nie einen der Knoten enthält, an denen Sie interessiert sind, geht alles gut
    2.1.2. Andernfalls befinden Sie sich in der gleichen Situation, als ob Ihr Code erwartet hätte, jeden Knoten zu durchlaufen
    2.2. Diese neue Art von Knoten wäre in der Untermenge enthalten, an der Sie interessiert sind, wenn Sie davon gewusst hätten.

Bei schnittstellenbasiertem AST funktioniert nur Fall 2.1.1 korrekt. Das ist vor allem Zufall. Die schrittweise Codereparatur funktioniert nicht. Der AST muss seine Version ändern und Ihr Code muss seine Version ändern.

Ein Vollständigkeits-Linter würde helfen, aber da der Linter nicht alle Schnittstellentypen untersuchen kann, muss ihm in irgendeiner Weise mitgeteilt werden, dass eine bestimmte Schnittstelle überprüft werden muss. Das bedeutet entweder einen Kommentar im Quelltext oder eine Art Konfigurationsdatei in Ihrem Repository. Wenn es sich um einen In-Source-Kommentar handelt, sind Sie, da der AST per Definition in einem separaten Projekt definiert ist, diesem Projekt ausgeliefert, um die Schnittstelle zur Vollständigkeitsprüfung zu markieren. Dies funktioniert nur in großem Maßstab, wenn es einen einzigen Vollständigkeitslinter gibt, auf den sich die gesamte Community einig ist und der immer verwendet wird.

Bei einem summenbasierten AST müssen Sie weiterhin die Versionsverwaltung verwenden. Der einzige Unterschied besteht in diesem Fall darin, dass der Vollständigkeitslinter in den Compiler integriert ist.

Bei 2.2 hilft beides nicht, aber was könnte?

Es gibt einen einfacheren, an AST angrenzenden Fall, in dem Summentypen nützlich wären: Token. Angenommen, Sie schreiben einen Lexer für einen einfacheren Taschenrechner. Es gibt Token wie * , denen keine Werte zugeordnet sind, und Token wie Var , die eine Zeichenfolge haben, die den Namen darstellt, und Token wie Val , die einen Float enthalten64 .

Das könnte man mit Schnittstellen umsetzen, aber das wäre mühsam. Sie würden jedoch wahrscheinlich so etwas tun:

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
}

Ein Vollständigkeits-Linter auf iota-basierten Aufzählungen könnte sicherstellen, dass niemals ein illegaler Typ verwendet wird, aber es würde nicht gut gegen jemanden funktionieren, der Name zuweist, wenn Typ == Times oder Number verwendet, wenn Typ == Var. Mit zunehmender Anzahl und Art der Token wird es nur noch schlimmer. Das Beste, was Sie hier tun können, ist, eine Methode hinzuzufügen, Valid() error , die alle Einschränkungen überprüft und eine Menge Dokumentation, die erklärt, wann Sie was tun können.

Ein Summentyp kodiert auf einfache Weise all diese Einschränkungen und die Definition wäre die gesamte erforderliche Dokumentation. Das Hinzufügen einer neuen Art von Token wäre eine bahnbrechende Änderung, aber alles, was ich über ASTs gesagt habe, gilt hier immer noch.

Ich denke, mehr Werkzeug ist notwendig. Ich bin einfach nicht überzeugt, dass es ausreicht.

@jimmyfrasche

Zweitens haben Sie in Ihrem Kommentar die schrittweise Codereparatur erwähnt. Das Hinzufügen eines neuen Begriffs zu einem Summentyp ist per Definition eine grundlegende Änderung, genauso wie das Hinzufügen einer neuen Methode zu einer Schnittstelle oder das Entfernen eines Felds aus einer Struktur.

Nein, es ist nicht auf Augenhöhe. Sie können beide Änderungen in einem schrittweisen Reparaturmodell vornehmen (für Schnittstellen: 1. Neue Methode zu allen Implementierungen hinzufügen, 2. Methode zur Schnittstelle hinzufügen. Für Strukturfelder: 1. Entfernen Sie alle Verwendungen von Feld, 2. Entfernen Sie das Feld). Das Hinzufügen eines Falls zu einem Summentyp kann in einem schrittweisen Reparaturmodell nicht funktionieren; Wenn Sie es hinzufügen, tun Sie die Lib zuerst, es würde alle Benutzer zerstören, da sie nicht mehr erschöpfend überprüfen, aber Sie können es nicht zuerst den Benutzern hinzufügen, da der neue Fall noch nicht existiert. Gleiches gilt für das Entfernen.

Es geht nicht darum, ob es sich um einen Breaking Change handelt oder nicht, sondern darum, ob es sich um einen Breaking Change handelt, der mit minimaler Unterbrechung orchestriert werden kann.

Aber das ist das richtige und erwünschte Verhalten.

Genau. Summentypen sind aufgrund ihrer Definition und aus jedem Grund, aus dem die Leute sie haben wollen, mit der Idee der schrittweisen Codereparatur grundsätzlich unvereinbar.

Bei schnittstellenbasiertem AST funktioniert nur Fall 2.1.1 korrekt.

Nein, es funktioniert auch im Fall 1.2 korrekt (ein Scheitern zur Laufzeit wegen unerkannter Grammatik ist völlig in Ordnung. Ich möchte aber wahrscheinlich nicht in Panik verfallen, sondern nur einen Fehler zurückgeben) und auch in vielen Fällen von 2.1. Der Rest ist ein grundlegendes Problem bei der Aktualisierung von Software; Wenn Sie einer Bibliothek eine neue Funktion hinzufügen, müssen Benutzer Ihrer Bibliothek den Code ändern, um sie nutzen zu können. Dies bedeutet jedoch nicht, dass Ihre Software falsch ist, bis dies der Fall ist.

Der AST muss seine Version ändern und Ihr Code muss seine Version ändern.

Ich sehe überhaupt nicht, wie dies aus dem, was Sie sagen, folgt. Für mich ist es in Ordnung zu sagen "diese neue Grammatik funktioniert noch nicht mit allen Tools, aber sie steht dem Compiler zur Verfügung". Genauso wie "wenn Sie dieses Tool mit dieser neuen Grammatik ausführen, wird es zur Laufzeit fehlschlagen" ist in Ordnung. Im schlimmsten Fall fügt dies dem schrittweisen Reparaturprozess nur einen weiteren Schritt hinzu: a) Fügen Sie den neuen Knoten dem AST-Paket und dem Parser hinzu. b) Reparieren Sie Tools, die das AST-Paket verwenden, um den neuen Knoten zu nutzen. c) Aktualisieren Sie den Code, um den neuen Knoten zu verwenden. Ja, der neue Knoten wird erst nach a) und b) benutzbar; aber in jedem Schritt dieses Prozesses wird alles ohne Unterbrechungen kompiliert und funktioniert korrekt.

Ich sage nicht, dass es Ihnen in einer Welt der schrittweisen Codereparatur und ohne umfassende Compilerprüfungen automatisch gut geht. Es erfordert immer noch eine sorgfältige Planung und Ausführung, Sie werden wahrscheinlich immer noch nicht gepflegte umgekehrte Abhängigkeiten aufbrechen und es könnte immer noch Änderungen geben, die Sie möglicherweise überhaupt nicht durchführen können (obwohl ich mir keine vorstellen kann). Aber zumindest a) gibt es einen schrittweisen Upgrade-Pfad und b) ob dies dein Tool zur Laufzeit kaputt machen soll oder nicht, liegt beim Autor des Tools. Sie können entscheiden, was in einem unbekannten Fall zu tun ist.

Ein Vollständigkeits-Linter würde helfen, aber da der Linter nicht alle Schnittstellentypen untersuchen kann, muss ihm in irgendeiner Weise mitgeteilt werden, dass eine bestimmte Schnittstelle überprüft werden muss.

Wieso den? Ich würde argumentieren, dass es für switchlint™ in Ordnung ist, sich über jeden Typwechsel ohne Standardfall zu beschweren; Schließlich würden Sie erwarten, dass der Code mit jeder Schnittstellendefinition funktioniert, daher ist es wahrscheinlich sowieso ein Problem, keinen Code für unbekannte Implementierungen zu haben. Ja, es gibt Ausnahmen von dieser Regel, aber Ausnahmen können bereits manuell ignoriert werden.

Ich wäre wahrscheinlich mehr an Bord mit der Durchsetzung von "jeder Typwechsel sollte einen Standardfall erfordern, auch wenn er leer ist" im Compiler als mit tatsächlichen Summentypen. Es würde die Leute sowohl befähigen als auch zwingen, die Entscheidung zu treffen, was ihr Code tun sollte, wenn sie mit einer unbekannten Entscheidung konfrontiert sind.

Das könnte man mit Schnittstellen umsetzen, aber das wäre mühsam.

Achselzucken es ist eine einmalige Anstrengung in einem Fall, der sehr selten vorkommt. Scheint mir gut zu sein.

Und FWIW, ich argumentiere derzeit nur gegen die erschöpfende Überprüfung von Summentypen. Ich habe noch keine starken Meinungen über die zusätzliche Bequemlichkeit, "einen dieser strukturell definierten Typen" zu sagen.

@Merovius Ich muss über Ihre hervorragenden Punkte zur schrittweisen

Vollständigkeitsprüfungen

Ich argumentiere derzeit nur gegen die erschöpfende Überprüfung von Summentypen.

Sie können die Vollständigkeitsprüfung mit einem Standardfall explizit ablehnen (naja, effektiv: Der Standard macht es erschöpfend, indem er einen Fall hinzufügt, der "alles andere, was auch immer das sein mag" abdeckt). Sie haben immer noch die Wahl, aber Sie müssen sie explizit treffen.

Ich würde argumentieren, dass es für switchlint™ in Ordnung ist, sich über jeden Typwechsel ohne Standardfall zu beschweren; Schließlich würden Sie erwarten, dass der Code mit jeder Schnittstellendefinition funktioniert, daher ist es wahrscheinlich sowieso ein Problem, keinen Code für unbekannte Implementierungen zu haben. Ja, es gibt Ausnahmen von dieser Regel, aber Ausnahmen können bereits manuell ignoriert werden.

Das ist eine interessante Idee. Es würde zwar Summentypen treffen, die mit Interface simuliert wurden, und Aufzählungen, die mit const/iota simuliert wurden, aber es sagt Ihnen nicht, dass Sie einen bekannten Fall übersehen haben, sondern nur, dass Sie den unbekannten Fall nicht behandelt haben. Unabhängig davon scheint es laut zu sein. Erwägen:

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

Das ist erschöpfend, wenn n ganzzahlig ist (bei Floats fehlt n != n ), aber ohne viele Informationen über Typen zu codieren, ist es wahrscheinlich einfacher, dies einfach als fehlende Standardeinstellung zu kennzeichnen. Für sowas wie:

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

selbst wenn die p[i] eine Äquivalenzrelation für die Typen von a und b es das nicht beweisen können, also muss es den Schalter als fehlende Vorgabe kennzeichnen case, was eine Möglichkeit bedeutet, es mit einem Manifest, einer Anmerkung in der Quelle, einem Wrapper-Skript, um egrep -v aus der Whitelist zu entfernen, oder eine unnötige Standardeinstellung für den Schalter, die fälschlicherweise impliziert, dass die p[i] sind nicht erschöpfend.

Jedenfalls wäre dies trivial zu implementieren, wenn der Weg "immer über keinen Ausfall unter allen Umständen beschweren" gewählt wird. Es wäre interessant, dies zu tun und es auf go-corpus auszuführen und zu sehen, wie laut und / oder nützlich es in der Praxis ist.

Token

Alternative Token-Implementierungen:

//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
}

Dadurch wird die Möglichkeit beseitigt, einen illegalen Token-Zustand zu definieren, bei dem etwas sowohl einen String- als auch einen Zahlenwert hat, aber die Erstellung eines StringToken mit einem Typ, der SimpleToken oder umgekehrt sein sollte, nicht verbietet umgekehrt.

Um dies mit Schnittstellen zu tun, müssen Sie einen Typ pro Token definieren ( type Plus struct{} , type Mul struct{} , etc.) und die meisten Definitionen sind für den Typnamen genau gleich. Einmaliger Aufwand oder nicht, das ist viel Arbeit (obwohl in diesem Fall gut für die Codegenerierung geeignet).

Ich nehme an, Sie könnten eine "Hierarchie" von Token-Schnittstellen haben, um die Arten von Token basierend auf den zulässigen Werten zu partitionieren: (Angenommen, in diesem Beispiel gibt es mehr als eine Art von Token, die eine Zahl oder eine Zeichenfolge usw. enthalten können.)

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
}

Unabhängig davon bedeutet dies, dass jedes Token eine Zeigerdeferenz erfordert, um auf seinen Wert zuzugreifen, im Gegensatz zum Typ struct oder sum, der nur Zeiger benötigt, wenn Zeichenfolgen beteiligt sind. Mit entsprechenden Linters und Verbesserungen an godoc liegt der große Gewinn für Summentypen in diesem Fall also darin, die Zuweisungen zu minimieren, während illegale Zustände und die Anzahl der Eingaben (im Sinne der Tastatur) nicht zugelassen werden, was nicht unwichtig erscheint.

Sie können die Vollständigkeitsprüfung mit einem Standardfall explizit ablehnen (naja, effektiv: Der Standard macht es erschöpfend, indem er einen Fall hinzufügt, der "alles andere, was auch immer das sein mag" abdeckt). Sie haben immer noch die Wahl, aber Sie müssen sie explizit treffen.

Es scheint also so, als ob wir beide die Wahl haben, die umfassende Überprüfung zu aktivieren oder zu deaktivieren :)

es sagt Ihnen nicht, dass Sie einen bekannten Fall verpasst haben, nur dass Sie den unbekannten Fall nicht bearbeitet haben.

Effektiv, glaube ich, führt der Compiler bereits eine Analyse des gesamten Programms durch, um festzustellen, welche konkreten Typen in welchen Schnittstellen meiner Meinung nach verwendet werden ? Ich würde zumindest erwarten, dass es zumindest für Nicht-Schnittstellen-Typ-Assertions (dh Typ-Assertions, die nicht auf einen Schnittstellentyp, sondern auf einen konkreten Typ setzen) die Funktionstabellen generiert, die in Schnittstellen zur Kompilierzeit verwendet werden.
Aber ehrlich gesagt wird dies von den ersten Prinzipien aus argumentiert, ich habe keine Ahnung von der tatsächlichen Umsetzung.

Auf jeden Fall sollte es ziemlich einfach sein, a) jeden konkreten Typ aufzulisten, der in einem ganzen Programm definiert ist, und b) für jeden Typwechsel, sie danach zu filtern, ob sie diese Schnittstelle implementieren. Wenn Sie so etwas wie verwenden diese , würden Sie mit einer zuverlässigen Liste landen. Ich denke.

Ich bin nicht 100% davon überzeugt, dass ein Tool geschrieben werden kann, das so zuverlässig ist wie die explizite Angabe der Optionen, aber ich bin überzeugt, dass Sie 90% der Fälle abdecken könnten und Sie könnten definitiv ein Tool schreiben, das dies außerhalb von dem Compiler, wenn die korrekten Anmerkungen gegeben sind (dh Summentypen zu einem pragmaähnlichen Kommentar machen, nicht zu einem tatsächlichen Typ). Zugegeben, keine gute Lösung.

Unabhängig davon scheint es laut zu sein. Erwägen:

Ich denke, das ist unfair. Die von Ihnen erwähnten Fälle haben mit Summentypen überhaupt nichts zu tun. Wenn ich ein solches Tool schreiben soll, würde ich es auf Typwechsel und Schalter mit einem Ausdruck beschränken, da dies auch die Art zu sein scheint, wie Summentypen behandelt würden.

Alternative Token-Implementierungen:

Warum keine Marker-Methode? Sie brauchen kein Typ-Feld, das bekommen Sie kostenlos von der Interface-Darstellung. Wenn Sie Bedenken haben, die Marker-Methode immer wieder zu wiederholen; Definieren Sie eine nicht exportierte Struktur{}, geben Sie ihr diese Markierungsmethode und betten Sie sie in jede Implementierung ein, ohne zusätzliche Kosten und weniger Eingaben pro Option als Ihre Methode.

Unabhängig davon bedeutet dies, dass jedes Token eine Zeigerdeferenz erfordert, um auf seinen Wert zuzugreifen

Jawohl. Es ist ein echter Preis, aber ich denke nicht, dass es im Grunde jedes andere Argument aufwiegt.

Ich denke, das ist unfair.

Das stimmt.

Ich habe eine Quick-and-Dirty-Version geschrieben und auf der stdlib ausgeführt. Die Überprüfung einer switch-Anweisung mit 1956 Treffern, die Beschränkung auf das Überspringen des switch { Formulars reduzierte diese Anzahl auf 1677. Ich habe keinen dieser Orte untersucht, um zu sehen, ob das Ergebnis aussagekräftig ist.

https://github.com/jimmyfrasche/switchlint

Es gibt sicherlich viel Raum für Verbesserungen. Es ist nicht besonders raffiniert. Pull-Requests willkommen.

(Auf den Rest antworte ich später)

Edit: falsches Markup-Format

Ich denke, dies ist eine (ziemlich voreingenommene) Zusammenfassung von allem bisher (und gehe narzisstisch von meinem zweiten Vorschlag aus)

Vorteile

  • prägnant, einfach, eine Reihe von Einschränkungen prägnant auf selbstdokumentierende Weise zu schreiben
  • bessere Kontrolle der Zuteilungen
  • einfacher zu optimieren (alle dem Compiler bekannten Möglichkeiten)
  • umfassende Prüfung (wenn gewünscht, kann sich abmelden)

Nachteile

  • Jede Änderung an den Membern eines Summentyps ist eine Breaking Change, die eine schrittweise Codereparatur nicht erlaubt, es sei denn, alle externen Pakete verzichten auf Vollständigkeitsprüfungen
  • noch eine Sache in der Sprache zu lernen, einige konzeptionelle Überschneidungen mit bestehenden Funktionen
  • Garbage Collector muss wissen, welche Member Zeiger sind
  • umständlich für Summen der Form 1 + 1 + ⋯ + 1

Alternativen

  • iota "enum" für Summen der Form 1 + 1 + ⋯ + 1
  • Schnittstellen mit einer nicht exportierten Tag-Methode für kompliziertere Summen (eventuell generiert)
  • oder struct mit einer iota-Aufzählung und außersprachlichen Regeln darüber, welche Felder abhängig vom Aufzählungswert festgelegt werden

Ungeachtet

  • besseres Werkzeug, immer besseres Werkzeug

Für eine schrittweise Reparatur, und das ist eine große, besteht meiner Meinung nach die einzige Möglichkeit darin, dass externe Pakete die Vollständigkeitsprüfungen ablehnen. Dies bedeutet, dass es legal sein muss, einen "unnötigen" Standardfall zu haben, der sich nur auf die Zukunftssicherung bezieht, obwohl Sie ansonsten alles andere abgleichen. Ich glaube, dass das jetzt implizit wahr ist, und wenn es nicht leicht genug zu spezifizieren ist.

Es könnte eine Ankündigung von einem Paketbetreuer geben, dass "Hey, wir werden in der nächsten Version ein neues Mitglied zu diesem Summentyp hinzufügen, stellen Sie sicher, dass Sie damit umgehen können" und dann könnte ein Switchlint-Tool alle Fälle finden, die dies erfordern abgemeldet werden.

Nicht so einfach wie andere Fälle, aber immer noch gut machbar.

Wenn Sie ein Programm schreiben, das einen extern definierten Summentyp verwendet, können Sie den Standardwert auskommentieren, um sicherzustellen, dass Sie keine bekannten Fälle übersehen, und ihn dann vor dem Festschreiben auskommentieren. Oder es könnte ein Tool geben, das Ihnen mitteilt, dass der Standardwert "unnötig" ist, das Ihnen sagt, dass Sie alles wissen und zukunftssicher gegen das Unbekannte sind.

Nehmen wir an, wir möchten die Vollständigkeitsprüfung mit einem Linter aktivieren, wenn Schnittstellentypen verwendet werden, die Summentypen simulieren, unabhängig davon, in welchem ​​Paket sie definiert sind.

@Merovius dein betterSumType() BetterSumType Trick ist sehr cool, aber es bedeutet, dass im definierenden Paket gewechselt werden muss (oder du entlarvst so etwas wie

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

und auch fusseln, die jedes Mal aufgerufen werden).

Welche Kriterien sind erforderlich, um zu überprüfen, ob alle Schalter in einem Programm vollständig sind?

Es kann nicht das leere Interface sein, denn dann ist alles Spiel. Es braucht also mindestens eine Methode.

Wenn die Schnittstelle keine nicht exportierten Methoden hat, könnte sie von jedem Typ implementiert werden, sodass die Vollständigkeit von allen Paketen im Callgraph jedes Switches abhängt. Es ist möglich, ein Paket zu importieren, seine Schnittstelle zu implementieren und diesen Wert dann an eine der Funktionen des Pakets zu senden; Ein Wechsel in dieser Funktion wäre also nicht erschöpfend, ohne einen Importzyklus zu erstellen. Es braucht also mindestens eine nicht exportierte Methode. (Dies subsumiert das vorherige Kriterium).

Das Einbetten würde die gesuchte Eigenschaft durcheinander bringen, daher müssen wir sicherstellen, dass keiner der Importer des Pakets jemals die Schnittstelle oder einen der Typen, die sie implementieren, einbettet. Ein wirklich schicker Linter kann vielleicht erkennen, dass Einbetten manchmal in Ordnung ist, wenn wir nie eine bestimmte Funktion aufrufen, die einen eingebetteten Wert erzeugt, oder keines der eingebetteten Interfaces jemals der API-Grenze des Pakets "entkommt".

Um gründlich zu sein, müssen wir entweder überprüfen, ob der Nullwert der Schnittstelle niemals weitergegeben wird, oder eine umfassende Switch-Prüfung case nil erzwingen. (Letzteres ist einfacher, aber ersteres wird bevorzugt, da das Einschließen von Null eine "Typ A oder Typ B oder Typ C" Summe in eine "Null oder Typ A oder Typ B oder Typ C" Summe verwandelt).

Nehmen wir an, wir haben einen Linter mit all diesen Fähigkeiten, sogar den optionalen, die diese Semantik für jeden Importbaum und jede gegebene Schnittstelle innerhalb dieses Baums überprüfen können.

Nehmen wir nun an, wir haben ein Projekt mit einer Abhängigkeit D. Wir möchten sicherstellen, dass eine in einem der Pakete von D definierte Schnittstelle in unserem Projekt vollständig ist. Sagen wir, es geht.

Jetzt müssen wir unserem Projekt D′ eine neue Abhängigkeit hinzufügen. Wenn D′ das Paket in D importiert, das den fraglichen Schnittstellentyp definiert hat, aber diesen Linter nicht verwendet, kann es leicht die Invarianten zerstören, die für die Verwendung umfassender Schalter erforderlich sind.

Nehmen wir an, D hat den Linter zufällig übergeben, nicht weil der Betreuer ihn ausführt. Ein Upgrade auf D könnte die Invarianten genauso leicht zerstören wie D′.

Auch wenn der Linter sagen kann "im Moment ist das 100% erschöpfend 👍", das kann sich ohne unser Zutun ändern.

Eine Vollständigkeitsprüfung für "Iota-Enums" scheint einfacher zu sein.

Für alle type t u wobei u ganzzahlig ist und t als const mit entweder individuell angegebenen Werten oder iota so dass die Null Wert für u ist in diesen Konstanten enthalten.

Anmerkungen:

  • Doppelte Werte können als Aliase behandelt und in dieser Analyse ignoriert werden. Wir gehen davon aus, dass alle benannten Konstanten unterschiedliche Werte haben.
  • 1 << iota kann als Powerset behandelt werden, glaube ich zumindest die meiste Zeit, würde aber wahrscheinlich zusätzliche Bedingungen erfordern, insbesondere um das bitweise Komplement. Sie werden vorerst nicht berücksichtigt

Für eine Kurzform nennen wir min(t) die Konstante, so dass für jede andere Konstante C , min(t) <= C , und ähnlich nennen wir max(t) die Konstante wie das für jede andere Konstante C , C <= max(t) .

Um sicherzustellen, dass t vollständig verwendet wird, müssen wir sicherstellen, dass

  • Werte von t sind immer die benannten Konstanten (oder 0 in bestimmten idiomatischen Positionen, wie Funktionsaufrufen)
  • Es gibt keine Ungleichheitsvergleiche mit einem Wert von t , v , außerhalb von min(t) <= v <= max(t)
  • Werte von t werden niemals in arithmetischen Operationen + , / usw. verwendet. Eine mögliche Ausnahme könnte sein, wenn das Ergebnis zwischen min(t) und max(t) geklemmt wird t .
  • Schalter enthalten alle Konstanten von t oder einen Standardfall.

Dies erfordert immer noch die Überprüfung aller Pakete im Importbaum und kann ebenso leicht ungültig gemacht werden, obwohl es im idiomatischen Code weniger wahrscheinlich ist, dass es ungültig wird.

Ich verstehe, dass dies, ähnlich wie bei Typaliasen, keine Änderungen verursachen wird, also warum es für Go 2 hochhalten?

Typaliase führen kein neues Schlüsselwort ein, was eine definitive Änderung darstellt. Es scheint auch ein Moratorium für selbst geringfügige Sprachänderungen zu geben, und dies wäre eine große Änderung. Selbst das Nachrüsten aller Marshal-/Unmarshal-Routinen, um reflektierte Summenwerte zu verarbeiten, wäre eine große Tortur.

Typalias behebt ein Problem, für das es keine Problemumgehung gab. Summentypen bieten einen Vorteil in Bezug auf die Typsicherheit, aber es ist kein Showstopper, sie nicht zu haben.

Nur ein (kleiner) Punkt zugunsten von so etwas wie dem ursprünglichen Vorschlag von @rogpeppe . Im Paket http gibt es den Schnittstellentyp Handler und einen Funktionstyp, der ihn implementiert, HandlerFunc . Um eine Funktion an http.Handle , müssen Sie sie jetzt explizit in HandlerFunc umwandeln. Wenn http.Handle stattdessen ein Argument vom Typ HandlerFunc | Handler akzeptiert, könnte es jede Funktion/Schließung akzeptieren, die direkt HandlerFunc zugewiesen werden kann. Die Union dient effektiv als Typhinweis, der dem Compiler mitteilt, wie Werte mit unbenannten Typen in den Schnittstellentyp konvertiert werden können. Da HandlerFunc Handler HandlerFunc implementiert, würde sich der Unionstyp ansonsten genau wie Handler verhalten.

@griesemer als Antwort auf Ihren Kommentar im Enum-Thread, https://github.com/golang/go/issues/19814#issuecomment -322752526, ich denke, mein Vorschlag früher in diesem Thread https://github.com/golang/ go/issues/19412#issuecomment -289588569 befasst sich mit der Frage, wie Summentypen ("swift style enums") in Go funktionieren müssten. So viel wie ich sie möchte, ich weiß nicht , ob sie eine notwendige Ergänzung zu gehen würden, aber ich denke , wenn sie hinzugefügt wurden sie zu sehen haben würden / viel wie das funktioniert.

Dieser Beitrag ist nicht vollständig und es gibt in diesem Thread vor und nach Klarstellungen, aber es macht mir nichts aus, diese Punkte zu wiederholen oder zusammenzufassen, da dieser Thread ziemlich lang ist.

Wenn Sie einen Summentyp haben, der von einer Schnittstelle mit einem Typ-Tag simuliert wird und absolut nicht durch Einbetten umgangen werden kann, ist dies die beste Verteidigung, die ich mir ausgedacht habe: https://play.golang.org/p/FqdKfFojp-

@jimmyfrasche Ich schrieb dieses eine Weile zurück.

Ein anderer möglicher Ansatz ist dieser: https://play.golang.org/p/p2tFm984S8

@rogpeppe Wenn Sie Reflexion verwenden

Ich habe eine überarbeitete Version meines zweiten Vorschlags verfasst, basierend auf Kommentaren hier und in anderen Ausgaben.

Insbesondere habe ich die Vollständigkeitsprüfung entfernt. Es ist jedoch trivial, eine externe Vollständigkeitsprüfung für den folgenden Vorschlag zu schreiben, obwohl ich glaube, dass keine für andere Go-Typen geschrieben werden kann, die verwendet werden, um einen Summentyp zu simulieren.

Bearbeiten: Ich habe die Möglichkeit entfernt, Assert für den dynamischen Wert eines Auswahlwerts einzugeben. Es ist zu magisch und der Grund, es zuzulassen, wird genauso gut durch die Codegenerierung bedient.

Edit2: Es wurde klargestellt, wie Feldnamen mit Assertions und Schaltern funktionieren, wenn die Auswahl in einem anderen Paket definiert ist.

Edit3: eingeschränkte Einbettung und geklärte implizite Feldnamen

Edit4: Standard in Schalter klären

Typen auswählen

Ein Pick ist ein zusammengesetzter Typ, der einer Struktur syntaktisch ähnlich ist:

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

Oben sind A , B , C , D und E die Feldnamen der Auswahl und S , T und U sind die jeweiligen Typen dieser Felder. Feldnamen können exportiert oder nicht exportiert werden.

Ein Pick darf ohne Indirektion nicht rekursiv sein.

Rechtliches

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

Illegal

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

Es gibt keine Einbettung für Picks, aber ein Pick kann in eine Struktur eingebettet werden. Wenn eine Auswahl in eine Struktur eingebettet ist, wird die Methode der Auswahl in die Struktur hochgestuft, die Felder einer Auswahl jedoch nicht.

Ein Typ ohne Feldnamen ist eine Abkürzung für die Definition eines Felds mit demselben Namen wie der Typ. (Dies ist ein Fehler, wenn der Typ unbenannt ist, mit einer Ausnahme für *T wo der Name T ).

Zum Beispiel,

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

hat drei Felder Reader , Writer und string mit den jeweiligen Typen. Beachten Sie, dass das Feld string exportiert wird, obwohl es sich im Universumsbereich befindet.

Ein Wert eines Picktyps besteht aus einem dynamischen Feld und dem Wert dieses Felds.

Der Nullwert eines Entnahmetyps ist das erste Feld in der Quellreihenfolge und der Nullwert dieses Felds.

Bei zwei Werten desselben Picktyps, a und b , kann der Pickwert als beliebiger anderer Wert zugewiesen werden

a = b

Die Zuweisung eines Nicht-Auswahlwerts, auch eines Feldtyps in einer Auswahl, ist unzulässig.

Ein Kommissioniertyp hat immer nur ein dynamisches Feld zu einem bestimmten Zeitpunkt.

Die zusammengesetzte Literal-Syntax ähnelt Structs, es gibt jedoch zusätzliche Einschränkungen. Schlüssellose Literale sind nämlich immer ungültig und es darf nur ein Schlüssel angegeben werden.

Folgendes ist gültig

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, "")

Die folgenden Kompilierzeitfehler sind:

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

Bei einem Wert p vom Typ pick {A int; B string} die folgende Zuweisung

p.B = "hi"

setzt das dynamische Feld von p auf B und den Wert von B auf "hi".

Die Zuweisung an das aktuelle dynamische Feld aktualisiert den Wert dieses Felds. Eine Zuweisung, die ein neues dynamisches Feld setzt, muss alle nicht spezifizierten Speicherorte auf Null setzen. Die Zuweisung zu einem Kommissionier- oder Strukturfeld eines Kommissionierfelds aktualisiert oder legt das dynamische Feld nach Bedarf fest.

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}}

Auf den in einer Auswahl enthaltenen Wert kann nur durch eine Feldbestätigung oder einen Feldwechsel zugegriffen werden.

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
}

Die Feldnamen in Feldzusicherungen und Feldschaltern sind eine Eigenschaft des Typs, nicht des Pakets, in dem es definiert wurde. Sie werden nicht durch den Paketnamen qualifiziert, der pick definiert, und können auch nicht durch den Paketnamen qualifiziert werden.

Dies gilt:

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

Dies ist ungültig:

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

Feldzusicherungen und Feldwechsel geben immer eine Kopie des Werts des dynamischen Felds zurück.

Nicht exportierte Feldnamen können nur in ihrem definierenden Paket bestätigt werden.

Typzusicherungen und Typwechsel funktionieren auch bei Picks.

//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

Typzusicherungen und Typwechsel geben immer eine Kopie des Werts des dynamischen Felds zurück.

Wenn die Auswahl in einer Schnittstelle gespeichert ist, stimmen Typzusicherungen für Schnittstellen nur mit dem Methodensatz der Auswahl selbst überein. [immer noch wahr, aber überflüssig, da das Obige entfernt wurde]

Wenn alle Arten einer Auswahl die Gleichheitsoperatoren unterstützen, dann:

  • Werte dieser Auswahl können als Kartenschlüssel verwendet werden
  • zwei Werte desselben Picks sind == wenn sie dasselbe dynamische Feld haben und seine Werte ==
  • zwei Werte mit unterschiedlichen dynamischen Feldern sind != auch wenn die Werte == .

Für Werte eines Auswahltyps werden keine anderen Operatoren unterstützt.

Ein Wert eines Picktyps P kann in einen anderen Picktyp Q wenn die Menge der Feldnamen und deren Typen in P eine Teilmenge der Feldnamen und ihrer gibt Q .

Wenn P und Q in verschiedenen Paketen definiert sind und nicht exportierte Felder haben, werden diese Felder unabhängig von Name und Typ als unterschiedlich betrachtet.

Beispiel:

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

Die Zuweisbarkeit zwischen zwei Pick-Typen wird als Konvertibilität definiert, sofern nicht mehr als einer der Typen definiert ist.

Methoden können für einen definierten Pick-Typ deklariert werden.

Ich habe einen Erfahrungsbericht erstellt (und dem Wiki hinzugefügt) https://gist.github.com/jimmyfrasche/ba2b709cdc390585ba8c43c989797325

Edit: und :heart: an @mewmew, der einen viel besseren und detaillierteren Bericht als Antwort auf diesen Kern hinterlassen hat

Was wäre, wenn wir für einen bestimmten Typ T die Liste der Typen angeben könnten, die in den Typ T konvertiert oder einer Variablen des Typs T zugewiesen werden könnten? Zum Beispiel

type T interface{} restrict { string, error }

definiert einen leeren Schnittstellentyp namens T , sodass ihm nur die Typen string oder error zugewiesen werden können. Jeder Versuch, einen Wert eines anderen Typs zuzuweisen, führt zu einem Kompilierzeitfehler. Jetzt kann ich sagen

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.
}

Welche Schlüsselelemente von Summentypen (oder Auswahltypen) würden durch diese Art von Ansatz nicht erfüllt?

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

Dies ist nicht ganz richtig, da v auch nil . Es würde eine ziemlich große Änderung an der Sprache erfordern, um diese Möglichkeit zu beseitigen, da dies die Einführung von Typen bedeuten würde, die keine Nullwerte haben, und alles, was dazu gehört. Der Nullwert vereinfacht Teile der Sprache, erschwert aber auch das Entwerfen solcher Funktionen.

Interessanterweise ist dieser Ansatz dem ursprünglichen Vorschlag von @rogpeppe ziemlich ähnlich. Was es nicht hat, ist Zwang zu den aufgeführten Typen, was in Situationen nützlich sein könnte, wie ich zuvor erwähnt habe ( http.Handler ). Eine andere Sache ist, dass jede Variante ein unterschiedlicher Typ sein muss, da Varianten eher nach Typ als nach einem unterschiedlichen Tag unterschieden werden. Ich denke, dies ist streng ausdrucksstark, aber einige Leute bevorzugen es, Varianten-Tags und -Typen zu unterscheiden.

@ianlancetaylor

die Profis

  • es ist möglich, sich auf eine geschlossene Menge von Typen zu beschränken – und das ist definitiv die Hauptsache
  • möglich, eine genaue Vollständigkeitsprüfung zu schreiben
  • Sie erhalten die Eigenschaft "Sie können dieser einen Wert zuweisen, der den Vertrag erfüllt". (Mir ist das egal, aber ich kann mir vorstellen, dass andere es tun).

Die Nachteile

  • Sie sind nur Schnittstellen mit Vorteilen und nicht wirklich eine andere Art (aber nette Vorteile!)
  • Sie haben immer noch null, also ist es nicht wirklich ein Summentyp im typtheoretischen Sinne. Was auch immer Sie für A + B + C angeben, ist wirklich ein 1 + A + B + C , @stevenblenkinsop betonte, während ich daran arbeitete.
  • Noch wichtiger ist, dass Sie aufgrund dieses impliziten Zeigers immer eine Umleitung haben. Mit dem Auswahlvorschlag können Sie zwischen p oder *p wählen, wodurch Sie mehr Kontrolle über Speicherabwägungen haben. Sie könnten sie nicht als diskriminierte Vereinigungen (im Sinne von C) als Optimierung implementieren.
  • keine Wahl des Nullwerts, was eine wirklich schöne Eigenschaft ist, zumal es in Go sehr wichtig ist, einen möglichst nützlichen Nullwert zu haben
  • vermutlich könnten Sie keine Methoden auf T (aber vermutlich hätten Sie die Methoden der Schnittstelle, die die Einschränkung ändert, aber die Typen in der Einschränkung müssten sie erfüllen? Sonst sehe ich den Sinn nicht nicht nur type T restrict {string, error} )
  • Wenn Sie die Beschriftungen für die Felder/Summanden/was-haben-Sie verlieren, wird es verwirrend, wenn es mit Schnittstellentypen interagiert. Sie verlieren die starke Eigenschaft "genau das oder genau das" von Summentypen. Sie könnten ein io.Reader einlegen und ein io.Writer herausziehen. Das ist für (unbeschränkte) Schnittstellen sinnvoll, aber nicht für Summentypen.
  • Wenn Sie möchten, dass zwei identische Typen unterschiedliche Dinge bedeuten, müssen Sie Wrapper-Typen verwenden, um eindeutig zu sein. ein solches Tag müsste sich in einem äußeren Namensraum befinden und nicht auf einen Typ beschränkt sein, wie es ein struct-Feld ist
  • dies könnte zu viel in Ihre spezifische Formulierung hineinlesen, aber es hört sich so an, als ob es die Regeln der Zuweisung basierend auf dem Typ des Bevollmächtigten ändert (ich lese es so, dass Sie error nichts Zuweisbares zuweisen können zu T muss genau ein Fehler sein).

Das heißt, es kreuzt die wichtigsten Kästchen an (die ersten beiden Profis, die ich aufgelistet habe) und ich würde es sofort aufnehmen, wenn das alles ist, was ich bekommen könnte. Ich hoffe aber auf Besseres.

Ich nahm an, dass Regeln für die Typzusicherung angewendet werden. Der Typ muss also entweder mit einem konkreten Typ identisch oder einem Schnittstellentyp zuordenbar sein. Im Grunde funktioniert es genau wie eine Schnittstelle, aber jeder Wert (außer nil ) muss für mindestens einen der aufgeführten Typen geltend gemacht werden können.

@jimmyfrasche
Wäre in Ihrem aktualisierten Vorschlag folgende Zuordnung möglich, wenn alle Elemente des Typs unterschiedliche Typen haben:

type p pick {
    A int
    B string
}

func Foo(P p) {
}

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

Foo(42)
Foo("foo")

Die Verwendbarkeit von Summentypen, wenn solche Zuordnungen möglich sind, ist viel größer.

Mit dem Auswahlvorschlag können Sie zwischen p oder *p wählen, wodurch Sie mehr Kontrolle über Speicherabwägungen haben.

Der Grund, warum Schnittstellen skalare Werte speichern, liegt darin, dass Sie kein Typwort lesen müssen, um zu entscheiden, ob das andere Wort ein Zeiger ist; siehe #8405 zur Diskussion. Die gleichen Implementierungsüberlegungen würden wahrscheinlich für einen Auswahltyp gelten, was in der Praxis bedeuten könnte, dass p sowieso zuordnen und nicht lokal sind.

@urandom nein, angesichts deiner Definitionen müsste es geschrieben werden

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"})

Stellen Sie sich diese am besten als Struktur vor, in der jeweils nur ein Feld festgelegt werden kann.

Wenn Sie das nicht haben und dann C uint zu p hinzufügen, was passiert mit p = 42 ?

Sie können viele Regeln basierend auf Reihenfolge und Zuweisbarkeit erstellen, aber sie bedeuten immer, dass Änderungen an der Typdefinition subtile und dramatische Auswirkungen auf den gesamten Code haben können, der den Typ verwendet.

Im besten Fall bricht eine Änderung den gesamten Code aufgrund der fehlenden Mehrdeutigkeit und sagt, dass Sie ihn in p = int(42) oder p = uint(42) ändern müssen, bevor er erneut kompiliert wird. Eine Änderung um eine Zeile sollte nicht die Korrektur von hundert Zeilen erfordern. Vor allem, wenn diese Zeilen in Paketen von Personen enthalten sind, die von Ihrem Code abhängig sind.

Du musst entweder 100% explizit sein oder einen sehr zerbrechlichen Typ haben, den niemand anfassen kann, weil er alles zerstören könnte.

Dies gilt für jeden Summentypvorschlag, aber wenn explizite Bezeichnungen vorhanden sind, haben Sie immer noch die Zuweisbarkeit, da die Bezeichnung explizit angibt, welchem ​​Typ zugewiesen wird.

@josharian also, wenn ich das richtig lese, ist der Grund, warum iface jetzt immer (*type, *value) anstatt wortgroße Werte im zweiten Feld zu verstauen, wie es Go zuvor getan hat, damit der gleichzeitige GC nicht beide überprüfen muss Felder, um zu sehen, ob die zweite ein Zeiger ist – sie kann einfach davon ausgehen, dass dies immer der Fall ist. Habe ich das richtig verstanden?

Mit anderen Worten, wenn der Picktyp implementiert wäre (mit C-Notation) wie

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

der GC müsste eine Sperre (oder etwas Ausgefallenes, aber Äquivalentes) nehmen, um which zu inspizieren, um festzustellen, ob summands gescannt werden muss?

Der Grund, warum iface jetzt immer (*type, *value) ist, anstatt wortgroße Werte im zweiten Feld zu verstauen, wie es zuvor Go getan hat, ist, dass der gleichzeitige GC nicht beide Felder untersuchen muss, um zu sehen, ob das zweite ein Zeiger ist – es kann einfach annehmen, dass es immer so ist.

Korrekt.

Natürlich würde die begrenzte Natur der Auswahltypen einige alternative Implementierungen ermöglichen. Der Auswahltyp könnte so ausgelegt werden, dass immer ein konsistentes Muster von Zeiger/Nicht-Zeiger vorliegt; zB können sich alle Skalartypen überlappen, und ein String-Feld könnte sich mit dem Anfang eines Slice-Feldes überlappen (weil beide "Zeiger, Nicht-Zeiger" beginnen). So

pick {
  a uintptr
  b string
  c []byte
}

könnte grob ausgelegt werden:

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

andere Pick-Typen ermöglichen jedoch möglicherweise kein solch optimales Packen. (Entschuldigung für das kaputte ASCII, ich kann es nicht schaffen, dass GitHub es richtig rendert. Ich hoffe, Sie verstehen den Punkt.)

Diese Fähigkeit, ein statisches Layout zu erstellen, könnte sogar ein Leistungsargument für die Einbeziehung von Auswahltypen sein; Mein Ziel ist es hier lediglich, relevante Implementierungsdetails für Sie zu kennzeichnen.

@josharian und danke dafür. Daran hatte ich nicht gedacht (um ehrlich zu sein, ich habe nur gegoogelt, ob es Forschungen dazu gibt, wie man diskriminierende Gewerkschaften durch GC diskriminiert, sah, dass man das tun kann, und nannte es einen Tag - aus irgendeinem Grund assoziierte mein Gehirn nicht mit "Gleichzeitigkeit" mit "Go" an diesem Tag: facepalm!).

Die Auswahl wäre geringer, wenn einer der Typen eine definierte Struktur hat, die bereits ein Layout hat.

Eine Möglichkeit wäre, die Summanden nicht zu "komprimieren", wenn sie Zeiger enthalten, was bedeutet, dass die Größe der äquivalenten Struktur entspricht (+ 1 für den Diskriminator int). Vielleicht einen hybriden Ansatz, wenn möglich, damit alle Typen, die ein gemeinsames Layout haben, dies tun.

Es wäre schade, die schönen Größeneigenschaften zu verlieren, aber das ist wirklich nur eine Optimierung.

Selbst wenn es immer 1 + die Größe einer äquivalenten Struktur wäre, selbst wenn sie keine Zeiger enthielten, hätte es immer noch alle anderen netten Eigenschaften des Typs selbst, einschließlich der Kontrolle über die Zuweisungen. Weitere Optimierungen könnten im Laufe der Zeit hinzugefügt werden und wären zumindest möglich, wie Sie darauf hinweisen.

type p pick {
    A int
    B string
}

Müssen A und B dabei sein? Eine Auswahl wählt aus einer Reihe von Typen aus, also warum nicht ihre Bezeichnernamen vollständig wegwerfen:

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

Ich glaube, dieses Formular ist bereits für struct gültig. Es kann eine Einschränkung geben, dass es für die Auswahl erforderlich ist.

@Als ob der Feldname weggelassen wird, entspricht er dem Typ, sodass Ihr Beispiel funktioniert, aber da diese Feldnamen nicht exportiert werden, können sie nur innerhalb des definierenden Pakets festgelegt/zugegriffen werden.

Die Feldnamen müssen vorhanden sein, auch wenn sie implizit basierend auf dem Typnamen generiert werden, oder es gibt schlechte Interaktionen mit Zuweisbarkeit und Schnittstellentypen. Die Feldnamen sorgen dafür, dass es mit dem Rest von Go funktioniert.

@entschuldigung , ich habe gerade gemerkt, dass du etwas anderes

Ihre Formulierung funktioniert, aber dann haben Sie Dinge, die wie Strukturfelder aussehen, sich aber aufgrund des üblichen exportierten/nicht exportierten Dings anders verhalten.

Ist String von außerhalb des Pakets zugänglich, das p weil es sich im Universum befindet?

Wie wäre es mit

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

?

Indem Sie den Feldnamen vom Typnamen trennen, können Sie Folgendes tun:

pick {
  unexported Exported
  Exported unexported
}

oder auch

pick { Recoverable, Fatal error }

Wenn sich Auswahlfelder wie Strukturfelder verhalten, können Sie vieles von dem, was Sie bereits über Strukturfelder wissen, verwenden, um über Auswahlfelder nachzudenken. Der einzige wirkliche Unterschied besteht darin, dass jeweils nur ein Auswahlfeld eingestellt werden kann.

@jimmyfrasche
Go unterstützt bereits das Einbetten anonymer Typen in Strukturen, daher ist die Einschränkung des Geltungsbereichs bereits in der Sprache vorhanden, und ich glaube, dass dieses Problem durch Typaliase gelöst wird. Aber geben Sie zu, dass ich nicht an jeden möglichen Anwendungsfall gedacht habe. Es scheint davon abzuhängen, ob dieses Idiom in Go üblich ist:

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

Das kleine _t_ existiert in einem Paket, in dem es in großes T eingebettet ist, und wird nur durch solche exportierten Typen sichtbar gemacht.

@wie

Ich bin mir jedoch nicht sicher, ob ich ganz folge:

//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
  //...
}

Wenn Sie nur den Typnamen für das Label hätten, müssten Sie type Strings = []string eingeben, um beispielsweise []string einzuschließen.

So möchte ich Pick-Typen implementiert sehen. In
Insbesondere Rust und C++ (die Goldstandards für Leistung) funktionieren so
es.

Wenn ich nur die Vollständigkeit überprüfen wollte, könnte ich einen Checker verwenden. Ich möchte
der Leistungsgewinn. Das bedeutet, dass auch Pick-Typen nicht null sein können.

Die Übernahme der Adresse eines Mitglieds eines Pick-Elements sollte nicht erlaubt sein (es
ist auch im Single-Thread-Fall nicht speichersicher, wie in . bekannt
die Rust-Community.). Wenn dies andere Einschränkungen für einen Entnahmetyp erfordert,
Dann ist es halt so. Aber für mich, wenn ich Pick-Typen habe, ordnen Sie immer auf dem Heap zu
wäre schlecht.

Am 18. August 2017 um 12:01 schrieb "jimmyfrasche" [email protected] :

@josharian https://github.com/josharian also wenn ich das richtig lese
der Grund für iface ist jetzt immer (*type, *value) statt stashing
wortgroße Werte im zweiten Feld, wie es Go zuvor getan hat, so dass die
Concurrent GC muss nicht beide Felder überprüfen, um zu sehen, ob das zweite
ist ein Zeiger – er kann einfach annehmen, dass er es immer ist. Habe ich das richtig verstanden?

Mit anderen Worten, wenn der Picktyp implementiert wäre (mit C-Notation) wie

strukturieren {
int welche;
Gewerkschaft {
Aa;
Bb;
Cc;
} Summanden;
}

der GC müsste eine Sperre (oder etwas Ausgefallenes, aber Äquivalentes) nehmen, um
überprüfen, welche Summanden gescannt werden müssen?


Sie erhalten dies, weil Sie den Thread verfasst haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/19412#issuecomment-323393003 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AGGWB3Ayi31dYwotewcfgmCQL-XVrfxIks5sZbVrgaJpZM4MTmSr
.

@DemiMarie

Es sollte nicht erlaubt sein, die Adresse eines Mitglieds eines pick-Elements zu übernehmen (es ist nicht speichersicher, selbst im Single-Thread-Fall, wie in der Rust-Community bekannt). Wenn dies andere Einschränkungen für einen Auswahltyp erfordert, dann soll es so sein.

Das ist ein guter Punkt. Ich hatte das drin, aber es muss in einer Bearbeitung verloren gegangen sein. Ich habe hinzugefügt, dass, wenn Sie auf den Wert von einer Auswahl zugreifen, aus dem gleichen Grund immer eine Kopie zurückgegeben wird.

Als Beispiel dafür, warum das für die Nachwelt wahr ist, betrachte

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

Wenn v so optimiert ist, dass die Felder A und B dieselbe Position im Speicher einnehmen, dann zeigt p nicht auf ein int: es zeigt zu einem bool. Die Speichersicherheit wurde verletzt.

@jimmyfrasche

Der zweite Grund, warum Sie nicht möchten, dass der Inhalt adressierbar ist, ist die Mutationssemantik. Wenn der Wert indirekt unter Umständen gespeichert, dann

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

Eine Stelle, an der pick Schnittstellen ähnelt, besteht darin, dass Sie die Wertesemantik beibehalten möchten, wenn Sie darin Werte speichern. Wenn Sie als Implementierungsdetail möglicherweise Indirektion benötigen, besteht die einzige Möglichkeit darin, den Inhalt nicht adressierbar zu machen (oder genauer gesagt, veränderlich adressierbar, aber die Unterscheidung existiert in Go derzeit nicht), damit Sie das Aliasing nicht beobachten können .

Edit: Ups (siehe unten)

@jimmyfrasche

Der Nullwert eines Entnahmetyps ist das erste Feld in der Quellreihenfolge und der Nullwert dieses Felds.

Beachten Sie, dass dies nicht funktionieren würde, wenn das erste Feld indirekt gespeichert werden muss, es sei denn, Sie geben den Nullwert in Sonderfällen an, damit v.[A] und v.(error) das Richtige tun.

@stevenblenkinsop Ich bin mir nicht sicher, was Sie mit "das erste Feld muss indirekt gespeichert werden" meinen. Ich nehme an, Sie meinen, wenn das erste Feld ein Zeiger oder ein Typ ist, der implizit einen Zeiger enthält. Wenn ja, finden Sie unten ein Beispiel. Wenn nicht, könnten Sie das bitte klären?

Gegeben

var p pick { A error; B int }

der Nullwert, p , hat das dynamische Feld A und der Wert von A ist null.

Ich bezog mich nicht darauf, dass der Wert, der in pick gespeichert ist, ein Zeiger ist/enthält, ich bezog sich auf einen Nicht-Zeigerwert, der aufgrund von Layoutbeschränkungen, die vom Garbage Collector auferlegt wurden, indirekt gespeichert wird, wie von @josharian . beschrieben .

In Ihrem Beispiel könnte p.B – das kein Zeiger ist – den überlappenden Speicher nicht mit p.A teilen, der aus zwei Zeigern besteht. Es müsste höchstwahrscheinlich indirekt gespeichert werden (dh als *int , das beim Zugriff automatisch dereferenziert wird, anstatt als int ). Wenn p.B das erste Feld wäre, wäre der Nullwert von pick new(int) , was kein akzeptabler Nullwert ist, da es eine Initialisierung erfordert. Sie müssen die Groß-/Kleinschreibung so festlegen, dass eine Null *int als new(int) .

@jimmyfrasche
Oh, das tut mir leid. Als ich das Gespräch noch einmal durchging, stellte ich fest, dass Sie erwägten, angrenzenden Speicher zu verwenden, um Varianten mit inkompatiblen Layouts zu speichern, anstatt den Schnittstellenmechanismus der indirekten Speicherung von Nicht-Zeiger-Typen zu kopieren. Meine letzten drei Kommentare machen in diesem Fall keinen Sinn.

Edit: Hoppla, Race Condition. Gepostet, dann deinen Kommentar gesehen.

@stevenblenkinsop ah, okay, ich

Die gemeinsame Nutzung überlappender Speicher ist eine Optimierung. Das könnte sie niemals tun: Die Semantik des Typs ist das Wichtige.

Wenn der Compiler den Speicher optimieren kann und sich dafür entscheidet, ist das ein netter Bonus.

In Ihrem Beispiel könnte der Compiler es genau so speichern wie die entsprechende Struktur (ein Tag hinzufügen, um zu wissen, welches das aktive Feld ist). Das wäre

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

Der Nullwert ist immer noch alle Bytes 0 und es besteht keine Notwendigkeit, als Sonderfall heimlich zuzuweisen.

Wichtig ist, dass zu einem bestimmten Zeitpunkt nur ein Feld im Spiel ist.

Die Motivation für das Zulassen von Typzusicherungen/-schaltern bei Auswahlen war, dass Sie beispielsweise, wenn jeder Typ in der Auswahl fmt.Stringer erfüllte, eine Methode auf die Auswahl schreiben könnten wie

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

Da es sich bei den Arten von Auswahlfeldern jedoch um Schnittstellen handeln kann, entsteht eine Feinheit.

Wenn die Auswahl P im vorherigen Beispiel ein Feld hätte, dessen Typ selbst fmt.Stringer , würde diese String Methode in Panik geraten, wenn dies das dynamische Feld wäre und sein Wert nil . Sie können eine nil Schnittstelle für nichts eingeben, nicht einmal für sich selbst. https://play.golang.org/p/HMYglwyVbl Dies war zwar schon immer so, aber es taucht nicht regelmäßig auf, aber es könnte regelmäßiger mit Picks auftauchen.

Die geschlossene Natur von Summentypen würde es einem Vollständigkeits-Linter jedoch ermöglichen, überall dort zu finden, wo dies auftauchen würde (möglicherweise mit einigen falsch positiven Ergebnissen) und den zu bearbeitenden Fall zu melden.

Es wäre auch überraschend, wenn Sie Methoden bei der Auswahl implementieren könnten, dass diese Methoden nicht verwendet werden, um eine Typzusicherung zu erfüllen.

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

Sie könnten die Typzusicherung Methoden aus dem aktuellen Feld heraufstufen lassen, wenn sie die Schnittstelle erfüllen, aber dies führt zu eigenen Problemen, z sogar wie man dies effizient umsetzt). Außerdem könnte man dann erwarten, dass Methoden, die allen Feldern gemeinsam sind, zum Pick selbst hochgestuft werden, aber dann müssten sie bei jedem Aufruf über die Variantenauswahl verteilt werden, zusätzlich möglicherweise zu einem virtuellen Versand, wenn der Pick in einer Schnittstelle gespeichert ist , und/oder zu einem virtuellen Versand, wenn das Feld eine Schnittstelle ist.

Bearbeiten: Übrigens ist das optimale Packen eines Picks ein Beispiel für das kürzeste allgemeine Superstring- Problem, das NP-vollständig ist, obwohl es häufig verwendete gierige Näherungen gibt.

Die Regel lautet, wenn es sich um einen Auswahlwert handelt, wird die Typzusicherung im dynamischen Feld des Auswahlwerts bestätigt, aber wenn der Auswahlwert in einer Schnittstelle gespeichert wird, befindet sich die Typzusicherung im Methodensatz des Auswahltyps. Es mag auf den ersten Blick überraschend sein, aber es ist ziemlich konsistent.

Es wäre kein Problem, das Zulassen von Typzusicherungen für einen Auswahlwert einfach wegzulassen. Es wäre jedoch schade, da es sehr einfach ist, Methoden zu fördern, die alle Typen in der Auswahl teilen, ohne alle Fälle ausschreiben oder Reflektion verwenden zu müssen.

Es wäre jedoch ziemlich einfach, die Codegenerierung zu verwenden, um die

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

Einfach weitermachen und Typzusicherungen fallen lassen. Vielleicht sollten sie hinzugefügt werden, aber sie sind kein notwendiger Teil des Vorschlags.

Ich möchte auf den vorherigen Kommentar von @ianlancetaylor zurückkommen , weil ich eine neue Perspektive dazu habe, nachdem ich mehr über die Fehlerbehandlung nachgedacht habe (insbesondere https://github.com/golang/go/issues/21161# Ausgabekommentar-320294933).

Was bringt uns die neue Art von Typ insbesondere, was wir von Schnittstellentypen nicht bekommen?

Aus meiner Sicht besteht der Hauptvorteil von Summentypen darin, dass wir zwischen der Rückgabe mehrerer Werte und der Rückgabe eines von mehreren Werten unterscheiden können – insbesondere, wenn einer dieser Werte eine Instanz der Fehlerschnittstelle ist.

Wir haben derzeit viele Funktionen des Formulars

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

Einige von ihnen, wie io.Reader.Read und io.Reader.Write , geben ein T zusammen mit einem error , während andere entweder ein T oder zurückgeben ein error aber nie beides. Beim früheren API-Stil ist das Ignorieren von T im Fehlerfall oft ein Fehler (zB wenn der Fehler io.EOF ); für den letzteren Stil ist die Rückgabe von T ungleich Null der Fehler.

Automatisierte Tools, einschließlich lint , können die Verwendung bestimmter Funktionen überprüfen, um sicherzustellen, dass der Wert korrekt ignoriert wird (oder nicht), wenn der Fehler nicht null ist, aber solche Überprüfungen erstrecken sich natürlich nicht auf beliebige Funktionen.

proto.Marshal soll beispielsweise der Stil "Wert und Fehler" sein, wenn der Fehler ein RequiredNotSetError , ansonsten scheint er jedoch der Stil "Wert oder Fehler" zu sein. Da das Typsystem nicht zwischen den beiden unterscheidet, ist es leicht, versehentlich Regressionen einzuführen: entweder einen Wert nicht zurückzugeben, wenn wir es sollten, oder einen Wert zurückzugeben, wenn wir es nicht sollten. Und Implementierungen von proto.Marshaler verkomplizieren die Sache weiter.

Auf der anderen Seite, wenn wir den Typ als Vereinigung ausdrücken könnten, könnten wir ihn viel expliziter machen:

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

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

@ianlancetaylor , ich habe mit deinem Vorschlag auf dem Papier

Gegeben

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

der dynamische Typ von r ist int und

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

ist illegal.

Gegeben

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

dann wäre var _ R = S{} illegal.

Aber gegeben

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

sowohl var _ R = C{} als auch var _ R = A(C{}) wären legal.

Beide

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

und

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

sind gleichwertig.

Gleichfalls,

interface{} restrict { error, net.Error }

ist äquivalent zu

interface { Error() string }

Gegeben

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

dann ist der zugrunde liegende Typ von R äquivalent zu

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

Edit: kleine Korrektur in Kursivschrift

@jimmyfrasche Ich würde nicht so weit gehen zu sagen, dass das, was ich oben geschrieben habe, ein Vorschlag war. Es war eher eine Idee. Ich müsste über Ihre Kommentare nachdenken, aber auf den ersten Blick sehen sie plausibel aus.

Der Vorschlag von Auswahltyp in Go verhält. Ich denke, es ist besonders erwähnenswert, dass sein Vorschlag, den Nullwert des ersten Felds für den Nullwert des Picks zu verwenden, intuitiv ist mit dem "Nullwert bedeutet, die Bytes auf Null zu setzen", vorausgesetzt, die Tag-Werte beginnen bei Null (vielleicht dies wurde schon angemerkt; dieser Thread ist mittlerweile sehr lang...). Ich mag auch die Auswirkungen auf die Leistung (keine unnötigen Allocs) und dass Picks vollständig orthogonal zu Schnittstellen sind (kein überraschendes Verhalten beim Wechseln bei einem Pick, der eine Schnittstelle enthält).

Das einzige, was ich ändern würde, ist, das Tag zu mutieren: foo.X = 0 scheint foo = Foo{X: 0} ; ein paar Zeichen mehr, aber expliziter, dass das Tag zurückgesetzt und der Wert auf Null gesetzt wird. Dies ist ein kleiner Punkt, und ich würde mich trotzdem sehr freuen, wenn sein Vorschlag so wie er ist angenommen würde.

@ns-cweber danke, aber ich kann das Nullwertverhalten nicht anerkennen. Die Ideen schwirrten seit einiger Zeit herum und waren in auftauchte . Meine Begründung war dieselbe wie die, die Sie angegeben haben.

Soweit foo.X = 0 vs foo = Foo{X: 0} , erlaubt mein Vorschlag eigentlich beides. Letzteres ist nützlich, wenn das Feld einer Auswahl eine Struktur ist, sodass Sie foo.X.Y = 0 anstelle von foo = Foo{X: image.Point{X: foo.[X].X, 0}} ausführen können, was zusätzlich zur ausführlichen Ausführung zur Laufzeit fehlschlagen kann.

Ich denke auch, dass es hilft, es als solches zu belassen, weil es den Elevator Pitch für seine Semantik verstärkt: Es ist eine Struktur, bei der nur ein Feld gleichzeitig festgelegt werden kann.

Eine Sache, die verhindern kann, dass sie unverändert akzeptiert wird, ist, wie das Einbetten eines Picks in eine Struktur funktionieren würde. Mir wurde neulich klar, dass ich die verschiedenen Auswirkungen beschönigt habe, die sich bei der Verwendung der Struktur ergeben würden. Ich denke, es ist reparierbar, aber nicht ganz sicher, was die besten Reparaturen sind. Das einfachste wäre, dass es nur die Methoden erbt und Sie direkt auf die eingebettete Auswahl mit Namen verweisen müssen, um zu ihren Feldern zu gelangen, und ich tendiere dazu, um zu vermeiden, dass eine Struktur sowohl Strukturfelder als auch Auswahlfelder enthält.

@jimmyfrasche Vielen Dank, dass Sie mich bezüglich des Nullwertverhaltens korrigiert haben. Ich stimme zu, dass Ihr Vorschlag beide Mutatoren zulässt, und ich denke, Ihr Elevator-Pitch-Punkt ist gut. Ihre Erklärung für Ihren Vorschlag ist sinnvoll, obwohl ich mir vorstellen könnte, foo.XY festzulegen, ohne zu wissen, dass dies das Auswahlfeld automatisch ändern würde. Ich würde mich trotzdem sehr freuen, wenn Ihr Vorschlag, auch mit dieser kleinen Einschränkung, Erfolg hätte.

Schließlich scheint Ihr einfacher Vorschlag für die Einbettung von Picks der zu sein, den ich intuitiv erahnen würde. Selbst wenn wir unsere Meinung ändern, können wir vom einfachen Vorschlag zum komplexen Vorschlag übergehen, ohne bestehenden Code zu zerstören, aber das Gegenteil ist nicht der Fall.

@ns-cweber

Ich konnte sehen, wie ich foo.XY einstellte, ohne zu ahnen, dass es das Auswahlfeld automatisch ändern würde

Das ist ein fairer Punkt, aber Sie könnten es über viele Dinge in der Sprache oder jeder anderen Sprache machen. Im Allgemeinen hat Go Sicherheitsschienen, aber keine Sicherheitsscheren.

Es gibt viele große Dinge, vor denen es Sie im Allgemeinen schützt, wenn Sie sich nicht bemühen, sie zu untergraben, aber Sie müssen trotzdem wissen, was Sie tun.

Das kann ärgerlich sein, wenn Sie einen solchen Fehler machen, aber, otoh, es ist nicht viel anders als "Ich habe bar.X = 0 aber ich wollte bar.Y = 0 festlegen", da die Hypothese davon abhängt, dass Sie es nicht merken dass foo ein Auswahltyp ist.

Ähnlich sehen i.Foo() , p.Foo() und v.Foo() alle gleich aus, aber wenn i eine nil Schnittstelle ist, p ist ein Null-Zeiger und Foo behandelt diesen Fall nicht, die ersten beiden könnten in Panik geraten, während v einen Wertmethodenempfänger verwendet, dies nicht könnte (zumindest nicht vom Aufruf selbst) .

Was die Einbettung angeht, ein guter Punkt, dass es später leicht zu lösen ist, also habe ich einfach den Vorschlag bearbeitet.

Summentypen haben oft ein wertloses Feld. Im Paket database/sql haben wir beispielsweise:

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

Wenn wir Summentypen / Picks / Unions hätten, könnte dies wie folgt ausgedrückt werden:

type NullString pick {
  Null   struct{}
  String string
}

Ein Summentyp hat in diesem Fall offensichtliche Vorteile gegenüber einer Struktur. Ich denke, dies ist eine übliche Verwendung, die es wert wäre, als Beispiel in jeden Vorschlag aufgenommen zu werden.

Bikeshedding (sorry), ich würde argumentieren, dass dies syntaktische Unterstützung und Inkonsistenz mit der Einbettungssyntax von struct-Feldern wert ist:

type NullString union {
  Null
  String string
}

@neild

Den letzten Punkt zuerst treffen: Als letzte Änderung vor dem Posten (in keiner Weise zwingend erforderlich ) habe ich hinzugefügt, dass, wenn es einen benannten Typ (oder einen Zeiger auf einen benannten Typ) ohne Feldnamen gibt, die Auswahl ein implizites Feld erstellt mit dem gleichen Namen wie der Typ. Das ist vielleicht nicht die beste Idee, aber es schien, als würde es einen der häufigsten Fälle von "jedem dieser Typen" ohne viel Aufhebens abdecken. Angesichts der Tatsache, dass Ihr letztes Beispiel geschrieben werden könnte:

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

Aber zurück zu Ihrem Hauptpunkt, ja, das ist eine ausgezeichnete Verwendung. Tatsächlich können Sie damit Aufzählungen erstellen: type Stoplight pick { Stop, Slow, Go struct{} } . Dies wäre ähnlich wie ein const/iota-faux-enum. Es würde sogar auf die gleiche Ausgabe kompilieren. Der Hauptvorteil in diesem Fall besteht darin, dass die Zahl, die den Bundesstaat repräsentiert, vollständig gekapselt ist und Sie keinen anderen als die drei aufgeführten Staaten eingeben können.

Leider gibt es eine etwas umständliche Syntax zum Erstellen und Festlegen von Werten von Stoplight , die in diesem Fall noch verschärft wird:

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

Es würde helfen, {} oder _ als Abkürzung für struct{}{} , wie an anderer Stelle vorgeschlagen.

Viele Sprachen, insbesondere funktionale Sprachen, umgehen dies, indem sie die Labels in den gleichen Bereich wie den Typ legen. Dies führt zu einer hohen Komplexität und würde verhindern, dass zwei Auswahlen, die im selben Bereich definiert sind, Feldnamen teilen.

Es ist jedoch leicht, dies mit einem Codegenerator zu umgehen, der eine Funktion mit demselben Namen jedes Felds in der Auswahl erstellt, die den Feldtyp als Argument verwendet. Wenn es als Sonderfall auch keine Argumente annehmen würde, wenn der Typ die Größe Null hat, dann würde die Ausgabe für das Stoplight Beispiel so aussehen

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

und für Ihr NullString Beispiel würde es so aussehen:

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

Es ist nicht schön, aber es ist go generate entfernt und wahrscheinlich sehr leicht inline.

Das würde nicht funktionieren, wenn implizite Felder basierend auf den Typnamen erstellt wurden (es sei denn, die Typen stammten aus anderen Paketen) oder es wurde auf zwei Picks im selben Paket mit gemeinsamen Feldnamen ausgeführt, aber das ist in Ordnung. Der Vorschlag macht nicht alles sofort, aber er ermöglicht viele Dinge und gibt dem Programmierer die Flexibilität zu entscheiden, was für eine bestimmte Situation am besten ist.

Weitere Syntax-Bikeshedding:

type NullString union {
  Null
  Value string
}

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

Konkret wird ein Literal mit einer Elementliste, die keine Schlüssel enthält, als Benennung des zu setzenden Felds interpretiert.

Dies wäre syntaktisch inkonsistent mit anderen Verwendungen von zusammengesetzten Literalen. Auf der anderen Seite ist es eine Verwendung, die im Kontext von Union/Pick/Sum-Typen (zumindest für mich) sinnvoll und intuitiv erscheint, da es keine vernünftige Interpretation eines Union-Initialisierers ohne Schlüssel gibt.

@neild

Dies wäre syntaktisch inkonsistent mit anderen Verwendungen von zusammengesetzten Literalen.

Das scheint mir ein großes Negativ zu sein, obwohl es im Kontext sinnvoll ist.

Beachten Sie auch, dass

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

Für den Umgang mit struct{}{} wenn ich ein map[T]struct{} ich werfe

var set struct{}

irgendwo und benutze theMap[k] = set , Ähnliches würde mit Picks funktionieren

Weiterer Bikeshedding: Der leere Typ (im Kontext von Summentypen) wird konventionell als "Einheit" bezeichnet, nicht als "Null".

@bcmillsSorta .

Wenn Sie in funktionalen Sprachen einen Summentyp erstellen, sind seine Beschriftungen tatsächlich Funktionen, die die Werte dieses Typs erstellen (wenn auch spezielle Funktionen, die als "Typkonstruktoren" oder "Tycons" bekannt sind, die der Compiler kennt, um einen Mustervergleich zu ermöglichen).

data Bool = False | True

erstellt den Datentyp Bool und zwei Funktionen im gleichen Umfang, True und False , jede mit der Signatur () -> Bool .

Hier schreiben Sie mit () die vom Typ ausgesprochene Einheit – den Typ mit nur einem einzigen Wert. In Go kann dieser Typ auf viele verschiedene Arten geschrieben werden, aber er wird idiomatisch als struct{} .

Der Typ des Arguments des Konstruktors würde also unit heißen. Die Konvention für den Namen des Konstruktors ist im Allgemeinen None wenn er als Optionstyp wie dieser verwendet wird, aber er kann an die Domäne angepasst werden. Null wäre ein schöner Name, wenn der Wert beispielsweise aus einer Datenbank käme.

@bcmills

Aus meiner Sicht besteht der Hauptvorteil von Summentypen darin, dass wir zwischen der Rückgabe mehrerer Werte und der Rückgabe eines von mehreren Werten unterscheiden können – insbesondere, wenn einer dieser Werte eine Instanz der Fehlerschnittstelle ist.

Aus einer anderen Perspektive sehe ich darin einen großen Nachteil der Summentypen in Go.

Viele Sprachen verwenden natürlich Summentypen genau für den Fall, dass ein Wert oder ein Fehler zurückgegeben wird, und das funktioniert gut für sie. Wenn in Go Summentypen hinzugefügt würden, wäre die Versuchung groß, sie auf die gleiche Weise zu verwenden.

Go verfügt jedoch bereits über ein großes Code-Ökosystem, das zu diesem Zweck mehrere Werte verwendet. Wenn neuer Code Summentypen verwendet, um Tupel (Wert, Fehler) zurückzugeben, wird dieses Ökosystem fragmentiert. Einige Autoren werden weiterhin mehrere Rückgaben verwenden, um die Konsistenz mit ihrem vorhandenen Code zu gewährleisten. einige Autoren verwenden Summentypen; einige werden versuchen, ihre vorhandenen APIs zu konvertieren. Autoren, die aus irgendeinem Grund an älteren Go-Versionen festhalten, werden von neuen APIs ausgeschlossen. Es wird ein Durcheinander sein, und ich glaube nicht, dass die Gewinne die Kosten wert sein werden.

Wenn neuer Code Summentypen verwendet, um Tupel (Wert, Fehler) zurückzugeben, wird dieses Ökosystem fragmentiert.

Wenn wir Summentypen in Go 2 hinzufügen und einheitlich verwenden, reduziert sich das Problem auf Migration, nicht auf Fragmentierung: Es müsste möglich sein, eine Go 1 (Wert, Fehler) API in eine Go 2 (Wert | Fehler) umzuwandeln ) API und umgekehrt, aber es könnten unterschiedliche Typen in den Go 2-Teilen des Programms sein.

Wenn wir in Go 2 Summentypen hinzufügen und einheitlich verwenden

Beachten Sie, dass dies ein ganz anderer Vorschlag ist als der hier bisher gesehene: Die Standardbibliothek muss umfassend überarbeitet werden, die Übersetzung zwischen API-Stilen muss definiert werden usw. Gehen Sie diesen Weg und dies wird ein ziemlich großer und komplizierter Vorschlag für einen API-Übergang mit einem Nebenkodizil bezüglich des Designs von Summentypen.

Die Absicht ist, dass Go 1 und Go 2 nahtlos im selben Projekt koexistieren können, daher denke ich nicht, dass jemand "aus irgendeinem Grund" mit einem Go 1-Compiler festhängt und nicht in der Lage ist, einen zu verwenden Gehen Sie 2 Bibliothek. Wenn Sie jedoch die Abhängigkeit A , die wiederum von B abhängt, und B Updates haben, um eine neue Funktion wie pick in seiner API zu verwenden, dann würde die Abhängigkeit A aufheben, es sei denn, sie wird aktualisiert, um die neue Version von B . A könnte einfach B verkaufen und die alte Version weiter verwenden, aber wenn die alte Version nicht wegen Sicherheitsfehlern usw. gewartet wird oder wenn Sie die neue Version von verwenden müssen B direkt und Sie können aus irgendeinem Grund nicht zwei Versionen in Ihrem Projekt haben, dies könnte zu Problemen führen.

Letztlich hat das Problem hier wenig mit Sprachversionen zu tun, sondern eher mit der Änderung der Signaturen von bestehenden exportierten Funktionen. Die Tatsache, dass es ein neues Feature wäre, das den Anstoß liefert, lenkt ein wenig davon ab. Wenn die Absicht besteht , vorhandene APIs so zu ändern, dass sie pick ohne die Abwärtskompatibilität zu beeinträchtigen, muss möglicherweise eine Art Brückensyntax vorhanden sein. Zum Beispiel (komplett als Strohmann):

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

Der Compiler könnte einfach ReadResult splatieren, wenn auf ihn über Legacy-Code zugegriffen wird, und Nullwerte verwenden, wenn ein Feld in einer bestimmten Variante nicht vorhanden ist. Ich bin mir nicht sicher, wie ich den anderen Weg gehen soll oder ob es sich lohnt. APIs wie template.Must möglicherweise einfach weiterhin mehrere Werte akzeptieren, anstatt pick und sich auf Splatting zu verlassen, um den Unterschied auszugleichen. Oder so etwas könnte verwendet werden:

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

Dies verkompliziert die Dinge, aber ich kann sehen, dass die Einführung einer Funktion, die ändert, wie APIs geschrieben werden sollten , eine Geschichte für den Übergang erfordert, ohne die Welt zu zerstören. Vielleicht gibt es eine Möglichkeit, die keine Bridge-Syntax erfordert.

Es ist trivial, von Summentypen zu Produkttypen (Strukturen, mehrere Rückgabewerte) zu wechseln – setzen Sie einfach alles, was nicht der Wert ist, auf Null. Der Übergang von Produkttypen zu Summentypen ist im Allgemeinen nicht gut definiert.

Wenn eine API nahtlos und schrittweise von einer produkttypbasierten Implementierung zu einer summentypbasierten Implementierung übergehen möchte, besteht der einfachste Weg darin, zwei Versionen von allem Notwendigen zu haben, wobei die Summentypversion die tatsächliche Implementierung hat und die Produkttypversion die . aufruft Summentyp-Version, alle erforderlichen Laufzeitprüfungen und jede Projektion in den Produktbereich.

Das ist wirklich abstrakt, also hier ein Beispiel

Variante 1 ohne Summen

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
}

Variante 2 mit Summen

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
  }
}

Version 3 würde Geben/Nehmen entfernen

Version 4 würde die Implementierung von GiveSum/TakeSum nach Give/Take verschieben, GiveSum/TakeSum dazu bringen, einfach Give/Take aufzurufen und GiveSum/TakeSum als veraltet zu bezeichnen.

Version 5 würde GiveSum/TakeSum entfernen

Es ist nicht schön oder schnell, aber es ist dasselbe wie jede andere groß angelegte Störung ähnlicher Art und erfordert nichts zusätzliches von der Sprache

Ich denke, (das meiste) der Nützlichkeit eines Summentyps könnte mit einem Mechanismus realisiert werden, um die Zuweisung zu einem Typ einer Typschnittstelle{} zur Kompilierzeit einzuschränken.

In meinen Träumen sieht es so aus:

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... 

... es wäre auch ein Fehler bei der Kompilierung, zu behaupten, dass ein Switch-Typ ein nicht explizit definierter Typ ist:

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
}

und go vet würde über mehrdeutige konstante Zuweisungen an Typen wie T3 nörgeln, aber für alle Absichten und Zwecke (zur Laufzeit) wäre var x T3 = 32 var x interface{} = 32 . Vielleicht wären einige vordefinierte Schaltertypen für Builtins in einem Paket mit dem Namen Schalter oder Ponys auch groovig.

@j7b , @ianlancetaylor bot eine ähnliche Idee in https://github.com/golang/go/issues/19412#issuecomment -323256891

Ich habe später unter https://github.com/golang/go/issues/19412#issuecomment -325048452 gepostet, was meiner Meinung nach die logischen Konsequenzen davon wären

Es sieht so aus, als würden viele von ihnen angesichts der Ähnlichkeit gleichermaßen zutreffen.

Es wäre wirklich toll, wenn so etwas funktionieren würde. Es wäre einfach, von Interfaces zu Interfaces+Restriktionen überzugehen (insbesondere mit Ians Syntax: einfach restrict an das Ende bestehender Pseudosummen anhängen, die mit Interfaces erstellt wurden). Es wäre einfach zu implementieren, da sie zur Laufzeit im Wesentlichen mit Schnittstellen identisch wären und die meiste Arbeit nur darin bestehen würde, dass der Compiler zusätzliche Fehler ausgibt, wenn ihre Invarianten beschädigt sind.

Aber ich glaube nicht, dass es möglich ist, es zum Laufen zu bringen.

Alles reiht mich so nah , dass es wie ein fit aussieht, aber Sie die Ansicht vergrößern und es ist einfach nicht ganz richtig, so dass Sie ihm einen kleinen Schub geben und dann etwas anderer Pop aus der Ausrichtung. Sie können versuchen, es zu reparieren, aber dann erhalten Sie etwas, das sehr nach Schnittstellen aussieht, sich aber in seltsamen Fällen anders verhält.

Vielleicht übersehe ich etwas.

An dem eingeschränkten Schnittstellenvorschlag ist nichts auszusetzen, solange Sie damit einverstanden sind, dass die Fälle nicht unbedingt unzusammenhängend sind. Ich glaube nicht, dass es so überraschend ist wie Sie, dass eine Vereinigung zwischen zwei Schnittstellentypen (wie io.Reader / io.Writer ) nicht zusammenhangslos ist. Dies stimmt völlig mit der Tatsache überein, dass Sie nicht feststellen können, ob ein Wert, der einem interface{} zugewiesen wurde, als io.Reader oder als io.Writer gespeichert wurde, wenn beides implementiert ist. Die Tatsache, dass Sie eine disjunkte Vereinigung konstruieren können, solange jeder Fall ein konkreter Typ ist, scheint vollkommen ausreichend.

Der Kompromiss besteht darin, dass Sie, wenn Unions eingeschränkte Schnittstellen sind, keine Methoden direkt auf ihnen definieren können. Und wenn es sich um eingeschränkte Schnittstellentypen handelt, erhalten Sie nicht den garantierten direkten Speicher, den pick Typen bieten. Ob es sich lohnt, der Sprache etwas Besonderes hinzuzufügen, um diese zusätzlichen Vorteile zu erhalten, bin ich mir nicht sicher.

@jimmyfrasche für type T switch {io.Reader,io.Writer} Es ist in Ordnung, T einen ReadWriter zuzuweisen, aber Sie können nur behaupten, dass T ein io.Reader oder Io.Writer ist ein ReadWriter, der dazu ermutigen sollte, ihn zum switchtype hinzuzufügen, wenn es eine nützliche Assertion ist.

@stevenblenkinsop Sie könnten den Auswahlvorschlag ohne Methoden definieren. Wenn Sie Methoden und implizite Feldnamen loswerden, können Sie die Einbettung von Picks zulassen. (Obwohl ich eindeutig denke, dass Methoden und in viel geringerem Maße implizite Feldnamen der nützlichere Kompromiss sind).

Und andererseits würde die Syntax von @ianlancetaylor dies zulassen

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

das würde kompilieren, solange A , B und C jeweils die Methoden Foo und Bar (obwohl Sie sich Sorgen machen müssten) über nil Werte).

edit: Klarstellung in Kursivschrift

Ich denke, eine Form von _eingeschränkter Schnittstelle_ wäre nützlich, aber ich bin mit der Syntax nicht einverstanden. Hier ist, was ich vorschlage. Er verhält sich ähnlich wie ein algebraischer Datentyp, der domänenbezogene Objekte gruppiert, die nicht unbedingt ein gemeinsames Verhalten aufweisen.

//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..
   }
}

Es gibt mehrere Vorteile dieses Ansatzes gegenüber dem herkömmlichen Ansatz der leeren Schnittstelle interface{} :

  • statische Typprüfung bei Verwendung der Funktion
  • Der Benutzer kann allein aus der Funktionssignatur ableiten, welche Art von Argument erforderlich ist, ohne sich die Funktionsimplementierung ansehen zu müssen

Eine leere Schnittstelle interface{} ist nützlich, wenn die Anzahl der beteiligten Typen unbekannt ist. Hier bleibt Ihnen wirklich nichts anderes übrig, als sich auf die Laufzeitverifizierung zu verlassen. Auf der anderen Seite, wenn die Anzahl der Typen begrenzt und während der Kompilierzeit bekannt ist, warum nicht den Compiler bitten, uns zu unterstützen?

@henryas Ich denke, ein nützlicherer Vergleich wäre die derzeit empfohlene Methode für (offene) Summentypen: Nicht leere Schnittstellen (wenn keine klare Schnittstelle destilliert werden kann, mit nicht exportierten Markerfunktionen).
Ich glaube nicht, dass Ihre Argumente darauf in signifikanter Weise zutreffen.

Hier ein Erfahrungsbericht zu Go-Protobufs:

  • Die proto2-Syntax lässt "optionale" Felder zu, bei denen es sich um Typen handelt, bei denen zwischen dem Nullwert und einem nicht gesetzten Wert unterschieden wird. Die aktuelle Lösung besteht darin, einen Zeiger (z. B. *int ) zu verwenden, wobei ein Null-Zeiger unset anzeigt, während ein Set-Zeiger auf den tatsächlichen Wert zeigt. Der Wunsch ist ein Ansatz, der es ermöglicht, eine Unterscheidung zwischen null und nicht gesetzt zu ermöglichen, ohne den üblichen Fall zu verkomplizieren, nur auf den Wert zuzugreifen (wobei der Wert null in Ordnung ist, wenn er nicht gesetzt ist).

    • Dies ist aufgrund einer zusätzlichen Zuweisung nicht leistungsbereit (obwohl Gewerkschaften je nach Umsetzung dasselbe Schicksal erleiden können).
    • Dies ist für Benutzer schmerzhaft, da die Notwendigkeit, den Zeiger ständig zu überprüfen, die Lesbarkeit beeinträchtigt (obwohl Standardwerte ungleich Null in Protos bedeuten können, dass die Überprüfung eine gute Sache ist ...).
  • Die Proto-Sprache ermöglicht "one ofs", die Proto-Version von Summentypen. Der derzeit verfolgte Ansatz ist wie folgt ( Bruttobeispiel ):

    • Definieren Sie einen Schnittstellentyp mit einer versteckten Methode (zB type Communique_Union interface { isCommunique_Union() } )
    • Definieren Sie für jeden der möglichen Go-Typen, die in der Union erlaubt sind, eine Wrapper-Struktur, deren einziger Zweck darin besteht, jeden erlaubten Typ zu umschließen (zB type Communique_Number struct { Number int32 } ), wobei jeder Typ die Methode isCommunique_Union .
    • Dies ist ebenfalls nicht performant, da die Wrapper eine Allokation bewirken. Ein Summentyp würde helfen, da wir wissen, dass der größte Wert (ein Slice) nicht mehr als 24B belegen würde.

@henryas Ich denke, ein nützlicherer Vergleich wäre die derzeit empfohlene Methode für (offene) Summentypen: Nicht leere Schnittstellen (wenn keine klare Schnittstelle destilliert werden kann, mit nicht exportierten Markerfunktionen).
Ich glaube nicht, dass Ihre Argumente darauf in signifikanter Weise zutreffen.

Sie meinen, indem Sie einem Objekt eine nicht exportierte Dummy-Methode hinzufügen, damit das Objekt wie folgt als Schnittstelle übergeben werden kann?

type MyInterface interface {
   belongToMyInterface() //dummy method definition
}

type MyObject struct{}
func (MyObject) belongToMyInterface(){} //dummy method

Das ist meiner Meinung nach überhaupt nicht zu empfehlen. Es ist eher ein Workaround als eine Lösung. Ich persönlich würde lieber auf eine statische Typüberprüfung verzichten, als leere Methoden und unnötige Methodendefinitionen herumliegen zu haben.

Dies sind die Probleme mit dem Ansatz der _Dummy-Methode_:

  • Unnötige Methoden und Methodendefinitionen, die das Objekt und die Schnittstelle überladen.
  • Jedes Mal, wenn eine neue _group_ hinzugefügt wird, müssen Sie die Implementierung des Objekts ändern (z. B. Dummy-Methoden hinzufügen). Das ist falsch (siehe nächster Punkt).
  • Der algebraische Datentyp (oder die Gruppierung basierend auf _domain_ statt auf Verhalten) ist domänenspezifisch . Abhängig von der Domäne müssen Sie die Objektbeziehung möglicherweise unterschiedlich anzeigen. Ein Buchhalter gruppiert Dokumente anders als ein Lagerleiter. Diese Gruppierung betrifft den Verbraucher des Objekts und nicht das Objekt selbst. Das Objekt muss nichts über das Problem des Verbrauchers wissen und sollte es auch nicht müssen. Muss eine Rechnung etwas über die Buchhaltung wissen? Wenn dies nicht der Fall ist, warum muss eine Rechnung dann ihre Implementierung ändern _(z. B. neue Dummy-Methoden hinzufügen)_ jedes Mal, wenn sich die Rechnungslegungsvorschrift ändert _(z. B. Anwenden einer neuen Beleggruppierung)_? Durch die Verwendung des _Dummy-Methode_-Ansatzes koppeln Sie Ihr Objekt an die Domäne des Verbrauchers und machen wichtige Annahmen über die Domäne des Verbrauchers. Sie sollten dies nicht tun. Dies ist noch schlimmer als der Ansatz mit der leeren Schnittstelle interface{} . Es gibt bessere Ansätze.

@henryas

Deinen dritten Punkt sehe ich nicht als starkes Argument. Wenn der Buchhalter Objektbeziehungen anders sehen möchte, kann der Buchhalter eine eigene Schnittstelle erstellen, die seiner Spezifikation entspricht. Das Hinzufügen einer privaten Methode zu einer Schnittstelle bedeutet nicht, dass die konkreten Typen, die sie erfüllen, mit Teilmengen der an anderer Stelle definierten Schnittstelle nicht kompatibel sind.

Der Go-Parser macht von dieser Technik intensiven Gebrauch und ehrlich gesagt kann ich mir nicht vorstellen, dass Picks dieses Paket so viel besser machen, dass es die Implementierung von Picks in der Sprache rechtfertigt.

@as Mein Punkt ist, dass jedes Mal, wenn eine neue _Beziehungsansicht_ erstellt wird, die relevanten konkreten Objekte aktualisiert werden müssen, um bestimmte Anpassungen für diese Ansicht vorzunehmen. Es scheint falsch zu sein, denn dazu müssen die Objekte oft eine bestimmte Annahme über die Domäne des Verbrauchers treffen. Wenn die Objekte und die Verbraucher eng verwandt sind oder innerhalb derselben Domäne leben, wie im Fall des Go-Parsers, ist dies möglicherweise nicht von Bedeutung. Wenn die Objekte jedoch grundlegende Funktionalitäten bereitstellen, die von mehreren anderen Domänen verwendet werden sollen, wird dies zu einem Problem. Die Objekte müssen nun ein wenig über alle anderen Domänen wissen, damit der Ansatz der _Dummy-Methode_ funktioniert.

Sie haben am Ende viele leere Methoden, die an die Objekte angehängt sind, und es ist für die Leser nicht offensichtlich, warum Sie diese Methoden benötigen, da die Schnittstellen, die sie benötigen, in einer separaten Domäne/einem Paket/einer separaten Schicht leben.

Der Punkt, dass der Open-Summen-über-Schnittstellen-Ansatz es Ihnen nicht ermöglicht, Summen einfach zu verwenden, ist fair genug. Explizite Summentypen würden es offensichtlich einfacher machen, Summen zu haben. Es ist jedoch ein ganz anderes Argument als "Summentypen geben Ihnen Typsicherheit" - Sie können auch heute noch Typsicherheit erhalten, wenn Sie sie brauchen.

Ich sehe jedoch immer noch zwei Nachteile von geschlossenen Summen, wie sie in anderen Sprachen implementiert sind: Zum einen die Schwierigkeit, sie in einem groß angelegten verteilten Entwicklungsprozess zu entwickeln. Und zweitens, dass ich denke, dass sie dem Typsystem mehr Kraft verleihen und ich mag, dass Go kein sehr mächtiges Typsystem hat, da dies das Codieren von Typen entmutigt und stattdessen Programme codiert - wenn ich das Gefühl habe, dass ein Problem von a profitieren kann leistungsfähigeres Typsystem, gehe ich zu einer leistungsfähigeren Sprache über (wie Haskell oder Rust).

Davon abgesehen ist zumindest die zweite definitiv eine der bevorzugten Optionen, und selbst wenn Sie zustimmen würden, ob die Nachteile die Vorteile überwiegen, hängt auch von Ihren persönlichen Vorlieben ab. Wollte nur darauf hinweisen, dass man ohne geschlossene Summentypen keine typsicheren Summen erhalten kann, ist nicht wirklich wahr :)

[1] Insbesondere ist es nicht einfach, aber immer noch möglich , z. B. können Sie es tun

type Node interface {
    node()
}

type Foo struct {
    bar.Baz
}

func (foo) node() {}

@Merovius
Deinem zweiten Nachteil stimme ich nicht zu. Die Tatsache, dass es viele Stellen in der Standardbibliothek gibt, die immens von Summentypen profitieren würden, aber jetzt mit leeren Schnittstellen und Paniken implementiert werden, zeigt, dass dieser Mangel dem Coding schadet. Natürlich könnten die Leute sagen, dass es kein Problem gibt, da ein solcher Code überhaupt geschrieben wurde, und wir keine Summentypen brauchen, aber die Torheit dieser Logik ist, dass wir dann keinen anderen Typ für die Funktion brauchen Signaturen, und wir sollten stattdessen nur leere Schnittstellen verwenden.

Bei der Verwendung von Schnittstellen mit einer Methode zur Darstellung von Summentypen gibt es derzeit einen großen Nachteil. Sie wissen nicht, welche Typen Sie für diese Schnittstelle verwenden können, da sie implizit implementiert sind. Beim richtigen Summentyp beschreibt der Typ selbst genau, welche Typen tatsächlich verwendet werden können.

Deinem zweiten Nachteil stimme ich nicht zu.

Sind Sie mit der Aussage "Summentypen fördern das Programmieren mit Typen" nicht einverstanden oder sind Sie nicht damit einverstanden, dass dies ein Nachteil ist? Da es nicht den Anschein hat, dass Sie mit dem ersten nicht einverstanden sind (Ihr Kommentar ist im Grunde nur eine erneute Behauptung) und in Bezug auf das zweite habe ich eingeräumt, dass dies oben auf der Präferenz liegt.

Die Tatsache, dass es viele Stellen in der Standardbibliothek gibt, die immens von Summentypen profitieren würden, aber jetzt mit leeren Schnittstellen und Paniken implementiert werden, zeigt, dass dieser Mangel dem Coding schadet. Natürlich könnten die Leute sagen, dass es kein Problem gibt, da ein solcher Code überhaupt geschrieben wurde, und wir keine Summentypen brauchen, aber die Torheit dieser Logik ist, dass wir dann keinen anderen Typ für die Funktion brauchen Signaturen, und wir sollten stattdessen nur leere Schnittstellen verwenden.

Diese Art von Schwarz-Weiß-Argument hilft nicht wirklich . Ich stimme zu, dass Summentypen in einigen Fällen Schmerzen lindern würden. Jede Veränderung, die das Typensystem mächtiger macht, wird in einigen Fällen Schmerzen lindern - aber in einigen Fällen auch Schmerzen verursachen . Die Frage ist also, was überwiegt (und das ist zu einem guten Teil eine Frage der Präferenz).

Die Diskussionen sollten nicht darum gehen, ob wir ein Python-artiges Typsystem (keine Typen) oder ein coq-artiges Typsystem (Korrektheitsbeweise für alles) wollen. Die Diskussion sollte lauten: "Überwiegen die Vorteile von Summentypen ihre Nachteile" und es ist hilfreich, beides anzuerkennen.


FTR, ich möchte noch einmal betonen, dass ich persönlich nicht so im Gegensatz zu offenen Summentypen stehe (dh jeder Summentyp hat einen impliziten oder expliziten "SomethingElse"-Fall), da dies die meisten technischen Nachteile von mildern würde sie (meistens, dass sie schwer zu entwickeln sind) und bieten gleichzeitig die meisten ihrer technischen Vorteile (statische Typprüfung, die von Ihnen erwähnte Dokumentation, Sie können Typen aus anderen Paketen aufzählen…).

Ich gehe jedoch auch davon aus, dass offene Summen a) kein befriedigender Kompromiss für Leute sind, die normalerweise auf Summentypen drängen, und b) wahrscheinlich nicht als groß genug angesehen werden, um eine Aufnahme durch das Go-Team zu rechtfertigen. Aber ich wäre bereit, mich bei einer oder beiden dieser Annahmen als falsch zu belegen :)

Noch eine Frage:

Die Tatsache, dass es viele Stellen in der Standardbibliothek gibt, die von Summentypen immens profitieren würden

Ich kann mir nur zwei Stellen in der Standardbibliothek vorstellen, an denen ich sagen würde, dass sie einen signifikanten Vorteil haben: Reflect und go/ast. Und selbst dort scheinen die Pakete ohne sie gut zu funktionieren. Von diesem Bezugspunkt aus scheinen die Worte "viel" und "immens" übertrieben zu sein - aber ich sehe natürlich nicht viele legitime Orte.

database/sql/driver.Value könnte davon profitieren, ein Summentyp zu sein (wie in #23077 erwähnt).
https://godoc.corp.google.com/pkg/database/sql/driver#Value

Die öffentlichere Schnittstelle in database/sql.Rows.Scan würde jedoch nicht ohne Einbußen an Funktionalität sein. Scan kann Werte einlesen, deren zugrunde liegender Typ zB int ; die Änderung seines Zielparameters in einen Summentyp würde die Beschränkung seiner Eingaben auf eine endliche Menge von Typen erfordern.
https://godoc.corp.google.com/pkg/database/sql#Rows.Scan

@Merovius

Ich wäre nicht so im Gegensatz zu offenen Summentypen (dh jeder Summentyp hat einen impliziten oder expliziten "SomethingElse"-Fall), da dies die meisten technischen Nachteile von ihnen mildern würde (meistens, dass sie schwer zu entwickeln sind)

Es gibt mindestens zwei weitere Optionen, die das „schwer zu entwickelnde“ Problem geschlossener Summen entschärfen.

Eine besteht darin, Übereinstimmungen mit Typen zuzulassen, die nicht wirklich Teil der Summe sind. Um dann der Summe ein Mitglied hinzuzufügen, aktualisieren Sie zuerst seine Verbraucher, um sie mit dem neuen Mitglied abzugleichen, und fügen dieses Mitglied erst dann tatsächlich hinzu, wenn die Verbraucher aktualisiert wurden.

Eine andere Möglichkeit besteht darin, „unmögliche“ Member zuzulassen, d. h. Member, die in Übereinstimmungen explizit erlaubt sind, aber in tatsächlichen Werten explizit nicht erlaubt sind. Um ein Mitglied zur Summe hinzuzufügen, fügen Sie es zuerst als unmögliches Mitglied hinzu, aktualisieren dann die Verbraucher und ändern schließlich das neue Mitglied auf möglich.

database/sql/driver.Value könnte davon profitieren, ein Summentyp zu sein

Stimmt, wusste davon nichts. Vielen Dank :)

Eine besteht darin, Übereinstimmungen mit Typen zuzulassen, die nicht wirklich Teil der Summe sind. Um dann der Summe ein Mitglied hinzuzufügen, aktualisieren Sie zuerst seine Verbraucher, um sie mit dem neuen Mitglied abzugleichen, und fügen dieses Mitglied erst dann tatsächlich hinzu, wenn die Verbraucher aktualisiert wurden.

Faszinierende Lösung.

@Merovius- Schnittstellen sind im Wesentlichen eine Familie von Typen mit unendlichen Summen. Alle Summentypen, ob unendlich oder nicht, haben den Fall default: . Ohne endliche Summentypen bedeutet default: entweder einen gültigen Fall, von dem Sie nichts wussten, oder einen ungültigen Fall, der irgendwo im Programm ein Fehler ist – bei endlichen Summen ist es nur ersteres und niemals letzteres.

json.Token und die sql.Null*-Typen sind weitere kanonische Beispiele. go/types würde genauso profitieren wie go/ast. Ich vermute, es gibt viele Beispiele, die nicht in den exportierten APIs enthalten sind, bei denen es einfacher gewesen wäre, komplizierte Installationen zu debuggen und zu testen, indem die Domäne des internen Zustands eingeschränkt würde. Ich finde sie am nützlichsten für interne Zustands- und Anwendungsbeschränkungen, die in öffentlichen APIs für allgemeine Bibliotheken nicht so oft vorkommen, obwohl sie dort auch gelegentlich verwendet werden.

Persönlich denke ich, dass Summentypen Go gerade genug zusätzliche Kraft geben, aber nicht zu viel. Das Go-Typ-System ist schon sehr schön und flexibel, hat aber auch seine Schwächen. Go2-Ergänzungen zum Schriftsystem werden einfach nicht so viel Leistung liefern wie das, was bereits vorhanden ist – die 80-90% dessen, was benötigt wird, sind bereits vorhanden. Ich meine, selbst Generika würden Sie nicht grundsätzlich etwas Neues machen lassen: Sie würden Dinge, die Sie bereits tun, sicherer, einfacher, leistungsfähiger und auf eine Weise tun, die eine bessere Werkzeugausstattung ermöglicht. Summentypen sind imo ähnlich (obwohl offensichtlich die eine oder andere Generika Vorrang hätte (und sie passen ziemlich gut zusammen)).

Wenn Sie bei Schaltern vom Typ Summe einen überflüssigen Standardwert (alle Fälle + Standardwert ist zulässig) zulassen und der Compiler die Vollständigkeit nicht erzwingen lässt (obwohl ein Linter es könnte), ist das Hinzufügen eines Falls zu einer Summe genauso einfach (und genauso schwierig). ) wie das Ändern einer anderen öffentlichen API.

json.Token und die sql.Null*-Typen sind weitere kanonische Beispiele.

Token - sicher. Eine weitere Instanz des AST-Problems (im Grunde profitiert jeder Parser von Summentypen).

Ich sehe jedoch keinen Vorteil für sql.Null*. Ohne Generika (oder das Hinzufügen eines "magischen" generischen optionalen Built-Ins) müssen Sie immer noch die Typen haben und es scheint keinen signifikanten Unterschied zwischen type NullBool enum { Invalid struct{}; Value Int } und type NullBool struct { Valid bool; Value Int } . Ja, ich bin mir bewusst, dass es einen Unterschied gibt, aber er ist verschwindend gering.

Wenn Sie bei Schaltern vom Typ Summe einen überflüssigen Standardwert (alle Fälle + Standardwert ist zulässig) zulassen und der Compiler die Vollständigkeit nicht erzwingen lässt (obwohl ein Linter es könnte), ist das Hinzufügen eines Falls zu einer Summe genauso einfach (und genauso schwierig). ) wie das Ändern einer anderen öffentlichen API.

Siehe oben. Das nenne ich offene Summen, ich bin weniger dagegen.

Das nenne ich offene Summen, ich bin weniger dagegen.

Mein spezifischer Vorschlag ist https://github.com/golang/go/issues/19412#issuecomment -323208336 und ich glaube, er könnte Ihrer Definition von offen entsprechen, obwohl er immer noch ein bisschen grob ist und ich bin sicher, dass noch mehr zu tun ist entfernen und polieren. Insbesondere ist mir aufgefallen, dass nicht klar war, ob ein Default-Fall zulässig war, selbst wenn alle Fälle aufgelistet waren, also habe ich ihn einfach aktualisiert.

Einverstanden, dass optionale Typen nicht die Killer-App von Summentypen sind. Sie sind jedoch ziemlich nett und wie Sie mit Generika feststellen, die a . definieren

type Nullable(T) pick { // or whatever syntax (on all counts)
  Null struct{}
  Value T
}

einmal und alle Fälle abzudecken wäre toll. Aber wie Sie auch betonen, könnten wir dasselbe mit einem generischen Produkt (struct) machen. Es gibt den ungültigen Zustand von Valid = false, Value != 0. In diesem Szenario wäre es leicht auszurotten, wenn dies Probleme verursacht, da 2 ⨯ T klein ist, auch wenn es nicht so klein wie 1 + T ist.

Wenn es eine kompliziertere Summe mit vielen Fällen und vielen überlappenden Invarianten wäre, wird es natürlich einfacher, einen Fehler zu machen und den Fehler selbst bei defensiver Programmierung schwerer zu entdecken ziehen.

Token - sicher. Eine weitere Instanz des AST-Problems (im Grunde profitiert jeder Parser von Summentypen).

Ich schreibe viele Programme, die einige Eingaben benötigen, etwas verarbeiten und etwas ausgeben, und ich teile dies normalerweise rekursiv in viele Durchgänge auf, die die Eingabe in Fälle aufteilen und sie basierend auf diesen Fällen transformieren, während ich immer näher an die gewünschte Ausgabe. Ich schreibe vielleicht nicht buchstäblich einen Parser (zugegeben, manchmal, weil das Spaß macht!), aber ich finde das AST-Problem, wie Sie es sagen, trifft auf viel Code zu – besonders wenn es sich um abstruse Geschäftslogik handelt, die zu viel Seltsames enthält Anforderungen und Edge-Cases, die in meinen winzigen Kopf passen.

Wenn ich eine allgemeine Bibliothek schreibe, taucht sie nicht so oft in der API auf, wie wenn ich eine ETL mache oder einen fantasievollen Bericht mache oder sicherzustellen, dass Benutzer im Zustand X die Aktion Y ausführen, wenn sie nicht mit Z gekennzeichnet sind eine allgemeine Bibliothek, obwohl ich Orte finde, an denen es hilfreich wäre, den internen Zustand einzuschränken, selbst wenn es nur ein 10-minütiges Debugging auf 1 Sekunde reduziert "oh der Compiler sagte, ich liege falsch".

Insbesondere bei Go ist ein Ort, an dem ich Summentypen verwenden würde, eine Goroutine, die über eine Reihe von Kanälen auswählt, wo ich einer Goroutine 3 und einer anderen 2 Chans geben muss. Es würde mir helfen zu verfolgen, was vor sich geht, um ein chan pick { a A; b B; c C } über chan A , chan B , chan C obwohl ein chan stuct { kind MsgKind; a A; b B; c C } kann erledigen Sie die Arbeit im Handumdrehen auf Kosten von zusätzlichem Platz und weniger Validierung.

Anstelle eines neuen Typs, was ist mit der Typlistenprüfung zur Kompilierzeit als Ergänzung zur vorhandenen Schnittstellentypwechselfunktion?

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)
    }
}

Der Fairness halber sollten wir Möglichkeiten zur Approximation von Summentypen im aktuellen Typensystem untersuchen und ihre Vor- und Nachteile abwägen. Wenn nichts anderes, gibt es eine Vergleichsbasis.

Das Standardmittel ist eine Schnittstelle mit einer nicht exportierten Do-Nothing-Methode als Tag.

Ein Argument dagegen ist, dass für jeden Typ in der Summe dieses Tag definiert sein muss. Das ist nicht ganz richtig, zumindest für Mitglieder, die structs sind, könnten wir das tun

type Sum interface { sum() }
type sum struct{}
func (sum) sum() {}

und betten Sie einfach das 0-width-Tag in unsere Strukturen ein.

Wir können unserer Summe externe Typen hinzufügen, indem wir einen Wrapper einführen

type External struct {
  sum
  *pkg.SomeType
}

auch wenn das etwas ungeschickt ist.

Wenn alle Elemente in der Summe ein gemeinsames Verhalten aufweisen, können wir diese Methoden in die Schnittstellendefinition aufnehmen.

Konstrukte wie diese lassen uns sagen, dass ein Typ in einer Summe enthalten ist, aber es lässt uns nicht sagen, was nicht in dieser Summe enthalten ist. Zusätzlich zum obligatorischen nil Fall kann derselbe Einbettungs-Trick auch von externen Paketen verwendet werden wie

import "p"
var member struct {
  p.Sum
}

Innerhalb des Pakets müssen wir darauf achten, Werte zu validieren, die zwar kompiliert, aber illegal sind.

Es gibt verschiedene Möglichkeiten, die Typsicherheit zur Laufzeit wiederherzustellen. Ich habe festgestellt, dass die Definition der Summenschnittstelle eine valid() error Methode in Verbindung mit einer Funktion wie . enthält

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")
}

nützlich sein, da es die gleichzeitige Durchführung von zwei Arten der Validierung ermöglicht. Für Mitglieder, die zufällig immer gültig sind, können wir einige Boilerplates vermeiden mit

type alwaysValid struct{}
func (alwaysValid) valid() error { return nil }

Eine der häufigeren Beschwerden über dieses Muster ist, dass die Mitgliedschaft in der Summe in godoc nicht klar ist. Da es uns auch nicht zulässt, Mitglieder auszuschließen und wir trotzdem validieren müssen, gibt es einen einfachen Weg, dies zu umgehen: Exportieren Sie die Dummy-Methode.
Anstatt von,

//A Node is one of (list of types).
type Node interface { node() }

schreiben

//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()
}

Wir können niemanden davon abhalten, Node befriedigen, also können wir sie genauso gut wissen lassen, was zu tun ist. Dies macht zwar nicht auf einen Blick klar, welche Typen Node (keine zentrale Liste) erfüllen, aber es macht deutlich, ob der bestimmte Typ, den Sie jetzt betrachten, Node erfüllt.

Dieses Muster ist nützlich, wenn die meisten Typen in der Summe im selben Paket definiert sind. Wenn dies nicht der Fall ist, besteht die übliche Möglichkeit darin, auf interface{} , wie json.Token oder driver.Value . Wir könnten das vorherige Muster mit Wrapper-Typen für jeden verwenden, aber am Ende sagt es so viel wie interface{} also macht es wenig Sinn. Wenn wir erwarten, dass solche Werte von außerhalb des Pakets kommen, können wir höflich sein und eine Fabrik definieren:

//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")
}

Summen werden häufig für optionale Typen verwendet, bei denen Sie zwischen "kein Wert" und "einem Wert, der Null sein kann" unterscheiden müssen. Dazu gibt es zwei Möglichkeiten.

*T Lassen Sie uns keinen Wert als nil Zeiger und einen (möglicherweise) Nullwert als Ergebnis der Defensierung eines Nicht-Null-Zeigers angeben.

Dies erfordert wie die bisherigen schnittstellenbasierten Approximationen und die verschiedenen Vorschläge, Summentypen als Schnittstellen mit Einschränkungen zu implementieren, eine zusätzliche Pointer-Dereferenzierung und eine mögliche Heap-Zuordnung.

Für optionale Elemente kann dies mit der Technik aus dem SQL-Paket vermieden werden

type OptionalT struct {
  Valid bool
  Value T
}

Der größte Nachteil davon ist, dass es die Codierung ungültiger Zustände ermöglicht: Valid kann false sein und Value kann ungleich null sein. Es ist auch möglich, Value zu erfassen, wenn Valid false ist (obwohl dies nützlich sein kann, wenn Sie die Null T möchten, wenn sie nicht angegeben wurde). Wenn Sie Valid auf false setzen, ohne Value auf Null zu setzen, gefolgt von dem Setzen von Valid auf true (oder Ignorieren), ohne Value zuzuweisen, wird ein zuvor verworfener Wert versehentlich wieder angezeigt. Dies kann umgangen werden, indem Setter und Getter bereitgestellt werden, um die Invarianten des Typs zu schützen.

Die einfachste Form von Summentypen ist die Identität, nicht der Wert: Aufzählungen.

Der traditionelle Weg, dies in Go zu handhaben, ist const/iota:

type Enum int
const (
  A Enum = iota
  B
  C
)

Wie der Typ OptionalT dieser keine unnötigen Umwege. Wie die Schnittstellensummen schränkt es die Domäne nicht ein: Es gibt nur drei gültige Werte und viele ungültige Werte, daher müssen wir zur Laufzeit validieren. Wenn es genau zwei Werte gibt, können wir bool verwenden.

Es gibt auch das Problem der fundamentalen Zahlheit dieses Typs. A+B == C . Wir können nicht typisierte ganzzahlige Konstanten etwas zu leicht in diesen Typ umwandeln. Es gibt viele Orte, an denen das wünschenswert ist, aber wir bekommen dies auf jeden Fall. Mit etwas zusätzlichem Aufwand können wir dies auf die Identität beschränken:

type Enum struct { v int }
var (
  A = Enum{0}
  B = Enum{1}
  C = Enum{2}
)

Jetzt sind dies nur undurchsichtige Etiketten. Sie können verglichen werden, aber das war's. Leider haben wir jetzt die Konstanz verloren, aber das könnten wir mit etwas mehr Arbeit zurückbekommen:

func A() Enum { return Enum{0} }
func B() Enum { return Enum{1} }
func C() Enum { return Enum{2} }

Wir haben die Unfähigkeit eines externen Benutzers wiederhergestellt, die Namen auf Kosten einiger Boilerplates und einiger Funktionsaufrufe zu ändern, die hochgradig inlinefähig sind.

Dies ist jedoch in gewisser Weise schöner als die Schnittstellensummen, da wir den Typ fast vollständig geschlossen haben. Externer Code kann nur A() , B() oder C() . Sie können die Labels nicht wie im var-Beispiel vertauschen und sie können nicht A() + B() tun und wir können beliebige Methoden für Enum . Es wäre immer noch möglich, dass Code im selben Paket irrtümlicherweise einen Wert erstellt oder ändert, aber wenn wir darauf achten, dass dies nicht passiert, ist dies der erste Summentyp, der keinen Validierungscode erfordert: Wenn er existiert, ist er gültig .

Manchmal haben Sie viele Labels und einige von ihnen haben zusätzliche Datumsangaben und solche mit der gleichen Art von Daten. Angenommen, Sie haben einen Wert mit drei wertlosen Zuständen (A, B, C), zwei mit einem Zeichenfolgenwert (D, E) und einen mit einem Zeichenfolgenwert und einem int-Wert (F). Wir könnten eine Reihe von Kombinationen der oben genannten Taktiken verwenden, aber der einfachste Weg ist

type Value struct {
  Which int // could have consts for A, B, C, D, E, F
  String string
  Int int
}

Dies ist dem obigen Typ OptionalT sehr ähnlich, aber anstelle eines bool hat er eine Aufzählung und es gibt mehrere Felder, die abhängig vom Wert von Which gesetzt werden können (oder nicht). Bei der Validierung muss darauf geachtet werden, dass diese richtig eingestellt sind (oder nicht).

Es gibt viele Möglichkeiten, "eine der folgenden Aussagen" in Go auszudrücken. Manche brauchen mehr Pflege als andere. Sie erfordern oft die Validierung der Invariante "eins von" zur Laufzeit oder überflüssige Dereferenzierungen. Ein großer Nachteil, den sie alle teilen, ist, dass, da sie in der Sprache simuliert werden, anstatt ein Teil der Sprache zu sein, die "eine von"-Invariante nicht in Reflect oder go/types auftaucht, was die Metaprogrammierung erschwert Sie. Um sie in der Metaprogrammierung zu verwenden, müssen Sie beide in der Lage sein, die richtige Form der Summe zu erkennen und zu validieren, und Sie müssen wissen, dass Sie genau danach suchen, da sie alle sehr nach gültigem Code ohne die Invariante "eins von" aussehen.

Wenn Summentypen ein Teil der Sprache wären, könnten sie reflektiert und leicht aus dem Quellcode herausgezogen werden, was zu besseren Bibliotheken und Tools führt. Der Compiler könnte eine Reihe von Optimierungen vornehmen, wenn er sich dieser "Eins-von"-Invariante bewusst wäre. Programmierer könnten sich auf den wichtigen Validierungscode konzentrieren, anstatt sich auf die triviale Wartung zu konzentrieren, um zu überprüfen, ob ein Wert tatsächlich in der richtigen Domäne ist.

Konstrukte wie diese lassen uns sagen, dass ein Typ in einer Summe enthalten ist, aber es lässt uns nicht sagen, was nicht in dieser Summe enthalten ist. Zusätzlich zum obligatorischen nil-Fall kann derselbe Einbettungs-Trick von externen Paketen wie verwendet werden
[…]
Innerhalb des Pakets müssen wir darauf achten, Werte zu validieren, die zwar kompiliert, aber illegal sind.

Wieso den? Als Paketautor scheint mir dies fest im Bereich "Ihres Problems" zu liegen. Wenn Sie mir ein io.Reader , dessen Read Methode Panik hat, werde ich mich nicht davon erholen und es einfach in Panik geraten lassen. Ebenso, wenn Sie sich alle Mühe geben, einen ungültigen Wert eines von mir deklarierten Typs zu erstellen - wer bin ich, um mit Ihnen zu streiten? Dh ich halte "Ich habe eine emulierte geschlossene Summe eingebettet" ein Problem, das selten (wenn überhaupt) zufällig auftaucht.

Davon abgesehen können Sie dieses Problem vermeiden, indem Sie die Schnittstelle auf type Sum interface { sum() Sum } ändern und jeden Wert sich selbst zurückgeben lassen. Auf diese Weise können Sie einfach die Rückgabe von sum() , die sich auch unter Einbettung gut verhält.

Eine der häufigeren Beschwerden über dieses Muster ist, dass die Mitgliedschaft in der Summe in godoc nicht klar ist.

Dies kann Ihnen helfen .

Der größte Nachteil davon ist, dass es die Codierung ungültiger Zustände ermöglicht: Valid kann false sein und Value kann ungleich null sein.

Das ist für mich kein ungültiger Zustand. Nullwerte sind nicht magisch. Es gibt keinen Unterschied, IMO, zwischen sql.NullInt64{false,0} und NullInt64{false,42} . Beide sind gültige und äquivalente Darstellungen einer SQL-NULL. Wenn der gesamte Code vor der Verwendung von Value Gültig prüft, ist der Unterschied für ein Programm nicht erkennbar.

Es ist eine faire und richtige Kritik, dass der Compiler diese Überprüfung nicht erzwingt (was er wahrscheinlich für "echte" optionale / Summentypen tun würde), was es einfacher macht, dies nicht zu tun. Aber wenn Sie es vergessen, würde ich es nicht für besser halten, versehentlich einen Nullwert zu verwenden, als versehentlich einen Wert ungleich Null zu verwenden (mit Ausnahme von zeigerförmigen Typen, da sie bei der Verwendung in Panik geraten würden, also laut versagen - aber für diese sollten Sie sowieso einfach den bloßen zeigerförmigen Typ verwenden und nil als "unset" verwenden).

Es gibt auch das Problem der fundamentalen Zahlheit dieses Typs. A+B == C. Untypisierte ganzzahlige Konstanten können wir etwas zu leicht in diesen Typ umwandeln.

Ist das ein theoretisches Problem oder ist es in der Praxis aufgetreten?

Programmierer könnten sich auf den wichtigen Validierungscode konzentrieren, anstatt sich auf die triviale Wartung zu konzentrieren, um zu überprüfen, ob ein Wert tatsächlich in der richtigen Domäne ist.

Nur FTR, in den Fällen, in denen ich Summe-Typen-als-Summen-Typen verwende (dh das Problem kann nicht eleganter über Golden-Varieté-Schnittstellen modelliert werden), schreibe ich niemals Validierungscode. Genauso wie ich nicht auf Null von Zeigern überprüfe, die als Empfänger oder Argumente übergeben wurden (es sei denn, es ist als gültige Variante dokumentiert). An den Stellen, an denen der Compiler mich dazu zwingt, damit umzugehen (zB Probleme mit dem Stil "keine Rückkehr am Ende der Funktion"), gerate ich im Standardfall in Panik.

Persönlich halte ich Go für eine pragmatische Sprache, die nicht nur um ihrer selbst willen Sicherheitsmerkmale hinzufügt oder weil "jeder weiß, dass sie besser sind", sondern basierend auf einem nachgewiesenen Bedarf. Ich denke, es auf pragmatische Weise zu verwenden, ist daher in Ordnung.

Das Standardmittel ist eine Schnittstelle mit einer nicht exportierten Do-Nothing-Methode als Tag.

Es gibt einen grundlegenden Unterschied zwischen Schnittstellen und Summentypen (ich habe ihn in Ihrem Beitrag nicht erwähnt gesehen). Wenn Sie einen Summentyp über eine Schnittstelle approximieren, gibt es wirklich keine Möglichkeit, den Wert zu handhaben. Als Verbraucher haben Sie keine Ahnung, was es tatsächlich enthält, und können nur raten. Das ist nicht besser, als nur eine leere Schnittstelle zu verwenden. Es ist nur nützlich, wenn eine Implementierung nur aus demselben Paket stammen kann, das die Schnittstelle definiert, da Sie nur dann steuern können, was Sie erhalten können.

Auf der anderen Seite haben Sie so etwas wie:

func foo(val string|int|error) {
    switch v:= val.(type) {
    case string:
        ...
    }
}

Gibt dem Verbraucher die volle Macht, den Wert des Summentyps zu verwenden. Ihr Wert ist konkret, nicht interpretierbar.

@Merovius
Diese von Ihnen erwähnten "offenen Summen" haben, was einige Leute als erheblichen Nachteil einstufen könnten, insofern, als sie es ermöglichen würden, sie für "Merkmalskriechen" zu missbrauchen. Genau aus diesem Grund wurden optionale Funktionsargumente als Feature abgelehnt.

Diese von Ihnen erwähnten "offenen Summen" haben, was einige Leute als erheblichen Nachteil einstufen könnten, insofern, als sie es ermöglichen würden, sie für "Merkmalskriechen" zu missbrauchen. Genau aus diesem Grund wurden optionale Funktionsargumente als Feature abgelehnt.

Das scheint mir ein ziemlich schwaches Argument zu sein - wenn nichts anderes, dann, weil sie existieren, so dass Sie bereits alles erlauben, was sie ermöglichen. Tatsächlich haben wir für alle Absichten und Zwecke bereits optionale Argumente (nicht, dass ich dieses Muster mag , aber es ist eindeutig in der Sprache möglich).

Es gibt einen grundlegenden Unterschied zwischen Schnittstellen und Summentypen (ich habe ihn in Ihrem Beitrag nicht erwähnt gesehen). Wenn Sie einen Summentyp über eine Schnittstelle approximieren, gibt es wirklich keine Möglichkeit, den Wert zu handhaben. Als Verbraucher haben Sie keine Ahnung, was es tatsächlich enthält, und können nur raten.

Ich habe versucht, dies ein zweites Mal zu analysieren und kann immer noch nicht. Warum kannst du sie nicht benutzen? Sie können reguläre, exportierte Typen sein. Ja, es müssen (natürlich) Typen sein, die in Ihrem Paket erstellt wurden, aber abgesehen davon scheint es keine Einschränkung in der Verwendung zu geben, verglichen mit tatsächlichen, geschlossenen Summen.

Ich habe versucht, dies ein zweites Mal zu analysieren und kann immer noch nicht. Warum kannst du sie nicht benutzen? Sie können reguläre, exportierte Typen sein. Ja, es müssen (natürlich) Typen sein, die in Ihrem Paket erstellt wurden, aber abgesehen davon scheint es keine Einschränkung in der Verwendung zu geben, verglichen mit tatsächlichen, geschlossenen Summen.

Was passiert, wenn die Dummy-Methode exportiert wird und jeder Dritte den "Summentyp" implementieren kann? Oder das ziemlich realistische Szenario, in dem ein Teammitglied mit den verschiedenen Benutzern der Schnittstelle nicht vertraut ist, beschließt, eine weitere Implementierung im selben Paket hinzuzufügen, und eine Instanz dieser Implementierung über verschiedene Arten des Codes an diese Benutzer weitergegeben wird? Auf die Gefahr hin, meine scheinbare "unparseable" Aussage zu wiederholen: "Als Verbraucher haben Sie keine Ahnung, was [der Summenwert] tatsächlich enthält, und können nur raten.". Wissen Sie, da es sich um eine Schnittstelle handelt, die Ihnen nicht sagt, wer sie implementiert.

@Merovius

Nur FTR, in den Fällen, in denen ich Summe-Typen-als-Summen-Typen verwende (dh das Problem kann nicht eleganter über Golden-Varieté-Schnittstellen modelliert werden), schreibe ich niemals Validierungscode. Genauso wie ich nicht auf Null von Zeigern überprüfe, die als Empfänger oder Argumente übergeben wurden (es sei denn, es ist als gültige Variante dokumentiert). An den Stellen, an denen der Compiler mich dazu zwingt, damit umzugehen (zB Probleme mit dem Stil "keine Rückkehr am Ende der Funktion"), gerate ich im Standardfall in Panik.

Ich behandle das nicht als immer oder nie .

Wenn jemand, der eine schlechte Eingabe übergibt, sofort explodieren würde, kümmere ich mich nicht um Validierungscode.

Aber wenn jemand, der eine schlechte Eingabe übergibt, irgendwann eine Panik auslösen könnte, die aber eine Weile nicht auftaucht, dann schreibe ich Validierungscode, damit die schlechte Eingabe so schnell wie möglich markiert wird und niemand herausfinden muss, dass der Fehler eingeführt wurde 150 Frames in der Aufrufliste nach oben (vor allem, da sie dann möglicherweise weitere 150 Frames in der Aufrufliste nach oben gehen müssen, um herauszufinden, wo dieser falsche Wert eingeführt wurde).

Es ist pragmatisch, jetzt eine halbe Minute zu verbringen, um später möglicherweise eine halbe Stunde Debugging zu sparen. Vor allem für mich, da ich die ganze Zeit dumme Fehler mache und je früher ich geschult werde, desto eher kann ich den nächsten dummen Fehler machen.

Wenn ich eine Funktion habe, die einen Reader nimmt und sofort verwendet, überprüfe ich nicht auf nil, aber wenn die Funktion eine Fabrik für eine Struktur ist, die den Reader erst aufruft, wenn eine bestimmte Methode aufgerufen wird, werde ich Überprüfen Sie es auf Null und Panik oder geben Sie einen Fehler mit etwas wie "Reader darf nicht Null" zurück, damit die Fehlerursache so nah wie möglich an der Fehlerquelle liegt.

godoc -Analyse

Ich bin mir bewusst, aber ich finde es nicht nützlich. Es lief 40 Minuten auf meinem Arbeitsbereich, bevor ich ^C drücke, und das muss jedes Mal aktualisiert werden, wenn ein Paket installiert oder geändert wird. Es gibt jedoch #20131 (abgezweigt aus diesem Thread!).

Davon abgesehen können Sie dieses Problem vermeiden, indem Sie die Schnittstelle auf type Sum interface { sum() Sum } ändern und jeden Wert sich selbst zurückgeben lassen. Auf diese Weise können Sie einfach die Rückgabe von sum() , die sich auch unter Einbettung gut verhält.

Das fand ich nicht so nützlich. Es bietet nicht mehr Vorteile als eine explizite Validierung und bietet weniger Validierung.

Ist [dass Sie Mitglieder einer const/iota-Aufzählung hinzufügen können] ein theoretisches Problem oder ist es in der Praxis aufgetreten?

Dieser spezielle war theoretisch: Ich versuchte, alle Vor- und Nachteile aufzulisten, die mir einfielen, theoretisch und praktisch. Mein wichtiger Punkt war jedoch, dass es viele Möglichkeiten gab, die Invariante "eins von" in der Sprache auszudrücken, die ziemlich häufig verwendet wird, aber keine so einfach ist, wie nur eine Art Typ in der Sprache zu sein.

Ist [die Tatsache, dass Sie einer const/iota-Aufzählung ein untypisiertes Integral zuordnen können] ein theoretisches Problem oder ist es in der Praxis aufgetreten?

Das hat sich in der Praxis herausgestellt. Es dauerte nicht lange, um herauszufinden, was schief gelaufen ist, aber es hätte noch weniger Zeit gedauert, wenn der Compiler gesagt hätte "da, diese Zeile - das ist die falsche". Es wird von anderen Möglichkeiten gesprochen, diesen speziellen Fall zu handhaben, aber ich sehe nicht, wie sie von allgemeinem Nutzen sein könnten.

Das ist für mich kein ungültiger Zustand. Nullwerte sind nicht magisch. Es gibt keinen Unterschied, IMO, zwischen sql.NullInt64{false,0} und NullInt64{false,42} . Beide sind gültige und äquivalente Darstellungen einer SQL-NULL. Wenn der gesamte Code vor der Verwendung von Value Gültig prüft, ist der Unterschied für ein Programm nicht erkennbar.

Es ist eine faire und richtige Kritik, dass der Compiler diese Überprüfung nicht erzwingt (was er wahrscheinlich für "echte" optionale / Summentypen tun würde), was es einfacher macht, dies nicht zu tun. Aber wenn Sie es vergessen, würde ich es nicht für besser halten, versehentlich einen Nullwert zu verwenden, als versehentlich einen Wert ungleich Null zu verwenden (mit Ausnahme von zeigerförmigen Typen, da sie bei der Verwendung in Panik geraten würden, also laut versagen - aber für diese sollten Sie sowieso nur den nackten zeigerförmigen Typ verwenden und nil als "unset" verwenden).

Dass "Wenn der gesamte Code gültig überprüft, bevor Value verwendet wird" ist der Ort, an dem die Fehler einschleichen und was der Compiler erzwingen könnte. Ich hatte solche Fehler (wenn auch mit größeren Versionen dieses Musters, bei denen es mehr als ein Wertfeld und mehr als zwei Zustände für den Diskriminator gab). Ich glaube/hoffe, ich habe all dies während der Entwicklung und des Testens gefunden und keiner ist in die Wildnis entkommen, aber es wäre schön, wenn der Compiler mir einfach hätte sagen können, wann ich diesen Fehler gemacht habe und ich sicher sein könnte, dass dies der einzige Weg ist Vorbei gerutscht war, wenn ein Fehler im Compiler aufgetreten war, genauso wie es mir angezeigt wurde, wenn ich versuchte, einer Variablen vom Typ int einen String zuzuweisen.

Und natürlich bevorzuge ich *T für optionale Typen, obwohl damit Kosten verbunden sind, die nicht Null sind, sowohl in Bezug auf die Ausführungszeit als auch in Bezug auf die Lesbarkeit des Codes.

(Für dieses spezielle Beispiel wäre der Code, um den tatsächlichen Wert oder den korrekten Nullwert mit dem Auswahlvorschlag zu erhalten, v, _ := nullable.[Value] was prägnant und sicher ist.)

Das ist ganz und gar nicht das, was ich will. Pick-Typen sollten Werttypen sein,
wie in Rust. Ihr erstes Wort sollte bei Bedarf ein Verweis auf GC-Metadaten sein.

Andernfalls ist ihre Verwendung mit einer Leistungseinbuße verbunden, die möglicherweise
inakzeptabel. Für mich ist der Pass 10:41 Uhr "Josh Bleecher Snyder" <
[email protected]> schrieb:

Mit dem Auswahlvorschlag können Sie wählen, ob Sie ap oder *p haben möchten, um mehr zu erhalten
größere Kontrolle über Speicherabwägungen.

Der Grund, warum Schnittstellen skalare Werte speichern, ist, dass Sie dies nicht tun
muss ein Typwort lesen, um zu entscheiden, ob das andere Wort a . ist
Zeiger; siehe #8405 https://github.com/golang/go/issues/8405 für
Diskussion. Die gleichen Implementierungsüberlegungen würden wahrscheinlich für a . gelten
Pick-Typ, was in der Praxis bedeuten könnte, dass p am Ende zuordnen und sein
sowieso nicht lokal.


Sie erhalten dies, weil Sie den Thread verfasst haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/19412#issuecomment-323371837 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AGGWB-wQD75N44TGoU6LWQhjED_uhKGUks5sZaKbgaJpZM4MTmSr
.

@urandom

Was passiert, wenn die Dummy-Methode exportiert wird und jeder Dritte den "Summentyp" implementieren kann?

Es gibt einen Unterschied zwischen der exportierten Methode und dem exportierten Typ. Wir scheinen aneinander vorbei zu reden. Für mich scheint das ganz gut zu funktionieren, ohne Unterschied zwischen offenen und geschlossenen Summen:

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 }

Es ist keine Erweiterung außerhalb des Pakets möglich, aber die Verbraucher des Pakets können die Werte wie alle anderen verwenden, erstellen und weitergeben.

Sie können X oder einen der lokalen Typen, die es erfüllen, extern einbetten und dann an eine Funktion in Ihrem Paket übergeben, die ein X akzeptiert.

Wenn diese Funktion x aufruft, gerät sie entweder in Panik (wenn X selbst eingebettet und auf nichts gesetzt wurde) oder gibt einen Wert zurück, mit dem Ihr Code arbeiten kann – aber es ist nicht das, was vom Aufrufer übergeben wurde, was für den Aufrufer etwas überraschend wäre (und ihr Code ist bereits verdächtig, wenn sie so etwas versuchen, weil sie die Dokumente nicht gelesen haben).

Das Aufrufen eines Validators, der mit einer "Do not do that"-Meldung in Panik gerät, scheint die am wenigsten überraschende Möglichkeit zu sein, damit umzugehen, und ermöglicht dem Aufrufer, seinen Code zu korrigieren.

Wenn diese Funktion x aufruft, gerät sie entweder in Panik […] oder gibt einen Wert zurück, mit dem Ihr Code arbeiten kann – aber es ist nicht das, was vom Aufrufer übergeben wurde, was für den Aufrufer etwas überraschend wäre

Wie ich oben sagte: Wenn Sie überrascht sind, dass Ihre absichtliche Konstruktion eines ungültigen Wertes ungültig ist, müssen Sie Ihre Erwartungen überdenken. Aber darum ging es bei dieser speziellen Diskussion jedenfalls nicht, und es wäre hilfreich, getrennte Argumente zu trennen. In diesem ging es darum, dass @urandom sagte, dass offene Summen über Schnittstellen mit Tag-Methoden nicht introspektiv oder von anderen Paketen verwendbar wären. Das finde ich eine fragwürdige Behauptung, es wäre toll, wenn das geklärt werden könnte.

Das Problem ist, dass jemand einen Typ erstellen kann, der nicht in der Summe enthalten ist, die kompiliert wird und an Ihr Paket übergeben werden kann.

Ohne der Sprache die richtigen Summentypen hinzuzufügen, gibt es drei Möglichkeiten, damit umzugehen

  1. ignoriere die Situation
  2. einen Fehler validieren und in Panik versetzen/zurückgeben
  3. Versuchen Sie, "zu tun, was Sie meinen", indem Sie den eingebetteten Wert implizit extrahieren und verwenden

3 scheint mir eine seltsame Mischung aus 1 und 2 zu sein: Ich sehe nicht, was es kauft.

Ich stimme zu, dass "Wenn Sie überrascht sind, dass Ihre absichtliche Konstruktion eines ungültigen Wertes ungültig ist, müssen Sie Ihre Erwartungen überdenken", aber bei 3 kann es sehr schwer sein zu bemerken, dass etwas schief gelaufen ist und selbst wenn Sie es tun es wäre schwer herauszufinden, warum.

2 scheint am besten zu sein, weil es sowohl den Code davor schützt, in einen ungültigen Zustand zu verfallen, als auch ein Aufflackern aussendet, wenn jemand es vermasselt, ihm mitzuteilen, warum er falsch liegt und wie man ihn korrigiert.

Verstehe ich die Absicht des Musters falsch oder nähern wir uns dem nur aus unterschiedlichen Philosophien?

@urandom Ich würde mich auch über eine Klarstellung

Das Problem ist, dass jemand einen Typ erstellen kann, der nicht in der Summe enthalten ist, die kompiliert wird und an Ihr Paket übergeben werden kann.

Sie können das immer tun; Im Zweifelsfall können Sie auch bei vom Compiler überprüften Summentypen immer unsicher verwenden (und ich sehe das nicht als eine qualitativ andere Möglichkeit, ungültige Werte zu konstruieren, als etwas einzubetten, das eindeutig als Summe gedacht ist, und es nicht mit a zu initialisieren gültiger Wert). Die Frage ist, "wie oft wird dies in der Praxis ein Problem darstellen und wie schwerwiegend wird dieses Problem sein". Meiner Meinung nach lautet die Antwort bei der Lösung von oben "ziemlich nie und sehr niedrig" - Sie sind anscheinend anderer Meinung, was in Ordnung ist. Aber so oder so scheint es nicht viel Sinn zu machen, daran zu arbeiten - die Argumente und Ansichten auf beiden Seiten dieses speziellen Punktes sollten ausreichend klar sein und ich versuche, zu viele laute Wiederholungen zu vermeiden und mich auf das Wirkliche zu konzentrieren neue Argumente. Ich habe die obige Konstruktion angesprochen, um zu zeigen, dass es keinen Unterschied in der Exportierbarkeit zwischen erstklassigen Summentypen und emulierten Summen über Schnittstellen gibt. Um nicht zu zeigen, dass sie in jeder Hinsicht besser sind.

Im Zweifelsfall können Sie auch bei vom Compiler überprüften Summentypen immer unsicher verwenden (und ich sehe das nicht als eine qualitativ andere Möglichkeit, ungültige Werte zu konstruieren, als etwas einzubetten, das eindeutig als Summe gedacht ist, und es nicht mit a zu initialisieren gültiger Wert).

Ich denke, es ist qualitativ anders: Wenn Leute das Einbetten auf diese Weise missbrauchen (zumindest mit proto.Message und den konkreten Typen, die es implementieren), denken sie im Allgemeinen nicht darüber nach, ob es sicher ist und welche Invarianten es brechen könnte . (Benutzer gehen davon aus, dass Schnittstellen das erforderliche Verhalten vollständig beschreiben, aber wenn Schnittstellen als Unions- oder Summentypen verwendet werden, tun sie dies oft nicht. Siehe auch https://github.com/golang/protobuf/issues/364.)

Im Gegensatz dazu, wenn jemand das Paket unsafe , um eine Variable auf einen Typ zu setzen, auf den sie normalerweise nicht verweisen kann, behauptet er mehr oder weniger explizit, zumindest darüber nachgedacht zu haben, was er kaputt machen könnte und warum.

@Merovius Vielleicht war ich mir unklar: Die Tatsache, dass der Compiler jemandem mitteilen würde, dass er eine falsche Einbettung verwendet hat, ist eher ein netter Nebenvorteil.

Der größte Vorteil des Sicherheitsmerkmals besteht darin, dass es durch Reflect honoriert und in go/types dargestellt wird. Dadurch erhalten Werkzeuge und Bibliotheken mehr Informationen, mit denen Sie arbeiten können. Es gibt viele Möglichkeiten, Summentypen in Go zu simulieren, aber sie sind alle identisch mit Nicht-Summen-Typ-Code, daher benötigen Werkzeuge und Bibliothek Informationen außerhalb des Bandes, um zu wissen, dass es sich um einen Summentyp handelt und in der Lage sein muss, das spezifische Muster zu erkennen verwendet werden, aber selbst diese Muster erlauben erhebliche Variationen.

Es würde auch die einzige Möglichkeit unsicher machen, einen ungültigen Wert zu erstellen: Jetzt haben Sie regulären Code, generierten Code und Reflect – die beiden letzteren verursachen eher ein Problem, da sie im Gegensatz zu einer Person die Dokumentation nicht lesen können.

Ein weiterer Nebenvorteil der Sicherheit bedeutet, dass der Compiler mehr Informationen hat und besseren, schnelleren Code generieren kann.

Es gibt auch die Tatsache, dass Sie die Pseudosumme nicht nur durch Schnittstellen ersetzen können, sondern auch die Pseudosumme "einen dieser regulären Typen" wie json.Token oder driver.Value ersetzen können. Das sind rar gesät, aber es wäre ein Ort weniger, wo interface{} notwendig ist.

Es würde auch die einzige Möglichkeit unsicher machen, einen ungültigen Wert zu erstellen

Ich glaube nicht, dass ich die Definition von "ungültiger Wert" verstehe, die zu dieser Aussage führt.

@neild wenn du hättest

var v pick {
  None struct{}
  A struct { X int; Y *T}
  B int
}

es würde im Gedächtnis abgelegt werden wie

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
}

und mit unsafe könnten Sie thePtr selbst dann setzen, wenn activeField 0 oder 2 ist, oder einen Wert von theInt selbst wenn activeField 0 ist.

In jedem Fall würde dies die Annahmen des Compilers entkräften und die gleichen theoretischen Fehler ermöglichen, die wir heute haben können.

Aber wie @bcmills darauf hingewiesen hat, dass Sie, wenn Sie unsicher sind, besser wissen, was Sie tun, da es sich um die nukleare Option handelt.

Was ich nicht verstehe, ist, warum unsicher die einzige Möglichkeit ist, einen ungültigen Wert zu erstellen.

var t time.Timer

t ist ein ungültiger Wert; t.C ist nicht aktiviert, ein Anruf von t.Stop gerät in Panik usw. Keine unsicheren Bedingungen erforderlich.

Einige Sprachen haben Typsysteme, die große Anstrengungen unternehmen, um die Erzeugung "ungültiger" Werte zu verhindern. Go gehört nicht dazu. Ich sehe nicht, wie Gewerkschaften diese Nadel signifikant bewegen. (Es gibt natürlich noch andere Gründe, Gewerkschaften zu unterstützen.)

@neild ja Entschuldigung, ich bin mit meinen Definitionen locker.

Ich hätte in Bezug auf die Invarianten des Summentyps ungültig sagen sollen.

Die einzelnen Typen in der Summe können natürlich in einem ungültigen Zustand sein.

Die Beibehaltung der Summentyp-Invarianten bedeutet jedoch, dass sie sowohl für Reflect and go/types als auch für den Programmierer zugänglich sind

@jimmyfrasche , ich sage, dass im Gegensatz zu einem insofern undurchsichtig ist, als Sie nicht wissen oder zumindest nicht verwenden können, was die Liste der Typen ist die die Schnittstelle implementieren sind. Dies macht das Schreiben des switch Teils des Codes zu einem Rätselraten:

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")
    }
}

Es scheint mir also, dass die meisten Probleme, die die Leute mit der schnittstellenbasierten Emulation des Summentyps haben, durch Maut und / oder Konvention gelöst werden können. ZB wenn eine Schnittstelle eine nicht exportierte Methode enthält, wäre es trivial, alle möglichen (ja, absichtliche Umgehungen) Implementierungen herauszufinden. Um die meisten Probleme mit iota-basierten Aufzählungen zu lösen, würde eine einfache Konvention von "eine Aufzählung ist ein type Foo int mit einer Deklaration der Form const ( FooA Foo = iota; FooB; FooC ) " ermöglichen, umfangreiche und präzise Werkzeuge zu schreiben auch für sie.

Ja, das entspricht nicht den tatsächlichen Summentypen (unter anderem würden sie keinen erstklassigen Reflect-Support bekommen, obwohl ich sowieso nicht wirklich verstehe, wie wichtig das wäre), aber es bedeutet, dass die bestehenden Lösungen erscheinen, aus meiner Sicht, besser, als sie oft gemalt werden. Und IMO wäre es wert, diesen Designraum zu erkunden, bevor man sie tatsächlich in Go 2 einsetzt – zumindest wenn sie für die Menschen wirklich so wichtig sind.

(Und ich möchte noch einmal betonen, dass ich mir der Vorteile von Summentypen bewusst bin, also brauche ich sie nicht zu meinem Vorteil umzuformulieren. Ich gewichte sie nur nicht so schwer wie andere Leute, sehe auch die Nachteile und somit zu unterschiedlichen Schlussfolgerungen bei den gleichen Daten kommen)

@Merovius das ist eine gute Position.

Die Reflect-Unterstützung würde es Bibliotheken sowie Offline-Tools – Linters, Code-Generatoren usw. – ermöglichen, auf die Informationen zuzugreifen und sie daran zu hindern, sie unangemessen zu ändern, was statisch nicht mit jeder Genauigkeit erkannt werden kann.

Unabhängig davon ist es eine gute Idee, es zu erkunden, also lassen Sie es uns erkunden.

Um die häufigsten Pseudosummenfamilien in Go zusammenzufassen: (ungefähr in der Reihenfolge des Vorkommens)

  • const/iota enum.
  • Schnittstelle mit Tag-Methode für Summe über Typen, die im selben Paket definiert sind.
  • *T für ein optionales T
  • struct mit einer Aufzählung, deren Wert bestimmt, welche Felder gesetzt werden können (wenn die Aufzählung ein Bool ist und es nur ein anderes Feld gibt, ist dies eine andere Art von optionalem T )
  • interface{} , die auf eine Grab-Bag mit einer endlichen Menge von Typen beschränkt ist.

Alle diese können sowohl für Summentypen als auch für Nicht-Summentypen verwendet werden. Die ersten beiden werden so selten für etwas anderes verwendet, dass es sinnvoll sein könnte, einfach davon auszugehen, dass sie Summentypen darstellen und gelegentlich falsch positive Ergebnisse akzeptieren. Für Schnittstellensummen könnte es auf nicht exportierte Methoden ohne Parameter oder Rückgaben und ohne Hauptteil für Member beschränkt werden. Bei Aufzählungen wäre es sinnvoll, sie nur zu erkennen, wenn sie nur Type = iota damit sie nicht ausgelöst werden, wenn iota als Teil eines Ausdrucks verwendet wird.

*T für ein optionales T wäre wirklich schwer von einem normalen Zeiger zu unterscheiden. Dies könnte die Konvention type O = *T . Das wäre möglich zu erkennen, wenn auch etwas schwierig, da der Aliasname nicht Teil des Typs ist. type O *T wäre einfacher zu erkennen, aber schwieriger im Code zu arbeiten. Auf der anderen Seite ist alles, was getan werden muss, im Wesentlichen in den Typ integriert, so dass es für die Werkzeuge wenig zu gewinnen ist, dies zu erkennen. Lassen Sie uns diesen einfach ignorieren. (Generika würden wahrscheinlich etwas in der Art von type Optional(T) *T zulassen, was das "Taggen" dieser Dateien vereinfachen würde).

Die Struktur mit einer Aufzählung wäre in Werkzeugen schwer nachvollziehbar. Welche Felder passen zu welchem ​​Wert für die Aufzählung? Wir könnten dies auf die Konvention vereinfachen, dass es ein Feld pro Member in der Aufzählung geben muss und dass der Aufzählungswert und der Feldwert gleich sein müssen, zum Beispiel:

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 } 
}

Das würde keine optionalen Typen erhalten, aber wir könnten in der Erkennung den Sonderfall "2 Felder, zuerst ist bool" verwenden.

Die Verwendung eines interface{} für eine Grab-Bag-Summe wäre ohne einen magischen Kommentar wie //gosum: int, float64, string, Foo impossible unmöglich zu erkennen

Alternativ könnte es ein spezielles Paket mit den folgenden Definitionen geben:

package sum
type (
  Type struct{}
  Enum int
  OneOf interface{}
)

und Enumerationen nur erkennen, wenn sie die Form type MyEnum sum.Enum , Interfaces und Structs nur erkennen, wenn sie sum.Type einbetten, und nur interface{} Grabbags wie type GrabBag sum.OneOf (aber das würde immer noch einen maschinenerkennbaren Kommentar benötigen, um seine Kommentare zu erklären). Das hätte folgende Vor- und Nachteile:
Vorteile

  • explizit im Code: Wenn es so markiert ist, handelt es sich zu 100 % um einen Summentyp, keine Fehlalarme.
  • Diese Definitionen könnten eine Dokumentation enthalten, die erklärt, was sie bedeuten, und die Paketdokumentation könnte auf Tools verweisen, die mit diesen Typen verwendet werden können
  • einige hätten eine gewisse Sichtbarkeit in Reflect
    Nachteile
  • Viele falsche Negative aus altem Code und der stdlib (die sie nicht verwenden würde).
  • Sie müssten verwendet werden, um nützlich zu sein, so dass die Einführung langsam wäre und wahrscheinlich nie 100% erreicht, und die Wirksamkeit von Tools, die dieses spezielle Paket erkennen, wäre eine Funktion der Einführung, die zwar experimentell, aber wahrscheinlich unrealistisch ist.

Unabhängig davon, welche dieser beiden Methoden zur Identifizierung von Summentypen verwendet wird, nehmen wir an, dass sie erkannt wurden, und verwenden diese Informationen, um zu sehen, welche Art von Werkzeugen wir erstellen können.

Wir können Werkzeuge grob in generativ (wie Stringer) und introspektiv (wie Golint) einteilen.

Der einfachste generative Code wäre ein Werkzeug zum Auffüllen einer switch-Anweisung mit fehlenden Fällen. Dies könnte von Redakteuren verwendet werden. Sobald ein Summentyp als Summentyp identifiziert wurde, ist dies trivial (ein bisschen mühsam, aber die eigentliche Generierungslogik wird mit oder ohne Sprachunterstützung dieselbe sein).

In allen Fällen wäre es möglich, eine Funktion zu generieren, die die Invariante "eins von" validiert.

Für Enums könnte es mehr Tools wie Stringer geben. In https://github.com/golang/go/issues/19814#issuecomment -291002852 habe ich einige Möglichkeiten erwähnt.

Das größte generative Werkzeug ist der Compiler, der mit diesen Informationen besseren Maschinencode erzeugen könnte, aber naja.

Andere fallen mir im Moment nicht ein. Steht jemand auf der Wunschliste?

Für die Introspektion ist der offensichtliche Kandidat die Linting-Erschöpfung. Ohne Sprachunterstützung sind eigentlich zwei verschiedene Arten von Linting erforderlich

  1. Sicherstellen, dass alle möglichen Zustände behandelt werden
  2. Sicherstellen, dass keine ungültigen Zustände erstellt werden (was die Arbeit von 1 ungültig machen würde)

1 ist trivial, aber es würde alle möglichen Zustände und einen Standardfall erfordern, da 2 nicht zu 100% verifiziert werden kann (auch wenn wir unsicher sind) und Sie nicht erwarten können, dass der gesamte Code, der Ihren Code verwendet, sowieso diesen Linter ausführt.

2 konnte nicht wirklich Werten durch Reflect folgen oder den gesamten Code identifizieren, der einen ungültigen Zustand für die Summe erzeugen könnte, aber es könnte viele einfache Fehler abfangen, wie wenn Sie einen Summentyp einbetten und dann eine Funktion damit aufrufen, könnte es heißen: "Sie haben pkg.F(v) geschrieben, aber Sie meinten pkg.F(v.EmbeddedField)" oder "Sie haben 2 an pkg.F übergeben, verwenden Sie pkg.B". Für die Struktur konnte es nicht viel tun, um die Invariante zu erzwingen, dass jeweils ein Feld gesetzt wird, außer in wirklich offensichtlichen Fällen wie "Sie schalten Welche ein und im Fall X setzen Sie das Feld F auf einen Wert ungleich Null". ". Es könnte darauf bestehen, dass Sie die generierte Validierungsfunktion verwenden, wenn Sie Werte von außerhalb des Pakets akzeptieren.

Die andere große Sache wäre, in godoc aufzutauchen. godoc gruppiert bereits const/iota und #20131 würde bei der Schnittstelle Pseudosummen helfen. Es gibt nicht wirklich etwas mit der Strukturversion zu tun, die in der Definition nicht explizit ist, außer die Invariante anzugeben.

sowie Offline-Tools – Linters, Codegeneratoren usw.

Nein. Die statische Information ist vorhanden, dafür braucht man das Typsystem (oder Reflect) nicht, Konvention funktioniert gut. Wenn Ihre Schnittstelle nicht exportierte Methoden enthält, kann jedes statische Tool dies als geschlossene Summe behandeln (weil es effektiv ist) und jede gewünschte Analyse/Codegen durchführen. Ebenso mit der Konvention der Jota-Enumerationen.

Reflect ist für Informationen zum Laufzeittyp - und in gewisser Weise löscht der Compiler die notwendigen Informationen, damit hier Summen nach Konvention funktionieren (da Sie keinen Zugriff auf eine Liste von Funktionen oder deklarierten Typen oder deklarierten Konstanten haben), was Deshalb stimme ich zu, dass tatsächliche Summen dies ermöglichen.

(Auch FTR, je nach Anwendungsfall, könnten Sie noch ein Tool haben, das die statisch bekannten Informationen verwendet, um die notwendigen Laufzeitinformationen zu generieren - zB könnte es die Typen aufzählen, die die erforderliche Tag-Methode haben und eine Lookup-Tabelle generieren Aber ich verstehe nicht, was ein Anwendungsfall wäre, daher ist es schwierig, die Praktikabilität davon zu bewerten).

Meine Frage war also absichtlich: Was wäre der Anwendungsfall, wenn diese Informationen zur Laufzeit verfügbar wären?

Unabhängig davon ist es eine gute Idee, es zu erkunden, also lassen Sie es uns erkunden.

Wenn ich sagte "erforsche es", meinte ich nicht "zählen Sie sie auf und diskutieren Sie in einem Vakuum darüber", ich meinte "Werkzeuge implementieren, die diese Konventionen verwenden und sehen, wie nützlich/notwendig/praktisch sie sind".

Der Vorteil von Erfahrungsberichten ist, dass sie auf Erfahrungswerten beruhen: Man musste etwas tun, man versuchte, dafür vorhandene Mechanismen zu nutzen, man stellte fest, dass sie nicht ausreichten. Dies konzentriert die Diskussion auf den tatsächlichen Anwendungsfall (wie in "der Fall, in dem es verwendet wurde") und ermöglicht es, alle vorgeschlagenen Lösungen gegen diese, gegen die versuchten Alternativen zu bewerten und zu sehen, wie eine Lösung nicht die gleichen Fallstricke haben würde.

Sie überspringen den Teil "Versuchen, vorhandene Mechanismen dafür zu verwenden". Sie möchten statische Vollständigkeitsprüfungen von Summen durchführen lassen (Problem). Schreiben Sie ein Werkzeug, das Schnittstellen mit nicht exportierten Methoden findet, führt die Vollständigkeitsprüfungen für jeden Typwechsel durch, in dem es verwendet wird, verwenden Sie dieses Werkzeug eine Weile (verwenden Sie die vorhandenen Mechanismen dafür). Schreiben Sie auf, wo es fehlgeschlagen ist.

Ich habe laut nachgedacht und mit der Arbeit an einem statischen Erkenner begonnen, der auf diesen Gedanken basiert, die Tools verwenden können. Ich vermute, ich habe implizit nach Feedback und weiteren Ideen gesucht (und das hat sich ausgezahlt, die zum Reflektieren notwendigen Informationen neu zu generieren).

FWIW, wenn ich da bin, würde ich die komplexen Fälle einfach ignorieren und mich auf die Dinge konzentrieren, die funktionieren: a) nicht exportierte Methoden in Schnittstellen und b) einfache const-iota-enums, die int als zugrunde liegenden Typ und eine einzelne const- Angabe des erwarteten Formats. Die Verwendung eines Tools würde die Verwendung einer dieser beiden Problemumgehungen erfordern, aber IMO ist das in Ordnung (um das Compiler-Tool zu verwenden, müssen Sie auch explizit Summen verwenden, also scheint das in Ordnung zu sein).

Das ist definitiv ein guter Anfang und kann eingewählt werden, nachdem Sie eine große Anzahl von Paketen durchlaufen und gesehen haben, wie viele falsch positive / negative Ergebnisse vorhanden sind

https://godoc.org/github.com/jimmyfrasche/closed

Noch sehr viel in Arbeit. Ich kann nicht versprechen, dass ich dem Konstruktor keine zusätzlichen Parameter hinzufügen muss. Es hat wahrscheinlich mehr Fehler als Tests. Aber zum Spielen ist es gut genug.

Es gibt ein Beispiel für die Verwendung in cmds/closed-exporer, das auch alle geschlossenen Typen auflistet, die in einem durch seinen Importpfad angegebenen Paket erkannt wurden.

Ich habe angefangen, alle Schnittstellen mit nicht exportierten Methoden zu erkennen, aber sie sind ziemlich verbreitet und während einige eindeutig Summentypen waren, waren andere eindeutig nicht. Wenn ich es nur auf die Konvention der leeren Tag-Methoden beschränkte, verlor ich viele Summentypen, also beschloss ich, beide getrennt aufzuzeichnen und das Paket ein wenig über Summentypen hinaus auf geschlossene Typen zu verallgemeinern.

Bei Enums bin ich den anderen Weg gegangen und habe einfach jede Nicht-Bitset-const eines definierten Typs aufgezeichnet. Ich habe vor, auch die entdeckten Bitsets freizulegen.

Es erkennt noch keine optionalen Strukturen oder definierten leeren Schnittstellen, da sie irgendeine Art von Markierungskommentar erfordern, aber es macht die in der stdlib Sonderfälle.

Ich habe angefangen, alle Schnittstellen mit nicht exportierten Methoden zu erkennen, aber sie sind ziemlich verbreitet und während einige eindeutig Summentypen waren, waren andere eindeutig nicht.

Ich würde es hilfreich finden, wenn Sie einige der Beispiele angeben könnten, die es nicht waren.

@Merovius tut mir leid, dass ich keine Liste geführt habe. Ich habe sie gefunden, indem ich stdlib.sh (in cmds/closed-explorer) ausgeführt habe. Wenn ich das nächste Mal auf ein gutes Beispiel stoße, werde ich es posten.

Diejenigen, die ich nicht als Summentypen betrachte, waren alle nicht exportierte Schnittstellen, die verwendet wurden, um eine von mehreren Implementierungen einzufügen: Nichts interessierte, was in der Schnittstelle war, nur dass es etwas gab, das sie zufriedenstellte. Sie wurden sehr oft als Schnittstellen verwendet, nicht als Summen, sondern wurden einfach geschlossen, weil sie nicht exportiert wurden. Vielleicht ist das eine Unterscheidung ohne Unterschied, aber ich kann meine Meinung nach weiteren Untersuchungen jederzeit ändern.

@jimmyfrasche Ich würde argumentieren, dass diese ordnungsgemäß als geschlossene Summen behandelt werden sollten. Ich würde argumentieren, dass sich ein statischer Linter nicht beschweren würde, wenn er sich nicht für den dynamischen Typ interessiert (dh nur die Methoden in der Schnittstelle aufruft), da "alle Schalter erschöpfend sind" - es gibt also keinen Nachteil bei der Behandlung als geschlossene Summen. Wenn OTOH, sie geben Schalter manchmal und auslassen einen Fall, wäre richtig beschweren - das ist genau die Art von Sache wäre die Linter zu fangen soll.

Ich möchte ein gutes Wort einlegen, um zu untersuchen, wie Unionstypen die Speicherauslastung reduzieren können. Ich schreibe einen Interpreter in Go und habe einen Value-Typ, der unbedingt als Schnittstelle implementiert ist, da Values ​​Zeiger auf verschiedene Typen sein können. Dies bedeutet vermutlich, dass ein []Wert doppelt so viel Speicher beansprucht wie das Packen des Zeigers mit einem kleinen Bit-Tag wie in C. Es scheint viel zu sein?

Die Sprachspezifikation muss dies nicht erwähnen, aber es scheint, dass die Halbierung des Speicherverbrauchs eines Arrays für einige kleine Unionstypen ein ziemlich überzeugendes Argument für Unions sein könnte? Damit können Sie etwas tun, was meines Wissens heute in Go unmöglich ist. Im Gegensatz dazu könnte die Implementierung von Unions auf Schnittstellen zu der Korrektheit und Verständlichkeit des Programms beitragen, bringt aber auf Maschinenebene nichts Neues.

Ich habe keine Leistungstests durchgeführt; nur eine Richtung für die Forschung aufzeigen.

Sie können stattdessen einen Wert als unsafe.Pointer implementieren.

Am 6. Februar 2018 um 15:54 Uhr schrieb "Brian Slesinsky" [email protected] :

Ich möchte ein gutes Wort dafür einlegen, wie sich Gewerkschaftstypen reduzieren könnten
Speichernutzung. Ich schreibe einen Interpreter in Go und habe einen Werttyp, der
ist notwendigerweise als Schnittstelle implementiert, da Werte Zeiger sein können
zu verschiedenen Typen. Dies bedeutet vermutlich, dass ein []Wert doppelt so viel benötigt
Speicher im Vergleich zum Packen des Zeigers mit einem kleinen Tag, wie Sie es tun könnten
in C. Es scheint viel zu sein?

Die Sprachspezifikation muss dies nicht erwähnen, aber es scheint, als würde der Speicher gekürzt
Die Verwendung eines Arrays in zwei Hälften für einige kleine Unionstypen könnte eine schöne Sache sein
überzeugendes Argument für Gewerkschaften? Es lässt dich etwas tun, das soweit wie ich
Wissen ist heute in Go nicht möglich. Im Gegensatz dazu, die Umsetzung von Gewerkschaften auf
top of interface könnte bei der Programmkorrektheit helfen und
Verständlichkeit, bringt aber auf Maschinenebene nichts Neues.

Ich habe keine Leistungstests durchgeführt; nur eine Richtung zeigen für
Forschung.


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/19412#issuecomment-363561070 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AGGWBz-L3t0YosVIJmYNyf2iQ-YgIXLGks5tSLv9gaJpZM4MTmSr
.

@skybrian Das erscheint in Bezug auf die Implementierung von

Das lässt Sie mit: Summentypen werden wahrscheinlich als Unions gekennzeichnet und werden wahrscheinlich genauso viel Platz in einem Slice einnehmen wie jetzt. Es sei denn, das Slice ist homogen, aber Sie können jetzt auch einen spezifischeren Slice-Typ verwenden.

Also ja. In sehr speziellen Fällen können Sie möglicherweise ein wenig Speicher sparen, wenn Sie speziell dafür optimieren, aber es scheint, dass Sie auch nur manuell dafür optimieren können, wenn Sie es tatsächlich benötigen.

@DemiMarie unsafe.Pointer funktioniert nicht in App Engine, und auf jeden Fall können Sie keine Bits packen, ohne den Garbage Collector durcheinander zu bringen. Selbst wenn es möglich wäre, wäre es nicht tragbar.

@Merovius ja, es erfordert eine Änderung der Laufzeit und des Garbage Collectors, um gepackte Speicherlayouts zu verstehen. Das ist der Punkt; Zeiger werden von der Go-Laufzeit verwaltet. Wenn Sie also auf sichere Weise besser als Schnittstellen arbeiten möchten, können Sie dies nicht in einer Bibliothek oder im Compiler tun.

Aber ich gebe gerne zu, dass das Schreiben eines schnellen Interpreters ein ungewöhnlicher Anwendungsfall ist. Gibt es vielleicht noch andere? Es scheint eine gute Möglichkeit zu sein, ein Sprachfeature zu motivieren, Dinge zu finden, die heute in Go nicht leicht zu erledigen sind.

Das ist wahr.

Meiner Meinung nach ist Go nicht die beste Sprache, um einen Dolmetscher zu schreiben,
aufgrund der wilden Dynamik solcher Software. Wenn Sie hohe Leistung benötigen,
Ihre heißen Schleifen sollten in Assembler geschrieben werden. Gibt es einen Grund für dich?
Sie müssen einen Interpreter schreiben, der auf App Engine funktioniert?

Am 6. Februar 2018, 18:15 Uhr, schrieb "Brian Slesinsky" [email protected] :

@DemiMarie https://github.com/demimarie unsafe.Pointer funktioniert nicht in der App
Motor, und auf jeden Fall können Sie keine Bits ohne packen
den Müllsammler durcheinander bringen. Selbst wenn es möglich wäre, wäre es nicht
tragbar.

@metrovius ja, es erfordert eine Änderung der Laufzeit und des Garbage Collectors
um gepackte Speicherlayouts zu verstehen. Das ist der Punkt; Zeiger sind
von der Go-Laufzeit verwaltet, also wenn Sie besser als Schnittstellen in a
Auf sichere Weise können Sie dies nicht in einer Bibliothek oder im Compiler tun.

Aber ich gebe gerne zu, dass das Schreiben eines schnellen Dolmetschers eine ungewöhnliche Verwendung ist
Fall. Gibt es vielleicht noch andere? Es scheint ein guter Weg zu sein, um einen zu motivieren
Sprachfeature besteht darin, Dinge zu finden, die heute in Go nicht einfach zu erledigen sind.


Sie erhalten dies, weil Sie erwähnt wurden.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/19412#issuecomment-363598572 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AGGWB65jRKg_qVPWTiq8LbGk3YM1RUasks5tSN0tgaJpZM4MTmSr
.

Ich finde den Vorschlag von @rogpeppe recht ansprechend. Ich frage mich auch, ob es Potenzial gibt, zusätzliche Vorteile freizuschalten, die mit denen einhergehen, die bereits von @griesemer identifiziert wurden.

Der Vorschlag lautet: "Die Methodenmenge des Summentyps enthält den Schnittpunkt der Methodenmenge
aller Komponententypen, mit Ausnahme aller Methoden, die die gleichen
Name, aber unterschiedliche Signaturen.".

Aber ein Typ ist mehr als nur ein Methodensatz. Was wäre, wenn der Summentyp die Schnittmenge der Operationen unterstützt, die von seinen Komponententypen unterstützt werden?

Betrachten Sie zum Beispiel:

var x int|float64

Die Idee ist, dass Folgendes funktionieren würde.

x += 5

Dies wäre äquivalent zum Schreiben des vollständigen Typschalters:

switch i := x.(type) {
case int:
    x = i + 5
case float64:
    x = i + 5
}

Eine andere Variante beinhaltet einen Typwechsel, bei dem ein Komponententyp selbst ein Summentyp ist.

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.
}

Außerdem denke ich, dass es möglicherweise eine wirklich schöne Synergie zwischen Summentypen und einem generischen System gibt, das Typbeschränkungen verwendet.

var x int|float64

Was ist mit var x, y int | float64 ? Welche Regeln gelten hier, wenn diese hinzugefügt werden? Welche verlustbehaftete Konvertierung wird durchgeführt (und warum)? Was wird der Ergebnistyp sein?

Go führt absichtlich keine automatischen Konvertierungen in Ausdrücken durch (wie C es tut) - diese Fragen sind nicht einfach zu beantworten und führen zu Fehlern.

Und für noch mehr Spaß:

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)

Alle int , string und rune haben einen + Operator; Was ist der obige Druck, warum und vor allem, wie kann das Ergebnis nicht völlig verwirrend sein?

Was ist mit var x, y int | float64 ? Welche Regeln gelten hier, wenn diese hinzugefügt werden? Welche verlustbehaftete Konvertierung wird durchgeführt (und warum)? Was wird der Ergebnistyp sein?

@Merovius Implizit wird keine verlustbehaftete Konvertierung vorgenommen, obwohl ich sehen kann, wie meine Formulierung diesen Eindruck erwecken könnte. Hier würde ein einfaches x + y nicht kompilieren, da es eine mögliche implizite Konvertierung impliziert. Aber einer der folgenden würde kompilieren:

z = int(x) + int(y)
z = float64(x) + float64(y)

Ebenso würde Ihr xyz-Beispiel nicht kompiliert, da es mögliche implizite Konvertierungen erfordert.

Ich denke, "unterstützt die Schnittmenge der unterstützten Operationen" klingt gut, vermittelt aber nicht ganz das, was ich beabsichtigte. Das Hinzufügen von etwas wie "kompiliert für alle Komponententypen" hilft zu beschreiben, wie es meiner Meinung nach funktionieren könnte.

Ein weiteres Beispiel ist, wenn alle Komponententypen Slices und Maps sind. Wäre schön, len über den Summentyp aufrufen zu können, ohne einen Typenwechsel zu benötigen.

Alle int, string und rune haben einen +-Operator; Was ist der obige Druck, warum und vor allem, wie kann das Ergebnis nicht völlig verwirrend sein?

Ich wollte nur hinzufügen, dass meine "Was wäre, wenn der Summentyp die Schnittmenge der Operationen unterstützt, die von seinen Komponententypen unterstützt werden?" wurde von der Beschreibung eines Typs in der Go Spec inspiriert als "Ein Typ bestimmt eine Reihe von Werten zusammen mit Operationen und Methoden, die für diese Werte spezifisch sind".

Der Punkt, den ich zu machen versuchte, ist, dass ein Typ mehr ist als nur Werte und Methoden, und daher könnte ein Summentyp versuchen, die Gemeinsamkeit dieser anderen Dinge aus seinen Komponententypen zu erfassen. Dieses "andere Zeug" ist nuancierter als nur eine Reihe von Operatoren.

Ein weiteres Beispiel ist der Vergleich mit nil:

var x []int | []string
fmt.Println(x == nil)  // Prints true
x = []string(nil)
fmt.Println(x == nil)  // Still prints true

Beide Komponententypen sind mindestens ein Typ ist mit nil vergleichbar, daher erlauben wir den Vergleich des Summentyps mit nil ohne Typwechsel. Natürlich steht dies etwas im Widerspruch zum aktuellen Verhalten von Schnittstellen, aber das ist möglicherweise keine schlechte Sache gemäß https://github.com/golang/go/issues/22729

Bearbeiten: Gleichheitstests sind hier ein schlechtes Beispiel, da sie meiner Meinung nach freizügiger sein sollten und nur eine potenzielle Übereinstimmung von einem oder mehreren Komponententypen erfordern. Spiegelt die Zuordnung in dieser Hinsicht.

Das Problem ist, dass das Ergebnis entweder a) die gleichen Probleme wie automatische Konvertierungen haben wird oder b) im Umfang extrem (und IMO verwirrend) eingeschränkt sein wird - nämlich alle Operatoren würden bestenfalls mit untypisierten Literalen arbeiten.

Ich habe auch noch ein weiteres Problem, nämlich, dass dies die Robustheit gegenüber der Entwicklung ihrer konstituierenden Typen noch weiter einschränken wird - jetzt können Sie unter Wahrung der Abwärtskompatibilität nur Typen hinzufügen, die alle Operationen ihrer konstituierenden Typen zulassen.

All dies erscheint mir für einen sehr kleinen (wenn überhaupt) greifbaren Vorteil wirklich chaotisch.

Jetzt können Sie unter Wahrung der Abwärtskompatibilität nur Typen hinzufügen, die alle Operationen ihrer konstituierenden Typen zulassen.

Oh, und um es noch einmal ausdrücklich zu sagen: Es impliziert, dass Sie sich nie entscheiden können, ob Sie einen Parameter oder Rückgabetyp oder eine Variable erweitern möchten oder ... von einem Singleton-Typ zu einer Summe. Da das Hinzufügen eines neuen Typs dazu führt, dass einige Operationen (wie Zuweisungen) nicht kompiliert werden können

@Merovius Beachten Sie, dass mit dem ursprünglichen Vorschlag bereits eine Variante des Kompatibilitätsproblems existiert, da "Der Methodensatz des
aller Komponententypen". Wenn Sie also einen neuen Komponententyp hinzufügen, der diesen Methodensatz nicht implementiert, ist dies eine nicht abwärtskompatible Änderung.

Oh, und um es explizit zu sagen: Es impliziert, dass Sie sich nie entscheiden können, ob Sie einen Parameter oder Rückgabetyp oder eine Variable erweitern möchten oder ... von einem Singleton-Typ zu einer Summe. Da das Hinzufügen eines neuen Typs dazu führt, dass einige Operationen (wie Zuweisungen) nicht kompiliert werden können

Das Zuweisungsverhalten würde wie von @rogpeppe beschrieben

Wenn nichts anderes, denke ich, dass der ursprüngliche Rogpeppe-Vorschlag hinsichtlich des Verhaltens des Summentyps außerhalb eines Typwechsels geklärt werden muss. Zuweisung und Methodensatz werden behandelt, aber das ist alles. Wie steht es um Gleichberechtigung? Ich denke, wir können es besser machen als die Schnittstelle{}:

var x int | float64
fmt.Println(x == "hello")  // compilation error?
x = 0.0
fmt.Println(x == 0) // true or false?  I vote true :-)

Wenn Sie also einen neuen Komponententyp hinzufügen, der diesen Methodensatz nicht implementiert, ist dies eine nicht abwärtskompatible Änderung.

Sie können jederzeit Methoden hinzufügen, aber Sie können Operatoren nicht überladen, um an neuen Typen zu arbeiten. Was genau der Unterschied ist - in ihrem Vorschlag können Sie die üblichen Methoden nur für einen Summenwert aufrufen (oder diesem zuweisen), es sei denn, Sie entpacken ihn mit einer Typzusicherung/-umschaltung. Solange der von Ihnen hinzugefügte Typ über die erforderlichen Methoden verfügt, wäre dies also keine grundlegende Änderung. In Ihrem Vorschlag wäre dies immer noch eine bahnbrechende Änderung, da Benutzer möglicherweise Operatoren verwenden, die Sie nicht überladen können.

(Sie möchten vielleicht darauf hinweisen, dass das Hinzufügen von Typen zur Summe immer noch eine bahnbrechende Änderung wäre, da Typ-Switches den neuen Typ nicht enthalten würden. Genau deshalb bin ich auch nicht für den ursprünglichen Vorschlag - I will aus genau diesem Grund keine geschlossenen Summen)

Das Zuweisungsverhalten würde wie von @rogpeppe beschrieben bleiben

Ihr Vorschlag spricht nur von der Zuordnung zu einem Summenwert, ich spreche von der Zuordnung von einem Summenwert (zu einem seiner Bestandteile). Ich stimme zu, dass ihr Vorschlag dies auch nicht zulässt, aber der Unterschied besteht darin, dass es in ihrem Vorschlag nicht darum geht, diese Möglichkeit hinzuzufügen. dh mein Argument ist genau, dass die von Ihnen vorgeschlagene Semantik nicht besonders vorteilhaft ist, da ihre Verwendung in der Praxis stark eingeschränkt ist.

fmt.Println(x == "hello") // compilation error?

Dies würde wahrscheinlich auch zu ihrem Vorschlag hinzugefügt werden. Wir haben bereits einen äquivalenten Spezialfall für Schnittstellen , nämlich

Ein Wert x vom Nicht-Schnittstellentyp X und ein Wert t vom Schnittstellentyp T sind vergleichbar, wenn Werte vom Typ X vergleichbar sind und X T implementiert. Sie sind gleich, wenn der dynamische Typ von t identisch mit X und der dynamische Wert von t gleich x . ist .

fmt.Println(x == 0) // true or false? I vote true :-)

Vermutlich falsch. Da die ähnlichen

var x int|float64 = 0.0
y := 0
fmt.Println(x == y)

ein Kompilierungsfehler sein sollte (wie wir oben festgestellt haben), macht diese Frage nur beim Vergleich mit nicht typisierten numerischen Konstanten wirklich Sinn. An diesem Punkt hängt es davon ab, wie dies der Spezifikation hinzugefügt wird. Man könnte argumentieren, dass dies vergleichbar ist mit der Zuweisung einer Konstanten zu einem Interface-Typ und dieser daher seinen Standardtyp haben sollte (und dann wäre der Vergleich falsch). Was IMO mehr als in Ordnung ist, wir akzeptieren diese Situation heute schon ohne viel Aufhebens. Sie können der Spezifikation jedoch auch einen Fall für nicht typisierte Konstanten hinzufügen, der den Fall abdeckt, sie zu Summen zuzuweisen / zu vergleichen und die Frage auf diese Weise zu lösen.

Um diese Frage in beiden Fällen zu beantworten, müssen jedoch nicht alle Ausdrücke zugelassen werden, die Summentypen verwenden, die für die Bestandteile sinnvoll sein könnten.

Aber um es noch einmal zu wiederholen: Ich plädiere nicht für einen anderen Betragsvorschlag. Ich argumentiere gegen diesen.

fmt.Println(x == "hello") // compilation error?

Dies würde wahrscheinlich auch zu ihrem Vorschlag hinzugefügt werden.

Korrektur: Die Spezifikation deckt diesen Kompilierungsfehler bereits ab, da sie die Anweisung enthält

Bei jedem Vergleich muss der erste Operand dem Typ des zweiten Operanden zuweisbar sein oder umgekehrt.

@Merovius Sie machen einige gute Punkte zu meiner Variante des Vorschlags. Ich werde davon Abstand nehmen, sie weiter zu diskutieren, aber ich möchte die Vergleichsfrage zur 0 etwas weiter vertiefen, da sie gleichermaßen für den ursprünglichen Vorschlag gilt.

fmt.Println(x == 0) // true or false? I vote true :-)

Vermutlich falsch. Da die ähnlichen

var x int|float64 = 0.0
y := 0
fmt.Println(x == y)
sollte ein Kompilierungsfehler sein (wie wir oben festgestellt haben),

Ich finde dieses Beispiel nicht sehr überzeugend, denn wenn Sie die erste Zeile in var x float64 = 0.0 ändern, können Sie dieselbe Argumentation verwenden, um zu argumentieren, dass der Vergleich von float64 mit 0 falsch sein sollte. (Kleine Punkte: (a) Ich nehme an, Sie meinten float64(0) in der ersten Zeile, da 0.0 int zuweisbar ist. (b) x==y sollte in Ihrem Beispiel kein Kompilierungsfehler sein. Es sollte jedoch false ausgeben.)

Ich denke, Ihre Idee, dass "dass dies dem Zuweisen einer Konstanten zu einem Schnittstellentyp ähnelt und daher seinen Standardtyp haben sollte", ist überzeugender (vorausgesetzt, Sie meinen den Summentyp), also wäre das Beispiel:

var x,y int|float64 = float64(0), 0
fmt.Println(x == y) // falsch

Ich würde immer noch argumentieren, dass x == 0 wahr sein sollte. Mein mentales Modell ist, dass ein Typ so spät wie möglich auf 0 gegeben wird. Mir ist klar, dass dies dem aktuellen Verhalten von Schnittstellen widerspricht, und genau deshalb habe ich es angesprochen. Ich stimme zu, dass dies nicht zu "viel Unschärfe" geführt hat, aber das ähnliche Problem des Vergleichens von Schnittstellen mit Null hat zu ziemlich viel Verwirrung geführt. Ich glaube, wir würden eine ähnliche Verwirrung beim Vergleich mit 0 sehen, wenn Summentypen entstehen und die alte Gleichheitssemantik beibehalten wird.

Ich finde dieses Beispiel nicht sehr überzeugend, denn wenn Sie die erste Zeile in var x float64 = 0.0 ändern, können Sie dieselbe Argumentation verwenden, um zu argumentieren, dass der Vergleich von float64 mit 0 falsch sein sollte.

Ich habe nicht gesagt, dass es so sein sollte , ich sagte, dass es wahrscheinlich wäre , da ich den wahrscheinlichsten Kompromiss zwischen Einfachheit / Nützlichkeit für die Umsetzung ihres Vorschlags einsehe. Ich wollte kein Werturteil fällen. Wenn wir es mit ebenso einfachen Regeln zum Ausdruck bringen könnten, würde ich es wahrscheinlich vorziehen. Ich bin einfach nicht optimistisch.

Beachten Sie, dass der Vergleich von float64(0) mit int(0) (dh das Beispiel mit der durch var x float64 = 0.0 ersetzten Summe) nicht false ist, sondern eine Kompilierzeit Fehler (wie es sein sollte). Das ist genau mein Punkt ; Ihr Vorschlag ist nur in Kombination mit nicht typisierten Konstanten wirklich nützlich, da er für alles andere nicht kompiliert werden würde.

(a) Ich nehme an, Sie meinten float64(0) in der ersten Zeile, da 0.0 int zuweisbar ist.

Sicher (ich habe eine Semantik angenommen, die näher am aktuellen "Standardtyp" für konstante Ausdrücke liegt, aber ich stimme zu, dass der aktuelle Wortlaut dies nicht impliziert).

(b) x==y sollte in Ihrem Beispiel kein Kompilierungsfehler sein. Es sollte jedoch false drucken.)

Nein, es sollte ein Kompilierzeitfehler sein. Sie haben gesagt, dass die Operation e1 == y , wobei e1 ein Summentyp-Ausdruck ist, nur dann erlaubt sein sollte, wenn der Ausdruck mit einer beliebigen Konstituentenart kompiliert werden würde. In meinem Beispiel hat x den Typ int|float64 und y den Typ int und float64 und int nicht vergleichbar sind, wird diese Bedingung eindeutig verletzt.

Um dies zu kompilieren, müssen Sie die Bedingung löschen, dass der ersetzende typisierte Ausdruck auch kompiliert werden muss. An diesem Punkt müssen wir Regeln aufstellen, wie Typen bei der Verwendung in diesen Ausdrücken hochgestuft oder konvertiert werden (auch bekannt als "das C-Chaos").

In der Vergangenheit war man sich einig, dass Summentypen den Schnittstellentypen nicht viel hinzufügen.

Sie tun es in der Tat nicht für die meisten Anwendungsfälle von Go: triviale Netzwerkdienste und Utils. Aber sobald das System größer wird, besteht eine gute Chance, dass sie nützlich sind.
Ich schreibe derzeit einen stark verteilten Dienst mit Datenkonsistenzgarantien, die durch viel Logik implementiert werden, und bin in die Situation gefahren, in der sie praktisch wären. Diese NPDs wurden zu nervig, als der Dienst größer wurde und wir keinen vernünftigen Weg sehen, ihn aufzuteilen.
Ich meine, die Typsystemgarantien von Go sind etwas zu schwach für etwas Komplexeres als typische primitive Netzwerkdienste.

Aber die Geschichte mit Rust zeigt, dass es eine schlechte Idee ist, Summentypen für NPD und Fehlerbehandlung zu verwenden, genau wie sie es in Haskell tun: Es gibt einen typischen natürlichen Imperativ-Workflow und der Haskellish-Ansatz passt nicht gut dazu.

Beispiel

Betrachten Sie iotuils.WriteFile -ähnliche Funktion in Pseudocode. Imperativer Fluss würde so aussehen

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

und wie es in Rust aussieht

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)

es ist sicher, aber hässlich.

Und mein Vorschlag:

type result[T, Err] oneof {
    default T
    Error Err
}

und wie das Programm aussehen könnte ( 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

Hier ist der Standardzweig anonym und der Fehlerzweig kann mit .Error aufgerufen werden (sobald er bekannt ist, ist das Ergebnis Fehler). Sobald bekannt ist, dass die Datei erfolgreich geöffnet wurde, kann der Benutzer über die Variable selbst darauf zugreifen. Stellen Sie zunächst sicher, dass file erfolgreich geöffnet wurde oder beenden Sie es anderweitig (und somit wissen weitere Anweisungen, dass die Datei kein Fehler ist).

Wie Sie sehen, bewahrt dieser Ansatz einen zwingenden Fluss und bietet Typsicherheit. Das NPD-Handling kann auf ähnliche Weise erfolgen:

type Reference[T] oneof {
    default T
    nil
}
// Reference[T] = *T

die Handhabung ist ähnlich wie result

@sirkon , Ihr Rust-Beispiel überzeugt mich nicht, dass mit einfachen if Anweisungen Go-ähnlicher gestaltet werden könnte. Etwas wie:

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)
...

(Im Sinne von Summentypen wäre es ein Kompilierungsfehler, wenn der Compiler nicht beweisen kann, dass eine unbedingte Übereinstimmung immer erfolgreich ist, weil genau ein Fall übrig ist.)

Für die grundlegende Fehlerprüfung scheint dies keine Verbesserung gegenüber mehreren Rückgabewerten zu sein, da es eine Zeile länger ist und eine weitere lokale Variable deklariert. Es würde jedoch besser auf mehrere Fälle skaliert (durch Hinzufügen weiterer if-Anweisungen), und der Compiler könnte überprüfen, ob alle Fälle behandelt werden.

@sirkon

Sie tun es in der Tat nicht für die meisten Anwendungsfälle von Go: triviale Netzwerkdienste und Utils. Aber sobald das System größer wird, besteht eine gute Chance, dass sie nützlich sind.
[…]
Ich meine, die Typsystemgarantien von Go sind etwas zu schwach für etwas Komplexeres als typische primitive Netzwerkdienste.

Aussagen wie diese sind unnötig konfrontativ und abwertend. Sie sind auch irgendwie peinlich, TBH, weil es in Go extrem große, nicht triviale Dienste gibt. Und da ein erheblicher Teil seiner Entwickler bei Google arbeitet, sollten Sie einfach davon ausgehen, dass sie besser wissen als Sie, ob es geeignet ist, große und nicht triviale Dienste zu schreiben. Go deckt möglicherweise nicht alle Anwendungsfälle ab (so sollte es auch nicht, IMO), aber es funktioniert empirisch nicht nur für "primitive Netzwerkdienste".

Das NPD-Handling kann auf ähnliche Weise erfolgen

Ich denke, dies zeigt wirklich, dass Ihr Ansatz keinen signifikanten Mehrwert bietet. Wie Sie darauf hinweisen, wird einfach eine andere Syntax für die Dereferenzierung hinzugefügt. Aber AFAICT nichts hindert einen Programmierer daran, diese Syntax bei einem Nullwert zu verwenden (was vermutlich immer noch in Panik geraten würde). jedes Programm , dh , die gültig ist mit *p ist auch gültig mit p.T (oder ist es p.default ? Es ist schwer zu sagen , was Ihre Idee ist speziell) und vice versa.

Der einzige Vorteil, den Summentypen zur Fehlerbehandlung und Null-Dereferenzierung hinzufügen können, besteht darin, dass der Compiler erzwingen kann, dass Sie durch Mustervergleiche nachweisen müssen, dass die Operation sicher ist. Ein Vorschlag, der die Durchsetzung auslässt, scheint keine wesentlichen neuen Dinge auf den Tisch zu bringen (wohl schlimmer als die Verwendung offener Summen über Schnittstellen), während ein Vorschlag, der dies enthält, genau das ist, was Sie als "hässlich" bezeichnen.

@Merovius

Und da ein erheblicher Teil der Entwickler bei Google arbeitet, sollten Sie einfach davon ausgehen, dass sie es besser wissen als Sie,

Gesegnet sind die Gläubigen.

Wie Sie darauf hinweisen, wird einfach eine andere Syntax für die Dereferenzierung hinzugefügt.

wieder

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(...)

diese Zwischenvariable ist es, die mich dazu zwingt, diese Idee zu verlassen. Wie Sie sehen, ist mein Ansatz speziell auf Fehler- und Nullbehandlung ausgerichtet. Diese winzigen Aufgaben sind zu wichtig und verdienen eine besondere Aufmerksamkeit IMO.

@sirkon Du hast anscheinend sehr wenig Interesse daran, auf Augenhöhe mit Leuten zu sprechen. Dabei belasse ich es.

Lassen Sie uns unsere Gespräche höflich halten und nicht konstruktive Kommentare vermeiden. Wir können über Dinge uneinig sein, aber dennoch einen respektablen Diskurs aufrechterhalten. https://golang.org/conduct.

Und da ein erheblicher Teil der Entwickler bei Google arbeitet, sollten Sie einfach davon ausgehen, dass sie es besser wissen als Sie

Ich bezweifle, dass Sie bei Google so argumentieren können.

@hasufell , der Typ kommt aus Deutschland, wo es keine großen IT-Firmen mit beschissenen Interviews gibt, um das Ego des Interviewers und das Gigant-Management zu pumpen, deshalb diese Worte.

@sirkon das gleiche gilt für dich. Ad-hominem und soziale Argumente sind nicht sinnvoll. Dies ist mehr als ein CoC-Problem. Ich habe gesehen, dass diese Art von "sozialen Argumenten" ziemlich häufig auftaucht, wenn es um die Kernsprache geht: Compiler-Entwickler wissen es besser, Sprachdesigner wissen es besser, Google-Leute wissen es besser.

Nein, das tun sie nicht. Es gibt keine intellektuelle Autorität. Es gibt nur Entscheidungsbefugnisse. Komm darüber hinweg.

Verstecke ein paar Kommentare, um die Konversation zurückzusetzen (und danke @agnivade für den Versuch, sie wieder auf die Schienen zu bringen).

Leute, bitte bedenkt eure Rolle in diesen Diskussionen im Lichte unserer Gopher-Werte : Jeder in der Gemeinschaft hat eine Perspektive einzubringen, und wir sollten uns bemühen, respektvoll und wohltätig zu sein, wie wir einander interpretieren und reagieren.

Erlauben Sie mir bitte, meine 2 Cent zu dieser Diskussion hinzuzufügen:

Wir brauchen eine Möglichkeit, verschiedene Typen nach anderen Merkmalen als ihren Methodensätzen zu gruppieren (wie bei Schnittstellen). Eine neue Gruppierungsfunktion sollte es ermöglichen, primitive (oder grundlegende) Typen einzubeziehen, die keine Methoden haben, und Schnittstellentypen als relevant ähnlich kategorisiert zu werden. Wir können primitive Typen (boolean, numerisch, string und sogar []byte, []int usw.) unverändert lassen, aber die Abstraktion von Unterschieden zwischen Typen ermöglichen, wenn eine Typdefinition sie in einer Familie gruppiert.

Ich schlage vor, wir fügen der Sprache so etwas wie ein Konstrukt vom Typ _family_ hinzu.

Die Syntax

Eine Typfamilie kann ähnlich wie jeder andere Typ definiert werden:

type theFamilyName family {
    someType
    anotherType
}

Die formale Syntax wäre etwa:
FamilyType = "family" "{" { TypeName ";" } "}" .

Eine Typfamilie kann innerhalb einer Funktionssignatur definiert werden:

func Display(s family{string; fmt.Stringer}) { /* function body */ }

Das heißt, die einzeilige Definition erfordert Semikolons zwischen den Typnamen.

Der Nullwert eines Familientyps ist nil, wie bei einer nil-Schnittstelle.

(Unter der Haube wird ein Wert, der hinter der Familienabstraktion sitzt, ähnlich wie eine Schnittstelle implementiert.)

Die Begründung

Wir brauchen etwas Präziseres als die leere Schnittstelle, in der wir angeben möchten, welche Typen als Argumente für eine Funktion oder als Rückgabe einer Funktion gültig sind.

Die vorgeschlagene Lösung würde eine bessere Typsicherheit ermöglichen, die zur Kompilierzeit vollständig überprüft wird und zur Laufzeit keinen zusätzlichen Overhead hinzufügt.

Der Punkt ist, dass _Go-Code selbstdokumentierender sein sollte_. Was eine Funktion als Argument annehmen kann, sollte in den Code selbst eingebaut werden.

Zu viel Code nutzt fälschlicherweise die Tatsache aus, dass „Schnittstelle{} nichts sagt“. Es ist ein wenig peinlich, dass ein so weit verbreitetes (und missbrauchtes) Konstrukt in Go, ohne das wir nicht viel tun könnten, _nichts_ sagt.

Einige Beispiele

Die Dokumentation für die Funktion sql.Rows.Scan enthält einen großen Block, der detailliert beschreibt, welche Typen an die Funktion übergeben werden können:

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)

Und für die Funktion sql.Row.Scan die Dokumentation den Satz „Weitere Informationen finden Sie in der Dokumentation zu Rows.Scan“. Weitere Informationen finden Sie in der Dokumentation zu _einer anderen Funktion_? Das ist nicht Go-like – und in diesem Fall ist dieser Satz nicht richtig, weil Rows.Scan tatsächlich einen *RawBytes Wert annehmen kann, Row.Scan jedoch nicht.

Das Problem ist, dass wir uns bei Garantien und Verhaltensverträgen oft auf Kommentare verlassen müssen, die der Compiler nicht durchsetzen kann.

Wenn in der Dokumentation einer Funktion steht, dass die Funktion genauso funktioniert wie eine andere Funktion – „sehen Sie sich also die Dokumentation für diese andere Funktion an“ – können Sie fast garantieren, dass die Funktion manchmal missbraucht wird. Ich wette, dass die meisten Leute, wie ich, erst herausgefunden haben, dass ein *RawBytes als Argument in Row.Scan nicht erlaubt ist, nachdem eine Fehlermeldung von Row.Scan ( sagen "sql: RawBytes ist bei Row.Scan nicht erlaubt"). Es ist traurig, dass das Typensystem solche Fehler zulässt.

Wir könnten stattdessen haben:

type Value family {
    *string
    *[]byte
    *int; *int8; *int16; *int32; *int64
    *uint; *uint8; *uint16; *uint32; *uint64
    *bool
    *float32; *float64
    *interface{}
    *RawBytes
    Scanner
}

Auf diese Weise muss der übergebene Wert einer der Typen in der angegebenen Familie sein, und der Typwechsel in der Funktion Rows.Scan muss keine unerwarteten oder Standardfälle verarbeiten; es würde eine andere Familie für die Row.Scan Funktion geben.

Betrachten Sie auch, wie das cloud.google.com/go/datastore.Property Struct ein "Wert"-Feld vom Typ interface{} und all diese Dokumentation erfordert:

// 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.

Das könnte sein:

type PropertyVal family {
  int64
  bool
  string
  float64
  *Key
  time.Time
  GeoPoint
  []byte
  *Entity
  nil
  []int64; []bool; []string; []float64; []*Key; []time.Time; []GeoPoint; [][]byte; []*Entity
}

(Sie können sich vorstellen, wie das Cleaner in zwei Familien aufgeteilt werden könnte.)

Der Typ json.Token wurde oben erwähnt. Die Typdefinition wäre:

type Token family {
    Delim
    bool
    float64
    Number
    string
    nil
}

Ein weiteres Beispiel, das mir vor kurzem aufgefallen ist:
Beim Aufrufen von Funktionen wie sql.DB.Exec oder sql.DB.Query oder jeder Funktion, die eine variadische Liste von interface{} wobei jedes Element einen Typ in einer bestimmten Menge haben muss und _nicht selbst sein muss a Slice_, ist es wichtig, daran zu denken, den "Spread"-Operator zu verwenden, wenn die Argumente von []interface{} an eine solche Funktion übergeben werden: Es ist falsch, DB.Exec("some query with placeholders", emptyInterfaceSlice) zu sagen; der richtige Weg ist: DB.Exec("the query...", emptyInterfaceSlice...) wobei emptyInterfaceSlice den Typ []interface{} . Eine elegante Möglichkeit, solche Fehler unmöglich zu machen, wäre, diese Funktion ein variadisches Argument von Value annehmen zu lassen, wobei Value wie oben beschrieben als Familie definiert ist.

Der Punkt dieser Beispiele ist, dass _echte Fehler gemacht werden_ wegen der Ungenauigkeit der interface{} .

var x int | float64 | string | rune
z = int(x) + int(y)
z = float64(x) + float64(y)

Dies sollte definitiv ein Compilerfehler sein, da der Typ von x nicht wirklich kompatibel mit dem ist, was an int() .

Ich mag die Idee, family . Es wäre im wesentlichen eine Schnittstelle eingeschränkt (schnürt?) Zu den aufgeführten Typen und die Compiler können Sie es sich gleich gegen die ganze Zeit sicherzustellen und ändern die Art der Variablen im lokalen Kontext des entsprechenden case .

Das Problem ist, dass wir uns bei Garantien oft auf Kommentare verlassen müssen und
Verhaltensverträge, die der Compiler nicht durchsetzen kann.

Das ist eigentlich der Grund, warum ich angefangen habe, Dinge wie nicht zu mögen

func foo() (..., error) 

weil Sie keine Ahnung haben, welche Art von Fehler es zurückgibt.

und ein paar andere Dinge, die eine Schnittstelle anstelle eines konkreten Typs zurückgeben. Einige Funktionen
return net.Addr und es ist manchmal etwas schwierig, den Quellcode zu durchsuchen, um herauszufinden, welche Art von net.Addr tatsächlich zurückgegeben wird, und es dann entsprechend zu verwenden. Die Rückgabe eines konkreten Typs hat nicht wirklich große Nachteile (da er die Schnittstelle implementiert und daher überall dort verwendet werden kann, wo die Schnittstelle verwendet werden kann), außer wenn Sie
planen Sie später, Ihre Methode zu erweitern, um eine andere Art von net.Addr . Aber wenn du
API erwähnt, dass es OpError zurückgibt, warum dann nicht diesen Teil der "Kompilierungszeit"-Spezifikation machen?

Zum Beispiel:

 OpError is the error type usually returned by functions in the net package. It describes the operation, network type, and address of an error. 

In der Regel? Sagt Ihnen nicht genau, welche Funktionen diesen Fehler zurückgeben. Und dies ist die Dokumentation für den Typ, nicht die Funktion. Die Dokumentation für Read erwähnt nirgendwo, dass OpError zurückgegeben wird. Auch wenn du es tust

err := blabla.(*OpError)

es stürzt ab, sobald es eine andere Art von Fehler zurückgibt. Deshalb würde ich dies gerne als Teil der Funktionsdeklaration sehen. Mindestens *OpError | error würde dir sagen, dass es zurückkehrt
ein solcher Fehler und der Compiler stellt sicher, dass Sie in Zukunft keine ungeprüfte Typzusicherung durchführen, die Ihr Programm zum Absturz bringt.

Übrigens: Wurde schon ein System wie Haskells Typpolymorphismus in Betracht gezogen? Oder ein auf „Eigenschaften“ basierendes Typensystem, dh:

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 bedeutet "was auch immer der Typ von a ist, es muss eine Funktion add(typeof a, typeof a) typeof a) existieren". < widgets.draw() error> bedeutet, dass "egal welcher Widget-Typ eine Methode Draw bereitstellen muss, die einen Fehler zurückgibt". Dies würde es ermöglichen, allgemeinere Funktionen zu erstellen:

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
}

(Beachten Sie, dass dies nicht mit herkömmlichen "Generika" gleichzusetzen ist).

Die Rückgabe eines konkreten Typs hat nicht wirklich große Nachteile (da er die Schnittstelle implementiert und daher überall dort verwendet werden kann, wo die Schnittstelle verwendet werden kann), außer wenn Sie später vorhaben, Ihre Methode zu erweitern, um eine andere Art von net.Addr .

Außerdem hat Go keine Varianten-Untertypisierung, sodass Sie ein func() *FooError als func() error wenn dies erforderlich ist. Was für die Schnittstellenzufriedenheit besonders wichtig ist. Und schließlich kompiliert das nicht:

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)
    }
}

Dh damit dies funktioniert (ich würde gerne, wenn wir irgendwie könnten) bräuchten wir eine viel ausgefeiltere Typinferenz - derzeit verwendet Go nur lokale Typinformationen aus einem einzigen Ausdruck. Nach meiner Erfahrung sind solche Typinferenzalgorithmen nicht nur deutlich langsamer (verlangsamen die Kompilierung und normalerweise nicht einmal die begrenzte Laufzeit), sondern erzeugen auch weit weniger verständliche Fehlermeldungen.

Außerdem verfügt Go nicht über Variant-Subtyping, sodass Sie bei Bedarf keinen func() *FooError als func()-Fehler verwenden können. Was für die Schnittstellenzufriedenheit besonders wichtig ist. Und schließlich kompiliert das nicht:

Ich hätte erwartet, dass dies in Go gut funktioniert, aber ich bin noch nie darüber gestolpert, da die derzeitige Praxis darin besteht, einfach error . Aber ja, in diesen Fällen zwingen Sie diese Einschränkungen praktisch dazu, error als Rückgabetyp zu verwenden.

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)
    }
}

Mir ist keine Sprache bekannt, die dies zulässt (naja, außer Esolangs), aber alles, was Sie tun müssen, ist eine "Typwelt" zu führen (die im Grunde eine Karte von variable -> type ) und wenn Sie es sind - Weisen Sie der Variablen zu, deren Typ Sie gerade in der "Typenwelt" aktualisieren.

Ich glaube nicht, dass Sie dafür komplizierte Typrückschlüsse benötigen, aber Sie müssen die Variablentypen im Auge behalten, aber ich gehe davon aus, dass Sie das sowieso tun müssen, weil

var int i = 0;
i = "hi";

Sie müssen sich sicher irgendwie merken, welche Variablen/Deklarationen welche Typen haben und für i = "hi" Sie auf i eine "Typsuche" durchführen, um zu prüfen, ob Sie ihr einen String zuweisen können.

Gibt es praktische Probleme, die das Zuweisen eines func () *ConcreteError zu einem func() error erschweren, außer dass die Typprüfung dies nicht unterstützt (wie Laufzeitgründe / Gründe für kompilierten Code)? Ich denke, derzeit müsstest du es in eine Funktion wie diese packen:

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
}

Wenn Sie mit einem func (a, b) c konfrontiert sind, aber ein func (x, y) z , müssen Sie nur prüfen, ob z c (und a , b müssen x , y zuweisbar sein), was zumindest auf Typebene keine komplizierte Typinferenz beinhaltet (es geht nur darum zu prüfen, ob ein Typ ist einem anderen Typ zuordenbar/kompatibel). Natürlich, ob dies Probleme mit der Laufzeit/Kompilierung verursacht ... Ich weiß es nicht, aber zumindest auf der Typebene sehe ich nicht, warum dies komplizierte Typrückschlüsse beinhalten würde. Der Typprüfer weiß bereits, ob x a zugewiesen werden kann und weiß daher auch leicht, ob func () x func () a zugewiesen werden kann. Natürlich kann es praktische Gründe geben (wenn man an Laufzeitdarstellungen denkt), warum dies nicht ohne weiteres möglich ist. (Ich vermute, das ist hier der eigentliche Knackpunkt, nicht die eigentliche Typüberprüfung).

Theoretisch könnten Sie die Laufzeitprobleme (sofern vorhanden) mit automatischen Wrapping-Funktionen (wie im obigen Snippet) umgehen, mit dem _potenziell riesigen_ Nachteil, dass es Vergleiche von funcs mit funcs vermasselt (da die verpackte func nicht gleich der func ist). es wickelt).

Mir ist keine Sprache bekannt, die dies zulässt (naja, außer Esolangs)

Nicht genau, aber ich würde argumentieren, dass das daran liegt, dass Sprachen mit mächtigen Typsystemen normalerweise funktionale Sprachen sind, die keine Variablen wirklich verwenden (und daher nicht wirklich die Möglichkeit benötigen, Bezeichner wiederzuverwenden). FWIW, ich würde argumentieren, dass zB Haskells Typsystem damit gut umgehen kann - zumindest solange Sie keine anderen Eigenschaften von FooError oder BarError , sollte es ableiten, dass err vom Typ error und damit umgehen. Auch dies ist natürlich hypothetisch, denn genau diese Situation lässt sich nicht ohne weiteres auf eine funktionale Sprache übertragen.

Aber ich gehe davon aus, dass du das sowieso tun musst, weil

Der Unterschied besteht darin, dass in Ihrem Beispiel i nach der ersten Zeile einen klaren und gut verstandenen Typ hat, der int und Sie dann auf einen Typfehler stoßen, wenn Sie ein string zuweisen

Gibt es praktische Probleme, die das Zuweisen eines func () *ConcreteError zu einem func() error erschweren, außer dass die Typprüfung dies nicht unterstützt (wie Laufzeitgründe / Gründe für kompilierten Code)?

Es gibt praktische Probleme, aber ich glaube, für func sie wahrscheinlich lösbar (durch die Ausgabe von un/-wrapping-Code, ähnlich wie beim Interface-Passing). Ich habe ein wenig über Varianz in Go geschrieben und einige der praktischen Probleme erklärt, die ich unten sehe. Ich bin jedoch nicht ganz davon überzeugt, dass es eine Ergänzung wert ist. Dh ich bin mir nicht sicher, ob es wichtige Probleme von selbst löst.

mit dem potenziell großen Nachteil, dass es Vergleiche von funcs mit funcs vermasselt (da die verpackte func nicht gleich der func ist, die sie umhüllt).

Funktionen sind nicht vergleichbar.

Wie auch immer, TBH, all dies scheint für dieses Thema ein bisschen off-topic zu sein :)

Zu Ihrer Information: Ich habe das gerade getan . Es ist nicht schön, aber es ist sicher typsicher. (Dasselbe kann für #19814 FWIW gemacht werden)

Ich bin etwas spät zur Party, aber auch ich möchte meine Gefühle nach 4 Jahren Go mit euch teilen:

  • Multi-Value-Retouren waren ein großer Fehler.
  • Nullfähige Schnittstellen waren ein Fehler.
  • Zeiger sind keine Synonyme für "optional", stattdessen hätten diskriminierte Unions verwendet werden sollen.
  • Der JSON-Unmarshaller hätte einen Fehler zurückgeben müssen, wenn ein erforderliches Feld nicht im JSON-Dokument enthalten ist.

In den letzten 4 Jahren habe ich viele damit verbundene Probleme festgestellt:

  • Garbage-Daten werden im Fehlerfall zurückgegeben.
  • Syntax-Clutter (Rückgabe von Nullwerten im Fehlerfall).
  • Multi-Error-Returns (verwirrende APIs, bitte, tun Sie das nicht!).
  • Nicht-Null-Schnittstellen, die auf Zeiger verweisen, die auf nil (verwirrt die Leute, die die Aussage "Go is a easy language" wie ein schlechter Witz klingen lassen).
  • ungeprüfte JSON-Felder lassen Server abstürzen (yey!).
  • ungeprüfte zurückgegebene Zeiger lassen Server abstürzen, aber niemand hat dokumentiert, dass der zurückgegebene Zeiger einen optionalen (vielleicht-Typ) darstellt und daher nil (yey!)

Die Änderungen, die erforderlich sind, um all diese Probleme zu beheben, würden jedoch eine wirklich abwärtsinkompatible Version von Go 2.0.0 (nicht Go2) erfordern, die vermutlich nie realisiert werden wird. Trotzdem...

So hätte die Fehlerbehandlung aussehen sollen:

// 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)
  }
}

Schnittstellen sind kein Ersatz für diskriminierte Gewerkschaften , sie sind zwei völlig unterschiedliche Tiere. Der Compiler stellt sicher, dass Typwechsel bei diskriminierten Unions abgeschlossen sind, was bedeutet, dass die Fälle alle möglichen Typen abdecken. Wenn Sie dies nicht möchten, können Sie die Typzusicherungsanweisung verwenden.

Zu oft habe ich gesehen, dass Leute total verwirrt waren über _Nicht-Null-Schnittstellen zu Null-Werten_ : https://play.golang.org/p/JzigZ2Q6E6F. Normalerweise sind die Leute verwirrt, wenn eine error Schnittstelle auf einen Zeiger mit einem benutzerdefinierten Fehlertyp zeigt, der auf nil zeigt.

Ein Interface ist wie eine Empfangsdame, Sie wissen, dass es ein Mensch ist, wenn Sie mit ihm sprechen, aber in Go könnte es eine Pappfigur sein und die Welt bricht plötzlich zusammen, wenn Sie versuchen, mit ihm zu sprechen.

Diskriminiert Unions hätten für optionale Elemente (vielleicht-Typen) verwendet werden sollen und die Übergabe von nil Zeigern an Schnittstellen hätte zu einer Panik führen sollen:

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)
  }
}

Zeiger und Vielleicht-Typen sind nicht austauschbar. Die Verwendung von Zeigern für optionale Typen ist schlecht, da dies zu verwirrenden APIs führt:

// 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 {}

Dann gibt es noch JSON. Dies könnte jedoch bei Unions niemals passieren, da der Compiler Sie zwingt, sie vor der Verwendung zu überprüfen . Der JSON-Unmarshaller sollte fehlschlagen, wenn ein erforderliches Feld (einschließlich Feldern vom Zeigertyp) nicht im JSON-Dokument enthalten ist:

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
Ich arbeite derzeit auch an einem funktionalen Sprachdesign und verwende dort diskriminierte Unions für die Fehlerbehandlung:

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)
}

Ich würde gerne sehen, dass dies eines Tages wahr wird. Mal sehen, ob ich ein bisschen helfen kann:

Vielleicht ist das Problem, dass wir versuchen, mit dem Vorschlag zu viel abzudecken. Wir könnten eine vereinfachte Version wählen, die den größten Teil des Wertes bringt, so dass es viel einfacher wäre, ihn kurzfristig in die Sprache aufzunehmen.

Aus meiner Sicht wäre diese

  1. Erlaube nur die |
    <any pointer type> | nil
    Wo ein Zeigertyp wäre: Zeiger, Funktionen, Kanäle, Slices und Maps (die Go-Zeigertypen)
  2. Verbieten Sie die Zuweisung von nil zu einem bloßen Zeigertyp. Wenn Sie nil zuweisen möchten, muss der Typ <pointer type> | nil . Zum Beispiel:
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.

Das sind die wichtigsten Ideen. Die folgenden sind die abgeleiteten Ideen aus den wichtigsten:

  1. Sie können eine Variable mit einem bloßen Zeigertyp nicht deklarieren und sie nicht initialisiert lassen. Wenn Sie dies tun möchten, müssen Sie den diskriminierten Typ | nil hinzufügen
var maybeAString *string       // Wrong: invalid initial value
var maybeAString *string | nil // Good
  1. Sie können einem "nilable" Zeigertyp einen nackten Zeigertyp zuweisen, aber nicht umgekehrt:
var value int = 42
var barePointer *int = &value          // Valid
var nilablePointer *int | nil = &value // Valid

nilablePointer = barePointer // Valid
barePointer = nilablePointer // Invalid: Incompatible types
  1. Die einzige Möglichkeit, den Wert aus einem "nilablen" Zeigertyp zu erhalten, ist über den Typschalter, wie andere darauf hingewiesen haben. Wenn wir beispielsweise dem obigen Beispiel folgen und wirklich den Wert von nilablePointer barePointer zuweisen möchten, müssen wir Folgendes tun:
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
}

Und das ist es. Ich weiß, dass diskriminierte Vereinigungen für viel mehr verwendet werden können (insbesondere im Fall von zurückgegebenen Fehlern), aber ich würde sagen, dass wir, wenn wir uns an das halten, was ich oben geschrieben habe, mit weniger Aufwand und ohne einen RIESIGEN Wert in die Sprache bringen würden komplizierter als nötig.
Vorteile sehe ich bei diesem einfachen Vorschlag:

  • a) Keine Null-Zeiger-Fehler . Okay, nie haben 4 Wörter so viel bedeutet. Deshalb habe ich das Bedürfnis, es von einem anderen Standpunkt aus zu sagen: Das No-Go-Programm wird _EVER_ wieder einen nil pointer dereference Fehler haben! 💥
  • b) Sie können Zeiger auf Funktionsparameter übergeben, ohne "Leistung vs. Absicht" zu handeln .
    Was ich damit meine ist, dass ich manchmal eine Struktur an eine Funktion übergeben möchte und keinen Zeiger darauf, weil ich nicht möchte, dass sich diese Funktion Sorgen über die Nichtigkeit macht und sie zwingt, die Parameter zu überprüfen . Normalerweise übergebe ich jedoch einen Zeiger, um den Kopieraufwand zu vermeiden.
  • c) Keine Nullkarten mehr! JA! Wir werden mit der Inkonsistenz über die "sicheren Nil-Slices" und die "unsicheren Nil-Karten" enden (die in Panik geraten, wenn Sie versuchen, ihnen zu schreiben). Eine Karte wird entweder initialisiert oder hat den Typ map | nil , in diesem Fall müssen Sie einen Typschalter verwenden 😃

Aber es gibt hier auch noch einen anderen immateriellen Wert, der viel Wert bringt: die Seelenfrieden des Entwicklers . Sie können mit Zeigern, Funktionen, Kanälen, Karten usw. arbeiten und spielen mit dem entspannten Gefühl, dass Sie sich keine Sorgen machen müssen, dass sie leer sind. _Ich würde dafür bezahlen!_ 😂

Ein Vorteil, mit dieser einfacheren Version des Vorschlags zu beginnen, besteht darin, dass sie uns nicht davon abhält, in Zukunft den vollständigen Vorschlag zu erstellen oder sogar Schritt für Schritt vorzugehen (für mich der nächste natürliche Schritt, um diskriminierte Fehlerrückgaben zu ermöglichen). , aber vergessen wir das jetzt).

Ein Problem besteht darin, dass selbst diese einfache Version des Vorschlags abwärtskompatibel ist, aber leicht mit gofix behoben werden kann: Ersetzen Sie einfach alle Zeigertypdeklarationen durch <pointer type> | nil .

Was denken Sie? Ich hoffe, dies könnte etwas Licht ins Dunkel bringen und die Aufnahme von Nullsicherheit in die Sprache beschleunigen. Es scheint, dass dieser Weg (durch die "diskriminierten Vereinigungen") der einfachere und orthogonalere Weg ist, dies zu erreichen.

@alvaroloes

Sie können eine Variable vom Typ bloßer Zeiger nicht deklarieren und sie nicht initialisiert lassen.

Das ist der springende Punkt. Das macht Go einfach nicht - jeder Typ hat einen Nullwert, Punkt. Sonst müsstest du antworten, was zB make([]T, 100) macht? Andere Dinge, die Sie erwähnen (zB Null-Maps, die beim Schreiben in Panik geraten) sind eine Konsequenz dieser Grundregel. (Und nebenbei gesagt, ich glaube nicht, dass es wirklich richtig ist zu sagen, dass Null-Slices sicherer sind als Karten - das Schreiben auf ein Null-Slice wird genauso in Panik geraten wie das Schreiben auf eine Null-Karte).

Mit anderen Worten: Ihr Vorschlag ist eigentlich gar nicht so einfach, da er ziemlich stark von einer ziemlich grundsätzlichen Designentscheidung in der Go-Sprache abweicht.

Ich denke, das Wichtigste, was Go tut, ist, Nullwerte nützlich zu machen und nicht einfach alles Nullwert zu geben. Nil map ist ein Nullwert, aber es ist nicht nützlich. Es ist tatsächlich schädlich. Warum also nicht den Nullwert verbieten, wenn es nicht nützlich ist. In dieser Hinsicht wäre eine Änderung von Go von Vorteil, aber der Vorschlag ist in der Tat nicht so einfach.

Der obige Vorschlag sieht eher nach einer optionalen/nicht optionalen Art aus, wie in Swift und anderen. Es ist cool und alles, aber:

  1. Das würde so ziemlich jedes Programm da draußen zerstören und der Fix wäre für gofix nicht trivial. Sie können nicht einfach alles durch <pointer type> | nil ersetzen, da dies pro Vorschlag einen Typwechsel erfordern würde, um den Wert zu entpacken.
  2. Damit dies tatsächlich nutzbar und erträglich ist, müsste Go viel mehr syntaktischen Zucker um diese optionalen Elemente herum haben. Nehmen Sie zum Beispiel Swift. Es gibt viele Funktionen in der Sprache, die speziell für die Arbeit mit Optionals geeignet sind - Guard, optionales Binding, optionales Chaining, Nil-Coalescing usw. Ich glaube nicht, dass Go in diese Richtung gehen würde, aber ohne sie wäre die Arbeit mit Optionals eine lästige Pflicht.

Warum also nicht den Nullwert verbieten, wenn es nicht nützlich ist.

Siehe oben. Das bedeutet, dass einige Dinge, die billig aussehen, mit sehr nicht trivialen Kosten verbunden sind.

Go in dieser Hinsicht zu ändern wäre von Vorteil

Es hat Vorteile, aber das ist nicht dasselbe wie von Vorteil. Es hat auch Schaden. Welches Gewicht schwerer ist, hängt von der Präferenz und einem Kompromiss ab. Die Go-Designer haben sich dafür entschieden.

FTR, dies ist ein allgemeines Muster in diesem Thread und eines der wichtigsten Gegenargumente zu jedem Konzept von Summentypen - dass Sie sagen müssen, was der Nullwert ist. Deshalb sollte jede neue Idee explizit darauf eingehen. Aber etwas frustrierenderweise haben die meisten Leute, die heutzutage hier posten, den Rest des Threads nicht gelesen und neigen dazu, diesen Teil zu ignorieren.

🤔 Aha! Ich wusste, dass mir etwas offensichtlich fehlte. Doh! Das Wort „einfach“ hat komplexe Bedeutungen. Ok, du kannst gerne das "einfache" Wort aus meinem vorherigen Kommentar entfernen.

Tut mir leid, wenn es für einige von euch frustrierend war. Meine Absicht war, zu versuchen, ein wenig zu helfen. Ich versuche, mit dem Thread Schritt zu halten, aber ich habe nicht viel Zeit, um mich damit zu beschäftigen.

Zurück zur Sache: Der Hauptgrund, der dies zurückhält, scheint also der Nullwert zu sein.
Nachdem ich eine Weile nachgedacht und viele Optionen verworfen habe, ist das einzige, was meiner Meinung nach einen Mehrwert bieten könnte und erwähnenswert ist, Folgendes:

Wenn ich mich richtig erinnere, besteht der Nullwert eines beliebigen Typs darin, seinen Speicherplatz mit Nullen zu füllen.
Wie Sie bereits wissen, ist dies für Nicht-Zeigertypen in Ordnung, aber es ist eine Fehlerquelle für Zeigertypen:

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

Was ist, wenn wir:

  • Definieren Sie für jeden Zeigertyp einen sinnvollen Nullwert
  • Initialisieren Sie es nur bei der ersten Verwendung (faule Initialisierung).

Ich denke, dies wurde in einer anderen Ausgabe vorgeschlagen, bin mir nicht sicher. Ich schreibe es nur hier, weil es den Haupthindernispunkt dieses Vorschlags anspricht.

Das Folgende könnte eine Liste der Nullwerte für die Zeigertypen sein. Beachten Sie, dass diese Nullwerte nur verwendet werden , wenn auf den Wert zugegriffen wird . Wir könnten es "dynamischer Nullwert" nennen und es ist nur eine Eigenschaft der Zeigertypen:

| Zeigertyp | Nullwert | Dynamischer Nullwert | Kommentar |
| --- | --- | --- | --- |
| *T | nil | neu(T) |
| []T | nil | []T{} |
| map[T]U | nil | map[T]U{} |
| func | nil | nein | Der dynamische Nullwert einer Funktion tut also nichts und gibt Nullwerte zurück. Wenn die Liste der Rückgabewerte in error endet, wird ein Standardfehler zurückgegeben, der besagt, dass die Funktion eine "keine Operation" ist |
| chan T | nil | make(chan T) |
| interface | nil | - | eine Standardimplementierung, bei der alle Methoden mit der oben beschriebenen noop Funktion initialisiert werden |
| diskriminierte Gewerkschaft | nil | dynamischer Nullwert erster Art | |

Wenn diese Typen jetzt initialisiert werden, sind sie nil , wie sie es jetzt sind. Der Unterschied liegt in dem Moment, in dem auf nil zugegriffen wird. In diesem Moment wird der dynamische Nullwert verwendet. Ein paar Beispiele:

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
}

Wahrscheinlich fehlen mir Implementierungsdetails und mögliche Schwierigkeiten, aber ich wollte mich zuerst auf die Idee konzentrieren.

Der Hauptnachteil besteht darin, dass wir jedes Mal, wenn Sie auf den Wert eines Zeigertyps zugreifen, eine zusätzliche Nullprüfung hinzufügen. Aber ich würde sagen:

  • Es ist ein guter Kompromiss für die Vorteile, die wir erhalten. Die gleiche Situation tritt bei den gebundenen Prüfungen bei Array-/Slice-Zugriffen auf, und wir akzeptieren die Zahlung dieser Leistungseinbußen für die damit verbundene Sicherheit.
  • Die nil-Checks könnten auf die gleiche Weise wie bei Arrays gebundene Checks vermieden werden: Wenn der Zeigertyp im aktuellen Gültigkeitsbereich initialisiert wurde, könnte der Compiler dies wissen und vermeiden, den nil-check hinzuzufügen.

Damit haben wir alle Vorteile, die im vorherigen Kommentar erläutert wurden, mit dem Plus, dass wir keinen Typwechsel verwenden müssen, um auf den Wert zuzugreifen (das wäre nur für die diskriminierten Unions), wodurch der Go-Code so sauber bleibt wie ist das jetzt.

Was denken Sie? Entschuldigung, falls dies schon besprochen wurde. Außerdem bin ich mir bewusst, dass dieser Kommentar-Vorschlag mehr mit nil als mit diskriminierten Gewerkschaften zu tun hat. Ich könnte dies auf ein Null-bezogenes Problem verschieben, aber wie gesagt, ich habe es hier gepostet, weil es versucht, das Hauptproblem der diskriminierten Vereinigungen zu beheben: die nützlichen Nullwerte.

Zurück zur Sache: Der Hauptgrund, der dies zurückhält, scheint also der Nullwert zu sein.

Dies ist ein wichtiger technischer Grund, der angegangen werden muss. Für mich ist der Hauptgrund dafür , dass sie schrittweise Reparatur kategorisch unmöglich (siehe oben) zu machen. dh für mich persönlich geht es weniger um die Umsetzung, sondern darum, dass ich grundsätzlich gegen das Konzept bin.
Welcher Grund "hauptsächlich" ist, ist in jedem Fall Geschmacks- und Präferenzsache.

Was ist, wenn wir:

  • Definieren Sie für jeden Zeigertyp einen sinnvollen Nullwert
  • Initialisieren Sie es nur bei der ersten Verwendung (faule Initialisierung).

Dies schlägt fehl, wenn Sie einen Zeigertyp herumgeben. z.B

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
}

Diese Diskussion ist alles andere als neu. Es gibt Gründe dafür, dass sich die Referenztypen so verhalten, wie sie es tun und es ist nicht so, dass die Go-Entwickler nicht darüber nachgedacht haben :)

Das ist der springende Punkt. Das macht Go einfach nicht - jeder Typ hat einen Nullwert, Punkt. Ansonsten müsstest du was beantworten, zB make([]T, 100)?

Dies (und new(T) ) müsste verboten werden, wenn T keinen Nullwert hat. Sie müssten make([]T, 0, 100) und dann append , um das Slice zu füllen. Größere Aufteilungen ( v[:0][:100] ) müsste ebenfalls ein Fehler sein. [10]T wäre im Grunde ein unmöglicher Typ (es sei denn, der Sprache wird die Fähigkeit hinzugefügt, ein Slice für einen Array-Zeiger zu bestätigen). Und Sie benötigen eine Möglichkeit, vorhandene nilbare Typen als nicht nilbar zu markieren, um die Abwärtskompatibilität aufrechtzuerhalten.

Dies würde ein Problem darstellen, wenn Generika hinzugefügt werden, da Sie alle Typparameter so behandeln müssen, als hätten sie keinen Nullwert, es sei denn, sie erfüllen eine bestimmte Grenze. Eine Teilmenge von Typen würde im Grunde auch überall eine Initialisierungsverfolgung benötigen. Dies wäre eine ziemlich große Änderung für sich allein, auch ohne darüber hinaus Summentypen hinzuzufügen. Es ist sicherlich machbar, aber es trägt erheblich zur Kostenseite einer Kosten-Nutzen-Analyse bei. Die bewusste Entscheidung, die Initialisierung einfach zu halten ("es gibt immer einen Nullwert") würde stattdessen dazu führen, dass die Initialisierung komplexer wird, als wenn die Initialisierungsverfolgung in der Sprache vom ersten Tag an erfolgt wäre.

Dies ist ein wichtiger technischer Grund, der angegangen werden muss. Für mich ist der Hauptgrund, dass sie eine schrittweise Reparatur kategorisch unmöglich machen (siehe oben). dh für mich persönlich geht es weniger um die Umsetzung, sondern darum, dass ich grundsätzlich gegen das Konzept bin.
Welcher Grund "hauptsächlich" ist, ist in jedem Fall Geschmacks- und Präferenzsache.

Okay, das verstehe ich. Wir müssen nur den Standpunkt anderer Leute sehen (ich sage nicht, dass Sie das nicht tun, ich mache nur einen Punkt :wink:) wo sie dies als etwas Mächtiges sehen, um ihre Programme zu schreiben. Passt es zu Go? Es hängt davon ab, wie die Idee umgesetzt und in die Sprache integriert wird, und das versuchen wir alle in diesem Thread (glaube ich)

Dies schlägt fehl, wenn Sie einen Zeigertyp herumgeben. z.B (...)

Ich verstehe das nicht ganz. Warum ist dies ein Misserfolg? Sie übergeben nur einen Wert an den Funktionsparameter, der zufällig ein Zeiger mit dem nil ist. Dann ändern Sie diesen Wert innerhalb der Funktion. Es wird erwartet, dass Sie diese Effekte außerhalb der Funktion nicht sehen. Lassen Sie mich einige Beispiele kommentieren:

// 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
}

Eine ähnliche Situation tritt bei Nicht-Zeiger-Empfänger-Methoden auf, und es ist für Neulinge verwirrend, Go zu verwenden (aber wenn Sie es einmal verstanden haben, macht es Sinn):

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
}

Wir müssen uns also entscheiden zwischen:

  • A) Ausfall mit Absturz
  • B) Fehler mit einer stillen Nicht-Modifikation des Wertes, auf den ein Zeiger zeigt, wenn dieser Zeiger an eine Funktion übergeben wird.

Die Lösung für beide Fälle ist die gleiche: Überprüfen Sie auf nil, bevor Sie etwas tun. Aber für mich ist A) viel schädlicher (die gesamte Anwendung stürzt ab!).
B) könnte als "stiller Fehler" angesehen werden, aber ich würde es nicht als Fehler betrachten. Es passiert nur, wenn Sie Zeiger an Funktionen übergeben, und wie ich gezeigt habe, gibt es Fälle mit Strukturen, die sich ähnlich verhalten. Dies ohne die enormen Vorteile zu berücksichtigen, die es mit sich bringt.

Hinweis: Ich versuche nicht blind "meine" Idee zu verteidigen, ich versuche wirklich Go zu verbessern (was schon sehr gut ist). Wenn es noch andere Punkte gibt, die die Idee nicht wert machen, dann habe ich keine Lust, sie wegzuwerfen und weiter in andere Richtungen zu denken

Anmerkung 2: Letztendlich gilt diese Idee nur für "Null"-Werte und hat nichts mit diskriminierten Gewerkschaften zu tun. Also werde ich ein anderes Problem erstellen, um dieses nicht zu verschmutzen

Okay, das verstehe ich. Wir müssen einfach auch den Standpunkt der anderen sehen (ich sage nicht, dass du das nicht tust, ich mache nur einen Punkt )

Dieses Schwert schneidet jedoch in beide Richtungen. Du sagtest "der Hauptgrund, das zurückzuhalten, war". Diese Aussage impliziert, dass wir uns alle einig sind, ob wir die Wirkung dieses Vorschlags haben wollen. Ich kann sicherlich zustimmen, dass es sich um ein technisches Detail handelt, das die spezifischen Vorschläge zurückhält (oder zumindest, dass jeder Vorschlag etwas zu dieser Frage sagen sollte) ) Aber ich mag es nicht, wenn die Diskussion stillschweigend in eine Parallelwelt umgestaltet wird, in der wir davon ausgehen, dass jeder sie wirklich will .

Warum ist dies ein Misserfolg?

Denn eine Funktion, die einen Pointer entgegennimmt, verspricht zumindest oft, den Pointee zu modifizieren. Wenn die Funktion dann stillschweigend nichts macht, würde ich das als Bug bezeichnen. Oder zumindest ist es ein einfaches Argument, dass Sie, indem Sie auf diese Weise eine Null-Panik verhindern, eine neue Klasse von Fehlern einführen.

Wenn Sie einen Null-Zeiger an eine Funktion übergeben, die dort etwas erwartet, ist das ein Fehler - und ich sehe nicht den tatsächlichen Wert, eine solche fehlerhafte Software stillschweigend fortzusetzen. Ich kann den Wert der ursprünglichen Idee sehen, diesen Fehler zur Kompilierzeit abzufangen, indem man nicht nilbare Zeiger unterstützt, aber ich sehe keinen Sinn darin, zuzulassen, dass dieser Fehler überhaupt nicht abgefangen wird.

dh Sie sprechen sozusagen ein anderes Problem an als den eigentlichen Vorschlag von nicht nilbaren Zeigern: Bei diesem Vorschlag ist die Laufzeitpanik nicht das Problem, sondern nur ein Symptom - das Problem ist der Fehler beim versehentlichen Vorbeigehen nil zu etwas, das es nicht erwartet und dass dieser Fehler nur zur Laufzeit abgefangen wird.

Eine ähnliche Situation tritt bei Nicht-Zeiger-Empfänger-Methoden auf

Ich kaufe diese Analogie nicht. IMO ist es absolut vernünftig zu überlegen

func Foo(p *int) { *p = 42 }

func main() {
    var v int
    Foo(&v)
    if v != 42 { panic("") }
}

korrekter Code sein. Ich denke nicht, dass es vernünftig ist, darüber nachzudenken

func Foo(v int) { v = 42 }

func main( ){
    var v int
    Foo(v)
    if v != 42 { panic("") }
}

richtig liegen. Vielleicht, wenn Sie ein absoluter Anfänger in Go sind und aus einer Sprache kommen, in der jeder Wert eine Referenz ist (obwohl es mir ehrlich gesagt schwerfällt, einen zu finden - sogar Python und Java machen nur die meisten Wertereferenzen). Aber IMO, Optimierung für diesen Fall ist zwecklos, es ist fair anzunehmen, dass die Leute einige Vertrautheit mit Zeigern und Werten haben. Ich denke, selbst ein erfahrener Go-Entwickler würde beispielsweise eine Methode mit einem Zeigerempfänger, der auf ihre Felder zugreift, als korrekt ansehen und Code, der diese Methoden aufruft, als korrekt ansehen. Tatsächlich ist das das ganze Argument dafür, nil -Zeiger statisch zu verhindern, dass es zu leicht ist, unbeabsichtigt einen Zeiger auf Null zu setzen und korrekt aussehender Code zur Laufzeit fehlzuschlagen.

Die Lösung für beide Fälle ist die gleiche: Überprüfen Sie auf nil, bevor Sie etwas tun.

IMO besteht die Lösung in der aktuellen Semantik darin, nicht nach nil zu suchen und es als Fehler zu betrachten, wenn jemand nil übergibt. Wie, in deinem Beispiel schreibst du

// The correct code would be:
if p == nil {
    // panic or return error
}
*p = 42

Aber ich halte diesen Code nicht für richtig. Der nil -Check macht nichts, weil die Dereferenzierung von nil bereits in Panik gerät.

Aber für mich ist A) viel schädlicher (die gesamte Anwendung stürzt ab!).

Das ist in Ordnung, aber denken Sie daran, dass viele Leute diesbezüglich stark anderer Meinung sind. Ich persönlich halte einen Absturz immer für besser, als mit korrupten Daten und falschen Annahmen fortzufahren. In einer idealen Welt hat meine Software keine Fehler und stürzt nie ab. In einer weniger idealen Welt haben meine Programme Fehler und versagen sicher, indem sie abstürzen, wenn sie erkannt werden. In der schlimmsten Welt werden meine Programme Fehler haben und einfach weiterhin Chaos anrichten, wenn sie auftreten.

Dieses Schwert schneidet jedoch in beide Richtungen. Sie sagten: "Der Hauptgrund, dies zurückzuhalten, war". Diese Aussage impliziert, dass wir uns alle einig sind, ob wir die Wirkung dieses Vorschlags haben wollen. Ich kann sicherlich zustimmen, dass es sich um ein technisches Detail handelt, das die spezifischen Vorschläge zurückhält (oder zumindest sollte jeder Vorschlag etwas zu dieser Frage sagen). Aber ich mag es nicht, wenn die Diskussion stillschweigend in eine Parallelwelt umgestaltet wird, in der wir davon ausgehen, dass jeder sie wirklich will.

Nun, das wollte ich nicht andeuten. Wenn das so verstanden wurde, habe ich vielleicht nicht die richtigen Worte gewählt und entschuldige mich. Ich wollte nur ein paar Ideen für eine mögliche Lösung liefern, das war's.

Ich schrieb _"... es scheint, dass der Hauptgrund dafür ist, dass dies zurückgehalten wird..."_ basierend auf Ihrem Satz _"Das ist der springende Punkt"_ bezogen auf den Nullwert. Deshalb bin ich davon ausgegangen, dass der Nullwert die Hauptsache ist, die dies zurückhält. Also meine schlechte Annahme.

In Bezug nil stille Behandlung von
Wenn wir uns nur auf das nil-bezogene Problem konzentrieren, würde ich es vorziehen, nicht-nil-Zeigertypen zur Kompilierzeit überprüfen zu lassen.

Ich würde sagen, dass wir (mit "wir" beziehe ich mich auf die gesamte Go-Community) irgendwann _irgendwie_ Veränderungen akzeptieren müssen. Zum Beispiel: Wenn es eine gute Lösung gibt, um nil Fehler vollständig zu vermeiden, und das, was dies zurückhält, ist die Designentscheidung "Alle Typen haben den Wert Null und bestehen aus Nullen", dann könnten wir die Idee in Betracht ziehen einige Optimierungen oder Änderungen an dieser Entscheidung vorzunehmen, wenn dies von Wert ist.

Der Hauptgrund, warum ich dies sage, ist Ihr Satz _"jeder Typ hat einen Nullwert, Punkt "_. Normalerweise schreibe ich nicht gerne "Punkte". Versteh mich nicht falsch! Ich akzeptiere voll und ganz, dass Sie so denken, es ist nur meine Denkweise: Ich bevorzuge keine Dogmen, da sie Wege verbergen können, die zu besseren Lösungen führen können.

Abschließend noch dazu:

Das ist in Ordnung, aber denken Sie daran, dass viele Leute diesbezüglich stark anderer Meinung sind. Ich persönlich halte einen Absturz immer für besser, als mit korrupten Daten und falschen Annahmen fortzufahren. In einer idealen Welt hat meine Software keine Fehler und stürzt nie ab. In einer weniger idealen Welt haben meine Programme Fehler und versagen sicher, indem sie abstürzen, wenn sie erkannt werden. In der schlimmsten Welt werden meine Programme Fehler haben und einfach weiterhin Chaos anrichten, wenn sie auftreten.

Dem stimme ich voll und ganz zu. Laut zu versagen ist immer besser als still zu versagen. Go hat jedoch einen Haken:

  • Wenn Sie eine App mit Tausenden von Goroutinen haben, führt eine unbehandelte Panik in einer davon zum Absturz des gesamten Programms. Das ist anders als in anderen Sprachen, wo nur der Thread abstürzt, der in Panik gerät

Abgesehen davon (obwohl es ziemlich gefährlich ist), besteht die Idee darin, eine ganze Kategorie von Fehlern ( nil -bezogene Fehler) zu vermeiden.

Lassen Sie uns also weiter iterieren und versuchen, eine Lösung zu finden.

Vielen Dank für Ihre Zeit und Energie!

Ich würde gerne die diskriminierte Unions-Syntax von Rust und nicht die Summentypen von Haskell sehen, die die Benennung von Varianten und einen besseren Syntaxvorschlag für den Mustervergleich ermöglicht.
Die Implementierung kann wie struct mit Tag-Feld (uint-Typ, hängt von der Anzahl der Varianten) und Union-Feld (das die Daten enthält) erfolgen.
Diese Funktion ist für einen geschlossenen Satz von Varianten erforderlich (die Zustandsdarstellung wäre viel einfacher und sauberer, mit Überprüfung der Kompilierungszeit). Nach Fragen zu Schnittstellen und ihrer Darstellung denke ich, dass ihre Implementierung im Summentyp nicht mehr als nur ein weiterer Fall des Summentyps sein darf, da es sich bei der Schnittstelle um jeden Typ handelt, der einige Anforderungen erfüllt, der Anwendungsfall des Summentyps jedoch anders ist.

Syntax:

type Type enum {
         Tuple (int,int),
         One int,
         None,
};

Im obigen Beispiel wäre die Größe sizeof((int,int)).
Der Musterabgleich kann mit einem neu erstellten Match-Operator oder innerhalb eines bestehenden Switch-Operators durchgeführt werden, genau wie:

var a Type
switch (a) {
         case Tuple{(b,c)}:
                    //do something
         case One{b}:
                    //do something else
         case None:
                    //...
}

Erstellungssyntax:
var a Type = Type{One=12}
Beachten Sie, dass beim Aufbau von Enumerationsinstanzen nur eine Variante angegeben werden kann.

Nullwert (Problem):
Wir können Namen in alphabetischer Reihenfolge sortieren, der Nullwert der Aufzählung ist der Nullwert des Typs des ersten Elements in der sortierten Elementliste.

PS Lösung des Nullwertproblems wird meistens durch Vereinbarung definiert.

Ich denke, es wäre vielleicht weniger verwirrend, den Nullwert der Summe als Nullwert des ersten benutzerdefinierten Summenfelds beizubehalten

Ich denke, es wäre vielleicht weniger verwirrend, den Nullwert der Summe als Nullwert des ersten benutzerdefinierten Summenfelds beizubehalten

Aber wenn der Wert Null von der Reihenfolge der Felddeklaration abhängt, denke ich, dass es schlimmer ist.

Jemand hat ein Designdokument geschrieben?

Ich habe eins:
19412-diskriminated_unions_and_pattern_matching.md.zip

Ich habe das geändert:

Ich denke, es wäre vielleicht weniger verwirrend, den Nullwert der Summe als Nullwert des ersten benutzerdefinierten Summenfelds beizubehalten

Jetzt in meinem Vorschlag wurde die Vereinbarung über den Nullwert (Problem) auf die Position von Urandoms verschoben.

UPD: Designdokument geändert, kleinere Korrekturen.

Ich habe zwei aktuelle Anwendungsfälle, in denen ich integrierte Summentypen benötigte:

  1. AST-Baumdarstellung, wie erwartet. Anfangs fand man eine Bibliothek, die auf den ersten Blick eine Lösung war, aber ihr Ansatz bestand darin, eine große Struktur mit vielen nilbaren Feldern zu verwenden. Das Schlimmste aus beiden Welten IMO. Natürlich keine Typensicherheit. Habe stattdessen unsere eigenen geschrieben.
  2. Hatte eine Warteschlange vordefinierter Hintergrundaufgaben: Wir haben einen Suchdienst, der gerade in Entwicklung ist und unsere Suchoperationen könnten zu lang sein usw. Also haben wir uns entschieden, sie im Hintergrund auszuführen, indem wir Suchindexoperationsaufgaben in einen Kanal senden. Dann entscheidet ein Disponent, was weiter mit ihnen zu tun ist. Könnte Besuchermuster verwenden, aber es ist offensichtlich ein Overkill für eine einfache gRPC-Anfrage. Und es ist zumindest nicht besonders klar, da es eine Verbindung zwischen einem Disponenten und einem Besucher herstellt.

In beiden Fällen so etwas implementiert (am Beispiel der 2. Aufgabe):

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() {}

Und dann

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:
    …
}

Das ist fast gut. Der Nachteil, dass Go keine vollständige Typsicherheit bietet, dh es wird kein Kompilierungsfehler auftreten, nachdem die neue Suchindexoperationsaufgabe hinzugefügt würde.

IMHO ist die Verwendung von Summentypen die klarste Lösung für diese Art von Aufgaben, die normalerweise mit Besuchern und Disponenten gelöst werden, bei denen die Besucherfunktionen nicht zahlreich und klein sind und der Besucher selbst ein fester Typ ist.

Ich glaube wirklich, dass ich so etwas habe wie

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:
}

wäre im Geiste viel mehr Goish als jeder andere Ansatz, den Go in seinem gegenwärtigen Zustand zulässt. Haskellish-Musterabgleich ist nicht erforderlich, es ist mehr als genug, bis zu einem bestimmten Typ zu tauchen.

Autsch, den Sinn des Syntaxvorschlags verfehlt. Repariere es.

Zwei Versionen, eine für den generischen Summentyp und den Summentyp für Aufzählungen:

Generische Summentypen

type Sum oneof {
    T₁ TypeDecl₁
    T₂ TypeDecl₂
    …
    Tₙ TypeDeclₙ
}

wobei T₁Tₙ Typdefinitionen auf derselben Ebene mit Sum ( oneof macht sie außerhalb ihres Geltungsbereichs verfügbar) und Sum deklariert eine Schnittstelle, die nur T₁Tₙ erfüllt.

Die Verarbeitung ist ähnlich wie bei dem (type) Schalter, außer dass sie implizit über oneof Objekten durchgeführt wird und es eine Compilerprüfung geben muss, ob alle Varianten aufgelistet wurden.

Sichere Aufzählungen des Realtyps

type Enum oneof {
    Value = iota
}

ziemlich ähnlich zu iota von consts, außer dass nur explizit aufgeführte Werte Enums sind und alles andere nicht.

switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

wäre im Geiste viel mehr Goish als jeder andere Ansatz, den Go in seinem gegenwärtigen Zustand zulässt. Haskellish-Musterabgleich ist nicht erforderlich, es ist mehr als genug, bis zu einem bestimmten Typ zu tauchen.

Ich denke nicht, dass die Manipulation der Bedeutung der Variablen task eine gute Idee ist, obwohl sie akzeptabel ist.

```go
switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

wäre im Geiste viel mehr Goish als jeder andere Ansatz, den Go in seinem gegenwärtigen Zustand zulässt. Haskellish-Musterabgleich ist nicht erforderlich, es ist mehr als genug, bis zu einem bestimmten Typ zu tauchen.

Ich denke nicht, dass die Manipulation der Bedeutung der Aufgabenvariablen eine gute Idee ist, obwohl sie akzeptabel ist.
```

Dann viel Glück mit deinen Besuchern.

@sirkon Was meinst du mit Besuchern? Ich mochte diese Syntax übrigens, aber sollte der Schalter so geschrieben werden:

switch task {
case Task.SearchAdd:
    // task is Task.SearchAdd in this scope
case Task.SearchUpdate:
case Task.SearchDelete:
}

Und was wäre der Nullwert für Task ? Zum Beispiel:

var task Task

Wäre es nil ? Wenn ja, sollten die switch zusätzliche case nil ?
Oder würde es auf den ersten Typ initialisiert werden? Dies wäre jedoch umständlich, da dann die Reihenfolge der Typdeklaration in einer Weise wichtig ist, die es zuvor nicht gegeben hat, aber es wäre wahrscheinlich für numerische Enumerationen in Ordnung.

Ich gehe davon aus, dass dies switch task.(type) aber der Switch würde erfordern, dass alle Fälle vorhanden sind, oder? wie in .. wenn Sie einen Fall verpassen, Kompilierungsfehler. Und kein default erlaubt. Ist das richtig?

Was meinen Sie mit Besuchern?

Ich meinte, sie sind die einzige typsichere Option in Go für diese Art von Funktionalität. Viel schlimmer noch für eine bestimmte Fallgruppe (begrenzte Anzahl vordefinierter Alternativen).

Was wäre auch der Nullwert für Task? Zum Beispiel:

var task Task

Ich fürchte, es sollte ein nabelhafter Typ in Go sein, so wie dieser

Oder würde es auf den ersten Typ initialisiert werden?

wäre vor allem für den beabsichtigten Zweck viel zu seltsam.

Ich gehe davon aus, dass dies der Switch-Aufgabe entspricht. (Typ), aber der Switch würde erfordern, dass alle Fälle vorhanden sind, oder? wie in .. wenn Sie einen Fall verpassen, Kompilierungsfehler.

Ja richtig.

Und keine Vorgabe erlaubt. Ist das richtig?

Nein, Standardeinstellungen sind zulässig. Allerdings entmutigt.

PS Ich scheine eine Idee zu haben, die Go @ianlancetaylor und andere Go-Leute über

Wenn es null ist, dann ist es meiner Meinung nach in Ordnung. Ich würde es vorziehen, dass case nil eine Voraussetzung für die switch-Anweisung wäre. Vorher ein if task != nil ist auch ok, ich mag es nur nicht so sehr :|

Wäre das auch erlaubt?

type Foo oneof {
  A = 3
  B = "3"
  C = 3.0
  D = struct { E bool }{ true }
}

Wäre das auch erlaubt?

type Foo oneof {
  A = 3
  B = "3"
  C = 3.0
  D = struct { E bool }{ true }
}

Nun, keine Konstanten, nur

type Foo oneof {
    A <type reference>
}

oder

type Foo oneof {
    A = iota
    B
    C
}

oder

type Foo oneof {
    A = 1
    B = 2
    C = 3
}

Keine Kombination von Jotas und Werten. Oder in Kombination mit der Kontrolle von Werten, sie sollten nicht wiederholt werden.

FWIW, eine Sache, die ich am neuesten Generika-Design interessant fand, ist, dass es einen anderen Ort aufzeigte, um zumindest einige der Anwendungsfälle von Summentypen anzugehen und gleichzeitig die Fallstricke über Nullwerte zu vermeiden. Es definiert disjunktive Verträge, die in gewisser Weise summarisch sind, aber da sie Einschränkungen und keine Typen beschreiben, müssen sie keinen Nullwert haben (da Sie keine Variablen dieses Typs deklarieren können). Das heißt, es ist zumindest möglich, eine Funktion zu schreiben, die eine begrenzte Menge möglicher Typen akzeptiert, mit einer Typüberprüfung dieser Menge zur Kompilierzeit.

Nun, so wie es ist, funktioniert das Design natürlich nicht wirklich für die hier beabsichtigten Anwendungsfälle: Disjunktionen listen nur zugrunde liegende Typen oder Methoden auf und sind daher immer noch weit offen. Und natürlich ist es auch als allgemeine Idee ziemlich begrenzt, da Sie keine generischen (oder summarischen) Funktionen oder Werte instanziieren können. Aber IMO zeigt es, dass der Gestaltungsraum für einige der Anwendungsfälle von Summen viel größer ist als die Idee der Summentypen selbst. Und dass das Denken über Summen daher eher auf eine konkrete Lösung fixiert ist als auf konkrete Probleme.

Trotzdem. Fand es nur interessant.

@Merovius macht einen hervorragenden Punkt, dass das neueste generische Design mit einigen der Anwendungsfälle von

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

würde werden:

contract intOrFloat64(T) {
    T int, float64
}

func addOne(type T intOrFloat64) (x T) T {
    return x + 1
}

Was die Summentypen selbst betrifft, wäre ich noch zweifelhafter als jetzt, wenn Generika irgendwann landen sollten, ob die Vorteile ihrer Einführung die Kosten für eine einfache Sprache wie Go überwiegen würden.

Wenn jedoch etwas getan werden sollte, dann wäre die einfachste und am wenigsten störende Lösung IMO die Idee von @ianlancetaylor von 'eingeschränkten Schnittstellen', die genau so implementiert würden, wie 'uneingeschränkte' Schnittstellen heute sind, aber nur erfüllt werden könnten nach den angegebenen Typen. In der Tat, wenn Sie ein Blatt aus dem Buch des generischen Designs nehmen und die Typbeschränkung zur ersten Zeile des Schnittstellenblocks machen:

type intOrFloat64 interface{ type int, float64 }    

dann wäre dies vollständig abwärtskompatibel, da Sie überhaupt kein neues Schlüsselwort (wie restrict ) benötigen würden. Sie könnten der Schnittstelle immer noch Methoden hinzufügen, und es wäre ein Kompilierzeitfehler, wenn die Methoden nicht von allen angegebenen Typen unterstützt würden.

Ich sehe überhaupt kein Problem darin, einer Variablen des eingeschränkten Schnittstellentyps Werte zuzuweisen. Wenn der Typ des Werts auf dem RHS (oder der Standardtyp eines nicht typisierten Literals) nicht genau mit einem der angegebenen Typen übereinstimmt, wird er einfach nicht kompiliert. Wir hätten also:

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

Es wäre ein Kompilierzeitfehler für die Fälle, in denen ein Typwechsel nicht mit einem spezifizierten Typ übereinstimmt, und eine Vollständigkeitsprüfung könnte implementiert werden. Es wäre jedoch immer noch eine Typzusicherung erforderlich, um den eingeschränkten Schnittstellenwert in einen Wert seines dynamischen Typs zu konvertieren, wie er heute ist.

Nullwerte sind bei diesem Ansatz kein Problem (jedenfalls kein größeres Problem als heute allgemein bei Schnittstellen). Der Nullwert einer eingeschränkten Schnittstelle wäre nil (was bedeutet, dass sie derzeit nichts enthält) und die angegebenen Typen hätten natürlich intern ihre eigenen Nullwerte, die nil wären für nilbare Typen.

All dies scheint mir jedoch perfekt praktikabel zu sein, obwohl, wie ich bereits sagte, die gewonnene Kompilierzeitsicherheit die zusätzliche Komplexität wirklich wert ist - ich habe meine Zweifel, da ich in meiner eigenen Programmierung nie wirklich das Bedürfnis nach Summentypen verspürte.

IIUC, das Generika-Ding wird nicht dynamisch sein, also ist dieser ganze Punkt nicht gültig. Wenn Schnittstellen jedoch als Verträge funktionieren dürfen (was ich bezweifle), würde dies keine erschöpfenden Prüfungen und Aufzählungen lösen, worum es bei sumtypes (ich denke, vielleicht nicht?) geht.

@alanfo , @Merovius Danke für den

Ich drehe den Standpunkt gerne für den Bruchteil einer Sekunde um: Ich versuche zu verstehen, warum Verträge nicht vollständig durch parametrisierte Schnittstellen ersetzt werden können, die die oben erwähnte Typbeschränkung zulassen. Im Moment sehe ich keinen zwingenden technischen Grund, außer dass solche "Summen"-Schnittstellentypen, wenn sie als "Summen"-Typen verwendet werden, die möglichen dynamischen Werte auf genau die in der Schnittstelle aufgezählten Typen beschränken möchten, während - wenn die dieselbe Schnittstelle wurde in der Vertragsposition verwendet - die aufgezählten Typen in der Schnittstelle müssten als zugrunde liegende Typen dienen, um eine einigermaßen nützliche generische Einschränkung zu sein.

@Guter Wein
Ich habe nicht vorgeschlagen, dass das Generika-Design alles anspricht , was man mit @Merovius in seinem letzten Beitrag klar erklärt hat, dass dies nicht der

Das generische Design würde es jedoch ermöglichen, eine Funktion zu schreiben, die mit einer begrenzten Anzahl von Typen arbeitet, die der Compiler erzwingen würde, und dies ist etwas, was wir derzeit überhaupt nicht tun können.

Was eingeschränkte Schnittstellen anbelangt, würde der Compiler die genauen Typen kennen, die verwendet werden könnten, und es wäre daher für ihn möglich, eine umfassende Prüfung in einer Typwechselanweisung durchzuführen.

@Griesemer

Ich bin verwirrt von dem, was Sie sagen, da ich dachte, dass der Entwurf des Entwurfsdokuments für Generika ziemlich klar erklärt (im Abschnitt "Warum nicht Schnittstellen anstelle von Verträgen verwenden"), warum letztere als besseres Vehikel angesehen wurden als erstere, um generische Einschränkungen auszudrücken.

Insbesondere kann ein Vertrag eine Beziehung zwischen Typparametern ausdrücken und daher ist nur ein einziger Vertrag erforderlich. Jeder seiner Typparameter kann als Empfängertyp einer im Vertrag aufgeführten Methode verwendet werden.

Das gleiche gilt nicht für eine Schnittstelle, ob parametrisiert oder nicht. Wenn sie überhaupt Einschränkungen hätten, würde jeder Typparameter eine separate Schnittstelle benötigen.

Dies macht es schwieriger, eine Beziehung zwischen Typparametern mithilfe von Schnittstellen auszudrücken, obwohl dies nicht unmöglich ist, wie das Diagrammbeispiel gezeigt hat.

Wenn Sie jedoch der Meinung sind, dass wir "zwei Fliegen mit einer Klappe schlagen" könnten, indem wir Schnittstellen zu Typbeschränkungen hinzufügen und sie dann sowohl für generische als auch für Summentypzwecke verwenden, dann (abgesehen von dem von Ihnen erwähnten Problem) denke ich, dass Sie wahrscheinlich richtig, dass dies technisch machbar wäre.

Ich denke, es wäre nicht wirklich wichtig, wenn Schnittstellentypeinschränkungen "nicht integrierte" Typen einschließen könnten, was Generics betrifft, obwohl eine Möglichkeit gefunden werden müsste, sie auf die genauen Typen (und nicht auch auf abgeleitete Typen) zu beschränken. sie wären also für Summentypen geeignet. Vielleicht könnten wir für letzteres const type (oder auch nur const ), wenn wir bei den aktuellen Schlüsselwörtern bleiben wollen.

@griesemer Es gibt einige Gründe, warum parametrisierte Schnittstellentypen kein direkter Ersatz für Verträge sind.

  1. Die Typparameter sind dieselben wie bei anderen parametrisierten Typen.
    In einer Art wie

    type C2(type T C1) interface { ... }
    

    der Typparameter T existiert außerhalb der Schnittstelle selbst. Jedes als T Typargument muss bereits bekannt sein , um den Vertrag C1 zu erfüllen, und der Rumpf der Schnittstelle kann T nicht weiter einschränken. Dies unterscheidet sich von Vertragsparametern, die durch den Vertragstext als Ergebnis der Übergabe eingeschränkt werden. Dies würde bedeuten, dass jeder Typparameter einer Funktion unabhängig eingeschränkt werden müsste, bevor er als Parameter an die Einschränkung eines anderen Typparameters übergeben werden kann.

  2. Es gibt keine Möglichkeit, den Empfängertyp im Hauptteil der Schnittstelle zu benennen.
    Schnittstellen müssten Sie etwas schreiben lassen wie:

    type C3(type U C1) interface(T) {
        Add(T) T
    }
    

    wobei T den Empfängertyp bezeichnet.

  3. Einige Schnittstellentypen würden sich als generische Einschränkungen nicht zufriedenstellen.
    Alle Operationen, die auf mehreren Werten des Empfängertyps beruhen, sind nicht mit dynamischem Versand kompatibel. Diese Operationen wären daher für Schnittstellenwerte nicht verwendbar. Dies würde bedeuten, dass die Schnittstelle sich selbst nicht zufriedenstellt (zum Beispiel als Typargument für einen Typparameter, der von derselben Schnittstelle eingeschränkt wird). Dies wäre überraschend. Eine Lösung besteht darin, die Erzeugung von Schnittstellenwerten für solche Schnittstellen überhaupt nicht zuzulassen, aber dies würde den hier vorgestellten Anwendungsfall sowieso nicht zulassen.

Zur Unterscheidung zwischen zugrunde liegenden Typeinschränkungen und Typidentitätseinschränkungen gibt es eine Methode, die funktionieren könnte. Stellen Sie sich vor, wir könnten benutzerdefinierte Einschränkungen definieren, wie z

contract (T) indenticalTo(U) {
    *T *U
}

(Hier verwende ich eine erfundene Notation, um einen einzelnen Typ als "Empfänger" anzugeben. Ich werde einen Vertrag mit einem expliziten Empfängertyp als "Beschränkung" aussprechen, genauso wie ein Funk mit einem Empfänger "Methode" ausgesprochen wird. Die Parameter nach dem Vertragsnamen sind normale Typparameter und können nicht auf der linken Seite einer Einschränkungsklausel im Hauptteil der Einschränkung erscheinen.)

Da der zugrunde liegende Typ eines Literalzeigertyps er selbst ist, impliziert diese Einschränkung, dass T mit U identisch ist. Da dies als Einschränkung deklariert ist, könnten Sie (identicalTo(int)), (identicalTo(uint)), ... als Einschränkungsdisjunktion schreiben.

Obwohl Verträge nützlich sein können, um eine Art von Summentypen auszudrücken, glaube ich nicht, dass Sie damit generische Summentypen ausdrücken können. Nach dem, was ich aus dem Entwurf gesehen habe, muss man konkrete Typen aufzählen, also kann man so etwas nicht schreiben:

contract Foo(T, U) {
    T U, int64
}

Welcher müsste einen generischen Summentyp eines unbekannten Typs und einen/mehrere bekannte Typen ausdrücken. Selbst wenn das Design solche Konstrukte zuließe, würden sie bei der Verwendung seltsam aussehen, da beide Parameter effektiv dasselbe wären.

Ich habe noch etwas darüber nachgedacht, wie sich der Entwurf des Generics-Designs ändern könnte, wenn Schnittstellen um Typbeschränkungen erweitert und dann verwendet würden, um Verträge im Design zu ersetzen.

Es ist vielleicht am einfachsten, die Situation zu analysieren, wenn wir eine unterschiedliche Anzahl von Typparametern berücksichtigen:

Keine Parameter

Keine Änderung :)

Ein Parameter

Keine wirklichen Probleme hier. Eine parametrisierte Schnittstelle (im Gegensatz zu einer nicht-generischen) würde nur dann benötigt, wenn entweder der Typparameter auf sich selbst verweist und/oder ein oder mehrere andere unabhängige feste Typen benötigt würden, um die Schnittstelle zu instanziieren.

Zwei oder mehr Parameter

Wie bereits erwähnt, müsste jeder Typparameter einzeln eingeschränkt werden, wenn überhaupt eine Einschränkung erforderlich ist.

Eine parametrisierte Schnittstelle wird nur benötigt, wenn:

  1. Der Typparameter bezog sich auf sich selbst.

  2. Die Schnittstelle verwies auf einen anderen Typparameter oder Parameter, die _bereits deklariert wurden_ im Typparameterabschnitt (vermutlich möchten wir hier nicht zurückverfolgen).

  3. Einige andere unabhängige feste Typen wurden benötigt, um die Schnittstelle zu instanziieren.

Von diesen ist (2) wirklich der einzige problematische Fall, da er Typparameter ausschließen würde, die sich wie im Graphenbeispiel aufeinander beziehen. Unabhängig davon, ob einer zuerst 'Node' oder 'Edge' deklariert hat, muss seine einschränkende Schnittstelle immer noch den anderen als Typparameter übergeben.

Wie im Entwurfsdokument angegeben, können Sie dies jedoch umgehen, indem Sie NodeInterface und EdgeInterface auf oberster Ebene ohne Parameter deklarieren (da sie sich nicht auf sich selbst beziehen), da es dann kein Problem geben würde, sich unabhängig voneinander zu referenzieren Deklarationsordnung. Sie können diese Schnittstellen dann verwenden, um die Typparameter der Graph-Struktur und die der zugehörigen Methode 'New' einzuschränken.

Es sieht also nicht so aus, als gäbe es hier unüberwindbare Probleme, auch wenn die Vertragsidee schöner ist.

Vermutlich könnte comparable jetzt einfach zu einer integrierten Schnittstelle werden und nicht mehr zu einem Vertrag.

Schnittstellen könnten natürlich wie bereits ineinander eingebettet sein.

Ich bin mir nicht sicher, wie man mit dem Zeigermethodenproblem umgehen würde (in den Fällen, in denen diese im Vertrag angegeben werden müssten), da Sie keinen Empfänger für eine Schnittstellenmethode angeben können. Eventuell wäre eine spezielle Syntax (z. B. vor dem Methodennamen mit einem Sternchen) erforderlich, um eine Zeigermethode anzugeben.

Wenn ich mich nun den Beobachtungen von @stevenblenkinsop zuwende , frage ich mich, ob es das Leben einfacher machen würde, wenn parametrisierte Schnittstellen es nicht zulassen würden, dass ihre eigenen Typparameter in irgendeiner Weise eingeschränkt werden? Ich bin mir nicht sicher, ob dies wirklich eine nützliche Funktion ist, es sei denn, jemand kann sich einen vernünftigen Anwendungsfall vorstellen.

Ich persönlich halte es nicht für überraschend, dass einige Interface-Typen sich nicht als generische Constraints befriedigen können. Ein Schnittstellentyp ist in jedem Fall kein gültiger Empfängertyp und kann daher keine Methoden haben.

Obwohl Stevens Idee einer eingebauten Funktion identischTo() funktionieren würde, scheint mir die Angabe von Summentypen potenziell langwierig zu sein. Ich würde eine Syntax bevorzugen, die es ermöglicht, eine ganze Reihe von Typen als exakt anzugeben.

@urandom ist natürlich richtig, dass man beim derzeitigen Stand des Generics-Entwurfs nur konkrete (eingebaute oder aggregierte eingebaute) Typen

interface Foo(T) {
    const type T, int64  // 'const' indicates types are exact i.e. no derived types
}

Warum können wir der Sprache nicht einfach diskriminierte Gewerkschaften hinzufügen, anstatt einen weiteren Umweg über ihre Abwesenheit zu erfinden?

@griesemer Vielleicht ich war von Anfang an dafür, Schnittstellen zu verwenden, um Einschränkungen anzugeben :) Ich schlage vor, die Betreiber anzusprechen). Und ich mag die neueste Version des Vertragsdesigns viel mehr als die vorherige. Aber im Allgemeinen stimme ich voll und ganz zu, dass (eventuell erweiterte) Schnittstellen als Einschränkungen praktikabel und eine Überlegung wert sind.

@urandom

Ich glaube nicht, dass man damit generische Summentypen ausdrücken kann

Ich möchte wiederholen, dass mein Punkt nicht "Sie können damit Summentypen erstellen", sondern "Sie können einige Probleme lösen, die Summentypen mit ihnen lösen". Wenn Ihre Problemstellung "Ich möchte Summentypen" lautet, ist es nicht verwunderlich, dass Summentypen die einzige Lösung sind. Ich wollte nur zum Ausdruck bringen, dass es vielleicht möglich ist, darauf zu verzichten , wenn wir uns auf die Probleme konzentrieren, die Sie damit lösen möchten.

@alanfo

Dies macht es schwieriger, eine Beziehung zwischen Typparametern mithilfe von Schnittstellen auszudrücken, obwohl dies nicht unmöglich ist, wie das Diagrammbeispiel gezeigt hat.

Ich finde "unangenehm" ist subjektiv. Persönlich finde ich die Verwendung parametrisierter Schnittstellen natürlicher und das Graph-Beispiel eine sehr gute Illustration. Für mich ist ein Graph eine Entität, keine Beziehung zwischen einer Art Edge und einer Art Node.

Aber TBH, ich glaube nicht, dass einer von beiden wirklich mehr oder weniger umständlich ist - Sie schreiben ziemlich genau den gleichen Code, um ziemlich genau die gleichen Dinge auszudrücken. Und FWIW, dafür gibt es Stand der Technik . Haskell-Typklassen verhalten sich sehr ähnlich wie Schnittstellen und wie dieser Wiki-Artikel hervorhebt, ist die Verwendung von Multiparameter-Typklassen zum Ausdrücken von Beziehungen zwischen Typen eine ziemlich normale Sache.

@stevenblenkinsop

Es gibt keine Möglichkeit, den Empfängertyp im Hauptteil der Schnittstelle zu benennen.

Die Art und Weise, wie Sie das ansprechen würden, ist mit Typargumenten auf der Verwendungsseite. dh

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
}

Dies erfordert einige Sorgfalt bei der Funktionsweise der Vereinheitlichung, damit Sie selbstreferenzierende Typparameter zulassen können, aber ich denke, es kann zum Laufen gebracht werden.

Deinen 1. und 3. verstehe ich nicht ganz, muss ich zugeben. Ich würde von einigen konkreten Beispielen profitieren.


Wie auch immer, es ist ein bisschen unaufrichtig, dies am Ende der Fortsetzung dieser Diskussion fallen zu lassen, aber dies ist wahrscheinlich nicht das richtige Thema, um die Details des Generika-Designs durchzusprechen. Ich habe es nur angesprochen, um den Gestaltungsspielraum für diese Ausgabe ein wenig zu erweitern :) Weil es sich anfühlte, als ob es schon eine Weile her ist, dass neue Ideen in die Diskussion um Summentypen eingebracht wurden.

```go
switch task {
case tasks.SearchAdd:
    // task is tasks.SearchAdd in this scope
case tasks.SearchUpdate:
case tasks.SearchDelete:
}

wäre im Geiste viel mehr Goish als jeder andere Ansatz, den Go in seinem gegenwärtigen Zustand zulässt. Haskellish-Musterabgleich ist nicht erforderlich, es ist mehr als genug, bis zu einem bestimmten Typ zu tauchen.
Ich denke nicht, dass die Manipulation der Bedeutung der Aufgabenvariablen eine gute Idee ist, obwohl sie akzeptabel ist.

Dann viel Glück mit deinen Besuchern.

Warum denken Sie, dass der Mustervergleich in Go nicht möglich ist? Wenn Ihnen Beispiele für den Mustervergleich fehlen, sehen Sie sich beispielsweise Rust an.

@Merovius re: "Für mich ist ein Graph eine Entität"

Ist es eine Entität zur Kompilierzeit oder hat sie eine Darstellung zur Laufzeit? Einer der Hauptunterschiede zwischen Verträgen und Schnittstellen besteht darin, dass eine Schnittstelle ein Laufzeitobjekt ist. Es nimmt an der Garbage Collection teil, hat Zeiger auf andere Laufzeitobjekte und so weiter. Die Konvertierung von einem Vertrag in eine Schnittstelle würde bedeuten, ein neues, temporäres Laufzeitobjekt einzuführen, das Zeiger auf die darin enthaltenen Knoten/Vertexe hat (wie viele?), was bei einer Sammlung von Graphfunktionen umständlich erscheint, von denen jede natürlicher sein könnte nehmen Parameter, die auf verschiedene Teile von Graphen zeigen, auf ihre eigene Weise, je nach den Anforderungen der Funktion.

Ihre Intuition könnte durch die Verwendung von "Graph" für einen Vertrag in die Irre geführt werden, da "Graph" objekthaft erscheint und der Vertrag nicht wirklich einen bestimmten Untergraphen spezifiziert; es ist eher so, als würden Sie eine Reihe von Begriffen definieren, die Sie später verwenden, wie Sie es in der Mathematik oder im Recht tun würden. In einigen Fällen möchten Sie möglicherweise sowohl einen Graphenvertrag als auch eine Graphenschnittstelle, was zu einem lästigen Namenskonflikt führt. Ich kann mir jedoch keinen besseren Namen vorstellen.

Im Gegensatz dazu ist eine diskriminierte Union ein Laufzeitobjekt. Während Sie die Implementierung nicht einschränken, müssen Sie sich überlegen, wie ein Array davon aussehen könnte. Ein Array mit N Elementen benötigt N Diskriminatoren und N Werte, und es gibt eine Vielzahl von Möglichkeiten, die durchgeführt werden können. (Julia hat interessante Darstellungen und legt die Diskriminatoren und Werte manchmal in separate Arrays.)

Um eine Reduzierung der Fehler vorzuschlagen, die derzeit überall bei den interface{} Schemata auftreten, aber um das kontinuierliche Tippen des | Operators zu entfernen, würde ich Folgendes vorschlagen:

type foobar union {
    int
    float64
}

Allein der Anwendungsfall, viele interface{} durch diese Art von Typsicherheit zu ersetzen, wäre ein massiver Gewinn für die Bibliothek. Ein Blick auf die Hälfte der Dinge in der Krypto-Bibliothek könnte dies gebrauchen.

Probleme wie: Ah, Sie haben ecdsa.PrivateKey anstelle von *ecdsa.PrivateKey eingegeben - hier ist ein allgemeiner Fehler, der nur von ecdsa.PrivateKey unterstützt wird. Die einfache Tatsache, dass dies klare Unionstypen sein sollten, würde die Typsicherheit erheblich erhöhen.

Obwohl dieser Vorschlag im Vergleich zu int|float64 mehr _Platz_ beansprucht, zwingt er den Benutzer, darüber nachzudenken. Halten Sie die Code-Basis viel sauberer.

Um eine Reduzierung der Fehler vorzuschlagen, die derzeit überall bei den interface{} Schemata auftreten, aber um das kontinuierliche Tippen des | Operators zu entfernen, würde ich Folgendes vorschlagen:

type foobar union {
    int
    float64
}

Allein der Anwendungsfall, viele interface{} durch diese Art von Typsicherheit zu ersetzen, wäre ein massiver Gewinn für die Bibliothek. Ein Blick auf die Hälfte der Dinge in der Krypto-Bibliothek könnte dies gebrauchen.

Probleme wie: Ah, Sie haben ecdsa.PrivateKey anstelle von *ecdsa.PrivateKey eingegeben - hier ist ein allgemeiner Fehler, der nur von ecdsa.PrivateKey unterstützt wird. Die einfache Tatsache, dass dies klare Unionstypen sein sollten, würde die Typsicherheit erheblich erhöhen.

Obwohl dieser Vorschlag im Vergleich zu int|float64 mehr _Platz_ beansprucht, zwingt er den Benutzer, darüber nachzudenken. Halten Sie die Code-Basis viel sauberer.

Siehe dies (Kommentar) , es ist mein Vorschlag.

Eigentlich können wir unsere beiden Ideen in die Sprache einbringen. Dies führt zur Existenz von zwei nativen Möglichkeiten, ADT auszuführen, jedoch mit unterschiedlichen Syntaxen.

Mein Vorschlag für Features, insbesondere Pattern Matching, Ihr für Kompatibilität und die Möglichkeit, von dem Feature für alte Codebasen zu profitieren.

Sieht aber nach Overkill aus, oder?

Der Summentyp kann auch mit nil als Standardwert festgelegt werden. Natürlich wird bei jedem Switch nil Fall benötigt.
Pattern Matching kann wie folgt durchgeführt werden:
-- Erklärung

type U enum{
    A(int64),
    B(string),
}

-- passend

...
var a U
...
switch a {
    case A{b}:
         //process b here
    case B{b}:
         //...
    case nil:
         //...
}
...

Wenn man Musterabgleich nicht mag - siehe Sirkons Vorschlag oben.

Der Summentyp kann auch mit nil als Standardwert festgelegt werden. Natürlich wird bei jedem Switch nil Fall benötigt.

Wäre es nicht einfacher, nicht initiierte Werte zur Kompilierzeit zu verbieten? In Fällen, in denen wir einen initialisierten Wert benötigen, können wir ihn zum Summentyp hinzufügen: dh

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): ...
}

Der Summentyp kann auch mit nil als Standardwert festgelegt werden. Natürlich wird bei jedem Switch nil Fall benötigt.

Wäre es nicht einfacher, nicht initiierte Werte zur Kompilierzeit zu verbieten? In Fällen, in denen wir einen initialisierten Wert benötigen, können wir ihn zum Summentyp hinzufügen: dh

Unterbricht bestehenden Code.

Der Summentyp kann auch mit nil als Standardwert festgelegt werden. Natürlich wird bei jedem Switch nil Fall benötigt.

Wäre es nicht einfacher, nicht initiierte Werte zur Kompilierzeit zu verbieten? In Fällen, in denen wir einen initialisierten Wert benötigen, können wir ihn zum Summentyp hinzufügen: dh

Unterbricht bestehenden Code.

Es ist kein Code mit Summentypen vorhanden. Obwohl ich denke, der Standardwert sollte etwas sein, das im Typ selbst definiert ist. Entweder der erste Eintrag oder der erste alphabetische Eintrag oder so.

Es ist kein Code mit Summentypen vorhanden. Obwohl ich denke, der Standardwert sollte etwas sein, das im Typ selbst definiert ist. Entweder der erste Eintrag oder der erste alphabetische Eintrag oder so.

Ich habe Ihnen auf den ersten Blick zugestimmt, aber nach einiger Überlegung könnte der neue reservierte Name für die Union zuvor in einer Codebasis (union, enum usw.)

Ich denke, die Verpflichtung, auf Null zu prüfen, wäre ziemlich schmerzhaft.

Es sieht aus wie eine bahnbrechende Änderung für die Abwärtskompatibilität, die nur von Go2.0 gelöst werden konnte

Es ist kein Code mit Summentypen vorhanden. Obwohl ich denke, der Standardwert sollte etwas sein, das im Typ selbst definiert ist. Entweder der erste Eintrag oder der erste alphabetische Eintrag oder so.

Aber es gibt viele existierende Go-Codes, die alles haben. Das wird definitiv den Wandel brechen. Schlimmer noch, gofix und ähnliche Tools können Variablentypen nur in Optionen (des gleichen Typs) ändern, was zumindest hässlichen Code erzeugt, in allen anderen Fällen wird einfach alles in der Welt zerstört.

Wenn nichts anderes, muss reflect.Zero etwas zurückgeben . Aber all dies sind technische Hürden , die gelöst werden können - zum Beispiel, diese Hürde ist ziemlich offensichtlich, wenn der Nullwert einer Summe-Typ ist gut definiert und wird wahrscheinlich „Panik“, wenn nicht. Die größere Frage ist immer noch, warum eine bestimmte Wahl die richtige ist und ob und wie eine Wahl in die Sprache insgesamt passt. IMO, der beste Weg, diese Probleme anzugehen, besteht immer noch darin, über konkrete Fälle zu sprechen, in denen Summentypen spezifische Probleme ansprechen oder deren Fehlen entstanden sind. Dafür gelten die

Beachten Sie insbesondere, dass sowohl "es sollte kein Nullwert sein und es sollte nicht erlaubt sein, nicht initialisierte Werte zu erstellen" als auch "der Standardwert sollte der erste Eintrag sein" oben mehrmals erwähnt wurden. Ob Sie also denken, dass es so sein sollte oder das bringt nicht wirklich neue Informationen. Aber es macht einen ohnehin schon riesigen Thread für die Zukunft noch länger und schwieriger, die relevanten Informationen darin zu finden.

Betrachten wir Reflect.Kind. Es gibt eine ungültige Art, die den Standardwert int von 0 hat. Wenn Sie eine Funktion haben, die eine Reflect.Kind akzeptiert, und Sie eine nicht initialisierte Variable dieses Typs übergeben, wäre sie am Ende ungültig. Wenn Reflect.Kind hypothetisch in einen Summentyp geändert werden kann, sollte es vielleicht das Verhalten eines benannten ungültigen Eintrags als Standardeintrag beibehalten, anstatt sich auf einen Nullwert zu verlassen.

Betrachten wir nun html/template.contentType. Der Typ Plain ist sein Standardwert und wird von der Funktion stringify tatsächlich als solcher behandelt, da er der Fallback ist. In einer hypothetischen Summenzukunft würden Sie dieses Verhalten nicht nur immer noch benötigen, sondern es ist auch unmöglich, einen Nullwert dafür zu verwenden, da Null für einen Benutzer dieser Art nichts bedeutet. Es ist ziemlich obligatorisch, hier immer einen benannten Wert zurückzugeben, und Sie haben eine klare Vorgabe für diesen Wert.

Ich bin es wieder mit einem anderen Beispiel, bei dem algebraische / variadische / Summe / was auch immer Datentypen gut funktionieren.

Wir verwenden also eine NoSQL-Datenbank ohne Transaktionen (verteiltes System, Transaktionen funktionieren bei uns nicht), aber wir lieben die Datenintegrität und -konsistenz aus offensichtlichen Gründen und müssen gleichzeitige Zugriffsprobleme umgehen, normalerweise mit etwas komplexen bedingten Aktualisierungsabfragen über eine einzelne record (Einzel-Record-Schreiben ist atomar).

Ich habe eine neue Aufgabe, eine Reihe von Entitäten zu schreiben, die eingefügt, angehängt oder gelöscht werden können (nur eine dieser Operationen).

Wenn wir sowas haben könnten

type EntityOp oneof {
    Insert   Reference
    NewState string
    Delete   struct{}
}

Die Methode könnte einfach sein

type DB interface {
    …
    Capture(ctx context.Context, processID string, ops map[string]EntityOp) (bool, error)
}

Eine fantastische Verwendung für Summenzeiten ist die Darstellung von Knoten in einem AST. Eine andere Möglichkeit besteht darin, nil durch ein option zu ersetzen, das zur Kompilierzeit überprüft wird.

@DemiMarie aber im heutigen Go kann diese Summe auch null sein, wie ich oben vorgeschlagen habe, wir können einfach null zu einer Variante jeder Aufzählung machen möchte diese Funktion, ohne den gesamten vorhandenen Go-Code zu brechen (derzeit haben wir alles nillbar)

Ich weiß nicht, ob es hierher gehört, aber das alles bleibt mir Typescript, wo ein sehr cooles Feature namens "String Literal Types" existiert und wir das tun können:

var name: "Peter" | "Consuela"; // string type with compile-time constraint

Es ist wie eine String-Aufzählung, die meiner Meinung nach viel besser ist als herkömmliche numerische Aufzählungen.

@Merovius
ein konkretes Beispiel ist das Arbeiten mit beliebigem JSON.
In Rust kann es dargestellt werden als
Aufzählungswert {
Null,
Bool(bool),
Zahl(Zahl),
Zeichenfolge (Zeichenfolge),
Array (Vec),
Objekt(Karte),
}

Ein Union-Typ als zwei Vorteile:

  1. Selbstdokumentation des Codes
  2. Dem Compiler oder go vet erlauben, die falsche Verwendung eines Union-Typs zu überprüfen
    (zB ein Schalter, bei dem nicht alle Typen überprüft werden)

Für die Syntax sollte Folgendes mit Go1 kompatibel type alias :

type Token = int | float64 | string

Ein Union-Typ kann intern als Schnittstelle implementiert werden; Wichtig ist, dass der Code durch die Verwendung eines Union-Typs besser lesbar ist und Fehler wie

var tok Token

switch t := tok.(type) {
case int:
    // do something
}

Der Compiler sollte einen Fehler ausgeben, da nicht alle Token Typen im Switch verwendet werden.

Das Problem dabei ist, dass es (meinem Wissen nach) keine Möglichkeit gibt, Zeigertypen (oder Typen, die Zeiger enthalten, wie string ) und Nicht-Zeigertypen zusammen zu speichern. Selbst Typen mit unterschiedlichen Layouts würden nicht funktionieren. Fühlen Sie sich frei, mich zu korrigieren, aber das Problem ist, dass präzises GC nicht gut mit Variablen funktioniert, die gleichzeitig Zeiger und einfache Variablen sein können.

Wir können den Weg des impliziten Boxens gehen - wie es interface{} derzeit tut. Aber ich denke nicht, dass dies ausreichende Vorteile bietet - es sieht immer noch wie ein verherrlichter Schnittstellentyp aus. Vielleicht kann stattdessen eine Art vet Check entwickelt werden?

Der Garbage Collector müsste die Tag-Bits aus der Union lesen, um das Layout zu bestimmen. Dies ist nicht unmöglich, wäre aber eine große Änderung der Laufzeit, die gc verlangsamen könnte.

Vielleicht kann stattdessen eine Art Vet-Check entwickelt werden?

https://github.com/BurntSushi/go-sumtype

Der Garbage Collector müsste die Tag-Bits aus der Union lesen, um das Layout zu bestimmen.

Das ist genau die gleiche Rasse, die es bei Schnittstellen gab, als sie Nicht-Zeiger enthalten konnten. Von diesem Design wurde ausdrücklich abgewichen.

go-sumtype ist interessant, danke. Aber was passiert, wenn dasselbe Paket zwei Unionstypen definiert?

Der Compiler könnte den Union-Typ intern als Schnittstelle implementieren, aber eine einheitliche Syntax und eine Standardtypprüfung hinzufügen.

Wenn es N Projekte gibt, die Unionstypen verwenden, jedes unterschiedlich und mit N groß genug, ist die Einführung der einen Möglichkeit möglicherweise die beste Lösung.

Aber was passiert, wenn dasselbe Paket zwei Unionstypen definiert?

Nicht viel? Die Logik ist pro Typ und verwendet eine Dummy-Methode, um Implementierer zu erkennen. Verwenden Sie einfach unterschiedliche Namen für die Dummy-Methoden.

@skybrian IIRC aktuelle Bitmap, die das

Das Problem dabei ist, dass es (meinem Wissen nach) keine Möglichkeit gibt, Zeigertypen (oder Typen, die Zeiger enthalten, wie z. B. string) und Nicht-Zeigertypen zusammen zu speichern

Ich glaube nicht, dass dies notwendig ist. Der Compiler könnte das Layout für Typen überlappen, wenn die Zeigerzuordnungen übereinstimmen, und sonst nicht. Wenn sie nicht übereinstimmen, können Sie sie nacheinander anordnen oder einen Zeigeransatz verwenden, wie er derzeit für Schnittstellen verwendet wird. Es könnte sogar nicht zusammenhängende Layouts für Strukturmember verwenden.

Aber ich denke nicht, dass dies ausreichende Vorteile bietet - es sieht immer noch wie ein verherrlichter Schnittstellentyp aus.

In meinem Vorschlag sind Union-Typen _exakt_ ein glorifizierter Schnittstellentyp - ein Union-Typ ist nur eine Teilmenge einer Schnittstelle, die nur eine Aufzählung von Typen speichern darf. Dies gibt dem Compiler möglicherweise die Freiheit, eine effizientere Speichermethode für bestimmte Typenmengen zu wählen, aber das ist ein Implementierungsdetail, nicht die Hauptmotivation.

@rogpeppe - Kann ich aus Neugierde den umwandeln , um etwas damit zu tun? Denn wenn ich es ständig in einen bekannten Typ umwandeln muss, weiß ich wirklich nicht, was das für Vorteile bringt, als das, was uns mit Schnittstellen bereits gegeben ist. Der Hauptvorteil, den ich sehe, ist die Fehlerüberprüfung während der Kompilierung, da das Demarshaling immer noch zur Laufzeit auftritt, was wahrscheinlicher ist, wenn ein Problem mit einem ungültigen Typ übergeben wird. Der andere Vorteil ist eine eingeschränktere Benutzeroberfläche, die meiner Meinung nach keine Sprachänderung rechtfertigt.

Kann ich tun

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
}

Wenn dies nicht möglich ist, sehe ich keinen großen Unterschied zu

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

Kann ich aus Neugierde den Summentyp direkt verwenden oder muss ich ihn explizit in einen bekannten Typ umwandeln, um etwas damit zu tun? Denn wenn ich es ständig in einen bekannten Typ umwandeln muss, weiß ich wirklich nicht, was das für Vorteile bringt, als das, was uns mit Schnittstellen bereits gegeben ist.

@rogpeppe , bitte korrigiert mich, wenn ich falsch liege
Immer einen Mustervergleich durchführen zu müssen (so wird "Casting" bei der Arbeit mit Summentypen in funktionalen Programmiersprachen genannt) ist eigentlich einer der größten Vorteile bei der Verwendung von Summentypen. Den Entwickler zu zwingen, alle möglichen Formen eines Summentyps explizit zu behandeln, ist eine Möglichkeit, den Entwickler daran zu hindern, eine Variable zu verwenden, die denkt, dass es sich um einen bestimmten Typ handelt, obwohl es sich tatsächlich um einen anderen handelt. Ein übertriebenes Beispiel wäre in JavaScript:

const a = "1" // string "1"
const b = a + 5 // string "15" and not number 6

Wenn dies nicht möglich ist, sehe ich keinen großen Unterschied zu

Ich glaube, Sie nennen selbst einige Vorteile, nicht wahr?

Der Hauptvorteil, den ich sehe, ist die Fehlerüberprüfung während der Kompilierung, da das Demarshaling immer noch zur Laufzeit auftritt, was wahrscheinlicher ist, wenn ein Problem mit einem ungültigen Typ übergeben wird. Der andere Vorteil ist eine eingeschränktere Benutzeroberfläche, die meiner Meinung nach keine Sprachänderung rechtfertigt.

// Would the compiler error out on incomplete switch types?

Basierend auf dem, was funktionale Programmiersprachen tun, sollte dies meiner Meinung nach möglich und konfigurierbar sein 👍

@xibz auch Leistung, da dies zur Kompilierzeit im meinem Tod Generika.

@xibz

Kann ich aus Neugierde den Summentyp direkt verwenden oder muss ich ihn explizit in einen bekannten Typ umwandeln, um etwas damit zu tun?

Sie können Methoden darauf aufrufen, wenn alle Member des Typs diese Methode gemeinsam nutzen.

Nimm dein int | float64 als Beispiel, was wäre das Ergebnis von:

var x int|float64 = int(2)
var y int|float64 = float64(0.5)
fmt.Println(x * y)

Würde es eine implizite Konvertierung von int in float64 ? Oder von float64 bis int . Oder würde es in Panik geraten?

Sie haben also fast Recht - Sie müssen in den meisten Fällen vor der Verwendung eine Typprüfung durchführen. Ich glaube, das ist ein Vorteil, aber kein Nachteil.

Der Laufzeitvorteil könnte übrigens erheblich sein. Um mit Ihrem Beispieltyp fortzufahren, müsste ein Slice vom Typ [](int|float64) keine Zeiger enthalten, da es möglich ist, alle Instanzen des Typs in wenigen Bytes darzustellen (wahrscheinlich 16 Bytes aufgrund von Ausrichtungsbeschränkungen), was möglicherweise führen in einigen Fällen zu erheblichen Leistungssteigerungen.

@rogpeppe

In den meisten Fällen müssen Sie vor der Verwendung eine Typprüfung durchführen. Ich glaube, das ist ein Vorteil, aber kein Nachteil.

Ich stimme zu, dass es kein Nachteil ist. Ich versuche nur zu sehen, welche Vorteile uns dies gegenüber Schnittstellen bietet.

ein Slice des Typs müsste keine Zeiger enthalten, da es möglich ist, alle Instanzen des Typs in wenigen Bytes darzustellen (wahrscheinlich 16 Byte aufgrund von Ausrichtungsbeschränkungen), was in einigen Fällen zu erheblichen Leistungsverbesserungen führen könnte.

Hm, ich bin mir nicht sicher, ob ich den wesentlichen Teil davon kaufe. Ich bin mir sicher, dass es in sehr seltenen Fällen die Speichergröße um die Hälfte reduzieren würde. Abgesehen davon glaube ich nicht, dass der gespeicherte Speicher für einen Sprachwechsel nicht signifikant genug ist.

@stouf

Immer einen Mustervergleich durchführen zu müssen (so wird "Casting" bei der Arbeit mit Summentypen in funktionalen Programmiersprachen genannt) ist eigentlich einer der größten Vorteile bei der Verwendung von Summentypen

Aber welche Vorteile bringt es der Sprache, die noch nicht mit Schnittstellen gehandhabt wird? Ursprünglich war ich total für die Summentypen, aber als ich anfing, darüber nachzudenken, verlor ich irgendwie, welche Vorteile es bringen würde.


Mit all dem, wenn die Verwendung eines Summentyps einen saubereren und besser lesbaren Code liefern kann, wäre ich 100% dafür. Wie es aussieht, scheint es jedoch fast identisch mit dem Schnittstellencode zu sein.

@xibz- Mustervergleich wäre in Tree-Walking-Code nützlich, bei dem Sie mehr als eine Ebene tief im Baum suchen möchten. Mit Typenschaltern können Sie jeweils nur eine Ebene tiefer sehen, sodass Sie sie verschachteln müssen.

Dies ist ein wenig konstruiert, aber wenn Sie beispielsweise einen Syntaxbaum für Ausdrücke haben, können Sie zum Abgleichen einer quadratischen Gleichung Folgendes tun:

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.
}

Einfache Beispiele, die nur eine Ebene tief gehen, werden keinen großen Unterschied zeigen, aber hier gehen wir bis zu fünf Ebenen tief, was mit verschachtelten Typschaltern ziemlich kompliziert wäre. Eine Sprache mit Pattern-Matching könnte mehrere Ebenen tief gehen und gleichzeitig sicherstellen, dass Sie keine Fälle übersehen.

Ich bin mir jedoch nicht sicher, wie viel es außerhalb von Compilern herauskommt.

@xibz
Ein Vorteil von Summentypen besteht darin, dass Sie und der Compiler beide genau wissen, welche Typen in der Summe vorkommen können. Das ist im Wesentlichen der Unterschied. Bei leeren Schnittstellen müssen Sie sich immer Sorgen machen und sich vor Missbrauch in der API schützen, indem Sie immer einen Zweig haben, dessen einziger Zweck darin besteht, wiederherzustellen, wenn ein Benutzer Ihnen einen nicht erwarteten Typ gibt.

Da es wenig Hoffnung gibt, dass Summentypen im Compiler implementiert werden, hoffe ich, dass zumindest eine Standard-Kommentardirektive wie //go:union A | B | C vorgeschlagen und von go vet .

Mit einer Standardmethode zum Deklarieren eines Summentyps ist es nach N Jahren möglich zu wissen, wie viele Pakete ihn verwenden.

Mit den neueren Designentwürfen für Generika könnten vielleicht Summentypen an diese gebunden werden.

In einem der Entwürfe steckte die Idee, Schnittstellen anstelle von Verträgen zu verwenden, und die Schnittstellen müssten Typenlisten unterstützen:

type Foo interface { 
     int64, int32, int, uint, uint32, uint64
}

Dies würde zwar an sich keine speichergefüllte Union erzeugen, aber vielleicht bei Verwendung in einer generischen Funktion oder Struktur, wäre es nicht geschachtelt und würde zumindest Typsicherheit bieten, wenn es sich um eine endliche Liste von Typen handelt.

Und vielleicht würde die Verwendung dieser speziellen Schnittstellen innerhalb von Typ-Switches erfordern, dass ein solcher Switch erschöpfend wäre.

Dies ist nicht die ideale kurze Syntax (zB: Foo | int32 | []Bar ), aber es ist etwas.

Mit den neueren Designentwürfen für Generika könnten vielleicht Summentypen an diese gebunden werden.

In einem der Entwürfe steckte die Idee, Schnittstellen anstelle von Verträgen zu verwenden, und die Schnittstellen müssten Typenlisten unterstützen:

type Foo interface { 
     int64, int32, int, uint, uint32, uint64
}

Dies würde zwar an sich keine speichergefüllte Union erzeugen, aber vielleicht bei Verwendung in einer generischen Funktion oder Struktur, wäre es nicht geschachtelt und würde zumindest Typsicherheit bieten, wenn es sich um eine endliche Liste von Typen handelt.

Und vielleicht würde die Verwendung dieser speziellen Schnittstellen innerhalb von Typ-Switches erfordern, dass ein solcher Switch erschöpfend wäre.

Dies ist nicht die ideale kurze Syntax (zB: Foo | int32 | []Bar ), aber es ist etwas.

Ziemlich ähnlich zu meinem Vorschlag: https://github.com/golang/go/issues/19412#issuecomment -520306000

type foobar union {
  int
  float
}

@mathieudevos wow, das gefällt mir eigentlich ganz gut.

Für mich sind die Typenlisten in Schnittstellen die größte Kuriosität (die einzige verbleibende Kuriosität, wirklich) mit dem neuesten Generika-Vorschlag. Sie passen einfach nicht ganz. Dann landen Sie bei einigen Schnittstellen, die Sie nur als Typparametereinschränkungen verwenden können usw.

Das Konzept von union funktioniert meiner Meinung nach großartig, weil Sie dann ein union in ein interface einbetten könnten, um eine "Einschränkung, die Methoden und Rohtypen enthält" zu erreichen. Schnittstellen funktionieren weiterhin so, wie sie sind, und mit Semantik, die um eine Union herum definiert ist, können sie in regulärem Code verwendet werden und das Gefühl der Fremdheit verschwindet.

// 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
    }
}

BEARBEITEN - Habe gerade diese CL gesehen: https://github.com/golang/go/commit/af48c2e84b52f99d30e4787b1b8d527b5cd2ab64

Der Hauptvorteil dieser Änderung besteht darin, dass sie die Tür für die Allgemeinheit öffnet
(non-constraint) Verwendung von Schnittstellen mit Typlisten

...Groß! Schnittstellen werden als Summentypen voll nutzbar, was die Semantik über die reguläre und die Einschränkungsverwendung hinweg vereinheitlicht. (Natürlich noch nicht eingeschaltet, aber ich denke, das ist ein großartiges Ziel.)

Ich habe #41716 geöffnet, um zu diskutieren, wie eine Version von Summentypen im aktuellen Entwurfsentwurf für Generika erscheint.

Ich wollte nur einen alten Vorschlag von @henryas zu algebraischen Datentypen teilen. Es ist sehr gut geschrieben mit bereitgestellten Anwendungsfällen.
https://github.com/golang/go/issues/21154
Leider wurde es von @mvdan am selben Tag ohne Wertschätzung der Arbeit geschlossen. Ich bin mir ziemlich sicher, dass diese Person das wirklich so empfunden hat und daher gibt es keine weiteren Aktivitäten auf dem gh-Konto. Der Typ tut mir leid.

#21154 gefällt mir sehr gut. Es scheint jedoch eine andere Sache zu sein (und daher der Kommentar von @mvdan ), der ihn als Dupe schließt, der nicht ganz trifft. Dort wieder öffnen oder hier in die Diskussion einbeziehen?

Ja, ich würde wirklich gerne die Möglichkeit haben, etwas mehr High-Level-Geschäftslogik auf ähnliche Weise zu modellieren, wie in dieser Ausgabe beschrieben. Summentypen für enum-ähnliche, eingeschränkte Optionen und die vorgeschlagenen akzeptierten Typen wie in der anderen Ausgabe wären in der Toolbox großartig. Business-/Domain-Code in Go fühlt sich im Moment manchmal etwas klobig an.

Mein einziges Feedback ist, dass type foo,bar innerhalb einer Schnittstelle etwas umständlich und zweitklassig aussieht, und ich stimme zu, dass es eine Wahl zwischen nullable und non-nullable geben sollte (wenn möglich).

@ProximaB Ich verstehe nicht, warum Sie sagen "es gibt keine weiteren Aktivitäten auf dem gh-Konto". Seitdem haben sie auch eine Reihe anderer Themen erstellt und kommentiert, viele davon zum Go-Projekt. Ich sehe keine Beweise dafür, dass ihre Tätigkeit von diesem Thema beeinflusst wurde.

Darüber hinaus stimme ich voll und ganz zu, dass Daniel dieses Thema als Duplikat dieses Themas schließt. Ich verstehe nicht, warum @andig sagt, dass sie etwas anderes vorschlagen. Soweit ich den Text von #21154 verstehen kann, schlägt er genau das vor, was wir hier besprechen, und es würde mich nicht wundern, wenn sogar die genaue Syntax irgendwo in diesem Megathread bereits vorgeschlagen würde (die Semantik, soweit beschrieben, sicherlich mehrfach). Tatsächlich würde ich sogar so weit gehen zu sagen, dass Daniels Abschluss durch die Länge dieser Ausgabe richtig ist, da sie bereits eine ziemlich detaillierte und nuancierte Diskussion von #21154 enthält, so dass es mühsam und überflüssig gewesen wäre, all das zu wiederholen.

Ich stimme zu und verstehe, dass es wahrscheinlich enttäuschend ist, einen Vorschlag als Dupe geschlossen zu haben. Aber ich kenne keine praktische Möglichkeit, das zu vermeiden. Die Diskussion an einem Ort scheint für alle Beteiligten von Vorteil zu sein, und es ist eindeutig sinnlos, mehrere Themen für dieselbe Sache offen zu halten, ohne dass darüber diskutiert wird.

Darüber hinaus stimme ich voll und ganz zu, dass Daniel dieses Thema als Duplikat dieses Themas schließt. Ich verstehe nicht, warum @andig sagt, dass sie etwas anderes vorschlagen. Soweit ich den Text von #21154 verstehen kann, schlägt er genau das vor, was wir hier besprechen

Beim erneuten Lesen dieser Ausgabe stimme ich zu. Scheint, dass ich mit diesem Problem mit Generika-Verträgen verwirrt bin. Ich würde Summentypen stark unterstützen. Ich wollte nicht hart klingen, bitte entschuldige mich, wenn es so rüberkam.

Ich bin ein Mensch und das Thema Gartenarbeit kann manchmal schwierig sein, also weisen Sie auf jeden Fall darauf hin, wenn ich einen Fehler mache :) Aber in diesem Fall denke ich, dass jeder Vorschlag für bestimmte Summentypen aus diesem Thread stammen sollte, genau wie https: /github.com/golang/go/issues/19412#issuecomment -701625548

Ich bin ein Mensch und Thema Gartenarbeit manchmal schwierig sein kann, so mit allen Mitteln darauf hin , wenn ich einen Fehler mache :) Aber in diesem Fall glaube ich , dass jeder bestimmte Summe Typen Vorschlag aus diesem Thread Gabel sollte genau wie # 19412 ( Kommentar)

@mvdan ist kein Mensch. Vertrau mir. Ich bin sein Nachbar. War nur Spaß.

Danke für die Aufmerksamkeit. Ich hänge nicht so sehr an meinen Vorschlägen. Fühlen Sie sich frei, jeden Teil davon zu zerstückeln, zu modifizieren und abzuschießen. Ich war im wirklichen Leben beschäftigt, also habe ich keine Chance, an den Diskussionen aktiv teilzunehmen. Es ist gut zu wissen, dass die Leute meine Vorschläge lesen und einige sie sogar mögen.

Die ursprüngliche Absicht besteht darin, die Gruppierung von Typen nach ihrer Domänenrelevanz zu ermöglichen, wenn sie nicht unbedingt ein gemeinsames Verhalten aufweisen, und dies vom Compiler erzwingen zu lassen. Meiner Meinung nach ist dies nur ein statisches Verifikationsproblem, das während der Kompilierung durchgeführt wird. Der Compiler muss keinen Code generieren, der die komplexe Beziehung zwischen den Typen beibehält. Der generierte Code kann diese Domänentypen normal behandeln, als ob es sich um den regulären Schnittstellentyp{} handelt. Der Unterschied besteht darin, dass der Compiler jetzt beim Kompilieren zusätzliche statische Typprüfungen durchführt. Das ist im Grunde die Essenz meines Vorschlags #21154

@henryas Schön dich zu sehen! 😊
Ich frage mich, ob Golang nicht die Ententypisierung verwendet hätte, die die Beziehung zwischen den Typen viel strenger gemacht hätte und die Gruppierung von Objekten nach ihrer Domänenrelevanz ermöglicht hätte, wie Sie in Ihrem Vorschlag beschrieben haben.

@henryas Schön dich zu sehen! 😊
Ich frage mich, ob Golang nicht die Ententypisierung verwendet hätte, die die Beziehung zwischen den Typen viel strenger gemacht hätte und die Gruppierung von Objekten nach ihrer Domänenrelevanz ermöglicht hätte, wie Sie in Ihrem Vorschlag beschrieben haben.

Es wäre, aber das würde das Kompatibilitätsversprechen mit Go 1 brechen. Wir würden wahrscheinlich keine Summentypen brauchen, wenn wir eine explizite Schnittstelle haben. Duck-Typing ist jedoch nicht unbedingt eine schlechte Sache. Es macht bestimmte Dinge leichter und bequemer. Ich genieße es, Enten zu tippen. Es kommt darauf an, das richtige Werkzeug für den Job zu verwenden.

@henryas Ich stimme zu. Es war eine hypothetische Frage. Go-Schöpfer haben definitiv alle Höhen und Tiefen in Betracht gezogen.
Auf der anderen Seite würden Programmieranleitungen wie die Überprüfung der Schnittstellenkonformität nie erscheinen.
https://github.com/uber-go/guide/blob/master/style.md#verify -interface-compliance

Können Sie diese Off-Topic-Diskussion bitte woanders führen? Es gibt viele Leute, die dieses Thema abonniert haben.
Die Zufriedenheit mit offenen Schnittstellen ist seit seiner Einführung Teil von Go und wird sich nicht ändern.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen