Go: Vorschlag: Spezifikation: generische Programmiermöglichkeiten

Erstellt am 14. Apr. 2016  ·  816Kommentare  ·  Quelle: golang/go

Diese Ausgabe schlägt vor, dass Go irgendeine Form von generischer Programmierung unterstützen sollte.
Es hat das Go2-Label, da für Go1.x die Sprache mehr oder weniger fertig ist.

Zu dieser Ausgabe gehört ein allgemeiner Generika-Vorschlag von @ianlancetaylor , der vier spezifische fehlerhafte Vorschläge für generische Programmiermechanismen für Go enthält.

Die Absicht besteht derzeit nicht darin, Generika zu Go hinzuzufügen, sondern den Menschen zu zeigen, wie ein vollständiger Vorschlag aussehen würde. Wir hoffen, dass dies allen helfen wird, die in Zukunft ähnliche Sprachänderungen vorschlagen.

Go2 LanguageChange NeedsInvestigation Proposal generics

Hilfreichster Kommentar

Lassen Sie mich alle vorsorglich an unsere https://golang.org/wiki/NoMeToo-Richtlinie erinnern. Die Emoji-Party ist oben.

Alle 816 Kommentare

CL https://golang.org/cl/22057 erwähnt dieses Problem.

Lassen Sie mich alle vorsorglich an unsere https://golang.org/wiki/NoMeToo-Richtlinie erinnern. Die Emoji-Party ist oben.

Es gibt eine Zusammenfassung der Go Generics-Diskussionen , die versucht, einen Überblick über Diskussionen von verschiedenen Orten zu geben. Es enthält auch einige Beispiele zur Lösung von Problemen, bei denen Sie Generika verwenden möchten.

Der verlinkte Vorschlag enthält zwei „Anforderungen“, die die Umsetzung erschweren und die Typensicherheit verringern können:

  • Definieren Sie generische Typen basierend auf Typen, die erst bei ihrer Instanziierung bekannt sind.
  • Erfordern Sie keine explizite Beziehung zwischen der Definition eines generischen Typs oder einer generischen Funktion und ihrer Verwendung. Das heißt, Programme sollten nicht explizit sagen müssen, dass Typ T generisches G implementiert.

Diese Anforderungen scheinen z. B. ein dem Trait-System von Rust ähnliches System auszuschließen, bei dem generische Typen durch Trait-Grenzen eingeschränkt werden. Warum werden diese benötigt?

Es ist verlockend, Generika auf einer sehr niedrigen Ebene in die Standardbibliothek einzubauen, wie in C++ std::basic_string, std::Zuordner>. Das hat seine Vorteile – sonst würde es niemand tun – aber es hat weitreichende und manchmal überraschende Auswirkungen, wie in unverständlichen C++-Fehlermeldungen.

Das Problem in C++ ergibt sich aus der Typüberprüfung des generierten Codes. Vor der Codegenerierung ist eine zusätzliche Typprüfung erforderlich. Der C++- Konzeptvorschlag ermöglicht dies, indem er dem Autor von generischem Code erlaubt, die Anforderungen eines generischen Typs zu spezifizieren. Auf diese Weise kann die Kompilierung vor der Codegenerierung fehlschlagen und einfache Fehlermeldungen können ausgegeben werden. Das Problem mit C++-Generika (ohne Konzepte) besteht darin, dass der generische Code die Spezifikation des generischen Typs _ist_. Dadurch entstehen die unverständlichen Fehlermeldungen.

Generischer Code sollte nicht die Spezifikation eines generischen Typs sein.

@tamird Es ist ein wesentliches Merkmal der Schnittstellentypen von Go, dass Sie einen Nicht-Schnittstellentyp T definieren und später einen Schnittstellentyp I definieren können, sodass T I implementiert. Siehe https://golang.org/doc/faq#implements_interface . Es wäre widersprüchlich, wenn Go eine Form von Generika implementieren würde, für die ein generischer Typ G nur mit einem Typ T verwendet werden könnte, der ausdrücklich sagt: „Ich kann verwendet werden, um G zu implementieren.“

Ich bin mit Rust nicht vertraut, aber ich kenne keine Sprache, in der T explizit angeben muss, dass sie zur Implementierung von G verwendet werden kann. Die beiden von Ihnen erwähnten Anforderungen bedeuten nicht, dass G keine Anforderungen an T stellen kann, nur da I Anforderungen an T stellt. Die Anforderungen bedeuten lediglich, dass G und T unabhängig voneinander geschrieben werden können. Das ist eine sehr wünschenswerte Funktion für Generika, und ich kann mir nicht vorstellen, darauf zu verzichten.

@ianlancetaylor https://doc.rust-lang.org/book/traits.html erklärt Rusts Eigenschaften. Obwohl ich denke, dass sie im Allgemeinen ein gutes Modell sind, würden sie schlecht zu Go passen, wie es heute existiert.

@sbunce Ich dachte auch, dass Konzepte die Antwort sind, und Sie können die Idee sehen, die in den verschiedenen Vorschlägen vor dem letzten verstreut ist. Aber es ist entmutigend, dass Konzepte ursprünglich für das geplant waren, was C++ 11 wurde, und es ist jetzt 2016, und sie sind immer noch umstritten und nicht besonders nahe daran, in die C++-Sprache aufgenommen zu werden.

Wäre die wissenschaftliche Literatur für Hinweise zur Bewertung von Ansätzen wertvoll?

Das einzige Papier, das ich zu diesem Thema gelesen habe, ist Profitieren Entwickler von generischen Typen? (Entschuldigung, Paywall, Sie könnten sich zu einem PDF-Download googlen), der Folgendes zu sagen hatte

Folglich eine konservative Interpretation des Experiments
ist, dass generische Typen als Kompromiss betrachtet werden können
zwischen den positiven Dokumentationsmerkmalen und der
negative Dehnbarkeitseigenschaften. Der spannende Teil von
Die Studie ist, dass sie eine Situation zeigte, in der die Verwendung von a
(stärker) statische Art System hatte einen negativen Einfluss auf die
Entwicklungszeit bei gleichzeitig erwartetem Nutzen
fit – die Verringerung der Zeit zur Behebung von Typfehlern – erschien nicht.
Wir denken, dass solche Aufgaben bei zukünftigen Experimenten helfen könnten
Identifizierung der Auswirkungen von Typensystemen.

Ich sehe auch, dass https://github.com/golang/go/issues/15295 auch auf Lightweight, Flexible Object-Oriented Generics verweist.

Wenn wir uns bei der Entscheidung auf die Wissenschaft stützen würden, wäre es meiner Meinung nach besser, im Voraus eine Literaturrecherche durchzuführen und wahrscheinlich frühzeitig zu entscheiden, ob wir empirische Studien anders gewichten würden als solche, die sich auf Beweise stützen.

Siehe: http://dl.acm.org/citation.cfm?id=2738008 von Barbara Liskov:

Die Unterstützung für generische Programmierung in modernen objektorientierten Programmiersprachen ist umständlich und es fehlt ihr an wünschenswerter Ausdruckskraft. Wir führen einen ausdrucksstarken Generizitätsmechanismus ein, der die Ausdruckskraft erhöht und die statische Prüfung stärkt, während er in gängigen Anwendungsfällen leicht und einfach bleibt. Wie Typklassen und -konzepte erlaubt der Mechanismus existierenden Typen, Typeinschränkungen rückwirkend zu modellieren. Für die Ausdruckskraft legen wir Modelle als benannte Konstrukte offen, die definiert und explizit ausgewählt werden können, um Einschränkungen zu bezeugen; Bei der üblichen Verwendung von Generizität weisen Typen jedoch implizit Einschränkungen ohne zusätzlichen Programmieraufwand auf.

Ich denke, was sie dort gemacht haben, ist ziemlich cool - es tut mir leid, wenn dies der falsche Ort ist, um aufzuhören, aber ich konnte keinen Ort zum Kommentieren in /proposals finden und ich habe hier kein passendes Problem gefunden.

Es könnte interessant sein, einen oder mehrere experimentelle Transpiler zu haben - einen Go-Generika-Quellcode für Go 1.xy-Quellcode-Compiler.
Ich meine - zu viel Gerede/Argumente-für-meine-Meinung, und niemand schreibt Quellcode, der _versucht_ , _irgendeine Art_ Generika für Go zu implementieren.

Nur um Wissen und Erfahrungen mit Go und Generika zu sammeln - um zu sehen, was funktioniert und was nicht.
Wenn alle Go-Generika-Lösungen nicht wirklich gut sind, dann; Keine Generika für Go.

Kann der Vorschlag auch die Auswirkungen auf die Binärgröße und den Speicherbedarf beinhalten? Ich würde erwarten, dass es für jeden konkreten Werttyp eine Codeduplizierung geben wird, damit Compileroptimierungen daran arbeiten. Ich hoffe auf eine Garantie, dass es bei konkreten Pointer-Typen zu keiner Code-Duplizierung kommt.

Ich biete eine Pugh-Entscheidungsmatrix an. Meine Kriterien umfassen die Auswirkungen auf die Sichtbarkeit (Komplexität der Quelle, Größe). Ich habe auch eine Rangfolge der Kriterien erzwungen, um die Gewichtungen für die Kriterien zu bestimmen. Ihre eigenen können natürlich variieren. Ich habe "Schnittstellen" als Standardalternative verwendet und dies mit "Kopieren/Einfügen"-Generika, vorlagenbasierten Generika (ich hatte so etwas im Sinn wie die D-Sprache funktioniert) und etwas, das ich Runtime-Instanziierungsstil-Generika genannt habe, verglichen. Ich bin mir sicher, dass dies eine gewaltige Vereinfachung ist. Nichtsdestotrotz könnte es einige Ideen zur Bewertung von Entscheidungen anregen ... dies sollte ein öffentlicher Link zu meinem Google Sheet sein, hier

Pingen Sie @yizhouzhang und @andrewcmyers , damit sie ihre Meinung zu Gattungen wie Generika in Go äußern können. Klingt so, als könnte es gut zusammenpassen :)

Das generische Design, das wir für Genus entwickelt haben, verfügt über eine statische, modulare Typprüfung, erfordert keine vorherige Deklaration, dass Typen eine Schnittstelle implementieren, und verfügt über eine angemessene Leistung. Ich würde es mir auf jeden Fall ansehen, wenn Sie an Generika für Go denken. Es scheint nach meinem Verständnis von Go gut zu passen.

Hier ist ein Link zu dem Papier, für das kein Zugriff auf die ACM Digital Library erforderlich ist:
http://www.cs.cornell.edu/andru/papers/genus/

Die Genus-Homepage ist hier: http://www.cs.cornell.edu/projects/genus/

Wir haben den Compiler noch nicht öffentlich veröffentlicht, aber wir planen, dies ziemlich bald zu tun.

Beantworten Sie gerne alle Fragen, die die Leute haben.

In Bezug auf die Entscheidungsmatrix von @mandolyte erzielt Genus eine 17, gleichauf auf Platz 1. Ich würde jedoch einige weitere Kriterien hinzufügen, um zu bewerten. Zum Beispiel ist die modulare Typprüfung wichtig, wie andere wie @sbunce oben beobachtet haben, aber vorlagenbasierten Schemata fehlt es. Der technische Bericht für das Genus-Papier enthält auf Seite 34 eine viel größere Tabelle, in der verschiedene Generika-Designs verglichen werden.

Ich habe gerade das gesamte Dokument Summary of Go Generics durchgesehen, das eine hilfreiche Zusammenfassung früherer Diskussionen war. Der Generics-Mechanismus in Genus leidet meines Erachtens nicht unter den Problemen, die für C++, Java oder C# identifiziert wurden. Genus-Generika werden anders als in Java reifiziert, sodass Sie Typen zur Laufzeit herausfinden können. Sie können auch primitive Typen instanziieren, und Sie erhalten kein implizites Boxing an den Stellen, an denen Sie es wirklich nicht wollen: Arrays von T, wobei T ein Primitiv ist. Das Typsystem ist Haskell und Rust am nächsten – eigentlich etwas leistungsfähiger, aber meiner Meinung nach auch intuitiv. Primitive Spezialisierung ala C# wird derzeit in Genus nicht unterstützt, könnte es aber sein. In den meisten Fällen kann die Spezialisierung zur Verbindungszeit bestimmt werden, sodass eine echte Laufzeitcodegenerierung nicht erforderlich wäre.

CL https://golang.org/cl/22163 erwähnt dieses Problem.

Eine Möglichkeit, generische Typen einzuschränken, ohne dass neue Sprachkonzepte hinzugefügt werden müssen: https://docs.google.com/document/d/1rX4huWffJ0y1ZjqEpPrDy-kk-m9zWfatgCluGRBQveQ/edit?usp=sharing.

Genus sieht wirklich cool aus und ist eindeutig ein wichtiger Fortschritt der Kunst, aber ich sehe nicht, wie es auf Go zutreffen würde. Hat jemand eine Skizze, wie es sich in das Go-Typ-System / die Philosophie integrieren würde?

Das Problem ist, dass das Go-Team Versuche abblockt. Der Titel bringt die Absichten des Go-Teams klar zum Ausdruck. Und als wäre das noch nicht genug, um alle Abnehmer abzuschrecken, machen die von ian geforderten Eigenschaften eines so breiten Bereichs in den Vorschlägen von ian deutlich, dass Generika Sie nicht wollen, wenn Sie Generika wollen. Es ist idiotisch, auch nur den Versuch zu unternehmen, mit dem Go-Team in Dialog zu treten. Für diejenigen, die auf der Suche nach Generika sind, sage ich die Sprache brechen. Beginnen Sie eine neue Reise – viele werden folgen. Ich habe bereits einige großartige Arbeit gesehen, die in Gabeln geleistet wurde. Organisiert euch, versammelt euch um eine Sache

Wenn jemand versuchen möchte, eine Generika-Erweiterung für Go auf der Grundlage des Genus-Designs zu entwickeln, helfen wir gerne weiter. Wir kennen Go nicht gut genug, um ein Design zu entwickeln, das mit der bestehenden Sprache harmoniert. Ich denke, der erste Schritt wäre ein Strohmann-Designvorschlag mit ausgearbeiteten Beispielen.

@andrewcmyers hofft, dass @ianlancetaylor dabei mit dir zusammenarbeiten wird. Nur ein paar Beispiele zum Anschauen zu haben, würde viel helfen.

Ich habe das Genus-Papier gelesen. Soweit ich es verstehe, scheint es nett für Java zu sein, scheint aber nicht für Go geeignet zu sein.

Ein Schlüsselaspekt von Go ist, dass beim Schreiben eines Go-Programms das meiste, was Sie schreiben, Code ist. Dies unterscheidet sich von C++ und Java, wo viel mehr von dem, was Sie schreiben, Typen sind. Bei Genus scheint es hauptsächlich um Typen zu gehen: Sie schreiben eher Einschränkungen und Modelle als Code. Das Typensystem von Go ist sehr, sehr einfach. Das Typensystem von Genus ist weitaus komplexer.

Die Ideen der rückwirkenden Modellierung scheinen, obwohl sie für Java eindeutig nützlich sind, überhaupt nicht zu Go zu passen. Es werden bereits Adaptertypen verwendet, um vorhandene Typen mit Schnittstellen abzugleichen; bei der Verwendung von Generika sollte nichts weiter benötigt werden.

Es wäre interessant zu sehen, wie diese Ideen auf Go angewendet werden, aber ich bin nicht optimistisch, was das Ergebnis angeht.

Ich bin kein Go-Experte, aber sein Typsystem scheint nicht einfacher zu sein als Java vor der Generik. Die Typsyntax ist auf nette Weise etwas leichter, aber die zugrunde liegende Komplexität scheint ungefähr gleich zu sein.

In Genus sind Einschränkungen Typen, aber Modelle sind Code. Modelle sind Adapter, aber sie passen sich an, ohne eine Schicht tatsächlicher Verpackung hinzuzufügen. Dies ist sehr nützlich, wenn Sie beispielsweise ein ganzes Array von Objekten an eine neue Oberfläche anpassen möchten. Durch rückwirkende Modellierung können Sie das Array als ein Array von Objekten behandeln, die die gewünschte Schnittstelle erfüllen.

Ich wäre nicht überrascht, wenn es im typtheoretischen Sinne komplizierter wäre als Java (vorgeneriert), obwohl es in der Praxis einfacher zu verwenden ist.

Abgesehen von der relativen Komplexität sind sie so unterschiedlich, dass Genus sie nicht 1:1 abbilden konnte. Keine Untertypisierung scheint groß zu sein.

Wenn Sie interessiert sind:

Die kürzeste Zusammenfassung der relevanten philosophischen / gestalterischen Unterschiede, die ich erwähnt habe, ist in den folgenden FAQ-Einträgen enthalten:

Im Gegensatz zu den meisten Sprachen ist die Go-Spezifikation sehr kurz und klar, was die relevanten Eigenschaften des Typsystems betrifft, beginnen Sie bei https://golang.org/ref/spec#Constants und gehen Sie direkt durch bis zum Abschnitt mit dem Titel „Blocks“ (alles weniger als 11 gedruckte Seiten).

Im Gegensatz zu Java- und C#-Generika basiert der Genus-Generika-Mechanismus nicht auf Subtypisierung. Andererseits scheint es mir, dass Go Subtyping hat, aber strukturelle Subtyping. Das passt auch gut zum Genus-Ansatz, der einen strukturellen Charakter hat, anstatt sich auf vordeklarierte Beziehungen zu verlassen.

Ich glaube nicht, dass Go eine strukturelle Subtypisierung hat.

Während zwei Typen, deren zugrunde liegender Typ identisch ist, daher identisch sind
können gegeneinander ausgetauscht werden, https://play.golang.org/p/cT15aQ-PFr

Dies erstreckt sich nicht auf zwei Typen, die eine gemeinsame Teilmenge von Feldern teilen,
https://play.golang.org/p/KrC9_BDXuh.

Am Do, 28. April 2016 um 13:09 Uhr, Andrew Myers [email protected]
schrieb:

Im Gegensatz zu Java- und C#-Generika basiert der Genus-Generika-Mechanismus nicht auf
Subtypisierung. Andererseits scheint es mir, dass Go Subtyping hat,
aber strukturelle Subtypisierung. Das passt auch gut zum Genus-Ansatz,
die einen strukturellen Geschmack hat, anstatt sich auf vordeklarierte zu verlassen
Beziehungen.


Sie erhalten dies, weil Sie diesen Thread abonniert haben.
Antworten Sie direkt auf diese E-Mail oder zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/15292#issuecomment -215298127

Danke, ich habe einen Teil der Sprache darüber falsch interpretiert, wann Typen Schnittstellen implementieren. Eigentlich sieht es für mich so aus, als ob Go-Schnittstellen mit einer bescheidenen Erweiterung als Beschränkungen im Genus-Stil verwendet werden könnten.

Genau aus diesem Grund habe ich Sie angepingt, Genus scheint ein viel besserer Ansatz zu sein als Java/C#-ähnliche Generika.

Es gab einige Ideen zur Spezialisierung auf die Schnittstellentypen; zB die _Paketvorlagen_ Ansatz "Vorschläge" 1 2 sind Beispiele dafür.

tl;dr; Das generische Paket mit Schnittstellenspezialisierung würde folgendermaßen aussehen:

package set
type E interface { Equal(other E) bool }
type Set struct { items []E }
func (s *Set) Add(item E) { ... }

Version 1. mit paketbezogener Spezialisierung:

package main
import items set[[E: *Item]]

type Item struct { ... }
func (a *Item) Equal(b *Item) bool { ... }

var xs items.Set
xs.Add(&Item{})

Version 2. die deklarationsbezogene Spezialisierung:

package main
import set

type Item struct { ... }
func (a *Item) Equal(b *Item) bool { ... }

var xs set.Set[[E: *Item]]
xs.Add(&Item{})

Die paketbezogenen Generika werden Menschen daran hindern, das Generikasystem erheblich zu missbrauchen, da die Nutzung auf grundlegende Algorithmen und Datenstrukturen beschränkt ist. Es verhindert grundsätzlich das Erstellen neuer Sprachabstraktionen und Funktionscodes.

Die deklarationsbezogene Spezialisierung hat mehr Möglichkeiten auf Kosten, wodurch sie anfälliger für Missbrauch ist und ausführlicher ist. Aber Funktionscode wäre möglich, zB:

type E interface{}
func Reduce(zero E, items []E, fn func(a, b E) E) E { ... }

Reduce[[E: int]](0, []int{1,2,3,4}, func(a, b int)int { return a + b } )
// there are probably ways to have some aliases (or similar) to make it less verbose
alias ReduceInt Reduce[[E: int]]
func ReduceInt Reduce[[E: int]]

Der Ansatz der Schnittstellenspezialisierung hat interessante Eigenschaften:

  • Bereits existierende Pakete, die Schnittstellen verwenden, wären spezialisierbar. zB wäre ich in der Lage, sort.Sort[[Interface:MyItems]](...) aufzurufen und die Sortierarbeit für den konkreten Typ statt für die Schnittstelle zu haben (mit potenziellen Gewinnen durch Inlining).
  • Das Testen wird vereinfacht, ich muss nur sicherstellen, dass der generische Code mit Schnittstellen funktioniert.
  • Es ist einfach zu erklären, wie es funktioniert. dh stellen Sie sich vor, dass [[E: int]] alle Deklarationen von E durch int ersetzt.

Bei der paketübergreifenden Arbeit treten jedoch Ausführlichkeitsprobleme auf:

type op
import "set"

type E interface{}
func Union(a, b set.Set[[set.E: E]]) set.Set[[set.E: E]] {
    result := set.New[[set.E: E]]()
    ...
}

_Natürlich ist das Ganze einfacher zu formulieren als zu realisieren. Intern gibt es wahrscheinlich haufenweise Probleme und Wege, wie es funktionieren könnte._

_PS, an die Nörgler über den langsamen Fortschritt bei Generika, ich applaudiere dem Go-Team dafür, dass es mehr Zeit auf Probleme verwendet, die einen größeren Nutzen für die Community haben, z. B. Compiler-/Laufzeitfehler, SSA, GC, http2._

@egonelbre Ihr Argument, dass Generika auf Paketebene "Missbrauch" verhindern, ist ein wirklich wichtiger Punkt, den die meisten Leute meiner Meinung nach übersehen. Das und ihre relative semantische und syntaktische Einfachheit (nur die Paket- und Importkonstrukte sind betroffen) machen sie für Go sehr attraktiv.

@andrewcymyers Interessant, dass Go-Schnittstellen Ihrer Meinung nach als Beschränkungen im Genus-Stil funktionieren. Ich hätte gedacht, dass sie immer noch das Problem haben, dass Sie mit ihnen keine Multi-Type-Parameter-Constraints ausdrücken können.

Eine Sache, die mir jedoch gerade klar geworden ist, ist, dass Sie in Go eine Schnittstelle inline schreiben können. Mit der richtigen Syntax könnten Sie also die Schnittstelle in den Geltungsbereich aller Parameter versetzen und Multiparameter-Einschränkungen erfassen:

type [V, E] Graph [V interface { Edges() E }, E interface { Endpoints() (V, V) }] ...

Ich denke, das größere Problem mit Schnittstellen als Einschränkungen ist, dass Methoden in Go nicht so allgegenwärtig sind wie in Java. Eingebaute Typen haben keine Methoden. Es gibt keinen Satz universeller Methoden wie in java.lang.Object. Benutzer definieren normalerweise keine Methoden wie Equals oder HashCode für ihre Typen, es sei denn, dies ist ausdrücklich erforderlich, da diese Methoden einen Typ nicht für die Verwendung als Zuordnungsschlüssel oder in einem Algorithmus qualifizieren, der Gleichheit erfordert.

(Equality in Go ist eine interessante Geschichte. Die Sprache gibt Ihrem Typ "==", wenn er bestimmte Anforderungen erfüllt (siehe https://golang.org/ref/spec#Logical_operators, suchen Sie nach "comparable"). Jeder Typ mit " ==" kann als Zuordnungsschlüssel dienen. Aber wenn Ihr Typ "==" nicht verdient, können Sie nichts schreiben, was ihn als Zuordnungsschlüssel funktionieren lässt.)

Da Methoden nicht allgegenwärtig sind und es keine einfache Möglichkeit gibt, Eigenschaften der integrierten Typen auszudrücken (z. B. mit welchen Operatoren sie arbeiten), habe ich vorgeschlagen, Code selbst als generischen Einschränkungsmechanismus zu verwenden. Siehe den Link in meinem Kommentar vom 18. April oben. Dieser Vorschlag hat seine Probleme, aber ein nettes Feature ist, dass generischer numerischer Code immer noch die üblichen Operatoren anstelle von umständlichen Methodenaufrufen verwenden könnte.

Die andere Möglichkeit besteht darin, Methoden zu Typen hinzuzufügen, denen sie fehlen. Sie können dies in der vorhandenen Sprache viel leichter tun als in Java:

Typ Int Int
func (i Int) Less(j Int) bool { return i < j }

Der Int-Typ „erbt“ alle Operatoren und andere Eigenschaften von int. Obwohl Sie zwischen den beiden umwandeln müssen, um Int und int zusammen zu verwenden, was ein Schmerz sein kann.

Gattungsmodelle könnten hier helfen. Aber sie müssten sehr einfach gehalten werden. Ich denke, @ianlancetaylor war zu eng in seiner Charakterisierung von Go als das Schreiben von mehr Code, weniger Typen. Das allgemeine Prinzip ist, dass Go Komplexität verabscheut. Wir schauen uns Java und C++ an und sind fest entschlossen, niemals dorthin zu gehen. (Nichts für ungut.)

Eine schnelle Idee für ein modellähnliches Feature wäre also: Lassen Sie den Benutzer Typen wie Int oben schreiben und erlauben Sie in generischen Instanziierungen "int mit Int", was bedeutet, dass Sie den Typ int verwenden, ihn aber wie Int behandeln. Dann gibt es kein offenkundiges Sprachkonstrukt namens model mit seinem Schlüsselwort Vererbungssemantik und so weiter. Ich verstehe Modelle nicht gut genug, um zu wissen, ob dies machbar ist, aber es ist mehr im Geiste von Go.

@jba Dem Prinzip der Vermeidung von Komplexität stimmen wir auf jeden Fall zu. "So einfach wie möglich, aber nicht einfacher." Aus diesen Gründen würde ich wahrscheinlich einige Genus-Features aus Go herauslassen, zumindest am Anfang.

Eines der netten Dinge am Genus-Ansatz ist, dass er eingebaute Typen reibungslos handhabt. Denken Sie daran, dass primitive Typen in Java keine Methoden haben und Genus dieses Verhalten erbt. Stattdessen behandelt Genus primitive Typen _als ob_ sie eine ziemlich große Sammlung von Methoden zum Zweck der Erfüllung von Beschränkungen hätten. Eine Hash-Tabelle erfordert, dass ihre Schlüssel gehasht und verglichen werden können, aber alle primitiven Typen erfüllen diese Einschränkung. Typ-Instanziierungen wie Map[int, boolean] sind also ohne weiteres Aufhebens vollkommen legal. Um dies zu erreichen, muss nicht zwischen zwei Arten von Ganzzahlen (int vs. Int) unterschieden werden. Wenn int jedoch nicht mit genügend Operationen für einige Anwendungen ausgestattet wäre, würden wir ein Modell verwenden, das fast genau wie die Verwendung von Int oben ist.

Erwähnenswert ist auch die Idee der "natürlichen Vorbilder" in Genus. Normalerweise müssen Sie kein Modell deklarieren, um einen generischen Typ zu verwenden: Wenn das Typargument die Einschränkung erfüllt, wird automatisch ein natürliches Modell generiert. Unserer Erfahrung nach ist dies der Regelfall; Das Deklarieren explizit benannter Modelle ist normalerweise nicht erforderlich. Wenn jedoch ein Modell benötigt wird – zum Beispiel, wenn Sie ints auf eine nicht standardmäßige Weise hashen möchten – dann ähnelt die Syntax dem, was Sie vorgeschlagen haben: Map[int with fancyHash, boolean] . Ich würde argumentieren, dass Genus in normalen Anwendungsfällen syntaktisch leicht ist, aber bei Bedarf mit Reserven ausgestattet ist.

@egonelbre Was Sie hier vorschlagen, sieht aus wie virtuelle Typen, die von Scala unterstützt werden. Es gibt ein ECOOP'97-Papier von Kresten Krab Thorup, "Genericity in Java with virtual types", das diese Richtung untersucht. Wir haben in unserer Arbeit auch Mechanismen für virtuelle Typen und virtuelle Klassen entwickelt ("J&: nested Intersection for Scalable Software Composition", OOPSLA'06).

Da wörtliche Initialisierungen in Go allgegenwärtig sind, musste ich mich fragen, wie ein Funktionsliteral aussehen würde. Ich vermute, dass der Code, um dies zu handhaben, größtenteils in Go generiert, repariert und umbenannt ist. Vielleicht wird es jemanden inspirieren :-)

// die (generische) Funktionstypdefinition
Typ Sum64 Funktion (X, Y) Float64 {
Rückgabe Float64(X) + Float64(Y)
}

// Eins instanziieren, positionell
ich := 42
var j uint = 86
Summe := &Summe64{i, j}

// einen instanziieren, nach benannten Parametertypen
sum := &Sum64{ X:int, Y:uint}

// jetzt benutze es...
Ergebnis := sum(i, j) // Ergebnis ist 128

Ians Vorschlag verlangt zu viel. Wir können unmöglich alle Funktionen auf einmal entwickeln, es wird viele Monate in einem unfertigen Zustand existieren.

In der Zwischenzeit kann das unvollendete Projekt nicht als offizielle Go-Sprache bezeichnet werden, bis es abgeschlossen ist, da dies zu einer Fragmentierung des Ökosystems führen könnte.

Die Frage ist also, wie man das plant.

Ein großer Teil des Projekts würde auch die Entwicklung des Referenzkorpus sein.
die eigentlichen generischen Sammlungen, Algorithmen und andere Dinge so zu entwickeln, dass wir uns alle einig sind, dass sie idiomatisch sind, während wir die neuen go 2.0-Funktionen verwenden

Eine mögliche Syntax?

// Module defining generic type
module list(type t)

type node struct {
    next *node
    data t
}
// Module using generic type:
import (
    intlist "module/path/to/list" (int)
    funclist "module/path/to/list" (func (int) int)
)

l := intlist.New()
l.Insert(5)

@md2perpe , Syntax ist nicht der schwierige Teil dieses Problems. Tatsächlich ist es bei weitem das einfachste. Bitte beachten Sie die Diskussion und die verlinkten Dokumente oben.

@md2perpe Wir haben die Parametrisierung ganzer Pakete ("Module") als einen Weg zur internen Generizität diskutiert - es scheint ein Weg zu sein, den syntaktischen Overhead zu reduzieren. Aber es hat andere Probleme; zB ist nicht klar, wie man es mit Typen parametrisieren würde, die nicht auf Paketebene sind. Aber die Idee könnte es dennoch wert sein, im Detail untersucht zu werden.

Ich möchte eine Perspektive teilen: In einem Paralleluniversum waren alle Go-Funktionssignaturen immer darauf beschränkt, nur Schnittstellentypen zu erwähnen, und anstatt heute nach Generika zu verlangen, gibt es eine Möglichkeit, die mit Schnittstellenwerten verbundene Umleitung zu vermeiden. Überlegen Sie, wie Sie dieses Problem lösen würden (ohne die Sprache zu ändern). Ich habe einige Ideen.

@thwd Würde der Autor der Bibliothek also weiterhin Schnittstellen verwenden, jedoch ohne die heute erforderlichen Typumschaltungen und Typzusicherungen. Und würde der Benutzer der Bibliothek einfach konkrete Typen übergeben, als ob die Bibliothek die Typen so verwenden würde, wie sie sind ... und würde der Compiler dann die beiden in Einklang bringen? Und wenn es nicht sagen konnte, warum? (z. B. wurde der Modulo-Operator in der Bibliothek verwendet, aber der Benutzer hat ein Stück von etwas geliefert.

Bin ich in der Nähe? :-)

@mandolyte ja! Lassen Sie uns E-Mails austauschen, um diesen Thread nicht zu verschmutzen. Sie erreichen mich unter "me at thwd dot me". Jeder andere, der dies liest und interessiert sein könnte; Schicken Sie mir eine E-Mail und ich füge Sie dem Thread hinzu.

Es ist eine großartige Funktion für type system und collection library .
Eine mögliche Syntax:

type Element<T> struct {
    prev, next *Element<T>
    list *List<T>
    value T
}
type List<E> struct {
    root Element<E>
    len int
}

Für interface

type Collection<E> interface {
    Size() int
    Add(e E) bool
}

super type oder type implement :

func contain(l List<parent E>, e E) bool
<V> func (c Collection<child E>)Map(fn func(e E) V) Collection

Das obige aka in Java:

boolean contain(List<? super E>, E e)
<V> Collection Map(Function<? extend E, V> mapFunc);

@leaxoy wie gesagt, die Syntax ist hier nicht der schwierige Teil. Siehe Diskussion oben.

Seien Sie sich nur bewusst, dass die Kosten für die Schnittstelle unglaublich hoch sind.

Bitte erläutern Sie, warum Sie denken, dass die Kosten für die Schnittstelle "unglaublich" sind.
groß.
Es sollte nicht schlechter sein als die nicht spezialisierten virtuellen Aufrufe von C++.

@minux Über die Leistungskosten kann ich nichts sagen, aber in Bezug auf die Codequalität. interface{} kann zur Kompilierzeit nicht verifiziert werden, Generika jedoch schon. Meiner Meinung nach ist dies in den meisten Fällen wichtiger als die Leistungsprobleme bei der Verwendung von interface{} .

@xoviat

Dies hat wirklich keinen Nachteil, da die dafür erforderliche Verarbeitung den Compiler nicht verlangsamt.

Es gibt (mindestens) zwei Nachteile.

Einer ist die erhöhte Arbeit für den Linker: Wenn die Spezialisierungen für zwei Typen zu demselben zugrunde liegenden Maschinencode führen, möchten wir nicht zwei Kopien dieses Codes kompilieren und verknüpfen.

Ein weiterer Grund ist, dass parametrisierte Pakete weniger aussagekräftig sind als parametrisierte Methoden. (Siehe die im ersten Kommentar verlinkten Vorschläge für Details.)

Ist Hypertyp eine gute Idee?

func getAddFunc (aType type) func(aType, aType) aType {
    return func(a, b aType) aType {
        return a+b
    }
}

Ist Hypertyp eine gute Idee?

Was Sie hier beschreiben, ist nur Typparametrisierung ala C++ (dh Vorlagen). Es führt keine modulare Typprüfung durch, da es keine Möglichkeit gibt, anhand der gegebenen Informationen zu wissen, dass der Typ aType eine +-Operation hat. Die Parametrisierung eingeschränkter Typen wie in CLU, Haskell, Java, Genus ist die Lösung.

@ golang101 Ich habe einen detaillierten Vorschlag in diese Richtung. Ich schicke eine CL, um sie der Liste hinzuzufügen, aber es ist unwahrscheinlich, dass sie angenommen wird.

CL https://golang.org/cl/38731 erwähnt dieses Problem.

@andrewcmyers

Es führt keine modulare Typprüfung durch, da es keine Möglichkeit gibt, anhand der gegebenen Informationen zu wissen, dass der Typ aType eine +-Operation hat.

Sicher gibt es das. Diese Einschränkung ist in der Definition der Funktion implizit enthalten, und Einschränkungen dieser Form können an alle (transitiven) Kompilierzeit-Aufrufer von getAddFunc werden.

Die Einschränkung ist nicht Teil eines Go _type_ – das heißt, sie kann nicht im Typsystem des Laufzeitteils der Sprache codiert werden – aber das bedeutet nicht, dass sie nicht modular ausgewertet werden kann.

Meinen Vorschlag als 2016-09-compile-time-functions.md hinzugefügt.

Ich gehe nicht davon aus, dass es angenommen wird, aber es kann zumindest als interessanter Bezugspunkt dienen.

@bcmills Ich denke, dass Kompilierzeitfunktionen eine starke Idee sind, abgesehen von der Berücksichtigung von Generika. Zum Beispiel habe ich einen Sudoku-Löser geschrieben, der einen Popcount benötigt. Um das zu beschleunigen, habe ich die Popcounts für die verschiedenen möglichen Werte vorberechnet und als Go source gespeichert. Dies ist etwas, was man mit go:generate machen könnte. Aber wenn es eine Kompilierzeitfunktion gäbe, könnte diese Nachschlagetabelle genauso gut zur Kompilierzeit berechnet werden, sodass der maschinell generierte Code nicht an das Repo übergeben werden muss. Im Allgemeinen eignet sich jede Art von memoisierbarer mathematischer Funktion gut für vorgefertigte Nachschlagetabellen mit Kompilierzeitfunktionen.

Spekulativer könnte man zB auch eine Protobuf-Definition aus einer kanonischen Quelle herunterladen und diese zum Erstellen von Typen zur Kompilierzeit verwenden. Aber vielleicht ist das zu viel, um es zur Kompilierzeit tun zu dürfen?

Ich habe das Gefühl, dass Funktionen zur Kompilierzeit gleichzeitig zu leistungsfähig und zu schwach sind: Sie sind zu flexibel und können auf seltsame Weise Fehler verursachen / das Kompilieren verlangsamen, wie es C++-Vorlagen tun, aber andererseits sind sie zu statisch und schwer zu handhaben sich an Dinge wie erstklassige Funktionen anpassen.

Für den zweiten Teil sehe ich keine Möglichkeit, so etwas wie ein "Slice von Funktionen, die Slices eines bestimmten Typs verarbeiten und ein Element zurückgeben" oder in einer Ad-hoc-Syntax []func<T>([]T) T zu erstellen, die ist in praktisch jeder statisch typisierten funktionalen Sprache sehr einfach durchzuführen. Was wirklich benötigt wird, sind Werte , die parametrische Typen annehmen können, und nicht eine Codegenerierung auf Quellcodeebene.

@Bunsim

Für den zweiten Teil sehe ich keine Möglichkeit, so etwas wie ein "Slice von Funktionen zu erstellen, die Slices eines bestimmten Typs verarbeiten und ein Element zurückgeben".

Wenn Sie über einen einzelnen Typparameter sprechen, würde das in meinem Vorschlag geschrieben werden:

const func SliceOfSelectors(T gotype) gotype { return []func([]T)T (type) }

Wenn Sie über das Mischen von Typparametern und Wertparametern sprechen, nein, mein Vorschlag lässt dies nicht zu: Ein Teil des Sinns von Funktionen zur Kompilierzeit besteht darin, mit unverpackten Werten und der Art der Laufzeitparametrik arbeiten zu können Ich denke, Sie beschreiben ziemlich viel, was das Boxen von Werten erfordert.

Ja, aber meiner Meinung nach sollte so etwas, das Boxen erfordert, unter Beibehaltung der Typsicherheit erlaubt sein, vielleicht mit einer speziellen Syntax, die die "Boxedness" anzeigt. Ein großer Teil des Hinzufügens von "Generika" besteht wirklich darin, die Typunsicherheit von interface{} zu vermeiden, selbst wenn der Overhead von interface{} nicht vermeidbar ist. (Vielleicht nur bestimmte parametrische Typkonstrukte mit Zeiger- und Schnittstellentypen zulassen, die "bereits" geboxt sind? Javas Integer etc. geboxte Objekte sind keine ganz schlechte Idee, obwohl Segmente von Werttypen schwierig sind.)

Ich habe einfach das Gefühl, dass Funktionen zur Kompilierzeit sehr C++-ähnlich sind, und wäre extrem enttäuschend für Leute wie mich, die erwarten würden, dass Go2 ein modernes parametrisches Typsystem hat, das auf einer soliden Typtheorie basiert, und nicht auf einem Hack, der auf der Manipulation von geschriebenen Quellcodestücken basiert in einer Sprache ohne Generika.

@bcmills
Was Sie vorschlagen, wird nicht modular sein. Wenn Modul A Modul B verwendet, das Modul C verwendet, das Modul D verwendet, muss eine Änderung an der Verwendung eines Typparameters in D möglicherweise bis zurück zu A weitergegeben werden, selbst wenn der Implementierer von A keine Ahnung hat, dass D ist im System. Die lose Kopplung von Modulsystemen wird geschwächt, und Software wird spröder. Dies ist eines der Probleme mit C++-Vorlagen.

Wenn andererseits Typsignaturen die Anforderungen an Typparameter erfassen, wie in Sprachen wie CLU, ML, Haskell oder Genus, kann ein Modul ohne Zugriff auf die Interna von Modulen, von denen es abhängt, kompiliert werden.

@Bunsim

Ein großer Teil des Hinzufügens von "Generika" besteht wirklich darin, die Typunsicherheit von interface{} zu vermeiden, selbst wenn der Overhead von interface{} nicht vermeidbar ist.

"nicht vermeidbar" ist relativ. Beachten Sie, dass der Overhead des Boxens Punkt Nr. 3 in Russ' Post von 2009 ist (https://research.swtch.com/generic).

Erwarten Sie, dass Go2 ein modernes parametrisches Typsystem hat, das auf einer soliden Typtheorie basiert, und nicht auf einem Hack, der auf der Manipulation von Quellcodestücken basiert

Eine gute "Lauttypentheorie" ist beschreibend, nicht präskriptiv. Mein Vorschlag bezieht sich insbesondere auf den Lambda-Kalkül zweiter Ordnung (in Anlehnung an System F), wobei gotype für die Art type steht und das gesamte Typensystem erster Ordnung in das zweite hochgezogen wird -order ("compile-time") Typen.

Es steht auch im Zusammenhang mit der modalen Typentheoriearbeit von Davies, Pfenning et al. an der CMU. Als Hintergrundinformationen würde ich mit A Modal Analysis of Staged Computation and Modal Types as Staging Specifications for Run-time Code Generation beginnen.

Es stimmt, dass die meinem Vorschlag zugrunde liegende Typentheorie weniger formal spezifiziert ist als in der akademischen Literatur, aber das bedeutet nicht, dass sie nicht vorhanden ist.

@andrewcmyers

Wenn Modul A Modul B verwendet, das Modul C verwendet, das Modul D verwendet, muss eine Änderung an der Verwendung eines Typparameters in D möglicherweise bis zurück zu A weitergegeben werden, selbst wenn der Implementierer von A keine Ahnung hat, dass D ist im System.

Das gilt bereits heute für Go: Wenn Sie genau hinsehen, werden Sie feststellen, dass die vom Compiler für ein bestimmtes Go-Paket generierten Objektdateien Informationen zu den Teilen der transitiven Abhängigkeiten enthalten, die sich auf die exportierte API auswirken.

Die lose Kopplung von Modulsystemen wird geschwächt, und Software wird spröder.

Ich habe gehört, dass das gleiche Argument verwendet wurde, um für den Export interface -Typen statt konkreter Typen in Go-APIs einzutreten, und es stellt sich heraus, dass das Gegenteil häufiger vorkommt: vorzeitige Abstraktion überfordert die Typen und behindert die Erweiterung von APIs. (Für ein solches Beispiel siehe #19584.) Wenn Sie sich auf diese Argumentationslinie verlassen wollen, müssen Sie meiner Meinung nach einige konkrete Beispiele liefern.

Dies ist eines der Probleme mit C++-Vorlagen.

Aus meiner Sicht sind die Hauptprobleme mit C++-Vorlagen (in keiner bestimmten Reihenfolge):

  • Übermäßige syntaktische Mehrdeutigkeit.
    A. Mehrdeutigkeit zwischen Typnamen und Wertnamen.
    B. Übermäßig breite Unterstützung für das Überladen von Operatoren, was zu einer geschwächten Fähigkeit führt, Einschränkungen aus der Operatornutzung abzuleiten.
  • Übermäßige Abhängigkeit von der Überladungsauflösung für die Metaprogrammierung (oder äquivalent Ad-hoc-Entwicklung der Metaprogrammierungsunterstützung).
    A. Besonders in Bezug auf referenzreduzierende Regeln.
  • Zu breite Anwendung des SFINAE-Prinzips, was zu sehr schwer zu verbreitenden Einschränkungen und viel zu vielen impliziten Bedingungen in Typdefinitionen führt, was zu sehr schwieriger Fehlerberichterstattung führt.
  • Übermäßiger Einsatz von Token-Paste und textuellem Einfügen (der C-Präprozessor) anstelle von AST-Substitution und Kompilierungsartefakten höherer Ordnung (was glücklicherweise zumindest teilweise mit Modulen angegangen zu sein scheint).
  • Mangel an guten Bootstrapping-Sprachen für C++-Compiler, was zu einer schlechten Fehlerberichterstattung in langlebigen Compiler-Linien (z. B. der GCC-Toolchain) führt.
  • Die Verdoppelung (und manchmal Multiplikation) von Namen, die sich aus der Abbildung von Operatorsätzen auf unterschiedlich benannte "Konzepte" ergibt (anstatt die Operatoren selbst als grundlegende Einschränkungen zu behandeln).

Ich programmiere jetzt seit einem Jahrzehnt ab und zu in C++ und bin gerne bereit, die Mängel von C++ ausführlich zu diskutieren, aber die Tatsache, dass Programmabhängigkeiten transitiv sind, stand noch nie im Entferntesten ganz oben auf meiner Beschwerdeliste.

Andererseits müssen Sie eine Kette von O(N)-Abhängigkeiten aktualisieren, nur um eine einzelne Methode zu einem Typ in Modul A hinzuzufügen und in Modul D verwenden zu können? Das ist die Art von Problem, das mich regelmäßig ausbremst. Wo Parametrik und lose Kopplung kollidieren, werde ich jeden Tag Parametrik wählen.

Dennoch bin ich fest davon überzeugt, dass Metaprogrammierung und parametrischer Polymorphismus getrennt werden sollten, und die Verwirrung in C++ ist die Hauptursache dafür, warum C++-Templates lästig sind. Einfach ausgedrückt versucht C++, eine Typtheorie-Idee zu implementieren, indem es im Wesentlichen Makros auf Steroiden verwendet, was sehr problematisch ist, da Programmierer Templates gerne als echten parametrischen Polymorphismus betrachten und von unerwartetem Verhalten getroffen werden. Funktionen zur Kompilierzeit sind eine großartige Idee für die Metaprogrammierung und das Ersetzen des Hacks go generate , aber ich glaube nicht, dass dies die gesegnete Art der generischen Programmierung sein sollte.

"Echter" parametrischer Polymorphismus hilft bei der lockeren Kopplung und sollte nicht damit in Konflikt geraten. Es sollte auch eng mit dem Rest des Typensystems integriert sein; Zum Beispiel sollte es wahrscheinlich in das aktuelle Schnittstellensystem integriert werden, so dass viele Verwendungen von Schnittstellentypen in Dinge umgeschrieben werden könnten wie:

func <T io.Reader> ReadAll(in T)

was Schnittstellen-Overhead vermeiden sollte (wie die Verwendung von Rust), obwohl es in diesem Fall nicht sehr nützlich ist.

Ein besseres Beispiel könnte das Paket sort sein, wo Sie so etwas haben könnten

func <T Comparable> Sort(slice []T)

wobei Comparable einfach eine gute alte Schnittstelle ist, die Typen implementieren können. Sort kann dann für ein Segment von Werttypen aufgerufen werden, die Comparable implementieren, ohne sie in Schnittstellentypen zu packen.

@bcmills Transitive Abhängigkeiten, die nicht durch das Typsystem eingeschränkt werden, sind meiner Ansicht nach der Kern einiger Ihrer Beschwerden über C ++. Transitive Abhängigkeiten sind kein so großes Problem, wenn Sie die Module A, B, C und D steuern. Im Allgemeinen entwickeln Sie Modul A und sind sich möglicherweise nur schwach bewusst, dass Modul D dort unten ist und umgekehrt der Entwickler von D möglicherweise A nicht bewusst ist. Wenn Modul D jetzt, ohne Änderungen an den in D sichtbaren Deklarationen vorzunehmen, beginnt, einen neuen Operator für einen Typparameter zu verwenden – oder diesen Typparameter lediglich als Typargument für ein neues Modul E mit seinem eigenen verwendet implizite Einschränkungen – diese Einschränkungen werden auf alle Clients durchsickern, die möglicherweise keine Typargumente verwenden, die die Einschränkungen erfüllen. Nichts sagt Entwickler D, dass sie es vermasseln. Tatsächlich haben Sie eine Art globalen Typrückschluss mit all den damit verbundenen Schwierigkeiten beim Debuggen.

Ich glaube, dass der Ansatz, den wir bei Genus [ PLDI'15 ] gewählt haben, viel besser ist. Typparameter haben explizite, aber leichtgewichtige Beschränkungen (ich verstehe Ihren Standpunkt bezüglich der Unterstützung von Betriebsbeschränkungen; CLU hat bereits 1977 gezeigt, wie man das richtig macht). Die Gattungstypprüfung ist vollständig modular. Allgemeiner Code kann entweder nur einmal kompiliert werden, um den Coderaum zu optimieren, oder für eine gute Leistung auf bestimmte Typargumente spezialisiert werden.

@andrewcmyers

Wenn Modul D jetzt, ohne Änderungen an den in D sichtbaren Deklarationen vorzunehmen, anfängt, einen neuen Operator für einen Typparameter zu verwenden, […] verwenden [Clients] möglicherweise keine Typargumente, die die Einschränkungen erfüllen. Nichts sagt Entwickler D, dass sie es vermasseln.

Sicher, aber das gilt bereits für viele implizite Einschränkungen in Go, unabhängig von generischen Programmiermechanismen.

Beispielsweise kann eine Funktion einen Parameter vom Typ Schnittstelle erhalten und ihre Methoden zunächst sequentiell aufrufen. Wenn sich diese Funktion später ändert, um diese Methoden gleichzeitig aufzurufen (durch das Spawnen zusätzlicher Goroutinen), wird die Einschränkung "muss für die gleichzeitige Verwendung sicher sein" nicht im Typsystem widergespiegelt.

In ähnlicher Weise legt das heutige Go-Typsystem keine Beschränkungen für die Lebensdauer von Variablen fest: Einige Implementierungen von io.Writer gehen fälschlicherweise davon aus, dass sie eine Referenz auf das übergebene Slice behalten und später daraus lesen können (z. B. indem sie den eigentlichen Schreibvorgang asynchron ausführen). in einer Hintergrund-Goroutine), aber das führt zu Datenrennen, wenn der Aufrufer von Write versucht, denselben Backing-Slice für ein nachfolgendes Write wiederzuverwenden.

Oder eine Funktion, die einen Typschalter verwendet, kann einen anderen Pfad einer Methode nehmen, die einem der Typen im Schalter hinzugefügt wird.

Oder eine Funktion, die nach einem bestimmten Fehlercode sucht, kann abbrechen, wenn die Funktion, die den Fehler generiert, die Art und Weise ändert, wie sie diese Bedingung meldet. (Siehe zum Beispiel https://github.com/golang/go/issues/19647.)

Oder eine Funktion, die nach einem bestimmten Fehlertyp sucht, kann abbrechen, wenn Wrapper um den Fehler hinzugefügt oder entfernt werden (wie im Standardpaket net in Go 1.5).

Oder die Pufferung auf einem in einer API offengelegten Kanal kann sich ändern, wodurch Deadlocks und/oder Races eingeführt werden.

...und so weiter.

Go ist in dieser Hinsicht nicht ungewöhnlich: Implizite Einschränkungen sind in realen Programmen allgegenwärtig.


Wenn Sie versuchen, alle relevanten Einschränkungen in expliziten Anmerkungen zu erfassen, gehen Sie am Ende in eine von zwei Richtungen.

In der einen Richtung bauen Sie ein komplexes, äußerst umfassendes System abhängiger Typen und Anmerkungen auf, und die Anmerkungen rekapitulieren am Ende einen wesentlichen Teil des Codes, den sie kommentieren. Wie Sie hoffentlich deutlich sehen können, entspricht diese Richtung überhaupt nicht dem Design der übrigen Go-Sprache: Go bevorzugt die Einfachheit der Spezifikation und die Prägnanz des Codes gegenüber einer umfassenden statischen Typisierung.

In der anderen Richtung würden die expliziten Anmerkungen nur eine Teilmenge der relevanten Einschränkungen für eine bestimmte API abdecken. Jetzt vermitteln die Anmerkungen ein falsches Sicherheitsgefühl: Der Code kann aufgrund von Änderungen an impliziten Einschränkungen immer noch brechen, aber das Vorhandensein expliziter Einschränkungen verleitet den Entwickler zu der Annahme, dass jede "typsichere" Änderung auch die Kompatibilität aufrechterhält.


Es ist mir nicht klar, warum diese Art von API-Stabilität durch explizite Quellcodeannotation erreicht werden muss: Die Art von API-Stabilität, die Sie beschreiben, kann auch (mit weniger Redundanz im Code) durch Quellcodeanalyse erreicht werden. Beispielsweise könnten Sie sich vorstellen, dass das api -Tool den Code analysiert und einen viel umfangreicheren Satz von Einschränkungen ausgibt, als im formalen Typsystem der Sprache ausgedrückt werden können, und dem guru -Tool die Fähigkeit, den berechneten Satz von Beschränkungen für jede gegebene API-Funktion, Methode oder Parameter abzufragen.

@bcmills Machst du das Perfekte nicht zum Feind des Guten? Ja, es gibt implizite Einschränkungen, die in einem Typsystem schwer zu erfassen sind. (Und ein gutes modulares Design vermeidet die Einführung solcher impliziter Einschränkungen, wenn dies möglich ist.) Es wäre großartig, eine allumfassende Analyse zu haben, die alle Eigenschaften, die Sie überprüfen möchten, statisch überprüfen kann – und Programmierern klare, nicht irreführende Erklärungen darüber liefert, wo sie sich befinden machen Fehler. Auch bei den jüngsten Fortschritten bei der automatischen Fehlerdiagnose und -lokalisierung halte ich nicht die Luft an. Zum einen können Analysetools nur den Code analysieren, den Sie ihnen geben. Entwickler haben nicht immer Zugriff auf den gesamten Code, der möglicherweise mit ihrem verknüpft ist.

Wo es also Einschränkungen gibt, die in einem Typsystem leicht zu erfassen sind, warum geben Sie Programmierern nicht die Möglichkeit, sie aufzuschreiben? Wir haben 40 Jahre Erfahrung in der Programmierung mit statisch eingeschränkten Typparametern. Dies ist eine einfache, intuitive statische Annotation, die sich auszahlt.

Sobald Sie anfangen, größere Software zu entwickeln, die Softwaremodule schichtet, möchten Sie sowieso Kommentare schreiben, die solche impliziten Einschränkungen erklären. Angenommen, es gibt eine gute, überprüfbare Möglichkeit, sie auszudrücken, warum lassen Sie den Compiler dann nicht auf den Witz ein, damit er Ihnen helfen kann?

Ich stelle fest, dass einige Ihrer Beispiele für andere implizite Einschränkungen die Fehlerbehandlung beinhalten. Ich denke, unsere einfache statische Überprüfung von Ausnahmen [ PLDI 2016 ] würde diese Beispiele ansprechen.

@andrewcmyers

Wo es also Einschränkungen gibt, die in einem Typsystem leicht zu erfassen sind, warum geben Sie Programmierern nicht die Möglichkeit, sie aufzuschreiben?
[…]
Sobald Sie anfangen, größere Software zu entwickeln, die Softwaremodule schichtet, möchten Sie sowieso Kommentare schreiben, die solche impliziten Einschränkungen erklären. Angenommen, es gibt eine gute, überprüfbare Möglichkeit, sie auszudrücken, warum lassen Sie den Compiler dann nicht auf den Witz ein, damit er Ihnen helfen kann?

Ich stimme diesem Punkt eigentlich vollkommen zu und verwende oft ein ähnliches Argument in Bezug auf die Speicherverwaltung. (Wenn Sie sowieso Invarianten zum Aliasing und zur Aufbewahrung von Daten dokumentieren müssen, warum erzwingen Sie diese Invarianten nicht zur Kompilierzeit?)

Aber ich würde dieses Argument noch einen Schritt weiterführen: Auch die Umkehrung gilt! Wenn Sie für eine Einschränkung _keinen_ Kommentar schreiben müssen (weil es im Kontext für die Menschen, die mit dem Code arbeiten, offensichtlich ist), warum sollten Sie diesen Kommentar für den Compiler schreiben müssen? Unabhängig von meinen persönlichen Vorlieben weist Gos Verwendung von Garbage-Collection und Nullwerten eindeutig auf eine Tendenz hin, "von Programmierern nicht zu verlangen, offensichtliche Invarianten anzugeben". Es mag der Fall sein, dass die Modellierung im Genus-Stil viele der Einschränkungen ausdrücken kann, die in Kommentaren ausgedrückt würden, aber wie schneidet sie ab, wenn es darum geht, die Einschränkungen zu beseitigen, die auch in Kommentaren aufgehoben würden?

Mir scheint, dass Modelle im Genus-Stil sowieso mehr als nur Kommentare sind: Sie ändern in einigen Fällen tatsächlich die Semantik des Codes, sie schränken ihn nicht nur ein. Jetzt hätten wir zwei verschiedene Mechanismen – Schnittstellen und Typmodelle – zum Parametrisieren von Verhaltensweisen. Das würde eine große Veränderung in der Go-Sprache darstellen: Wir haben im Laufe der Zeit einige Best Practices für Schnittstellen entdeckt (z. B. „Definieren von Schnittstellen auf der Verbraucherseite“), und es ist nicht einmal offensichtlich, dass sich diese Erfahrung auf ein so radikal anderes System übertragen würde Vernachlässigung der Go 1-Kompatibilität.

Darüber hinaus ist eine der herausragenden Eigenschaften von Go, dass seine Spezifikation an einem Nachmittag gelesen (und weitgehend verstanden) werden kann. Es ist für mich nicht offensichtlich, dass ein Beschränkungssystem im Genus-Stil zur Go-Sprache hinzugefügt werden könnte, ohne sie wesentlich zu verkomplizieren – ich wäre gespannt auf einen konkreten Vorschlag für Änderungen an der Spezifikation.

Hier ist ein interessanter Datenpunkt für "Metaprogrammierung". Es wäre schön, wenn bestimmte Typen in den Paketen sync und atomic – nämlich atomic.Value und sync.MapCompareAndSwap -Methoden unterstützen würden, aber diese funktionieren nur für Typen, die zufällig vergleichbar sind. Der Rest der atomic.Value - und sync.Map -APIs bleibt ohne diese Methoden nützlich, also brauchen wir für diesen Anwendungsfall entweder so etwas wie SFINAE (oder andere Arten von bedingt definierten APIs) oder müssen fallen zurück zu einer komplexeren Hierarchie von Typen.

Ich möchte diese kreative Syntaxidee der Verwendung von Silben der Ureinwohner fallen lassen.

@bcmills Können Sie diese drei Punkte näher erläutern?

  1. Mehrdeutigkeit zwischen Typnamen und Wertnamen.
  2. Übermäßig breite Unterstützung für das Überladen von Operatoren
    3.Zu starke Abhängigkeit von der Überladungsauflösung für die Metaprogrammierung

@mahdix Sicher.

  1. Mehrdeutigkeit zwischen Typnamen und Wertnamen.

Dieser Artikel gibt eine gute Einführung. Um ein C++-Programm zu parsen, müssen Sie wissen, welche Namen Typen und welche Werte sind. Wenn Sie ein auf Vorlagen basierendes C++-Programm parsen, stehen Ihnen diese Informationen für Mitglieder der Vorlagenparameter nicht zur Verfügung.

Ein ähnliches Problem tritt in Go für zusammengesetzte Literale auf, aber die Mehrdeutigkeit besteht eher zwischen Werten und Feldnamen als zwischen Werten und Typen. In diesem Go-Code:

const a = someValue
x := T{a: b}

ist a ein wörtlicher Feldname oder wird die Konstante a als Zuordnungsschlüssel oder Array-Index verwendet?

  1. Übermäßig breite Unterstützung für das Überladen von Operatoren

Die argumentabhängige Suche ist ein guter Ausgangspunkt. Überladungen von Operatoren in C++ können als Methoden für den Empfängertyp oder als freie Funktionen in einem von mehreren Namespaces auftreten, und die Regeln zum Auflösen dieser Überladungen sind ziemlich komplex.

Es gibt viele Möglichkeiten, diese Komplexität zu vermeiden, aber die einfachste (wie Go es derzeit tut) besteht darin, das Überladen von Operatoren vollständig zu verbieten.

  1. Übermäßige Abhängigkeit von der Überladungsauflösung für die Metaprogrammierung

Die <type_traits> -Bibliothek ist ein guter Ausgangspunkt. Schauen Sie sich die Implementierung in Ihrer freundlichen Nachbarschaft libc++ an, um zu sehen, wie die Auflösung von Überladungen ins Spiel kommt.

Wenn Go jemals Metaprogrammierung unterstützt (und selbst das ist sehr zweifelhaft), würde ich nicht erwarten, dass es die Auflösung von Überladungen als grundlegende Operation zum Schutz bedingter Definitionen beinhaltet.

@bcmills
Da ich C++ noch nie verwendet habe, könnten Sie etwas Licht ins Dunkel bringen, wo das Überladen von Operatoren durch die Implementierung vordefinierter „Schnittstellen“ in Bezug auf die Komplexität steht. Python und Kotlin sind Beispiele dafür.

Ich denke, dass ADL selbst ein großes Problem mit C++-Vorlagen ist, die größtenteils unerwähnt blieben, da sie den Compiler zwingen, die Auflösung aller Namen bis zur Instanziierung zu verzögern, und zu sehr subtilen Fehlern führen können, teilweise weil das "Ideal" und " faule" Compiler verhalten sich hier anders und der Standard lässt es zu. Die Tatsache, dass es das Überladen von Operatoren unterstützt, ist bei weitem nicht das Schlimmste daran.

Dieser Vorschlag basiert auf Templates, ein System zur Makroerweiterung wäre nicht genug? Ich spreche nicht von go generate oder Projekten wie gotemplate. Mir geht es eher um Folgendes:

macro MacroFoo(stmt ast.Statement) {
    ....
}

Makro könnte die Boilerplate und die Verwendung von Reflektion reduzieren.

Ich denke, dass C++ ein gutes Beispiel dafür ist, dass Generics nicht auf Templates oder Makros basieren sollten. Besonders wenn man bedenkt, dass Go Dinge wie anonyme Funktionen hat, die zur Kompilierzeit wirklich nicht "instanziiert" werden können, außer als Optimierung.

@samadadi Sie können Ihren Standpunkt klar machen, ohne zu sagen, "was mit Ihnen nicht stimmt". Allerdings ist das Argument der Komplexität bereits mehrfach vorgebracht worden.

Go ist nicht die erste Sprache, die versucht, Einfachheit zu erreichen, indem sie die Unterstützung für parametrischen Polymorphismus (Generika) weglässt, obwohl diese Funktion in den letzten 40 Jahren immer wichtiger wurde – meiner Erfahrung nach ist es ein fester Bestandteil von Programmierkursen im zweiten Semester.

Das Problem, wenn das Feature nicht in der Sprache vorhanden ist, besteht darin, dass Programmierer am Ende auf noch schlimmere Problemumgehungen zurückgreifen. Zum Beispiel schreiben Go-Programmierer oft Codevorlagen, die makroerweitert werden, um den "echten" Code für verschiedene gewünschte Typen zu erzeugen. Aber die eigentliche Programmiersprache ist die, die Sie eingeben, nicht die, die der Compiler sieht. Diese Strategie bedeutet also effektiv, dass Sie eine (nicht mehr standardmäßige) Sprache verwenden, die all die Sprödigkeit und den aufgeblähten Code von C++-Vorlagen aufweist.

Wie auf https://blog.golang.org/toward-go2 erwähnt, müssen wir „Erfahrungsberichte“ bereitstellen, damit Bedarf und Gestaltungsziele ermittelt werden können. Könnten Sie sich ein paar Minuten Zeit nehmen und die von Ihnen beobachteten Makrofälle dokumentieren?

Bitte halten Sie diesen Fehler beim Thema und höflich. Und nochmal https://golang.org/wiki/NoMeToo. Bitte kommentieren Sie nur, wenn Sie einzigartige und konstruktive Informationen hinzufügen möchten.

@mandolyte Es ist sehr einfach, im Web detaillierte Erklärungen zu finden, die die Codegenerierung als (teilweisen) Ersatz für Generika befürworten:
https://appliedgo.net/generics/
https://www.calhoun.io/using-code-generation-to-survive-without-generics-in-go/
http://blog.ralch.com/tutorial/golang-code-generation-and-generics/

Offensichtlich gibt es viele Leute da draußen, die diesen Ansatz verfolgen.

@andrewcmyers , es gibt einige Einschränkungen sowie Einschränkungen bei der Verwendung der Codegenerierung ABER .
Im Allgemeinen - wenn Sie glauben, dass dieser Ansatz am besten / gut genug ist, denke ich, dass die Anstrengung, eine etwas ähnliche Generierung aus der Go-Toolkette heraus zu ermöglichen, ein Segen wäre.

  • Die Compiler-Optimierung kann in diesem Fall eine Herausforderung sein, aber die Laufzeit wird konsistent sein, UND die Codewartung, die Benutzererfahrung (Einfachheit...), standardmäßige Best Practices und vereinheitlichte Codestandards können beibehalten werden.
    Darüber hinaus bleibt die gesamte Toolkette gleich, abgesehen von Debugging-Tools (Profiler, Step-Debugger usw.), die Codezeilen sehen, die nicht vom Entwickler geschrieben wurden, aber das ist ein bisschen so, als würde man während des Debuggens in ASM-Code einsteigen - nur Es ist ein lesbarer Code :) .

Nachteil - kein Präzedenzfall (den ich kenne) für diesen Ansatz innerhalb der Go-Tool-Kette.

Um es zusammenzufassen: Betrachten Sie die Codegenerierung als Teil des Build-Prozesses, sie sollte nicht zu kompliziert, ziemlich sicher, laufzeitoptimiert sein, kann Einfachheit und sehr kleine Änderungen in der Sprache beibehalten.

IMHO: Es ist ein leicht zu erreichender Kompromiss zu einem niedrigen Preis.

Um es klar zu sagen, ich halte die Generierung von Code im Makrostil, ob mit gen, cpp, gofmt -r oder anderen Makro-/Vorlagenwerkzeugen, nicht für eine gute Lösung des Generika-Problems, selbst wenn sie standardisiert ist. Es hat die gleichen Probleme wie C++-Templates: aufgeblähter Code, fehlende modulare Typprüfung und Schwierigkeiten beim Debuggen. Es wird schlimmer, wenn Sie beginnen, wie es natürlich ist, generischen Code in Bezug auf anderen generischen Code zu erstellen. Meiner Meinung nach sind die Vorteile begrenzt: Es würde das Leben für die Autoren des Go-Compilers relativ einfach machen und es produziert effizienten Code – es sei denn, es gibt einen Befehls-Cache-Druck, eine häufige Situation in moderner Software!

Ich denke, der Punkt war eher, dass die Codegenerierung als Ersatz verwendet wird
Generika, daher sollten Generika versuchen, die meisten dieser Anwendungsfälle zu lösen.

Am Mittwoch, 26. Juli 2017, 22:41 schrieb Andrew Myers, [email protected] :

Um es klar zu sagen, ich ziehe die Codegenerierung im Makrostil nicht in Betracht, unabhängig davon, ob sie durchgeführt wird
mit gen, cpp, gofmt -r oder anderen Makro-/Template-Tools, um gut zu sein
Lösung des Generika-Problems, auch wenn es standardisiert ist. Es hat das gleiche
Probleme als C++-Templates: aufgeblähter Code, fehlende modulare Typprüfung und
Schwierigkeiten beim Debuggen. Es wird schlimmer, wenn Sie beginnen, wie es natürlich ist, zu bauen
generischer Code in Bezug auf anderen generischen Code. Die Vorteile sind meiner Meinung nach
begrenzt: Es würde das Leben für die Autoren des Go-Compilers relativ einfach halten
und es produziert effizienten Code – es sei denn, es gibt einen Befehls-Cache
Druck, eine häufige Situation in moderner Software!


Sie erhalten dies, weil Sie kommentiert haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/15292#issuecomment-318242016 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AT4HVb2SPMpe5dlEDUQeadIRKPaB74zoks5sR_jSgaJpZM4IG-xv
.

Zweifellos ist die Codegenerierung keine ECHTE Lösung, auch wenn sie mit etwas Sprachunterstützung verpackt ist, um das Erscheinungsbild als "Teil der Sprache" zu gestalten.

Mein Punkt war, dass es sehr kostengünstig war.

Übrigens, wenn Sie sich einige der Ersatzprodukte für die Codegenerierung ansehen, können Sie leicht erkennen, dass sie viel besser lesbar und schneller hätten sein können und einige falsche Konzepte hätten fehlen können (z. B. Iteration über Arrays von Zeigern vs. Werten), wenn die Sprache ihnen bessere Werkzeuge gegeben hätte dafür.

Und vielleicht ist das kurzfristig ein besserer Lösungsweg, der sich nicht wie ein Patch anfühlen würde:
Bevor Sie an die "beste Generika-Unterstützung denken, die auch idiomatisch sein wird" (ich glaube, einige der oben genannten Implementierungen würden Jahre dauern, um eine vollständige Integration zu erreichen), implementieren Sie einige Sätze von "in der Sprache" unterstützten Funktionen, die sowieso benötigt werden (wie eine eingebaute Strukturen Deep Copy) würde diese Codegenerierungslösung viel benutzerfreundlicher machen.

Nachdem ich die Generika-Vorschläge von @bcmills und @ianlancetaylor gelesen habe , habe ich folgende Beobachtungen gemacht:

Funktionen zur Kompilierzeit und erstklassige Typen

Ich mag die Idee der Auswertung zur Kompilierzeit, aber ich sehe keinen Vorteil darin, sie auf reine Funktionen zu beschränken. Dieser Vorschlag führt das eingebaute gotype ein, beschränkt seine Verwendung jedoch auf konstante Funktionen und alle Datentypen, die innerhalb des Funktionsbereichs definiert sind. Aus Sicht eines Bibliotheksbenutzers ist die Instanziierung auf Konstruktorfunktionen wie "New" beschränkt und führt zu Funktionssignaturen wie dieser:

const func New(K, V gotype, hashfn Hashfn(K), eqfn Eqfn(K)) func()*Hashmap(K, V, hashfn, eqfn)

Der Rückgabetyp kann hier nicht in einen Funktionstyp getrennt werden, da wir auf reine Funktionen beschränkt sind. Außerdem definiert die Signatur zwei neue "Typen" in der Signatur selbst (K und V), was bedeutet, dass wir zum Parsen eines einzelnen Parameters die gesamte Parameterliste parsen müssen. Dies ist für einen Compiler in Ordnung, aber ich frage mich, ob die öffentliche API eines Pakets komplexer wird.

Geben Sie Parameter in Go ein

Parametrisierte Typen ermöglichen die meisten Anwendungsfälle der generischen Programmierung, z. B. die Fähigkeit, generische Datenstrukturen und Operationen über verschiedene Datentypen zu definieren. Der Vorschlag listet erschöpfend Verbesserungen des Typprüfers auf, die erforderlich wären, um bessere Kompilierungsfehler, schnellere Kompilierungszeiten und kleinere Binärdateien zu erzeugen.

Unter dem Abschnitt "Type Checker" listet der Vorschlag auch einige nützliche Typbeschränkungen auf, um den Prozess zu beschleunigen, wie "Indexable", "Comparable", "Callable", "Composite" usw. Was ich nicht verstehe, ist Warum nicht dem Benutzer erlauben, seine eigenen Typbeschränkungen anzugeben? Das sagt der Vorschlag

Es gibt keine Einschränkungen, wie parametrisierte Typen in einer parametrisierten Funktion verwendet werden können.

Wenn jedoch an die Bezeichner mehr Einschränkungen gebunden wären, würde das nicht den Effekt haben, den Compiler zu unterstützen? Erwägen:

HashMap[Anything,Anything] // Compiler must always compare the implementation and usages to make sure this is valid.

vs

HashMap[Comparable,Anything] // Compiler can first filter out instantiations for incomparable types before running an exhaustive check.

Das Trennen von Typbeschränkungen von Typparametern und das Zulassen benutzerdefinierter Beschränkungen könnte auch die Lesbarkeit verbessern und generische Pakete leichter verständlich machen. Interessanterweise könnten die am Ende des Vorschlags aufgeführten Mängel bezüglich der Komplexität von Typableitungsregeln tatsächlich gemildert werden, wenn diese Regeln explizit vom Benutzer definiert werden.

@smasher164

Ich mag die Idee der Auswertung zur Kompilierzeit, aber ich sehe keinen Vorteil darin, sie auf reine Funktionen zu beschränken.

Der Vorteil ist, dass es eine getrennte Kompilierung ermöglicht. Wenn eine Funktion zur Kompilierzeit den globalen Status ändern kann, muss der Compiler diesen Status entweder verfügbar haben oder die Änderungen so protokollieren, dass der Linker sie zur Linkzeit sequenzieren kann. Wenn eine Funktion zur Kompilierzeit den lokalen Status ändern kann, benötigen wir eine Möglichkeit, um nachzuverfolgen, welcher Status lokal oder global ist. Beides erhöht die Komplexität, und es ist nicht offensichtlich, dass beides genügend Vorteile bieten würde, um dies auszugleichen.

@smasher164

Was ich nicht verstehe, ist, warum dem Benutzer nicht erlaubt wird, seine eigenen Typbeschränkungen anzugeben?

Die Typbeschränkungen in diesem Vorschlag entsprechen Operationen in der Syntax der Sprache. Dadurch wird die Oberfläche der neuen Funktionen reduziert: Es ist nicht erforderlich, zusätzliche Syntax für einschränkende Typen anzugeben, da alle syntaktischen Einschränkungen aus der Verwendung abgeleitet werden können.

Wenn an die Bezeichner mehr Einschränkungen gebunden wären, würde das nicht den Effekt haben, den Compiler zu unterstützen?

Die Sprache sollte für ihre Benutzer entworfen werden, nicht für die Compiler-Schreiber.

Es besteht keine Notwendigkeit, zusätzliche Syntax zum Einschränken von Typen anzugeben, da alle syntaktischen Einschränkungen aus der Verwendung abgeleitet werden können.

Dies ist der Weg, den C++ gegangen ist. Es bedarf einer globalen Programmanalyse, um die relevanten Nutzungen zu identifizieren. Code kann von Programmierern nicht modular begründet werden, und Fehlermeldungen sind ausführlich und unverständlich.

Es kann so einfach und leicht sein, die erforderlichen Operationen anzugeben. Siehe CLU (1977) für ein Beispiel.

@andrewcmyers

Es bedarf einer globalen Programmanalyse, um die relevanten Nutzungen zu identifizieren. Code kann von Programmierern nicht modular begründet werden,

Das verwendet eine bestimmte Definition von "modular", die meiner Meinung nach nicht so universell ist, wie Sie anzunehmen scheinen. Gemäß dem Vorschlag von 2013 hätte jede Funktion oder jeder Typ einen eindeutigen Satz von Einschränkungen, die von unten nach oben aus importierten Paketen abgeleitet werden, genau so, wie die Laufzeit (und Laufzeiteinschränkungen) von nichtparametrischen Funktionen von unten abgeleitet werden. heute aus Anrufketten auf.

Sie könnten vermutlich die abgeleiteten Einschränkungen mit guru oder einem ähnlichen Tool abfragen, und es könnte diese Abfragen mit lokalen Informationen aus den exportierten Paketmetadaten beantworten.

und Fehlermeldungen sind ausführlich und unverständlich.

Wir haben einige Beispiele (GCC und MSVC), die zeigen, dass naiv generierte Fehlermeldungen unverständlich sind. Ich denke, es ist weit hergeholt anzunehmen, dass Fehlermeldungen für implizite Einschränkungen an sich schlecht sind.

Ich denke, der größte Nachteil von abgeleiteten Einschränkungen ist, dass sie es einfach machen, einen Typ so zu verwenden, dass eine Einschränkung eingeführt wird, ohne sie vollständig zu verstehen. Im besten Fall bedeutet dies nur, dass Ihre Benutzer auf unerwartete Kompilierzeitfehler stoßen können, aber im schlimmsten Fall bedeutet dies, dass Sie das Paket für Verbraucher beschädigen können, indem Sie versehentlich eine neue Einschränkung einführen. Explizit spezifizierte Constraints würden dies vermeiden.

Ich persönlich bin auch nicht der Meinung, dass explizite Beschränkungen nicht mit dem bestehenden Go-Ansatz übereinstimmen, da Schnittstellen explizite Beschränkungen des Laufzeittyps sind, obwohl sie eine begrenzte Ausdruckskraft haben.

Wir haben einige Beispiele (GCC und MSVC), die zeigen, dass naiv generierte Fehlermeldungen unverständlich sind. Ich denke, es ist weit hergeholt anzunehmen, dass Fehlermeldungen für implizite Einschränkungen an sich schlecht sind.

Die Liste der Compiler, bei denen nicht-lokaler Typrückschluss - was Sie vorschlagen - zu schlechten Fehlermeldungen führt, ist um einiges länger. Es umfasst SML, OCaml und GHC, wo bereits viel Mühe in die Verbesserung ihrer Fehlermeldungen gesteckt wurde und wo zumindest eine explizite Modulstruktur hilft. Sie können es vielleicht besser machen, und wenn Sie mit dem von Ihnen vorgeschlagenen Schema einen Algorithmus für gute Fehlermeldungen finden, haben Sie eine schöne Veröffentlichung. Als Ausgangspunkt für diesen Algorithmus finden Sie möglicherweise unsere POPL 2014- und PLDI 2015-Papiere zur Fehlerlokalisierung nützlich. Sie sind mehr oder weniger Stand der Technik.

weil alle syntaktischen Beschränkungen aus der Verwendung gefolgert werden können.

Schränkt das nicht die Breite typüberprüfbarer generischer Programme ein? Beachten Sie beispielsweise, dass der Type-Params-Vorschlag keine „Iterable“-Einschränkung angibt. In der aktuellen Sprache würde dies entweder einem Slice oder einem Kanal entsprechen, aber ein zusammengesetzter Typ (z. B. eine verknüpfte Liste) würde diese Anforderungen nicht unbedingt erfüllen. Definieren einer Schnittstelle wie

type Iterable[T] interface {
    Next() T
}

hilft dem Fall der verknüpften Liste, aber jetzt müssen die eingebauten Slice- und Kanaltypen erweitert werden, um diese Schnittstelle zu erfüllen.

Eine Einschränkung, die besagt: „Ich akzeptiere die Menge aller Typen, die entweder Iterables, Slices oder Channels sind“, scheint eine Win-Win-Win-Situation für den Benutzer, den Paketautor und den Compiler-Implementierer zu sein. Der Punkt, den ich zu machen versuche, ist, dass Einschränkungen eine Obermenge von syntaktisch gültigen Programmen sind, und einige möglicherweise aus sprachlicher Sicht keinen Sinn ergeben, sondern nur aus API-Perspektive.

Die Sprache sollte für ihre Benutzer entworfen werden, nicht für die Compiler-Schreiber.

Ich stimme zu, aber vielleicht hätte ich es anders formulieren sollen. Eine verbesserte Compilereffizienz könnte ein Nebeneffekt von benutzerdefinierten Einschränkungen sein. Der Hauptvorteil wäre die Lesbarkeit, da der Benutzer ohnehin eine bessere Vorstellung von seinem API-Verhalten hat als der Compiler. Der Kompromiss hier ist, dass generische Programme etwas expliziter darüber sein müssten, was sie akzeptieren.

Was wäre wenn statt

type Iterable[T] interface {
    Next() T
}

Wir haben die Idee von "Schnittstellen" von "Einschränkungen" getrennt. Dann haben wir vielleicht

type T generic

type Iterable class {
    Next() T
}

wobei "Klasse" eine Typklasse im Haskell-Stil bedeutet, keine Klasse im Java-Stil.

"Typklassen" getrennt von "Schnittstellen" zu haben, könnte helfen, einige der Nicht-Orthogonalität der beiden Ideen aufzuklären. Dann könnte Sortable (sort.Interface ignorieren) etwa so aussehen:

type T generic

type Comparable class {
    Less(a, b T) bool
}

type Sortable class {
    Next() Comparable
}

Hier ist ein Feedback zum Abschnitt „Typklassen und -konzepte“ in Genus von @andrewcmyers und seiner Anwendbarkeit auf Go.

Dieser Abschnitt befasst sich mit den Einschränkungen von Typklassen und -konzepten und gibt an

Erstens muss die Einschränkungserfüllung eindeutig bezeugt werden

Ich bin mir nicht sicher, ob ich diese Einschränkung verstehe. Würde das Binden einer Einschränkung an separate Bezeichner nicht verhindern, dass sie für einen bestimmten Typ eindeutig ist? Es scheint mir, dass die "where" -Klausel in Genus im Wesentlichen einen Typ/eine Einschränkung aus einer gegebenen Einschränkung konstruiert, aber dies scheint analog zum Instanziieren einer Variablen von einem gegebenen Typ zu sein. Eine Einschränkung auf diese Weise ähnelt einer Art .

Hier ist eine dramatische Vereinfachung der Constraint-Definitionen, angepasst an Go:

kind Any interface{} // accepts any type that satisfies interface{}.
type T Any // Declare a type of Any kind. Also binds it to an identifier.
kind Eq T == T // accepts any type for which equality is defined.

Eine Kartendeklaration würde also folgendermaßen aussehen:

type Map[K Eq, V Any] struct {
}

wo in Genus könnte es so aussehen:

type Map[K, V] where Eq[K], Any[V] struct {
}

und im bestehenden Type-Params-Vorschlag würde es so aussehen:

type Map[K,V] struct {
}

Ich denke, wir sind uns alle einig, dass das Zulassen von Beschränkungen zur Nutzung des bestehenden Typsystems sowohl Überschneidungen zwischen Funktionen der Sprache beseitigen als auch das Verständnis neuer erleichtern kann.

und zweitens definieren ihre Modelle, wie ein einzelner Typ angepasst wird, während in einer Sprache mit Subtypisierung jeder angepasste Typ im Allgemeinen alle seine Subtypen darstellt.

Diese Einschränkung scheint für Go weniger relevant zu sein, da die Sprache bereits über gute Konvertierungsregeln zwischen benannten/unbenannten Typen und überlappenden Schnittstellen verfügt.

Die angegebenen Beispiele schlagen Modelle als Lösung vor, was ein nützliches, aber nicht notwendiges Feature für Go zu sein scheint. Wenn eine Bibliothek beispielsweise erwartet, dass ein Typ http.Handler implementiert, und der Benutzer je nach Kontext unterschiedliche Verhaltensweisen wünscht, ist das Schreiben von Adaptern einfach:

type handleFunc func(http.ResponseWriter, *http.Request)
func (f handlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { f(w,r) }

Tatsächlich ist dies das, was die Standardbibliothek tut.

@smasher164

Erstens muss die Einschränkungserfüllung eindeutig bezeugt werden
Ich bin mir nicht sicher, ob ich diese Einschränkung verstehe. Würde das Binden einer Einschränkung an separate Bezeichner nicht verhindern, dass es für einen bestimmten Typ eindeutig ist?

Die Idee ist, dass Sie in Genus dieselbe Einschränkung mit demselben Typ auf mehr als eine Weise erfüllen können, anders als in Haskell. Wenn Sie beispielsweise ein HashSet[T] haben, können Sie HashSet[String] auf die übliche Weise in Hash-Strings schreiben, aber HashSet[String with CaseInsens] in Hash- und Vergleichsstrings mit CaseInsens -Modell, das Zeichenfolgen vermutlich ohne Berücksichtigung der Groß-/Kleinschreibung behandelt. Die Gattung unterscheidet tatsächlich diese beiden Typen; Dies könnte für Go übertrieben sein. Selbst wenn das Typsystem dies nicht verfolgt, scheint es dennoch wichtig zu sein, die von einem Typ bereitgestellten Standardoperationen überschreiben zu können.

kind Any interface{} // akzeptiert jeden Typ, der interface{} erfüllt.
type T Any // Einen beliebigen Typ deklarieren. Bindet es auch an einen Bezeichner.
kind Eq T == T // akzeptiert jeden Typ, für den Gleichheit definiert ist.
type Map[K Eq, V Any] struct { ...
}

Das moralische Äquivalent dazu in Genus wäre:

constraint Any[T] {}
// Just use Any as if it were a type
constraint Eq[K] {
   boolean equals(K);
}
class Map[K, V] where Eq[K] { ... }

In Familia würden wir einfach schreiben:

interface Eq {
    boolean equals(This);
}
class Map[K where Eq, V] { ... }

Bearbeiten: Dies zugunsten einer auf Reflexion basierenden Lösung zurückziehen, wie in # 4146 beschrieben. Eine auf Generika basierende Lösung, wie ich sie unten beschreibe, wächst linear in der Anzahl der Kompositionen. Während eine auf Reflexion basierende Lösung immer ein Leistungshandicap haben wird, kann sie sich zur Laufzeit selbst optimieren, so dass das Handicap unabhängig von der Anzahl der Kompositionen konstant ist.

Dies ist kein Vorschlag, sondern ein potenzieller Anwendungsfall, der beim Entwerfen eines Vorschlags berücksichtigt werden sollte.

Zwei Dinge sind heute im Go-Code üblich

  • Wrapping eines Schnittstellenwerts, um zusätzliche Funktionalität bereitzustellen (Wrapping eines http.ResponseWriter für ein Framework)
  • optionale Methoden haben, die manchmal Schnittstellenwerte haben (wie Temporary() bool auf net.Error )

Diese sind sowohl gut als auch nützlich, aber sie mischen sich nicht. Sobald Sie eine Schnittstelle umschlossen haben, haben Sie die Möglichkeit verloren, auf alle Methoden zuzugreifen, die nicht für den Umbruchtyp definiert sind. Das heißt, gegeben

type MyError struct {
  error
  extraContext extraContextType
}
func (m MyError) Error() string {
  return fmt.Sprintf("%s: %s", m.extraContext, m.error)
}

Wenn Sie einen Fehler in diese Struktur einschließen, verbergen Sie alle zusätzlichen Methoden für den ursprünglichen Fehler.

Wenn Sie den Fehler nicht in die Struktur einschließen, können Sie den zusätzlichen Kontext nicht bereitstellen.

Nehmen wir an, dass Sie mit dem akzeptierten generischen Vorschlag etwas wie das Folgende definieren können (willkürliche Syntax, die ich absichtlich hässlich zu machen versuchte, damit sich niemand darauf konzentriert).

type MyError generic_over[E which_is_a_type_satisfying error] struct {
  E
  extraContext extraContextType
}
func (m MyError) Error() string {
  return fmt.Sprintf("%s: %s", m.extraContext, m.E)
}

Durch die Nutzung der Einbettung könnten wir jeden konkreten Typ einbetten, der die Fehlerschnittstelle erfüllt, und ihn sowohl umschließen als auch Zugriff auf seine anderen Methoden haben. Leider bringt uns das nur einen Teil des Weges dorthin.

Was wir hier wirklich brauchen, ist, einen beliebigen Wert der Fehlerschnittstelle zu nehmen und seinen dynamischen Typ einzubetten.

Dies wirft sofort zwei Bedenken auf

  • der Typ müsste zur Laufzeit erstellt werden (wahrscheinlich ohnehin von Reflect benötigt)
  • Die Typerstellung müsste in Panik geraten, wenn der Fehlerwert null ist

Wenn diese Sie nicht auf den Gedanken gebracht haben, brauchen Sie auch einen Mechanismus, um über die Schnittstelle zu ihrem dynamischen Typ zu "springen", entweder durch eine Anmerkung in der Liste der generischen Parameter, um zu sagen: "Immer den dynamischen Typ von Schnittstellenwerten instanziieren " oder durch eine magische Funktion, die nur während der Typinstanziierung aufgerufen werden kann, um die Schnittstelle zu entpacken, damit ihr Typ und Wert korrekt eingefügt werden können.

Ohne das instanziieren Sie nur MyError für den Fehlertyp selbst, nicht für den dynamischen Typ der Schnittstelle.

Nehmen wir an, wir haben eine magische unbox -Funktion, um die Informationen herauszuziehen und (irgendwie) anzuwenden:

func wrap(ec extraContext, err error) error {
  if err == nil {
    return nil
  }
  return MyError{
    E: unbox(err),
    extraContext: ec,
  }
}

Nehmen wir nun an, dass wir einen Nicht-Null-Fehler haben, err , dessen dynamischer Typ *net.DNSError ist. Dann das

wrapped := wrap(getExtraContext(), err)
//wrapped 's dynamic type is a MyStruct embedding E=*net.DNSError
_, ok := wrapped.(net.Error)
fmt.Println(ok)

würde true drucken. Aber wenn der dynamische Typ von err *os.PathError gewesen wäre, hätte er falsch ausgegeben.

Ich hoffe, die vorgeschlagene Semantik ist angesichts der stumpfen Syntax, die in der Demonstration verwendet wird, klar.

Ich hoffe auch, dass es einen besseren Weg gibt, dieses Problem mit weniger Mechanismus und Zeremonie zu lösen, aber ich denke, dass das oben Genannte funktionieren könnte.

@jimmyfrasche Wenn ich verstehe, was Sie wollen, ist es ein Wrapper-freier Anpassungsmechanismus. Sie möchten in der Lage sein, den Satz von Operationen zu erweitern, die ein Typ bietet, ohne ihn in ein anderes Objekt einzuhüllen, das das Original verbirgt. Dies ist eine Funktionalität, die Genus anbietet.

@andrewcmyers nein.

Struct's in Go ermöglichen das Einbetten. Wenn Sie ein Feld ohne Namen, aber mit einem Typ zu einer Struktur hinzufügen, bewirkt dies zwei Dinge: Es erstellt ein Feld mit demselben Namen wie der Typ und ermöglicht die transparente Weiterleitung an alle Methoden dieses Typs. Das klingt schrecklich nach Vererbung, ist es aber nicht. Wenn Sie einen Typ T hatten, der eine Methode Foo() hatte, dann sind die folgenden äquivalent

type S struct {
  T
}

und

type S struct {
  T T
}
func (s S) Foo() {
  s.T.Foo()
}

(Wenn Foo aufgerufen wird, ist sein "this" immer vom Typ T).

Sie können Schnittstellen auch in Strukturen einbetten. Dies gibt der Struktur alle Methoden im Vertrag der Schnittstelle (obwohl Sie dem impliziten Feld einen dynamischen Wert zuweisen müssen oder es eine Panik mit dem Äquivalent einer Nullzeiger-Ausnahme verursacht)

Go verfügt über Schnittstellen, die einen Vertrag in Bezug auf die Methoden eines Typs definieren. Ein Wert eines beliebigen Typs, der den Vertrag erfüllt, kann in einen Wert dieser Schnittstelle geboxt werden. Ein Wert einer Schnittstelle ist ein Zeiger auf das interne Typmanifest (dynamischer Typ) und ein Zeiger auf einen Wert dieses dynamischen Typs (dynamischer Wert). Sie können Assertionen für einen Schnittstellenwert eingeben, um (a) den dynamischen Wert abzurufen, wenn Sie dessen Nicht-Schnittstellentyp bestätigen, oder (b) einen neuen Schnittstellenwert abzurufen, wenn Sie gegenüber einer anderen Schnittstelle bestätigen, dass der dynamische Wert ebenfalls erfüllt wird. Es ist üblich, letzteres zu verwenden, um ein Objekt zu testen, um zu sehen, ob es optionale Methoden unterstützt. Um ein früheres Beispiel wiederzuverwenden, haben einige Fehler eine "Temporary() bool"-Methode, damit Sie sehen können, ob ein Fehler temporär ist, mit:

func isTemp(err error) bool {
  if t, ok := err.(interface{ Temporary() bool}); ok {
    return t.Temporary()
  }
  return false
}

Es ist auch üblich, einen Typ in einen anderen Typ einzuschließen, um zusätzliche Funktionen bereitzustellen. Dies funktioniert gut mit Nicht-Schnittstellentypen. Wenn Sie eine Schnittstelle umschließen, verstecken Sie jedoch auch die Methoden, die Sie nicht kennen, und Sie können sie nicht mit Assertionen vom Typ "Feature-Test" wiederherstellen: Der umschlossene Typ macht nur die erforderlichen Methoden der Schnittstelle verfügbar, selbst wenn er optionale Methoden hat . Erwägen:

type A struct {}
func (A) Foo()
func (A) Bar()

type I interface {
  Foo()
}

type B struct {
  I
}

var i I = B{A{}}

Sie können Bar nicht auf i aufrufen oder sogar wissen, dass es existiert, es sei denn, Sie wissen, dass der dynamische Typ von i ein B ist, also können Sie es auspacken und in das I-Feld gelangen, um darauf eine Bestätigung einzugeben .

Dies verursacht echte Probleme, insbesondere im Umgang mit gängigen Schnittstellen wie Error oder Reader.

Wenn es eine Möglichkeit gäbe, den dynamischen Typ und Wert aus einer Schnittstelle zu entfernen (auf eine sichere, kontrollierte Weise), könnten Sie damit einen neuen Typ parametrisieren, das eingebettete Feld auf den Wert setzen und eine neue Schnittstelle zurückgeben. Dann erhalten Sie einen Wert, der die ursprüngliche Schnittstelle erfüllt, alle erweiterten Funktionen enthält, die Sie hinzufügen möchten, aber die restlichen Methoden des ursprünglichen dynamischen Typs sind immer noch vorhanden, um auf ihre Funktionen getestet zu werden.

@jimmyfrasche In der Tat. Genus erlaubt Ihnen, einen Typ zu verwenden, um einen "Schnittstellen"-Vertrag zu erfüllen, ohne ihn einzuschränken. Der Wert hat immer noch seinen ursprünglichen Typ und seine ursprünglichen Operationen. Außerdem kann das Programm angeben, welche Operationen der Typ verwenden soll, um den Vertrag zu erfüllen – standardmäßig sind dies die Operationen, die der Typ bereitstellt, aber das Programm kann neue bereitstellen, wenn der Typ nicht über die erforderlichen Operationen verfügt. Es kann auch die Operationen ersetzen, die der Typ verwenden würde.

@jimmyfrasche @andrewcmyers Für diesen Anwendungsfall siehe auch https://github.com/golang/go/issues/4146#issuecomment -318200547.

@jimmyfrasche Für mich klingt es so, als ob das Hauptproblem hier darin besteht, den dynamischen Typ/Wert einer Variablen zu erhalten. Abgesehen von der Einbettung wäre ein vereinfachtes Beispiel

type MyError generic_over[E which_is_a_type_satisfying error] struct {
  e E
  extraContext extraContextType
}
func (m MyError) Error() string {
  return fmt.Sprintf("%s: %s", m.extraContext, m.e)
}

Der e zugewiesene Wert muss einen dynamischen (oder konkreten) Typ wie etwa *net.DNSError haben, der error implementiert. Hier sind ein paar Möglichkeiten, wie eine zukünftige Sprachänderung dieses Problem lösen könnte:

  1. Haben Sie eine magische unbox -ähnliche Funktion, die den dynamischen Wert einer Variablen aufdeckt. Dies gilt für jeden Typ, der nicht konkret ist, zum Beispiel Unions.
  2. Wenn die Sprachänderung Typvariablen unterstützt, stellen Sie eine Möglichkeit bereit, um den dynamischen Typ der Variablen abzurufen. Mit Typinformationen können wir die Funktion unbox selbst schreiben. Beispielsweise,
func unbox(v T1) T2 {
    t := dynTypeOf(v)
    return v.(t)
}

wrap kann genauso geschrieben werden wie zuvor oder als

func wrap(ec extraContext, err error) error {
  if err == nil {
    return nil
  }
  t := dynTypeOf(err)
  return MyError{
    e: v.(t),
    extraContext: ec,
  }
}
  1. Wenn die Sprachänderung Typbeschränkungen unterstützt, hier eine alternative Idee:
type E1 which_is_a_type_satisfying error
type E2 which_is_a_type_satisfying error

func wrap(ec extraContext, err E1) E2 {
  if err == nil {
    return nil
  }
  return MyError{
    e: err,
    extraContext: ec,
  }
}

In diesem Beispiel akzeptieren wir einen Wert eines beliebigen Typs, der Fehler implementiert. Jeder Benutzer von wrap , der error erwartet, erhält einen. Der Typ von e innerhalb MyError ist jedoch derselbe wie der von err , der übergeben wird, was nicht auf einen Schnittstellentyp beschränkt ist. Wenn man das gleiche Verhalten wie 2 wollte,

var iface error = ...
wrap(getExtraContext(), unbox(iface))

Da es anscheinend sonst niemand gemacht hat, möchte ich auf die sehr offensichtlichen "Erfahrungsberichte" für Generika hinweisen, wie sie von https://blog.golang.org/toward-go2 gefordert werden.

Der erste ist der eingebaute Typ map :

m := make(map[string]string)

Der nächste ist der eingebaute Typ chan :

c := make(chan bool)

Schließlich ist die Standardbibliothek mit interface{} Alternativen gespickt, bei denen Generika sicherer funktionieren würden:

  • heap.Interface (https://golang.org/pkg/container/heap/#Interface)
  • list.Element (https://golang.org/pkg/container/list/#Element)
  • ring.Ring (https://golang.org/pkg/container/ring/#Ring)
  • sync.Pool (https://golang.org/pkg/sync/#Pool)
  • bevorstehende sync.Map (https://tip.golang.org/pkg/sync/#Map)
  • atomic.Value (https://golang.org/pkg/sync/atomic/#Value)

Vielleicht fehlen mir noch andere. Der Punkt ist, dass ich bei jedem der oben genannten Bereiche davon ausgehen würde, dass Generika nützlich sind.

(Hinweis: Ich füge hier sort.Sort nicht hinzu, weil es ein hervorragendes Beispiel dafür ist, wie Schnittstellen anstelle von Generika verwendet werden können.)

http://www.yinwang.org/blog-cn/2014/04/18/golang
Ich denke, das Generikum ist wichtig. Andernfalls kann es nicht mit ähnlichen Typen umgehen. Manchmal kann die Schnittstelle das Problem nicht lösen.

Einfache Syntax und Typsystem sind die wichtigen Vorteile von Go. Wenn Sie Generika hinzufügen, wird die Sprache zu einem hässlichen Durcheinander wie Scala oder Haskell. Außerdem wird dieses Feature pseudoakademische Fanboys anziehen, die schließlich die Community-Werte von "Let's get this done" in "Lasst uns über CS-Theorie und Mathematik reden" verwandeln werden. Vermeiden Sie Generika, es ist ein Weg in den Abgrund.

@bxqgit bitte bleib höflich. Es ist nicht nötig, jemanden zu beleidigen.

Was die Zukunft bringen wird, werden wir sehen, aber ich weiß, dass ich zwar 98 % meiner Zeit keine Generika brauche, aber wann immer ich sie brauche, ich wünschte, ich könnte sie verwenden. Wie sie verwendet werden und wie sie falsch verwendet werden, ist eine andere Diskussion. Die Schulung der Benutzer sollte Teil des Prozesses sein.

@bxqgit
Es gibt Situationen, in denen Generika benötigt werden, wie generische Datenstrukturen (Trees, Stacks, Queues, ...) oder generische Funktionen (Map, Filter, Reduce, ...) und diese Dinge sind unvermeidlich, indem Schnittstellen anstelle von Generika verwendet werden Diese Situationen erhöhen nur die Komplexität sowohl für den Codeschreiber als auch für den Codeleser und wirken sich auch negativ auf die Codeeffizienz zur Laufzeit aus. Daher sollte es viel rationaler sein, Sprachgenerika hinzuzufügen, als zu versuchen, Schnittstellen zu verwenden und zu reflektieren, um komplex zu schreiben und ineffizienter Code.

@bxqgit Das Hinzufügen von Generika erhöht nicht unbedingt die Komplexität der Sprache, dies kann auch mit einfacher Syntax erreicht werden. Mit Generika fügen Sie eine Typbeschränkung für die variable Kompilierzeit hinzu, die bei Datenstrukturen sehr nützlich ist, wie @riwogo sagte.

Das aktuelle Schnittstellensystem in go ist sehr nützlich, ist aber sehr schlecht, wenn Sie beispielsweise eine allgemeine Implementierung von Listen benötigen, die bei Schnittstellen eine Ausführungszeit-Typbeschränkung benötigen. Wenn Sie jedoch Generika hinzufügen, kann der generische Typ ersetzt werden Kompilierzeit mit dem tatsächlichen Typ, wodurch die Einschränkung unnötig wird.

Denken Sie auch daran, dass die Leute dahinter die Sprache entwickeln, indem Sie das verwenden, was Sie "CS-Theorie und Mathematik" nennen, und auch die Leute sind, die "dies erledigen".

Denken Sie auch daran, dass die Leute dahinter die Sprache entwickeln, indem Sie das verwenden, was Sie "CS-Theorie und Mathematik" nennen, und auch die Leute sind, die "dies erledigen".

Persönlich sehe ich nicht viel CS-Theorie und Mathematik im Go-Sprachdesign. Es ist eine ziemlich primitive Sprache, was meiner Meinung nach gut ist. Auch die Leute, von denen Sie sprechen, haben beschlossen, Generika zu vermeiden, und haben die Dinge erledigt. Wenn es gut funktioniert, warum etwas ändern? Im Allgemeinen denke ich, dass es eine schlechte Praxis ist, die Syntax einer Sprache ständig weiterzuentwickeln und zu erweitern. Es erhöht nur die Komplexität, was zu einem Chaos von Haskell und Scala führt.

Die Vorlage ist kompliziert, aber Generics ist einfach

Sehen Sie sich die Funktionen SortInts, SortFloats, SortStrings im Sort-Paket an. Oder SearchInts, SearchFloats, SearchStrings. Oder die Methoden Len, Less und Swap von byName im Paket io/ioutil. Reines Boilerplate-Kopieren.

Die Kopier- und Anhängefunktionen existieren, weil sie Slices viel nützlicher machen. Generika würden bedeuten, dass diese Funktionen unnötig sind. Generics würden es ermöglichen, ähnliche Funktionen für Karten und Kanäle zu schreiben, ganz zu schweigen von benutzerdefinierten Datentypen. Zugegeben, Slices sind der wichtigste zusammengesetzte Datentyp, und deshalb wurden diese Funktionen benötigt, aber andere Datentypen sind immer noch nützlich.

Meine Stimme ist Nein zu generalisierten Anwendungsgenerika, Ja zu mehr integrierten generischen Funktionen wie append und copy , die auf mehreren Basistypen funktionieren. Vielleicht könnten sort und search für die Sammlungstypen hinzugefügt werden?

Für meine Anwendungen fehlt als einziger Typ ein ungeordneter Satz (https://github.com/golang/go/issues/7088). Ich möchte dies als integrierten Typ, damit er die generische Typisierung wie slice erhält map . Legen Sie die Arbeit in den Compiler (Benchmarking für jeden Basistyp und einen ausgewählten Satz von struct -Typen und anschließendes Optimieren für die beste Leistung) und halten Sie zusätzliche Anmerkungen aus dem Anwendungscode heraus.

smap eingebaut statt sync.Map auch bitte. Aus meiner Erfahrung ist die Verwendung interface{} für die Typsicherheit zur Laufzeit ein Designfehler. Die Typüberprüfung zur Kompilierzeit ist ein Hauptgrund, Go überhaupt zu verwenden.

@pciet

Aus meiner Erfahrung ist die Verwendung von Interface{} für Laufzeittypsicherheit ein Designfehler.

Können Sie einfach einen kleinen (typsicheren) Wrapper schreiben?
https://play.golang.org/p/tG6hd-j5yx

@pierrre Dieser Wrapper ist besser als ein reflect.TypeOf(item).AssignableTo(type) Scheck. Aber das Schreiben Ihres eigenen Typs mit map + sync.Mutex oder sync.RWMutex ist ohne die Typzusicherung, die sync.Map erfordert, genauso komplex.

Meine synchronisierte Kartenverwendung war für globale Karten von Mutexe mit einem var myMapLock = sync.RWMutex{} daneben, anstatt einen Typ zu erstellen. Das könnte sauberer sein. Ein generischer eingebauter Typ klingt für mich richtig, erfordert aber Arbeit, die ich nicht erledigen kann, und ich bevorzuge meinen Ansatz, anstatt den Typ zu bestätigen.

Ich vermute, dass die negative innere Reaktion auf Generika, die viele Go-Programmierer zu haben scheinen, darauf zurückzuführen ist, dass sie Generika hauptsächlich über C++-Vorlagen ausgesetzt waren. Das ist bedauerlich, weil C++ Generics vom ersten Tag an auf tragische Weise falsch verstanden hat und seitdem den Fehler verstärkt. Generics for Go könnten viel einfacher und weniger fehleranfällig sein.

Es wäre enttäuschend zu sehen, wie Go immer komplexer wird , indem eingebaute parametrisierte Typen hinzugefügt werden. Es wäre besser, nur die Sprachunterstützung hinzuzufügen, damit Programmierer ihre eigenen parametrisierten Typen schreiben können. Dann könnten die speziellen Typen einfach als Bibliotheken bereitgestellt werden, anstatt die Kernsprache zu überladen.

@andrewcmyers "Generika für Go könnten viel einfacher und weniger fehleranfällig sein." --- wie Generika in C#.

Es ist enttäuschend zu sehen, dass Go immer komplexer wird, indem eingebaute parametrisierte Typen hinzugefügt werden.

Trotz der Spekulationen in dieser Ausgabe halte ich dies für äußerst unwahrscheinlich.

Der Exponent des Komplexitätsmaßes parametrisierter Typen ist die Varianz.
Die Typen von Go (mit Ausnahme von Schnittstellen) sind unveränderlich, und das kann und sollte es auch sein
Regel gehalten.

Eine mechanische, Compiler-unterstützte "type copy-paster"-Generika-Implementierung
würde 99 % des Problems auf eine Art und Weise lösen, die Gos Basiswert entspricht
Prinzipien der Oberflächlichkeit und Nichtüberraschung.

Übrigens wurden diese und Dutzende anderer brauchbarer Ideen diskutiert
davor und einige mündeten sogar in gute, praktikable Ansätze. Bei diesem
Punkt, ich bin grenzwertig mit Stanniolhüten darüber, wie sie alle verschwunden sind
lautlos ins Leere.

Am 28. November 2017 um 23:54 Uhr schrieb „Andrew Myers“ [email protected] :

Ich vermute, dass die negative viszerale Reaktion auf Generika, die viele gehen
Programmierer scheinen entstanden zu sein, weil sie hauptsächlich Generika ausgesetzt waren
über C++-Templates. Das ist bedauerlich, weil C++ auf tragische Weise Generika bekommen hat
vom ersten Tag an falsch und hat den Fehler seitdem verstärkt. Generika für
Go könnte viel einfacher und weniger fehleranfällig sein.

Es ist enttäuschend zu sehen, wie Go durch Hinzufügen immer komplexer wird
eingebaute parametrisierte Typen. Es wäre besser, nur die Sprache hinzuzufügen
Unterstützung für Programmierer, ihre eigenen parametrisierten Typen zu schreiben. Dann ist die
spezielle Typen könnten einfach als Bibliotheken bereitgestellt werden, anstatt sie zu überladen
die Kernsprache.


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/15292#issuecomment-347691444 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AJZ_jPsQd2qBbn9NI1wZeT-O2JpyraTMks5s7I81gaJpZM4IG-xv
.

Ja, Sie können Generika ohne Vorlagen haben. Vorlagen sind eine Form von fortgeschrittenem parametrischem Polymorphismus, hauptsächlich für Metaprogrammierungseinrichtungen.

@ianlancetaylor Rust ermöglicht es einem Programm, eine Eigenschaft T auf einem vorhandenen Typ Q zu implementieren, vorausgesetzt, dass ihre Kiste entweder T oder Q definiert.

Nur ein Gedanke: Ich frage mich, ob Simon Peyton Jones (ja, Haskell-Ruhm) und/oder die Rust-Entwickler vielleicht helfen können. Rust und Haskell haben wahrscheinlich die beiden fortschrittlichsten Typsysteme aller Produktionssprachen, und Go sollte von ihnen lernen.

Da ist auch Phillip Wadler , der an Generic Java arbeitete, was schließlich zu der Generics-Implementierung führte, die Java heute hat.

@tarcieri Ich denke nicht, dass die Generika von Java sehr gut sind, aber sie sind kampferprobt.

@DemiMarie Glücklicherweise hat Andrew Myers hier mitgeholfen.

Aufgrund meiner persönlichen Erfahrung denke ich, dass Menschen, die sich gut mit verschiedenen Sprachen und verschiedenen Schriftsystemen auskennen, bei der Prüfung von Ideen sehr hilfreich sein können. Aber um die Ideen überhaupt zu produzieren, brauchen wir Leute, die mit Go sehr vertraut sind, wie es heute funktioniert und wie es in Zukunft vernünftig funktionieren kann. Go ist unter anderem als einfache Sprache konzipiert. Das Importieren von Ideen aus Sprachen wie Haskell oder Rust, die wesentlich komplizierter sind als Go, wird wahrscheinlich nicht gut passen. Und im Allgemeinen sind Ideen von Leuten, die nicht bereits eine angemessene Menge an Go-Code geschrieben haben, wahrscheinlich nicht gut geeignet; nicht, dass die Ideen als solche schlecht wären, nur, dass sie nicht gut zum Rest der Sprache passen würden.

Es ist beispielsweise wichtig zu verstehen, dass Go bereits teilweise Unterstützung für generische Programmierung mit Schnittstellentypen und bereits (fast) vollständige Unterstützung mit dem Reflect-Paket bietet. Während diese beiden Ansätze zur generischen Programmierung aus verschiedenen Gründen unbefriedigend sind, muss jeder Vorschlag für Generika in Go gut mit ihnen interagieren und gleichzeitig ihre Mängel ansprechen.

Tatsächlich habe ich, während ich hier bin, vor einiger Zeit über generische Programmierung mit Schnittstellen nachgedacht und drei Gründe gefunden, warum sie nicht zufriedenstellend ist.

  1. Schnittstellen erfordern, dass alle Operationen als Methoden ausgedrückt werden. Das macht es mühsam, eine Schnittstelle für eingebaute Typen wie Kanaltypen zu schreiben. Alle Kanaltypen unterstützen den <- -Operator für Sende- und Empfangsoperationen, und es ist einfach genug, eine Schnittstelle mit den Methoden Send und Receive zu schreiben, aber um einen Kanalwert zuzuweisen Für diesen Schnittstellentyp müssen Sie die Methoden Boilerplate Send und Receive schreiben. Diese Boilerplate-Methoden sehen für jeden Kanaltyp genau gleich aus, was mühsam ist.

  2. Schnittstellen werden dynamisch typisiert, sodass Fehler, die verschiedene statisch typisierte Werte kombinieren, nur zur Laufzeit abgefangen werden, nicht zur Kompilierzeit. Beispielsweise erfordert eine Merge -Funktion, die zwei Kanäle mit ihren Methoden Send und Receive zu einem einzigen Kanal zusammenführt, dass die beiden Kanäle Elemente desselben Typs haben müssen, aber das Die Überprüfung kann nur zur Laufzeit durchgeführt werden.

  3. Schnittstellen sind immer eingerahmt. Beispielsweise gibt es keine Möglichkeit, Schnittstellen zu verwenden, um ein Paar anderer Typen zu aggregieren, ohne diese anderen Typen in Schnittstellenwerte einzufügen, was zusätzliche Speicherzuweisungen und Zeigerverfolgung erfordert.

Auf Generika-Vorschläge für Go freue ich mich über Kibitz. Vielleicht auch von Interesse ist die in letzter Zeit zunehmende Forschung zu Generika bei Cornell, die anscheinend relevant für das ist, was mit Go gemacht werden könnte:

http://www.cs.cornell.edu/andru/papers/familia/ (Zhang & Myers, OOPSLA'17)
http://io.livecode.ch/learn/namin/unsound (Amin & Tate, OOPSLA'16)
http://www.cs.cornell.edu/projects/genus/ (Zhang et al., PLDI '15)
https://www.cs.cornell.edu/~ross/publications/shapes/shapes-pldi14.pdf (Greenman, Muehlboeck & Tate, PLDI '14)

Beim Benchmarking von Map vs. Slice für einen ungeordneten Set-Typ habe ich separate Unit-Tests für jeden geschrieben, aber mit Interface-Typen kann ich diese beiden Testlisten zu einer kombinieren:

type Item interface {
    Equal(Item) bool
}

type Set interface {
    Add(Item) Set
    Remove(Item) Set
    Combine(...Set) Set
    Reduce() Set
    Has(Item) bool
    Equal(Set) bool
    Diff(Set) Set
}

Entfernen eines Elements testen:

type RemoveCase struct {
    Set
    Item
    Out Set
}

func TestRemove(t *testing.T) {
    for i, c := range RemoveCases {
        if c.Out.Equal(c.Set.Remove(c.Item)) == false {
            t.Fatalf("%v failed", i)
        }
    }
}

Auf diese Weise kann ich meine zuvor getrennten Fälle problemlos zu einem Teil von Fällen zusammenfügen:

var RemoveCases = []RemoveCase{
    {
        Set: MapPathSet{
            &Path{{0, 0}}:         {},
            &Path{{0, 1}, {1, 1}}: {},
        },
        Item: Path{{0, 0}},
        Out: MapPathSet{
            &Path{{0, 1}, {1, 1}}: {},
        },
    },
    {
        Set: SlicePathSet{
            {{0, 0}},
            {{0, 1}, {1, 1}},
        },
        Item: Path{{0, 0}},
        Out: SlicePathSet{
            {{0, 1}, {1, 1}},
        },
    },
}

Für jeden konkreten Typ musste ich die Schnittstellenmethoden definieren. Beispielsweise:

func (the MapPathSet) Remove(an Item) Set {
    return MapDelete(the, an.(Path))
}
func (the SlicePathSet) Remove(an Item) Set {
    return SliceDelete(the, an.(Path))
}

Diese generischen Tests könnten eine vorgeschlagene Typprüfung zur Kompilierzeit verwenden:

type Item generic {
    Equal(Item) bool
}
func (the SlicePathSet) Remove(an Item) Set {
    return SliceDelete(the, an)
}

Quelle: https://github.com/pciet/pathsetbenchmark

Wenn man darüber nachdenkt, scheint es nicht so, als wäre eine Typüberprüfung zur Kompilierzeit für einen solchen Test möglich, da Sie das Programm ausführen müssten, um zu wissen, ob ein Typ an die entsprechende Schnittstellenmethode übergeben wird.

Was ist also mit einem "generischen" Typ, der eine Schnittstelle ist und bei der der Compiler bei konkreter Verwendung eine unsichtbare Typzusicherung hinzugefügt hat?

@andrewcmyers Das "Familia"-Papier war interessant (und weit über meinem Kopf). Ein Schlüsselbegriff war die Vererbung. Wie würden sich die Konzepte für eine Sprache wie Go ändern, die auf Komposition statt Vererbung setzt?

Danke. Der Vererbungsteil gilt nicht für Go – wenn Sie nur an Generika für Go interessiert sind, können Sie nach Abschnitt 4 des Papiers aufhören zu lesen. Das Wichtigste an diesem Artikel, der für Go relevant ist, ist, dass er zeigt, wie Schnittstellen verwendet werden, sowohl in der Art und Weise, wie sie jetzt für Go verwendet werden, als auch als Einschränkungen für Typen für generische Abstraktionen. Das bedeutet, dass Sie die Leistungsfähigkeit von Haskell-Klassen erhalten, ohne der Sprache ein völlig neues Konstrukt hinzuzufügen.

@andrewcmyers Kannst du ein Beispiel geben, wie das in Go aussehen würde?

Das Wichtigste an diesem Artikel, der für Go relevant ist, ist, dass er zeigt, wie Schnittstellen verwendet werden, sowohl in der Art und Weise, wie sie jetzt für Go verwendet werden, als auch als Einschränkungen für Typen für generische Abstraktionen.

Mein Verständnis ist, dass die Go-Schnittstelle eine Einschränkung für einen Typ definiert (z. B. "dieser Typ kann mit der 'type Comparable interface' auf Gleichheit verglichen werden, weil er eine Eq-Methode erfüllt"). Ich bin mir nicht sicher, ob ich verstehe, was Sie mit einer Typbeschränkung meinen.

Ich bin mit Haskell nicht vertraut, aber wenn ich einen kurzen Überblick lese, vermute ich, dass Typen, die zu einer Go-Schnittstelle passen, in diese Typklasse passen würden. Können Sie erklären, was bei Klassen vom Typ Haskell anders ist?

Ein konkreter Vergleich zwischen Familia und Go wäre interessant. Danke, dass Sie Ihr Papier geteilt haben.

Go-Schnittstellen können so angesehen werden, dass sie eine Einschränkung für Typen durch strukturelle Subtypisierung beschreiben. Diese Typbeschränkung ist jedoch so wie sie ist nicht aussagekräftig genug, um die Beschränkungen zu erfassen, die Sie für die generische Programmierung wünschen. Beispielsweise können Sie die Typbeschränkung mit dem Namen Eq im Familia-Papier nicht ausdrücken.

Einige Gedanken zur Motivation für allgemeinere Programmiermöglichkeiten in Go:

Es gibt also meine generische Testliste, bei der der Sprache eigentlich nichts hinzugefügt werden muss. Meiner Meinung nach erfüllt der von mir vorgeschlagene generische Typ nicht das Go-Ziel des einfachen Verständnisses, er hat nicht viel mit dem allgemein akzeptierten Programmierbegriff zu tun, und die Typzusicherung dort zu machen, war nicht hässlich, da eine Panik bei Fehlern ist fein. Ich bin mit den generischen Programmiermöglichkeiten von Go bereits für meine Bedürfnisse zufrieden.

Aber sync.Map ist ein anderer Anwendungsfall. In der Standardbibliothek besteht ein Bedarf für eine ausgereifte generische synchronisierte Zuordnungsimplementierung, die über eine Struktur mit einer Zuordnung und einem Mutex hinausgeht. Für die Typbehandlung können wir ihn mit einem anderen Typ umschließen, der einen Nicht-Interface{}-Typ festlegt und eine Typzusicherung durchführt, oder wir können intern eine Reflexionsprüfung hinzufügen, sodass Elemente, die dem ersten folgen, demselben Typ entsprechen müssen. Beide haben Laufzeitprüfungen, das Wrapping erfordert das Neuschreiben jeder Methode für jeden Verwendungstyp, aber es fügt eine Kompilierzeit-Typprüfung für die Eingabe hinzu und verbirgt die Ausgabetyp-Assertion, und mit der internen Prüfung müssen wir trotzdem eine Ausgabetyp-Assertion durchführen. In beiden Fällen führen wir Schnittstellenkonvertierungen durch, ohne dass Schnittstellen tatsächlich verwendet werden. interface{} ist ein Hack der Sprache und wird neuen Go-Programmierern nicht klar sein. Obwohl json.Marshal meiner Meinung nach ein gutes Design ist (einschließlich der hässlichen, aber sinnvollen Struktur-Tags).

Da sich sync.Map in der Standardbibliothek befindet, möchte ich hinzufügen, dass die Implementierung idealerweise für die gemessenen Anwendungsfälle ausgetauscht werden sollte, in denen die einfache Struktur leistungsfähiger ist. Die nicht synchronisierte Karte ist eine häufige frühe Falle bei der gleichzeitigen Go-Programmierung, und eine Standardbibliothekskorrektur sollte einfach funktionieren.

Die reguläre Karte hat nur eine Typüberprüfung zur Kompilierzeit und benötigt keines dieser Gerüste. Ich argumentiere, dass sync.Map die gleiche sein sollte oder nicht in der Standardbibliothek für Go 2 sein sollte.

Ich schlug vor, sync.Map zur Liste der integrierten Typen hinzuzufügen und dasselbe für zukünftige ähnliche Anforderungen zu tun. Aber mein Verständnis ist, Go-Programmierern eine Möglichkeit zu geben, dies zu tun, ohne am Compiler arbeiten und den Open-Source-Akzeptanzhandschuh durchlaufen zu müssen, ist die Idee hinter dieser Diskussion. Meiner Ansicht nach ist das Fixieren von sync.Map ein echter Fall, der teilweise definiert, was dieser Generika-Vorschlag sein sollte.

Wenn Sie sync.Map als integrierte Funktion hinzufügen, wie weit gehen Sie dann? Haben Sie einen Sonderfall für jeden Container?
sync.Map ist nicht der einzige Container und einige sind für einige Fälle besser als andere.

@Azareal : @chowey listete diese im August auf:

Schließlich ist die Standardbibliothek gespickt mit Interface{}-Alternativen, bei denen Generika sicherer funktionieren würden:

• heap.Interface (https://golang.org/pkg/container/heap/#Interface)
• list.Element (https://golang.org/pkg/container/list/#Element)
• ring.Ring (https://golang.org/pkg/container/ring/#Ring)
• sync.Pool (https://golang.org/pkg/sync/#Pool)
• kommende sync.Map (https://tip.golang.org/pkg/sync/#Map)
• atomar.Wert (https://golang.org/pkg/sync/atomic/#Value)

Vielleicht fehlen mir noch andere. Der Punkt ist, dass ich bei jedem der oben genannten Bereiche davon ausgehen würde, dass Generika nützlich sind.

Und ich möchte die ungeordnete Menge für Typen, die auf Gleichheit verglichen werden können.

Ich möchte viel Arbeit in eine variable Implementierung in der Laufzeit für jeden Typ stecken, der auf Benchmarking basiert, damit normalerweise die bestmögliche Implementierung verwendet wird.

Ich frage mich, ob es vernünftige alternative Implementierungen mit Go 1 gibt, die das gleiche Ziel für diese Standardbibliothekstypen ohne Schnittstelle {} und ohne Generika erreichen.

Golang-Schnittstellen und Klassen vom Typ Haskell überwinden zwei Dinge (die sehr großartig sind!):

1.) (Type Constraint) Sie gruppieren verschiedene Typen mit einem Tag, dem Schnittstellennamen
2.) (Dispatch) Sie bieten an, für jeden Typ für einen bestimmten Satz von Funktionen über die Schnittstellenimplementierung unterschiedlich zu versenden

Aber,

1.) Manchmal möchten Sie nur anonyme Gruppen wie eine Gruppe von int, float64 und string. Wie sollten Sie eine solche Schnittstelle NumericandString nennen?

2.) Sehr oft möchte man nicht für jeden Interface-Typ unterschiedlich dispatchieren, sondern nur eine Methode für alle aufgelisteten Interface-Typen bereitstellen (evtl. mit Default-Methoden von Interfaces möglich)

3.) Sehr oft möchte man nicht alle möglichen Typen für eine Gruppe aufzählen. Stattdessen gehen Sie den faulen Weg und sagen, ich möchte, dass alle Typen T ein Interface A implementieren, und der Compiler sucht dann nach allen Typen in allen Quelldateien, die Sie bearbeiten, und in allen Bibliotheken, die Sie verwenden, um die entsprechenden Funktionen zur Kompilierzeit zu generieren.

Obwohl der letzte Punkt in go über Schnittstellenpolymorphismus möglich ist, hat er den Nachteil, dass es sich um einen Laufzeitpolymorphismus handelt, der Umwandlungen beinhaltet, und wie Sie die Parametereingabe einer Funktion so einschränken, dass sie Typen enthält, die mehr als eine Schnittstelle oder eine von vielen Schnittstellen implementieren. Der Weg ist, neue Schnittstellen einzuführen, die andere Schnittstellen erweitern (durch Verschachtelung von Schnittstellen), um etwas Ähnliches zu erreichen, aber nicht mit Best Practice.

Übrigens.
Ich gebe diejenigen zu, die sagen, dass go bereits Polymorphismus hat und genau deshalb ist go keine einfache Sprache mehr wie C. Es ist eine Systemprogrammiersprache auf hohem Niveau. Warum also nicht die Polymorphismus-Angebote erweitern?

Hier ist eine Bibliothek, die ich heute für generische ungeordnete Set-Typen gestartet habe: https://github.com/pciet/unordered

Dies enthält Dokumentations- und Testbeispiele, die das Typ-Wrapper-Muster (danke @pierrre) für Typsicherheit zur Kompilierungszeit und auch die Reflexionsprüfung für Typsicherheit zur Laufzeit enthalten.

Welche Bedürfnisse gibt es für Generika? Meine negative Einstellung gegenüber generischen Typen von Standardbibliotheken konzentrierte sich früher auf die Verwendung von interface{}; Meine Beschwerde könnte mit einem paketspezifischen Typ für interface{} (wie type Item interface{} in pciet/unordered) gelöst werden, der die beabsichtigten nicht ausdrückbaren Einschränkungen dokumentiert.

Ich sehe keine Notwendigkeit für eine zusätzliche Sprachfunktion, wenn uns jetzt nur die Dokumentation dorthin bringen könnte. Es gibt bereits große Mengen an kampferprobtem Code in der Standardbibliothek, die generische Funktionen bietet (siehe https://github.com/golang/go/issues/23077).

Ihr Codetyp wird zur Laufzeit überprüft (und aus dieser Perspektive ist es in keiner Weise besser als nur interface{} , wenn nicht sogar schlechter). Mit Generika hätten Sie die Sammlungstypen mit Typprüfungen zur Kompilierzeit haben können.

@zerkms -Laufzeitprüfungen können durch Setzen von asserting = false (das würde nicht in die Standardbibliothek gehen) abgeschaltet werden, es gibt ein Verwendungsmuster für Prüfungen zur Kompilierzeit, und eine Typprüfung betrachtet ohnehin nur die Schnittstellenstruktur (using Schnittstelle verursacht mehr Aufwand als die Typprüfung). Wenn die Schnittstelle nicht funktioniert, müssen Sie Ihren eigenen Typ schreiben.

Sie sagen, dass generischer Code mit maximaler Leistung ein Schlüsselbedürfnis ist. Es war nicht für meine Anwendungsfälle, aber vielleicht könnte die Standardbibliothek schneller werden, und vielleicht brauchen andere so etwas.

Laufzeitprüfungen können durch Setzen von asserting = false abgeschaltet werden

dann garantiert nichts die Richtigkeit

Sie sagen, dass generischer Code mit maximaler Leistung ein Schlüsselbedürfnis ist.

Das habe ich nicht gesagt. Typensicherheit wäre sehr viel. Ihre Lösung ist immer noch interface{} -infiziert.

aber vielleicht könnte die Standardbibliothek schneller werden, und vielleicht brauchen andere so etwas.

kann sein, wenn das Kernentwicklungsteam bereit ist, alles, was ich brauche, bei Bedarf und schnell zu implementieren.

@pciet

Ich sehe keine Notwendigkeit für eine zusätzliche Sprachfunktion, wenn uns jetzt nur die Dokumentation dorthin bringen könnte.

Sie sagen das, haben aber kein Problem damit, die generischen Sprachfeatures in Form von Slices und der Make-Funktion zu verwenden.

Ich sehe keine Notwendigkeit für eine zusätzliche Sprachfunktion, wenn uns jetzt nur die Dokumentation dorthin bringen könnte.

Warum sollte man sich dann die Mühe machen, eine statisch typisierte Sprache zu verwenden? Sie können eine dynamisch typisierte Sprache wie Python verwenden und sich auf die Dokumentation verlassen, um sicherzustellen, dass die richtigen Datentypen an Ihre API gesendet werden.

Ich denke, einer der Vorteile von Go ist die Möglichkeit, einige Einschränkungen durch den Compiler durchzusetzen, um zukünftige Fehler zu verhindern. Diese Einrichtungen können (mit Generika-Unterstützung) erweitert werden, um einige andere Einschränkungen durchzusetzen, um weitere Fehler in der Zukunft zu verhindern.

Sie sagen das, haben aber kein Problem damit, die generischen Sprachfeatures in Form von Slices und der Make-Funktion zu verwenden.

Ich sage, die bestehenden Funktionen bringen uns zu einem ausgewogenen Punkt, der generische Programmierlösungen hat, und es sollte starke, echte Gründe geben, vom System des Typs Go 1 zu wechseln. Nicht, wie eine Änderung die Sprache verbessern würde, sondern mit welchen Problemen die Leute jetzt konfrontiert sind, wie z.

Warum sollte man sich dann die Mühe machen, eine statisch typisierte Sprache zu verwenden? Sie können eine dynamisch typisierte Sprache wie Python verwenden und sich auf die Dokumentation verlassen, um sicherzustellen, dass die richtigen Datentypen an Ihre API gesendet werden.

Ich habe Vorschläge gehört, Systeme in Python zu schreiben, anstatt statisch typisierte Sprachen und Organisationen zu verwenden.

Die meisten Go-Programmierer, die die Standardbibliothek verwenden, verwenden Typen, die ohne Dokumentation oder ohne Blick auf die Implementierung nicht vollständig beschrieben werden können. Typen mit parametrischen Untertypen oder allgemeine Typen mit angewendeten Einschränkungen beheben nur eine Teilmenge dieser Fälle programmgesteuert und würden viel Arbeit erzeugen, die bereits in der Standardbibliothek erledigt ist.

Im Vorschlag für Summentypen habe ich eine Build-Funktion für den Schnittstellentyp-Schalter vorgeschlagen, bei der eine Schnittstellenverwendung in einer Funktion oder Methode einen Build-Fehler ausgibt, wenn ein möglicher Wert, der der Schnittstelle zugewiesen ist, nicht mit einem enthaltenen Schnittstellentyp-Schalter übereinstimmt.

Eine Funktion/Methode, die eine Schnittstelle verwendet, könnte einige Typen beim Erstellen ablehnen, da sie keinen Standardfall und keinen Fall für den Typ hat. Dies scheint eine vernünftige generische Programmierergänzung zu sein, wenn die Funktion realisierbar ist.

Wenn Go-Schnittstellen den Typ des Implementierers erfassen könnten, könnte es eine Form von Generika geben, die vollständig mit der aktuellen Go-Syntax kompatibel ist – eine Form von Generika mit einem einzigen Parameter ( Demonstration ).

@dc0d für generische Containertypen Ich glaube, dass diese Funktion eine Typüberprüfung zur Kompilierzeit hinzufügt, ohne dass ein Wrappertyp erforderlich ist: https://gist.github.com/pciet/36a9dcbe99f6fb71f5fc2d3c455971e5

@pciet Du hast recht. Im bereitgestellten Code, Nr. 4, Beispiel heißt es, dass der Typ für Slices und Kanäle (und Arrays) erfasst wird. Aber nicht für Karten, denn es gibt nur einen und nur einen Typparameter: den Implementierer. Und da eine Karte zwei Typparameter benötigt, werden Wrapper-Schnittstellen benötigt.

Übrigens muss ich den Demonstrationszweck dieses Codes als Gedankengang betonen. Ich bin kein Sprachdesigner. Dies ist nur eine hypothetische Denkweise über die Implementierung von Generika in Go:

  • Kompatibel mit aktuellen Go
  • Einfach (einzelner generischer Typparameter, der sich wie _dieser_ in anderen OO anfühlt und sich auf den aktuellen Implementierer bezieht)

Die Erörterung der Generizität und aller möglichen Anwendungsfälle im Zusammenhang mit dem Wunsch, Auswirkungen zu minimieren und gleichzeitig wichtige Anwendungsfälle und Ausdrucksflexibilität zu maximieren, ist eine sehr komplexe Analyse. Ich bin mir nicht sicher, ob einer von uns in der Lage sein wird, es auf eine kurze Reihe von Prinzipien, auch bekannt als generative Essenz, zu reduzieren. Ich versuche. Wie dem auch sei, hier einige meiner ersten Gedanken aus meiner _flüchtigen_ Durchsicht dieses Threads …

@adg schrieb:

Zu dieser Ausgabe gehört ein allgemeiner Generika-Vorschlag von @ianlancetaylor , der vier spezifische fehlerhafte Vorschläge für generische Programmiermechanismen für Go enthält.

Afaics, der verlinkte Abschnitt, der wie folgt ausgezogen ist, gibt keinen Fall von fehlender Generizität bei aktuellen Schnittstellen an: _„Es gibt keine Möglichkeit, eine Methode zu schreiben, die eine Schnittstelle für den vom Aufrufer bereitgestellten Typ T für jedes T akzeptiert und einen Wert von zurückgibt derselbe Typ T.“_.

Es gibt keine Möglichkeit, eine Schnittstelle mit einer Methode zu schreiben, die ein Argument vom Typ T für ein beliebiges T akzeptiert und einen Wert desselben Typs zurückgibt.

Wie sonst könnte der Code beim Aufruf-Site-Typ überprüfen, ob er einen Typ T als Ergebniswert hat? Beispielsweise kann die besagte Schnittstelle eine Factory-Methode zum Erstellen von Typ T haben. Aus diesem Grund müssen wir Schnittstellen auf Typ T parametrisieren.

Schnittstellen sind nicht einfach Typen; sie sind auch Werte. Es gibt keine Möglichkeit, Schnittstellentypen ohne Schnittstellenwerte zu verwenden, und Schnittstellenwerte sind nicht immer effizient.

Einverstanden, dass der Typ T für den Programmierer nicht zugänglich ist, da Schnittstellen derzeit nicht explizit auf den Typ T parametrisiert werden können, auf dem sie arbeiten.

Das also tun Typklassengrenzen auf der Funktionsdefinitionsseite, die einen Typ T als Eingabe nehmen und eine where - oder requires -Klausel haben, die die Schnittstelle(n) angibt, die für Typ T erforderlich sind. In vielen Fällen Diese Schnittstellenwörterbücher können zur Kompilierzeit automatisch monomorphisiert werden, sodass zur Laufzeit keine Wörterbuchzeiger (für die Schnittstellen) an die Funktion übergeben werden (Monomorphisierung, von der ich annehme, dass der Go-Compiler derzeit auf Schnittstellen anwendet?). Ich nehme an, dass er mit „Werten“ im obigen Zitat den Eingabetyp T meint und nicht das Wörterbuch der Methoden für den vom Typ T implementierten Schnittstellentyp.

Wenn wir dann Typparameter für Datentypen zulassen (z. B. struct ), dann kann der oben genannte Typ T selbst parametrisiert werden, sodass wir wirklich einen Typ T<U> haben. Fabriken für solche Typen, die das Wissen über U behalten müssen, werden höherwertige Typen (HKT) genannt.

Generics erlauben typsichere polymorphe Container.

Vgl. auch das weiter unten diskutierte Problem der _heterogenen_ Container. Mit polymorph meinen wir also die Generizität des Werttyps des Containers (z. B. Elementtyp der Sammlung), aber es stellt sich auch die Frage, ob wir mehr als einen Werttyp gleichzeitig in den Container einfügen können, um sie heterogen zu machen.


@tamird schrieb:

Diese Anforderungen scheinen z. B. ein dem Trait-System von Rust ähnliches System auszuschließen, bei dem generische Typen durch Trait-Grenzen eingeschränkt werden.

Die Eigenschaftsgrenzen von Rust sind im Wesentlichen Typklassengrenzen.

@alex schrieb:

Rusts Eigenschaften. Obwohl ich denke, dass sie im Allgemeinen ein gutes Modell sind, würden sie schlecht zu Go passen, wie es heute existiert.

Warum denkst du, dass sie schlecht passen? Vielleicht denken Sie an die Trait-Objekte, die Runtime-Dispatch verwenden, also weniger leistungsfähig sind als Monomorphismus? Aber diese können getrennt vom Typklassengrenzen-Generitätsprinzip betrachtet werden (vgl. meine Diskussion über heterogene Container/Sammlungen weiter unten). Die Schnittstellen von Afaics, Go sind bereits merkmalsähnliche Grenzen und erreichen das Ziel von Typklassen, die Wörterbücher zu spät an die Datentypen an der Aufrufstelle zu binden, und nicht das Antimuster von OOP, das früh bindet (selbst wenn es noch beim Kompilieren ist). Zeit) Wörterbücher zu Datentypen (bei Instanziierung/Konstruktion). Typklassen können (zumindest eine teilweise Verbesserung der Freiheitsgrade) das Ausdrucksproblem lösen, was OOP nicht kann.

@jimmyfrasche schrieb:

  • https://golang.org/doc/faq#covariant_types

Ich stimme dem obigen Link zu, dass Typklassen tatsächlich keine Untertypisierung sind und keine Vererbungsbeziehung ausdrücken. Und stimmen Sie zu, „Generizität“ (als ein allgemeineres Konzept der Wiederverwendung oder Modularität als parametrischer Polymorphismus) nicht unnötigerweise mit Vererbung zu verschmelzen, wie dies bei Unterklassen der Fall ist.

Allerdings möchte ich auch darauf hinweisen, dass Vererbungshierarchien (auch Subtyping genannt) bei der Zuweisung an (Funktionseingaben) und von (Funktionsausgaben) unvermeidlich sind, wenn die Sprache Vereinigungen und Schnittmengen unterstützt, weil zum Beispiel ein int ν string kann eine Zuweisung von einem int oder string annehmen, aber keiner kann eine Zuweisung von einem int ν string annehmen. Ohne Vereinigungen sind die einzigen alternativen Möglichkeiten, statisch typisierte heterogene Container/Sammlungen bereitzustellen, Unterklassen oder existenziell begrenzter Polymorphismus (auch bekannt als Trait-Objekte in Rust und existenzielle Quantifizierung in Haskell). Die obigen Links enthalten Diskussionen über die Kompromisse zwischen Existentials und Gewerkschaften. Afaik, die einzige Möglichkeit, heterogene Container/Sammlungen in Go zu erstellen, besteht jetzt darin, alle Typen in ein leeres interface{} zu subsumieren, wodurch die Typisierungsinformationen weggeworfen werden und ich annehme, dass Umwandlungen und Laufzeit-Typprüfung erforderlich sind, welche Art von 2 besiegt den Punkt der statischen Typisierung.

Das zu vermeidende „Anti-Muster“ ist die Unterklassenbildung, auch bekannt als virtuelle Vererbung (vgl. auch „EDIT#2“ zu den Problemen mit impliziter Subsumtion und Gleichheit usw.).

1 Unabhängig davon, ob sie strukturell oder nominell übereinstimmen, da die Subtypisierung aufgrund des Liskov-Substitutionsprinzips auf der Grundlage von Vergleichsmengen und der Zuweisungsrichtung mit Funktionseingängen entgegengesetzt zu Rückgabewerten erfolgt, z. B. ein Typparameter von struct oder interface kann nicht sowohl in den Funktionseingaben als auch in den Rückgabewerten vorhanden sein, es sei denn, es ist invariant anstelle von Co- oder Contra-Variante.

2 Absolutismus gilt nicht, weil wir das Universum des unbegrenzten Nichtdeterminismus nicht typisieren können. So wie ich es verstehe, geht es in diesem Thread darum, eine optimale ("Sweet Spot") Grenze für die Ebene der Angabe der Typisierung in Bezug auf die Probleme der Generizität zu wählen.

@andrewcmyers schrieb:

Im Gegensatz zu Java- und C#-Generika basiert der Genus-Generika-Mechanismus nicht auf Subtypisierung.

Es ist die Vererbung und Unterklassenbildung ( nicht die strukturelle Untertypisierung ), die das schlechteste Anti-Muster ist, das Sie nicht von Java, Scala, Ceylon und C++ kopieren möchten (unabhängig von den Problemen mit C++-Vorlagen ).

@thwd schrieb:

Der Exponent des Komplexitätsmaßes parametrisierter Typen ist die Varianz. Die Typen von Go (mit Ausnahme von Schnittstellen) sind unveränderlich, und dies kann und sollte als Regel beibehalten werden.

Die Subtypisierung mit Unveränderlichkeit umgeht die Komplexität der Kovarianz. Unveränderlichkeit verbessert auch einige der Probleme mit Unterklassenbildung (z. B. Rectangle vs. Square ), aber nicht andere (z. B. implizite Subsumtion, Gleichheit usw.).

@bxqgit schrieb:

Einfache Syntax und Typsystem sind die wichtigen Vorteile von Go. Wenn Sie Generika hinzufügen, wird die Sprache zu einem hässlichen Durcheinander wie Scala oder Haskell.

Beachten Sie, dass Scala versucht, OOP, Subsclassing, FP, generische Module, HKT und Typklassen (über implicit ) in einer PL zusammenzuführen. Vielleicht reichen Typklassen allein aus.

Haskell ist nicht unbedingt wegen Typklassen-Generika stumpfsinnig, sondern wahrscheinlicher, weil es überall reine Funktionen erzwingt und die monadische Kategorientheorie verwendet, um kontrollierte imperative Effekte zu modellieren.

Daher denke ich, dass es nicht richtig ist, die Stumpfheit und Komplexität dieser PLs mit Typklassen in beispielsweise Rust in Verbindung zu bringen. Und lassen Sie uns nicht Typklassen für Rusts Lebenszeit+exklusive Mutabilitätsausleihe-Abstraktion verantwortlich machen.

Afaics, im Semantics -Abschnitt von _Type Parameters in Go_, ist das Problem, auf das @ianlancetaylor stößt, ein Konzeptualisierungsproblem, weil er ( Afaics ) anscheinend unwissentlich Typklassen neu erfindet:

Können wir SortableSlice und PSortableSlice zusammenführen, um das Beste aus beiden Welten zu haben? Nicht ganz; Es gibt keine Möglichkeit, eine parametrisierte Funktion zu schreiben, die entweder einen Typ mit einer Less -Methode oder einen eingebauten Typ unterstützt. Das Problem besteht darin, dass SortableSlice.Less für einen Typ ohne eine Less -Methode nicht instanziiert werden kann und dass es keine Möglichkeit gibt, nur für einige Typen eine Methode zu instanziieren, für andere jedoch nicht.

Die requires Less[T] -Klausel für die gebundene Typklasse (auch wenn sie vom Compiler implizit abgeleitet wird) in der Less -Methode für []T ist auf T und nicht []T . Die Implementierung der Typklasse Less[T] (die eine Methode Less enthält) für jede T stellt entweder eine Implementierung im Funktionsrumpf der Methode bereit oder weist < zu U[T] erfordert, wenn die Methoden von Sortable[U] einen Typparameter U $ benötigen, der den implementierenden Typ darstellt, z. B. []T . Afair @keean hat eine andere Möglichkeit , eine Sortierung zu strukturieren, indem eine separate Typklasse für den Werttyp T verwendet wird, für die kein HKT erforderlich ist.

Beachten Sie, dass diese Methoden für []T möglicherweise eine Typklasse Sortable[U] implementieren, wobei U []T ist.

(Abgesehen von der Technik: Es scheint, dass wir SortableSlice und PSortableSlice zusammenführen könnten, indem wir einen Mechanismus hätten, um eine Methode nur für einige Typargumente zu instanziieren, aber nicht für andere. Das Ergebnis wäre jedoch, die Kompilierung zu opfern -time Typsicherheit, da die Verwendung des falschen Typs zu einer Laufzeitpanik führen würde. In Go kann man bereits Schnittstellentypen und -methoden und Typzusicherungen verwenden, um das Verhalten zur Laufzeit auszuwählen. Es muss keine andere Möglichkeit bereitgestellt werden, dies mit Typparametern zu tun .)

Die Auswahl der an der Aufrufstelle gebundenen Typklasse wird zur Kompilierzeit für ein statisch bekanntes T aufgelöst. Wenn ein heterogener dynamischer Versand erforderlich ist, sehen Sie sich die Optionen an, die ich in meinem vorherigen Beitrag erläutert habe.

Ich hoffe, @keean findet Zeit, hierher zu kommen und zu helfen, Typklassen zu erklären, da er Experte ist und mir geholfen hat, diese Konzepte zu lernen. Ich könnte einige Fehler in meiner Erklärung haben.

PS Hinweis für diejenigen, die meinen vorherigen Beitrag bereits gelesen haben, beachten Sie, dass ich ihn etwa 10 Stunden nach dem Posten (nach etwas Schlaf) ausführlich bearbeitet habe, um hoffentlich die Punkte zu heterogenen Containern kohärenter zu machen.


Der Abschnitt " Zyklen " scheint falsch zu sein. Die Laufzeitkonstruktion der S[T]{e} -Instanz eines struct hat nichts mit der Auswahl der Implementierung der aufgerufenen generischen Funktion zu tun. Er denkt vermutlich, dass der Compiler nicht weiß, ob er die Implementierung der generischen Funktion für den Typ der Argumente spezialisiert, aber alle diese Typen sind zur Kompilierzeit bekannt.

Vielleicht könnte die Spezifikation des Typüberprüfungsabschnitts vereinfacht werden, indem das @ keean -Konzept eines verbundenen Graphen verschiedener Typen als Knoten für einen Vereinigungsalgorithmus untersucht wird. Alle unterschiedlichen Typen, die durch eine Kante verbunden sind, müssen kongruente Typen haben, wobei Kanten für alle Typen erstellt werden, die über eine Zuweisung oder anderweitig im Quellcode verbunden sind. Wenn es Union und Schnittpunkt gibt (aus meinem vorherigen Post), dann muss die Richtung der Zuordnung berücksichtigt werden ( irgendwie? ). Jeder unterschiedliche unbekannte Typ beginnt mit einer kleinsten Obergrenze (LUB) von Top und einer größten Untergrenze (GLB) von Bottom , und dann können Einschränkungen diese Grenzen ändern. Verbundene Typen müssen kompatible Grenzen haben. Einschränkungen sollten alle Typklassengrenzen sein.

In Umsetzung :

Beispielsweise ist es immer möglich, parametrisierte Funktionen zu implementieren, indem für jede Instanziierung eine neue Kopie der Funktion generiert wird, wobei die neue Funktion erstellt wird, indem die Typparameter durch die Typargumente ersetzt werden.

Ich glaube, der richtige Fachbegriff ist Monomorphisierung .

Dieser Ansatz würde die effizienteste Ausführungszeit auf Kosten einer beträchtlichen zusätzlichen Kompilierzeit und einer erhöhten Codegröße ergeben. Es ist wahrscheinlich eine gute Wahl für parametrisierte Funktionen, die klein genug für Inline sind, aber in den meisten anderen Fällen wäre es ein schlechter Kompromiss.

Die Profilerstellung würde dem Programmierer mitteilen, welche Funktionen am meisten von der Monomorphisierung profitieren können. Vielleicht führt der Java-Hotspot-Optimierer zur Laufzeit eine Monomorphisierungsoptimierung durch?

@egonelbre schrieb:

Es gibt eine Zusammenfassung der Go Generics-Diskussionen , die versucht, einen Überblick über Diskussionen von verschiedenen Orten zu geben.

Der Übersichtsabschnitt scheint zu implizieren, dass Javas universelle Verwendung von Boxing-Referenzen für Instanzen in einem Container die einzige Designachse ist, die der Monomorphisierung von Templates in C++ diametral entgegengesetzt ist. Aber Typklassengrenzen (die auch mit C++-Vorlagen implementiert werden können, aber immer monomorphisiert sind) werden auf Funktionen angewendet, nicht auf Parameter vom Containertyp. Daher fehlt in der Übersicht afaics die Designachse für Typklassen, in der wir wählen können, ob wir jede Typklassen-begrenzte Funktion monomorphisieren möchten. Mit Typklassen machen wir Programmierer immer schneller (weniger Boilerplate) und können ein verfeinertes Gleichgewicht zwischen schnelleren/langsameren Compilern/Ausführung und größerem/geringerem Aufblähen des Codes erreichen. Laut meinem vorherigen Beitrag wäre es vielleicht das Optimum, wenn die Auswahl der zu monomorphisierenden Funktionen vom Profiler gesteuert würde (automatisch oder wahrscheinlicher durch Anmerkung).

Im Abschnitt Probleme: Generische Datenstrukturen :

Nachteile

  • Generische Strukturen neigen dazu, Features aus allen Verwendungen zu akkumulieren, was zu längeren Kompilierungszeiten oder Code-Aufblähung führt oder einen intelligenteren Linker benötigt.

Für Typklassen gilt dies entweder nicht oder ist weniger problematisch, da Schnittstellen nur für Datentypen implementiert werden müssen, die an Funktionen geliefert werden, die diese Schnittstellen verwenden. Bei Typklassen geht es um die späte Bindung der Implementierung an die Schnittstelle, im Gegensatz zu OOP, das jeden Datentyp an seine Methoden für die class -Implementierung bindet.

Außerdem müssen nicht alle Methoden in einer einzigen Schnittstelle untergebracht werden. Die requires -Klausel (auch wenn sie implizit vom Compiler abgeleitet wird) in einer Typklasse, die für eine Funktionsdeklaration gebunden ist, kann die erforderlichen Schnittstellen mischen und abgleichen.

  • Generische Strukturen und die APIs, die auf ihnen arbeiten, sind in der Regel abstrakter als speziell entwickelte APIs, was den Aufrufern eine kognitive Belastung auferlegen kann

Ein Gegenargument, das meiner Meinung nach diese Bedenken erheblich mildert, ist, dass die kognitive Belastung durch das Erlernen einer unbegrenzten Anzahl von Sonderfall-Neuimplementierungen der im Wesentlichen gleichen generischen Algorithmen unbegrenzt ist. Wohingegen das Lernen der abstrakten generischen APIs begrenzt ist.

  • Tiefenoptimierungen sind sehr nicht generisch und kontextspezifisch, daher ist es schwieriger, sie in einem generischen Algorithmus zu optimieren.

Dies ist kein gültiger Betrug. Die 80/20-Regel sagt, fügen Sie keine unbegrenzte Komplexität (z. B. vorzeitige Optimierung) für Code hinzu, der, wenn er profiliert wird, dies nicht erfordert. Der Programmierer kann in 20 % der Fälle optimieren, während die restlichen 80 % von der begrenzten Komplexität und kognitiven Belastung der generischen APIs bewältigt werden.

Worauf wir hier wirklich hinaus wollen, ist die Regelmäßigkeit einer Sprache und generischer APIs helfen, nicht schaden. Diese Nachteile sind wirklich nicht richtig begriffen.

Alternativlösungen:

  • Verwenden Sie einfachere Strukturen anstelle komplizierter Strukturen

    • Verwenden Sie zB map[int]struct{} anstelle von Set

Rob Pike (und ich habe auch gesehen, wie er diesen Punkt im Video betont hat) scheint den Punkt zu übersehen, dass generische Container nicht ausreichen, um generische Funktionen zu erstellen. Wir brauchen dieses T in map[T] , damit wir den generischen Datentyp in Funktionen für Eingaben, Ausgaben und für unsere eigenen struct weitergeben können. Generika nur für Containertypparameter sind völlig unzureichend, um generische APIs auszudrücken, und generische APIs sind erforderlich, um die Komplexität und kognitive Belastung zu begrenzen und Regelmäßigkeit in einem Sprachökosystem zu erhalten. Außerdem habe ich nicht das erhöhte Maß an Refactoring gesehen (daher die reduzierte Zusammensetzbarkeit von Modulen, die nicht einfach umgestaltet werden können), die nicht-generischer Code erfordert, und darum geht es beim Ausdrucksproblem, das ich in meinem ersten Beitrag erwähnt habe.

Im Abschnitt Generische Ansätze :

Paketvorlagen
Dies ist ein Ansatz, der von Modula-3, OCaml, SML (sogenannte „Funktoren“) und Ada verwendet wird. Anstatt einen individuellen Typ für die Spezialisierung anzugeben, ist das gesamte Paket generisch. Sie spezialisieren das Paket, indem Sie beim Importieren die Typparameter festlegen.

Ich kann mich irren, aber das scheint nicht ganz richtig zu sein. ML-Funktoren (nicht zu verwechseln mit FP-Funktoren) können auch eine Ausgabe zurückgeben, die typparametriert bleibt. Es gäbe sonst keine Möglichkeit, die Algorithmen in anderen generischen Funktionen zu verwenden, sodass generische Module nicht in der Lage wären, andere generische Module wiederzuverwenden (durch Importieren mit konkreten Typen in). Dies scheint ein Versuch zu sein, den Punkt der Generika, der Wiederverwendung von Modulen usw. zu stark zu vereinfachen und dann völlig zu verfehlen.

Mein Verständnis ist eher, dass diese Parametrisierung des Pakettyps (auch bekannt als Modul) die Möglichkeit ermöglicht, Typparameter auf eine Gruppierung von struct , interface und func anzuwenden.

Komplizierteres Typensystem
Dies ist der Ansatz, den Haskell und Rust verfolgen.
[…]
Nachteile:

  • schwer in eine einfachere Sprache zu passen (https://groups.google.com/d/msg/golang-nuts/smT_0BhHfBs/MWwGlB-n40kJ)

Zitieren von @ianlancetaylor im verlinkten Dokument:

Wenn Sie das glauben, dann lohnt es sich darauf hinzuweisen, dass der Kern der
Map- und Slice-Code in der Go-Laufzeit ist nicht generisch im Sinne von
Typpolymorphismus verwenden. Es ist generisch in dem Sinne, dass es betrachtet
Geben Sie Reflexionsinformationen ein, um zu sehen, wie Sie Schrift verschieben und vergleichen können
Werte. Wir haben also den Existenzbeweis, dass es akzeptabel ist, zu schreiben
"generischer" Code in Go, indem nicht-polymorpher Code geschrieben wird, der Typ verwendet
Reflektionsinformationen effizient zu reflektieren und diesen Code dann einzuschließen
typsichere Boilerplate zur Kompilierzeit (im Fall von Maps und Slices
diese Textbausteine ​​werden natürlich vom Compiler bereitgestellt).

Und genau das würde ein Compiler, der aus einer Obermenge von Go mit hinzugefügten Generika transpiliert, als Go-Code ausgeben. Aber die Verpackung würde nicht auf einer Abgrenzung wie einer Verpackung basieren, da dies die bereits erwähnte Zusammensetzbarkeit verfehlen würde. Der Punkt ist, dass es keine Abkürzung zu einem guten zusammensetzbaren generischen Typsystem gibt. Entweder wir machen es richtig oder wir machen nichts, denn das Hinzufügen eines nicht komponierbaren Hacks, der nicht wirklich generisch ist, wird schließlich eine Clusterfuck-Trägheit von Patchwork, halbherziger Generizität und Unregelmäßigkeit von Eckfällen und Problemumgehungen erzeugen, die Go-Ökosystem-Code machen unverständlich.

Es stimmt auch, dass die meisten Leute große, komplexe Go-Programme schreiben
kein nennenswerter Bedarf an Generika festgestellt. Bisher war es eher so
eine irritierende Warze - die Notwendigkeit, drei Zeilen Textbausteine ​​dafür zu schreiben
jeder Typ muss sortiert werden - eher als ein großes Hindernis, um nützlich zu schreiben
Code.

Ja, das war einer der Gedanken in meinem Kopf, ob es gerechtfertigt ist, zu einem ausgewachsenen Typklassensystem zu gehen. Wenn alle Ihre Bibliotheken darauf basieren, könnte es anscheinend eine schöne Harmonie sein, aber wenn wir über die Trägheit bestehender Go-Hacks für Generizität nachdenken, dann wird die zusätzliche Synergie vielleicht für viele Projekte gering sein ?

Aber wenn ein Transpiler aus einer Typklassensyntax die vorhandene manuelle Art und Weise emuliert, wie Go Generika modellieren kann (Bearbeiten: was ich gerade gelesen habe, dass @andrewcmyers angibt, ist plausibel ), könnte dies weniger belastend sein und nützliche Synergien finden. Zum Beispiel habe ich festgestellt, dass zwei Parametertypklassen mit interface emuliert werden können, das auf einem struct $ implementiert ist, das ein Tupel emuliert, oder @jba hat eine Idee erwähnt , um interface inline im Kontext zu verwenden . Anscheinend sind struct strukturell statt nominell typisiert, es sei denn, man erhält einen Namen mit type ? Außerdem habe ich bestätigt, dass eine Methode von interface weitere interface eingeben kann, so dass es möglich sein kann, von HKT in Ihrem Sortierbeispiel, über das ich in meinem vorherigen Beitrag hier geschrieben habe, zu transpilieren. Aber ich muss darüber nachdenken, wenn ich nicht so müde bin.

Ich denke, es ist fair zu sagen, dass die meisten im Go-Team C++ nicht mögen
Templates, in denen eine vollständige Turing-Sprache überlagert wurde
eine andere Turing-Sprache vollständig, so dass die beiden Sprachen haben
völlig unterschiedliche Syntaxen und Programme in beiden Sprachen sind
ganz unterschiedlich geschrieben. C++-Vorlagen dienen als Warnung
Geschichte, weil die komplexe Implementierung das Ganze durchdrungen hat
Standardbibliothek, wodurch C++-Fehlermeldungen zu einer Quelle von werden
Staunen und Staunen. Dies ist kein Weg, dem Go jemals folgen wird.

Ich bezweifle, dass irgendjemand anderer Meinung sein wird! Der Vorteil der Monomorphisierung ist orthogonal zu den Nachteilen einer vollständigen generischen Metaprogrammierungs-Engine von Turing.

Übrigens scheint mir der Designfehler von C++-Vorlagen die gleiche generative Essenz des Fehlers von generativen (im Gegensatz zu applikativen) ML-Funktoren zu sein. Es gilt das Prinzip der geringsten Leistung.


@ianlancetaylor schrieb:

Es ist enttäuschend zu sehen, dass Go immer komplexer wird, indem eingebaute parametrisierte Typen hinzugefügt werden.

Trotz der Spekulationen in dieser Ausgabe halte ich dies für äußerst unwahrscheinlich.

Hoffentlich. Ich bin der festen Überzeugung, dass Go entweder ein kohärentes Generika-System hinzufügen oder einfach akzeptieren sollte, dass es niemals Generika geben wird.

Ich denke, dass ein Fork zu einem Transpiler eher passieren wird, zum Teil, weil ich die Finanzierung habe, um ihn zu implementieren, und daran interessiert bin. Trotzdem analysiere ich die Situation.

Das würde zwar das Ökosystem zerbrechen, aber zumindest kann Go dann seinen minimalistischen Prinzipien treu bleiben. Um das Ökosystem nicht zu zerbrechen und einige andere Innovationen zuzulassen, die ich gerne hätte, würde ich es wahrscheinlich nicht zu einer Obermenge machen und es stattdessen Zero nennen .

@pciet schrieb:

Meine Stimme ist Nein zu generalisierten Anwendungsgenerika, Ja zu mehr integrierten generischen Funktionen wie append und copy , die auf mehreren Basistypen funktionieren. Vielleicht könnten sort und search für die Sammlungstypen hinzugefügt werden?

Die Ausweitung dieser Trägheit wird vielleicht verhindern, dass ein umfassendes Generika-Feature jemals in Go aufgenommen wird. Diejenigen, die Generika wollten, werden wahrscheinlich auf grünere Weiden aufbrechen. @andrewcmyers wiederholte dies:

Es wäre enttäuschend zu sehen, wie Go immer komplexer wird, indem eingebaute parametrisierte Typen hinzugefügt werden. Es wäre besser, nur die Sprachunterstützung hinzuzufügen, damit Programmierer ihre eigenen parametrisierten Typen schreiben können.

@ shelby3

Afaik, die einzige Möglichkeit, heterogene Container/Sammlungen in Go zu erstellen, besteht jetzt darin, alle Typen in einer leeren Schnittstelle zusammenzufassen{}, die die Typisierungsinformationen wegwirft, und würde, wie ich annehme, Umwandlungen und eine Typprüfung zur Laufzeit erfordern, was irgendwie2 den Sinn verfehlt statische Typisierung.

Sehen Sie sich das Wrapper-Muster in den obigen Kommentaren für die statische Typprüfung von Interface{}-Sammlungen in Go an.

Der Punkt ist, dass es keine Abkürzung zu einem guten zusammensetzbaren generischen Typsystem gibt. Entweder wir machen es richtig oder gar nichts, denn das Hinzufügen eines nicht komponierbaren Hacks, der nicht wirklich generisch ist …

Können Sie das näher erläutern? Für den Fall der Sammlungstypen scheint es vernünftig zu sein, eine Schnittstelle zu haben, die das notwendige generische Verhalten enthaltener Elemente definiert, um Funktionen zu schreiben.

@pciet Dieser Code macht buchstäblich genau das, was @shelby3 beschrieben hat, und erwägt ein Antimuster. Ich zitiere dich von vorhin:

Dies enthält Dokumentations- und Testbeispiele, die das Typ-Wrapper-Muster (danke @pierrre) für Typsicherheit zur Kompilierungszeit und auch die Reflexionsprüfung für Typsicherheit zur Laufzeit enthalten.

Sie nehmen Code, dem Typinformationen fehlen, und fügen Typ-für-Typ-Umwandlungen und Laufzeit-Typprüfungen mithilfe von Reflect hinzu. Genau darüber hat sich @shelby3 beschwert. Ich neige dazu, diesen Ansatz "Monomorphisierung von Hand" zu nennen, und es ist genau die Art von mühsamer Arbeit, die meiner Meinung nach am besten einem Compiler überlassen wird.

Dieser Ansatz hat eine Reihe von Nachteilen:

  • Erfordert Typ-für-Typ-Wrapper, die entweder von Hand oder mit einem go generate -ähnlichen Werkzeug verwaltet werden
  • (Wenn es von Hand statt mit einem Tool gemacht wird) Gelegenheit, Fehler in der Boilerplate zu machen, die erst zur Laufzeit abgefangen werden
  • Erfordert dynamisches Dispatch anstelle von statischem Dispatch, das sowohl langsamer ist als auch mehr Arbeitsspeicher benötigt
  • Verwendet Runtime Reflection anstelle von Assertionen zur Kompilierzeit, was ebenfalls langsam ist
  • Nicht zusammensetzbar: wirkt vollständig auf konkrete Typen ohne Möglichkeiten, typklassenähnliche (oder sogar schnittstellenähnliche) Grenzen für Typen zu verwenden, es sei denn, Sie handrollen eine weitere Indirektionsebene für jede nicht leere Schnittstelle, über die Sie auch abstrahieren möchten

Können Sie das näher erläutern? Für den Fall der Sammlungstypen scheint es vernünftig zu sein, eine Schnittstelle zu haben, die das notwendige generische Verhalten enthaltener Elemente definiert, um Funktionen zu schreiben.

Überall dort, wo Sie anstelle oder zusätzlich zu einem konkreten Typ eine Bindung verwenden möchten, müssen Sie auch für jeden Schnittstellentyp die gleiche Typprüfungs-Boilerplate schreiben. Es verschlimmert nur die (vielleicht kombinatorische) Explosion von Wrappern statischen Typs, die Sie schreiben müssen.

Es gibt auch Ideen, die, soweit ich weiß, heute in Gos Typensystem überhaupt nicht mehr ausgedrückt werden können, wie zum Beispiel eine Bindung an eine Kombination von Schnittstellen. Stellen Sie sich vor, wir haben:

type Foo interface {
    ...
}

type Bar interface {
    ...
}

Wie drücken wir mit einer rein statischen Typprüfung aus, dass wir einen Typ wollen, der sowohl Foo als auch Bar implementiert? Soweit ich weiß, ist dies in Go nicht möglich (ohne auf Laufzeitprüfungen zurückzugreifen, die möglicherweise fehlschlagen, und auf statische Typsicherheit zu verzichten).

Mit einem Typklassen-basierten Generika-System könnten wir dies wie folgt ausdrücken:

func baz<T Foo + Bar>(t T) {
    ...
}

@tarcieri

Wie drücken wir mit einer rein statischen Typprüfung aus, dass wir einen Typ wollen, der sowohl Foo als auch Bar implementiert?

einfach so:

type T interface {
    Foo
    Bar
}

func baz(t T) { ... }

@sbinet ordentlich, BIS

Persönlich halte ich die Laufzeitreflexion für eine Fehlfunktion, aber das bin nur ich ... Ich kann erklären, warum, wenn es jemanden interessiert.

Ich denke, jeder, der Generika jeglicher Art implementiert, sollte Stepanovs "Elements of Programming" zuerst mehrmals lesen. Es würde viele Not Invented Here-Probleme vermeiden und das Rad neu erfinden. Nachdem Sie das gelesen haben, sollte klar sein, warum „C++-Konzepte“ und „Haskell-Typklassen“ der richtige Weg sind, Generika zu erstellen.

Wie ich sehe, scheint dieses Problem wieder aktiv zu sein
Hier ist ein Spielplatz für Strohmänner
https://go-li.github.io/test.html
Fügen Sie einfach Demoprogramme von hier ein
https://github.com/go-li/demo

Vielen Dank für Ihre Bewertung dieses einzelnen Parameters
Funktionen Generika.

Wir pflegen das gehackte gccgo und
Dieses Projekt wäre ohne Sie nicht möglich, also wir
wollte etwas zurückgeben.

Wir freuen uns auch auf die Generika, die Sie verwenden, machen Sie weiter mit der großartigen Arbeit!

@anlhord wo sind die Implementierungsdetails dazu? Wo kann man die Syntax nachlesen? Was wird umgesetzt? Was ist nicht implementiert? Was sind die Spezifikationen für diese Implementierungen? Was sind die Vor- und Nachteile dafür?

Der Spielplatz-Link enthält das denkbar schlechteste Beispiel dafür:

package main

import "fmt"

func main() {
    fmt.Println("Hello, playground")
}

Dieser Code sagt mir nichts darüber, wie man ihn benutzt und was ich testen kann.

Wenn Sie diese Dinge verbessern könnten, würde dies helfen, besser zu verstehen, was Ihr Vorschlag ist und wie er im Vergleich zu den vorherigen Vorschlägen abschneidet / zu sehen, wie die anderen hier angesprochenen Punkte darauf zutreffen oder nicht.

Ich hoffe, dies hilft Ihnen, die Probleme mit Ihrem Kommentar zu verstehen.

@joho schrieb:

Wäre die wissenschaftliche Literatur für Hinweise zur Bewertung von Ansätzen wertvoll?

Das einzige Papier, das ich zu diesem Thema gelesen habe, ist Profitieren Entwickler von generischen Typen ? (Entschuldigung, Paywall, Sie könnten sich zu einem PDF-Download googlen), der Folgendes zu sagen hatte

Folglich eine konservative Interpretation des Experiments
ist, dass generische Typen als Kompromiss betrachtet werden können
zwischen den positiven Dokumentationsmerkmalen und der
negative Dehnbarkeitseigenschaften.

Ich nehme an, OOP und Unterklassen (z. B. Klassen in Java und C++) werden beide nicht ernsthaft in Betracht gezogen, da Go bereits eine Typklasse wie interface hat (ohne den expliziten generischen Typparameter T ), Java schon zitiert, was nicht kopiert werden sollte, und weil viele argumentiert haben, dass sie ein Anti-Muster sind. Upthread Ich habe einige dieser Argumente verlinkt. Wir könnten diese Analyse vertiefen, wenn es jemanden interessiert.

Neuere Forschungen wie das oben erwähnte Genus-System habe ich noch nicht studiert. Ich bin misstrauisch gegenüber „Küchenspülen“-Systemen, die versuchen, so viele Paradigmen zu mischen (z. B. Unterklassenbildung, Mehrfachvererbung, OOP, Trait-Linearisierung, implicit , Typklassen, abstrakte Typen usw.), aufgrund der Beschwerden über Scala in der Praxis so viele Eckfälle zu haben, obwohl sich das vielleicht mit Scala 3 (auch bekannt als Dotty und der DOT-Kalkül) verbessert. Ich bin neugierig, ob ihre Vergleichstabelle mit der experimentellen Scala 3 oder der aktuellen Version von Scala verglichen wird?

Also afaics, was bleibt, sind ML-Funktoren und Haskell-Typklassen im Sinne von bewährten Generizitätssystemen, die die Erweiterbarkeit und Flexibilität im Vergleich zu OOP+Unterklassifizierung erheblich verbessern.

Ich habe einige der privaten Diskussionen aufgeschrieben, die @keean und ich über ML-Funktormodule im Vergleich zu Typklassen geführt haben. Die Highlights scheinen zu sein:

  • Typklassen _modellieren eine Algebra_ (aber ohne überprüfte Axiome ) und implementieren jeden Datentyp für jede Schnittstelle nur auf eine Weise. Dadurch wird eine implizite Auswahl der Implementierungen durch den Compiler ohne Annotation an der Aufrufstelle ermöglicht.

  • Applikative Funktoren haben referenzielle Transparenz, während generative Funktoren bei jeder Instanziierung eine neue Instanz erstellen, was bedeutet, dass sie nicht invariant in der Initialisierungsreihenfolge sind.

  • ML-Funktoren sind leistungsfähiger/flexibler als Typklassen, aber dies geht auf Kosten von mehr Anmerkungen und potenziell mehr Eckfallinteraktionen. Und laut @keean benötigen sie abhängige Typen (für zugeordnete Typen ), was ein komplexeres Typsystem ist. @keean hält Stepanovs _Ausdruck der Generizität als Algebra_ plus Typklassen für ausreichend leistungsfähig und flexibel, so dass dies der ideale Punkt für hochmoderne, gut bewährte (in Haskell und jetzt in Rust) Generizität zu sein scheint. Die Axiome werden jedoch nicht durch Typklassen erzwungen .

  • Ich habe vorgeschlagen, Vereinigungen für heterogene Container mit Typklassen hinzuzufügen, die sich entlang einer anderen Achse des Ausdrucksproblems erstrecken sollen, obwohl dies Unveränderlichkeit oder Kopieren erfordert (nur für die Fälle, in denen die heterogene Erweiterbarkeit verwendet wird), von denen bekannt ist, dass sie ein O (log n) haben.

@larst schrieb:

Es könnte interessant sein, einen oder mehrere experimentelle Transpiler zu haben - einen Go-Generika-Quellcode für Go 1.xy-Quellcode-Compiler.

PS: Ich bezweifle, dass Go ein so ausgefeiltes Typisierungssystem übernehmen wird, aber ich erwäge einen Transpiler für die bestehende Go-Syntax, wie ich in meinem vorherigen Beitrag erwähnt habe (siehe die Bearbeitung unten). Und ich möchte ein robustes generisches System zusammen mit diesen sehr wünschenswerten Go-Funktionen. Typeclass-Generika auf Go scheinen das zu sein, was ich will.

@bcmills schrieb über seinen Vorschlag zu Kompilierzeitfunktionen für Generizität:

Ich habe gehört, dass das gleiche Argument verwendet wurde, um für den Export interface -Typen statt konkreter Typen in Go-APIs einzutreten, und es stellt sich heraus, dass das Gegenteil häufiger vorkommt: vorzeitige Abstraktion überfordert die Typen und behindert die Erweiterung von APIs. (Für ein solches Beispiel siehe #19584.) Wenn Sie sich auf diese Argumentationslinie verlassen wollen, müssen Sie meiner Meinung nach einige konkrete Beispiele liefern.

Es ist sicherlich richtig, dass Typsystem-Abstraktionen zwangsläufig einige Freiheitsgrade aufgeben, und manchmal sind wir mit „unsicher“ (dh unter Verletzung der statisch geprüften Abstraktion) aus diesen Beschränkungen ausgebrochen, aber das muss gegen die Vorteile von abgewogen werden modulare Entkopplung mit prägnant annotierten Invarianten.

Beim Entwerfen eines Systems für Generizität möchten wir wahrscheinlich die Regelmäßigkeit und Vorhersagbarkeit des Ökosystems als eines der Hauptziele erhöhen, insbesondere wenn die Kernphilosophie von Go berücksichtigt wird (z. B. dass durchschnittliche Programmierer Priorität haben).

Es gilt das Prinzip der geringsten Leistung. Die Stärke/Flexibilität der Invarianten, die in Kompilierzeitfunktionen für Generizität „versteckt“ sind, muss gegen ihre Fähigkeit abgewogen werden, beispielsweise die Lesbarkeit des Quellcodes im Ökosystem zu beeinträchtigen (wobei die modulare Entkopplung äußerst wichtig ist, da der Leser dies nicht tut müssen aufgrund impliziter transitiver Abhängigkeiten keine potenziell unbegrenzte Menge an Code lesen, um ein bestimmtes Modul/Paket zu verstehen!). Bei der impliziten Auflösung von Typklassen-Implementierungsinstanzen tritt dieses Problem auf, wenn ihre Algebra nicht eingehalten wird .

Sicher, aber das gilt bereits für viele implizite Einschränkungen in Go, unabhängig von generischen Programmiermechanismen.

Beispielsweise kann eine Funktion einen Parameter vom Typ Schnittstelle erhalten und ihre Methoden zunächst sequentiell aufrufen. Wenn sich diese Funktion später ändert, um diese Methoden gleichzeitig aufzurufen (durch das Spawnen zusätzlicher Goroutinen), wird die Einschränkung "muss für die gleichzeitige Verwendung sicher sein" nicht im Typsystem widergespiegelt.

Aber afaik Go hat nicht versucht, eine Abstraktion zu entwerfen, um diese Effekte zu modularisieren. Rust hat eine solche Abstraktion (was ich übrigens für übertrieben pita/tsuris/limitierend für einige/die meisten Anwendungsfälle halte und ich plädiere für eine einfachere Singlethread-Modellabstraktion, aber leider unterstützt Go nicht die Beschränkung aller erzeugten Goroutinen auf denselben Thread ) . Und Haskell erfordert eine monadische Kontrolle über Effekte , da reine Funktionen für referenzielle Transparenz erzwungen werden.


@alerca schrieb:

Ich denke, der größte Nachteil von abgeleiteten Einschränkungen ist, dass sie es einfach machen, einen Typ so zu verwenden, dass eine Einschränkung eingeführt wird, ohne sie vollständig zu verstehen. Im besten Fall bedeutet dies nur, dass Ihre Benutzer auf unerwartete Kompilierzeitfehler stoßen können, aber im schlimmsten Fall bedeutet dies, dass Sie das Paket für Verbraucher beschädigen können, indem Sie versehentlich eine neue Einschränkung einführen. Explizit spezifizierte Constraints würden dies vermeiden.

Einverstanden. Code in anderen Modulen heimlich brechen zu können, weil die Invarianten der Typen nicht explizit annotiert sind, ist ungeheuer heimtückisch.


@andrewcmyers schrieb:

Um es klar zu sagen, ich halte die Generierung von Code im Makrostil, ob mit gen, cpp, gofmt -r oder anderen Makro-/Vorlagenwerkzeugen, nicht für eine gute Lösung des Generika-Problems, selbst wenn sie standardisiert ist. Es hat die gleichen Probleme wie C++-Templates: aufgeblähter Code, fehlende modulare Typprüfung und Schwierigkeiten beim Debuggen. Es wird schlimmer, wenn Sie beginnen, wie es natürlich ist, generischen Code in Bezug auf anderen generischen Code zu erstellen. Meiner Meinung nach sind die Vorteile begrenzt: Es würde das Leben für die Autoren des Go-Compilers relativ einfach machen und es produziert effizienten Code – es sei denn, es gibt einen Befehls-Cache-Druck, eine häufige Situation in moderner Software!

@keean scheint dir zuzustimmen .

@shelby3 Danke für die Kommentare. Kannst du das nächste mal die Kommentare/Änderungen direkt im Dokument selbst vornehmen. Es ist einfacher zu verfolgen, wo Dinge repariert werden müssen, und einfacher sicherzustellen, dass alle Noten eine angemessene Antwort erhalten.

Der Abschnitt Übersicht scheint zu implizieren, dass Javas universelle Verwendung von Boxing-Referenzen für Instanzen ...

Kommentar hinzugefügt, um klarzustellen, dass es sich nicht um eine umfassende Liste handelt. Es ist hauptsächlich da, damit die Leute den Kern der verschiedenen Kompromisse verstehen. Die vollständige Liste der verschiedenen Ansätze finden Sie weiter unten.

Generische Strukturen neigen dazu, Features aus allen Verwendungen zu akkumulieren, was zu längeren Kompilierungszeiten oder Code-Aufblähung führt oder einen intelligenteren Linker benötigt.
Für Typklassen gilt dies entweder nicht oder ist weniger problematisch, da Schnittstellen nur für Datentypen implementiert werden müssen, die an Funktionen geliefert werden, die diese Schnittstellen verwenden. Bei Typklassen geht es um die späte Bindung der Implementierung an die Schnittstelle, im Gegensatz zu OOP, das jeden Datentyp an seine Methoden für die Klassenimplementierung bindet.

Bei dieser Aussage geht es darum, was langfristig mit generischen Datenstrukturen passiert. Mit anderen Worten, eine generische Datenstruktur endet oft damit, alle verschiedenen Verwendungen zu sammeln – anstatt mehrere kleinere Implementierungen für verschiedene Zwecke zu haben. Schauen Sie sich nur als Beispiel https://www.scala-lang.org/api/2.12.3/scala/collection/immutable/List.html an.

Es ist wichtig zu beachten, dass nur das „mechanische Design“ und „so viel Flexibilität“ nicht ausreichen, um eine gute „Generika-Lösung“ zu erstellen. Es braucht auch gute Anweisungen, wie Dinge verwendet werden sollten und was zu vermeiden ist, und überlegen Sie, wie die Leute es letztendlich verwenden.

Generische Strukturen und die darauf operierenden APIs sind tendenziell abstrakter als speziell entwickelte APIs ...

Ein Gegenargument, das meiner Meinung nach diese Bedenken erheblich mildert, ist, dass die kognitive Belastung des Erlernens einer unbegrenzten Anzahl von Sonderfall-Neuimplementierungen der im Wesentlichen gleichen generischen Algorithmen unbegrenzt ist ...

Hinweis zur kognitiven Belastung vieler ähnlicher APIs hinzugefügt.

Die Neuimplementierungen in Sonderfällen sind in der Praxis nicht unbegrenzt. Sie sehen nur eine feste Anzahl von Spezialisierungen.

Dies ist kein gültiger Betrug.

Sie mögen mit einigen Punkten nicht einverstanden sein, ich bin mit einigen bis zu einem gewissen Grad nicht einverstanden, aber ich verstehe ihren Standpunkt und versuche, die Probleme zu verstehen, mit denen die Menschen täglich konfrontiert sind. Ziel des Dokuments sei es, unterschiedliche Meinungen zu sammeln, nicht zu beurteilen, „wie nervig etwas für jemanden ist“.

Das Dokument bezieht jedoch Stellung zu "Problemen, die auf Probleme der realen Welt zurückzuführen sind", da abstrakte und erleichterte Probleme in Foren dazu neigen, in bedeutungsloses Geschwätz zu verfallen, ohne dass Verständnis aufgebaut wird.

Worauf wir hier wirklich hinaus wollen, ist die Regelmäßigkeit einer Sprache und generischer APIs helfen, nicht schaden.

Sicher, in der Praxis benötigen Sie diese Art der Optimierung möglicherweise nur für weniger als 1 % der Fälle.

Alternativlösungen:

Alternative Lösungen sind nicht als Ersatz für Generika gedacht. Sondern eher eine Liste möglicher Lösungen für verschiedene Arten von Problemen.

Paketvorlagen

Ich kann mich irren, aber das scheint nicht ganz richtig zu sein. ML-Funktoren (nicht zu verwechseln mit FP-Funktoren) können auch eine Ausgabe zurückgeben, die typparametriert bleibt.

Können Sie es klarer formulieren und gegebenenfalls in zwei verschiedene Ansätze aufteilen?

@egonelbre danke auch für die Antwort, damit ich weiß, zu welchen Punkten ich meine Gedanken weiter präzisieren muss.

Kannst du das nächste mal die Kommentare/Änderungen direkt im Dokument selbst vornehmen.

Entschuldigung, ich wünschte, ich könnte dem nachkommen, aber ich habe noch nie die Diskussionsfunktionen von Google Doc verwendet, habe keine Zeit, es zu lernen, und ich bevorzuge es auch, meine Diskussionen auf Github für zukünftige Referenzen verlinken zu können.

Schauen Sie sich nur als Beispiel https://www.scala-lang.org/api/2.12.3/scala/collection/immutable/List.html an.

Das Design der Bibliothek der Scala-Sammlungen wurde von vielen Personen kritisiert, darunter auch von einem ihrer ehemaligen Schlüsselteammitglieder . Ein an LtU geposteter Kommentar ist repräsentativ. Beachten Sie, dass ich Folgendes zu einem meiner früheren Beiträge in diesem Thread hinzugefügt habe, um dies zu beheben:

Ich bin misstrauisch gegenüber „Küchenspülen“-Systemen, die versuchen, so viele Paradigmen zu mischen (z. B. Unterklassenbildung, Mehrfachvererbung, OOP, Trait-Linearisierung, implicit , Typklassen, abstrakte Typen usw.), aufgrund der Beschwerden über Scala in der Praxis so viele Eckfälle zu haben, obwohl sich das vielleicht mit Scala 3 (auch bekannt als Dotty und der DOT-Kalkül) verbessert.

Ich glaube nicht, dass die Sammlungsbibliothek von Scala repräsentativ für Bibliotheken wäre, die für eine PL mit nur Typklassen für Polymorphismus erstellt wurden. Afair verwenden die Scala-Sammlungen das Anti-Vererbungsmuster , das die komplexen Hierarchien verursachte, kombiniert mit implicit -Helfern wie CanBuildFrom , die das Komplexitätsbudget sprengten. Und ich denke, wenn @keeans Argument eingehalten wird, dass Stepanovs _Elements of Programming_ eine Algebra ist, könnte eine elegante Sammlungsbibliothek erstellt werden. Es war die erste Alternative, die ich zu einer auf Funktoren (FP) basierenden Sammlungsbibliothek gesehen hatte (dh Haskell nicht kopierte ), die ebenfalls auf Mathematik basierte. Ich möchte dies in der Praxis sehen, was einer der Gründe ist, warum ich mit ihm am Design eines neuen PL zusammenarbeite/diskutiere. Und von diesem Moment an plane ich, diese Sprache zunächst nach Go transpilieren zu lassen (obwohl ich jahrelang versucht habe, einen Weg zu finden, dies zu vermeiden). Hoffentlich können wir bald experimentieren, um zu sehen, wie es funktioniert.

Meine Wahrnehmung ist, dass die Go-Gemeinschaft/Philosophie lieber abwarten würde, was in der Praxis funktioniert, und es später, wenn es einmal bewiesen ist, übernehmen würde, als die Sprache mit fehlgeschlagenen Experimenten zu überstürzen und zu verschmutzen. Denn wie Sie wiederholt haben, sind all diese abstrakten Behauptungen nicht so konstruktiv (außer vielleicht für PL-Designtheoretiker). Außerdem ist es wahrscheinlich nicht plausibel, ein kohärentes Generikasystem von einem Komitee zu entwerfen.

Es braucht auch gute Anweisungen, wie Dinge verwendet werden sollten und was zu vermeiden ist, und überlegen Sie, wie die Leute es letztendlich verwenden.

Und ich denke, es wird helfen, nicht so viele verschiedene Paradigmen zu mischen, die dem Programmierer in derselben Sprache zur Verfügung stehen. Es ist anscheinend nicht notwendig ( @keean und ich müssen diese Behauptung beweisen). Ich denke, wir schreiben beide der Philosophie zu, dass das Komplexitätsbudget endlich ist und dass das, was Sie aus der PL weglassen , genauso wichtig ist wie die enthaltenen Funktionen.

Das Dokument bezieht jedoch Stellung zu "Problemen, die auf Probleme der realen Welt zurückzuführen sind", da abstrakte und erleichterte Probleme in Foren dazu neigen, in bedeutungsloses Geschwätz zu verfallen, ohne dass Verständnis aufgebaut wird.

Einverstanden. Und es ist auch für alle schwierig, den abstrakten Punkten zu folgen. Der Teufel steckt im Detail und den tatsächlichen Ergebnissen in freier Wildbahn.

Sicher, in der Praxis benötigen Sie diese Art der Optimierung möglicherweise nur für weniger als 1 % der Fälle.

Go hat bereits interface für Generizität, sodass Fälle behandelt werden können, in denen kein parametrischer Polymorphismus für den Typ T für die Instanz der Schnittstelle benötigt wird, die von der Aufrufsite bereitgestellt wird.

Ich glaube, ich habe irgendwo gelesen, vielleicht war es ein Upthread, das Argument, dass die Standardbibliothek von Go tatsächlich unter der Inkonsistenz der optimalen Verwendung der aktuellsten Redewendungen leidet. Ich weiß nicht, ob das stimmt, weil ich noch keine Erfahrung mit Go habe. Der Punkt, den ich mache, ist, dass das gewählte Generika-Paradigma alle Bibliotheken infiziert. Ja, ab sofort können Sie behaupten, dass nur 1 % des Codes dies benötigen würde, da es bereits eine Trägheit in Redewendungen gibt, die die Notwendigkeit von Generika vermeiden.

Du magst Recht haben. Ich habe auch meine Skepsis darüber, wie viel ich eine bestimmte Sprachfunktion verwenden werde. Ich denke, durch Experimentieren werde ich vorgehen, um das herauszufinden. PL-Design ist ein iterativer Prozess, und das Problem besteht darin, gegen die sich entwickelnde Trägheit anzukämpfen, die es schwierig macht, den Prozess zu iterieren. Ich denke also, Rob Pike hat Recht in dem Video, in dem er vorschlägt, Programme zu schreiben, die Code für Programme schreiben (was bedeutet, dass Tools zur Generierung von Werkzeugen und Transpilern geschrieben werden), um zu experimentieren und Ideen zu testen.

Wenn wir zeigen können, dass ein bestimmter Satz von Funktionen in der Praxis (und hoffentlich auch beliebter) denen von Go überlegen ist, dann können wir vielleicht einen Konsens darüber sehen, sie zu Go hinzuzufügen. Ich ermutige andere, ebenfalls experimentelle Systeme zu entwickeln, die auf Go transpilieren.

Können Sie es klarer formulieren und gegebenenfalls in zwei verschiedene Ansätze aufteilen?

Ich schließe mich denjenigen an, die den Versuch entmutigen möchten, ein allzu einfaches Templating-Feature in Go einzubauen und zu behaupten, dass es sich um Generika handelt. IOW, ich denke, ein gut funktionierendes Generika-System, das nicht zu einer schlechten Trägheit wird, ist grundsätzlich unvereinbar mit dem Wunsch nach einem allzu einfachen Design für Generika. Afaik, ein Generika-System braucht ein gut durchdachtes und bewährtes ganzheitliches Design. In Anlehnung an das, was @larsth geschrieben hat , ermutige ich diejenigen mit ernsthaften Vorschlägen, zuerst einen Transpiler zu bauen (oder in einem Fork des gccgo-Frontends zu implementieren) und dann mit dem Vorschlag zu experimentieren, damit wir alle seine Einschränkungen besser verstehen können. Ich wurde ermutigt, oben im Thread zu lesen, dass @ianlancetaylor nicht glaubte, dass eine Verschmutzung durch schlechte Trägheit zu Go hinzugefügt werden würde. Was meine spezielle Beschwerde über den Parametrisierungsvorschlag auf Paketebene betrifft, mein Vorschlag für alle, die ihn vorschlagen, überlegen Sie bitte, ob Sie einen Compiler erstellen, mit dem wir alle spielen können, und dann können wir alle über Beispiele sprechen, was uns gefällt und was nicht. mag es nicht. Sonst reden wir aneinander vorbei, weil ich vielleicht den abstrakt beschriebenen Vorschlag nicht einmal richtig verstehe. Ich muss den Vorschlag nicht verstehen, weil ich nicht verstehe, wie das parametrisierte Paket in einem anderen Paket wiederverwendet werden kann, das ebenfalls parametrisiert ist. IOW, wenn ein Paket Parameter übernimmt, muss es auch andere Pakete mit Parametern instanziieren. Aber es schien, dass der Vorschlag besagte, dass die einzige Möglichkeit, ein parametrisiertes Paket zu instanziieren, mit einem konkreten Typ und nicht mit Typparametern bestand.

Entschuldigung so langatmig. Möchte sicher gehen, dass ich nicht missverstanden werde.

@shelby3 ah, dann habe ich die anfängliche Beschwerde falsch verstanden. Zunächst sollte ich klarstellen, dass die Abschnitte in "Generics Approaches" keine konkreten Vorschläge sind. Es sind Ansätze oder, anders ausgedrückt, größere Designentscheidungen, die man in einem konkreten Generika-Ansatz treffen könnte. Die Gruppierungen sind jedoch stark durch bestehende Implementierungen oder konkrete/informelle Vorschläge motiviert. Außerdem vermute ich, dass auf dieser Liste noch mindestens 5 große Ideen fehlen.

Für den Ansatz "Paketvorlagen" gibt es zwei Varianten davon (siehe die verlinkten Diskussionen im Dokument):

  1. "Schnittstellen"-basierte generische Pakete,
  2. explizit generische Pakete.

Für 1. erfordert das generische Paket nichts Besonderes -- zum Beispiel würde das aktuelle container/ring für die Spezialisierung nutzbar werden. Stellen Sie sich „Spezialisierung“ hier so vor, dass alle Instanzen der Schnittstelle im Paket durch den konkreten Typ ersetzt werden (und zirkuläre Importe ignoriert werden). Wenn dieses Paket selbst ein anderes Paket spezialisiert, kann es die "Schnittstelle" als Spezialisierung verwenden - daraus folgt, dass diese Verwendung dann ebenfalls spezialisiert wird.

Für 2. können Sie sie auf zwei Arten betrachten. Eine davon ist die rekursive konkrete Spezialisierung bei jedem Import – ähnlich wie beim Templating/Makroing, es gäbe zu keinem Zeitpunkt ein „teilweise angewendetes Paket“. Natürlich kann man auch von der funktionalen Seite her sehen, dass das generische Paket ein Partial mit Parametern ist und man es dann spezialisiert.

Also, ja, Sie können ein parametrisiertes Paket in einem anderen verwenden.

In Anlehnung an das, was @larsth geschrieben hat, ermutige ich diejenigen mit ernsthaften Vorschlägen, zuerst einen Transpiler zu bauen (oder in einem Fork des gccgo-Frontends zu implementieren) und dann mit dem Vorschlag zu experimentieren, damit wir alle seine Einschränkungen besser verstehen können.

Ich weiß, dass dies nicht explizit auf diesen Ansatz ausgerichtet war, aber es gibt 4 verschiedene Prototypen, um die Idee zu testen. Natürlich sind sie keine vollständigen Transpiler, aber sie reichen aus, um einige der Ideen zu testen. dh ich bin mir nicht sicher, ob jemand den Fall "parametrisiertes Paket von einem anderen verwenden" implementiert hat.

Parametrisierte Pakete klingen sehr nach ML-Modulen (und ML-Funktoren sind die Parameter, die andere Pakete sein können). Es gibt zwei Möglichkeiten, wie diese "applikativ" oder "generativ" wirken können. Ein applikativer Funktor ist wie ein Wert oder ein Typ-Alias. Ein generativer Funktor muss konstruiert werden und jede Instanz ist anders. Eine andere Möglichkeit, darüber nachzudenken, ist, dass ein Paket, um anwendbar zu sein, rein sein muss (dh keine veränderlichen Variablen auf Paketebene). Wenn auf Paketebene ein Zustand vorhanden ist, muss dieser generativ sein, da dieser Zustand initialisiert werden muss, und es spielt eine Rolle, welche "Instanz" eines generativen Pakets Sie tatsächlich als Parameter an andere Pakete übergeben, die wiederum generativ sein müssen. Beispielsweise sind Ada-Pakete generativ.

Das Problem mit dem generativen Paketansatz besteht darin, dass er viele Boilerplates erstellt, in denen Sie Pakete mit Parametern instanziieren. Sie können sich Ada-Generika ansehen, um zu sehen, wie das aussieht.

Typklassen vermeiden diesen Textbaustein, indem sie die Typklasse implizit nur basierend auf den in der Funktion verwendeten Typen auswählen. Sie können Typklassen auch als eingeschränktes Überladen mit mehrfachem Dispatch betrachten, wobei die Überladungsauflösung fast immer statisch zur Kompilierzeit erfolgt, mit Ausnahmen für polymorphe Rekursion und existenzielle Typen (die im Wesentlichen Varianten sind, aus denen Sie nicht austreten können, sondern nur verwenden können die Schnittstellen, die die Variante bestätigt).

Ein applikativer Funktor ist wie ein Wert oder ein Typ-Alias. Ein generativer Funktor muss konstruiert werden und jede Instanz ist anders. Eine andere Möglichkeit, darüber nachzudenken, ist, dass ein Paket, um anwendbar zu sein, rein sein muss (dh keine veränderlichen Variablen auf Paketebene). Wenn auf Paketebene ein Zustand vorhanden ist, muss dieser generativ sein, da dieser Zustand initialisiert werden muss, und es spielt eine Rolle, welche "Instanz" eines generativen Pakets Sie tatsächlich als Parameter an andere Pakete übergeben, die wiederum generativ sein müssen. Beispielsweise sind Ada-Pakete generativ.

Vielen Dank für die genaue Terminologie, ich muss überlegen, wie ich diese Ideen in das Dokument integrieren kann.

Außerdem sehe ich keinen Grund, warum Sie keinen "automatischen Typ-Alias ​​für ein generiertes Paket" haben könnten - in gewissem Sinne etwas zwischen "applicative functor" und "generative functor" -Ansatz. Wenn das Paket irgendeine Form von Status enthält, kann es natürlich kompliziert werden, es zu debuggen und zu verstehen.

Das Problem mit dem generativen Paketansatz besteht darin, dass er viele Boilerplates erstellt, in denen Sie Pakete mit Parametern instanziieren. Sie können sich Ada-Generika ansehen, um zu sehen, wie das aussieht.

Soweit ich sehe, würde es weniger Boilerplate als C++-Vorlagen, aber mehr als Typklassen erstellen. Haben Sie ein gutes reales Programm für Ada, das das Problem demonstriert? _(Mit realer Welt meine ich Code, den jemand in der Produktion verwendet/verwendet hat.)_

Klar, schau dir mein Ada Go-Board an: https://github.com/keean/Go-Board-Ada/blob/master/go.adb

Obwohl dies eine ziemlich lockere Definition von Produktion ist, ist der Code optimiert, funktioniert genauso gut wie die C++-Version und ihre Open-Source-Version, und der Algorithmus wurde über mehrere Jahre verfeinert. Sie können sich auch die C++-Version ansehen: https://github.com/keean/Go-Board/blob/master/go.cpp

Dies zeigt (glaube ich), dass Ada-Generika eine sauberere Lösung sind als C++-Templates (aber das ist nicht schwer), andererseits ist es schwierig, den schnellen Zugriff auf die Datenstrukturen in Ada aufgrund der Einschränkungen bei der Rückgabe einer Referenz durchzuführen .

Wenn Sie sich ein Paket-Generika-System für eine imperative Sprache ansehen möchten, ist Ada meiner Meinung nach eines der besten, die Sie sich ansehen sollten. Es ist eine Schande, dass sie sich entschieden haben, auf mehrere Paradigmen umzusteigen und all das OO-Zeug zu Ada hinzuzufügen. Ada ist ein erweitertes Pascal, und Pascal war eine kleine und elegante Sprache. Pascal plus Ada-Generika wäre noch eine recht kleine Sprache gewesen, wäre aber meiner Meinung nach viel besser gewesen. Da sich der Fokus von Ada auf einen OO-Ansatz verlagert hat, scheint es schwierig zu sein, eine gute Dokumentation und Beispiele dafür zu finden, wie man die gleichen Dinge mit Generika macht.

Obwohl ich denke, dass Typklassen einige Vorteile haben, könnte ich mit Generika im Ada-Stil leben, aber es gibt ein paar Probleme, die mich davon abhalten, Ada breiter zu verwenden, ich denke, es setzt Werte/Objekte falsch (ich denke, sehr wenige Sprachen machen das richtig, 'C' ist einer der wenigen), es ist schwierig, mit Zeigern (Zugriffsvariablen) zu arbeiten und sichere Zeigerabstraktionen zu erstellen, und es bietet keine Möglichkeit, Pakete mit Laufzeitpolymorphismus zu verwenden (es bietet ein Objektmodell dafür, aber es fügt ein ganz neues Paradigma hinzu, anstatt zu versuchen, einen Weg zu finden, Laufzeitpolymorphismus mit Paketen zu haben).

Die Lösung für Laufzeitpolymorphismus besteht darin, Pakete erstklassig zu machen, damit Instanzen von Paketsignaturen als Funktionsargumente übergeben werden können. Dies erfordert leider abhängige Typen (siehe die Arbeit an abhängigen Objekttypen für Scala, um das Chaos zu beseitigen, das sie verursacht haben ihr ursprüngliches Typensystem).

Ich denke also, dass Paket-Generika funktionieren können, aber Ada brauchte Jahrzehnte, um mit allen Randfällen fertig zu werden, also würde ich mir ein Produktions-Generika-System ansehen, um zu sehen, welche Verfeinerungen in der produzierten Produktion verwendet werden. Ada ist jedoch immer noch unzureichend, da die Pakete nicht erstklassig sind und nicht in Laufzeitpolymorphismus verwendet werden können, und dies müsste angegangen werden.

@keean schrieb :

Persönlich halte ich die Laufzeitreflexion für eine Fehlfunktion, aber das bin nur ich ... Ich kann erklären, warum, wenn es jemanden interessiert.

Das Löschen von Typen ermöglicht „Theoreme for free“, was praktische Auswirkungen hat. Beschreibbare (und vielleicht sogar lesbare aufgrund transitiver Beziehungen zu imperativem Code?) Laufzeitreflexion macht es unmöglich, referenzielle Transparenz in irgendeinem Code zu garantieren, und daher sind bestimmte Compileroptimierungen nicht möglich und typsichere Monaden sind nicht möglich. Mir ist klar, dass Rust noch nicht einmal eine Unveränderlichkeitsfunktion hat. OTOH, Reflexion ermöglicht andere Optimierungen , die sonst nicht möglich wären, wenn sie nicht statisch typisiert werden könnten.

Ich hatte oben im Thread auch gesagt:

Und genau das würde ein Compiler, der aus einer Obermenge von Go mit hinzugefügten Generika transpiliert, als Go-Code ausgeben. Aber die Verpackung würde nicht auf einer Abgrenzung wie einer Verpackung basieren, da dies die bereits erwähnte Zusammensetzbarkeit verfehlen würde. Der Punkt ist, dass es keine Abkürzung zu einem guten zusammensetzbaren generischen Typsystem gibt. Entweder wir machen es richtig oder wir machen nichts, denn das Hinzufügen eines nicht komponierbaren Hacks, der nicht wirklich generisch ist, wird schließlich eine Clusterfuck-Trägheit von Patchwork, halbherziger Generizität und Unregelmäßigkeit von Eckfällen und Problemumgehungen erzeugen, die Go-Ökosystem-Code machen unverständlich.


@keean schrieb:

[…] Damit ein Paket anwendbar ist, muss es rein sein (d. h. keine veränderlichen Variablen auf Paketebene).

Und es dürfen keine unreinen Funktionen verwendet werden, um unveränderliche Variablen zu initialisieren.

@egonelbre schrieb:

Also, ja, Sie können ein parametrisiertes Paket in einem anderen verwenden.

Was ich anscheinend im Sinn hatte, waren „erstklassige parametrisierte Pakete“ und der entsprechende Laufzeitpolymorphismus (auch bekannt als dynamisch), den @keean später erwähnt hat, weil ich annahm, dass die parametrisierten Pakete anstelle von Typklassen oder OOP vorgeschlagen wurden.

EDIT: aber es gibt zwei mögliche Bedeutungen für „erstklassige“ Module: Module als erstklassige Werte wie in Successor ML und MixML unterschieden von Modulen als erstklassige Werte mit erstklassigen Typen wie in 1ML und der notwendige Kompromiss in Modulrekursion (dh Mischen ) zwischen ihnen.

@keean schrieb:

Die Lösung für Laufzeitpolymorphismus besteht darin, Pakete erstklassig zu machen, damit Instanzen von Paketsignaturen als Funktionsargumente übergeben werden können. Dies erfordert leider abhängige Typen (siehe die Arbeit an abhängigen Objekttypen für Scala, um das Chaos zu beseitigen, das sie verursacht haben ihr ursprüngliches Typensystem).

Was meinst du mit abhängigen Typen? (EDIT: Ich nehme an, er meinte jetzt "nicht-wertabhängige" Typisierung, dh " Funktionen, deren Ergebnistyp vom [Laufzeit?]-Argument ['s Typ] abhängt ") Sicherlich nicht abhängig von den Werten von beispielsweise int -Daten, wie in Idris. Ich denke, Sie beziehen sich auf das abhängige Eingeben (dh Verfolgen) des Typs der Werte, die instanziierte Modulinstanzen in der Aufrufhierarchie darstellen, damit solche polymorphen Funktionen zur Kompilierzeit monomorphisiert werden können? Tritt der Laufzeitpolymorphismus ein, weil solche monomorphisierten Typen der existenzielle Typ sind, der für dynamische Typen gebunden ist? F-ing-Module zeigten, dass „abhängige“ Typen nicht unbedingt erforderlich sind, um ML-Module im System F ω zu modellieren. Habe ich zu stark vereinfacht, wenn ich annehme, dass @rossberg das Typisierungsmodell neu formuliert hat, um alle Monomorphisierungsanforderungen zu entfernen?

Das Problem mit dem generativen Paketansatz ist, dass er viele Boilerplates erzeugt […]
Typklassen vermeiden diesen Textbaustein, indem sie die Typklasse implizit nur basierend auf den in der Funktion verwendeten Typen auswählen.

Gibt es nicht auch eine Boilerplate mit applikativen ML-Funktoren? Es gibt keine bekannte Vereinheitlichung von Typklassen und ML-Funktoren (Modulen), die die Kürze beibehält, ohne Beschränkungen einzuführen , die notwendig sind, um die inhärente Anti-Modularität des globalen Eindeutigkeitskriteriums von Typklassen-Implementierungsinstanzen zu verhindern (vgl. auch ).

Typklassen können jeden Typ nur auf eine Weise implementieren und benötigen ansonsten newtype Wrapper-Boilerplate, um die Einschränkung zu überwinden. Hier ist ein weiteres Beispiel für mehrere Möglichkeiten, einen Algorithmus zu implementieren. Afaics, @keean hat diese Einschränkung in seinem Typeclass-Sortierungsbeispiel umgangen, indem er die implizite Auswahl mit einem explizit ausgewählten Relation überschrieben hat, indem er data -Typen verwendet, um verschiedene Beziehungen generisch für den Werttyp zu benennen, aber ich bezweifle ob solche Taktiken allgemein für alle Varianten der Modularität gelten. Eine allgemeinere Lösung (die bei der Verbesserung des Modularitätsproblems der globalen Eindeutigkeit möglicherweise in Kombination mit einer Waisenbeschränkung als Verbesserung der vorgeschlagenen Versionierung für die Auflösung von Waisen helfen kann, indem eine Nicht-Standardeinstellung für Implementierungen verwendet wird, die verwaist sein könnten) könnte jedoch darin bestehen, eine zu haben interface , der, wenn er nicht angegeben ist, standardmäßig die normale implizite Übereinstimmung verwendet, aber wenn er angegeben ist (oder wenn er nicht angegeben ist, mit keinem anderen übereinstimmt 2 ), dann die Implementierung auswählt, die denselben Wert hat in seiner durch Kommas getrennten Liste benutzerdefinierter Werte (dies ist also ein allgemeineres, modulares Matching als das Benennen einer bestimmten implement -Instanz). Die durch Kommas getrennte Liste dient dazu, dass eine Implementierung in mehr als einem Freiheitsgrad unterschieden werden kann, z. B. wenn sie zwei orthogonale Spezialisierungen hat. Das gewünschte nicht standardmäßige spezialisierte kann entweder an der Funktionsdeklaration oder an der Aufrufstelle angegeben werden. Auf der Aufrufseite, zB f<non-default>(…) .

Warum brauchen wir also parametrisierte Module, wenn wir Typklassen haben? Afaics nur für (← wichtiger Link zum Anklicken) Substitution, da die Wiederverwendung von Typklassen für diese Zwecke nicht gut passt, da wir beispielsweise möchten, dass ein Paketmodul mehrere Dateien umfassen kann und wir in der Lage sein möchten, den Inhalt von implizit zu öffnen das Modul ohne zusätzliche Textbausteine ​​in den Anwendungsbereich aufnehmen . Daher ist es vielleicht ein vernünftiger erster Schritt, mit einer Paketparametrisierung nur _syntaktisch_ nur mit Substitution (nicht erstklassig) fortzufahren, die die Generizität auf Modulebene ansprechen kann, während sie offen für Kompatibilität mit und Nichtüberschneidung von Funktionalität bleibt, wenn Typklassen später für Funktionsebene hinzugefügt werden Generizität. Es gibt Makros beispielsweise typisiert sind oder nur eine syntaktische (auch als „Präprozessor“ bezeichnete) Substitution sind. Wenn sie typisiert sind, duplizieren Module die Funktionalität von Typklassen, was sowohl vom Standpunkt der Minimierung der sich überschneidenden Paradigmen/Konzepte der PL als auch von möglichen Eckfällen aufgrund von Wechselwirkungen der Überlappung (z. B. beim Versuch , sowohl ML-Funktoren als auch Typklassen anzubieten) unerwünscht ist ). Typisierte Module sind modularer, da Änderungen an gekapselten Implementierungen innerhalb des Moduls, die die exportierten Signaturen nicht ändern, nicht dazu führen können, dass Verbraucher des Moduls inkompatibel werden (mit Ausnahme des oben erwähnten Anti-Modularitätsproblems von Typklassen überlappenden Implementierungsinstanzen). Ich bin daran interessiert, die Gedanken von @keean dazu zu lesen.

[…] mit Ausnahmen für polymorphe Rekursion und existentielle Typen (bei denen es sich im Wesentlichen um Varianten handelt, aus denen Sie nicht austreten können, Sie können nur die Schnittstellen verwenden, die die Variante bestätigt).

Um anderen Lesern zu helfen. Unter „polymorpher Rekursion“ versteht man meiner Meinung nach höherrangige Typen, z. B. parametrisierte Callbacks, die zur Laufzeit gesetzt werden, wobei der Compiler den Rumpf der Callback-Funktion nicht monomorphisieren kann, weil er zur Kompilierzeit nicht bekannt ist. Die existenziellen Typen sind, wie ich bereits erwähnt habe, äquivalent zu den Trait-Objekten von Rust, die eine Möglichkeit darstellen, heterogene Container mit einer späteren Bindung im Ausdrucksproblem als die virtuelle Vererbung class zu erreichen, aber nicht so offen für Erweiterungen im Ausdruck Problem als Vereinigungen mit unveränderlichen Datenstrukturen oder Kopieren von 3 , die einen Leistungsaufwand von O(log n) haben.

1 Was HKT im obigen Beispiel nicht erfordert, da SET nicht erfordert, dass der Typ elem ein Typparameter des generischen Typs von set ist, dh es ist nicht set<elem> .

2 Wenn jedoch mehr als eine Nicht-Standardimplementierung und keine Standardimplementierung vorhanden wäre, wäre die Auswahl mehrdeutig, sodass der Compiler einen Fehler generieren sollte.

3 Beachten Sie, dass das Mutieren mit unveränderlichen Datenstrukturen nicht unbedingt das Kopieren der gesamten Datenstruktur erfordert, wenn die Datenstruktur intelligent genug ist, um den Verlauf wie eine einfach verknüpfte Liste zu isolieren.

Die Implementierung func pick(a CollectionOfT, count uint) []T wäre eine gute Beispielanwendung für Generika (von https://github.com/golang/go/issues/23717):

// pick returns a slice (len = n) of pseudorandomly chosen elements 
// in unspecified order from c which is an array, slice, or map.
for i, e := range pick(c, n) {

Der Interface{}-Ansatz hier ist kompliziert.

Ich habe einige Male zu diesem Thema kommentiert, dass eines der Hauptprobleme des C++-Template-Ansatzes darin besteht, dass er sich auf die Auflösung von Überladungen als Mechanismus für die Metaprogrammierung zur Kompilierzeit verlässt.

Es scheint, dass Herb Sutter zu demselben Schluss gekommen ist: Es gibt jetzt einen interessanten Vorschlag für die Kompilierzeit-Programmierung in C++ .

Es hat einige Elemente sowohl mit dem Paket Go reflect als auch mit meinem früheren Vorschlag für Funktionen zur Kompilierzeit in Go gemeinsam.

Hallo.
Ich habe einen Vorschlag für Generika mit Einschränkungen für Go geschrieben. Sie können es hier lesen. Vielleicht kann es als Dokument von 15292 hinzugefügt werden. Es handelt hauptsächlich von Einschränkungen und liest sich als Ergänzung zu Taylors Typparametern in Go .
Es ist als Beispiel für eine praktikable (glaube ich) Möglichkeit gedacht, "typsichere" Generika in Go zu erstellen - hoffentlich kann es etwas zu dieser Diskussion beitragen.
Bitte beachten Sie, dass ich, obwohl ich (den größten Teil) dieses sehr langen Threads gelesen habe, nicht allen darin enthaltenen Links gefolgt bin, sodass andere möglicherweise ähnliche Vorschläge gemacht haben. Wenn das der Fall ist, entschuldige ich mich.

Br. Chr.

Syntax bikeshedding:

constraint[T] Array {
    :[#]T
}

könnte sein

type [T] Array constraint {
    _ [...]T
}

was eher wie Go to me aussieht. :-)

Hier mehrere Elemente.

Eine Sache ist, : durch _ und # durch ... zu ersetzen.
Ich nehme an, Sie könnten das tun, wenn es bevorzugt wird.

Eine andere Sache ist das Ersetzen constraint[T] Array durch type[T] Array constraint .
Das scheint darauf hinzudeuten, dass Einschränkungen Typen sind, was ich nicht für richtig halte. Formal ist eine Einschränkung ein _Prädikat_ auf der Menge aller Typen, dh. eine Zuordnung von der Menge der Typen zur Menge { true , false }.
Oder wenn Sie es vorziehen, können Sie sich eine Einschränkung einfach als _eine Reihe von_ Typen vorstellen.
Es ist nicht _ein_ Typ.

Br. Chr.

Warum ist das constraint nicht nur ein interface ?

type [T io.Writer] List struct { 
    element T; 
    next *List[T];
}

Eine Schnittstelle wäre als Einschränkung mit dem folgenden Vorschlag etwas nützlicher: #23796, was wiederum auch dem Vorschlag selbst einen gewissen Wert verleihen würde.

Wenn der Vorschlag für Summentypen in irgendeiner Form akzeptiert wird (#19412), dann sollten diese verwendet werden, um den Typ einzuschränken.

Obwohl ich das Schlüsselwort Constraint glaube, sollte etwas Ähnliches hinzugefügt werden, um große Constraints nicht zu wiederholen und Fehler aufgrund von Zerstreutheit zu vermeiden.

Schließlich denke ich, dass für den Teil des Fahrradabwurfs Einschränkungen am Ende einer Definition aufgeführt werden sollten, um eine Überfüllung zu vermeiden (Rost scheint hier eine gute Idee zu haben):

// similar to the map[T]... syntax
// also no constraint
type List[T] struct {
    element T
    next *List[T]
}

// with constraint
type List[T] struct {
    element T
    next *List[T]
} where T is io.Writer | encoding.BinaryMarshaler

type BigConstraint constraint {
     io.Writer
     SomeFunc() int
     AnotherFunc()
     AField int64
     StringField string
}


// with predefined constraint
type List[T, U] struct {
    element T
    val U
    next *List[T, U]
} where T is BigConstraint | encoding.BinaryMarshaler,
    U is io.Reader

@urandom : Ich denke, es ist ein großer Vorteil für go, Schnittstellen implizit statt explizit zu implementieren. Der Vorschlag von @surlykke in diesem Kommentar ist meiner Meinung nach viel näher an der anderen Go-Syntax im Geiste.

@surlykke Ich entschuldige mich, wenn der Vorschlag die Antwort auf eine dieser Fragen enthält.

Eine Verwendung von Generika besteht darin, eingebaute Stilfunktionen zuzulassen. Wie würden Sie damit Len auf Anwendungsebene implementieren? Das Speicherlayout ist für jede zulässige Eingabe unterschiedlich, also wie ist das besser als eine Schnittstelle?

Das zuvor beschriebene „Pick“ hat ein ähnliches Problem, bei dem die Indizierung in eine Map und die Indizierung in einen Slice unterschiedlich sind. Wenn im Kartenfall zuerst eine Konvertierung in Slice erfolgte, kann derselbe Kommissioniercode verwendet werden, aber wie wird dies durchgeführt?

Sammlungen ist eine weitere Verwendung:

// An unordered collection of comparable items.
type [T Comparable] Set []T

func (a Set) Diff(from Set) Set {
    // the implementation is the same as one with
    //     type Comparable interface { Equal(Comparable) bool }
    //     type Set []Comparable
}

// compile error
d := Set[int]{1, 2}.Diff(Set[string]{“abc”, “def”})

// Go 1, easier to read but runtime error
d := Set{1, 2}.Diff(Set{“abc”, “def”})

Für den Sammlungstyp bin ich nicht überzeugt, dass dies ein großer Gewinn gegenüber Go 1-Generika ist, da es Kompromisse bei der Lesbarkeit gibt.

Ich stimme zu, dass Typparameter irgendeine Form von Einschränkungen haben müssen. Andernfalls werden wir die Fehler von C++-Vorlagen wiederholen. Die Frage ist, wie aussagekräftig sollten die Einschränkungen sein?

An einem Ende könnten wir einfach Schnittstellen verwenden. Aber wie Sie betonen, können viele nützliche Muster auf diese Weise nicht erfasst werden.

Dann gibt es Ihre Idee und ähnliche, die versuchen, eine Reihe nützlicher Einschränkungen herauszuarbeiten und eine neue Syntax für deren Ausdruck bereitzustellen. Abgesehen von dem Problem, noch mehr Syntax hinzuzufügen, ist nicht klar, wo man aufhören soll. Wie Sie betonen, erfasst Ihr Vorschlag viele Muster, aber keineswegs alle.

Das andere Extrem ist die Idee, die ich in diesem Dokument vorschlage. Es verwendet Go-Code selbst als Constraint-Sprache. Auf diese Weise können Sie praktisch jede Einschränkung erfassen, und es ist keine neue Syntax erforderlich.

@jba
Es ist ein bisschen ausführlich. Wenn Go eine Lambda-Syntax hätte, wäre es vielleicht etwas schmackhafter. Andererseits scheint das größte Problem, das es zu lösen versucht, zu prüfen, ob ein Typ einen Operator irgendeiner Art unterstützt. Es könnte einfacher sein, wenn Go nur vordefinierte Schnittstellen für verschiedene Operatoren hätte:

func equal[T](x, y T) bool
    where T is runtime.Equitable {
    return x == y
}

func copyable[T](x, y []T) int {
    return copy(x, y)
}

oder so ähnlich.

Wenn das Problem beim Erweitern von Builtins liegt, liegt das Problem möglicherweise in der Art und Weise, wie die Sprache Adaptertypen erstellt. Ist zum Beispiel das mit sort.Interface verbundene Aufblähen nicht der ganze Grund hinter https://github.com/golang/go/issues/16721 und sort.Slice?
Wenn Sie sich https://github.com/golang/go/issues/21670#issuecomment -325739411 ansehen, könnte die Idee von @Sajmani , Schnittstellenliterale zu haben, die Zutat sein, die erforderlich ist, damit Typparameter problemlos mit integrierten Funktionen arbeiten können.
Schauen Sie sich die folgende Definition von Iterator an:

type [T] Iterator interface {
    Next() (elem T, done bool)
}

Wenn print eine Funktion ist, die einfach über eine Liste iteriert und ihren Inhalt ausgibt, dann verwendet das folgende Beispiel Schnittstellenliterale, um eine zufriedenstellende Schnittstelle für print zu erstellen.

func SliceIterator(slice []T) Iterator {
    i := 0
    return Iterator{
        Next: func() (elem int, done bool) {
            v := slice[i]
            if i+1 == len(slice) {
                return v, true
            }
            i++
            return v, false
        },
    }
}

func main() {
    arr := []int{1,2,3,4,5}
    // SliceIterator works for an arbitrary slice
    print(SliceIterator(arr))
}

Dies kann man bereits tun, wenn sie Typen global deklarieren, deren einzige Aufgabe es ist, eine Schnittstelle zu erfüllen. Diese Umwandlung von einer Funktion in eine Methode macht es jedoch einfacher, Schnittstellen (und damit "Einschränkungen") zu erfüllen. Wir verunreinigen Top-Level-Deklarationen nicht mit einfachen Adaptern (wie „widgetsByName“ beim Sortieren).
Benutzerdefinierte Typen können diese Funktion offensichtlich ebenfalls nutzen, wie dieses LinkedList-Beispiel zeigt:

type ListNode struct {
    v string
    next *ListNode
}
func (l *ListNode) Iterator() Iterator {
    ptr := l
    return Iterator{
        Next: func() (elem int, done bool) {
            v := ptr.v
            if ptr.next == nil {
                return v, true
            }
            ptr = ptr.next
            return v, false
        },
    }
}

@geovanisouza92 : Einschränkungen, wie ich sie beschrieben habe, sind ausdrucksstärker als Schnittstellen (Felder, Operatoren). Ich habe kurz darüber nachgedacht, Schnittstellen zu erweitern, anstatt Beschränkungen einzuführen, aber ich denke, das wäre eine viel zu aufdringliche Änderung an einem bestehenden Element von Go.

@pciet Ich bin mir nicht ganz sicher, was Sie mit "Anwendungsebene" meinen. Go hat eine eingebaute len -Funktion, die auf Arrays, Zeiger auf Arrays, Slices, Strings und Kanäle angewendet werden kann. Wenn also in meinem Vorschlag ein Typparameter darauf beschränkt ist, einen von diesen als zugrunde liegenden Typ zu haben , len kann darauf angewendet werden.

@pciet Über Ihr Beispiel mit Comparable Einschränkung/Schnittstelle. Beachten Sie Folgendes, wenn Sie (die Schnittstellenvariante) definieren:

type Comparable interface { Equal(Comparable) bool }
type Set []Comparable

Dann können Sie alles, was Comparable implementiert, in Set . Vergleichen Sie das mit:

constraint [T] Comparable { Equal(t T) bool }
type [T Comparable[T]] Set []T
...
type FooSet Set[Foo] // Where Foo satisfies constraint Comparable

wo Sie nur Werte vom Typ Foo in FooSet können. Das ist stärkere Typensicherheit.

@urandom Nochmal, ich bin kein Fan von:

type MyConstraint constraint {....}

da ich nicht glaube, dass eine Einschränkung ein Typ ist. Außerdem würde ich definitiv nicht zulassen:

var myVar MyConstraint

was für mich keinen sinn macht. Ein weiterer Hinweis darauf, dass Constraints keine Typen sind.

@urandom On bikeshedding: Ich glaube, Einschränkungen sollten direkt neben den Typparametern deklariert werden. Stellen Sie sich eine gewöhnliche Funktion vor, die wie folgt definiert ist:

func MyFunc(i) {
     if (i>0) fmt.Println("It's positive")
} with i being an integer

Das konnte man nicht von links nach rechts lesen. Stattdessen würden Sie zuerst func MyFunc(i) lesen, um festzustellen, dass es sich um eine Funktionsdefinition handelt. Dann müssten Sie zum Ende springen, um herauszufinden, was i ist, und dann zurück zum Funktionskörper. Nicht ideal, IMO. Und ich sehe nicht, wie generische Definitionen anders sein sollten.
Aber offensichtlich ist diese Diskussion orthogonal zu der darüber, ob Go Einschränkungen oder Generika haben sollte.

@surlykke
Ich finde es gut, dass es kein Typ ist. Das Wichtigste ist, dass sie einen Namen haben, damit von mehreren Typen auf sie verwiesen werden kann.

Für Funktionen wäre es, wenn wir der Rust-Syntax folgen:

func MyFunc[I](i I) int64
     where I is being an integer {
   return 42
}

Es werden also Dinge wie der Name der Funktion oder ihre Parameter nicht ausgeblendet, und Sie müssten nicht bis zum Ende des Funktionskörpers gehen, um zu sehen, was die Einschränkungen für die generischen Typen sind

@surlykke für die Nachwelt, könnten Sie herausfinden, wo Ihr Vorschlag hinzugefügt werden könnte:
https://docs.google.com/document/d/1vrAy9gMpMoS3uaVphB32uVXX4pi-HnNjkMEgyAHX4N4

Es ist ein großartiger Ort, um alle Vorschläge zu "kompilieren".

Eine weitere Frage, die ich Ihnen allen stelle, ist, wie man mit der Spezialisierung verschiedener Instanziierungen eines generischen Typs umgehen würde. Im Type-Params- Vorschlag besteht die Möglichkeit darin, dieselbe Template-Funktion für jeden instanziierten Typ zu generieren und den Typparameter durch den Typnamen zu ersetzen. Um separate Funktionalität für verschiedene Typen zu haben, führen Sie einen Typwechsel am Typparameter durch.

Kann man davon ausgehen, dass der Compiler, wenn er einen Typwechsel für einen Typparameter sieht, für jede Behauptung eine separate Implementierung generieren darf? Oder ist das eine zu aufwendige Optimierung, da verschachtelte Typparameter in den behaupteten Strukturen einen parametrischen Aspekt für die Codegenerierung erzeugen können?

Da wir wissen, dass diese Deklarationen zur Kompilierzeit generiert werden, verursacht ein Typwechsel im Vorschlag für Funktionen zur Kompilierzeit keine Laufzeitkosten.

Ein praktisches Szenario: Wenn wir einen Fall des math/bits -Pakets betrachten, würde das Durchführen einer Typ-Assertion zum Aufrufen OnesCount für jedes uintXX den Punkt einer effizienten Bit-Manipulationsbibliothek übertreffen. Wenn jedoch die Typ-Assertionen in das Folgende umgewandelt wurden

func OnesCount(x T) int {
    switch x.(type) {
    case uint:
        // separate uint functionality...
    case uint8:
        // separate uint8 functionality...
    case uint16:
        // separate uint16 functionality...
    case uint32:
        // separate uint32 functionality...
    case uint64:
        // separate uint64 functionality...
    }
}

Ein Anruf bei

var x uint8 = 255
bits.OnesCount(x)

würde dann die folgende generierte Funktion aufrufen (Name ist hier nicht wichtig):

func $OnesCount_uint8(x uint8) {
    // separate uint8 functionality...
}

@jba Das ist ein interessanter Vorschlag, aber für mich unterstreicht er hauptsächlich die Tatsache, dass die Definition der parametrischen Funktion selbst normalerweise ausreicht, um ihre Einschränkungen zu definieren.

Wenn Sie „in einer Funktion verwendete Operatoren“ als Einschränkungen verwenden, welchen Vorteil bringt es Ihnen dann, eine zweite Funktion zu schreiben, die eine Teilmenge der in der ersten verwendeten Operatoren enthält?

@bcmills Eine davon ist eine Spezifikation und die andere die Implementierung. Es ist der gleiche Vorteil wie beim statischen Schreiben: Sie können Fehler früher abfangen.

Wenn es sich bei der Implementierung um die Spezifikation handelt, à la C++-Vorlagen, kann jede Änderung an der Implementierung möglicherweise abhängige Elemente beschädigen. Dies wird möglicherweise erst viel später entdeckt, wenn die Abhängigen neu kompilieren und die Entdecker keinen Kontext haben, um die Fehlermeldung zu verstehen. Mit der Spezifikation in derselben Verpackung können Sie den Bruch lokal erkennen.

@mandolyte Ich bin mir nicht ganz sicher, wo ich es hinzufügen soll - vielleicht ein Absatz unter "Generika-Ansätze" mit dem Namen "Generika mit Einschränkungen"?
Das Dokument scheint nicht viel über das Einschränken von Typparametern zu enthalten. Wenn Sie also einen Absatz hinzufügen, in dem mein Vorschlag erwähnt wird, könnten dort auch andere Ansätze für Einschränkungen aufgeführt werden.

@surlykke Der allgemeine Ansatz für das Dokument besteht darin, eine Änderung vorzunehmen, die sich richtig anfühlt, und ich werde versuchen, sie zu akzeptieren, zu integrieren und mit dem Rest des Dokuments zu organisieren. Ich habe hier einen Abschnitt hinzugefügt. Fühlen Sie sich frei, Dinge hinzuzufügen, die ich verpasst habe.

@egonelbre Das ist sehr schön. Danke!

@jba
Ich mag deinen Vorschlag, aber ich denke, er ist viel zu schwer für Golang. Es erinnert mich sehr an Templates in C++. Das Hauptproblem ist meiner Meinung nach, dass man damit wirklich komplexen Code schreiben kann.
Zu entscheiden, ob sich zwei generische Schnittstelleninstanzen überschneiden, weil sich der eingeschränkte Satz von Typen überschneidet, wäre eine schwierige Aufgabe, die langsamere Kompilierzeiten verursacht. Dasselbe gilt für die Codegenerierung.

Ich denke, dass die vorgeschlagenen Einschränkungen für unterwegs leichter sind. Soweit ich gehört habe, könnten Einschränkungen, auch Typklassen genannt, orthogonal zum Typsystem einer Sprache implementiert werden.

Ich muss ausdrücklich zustimmen, dass wir nicht mit impliziten Einschränkungen aus dem Körper der Funktion gehen sollten. Sie werden weithin als eine der wichtigsten Fehlfunktionen von C++-Vorlagen angesehen:

  • Die Einschränkungen sind nicht leicht sichtbar. Während godoc theoretisch alle Einschränkungen in der Dokumentation aufzählen könnte, sind sie im Quellcode nur implizit sichtbar.
  • Aus diesem Grund ist es möglich, versehentlich eine zusätzliche Einschränkung einzufügen, die nur sichtbar ist, wenn Sie versuchen, die Funktion auf eine unerwartete Weise zu verwenden. Indem eine explizite Spezifikation der Beschränkungen gefordert wird, muss der Programmierer genau wissen, welche Beschränkungen er einführt.
  • Es trifft die Entscheidung darüber, welche Arten von Beschränkungen erlaubt sind, viel eher ad-hoc. Darf ich zum Beispiel die folgende Funktion definieren? Was sind hier die tatsächlichen Einschränkungen für T, U und V? Wenn wir vom Programmierer verlangen, Einschränkungen explizit anzugeben, dann sind wir konservativ in der Art von Einschränkungen, die wir zulassen (und uns das langsam und bewusst erweitern lassen). Wenn wir versuchen, trotzdem konservativ zu sein, wie geben wir dann eine Fehlermeldung für eine solche Funktion aus? "Fehler: T kann uv() nicht zuweisen, da es eine unzulässige Einschränkung auferlegt"?
func[T, U, V] Foo(u U, v V) {
  var t T = u.v(V) + 1;
}
  • Das Aufrufen generischer Funktionen in anderen generischen Funktionen verschlimmert die oben genannten Situationen, da Sie jetzt alle Einschränkungen der aufgerufenen Personen durchsehen müssen, um die Einschränkungen der Funktion zu verstehen, die Sie schreiben oder lesen.
  • Das Debuggen kann sehr schwierig sein, da Fehlermeldungen entweder nicht genügend Informationen liefern müssen, um die Quelle der Einschränkung zu finden, oder interne Details der Funktion preisgeben müssen. Wenn zum Beispiel F eine Anforderung an einen Typ T hat und der Autor von F versucht herauszufinden, woher diese Anforderung stammt, möchte er, dass der Compiler dies tut Machen Sie sie darauf aufmerksam, welche Anweisung genau zu der Einschränkung führt (insbesondere, wenn sie von einem generischen Aufgerufenen stammt). Aber ein Benutzer von F möchte diese Informationen nicht, und tatsächlich, wenn sie in den Fehlermeldungen enthalten sind, dann geben wir Implementierungsdetails von F in Fehlermeldungen von seinen Benutzern preis, was sind eine schreckliche Benutzererfahrung.

@alercah

Darf ich zum Beispiel die folgende Funktion definieren?

func[T, U, V] Foo(u U, v V) {
  var t T = u.v(V) + 1;
}

Nein. u.v(V) ist ein Syntaxfehler, da V ein Typ ist und die Variable t nicht verwendet wird.

Sie könnten jedoch diese Funktion definieren, die möglicherweise die von Ihnen beabsichtigte ist:

func[T, U, V] Foo(u U, v V) {
    var _ T = u.v(v) + 1;
}

Was sind hier die tatsächlichen Einschränkungen für T, U und V?

  • Der Typ V ist uneingeschränkt.
  • Der Typ U muss eine Methode v haben, die einen einzelnen Parameter oder Varargs eines Typs akzeptiert, der von V zuweisbar ist, da u.v mit einem einzigen Argument aufgerufen wird vom Typ V .

    • U.v könnte ein Feld vom Funktionstyp sein, aber das sollte wohl eine Methode implizieren; siehe #23796.

  • Der von U.v zurückgegebene Typ muss numerisch sein, da ihm die Konstante 1 hinzugefügt wird.
  • Der Rückgabetyp von U.v muss T zuweisbar sein, da u.v(…) + 1 einer Variablen vom Typ T zugewiesen wird.
  • Der Typ T muss numerisch sein, da der Rückgabetyp von U.v numerisch ist und T werden kann.

(Nebenbei: Sie könnten argumentieren, dass U und V die Einschränkung „kopierbar“ haben sollten, da Argumente dieser Typen als Wert übergeben werden, aber das vorhandene, nicht generische Typsystem erzwingt dies nicht diese Einschränkung auch nicht. Das ist eine Angelegenheit für einen separaten Vorschlag.)

Wenn wir vom Programmierer verlangen, Einschränkungen explizit anzugeben, dann sind wir konservativ in der Art von Einschränkungen, die wir zulassen (und uns das langsam und bewusst erweitern lassen).

Ja, das stimmt, aber das Weglassen einer Einschränkung wäre ein schwerwiegender Fehler, unabhängig davon, ob diese Einschränkungen implizit sind oder nicht. Meiner Meinung nach besteht die wichtigere Rolle von Einschränkungen darin, Mehrdeutigkeiten aufzulösen. Beispielsweise muss der Compiler in den obigen Einschränkungen darauf vorbereitet sein, u.v entweder als Einzelargument- oder als variadische Methode zu instanziieren.

Die interessanteste Mehrdeutigkeit tritt bei Literalen auf, wo wir zwischen Strukturtypen und zusammengesetzten Typen unterscheiden müssen:

func[T] Foo() (t T) {
    x := 42;
    t = T{x: "some string"}  // Is x an index, or a field name?
    _ = x
}

Wenn wir versuchen, trotzdem konservativ zu sein, wie geben wir dann eine Fehlermeldung für eine solche Funktion aus? "Fehler: T kann uv() nicht zuweisen, da es eine unzulässige Einschränkung auferlegt"?

Ich bin mir nicht ganz sicher, was Sie fragen, da ich für dieses Beispiel keine widersprüchlichen Einschränkungen sehe. Was meinst du mit einer "illegalen Beschränkung"?

Das Debuggen kann sehr schwierig sein, da Fehlermeldungen entweder nicht genügend Informationen liefern müssen, um die Quelle der Einschränkung zu finden, oder interne Details der Funktion preisgeben müssen.

Nicht jede relevante Einschränkung kann durch das Typsystem ausgedrückt werden (siehe auch https://github.com/golang/go/issues/22876#issuecomment-347035323). Einige Beschränkungen werden durch Laufzeitpaniken erzwungen; einige werden vom Renndetektor erzwungen; die gefährlichsten Engpässe werden lediglich dokumentiert und gar nicht erkannt.

Alle diese „internen Details“ lassen bis zu einem gewissen Grad nach. (Siehe auch https://xkcd.com/1172/.)

Wenn zum Beispiel […] der Autor von F herauszufinden versucht, woher diese Anforderung stammt, möchte er, dass der Compiler ihn darauf hinweist, welche Anweisung genau zu der Einschränkung führt (insbesondere, wenn sie von einem generischen Aufgerufenen stammt). Aber ein Benutzer von F möchte diese Informationen nicht[.]

Könnte sein? Auf diese Weise verwenden API-Autoren Typannotationen in typabgeleiteten Sprachen wie Haskell und ML, aber es führt auch im Allgemeinen in ein Kaninchenloch tief parametrischer Typen („höherer Ordnung“).

Angenommen, Sie haben diese Funktion:

func [F, Arg, Result] InvokeAsync(f F, x Arg) (<-chan Result) {
    c := make(chan result, 1)
    go func() { c <- f(x) }()
    return c
}

Wie drücken Sie die expliziten Einschränkungen für den Typ Arg aus? Sie hängen von der spezifischen Instanziierung von F ab. Diese Art von Abhängigkeit scheint in vielen der jüngsten Vorschläge für Beschränkungen zu fehlen.

Nein. uv(V) ist ein Syntaxfehler, da V ein Typ ist und die Variable t nicht verwendet wird.

Sie könnten jedoch diese Funktion definieren, die möglicherweise die von Ihnen beabsichtigte ist:

Ja, das war die Absicht, entschuldige bitte.

Der Typ T muss numerisch sein, da der Rückgabetyp von U.v numerisch ist und T werden kann.

Sollten wir dies wirklich als Einschränkung betrachten? Es ist aus den anderen Einschränkungen ableitbar, aber ist es mehr oder weniger sinnvoll, dies als eindeutige Einschränkung zu bezeichnen? Implizite Constraints stellen diese Frage auf eine Weise, wie es explizite Constraints nicht tun.

Ja, das stimmt, aber das Weglassen einer Einschränkung wäre ein schwerwiegender Fehler, unabhängig davon, ob diese Einschränkungen implizit sind oder nicht. Meiner Meinung nach besteht die wichtigere Rolle von Einschränkungen darin, Mehrdeutigkeiten aufzulösen. Beispielsweise muss der Compiler in den obigen Einschränkungen darauf vorbereitet sein, uv entweder als Einzelargument- oder als Variadic-Methode zu instanziieren.

Ich meinte "Einschränkungen, die wir zulassen" wie in der Sprache. Mit expliziten Beschränkungen ist es für uns viel einfacher zu entscheiden, welche Art von Beschränkungen wir Benutzern erlauben zu schreiben, anstatt nur zu sagen, dass die Beschränkung „was auch immer Dinge kompilieren lässt“ ist. Zum Beispiel beinhaltet mein obiges Beispiel Foo tatsächlich einen impliziten zusätzlichen Typ getrennt von T , U oder V , da wir den Rückgabetyp von berücksichtigen müssen u.v . Auf diesen Typ wird in der Deklaration f in keiner Weise ausdrücklich verwiesen; die Eigenschaften, die es haben muss, sind völlig implizit. Sind wir ebenfalls bereit, Typen mit höherem Rang ( forall ) zuzulassen? Ich kann mir auf Anhieb kein Beispiel einfallen lassen, aber ich kann mich auch nicht davon überzeugen, dass Sie nicht implizit eine Typbindung mit höherem Rang schreiben können.

Ein weiteres Beispiel ist, ob wir zulassen sollten, dass eine Funktion eine überladene Syntax nutzt. Wenn eine implizit eingeschränkte Funktion for i := range t für einige t des generischen Typs T ausführt, funktioniert die Syntax, wenn T irgendein Array, Slice, Kanal, oder Karte. Aber die Semantik ist ganz anders, besonders wenn T ein Kanaltyp ist. Wenn beispielsweise t == nil (was passieren kann, solange T ein Array ist), dann macht die Iteration entweder nichts, da es keine Elemente in einem Null-Slice oder einer Null-Map gibt, oder blockiert für immer denn genau das tun Empfänger auf nil Kanälen. Dies ist eine große Fußwaffe, die darauf wartet, passiert zu werden. Ähnlich verhält es sich mit m[i] = ... ; Wenn ich beabsichtige, dass m eine Karte ist, muss ich mich davor hüten, dass es tatsächlich ein Slice ist, da der Code sonst bei einer Zuweisung außerhalb des Bereichs in Panik geraten könnte.

Tatsächlich denke ich, dass dies ein weiteres Argument gegen implizite Beschränkungen ist: API-Autoren könnten künstliche Anweisungen schreiben, nur um Beschränkungen hinzuzufügen. Zum Beispiel verhindert for _, _ := range t { break } einen Kanal, lässt aber weiterhin Maps, Slices und Arrays zu; x = append(x) erzwingt x den Slice-Typ. var _ = make(T, 0) erlaubt Slices, Maps und Channels, aber keine Arrays. Es wird ein Rezeptbuch zum impliziten Hinzufügen von Einschränkungen geben, damit jemand Ihre Funktion nicht mit einem Typ aufrufen kann, für den Sie keinen korrekten Code geschrieben haben. Ich kann mir nicht einmal vorstellen, Code zu schreiben, der nur für Kartentypen kompiliert, es sei denn, ich kenne auch den Schlüsseltyp. Und ich denke nicht, dass das überhaupt hypothetisch ist; Maps und Slices verhalten sich für die meisten Anwendungen recht unterschiedlich

Ich bin mir nicht ganz sicher, was Sie fragen, da ich für dieses Beispiel keine widersprüchlichen Einschränkungen sehe. Was meinst du mit einer "illegalen Beschränkung"?

Ich meine eine Einschränkung, die von der Sprache nicht zugelassen wird, z. B. wenn die Sprache entscheidet, höherrangige Einschränkungen zu verbieten.

Nicht jede relevante Einschränkung kann durch das Typsystem ausgedrückt werden (siehe auch #22876 (Kommentar)). Einige Beschränkungen werden durch Laufzeitpaniken erzwungen; einige werden vom Renndetektor erzwungen; die gefährlichsten Engpässe werden lediglich dokumentiert und gar nicht erkannt.

Alle diese „internen Details“ lassen bis zu einem gewissen Grad nach. (Siehe auch https://xkcd.com/1172/.)

Ich verstehe nicht wirklich, wie #22876 dazu kommt; das versucht, das Typensystem zu verwenden, um eine andere Art von Einschränkung auszudrücken. Es wird immer wahr sein, dass wir einige Beschränkungen für Werte oder Programme nicht ausdrücken können, selbst mit einem Typensystem beliebiger Komplexität. Aber wir sprechen hier nur über Einschränkungen für Typen . Der Compiler muss in der Lage sein, die Frage „Kann ich dieses Generikum mit dem Typ T instanziieren?“ zu beantworten. was bedeutet, dass es die Beschränkungen verstehen muss, ob sie implizit oder explizit sind. (Beachten Sie, dass einige Sprachen, wie C++ und Rust, diese Frage im Allgemeinen nicht entscheiden können, da sie von willkürlicher Berechnung abhängen kann und somit zum Halteproblem führt, aber sie drücken immer noch die Einschränkungen aus, die erfüllt werden müssen.)

Was ich meine, ist eher "Welche Fehlermeldung soll das folgende Beispiel geben?"

func [U] DirectlyConstrained(U t) {
    t.DoSomething();
}
func [T] IndirectlyConstrained(T t) {
    DirectlyConstrainted(t);
}
func Illegal() {
    IndirectlyConstrained(4);
}

Wir können Error: cannot call IndirectlyConstrained with [T = int]; T must have a method with signature func (T t) DoSomething() sagen. Diese Fehlermeldung ist hilfreich für einen Benutzer von IndirectlyConstrained , da sie die fehlenden Einschränkungen deutlich macht. Aber es liefert niemandem, der zu debuggen versucht, warum IndirectlyConstrained diese Einschränkung hat, was ein großes Usability-Problem ist, wenn es sich um eine große Funktion handelt. Wir könnten Note: this constraint exists on T because IndirectlyConstrained calls DirectlyConstrained with [U = T] on line N hinzufügen, aber jetzt geben wir Details zur Implementierung von IndirectlyConstrained preis. Darüber hinaus haben wir nicht erklärt, warum IndirectlyConstrained die Einschränkung hat, also fügen wir ein weiteres Note: this constraint exists on U because DirectlyConstrained calls t.DoSomething() on line M hinzu? Was ist, wenn die implizite Einschränkung von einem Aufgerufenen vier Ebenen weiter unten im Aufrufstapel stammt?

Wie formatieren wir außerdem diese Fehlermeldungen für Typen, die nicht explizit als Parameter aufgeführt sind? ZB wenn im obigen Beispiel IndirectlyConstrained DirectlyConstrained(t.U()) $ aufruft. Wie beziehen wir uns überhaupt auf den Typ? In diesem Fall könnten wir the type of t.U() sagen, aber der Wert ist nicht unbedingt das Ergebnis eines einzelnen Ausdrucks; es könnte über mehrere Anweisungen aufgebaut werden. Dann müssten wir entweder einen Ausdruck mit den richtigen Typen synthetisieren, um die Fehlermeldung einzufügen, einen, der nie im Code erscheint, oder wir müssten einen anderen Weg finden, darauf zu verweisen, der für die weniger klar wäre armer Anrufer, der gegen die Beschränkung verstoßen hat.

Wie drücken Sie die expliziten Einschränkungen für den Typ Arg aus? Sie hängen von der spezifischen Instanziierung von F ab. Diese Art von Abhängigkeit scheint in vielen der neueren Vorschläge für Einschränkungen zu fehlen.

Lassen Sie F und lassen Sie den Typ von f func (Arg) Result sein. Ja, es ignoriert variadische Funktionen, aber der Rest von Go tut es auch. Ein Vorschlag, Varargs funcs kompatiblen Signaturen zuweisbar zu machen, könnte separat gemacht werden.

In Fällen, in denen wir wirklich Typgrenzen höherer Ordnung benötigen, kann es sinnvoll sein, sie in Generics v1 aufzunehmen oder nicht. Explizite Einschränkungen zwingen uns, explizit zu entscheiden, ob wir Typen höherer Ordnung unterstützen wollen und wie. Die bisherige mangelnde Rücksichtnahme ist meines Erachtens ein Symptom dafür, dass Go derzeit keine Möglichkeit hat, auf Eigenschaften eingebauter Typen zu verweisen. Es ist eine allgemeine offene Frage, wie ein generisches System generische Funktionen über alle numerischen Typen oder alle ganzzahligen Typen zulassen wird, und die meisten Vorschläge haben sich nicht sehr darauf konzentriert.

Bitte bewerten Sie meine Generika-Implementierung in Ihrem nächsten Projekt
http://go-li.github.io/

Wir können Error: cannot call IndirectlyConstrained with [T = int]; T must have a method with signature func (T t) DoSomething() sagen. Diese Fehlermeldung […] liefert jemandem, der zu debuggen versucht, nicht, warum IndirectlyConstrained diese Einschränkung hat, was ein großes Usability-Problem ist, wenn es sich um eine große Funktion handelt.

Ich möchte auf eine große Annahme hinweisen, die Sie hier machen: dass die Fehlermeldung von go build das _einzige_ Werkzeug ist, das dem Programmierer zur Verfügung steht, um das Problem zu diagnostizieren.

Um eine Analogie zu verwenden: Wenn Sie zur Laufzeit auf ein error stoßen, haben Sie mehrere Möglichkeiten zum Debuggen. Der Fehler selbst enthält nur eine einfache Meldung, die zur Beschreibung des Fehlers geeignet sein kann oder nicht. Aber das ist nicht die einzige Information, die Ihnen zur Verfügung steht: Sie haben zum Beispiel auch alle Protokollanweisungen, die das Programm ausgegeben hat, und wenn es sich um einen wirklich knallharten Fehler handelt, können Sie ihn in einen interaktiven Debugger laden.

Das heißt, das Debuggen zur Laufzeit ist ein interaktiver Prozess. Warum also sollten wir nicht-interaktives Debugging für Kompilierzeitfehler annehmen⸮ Als eine Alternative könnten wir dem guru -Tool Type Constraints beibringen. Dann würde die Ausgabe des Compilers etwa so aussehen:

somefile.go:123: Argument `4` to DirectlyConstrained has type `int`,
    but DirectlyConstrained requires a type `T` with method `DoSomething()`.
    (For more detail, run `guru contraints path/to/somefile.go:#1033`.)

Das gibt dem Benutzer des generischen Pakets die Informationen, die er braucht, um die Seite mit dem sofortigen Aufruf zu debuggen, aber _auch_ gibt dem Paketbetreuer (und, was noch wichtiger ist, seiner Bearbeitungsumgebung!) einen Breadcrumb, um weitere Nachforschungen anzustellen.

Wir könnten Note: this constraint exists on T because IndirectlyConstrained calls DirectlyConstrained with [U = T] on line N hinzufügen, aber jetzt geben wir Details zur Implementierung von IndirectlyConstrained preis.

Ja, das meine ich damit, dass Informationen sowieso durchsickern. Sie können guru describe bereits verwenden, um einen Blick in eine Implementierung zu werfen. Sie können mit einem Debugger einen Blick in ein laufendes Programm werfen und nicht nur den Stack nachschlagen, sondern auch in beliebig niedrige Funktionen heruntersteigen.

Ich stimme absolut zu, dass wir wahrscheinlich irrelevante Informationen _standardmäßig_ verbergen sollten, aber das bedeutet nicht, dass wir sie absolut verbergen müssen.

Wenn eine implizit eingeschränkte Funktion für i := range t für einige t des generischen Typs T funktioniert, funktioniert die Syntax, wenn T irgendein Array, Slice oder Kanal ist , oder Karte. Aber die Semantik ist ganz anders, besonders wenn T ein Kanaltyp ist.

Ich denke, das ist das überzeugendere Argument für Typbeschränkungen, aber das erfordert nicht, dass explizite Beschränkungen auch nur annähernd so ausführlich sind wie das, was einige Leute vorschlagen. Um Aufrufseiten eindeutig zu machen, scheint es ausreichend zu sein, die Typparameter durch etwas näher an reflect.Kind . Wir müssen keine Operationen beschreiben, die bereits aus dem Code ersichtlich sind; Stattdessen müssen wir nur Dinge wie „ T ist ein Slice-Typ“ sagen. Das führt zu einer viel einfacheren Reihe von Einschränkungen:

  • ein Typ, der Indexoperationen unterliegt, muss als linear oder assoziativ gekennzeichnet werden,
  • ein Typ, der range -Operationen unterliegt, muss als nil-empty oder nil-blocking gekennzeichnet werden,
  • ein Typ mit Literalen muss mit Feldern oder Indizes gekennzeichnet werden, und
  • (vielleicht) muss ein Typ mit numerischen Operationen als Fest- oder Fließkomma gekennzeichnet werden.

Das führt zu einer viel engeren Beschränkungssprache, vielleicht so etwas wie:

TypeConstraint = "sliceable" | "map" | "chan" | "struct" | "integer" | "float" | "type"

mit Beispielen wie:

func[T:integer, U, V] Foo(u U, v V) {
    var _ T = u.v(v) + 1;
}
func [S:sliceable, T] append(s S, x ...T) S {
    dst := s
    if cap(s) - len(s) < len(x) {
        dst = make(S, len(s), nextSizeClass(cap(s)))
        copy(dst, s)
    }
    copy(dst[len(s):cap(s)], x)
    return dst[:len(s)+len(x)]
}

Ich habe das Gefühl, dass wir einen großen Schritt in Richtung benutzerdefinierter generischer Typen gemacht haben, indem wir Typ-Alias ​​eingeführt haben.
Typ-Alias ​​ermöglicht Super-Typen (Typen von Typen).
Wir können Typen bei using wie Werte behandeln.

Um die Erklärungen zu vereinfachen, können wir ein neues Codeelement hinzufügen, genre .
Die Beziehung zwischen Gattungen und Typen ist wie die Beziehung zwischen Typen und Werten.
Mit anderen Worten, ein Genre bedeutet eine Art von Typen.

Jede Art von Typ, mit Ausnahme von struct- und interface- und function-Arten, entspricht einem vorher deklarierten Genre.

  • Bool
  • Schnur
  • Int8, Uint8, Int16, Uint16, Int32, Uint32, Int64, Uint64, Int, Uint, Uintptr
  • Float32, Float64
  • Komplex64, Komplex128
  • Array, Slice, Map, Kanal, Pointer, UnsafePointer

Es gibt einige andere vordeklarierte Genres, wie z. B. Comaprable, Numeric, Integer, Float, Complex, Container usw. Wir können Type oder * verwenden, um das Genre aller Typen zu bezeichnen.

Die Namen aller eingebauten Genres beginnen alle mit einem Großbuchstaben.

Jede Struktur und jeder Schnittstellen- und Funktionstyp entspricht einem Genre.

Wir können auch benutzerdefinierte Genres deklarieren:

genre Addable = Numeric | String
genre Orderable = Interger | Float | String
genre Validator = func(int) bool // each parameter and result type must be a specified type.
genre HaveFieldsAndMethods = {
    width  int // we must use a specific type to define the fields.
    height int // we can't use a genre to define the fields.
    Load(v []byte) error // each parameter and result type must be a specified type.
    DoSomthing()
}
genre GenreFromStruct = aStructType // declare a genre from a struct type
genre GenreFromInterface = anInterfaceType // declare a genre from an interface type
genre GenreFromStructInterface = aStructType + anInterfaceType
genre ComparableStruct = HaveFieldsAndMethods & Comprable
genre UncomparableStruct = HaveFieldsAndMethods &^ Comprable

Um die folgende Erklärung konsistent zu machen, wird ein Genre-Modifikator benötigt.
Der Genre-Modifizierer wird mit Const bezeichnet. Beispielsweise:

  • Const Integer ist ein Genre (anders als Integer ) und seine Instanz muss ein konstanter Wert sein, dessen Typ eine ganze Zahl sein muss. Der konstante Wert kann jedoch als besonderer Typ angesehen werden.
  • Const func(int) bool ist ein Genre (anders als func(int) bool ) und seine Instanz muss ein decarierter Funktionswert sein. Die Funktionsdeklaration kann jedoch als besonderer Typ betrachtet werden.

(Die Modifikatorlösung ist etwas knifflig, vielleicht gibt es andere bessere Designlösungen.)

Okay, machen wir weiter.
Wir brauchen ein anderes Konzept. Einen guten Namen dafür zu finden ist nicht einfach,
Nennen wir es einfach crate .
Im Allgemeinen ist die Beziehung zwischen Kisten und Genres wie die Beziehung zwischen Funktionen und Typen.
Eine Kiste kann Typen als Parameter und Rückgabetypen annehmen.

Eine Kistendeklaration (angenommen, der folgende Code ist im Paket lib deklariert):

crate Example [T Float, S {width, height T}, N Const Integer] [*, *, *] {
    type MyArray [N]T

    func Add(a, b T) T {
        return a+b
    }

    type M struct {
        x T
        y S
    }

    func (m *M) Area() T {
        m.DoSomthing()
        return m.y.width * m.y.height
    }

    func (m *M) Perimeter() T {
        return 2 * Add(m.y.width, m.y.height)
    }

    export M, Add, MyArray
}

Verwenden Sie die obige Kiste.

import "lib"

// We can use AddFunc as a normal delcared function.
// Its genre is "Const func (a, b T) T"
type Rect, AddFunc, Array = lib.Example[float32, struct{x, y float32}, 100]

func demo() {
    var r Rect
    a, p = r.Area(), r.Perimeter()
    _ = AddFunc(a, p)
}

Meine Ideen absorbieren viele der oben gezeigten Ideen anderer.
Sie sind jetzt sehr unreif.
Ich poste sie hier, nur weil ich finde, dass sie interessant sind,
und ich will es nicht mehr verbessern.
So viele Gehirnzellen wurden getötet, indem die Löcher in den Ideen repariert wurden.
Ich hoffe, dass diese Ideen andere Gophers inspirieren können.

Was Sie „Genre“ nennen, heißt eigentlich „Art“ und ist in der Welt bekannt
funktionale Programmiergemeinschaft. Was Sie eine Kiste nennen, ist eine eingeschränkte
eine Art ML-Funktor.

Am Mittwoch, 4. April 2018, 12:41 Uhr schrieb dotaheor [email protected] :

Ich habe das Gefühl, dass wir mit der Einführung einen großen Schritt in Richtung benutzerdefinierter Generika gemacht haben
Alias ​​eingeben.
Typ-Alias ​​ermöglicht Super-Typen (Typen von Typen).
Wir können Typen bei using wie Werte behandeln.

Um die Erklärungen einfacher zu machen, können wir ein neues Codeelement namens genre hinzufügen.
Die Beziehung zwischen Gattungen und Typen ist wie die Beziehung zwischen Typen
und Werte.
Mit anderen Worten, ein Genre bedeutet eine Art von Typen.

Jede Art von Typ, außer Struktur- und Schnittstellen- und Funktionsarten,
entspricht einem vorher festgelegten Genre.

  • Bool
  • Schnur
  • Int8, Uint8, Int16, Uint16, Int32, Uint32, Int64, Uint64, Int, Uint,
    Uintptr
    & Float32, Float64
  • Komplex64, Komplex128
  • Array, Slice, Map, Kanal, Pointer, UnsafePointer

Es gibt einige andere vordeklarierte Genres, wie Comparable, Numeric,
Integer, Float, Complex, Container usw. Wir können Typ oder * verwenden
das Genre aller Art.

Die Namen aller eingebauten Genres beginnen alle mit einem Großbuchstaben.

Jede Struktur und jeder Schnittstellen- und Funktionstyp entspricht einem Genre.

Wir können auch benutzerdefinierte Genres deklarieren:

Genre Addierbar = Numerisch | Schnur
Gattung Bestellbar = Integer | Schwimmer | Schnur
genre Validator = func(int) bool // Jeder Parameter und Ergebnistyp muss ein bestimmter Typ sein.
Genre HaveFieldsAndMethods = {
width int // Wir müssen einen bestimmten Typ verwenden, um die Felder zu definieren.
height int // Wir können kein Genre verwenden, um die Felder zu definieren.
Load(v []byte) error // Jeder Parameter und Ergebnistyp muss ein bestimmter Typ sein.
Etwas tun ()
}
genre GenreFromStruct = aStructType // ein Genre aus einem Strukturtyp deklarieren
genre GenreFromInterface = anInterfaceType // ein Genre aus einem Schnittstellentyp deklarieren
genre GenreFromStructInterface = aStructType | anInterfaceType

Um die folgende Erklärung konsistent zu machen, wird ein Genre-Modifikator benötigt.
Der Genre-Modifikator wird mit Const bezeichnet. Beispielsweise:

  • Const Integer ist ein Genre und seine Instanz muss ein konstanter Wert sein
    welcher Typ muss eine Ganzzahl sein.
    Der konstante Wert kann jedoch als besonderer Typ angesehen werden.
  • Const func(int) bool ist ein Genre und seine Instanz muss delcared sein
    Funktionswert.
    Die Funktionsdeklaration kann jedoch als besonderer Typ angesehen werden.

(Die Modifikatorlösung ist etwas knifflig, vielleicht gibt es andere bessere Designs
Lösungen.)

Okay, machen wir weiter.
Wir brauchen ein anderes Konzept. Einen guten Namen dafür zu finden ist nicht einfach,
Nennen wir es einfach Kiste.
Im Allgemeinen ist die Beziehung zwischen Kisten und Genres wie die Beziehung
zwischen Funktionen und Typen.
Eine Kiste kann Typen als Parameter und Rückgabetypen annehmen.

Eine Crate-Deklaration (angenommen, der folgende Code ist in lib
Paket):

Kiste Beispiel [T Float, S {Breite, Höhe T}, N Const Integer] [*, *, *] {
Geben Sie MyArray [N]T ein

func Add(a, b T) T {
gib a+b zurück
}

// Ein Kisten-Scope-Genre. Kann nur in der Kiste verwendet werden.

// M ist eine Art von Genre G
Typ M Struktur {
xT
y S
}

func (m *M) Bereich() T {
m.DoSothing()
geben Sie meine Breite * meine Höhe zurück
}

func (m *M) Perimeter() T {
return 2 * Add(meineBreite, meineHöhe)
}

export M, Hinzufügen, MyArray
}

Verwenden Sie die obige Kiste.

importiere "lib"

// Wir können AddFunc als normale Delcared-Funktion verwenden.
Typ Rect, AddFunc, Array = lib.Example(float32, struct{x, y float32})

func demo() {
var r Rect
a, p = r.Fläche(), r.Umfang()
_ = AddFunc(a, p)
}

Meine Ideen absorbieren viele der oben gezeigten Ideen anderer.
Sie sind jetzt sehr unreif.
Ich poste sie hier, nur weil ich finde, dass sie interessant sind,
und ich will es nicht mehr verbessern.
So viele Gehirnzellen wurden getötet, indem die Löcher in den Ideen repariert wurden.


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/15292#issuecomment-378665695 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AGGWB78BrjN0BxRfroH-jRNy4mCXgSwCks5tlPfMgaJpZM4IG-xv
.

Ich habe das Gefühl, dass es einige Unterschiede zwischen Art und Genre gibt.

Übrigens, wenn eine Kiste nur einen Typ zurückgibt, können wir ihren Aufruf direkt als Typ verwenden.

package lib

// export a type
crate List [T *] * {
    type List struct {
        ...
    }

    export List
}

benutze es:

import "lib"

var l lib.List[int]

Es gäbe einige "Genre-Deduktion"-Regeln, genau wie die "Typ-Deduktion" im aktuellen System.

@dotaheor , @DemiMarie ist richtig. Ihr „Genre“-Konzept klingt genau wie das „Art“ aus der Typentheorie. (Ihr Vorschlag erfordert zufällig eine Subkinding-Regel, aber das ist nicht ungewöhnlich.)

Das Schlüsselwort genre in Ihrem Vorschlag definiert neue Arten als Superarten bestehender Arten. Das Schlüsselwort crate definiert Objekte mit „Kistensignaturen“, die eine Art sind, die keine Unterart von Type ist.

Als formales System scheint Ihr Vorschlag in etwa so zu sein:

Kiste ::= χ | ⋯
Geben Sie ::= τ | ein χ | int | bool | ⋯ | func(τ) | func(τ) τ | []τ | χ[τ₁, …]

CrateSig ::= [κ₁, …] ⇒ [κₙ, …]
Art ::= κ | exactly τ | kindOf κ | Map | Chan | ⋯ | Const κ | Type | CrateSig

Um eine typtheoretische Notation zu missbrauchen:

  • Lesen Sie „⊢“ als „beinhaltet“.
  • Lesen Sie „ k1k2 “ als „ k1 ist eine Unterart von k2 “.
  • Lesen Sie „:“ als „ist von Art“.

Dann sehen die Regeln in etwa so aus:

τ : exactly τ
exactly τkindOf exactly τ
kindOf exactly τType

τ : κ₁κ₁κ₂τ : κ₂

τ₁ : Typeτ₂ : TypekindOf exactly map[τ₁]τ₂Map
MapType

κ₁κ₂Const κ₁Const κ₂

[…]
(Und so weiter, für alle eingebauten Arten)


Typdefinitionen verleihen Arten, und die zugrunde liegenden Arten fallen zu den Arten der eingebauten Typen zusammen:

type τ₁ τ₂τ₂ : κτ₁ : kindOf κ

kindOf kindOf κkindOf κ
kindOf MapMap
[…]


genre definiert neue Untertypbeziehungen:
genre κ = κ₁ | κ₂κ₁κ
genre κ = κ₁ | κ₂κ₂κ

(Sie können Numeric und dergleichen in Form von | definieren.)

genre κ = κ₁ & κ₂ ∧ ( κ₃κ₁ ) ∧ ( κ₃κ₂ ) ⊢ κ₃κ


Die Kistenerweiterungsregel ist ähnlich:
type τₙ, … = χ[τ₁, …] ∧ ( χ : [κ₁, …] ⇒ [κₙ, …] ) ∧ ( τ₁ : κ₁ ) ∧ ⋯ ⊢ τₙ : κₙ

Dies alles spricht natürlich nur über die Arten. Wenn Sie daraus ein Typsystem machen wollen, brauchen Sie auch Typregeln. 🙂


Was Sie also beschreiben, ist eine ziemlich gut verstandene Form der Parametrisierung. Das ist schön, da es gut verstanden wird, aber enttäuschend, da es nicht hilft, die einzigartigen Probleme zu lösen, die Go einführt.

Die wirklich interessanten und knorrigen Probleme, die Go einführt, betreffen hauptsächlich die dynamische Typprüfung. Wie sollten Typparameter mit Typzusicherungen und Reflektion interagieren?

(Sollte es beispielsweise möglich sein, Schnittstellen mit Methoden parametrischer Typen zu definieren? Wenn ja, was passiert, wenn Sie zur Laufzeit einen Wert dieser Schnittstelle mit einem neuartigen Parameter bestätigen?)

In einem ähnlichen Zusammenhang, gab es eine Diskussion darüber, wie man Code über eingebaute und benutzerdefinierte Typen generisch macht? Zum Beispiel Code erstellen, der Bigints und primitive Integer verarbeiten kann?

In einem ähnlichen Zusammenhang, gab es eine Diskussion darüber, wie man Code über eingebaute und benutzerdefinierte Typen generisch macht? Zum Beispiel Code erstellen, der Bigints und primitive Integer verarbeiten kann?

Typklassenbasierte Mechanismen wie in Genus und Familia können dies effizient tun. Einzelheiten finden Sie in unserem PLDI 2015-Papier.

@DemiMarie
Ich denke, "Genre" == "Eigenschaftssatz".

[bearbeiten]
Vielleicht ist traits ein besseres Schlüsselwort.
Wir können sehen, dass jede Art auch ein Merkmalssatz ist.

Die meisten Merkmale sind nur für einen einzigen Typ definiert.
Aber ein komplexeres Merkmal kann eine Beziehung zwischen zwei Typen definieren.

[Bearbeiten 2]
Angenommen, es gibt zwei Merkmalssätze A und B, können wir die folgenden Operationen durchführen:

A + B: union set
A - B: difference set
A & B: intersection set

Die Eigenschaftsmenge eines Argumenttyps muss eine Obermenge der entsprechenden Parametergattung (eine Eigenschaftsmenge) sein.
Die Merkmalsmenge eines Ergebnistyps muss eine Teilmenge des entsprechenden Ergebnistyps (eine Merkmalsmenge) sein.

(MEINER BESCHEIDENEN MEINUNG NACH)

Dennoch denke ich, dass das erneute Binden von Typ-Aliasen der richtige Weg ist, um Generika zu Go hinzuzufügen. Es braucht keine große Änderung in der Sprache. Auf diese Weise verallgemeinerte Pakete können weiterhin in Go 1.x verwendet werden. Und es besteht keine Notwendigkeit, Einschränkungen hinzuzufügen, da dies möglich ist, indem der Standardtyp für den Typalias auf etwas festgelegt wird, das diese Einschränkungen bereits erfüllt. Und der wichtigste Aspekt der erneuten Bindung von Typaliasen ist, dass die integrierten zusammengesetzten Typen (Slices, Maps und Channels) nicht geändert und generalisiert werden müssen.

@dc0d

Wie sollten Typaliase Generika ersetzen?

@sighoya Rebinding Type Aliases können Generika ersetzen (nicht nur Type Aliases). Nehmen wir an, ein Paket führt einige Typenaliase auf Paketebene ein wie:

package likedlist

type T = interface{}

type LinkedList struct {
    // ...
}

Wenn Type Alias ​​Rebinding (und Compiler-Einrichtungen) bereitgestellt werden, ist es möglich, dieses Paket zum Erstellen verknüpfter Listen für verschiedene konkrete Typen anstelle einer leeren Schnittstelle zu verwenden:

package main

import (
    "likedlist"
)

type intLL = likedlist.LinkedList(likedlist.T = int)
type stringLL = likedlist.LinkedList(likedlist.T = string)

func main() {}

Wenn wir den Alias ​​als solchen verwenden, ist der folgende Weg sauberer.

// pkg.go
package pkg

type ListNode struct {
    prev, next *ListNode
    element    ?Element
}

func Add(x, y ?T) ?T {
    return x+y
}



// main.go
package main

import "pkg"

type intList = pkg.ListNode[Element=int]
func stringAdd = pkg.Add[T=string]

func main() {
}

@dc0d und wie genau würde das umgesetzt werden? Der Code ist nett, aber er sagt nichts darüber aus, wie er tatsächlich im Inneren funktioniert. Und wenn man sich die Geschichte der Generika-Vorschläge ansieht, ist es für Go sehr wichtig, nicht nur, wie es aussieht und sich anfühlt.

@dotaheor Das ist nicht kompatibel mit Go 1.x.

@creker Ich habe ein Tool (mit dem Namen goreuse ) implementiert, das diese Technik zum Generieren von Code verwendet und als Konzept für Type Alias ​​Rebinding geboren wurde.

Es ist hier zu finden. Es gibt ein 15-minütiges Video, das das Tool erklärt.

@dc0d , es funktioniert also ähnlich wie C++-Vorlagen, die spezialisierte Implementierungen generieren. Ich glaube nicht, dass es akzeptiert würde, da das Go-Team (und ehrlich gesagt ich und viele andere Leute hier) gegen etwas zu sein scheint, das C++-Vorlagen ähnlich ist. Es erhöht Binärdateien, verlangsamt die Kompilierung und wäre möglicherweise nicht in der Lage, sinnvolle Fehler zu erzeugen. Und darüber hinaus ist es nicht mit reinen Binärpaketen kompatibel, die Go unterstützt. Aus diesem Grund hat sich C++ dafür entschieden, Vorlagen in Header-Dateien zu schreiben.

@kreker

Es funktioniert also ähnlich wie C++-Vorlagen, die spezialisierte Implementierungen für jeden verwendeten Typ generieren.

Ich weiß es nicht (es ist ungefähr 16 Jahre her, seit ich C++ geschrieben habe). Aber nach deiner Erklärung scheint es so zu sein. Ich bin mir jedoch nicht sicher, ob oder wie sie gleich sind.

Ich glaube nicht, dass es akzeptiert würde, da das Go-Team (und ehrlich gesagt ich und viele andere Leute hier) gegen etwas zu sein scheint, das C++-Vorlagen ähnlich ist.

Sicherlich hat jeder hier gute Gründe für seine Präferenzen basierend auf seinen Prioritäten. Als erstes auf meiner Liste steht die Kompatibilität mit Go 1.x.

Es erhöht Binärdateien,

Es könnte.

verlangsamt die Kompilierung,

Ich bezweifle das sehr (wie es mit goreuse erlebt werden kann).

Und darüber hinaus ist es nicht mit reinen Binärpaketen kompatibel, die Go unterstützt.

Ich bin nicht sicher. Wird dies durch andere Möglichkeiten der Implementierung von Generika unterstützt?

möglicherweise nicht in der Lage, sinnvolle Fehler zu produzieren.

Dies könnte etwas mühsam sein. Dennoch passiert es zur Kompilierzeit und kann mit einigen Tools weitgehend kompensiert werden. Außerdem kann, wenn der Typalias, der als Typparameter für das Paket fungiert, eine Schnittstelle ist, einfach überprüft werden, ob er von dem konkret bereitgestellten Typ zuweisbar ist. Das Problem für primitive Typen wie int und string und Strukturen bleibt jedoch bestehen.

@dc0d

Ich denke ein bisschen darüber nach.
Außerdem ist es intern auf Schnittstellen festgelegt, in Ihrem Beispiel das 'T'

type T=interface{}

wird als veränderliche Typvariable behandelt, sollte aber ein Alias ​​für einen bestimmten Typ sein, dh eine konstante Referenz auf einen Typ.
Was Sie wollen, ist T Type, aber das würde die Einführung von Generika implizieren.

@sighoya Ich bin mir nicht sicher, ob ich verstehe, was du gesagt hast.

Es wird intern auf Schnittstellen aufgebaut

Nicht wahr. Wie in meinem ursprünglichen Kommentar beschrieben, ist es möglich, bestimmte Typen zu verwenden, die eine Einschränkung erfüllen. Beispielsweise kann der Aliastyp des Typparameters wie folgt deklariert werden:

type T = int

Und nur Typen mit dem Operator + (oder - oder * ; hängt davon ab, ob dieser Operator überhaupt im Hauptteil des Pakets verwendet wird) können als Typwert verwendet werden das sitzt in diesem Typparameter.

Es können also nicht nur Schnittstellen als Platzhalter für Typparameter verwendet werden.

dies würde jedoch die Einführung von Generika bedeuten.

Dies _ist_ eine Möglichkeit, Generika in der Go-Sprache selbst einzuführen/implementieren.

@dc0d

Um Polymorphismus bereitzustellen, verwenden Sie interface{}, da dies es ermöglicht, T später auf einen beliebigen Typ zu setzen.

Das Setzen von 'type T=Int' würde nicht viel bringen.

Wenn Sie sagen würden, dass „Typ T“ zuerst nicht deklariert/undefiniert ist, was später festgelegt werden kann, dann haben Sie so etwas wie Generika.

Das Problem dabei ist, dass 'T' das gesamte Modul/Paket enthält und nicht lokal für eine Funktion oder Struktur ist (okay, gut, vielleicht eine verschachtelte Typdeklaration in einer Struktur, auf die von außen zugegriffen werden kann).

Warum nicht stattdessen schreiben?:

fun<type T>(t T)

oder

fun[type T](t T)

Außerdem benötigen wir eine Art Inferenzmaschinerie, um die richtigen Typen abzuleiten, wenn eine generische Funktion oder Struktur zunächst ohne Typparameterspezialisierung aufgerufen wird.

@dc0d schrieb

Und nur Typen mit + Operator (oder - oder *; hängt davon ab, ob dieser Operator überhaupt im Hauptteil des Pakets verwendet wird) können als Typwert verwendet werden, der sich in diesem Typparameter befindet.

Können Sie das näher erläutern?

@sighoya

Um Polymorphismus bereitzustellen, verwenden Sie interface{}, da dies es ermöglicht, T später auf einen beliebigen Typ zu setzen.

Polymorphismus wird nicht durch kompatible Typen erreicht, wenn Typaliase neu gebunden werden. Die einzige tatsächliche Einschränkung ist der Hauptteil des generischen Pakets. Sie müssen mechanisch kompatibel sein.

Können Sie das näher erläutern?

Beispiel: Wenn ein Parametertyp-Alias ​​auf Paketebene wie folgt definiert ist:

package genericadd

type T = int

func Add(a, b T) T { return a + b }

Dann können praktisch alle numerischen Typen T zugewiesen werden, wie zum Beispiel:

package main

import (
    "genericadd"
)

var add = genericadd.Add(
    T = float64
)

func main() {
    var (
        a, b float64
    )

    println(add(a, b))
}

@dc0d

Ich bin mir jedoch nicht sicher, ob oder wie sie gleich sind.

Sie sind in dem Sinne gleich, dass sie, wie ich sehe, ziemlich identisch funktionieren. Für jede Klassenvorlagen-Instanziierung würde der Compiler eine eindeutige Implementierung generieren, wenn er zum ersten Mal die Verwendung der bestimmten Kombination aus Klassenvorlage und ihrer Parameterliste sieht. Dadurch wird die Binärgröße erhöht, da Sie jetzt mehrere Implementierungen derselben Klassenvorlage haben. Verlangsamt die Kompilierung, da der Compiler nun diese Implementierungen generieren und alle möglichen Überprüfungen durchführen müsste. Im Fall von C++ könnte die Verlängerung der Kompilierzeit enorm sein. Ihre Spielzeugbeispiele sind schnell, C++ aber auch.

Ich bin nicht sicher. Wird dies durch andere Möglichkeiten der Implementierung von Generika unterstützt?

Andere Sprachen haben damit kein Problem. Insbesondere C# ist mir am vertrautesten. Aber es verwendet Laufzeitcodegenerierung, die das Go-Team vollständig ausschließt. Java funktioniert auch, aber ihre Implementierung ist, gelinde gesagt, nicht die beste. Einige der ianlancetaylor-Vorschläge könnten nach meinem Verständnis nur binäre Pakete verarbeiten.

Das einzige, was ich nicht verstehe, ist, ob reine Binärpakete unterstützt werden müssen. Ich sehe nicht, dass sie in den Vorschlägen ausdrücklich erwähnt werden. Ich interessiere mich nicht wirklich für sie, aber trotzdem ist es eine Sprachfunktion.

Nur um mein Verständnis zu testen ... betrachten Sie dieses Repo von Copy/Paste-Algorithmen [ hier ]. Sofern Sie nicht "int" verwenden möchten, kann der Code nicht direkt verwendet werden. Es muss kopiert und eingefügt und geändert werden, damit es funktioniert. Und mit Modifikationen meine ich, dass jede Instanz von "int" in den Typ geändert werden muss, den Sie wirklich benötigen.

Der Typ-Alias-Ansatz würde die Änderungen einmal an beispielsweise T vornehmen und eine Zeile "type T int" einfügen. Dann müsste der Compiler T an etwas anderes binden, sagen wir float64.

Deswegen:
a) Ich würde argumentieren, dass es keine Verlangsamung des Compilers geben würde, es sei denn, Sie hätten diese Technik tatsächlich verwendet. Es ist also Ihre Wahl.
b) Angesichts des neuen vgo-Zeugs, wo mehrere Versionen desselben Codes verwendet werden können ... was bedeutet, dass es eine Methode geben muss, um die verwendeten Quellen außer Sichtweite zu verstauen, dann kann der Compiler sicherlich verfolgen, ob zwei Verwendungen der gleichen Neubindung verwendet werden und Duplikate vermieden werden. Ich denke also, dass Code Bloat das gleiche wäre wie bei aktuellen Copy/Paste-Techniken.

Es scheint mir, dass zwischen Typenaliasen und dem kommenden vgo die Grundlagen für diesen Ansatz für Generika fast vollständig sind ...

Es gibt einige "Unbekannte", die im Vorschlag [ hier ] aufgeführt sind. Es wäre also schön, es ein bisschen mehr zu konkretisieren.

@mandolyte Sie können eine weitere Indirektionsebene hinzufügen, indem Sie spezialisierte Typen in einen allgemeinen Container einschließen. Auf diese Weise kann Ihre Implementierung gleich bleiben. Der Compiler erledigt dann die ganze Magie. Ich denke, Ians Vorschlag für Typparameter funktioniert so.

Ich denke, der Benutzer muss zwischen Typlöschung und Monomorphisierung wählen können.
Letzteres ist der Grund, warum Rust kostenlose Abstraktionen anbietet. Gehen sollte auch.

Am Mo, 9. April 2018, 8:32 Uhr Antonenko Artem [email protected]
schrieb:

@mandolyte https://github.com/mandolyte können Sie eine weitere Ebene hinzufügen
Indirektion durch Einhüllen spezialisierter Typen in einen allgemeinen Container. Dass
wie Ihre Implementierung gleich bleiben kann. Der Compiler erledigt dann alles
die Magie. Ich denke, Ians Vorschlag für Typparameter funktioniert so.


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/15292#issuecomment-379735199 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AGGWB1v9h5kWmuHCBuoewTTSX751OHgrks5tm1TsgaJpZM4IG-xv
.

Mir scheint, dass in dieser Diskussion über den Kompromiss zwischen Modularität und Leistung eine verständliche Verwirrung herrscht. Die C++-Technik der erneuten Typüberprüfung und Instanziierung von generischem Code bei jedem Typ, für den er verwendet wird, ist schlecht für die Modularität, schlecht für Binärdistributionen und aufgrund von aufgeblähtem Code schlecht für die Leistung. Das Gute an diesem Ansatz ist, dass der generierte Code automatisch auf die verwendeten Typen spezialisiert wird, was besonders hilfreich ist, wenn es sich bei den verwendeten Typen um primitive Typen wie int handelt. Java übersetzt generischen Code homogen, zahlt jedoch einen Preis in der Leistung, insbesondere wenn der Code den Typ T[] verwendet.

Glücklicherweise gibt es ein paar Möglichkeiten, dies ohne die Nicht-Modularität von C++ und ohne vollständige Generierung von Laufzeitcode anzugehen:

  1. Generieren Sie spezialisierte Instanziierungen für primitive Typen. Dies könnte entweder automatisch oder durch eine Programmiereranweisung erfolgen. Ein gewisses Dispatching ist erforderlich, um auf die korrekte Instanziierung zuzugreifen, kann aber in das bereits benötigte Dispatching durch eine homogene Übersetzung gefaltet werden. Dies würde ähnlich wie C# funktionieren, erfordert jedoch keine vollständige Generierung von Laufzeitcode; Ein wenig zusätzliche Unterstützung könnte in der Laufzeit wünschenswert sein, um Dispatch-Tabellen einzurichten, wenn Code geladen wird.
  2. Verwenden Sie eine einzelne generische Implementierung, in der ein Array von T tatsächlich als Array eines primitiven Typs dargestellt wird, wenn T als primitiver Typ instanziiert wird. Dieser Ansatz, den wir in PolyJ, Genus und Familia verwendet haben, verbessert die Leistung im Vergleich zum Java-Ansatz erheblich, obwohl er nicht ganz so schnell ist wie eine vollständig spezialisierte Implementierung.

@dc0d

Polymorphismus wird nicht durch kompatible Typen erreicht, wenn Typaliase neu gebunden werden. Die einzige tatsächliche Einschränkung ist der Hauptteil des generischen Pakets. Sie müssen mechanisch kompatibel sein.

Typaliase sind der falsche Weg, da es sich um eine konstante Referenz handeln sollte.
Es ist besser, direkt 'T Type' zu schreiben, und dann sehen Sie, dass Sie tatsächlich Generika verwenden.

Warum Sie eine globale Typvariable 'T' für das gesamte Paket/Modul verwenden möchten, lokale Typvariablen in <> oder [] sind modularer.

@kreker

Insbesondere C# ist mir am vertrautesten. Aber es verwendet Laufzeitcodegenerierung, die das Go-Team vollständig ausschließt.

Für Referenztypen, aber nicht für Werttypen.

@DemiMarie

Ich denke, der Benutzer muss zwischen Typlöschung und Monomorphisierung wählen können.
Letzteres ist der Grund, warum Rust kostenlose Abstraktionen anbietet. Gehen sollte auch.

"Type Erasure" ist mehrdeutig, ich nehme an, Sie meinen Type Parameter Erasure, das, was Java bietet, was auch nicht ganz stimmt.
Java hat eine Monomorphisierung, aber es monomorphisiert (halb) ständig bis zur oberen Grenze in der generischen Einschränkung, die meistens Object ist.
Um Methoden und Felder anderer Typen bereitzustellen, wird die Obergrenze intern in Ihren entsprechenden Typ umgewandelt, was ziemlich hässlich ist.
Wenn das Valhalla- Projekt akzeptiert wird, werden sich die Dinge für Werttypen ändern, aber leider nicht für Referenztypen.

Go muss nicht den Java-Weg gehen, weil:

"Binärkompatibilität für kompilierte Pakete zwischen Releases ist nicht garantiert"

während dies in Java nicht möglich ist.

Mir scheint, dass in dieser Diskussion über den Kompromiss zwischen Modularität und Leistung eine verständliche Verwirrung herrscht. Die C++-Technik der erneuten Typüberprüfung und Instanziierung von generischem Code bei jedem Typ, für den er verwendet wird, ist schlecht für die Modularität, schlecht für Binärdistributionen und aufgrund von aufgeblähtem Code schlecht für die Leistung.

Von welcher Leistung redest du hier?

Wenn Sie mit „aufgeblähtem Code“ und „Leistung“ „Binärgröße“ und „Anweisungs-Cache-Druck“ meinen, dann ist das Problem ziemlich einfach zu lösen: Solange Sie die Debug-Informationen für jede Spezialisierung nicht übermäßig aufbewahren, können Sie das kollabieren Funktionen mit denselben Körpern in dieselbe Funktion zur Verbindungszeit (das sogenannte „Borland-Modell“ ). Dadurch werden Spezialisierungen für primitive Typen und Typen ohne Aufrufe von nicht-trivialen Methoden behandelt.

Wenn Sie mit „Code-Bloat“ und „Performance“ „Linker-Eingabegröße“ und „Linking-Zeit“ meinen, dann ist das Problem auch ziemlich einfach, wenn Sie bestimmte (vernünftige) Annahmen über Ihr Build-System treffen können. Anstatt jede Spezialisierung in jeder Kompilierungseinheit auszugeben, können Sie stattdessen eine Liste der benötigten Spezialisierungen ausgeben und das Build-System jede eindeutige Spezialisierung genau einmal vor dem Verknüpfen instanziieren lassen (das „Cfront-Modell“). IIRC, dies ist eines der Probleme, die C++-Module zu lösen versuchen.

Wenn Sie also nicht eine dritte Art von „Code-Bloat“ und „Performance“ meinen, die ich übersehen habe, scheint es, als würden Sie von einem Problem mit der Implementierung sprechen, nicht mit der Spezifikation: _solange die Implementierung das Debug nicht überbewahrt Informationen,_ sind die Leistungsprobleme ziemlich einfach zu beheben.


Das größere Problem für Go besteht darin, dass es, wenn wir nicht aufpassen, möglich wird, Typzusicherungen oder Reflektion zu verwenden, um zur Laufzeit eine neuartige Instanz eines parametrisierten Typs zu erzeugen, was keine Menge an Implementierungsklugheit erfordert – abgesehen von einem teuren Ganzen ‐Programmanalyse – kann beheben.

Das ist in der Tat ein Versagen der Modularität, aber es hat nichts mit Code-Bloat zu tun: Stattdessen kommt es von der Tatsache, dass die Typen von Go-Funktionen (und -Methoden) keinen ausreichend vollständigen Satz von Einschränkungen für ihre Argumente erfassen.

@sighoya

Für Referenztypen, aber nicht für Werttypen.

Nach dem, was ich gelesen habe, führt C# JIT eine Spezialisierung zur Laufzeit für jeden Werttyp und einmal für alle Referenztypen durch. Es gibt keine Spezialisierung zur Kompilierzeit (IL-Zeit). Aus diesem Grund wird der C#-Ansatz vollständig ignoriert - das Go-Team möchte sich nicht auf die Generierung von Laufzeitcode verlassen, da dies die Plattformen einschränkt, auf denen Go ausgeführt werden kann. Insbesondere dürfen Sie unter iOS keine Codegenerierung zur Laufzeit durchführen. Es funktioniert und ich habe tatsächlich einiges davon gemacht, aber Apple erlaubt es nicht im AppStore.

Wie hast du es gemacht?

Am Mo, 9. April 2018, 15:41 Uhr Antonenko Artem [email protected]
schrieb:

@sighoya https://github.com/sighoya

Für Referenztypen, aber nicht für Werttypen.

Soweit ich gelesen habe, führt C# JIT eine Spezialisierung zur Laufzeit für jeden Wert durch
Typ und einmal für alle Referenztypen. Es gibt keine Kompilierzeit
Spezialisierung. Aus diesem Grund wird der C#-Ansatz vollständig ignoriert - Go team
möchte sich nicht auf die Generierung von Laufzeitcode verlassen, da dies die Plattformen Go einschränkt
weiterlaufen kann. Insbesondere dürfen Sie unter iOS keine Codegenerierung durchführen
zur Laufzeit. Es funktioniert und ich habe tatsächlich einiges davon gemacht, aber Apple erlaubt es nicht
es im AppStore.


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/15292#issuecomment-379870005 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AGGWB-tslGeUSGXl2ZlEDLf0dCATUaYvks5tm7lvgaJpZM4IG-xv
.

@DemiMarie hat meinen alten Forschungscode gestartet, nur um sicherzugehen (dass die Forschung aus anderen Gründen eingestellt wurde). Wieder einmal führt mich der Debugger in die Irre. Ich weise eine Seite zu, schreibe einige Anweisungen dazu, mschütze sie mit PROT_EXEC und springe zu ihr. Unter Debugger funktioniert es. Ohne Debugger-App wird wie erwartet SIGKILL mit CODESIGN-Meldung im Absturzprotokoll angezeigt. Also auch ohne AppStore geht es nicht. Noch stärkeres Argument gegen die Generierung von Laufzeitcode, wenn iOS für Go wichtig ist.

Zunächst wäre es hilfreich, noch einmal über die 5 Programmierregeln von Rob Pike nachzudenken.

Zweitens (IMHO):

Bezüglich langsamer Kompilierung und Binärgröße, wie viele generische Typen werden in gängigen Anwendungstypen verwendet, die mit Go entwickelt werden (_n ist normalerweise klein_ aus Regel 3)? Sofern das Problem nicht eine hohe Kardinalität in konkreten Konzepten (hohe Anzahl von Typen) erfordert, kann dieser Overhead übersehen werden. Selbst dann würde ich argumentieren, dass etwas mit diesem Ansatz nicht stimmt. Bei der Implementierung eines E-Commerce-Systems definiert niemand einen separaten Typ für jede Art von Produkt und seine Variationen und möglicherweise die möglichen Anpassungen.

Ausführlichkeit ist eine gute Form der Einfachheit und Vertrautheit (z. B. in der Syntax), die die Dinge offensichtlicher und sauberer macht. Obwohl ich bezweifle, dass das Aufblähen des Codes mit Type Alias ​​Rebinding höher wäre, mag ich die vertraute Go-ish-Syntax und die damit einhergehende offensichtliche Ausführlichkeit. Eines der Ziele von Go ist es, leicht zu lesen zu sein (obwohl ich persönlich es relativ einfach und angenehm finde, darin zu schreiben).

Ich verstehe nicht, wie dies die Leistung beeinträchtigen kann, da zur Laufzeit nur konkrete begrenzte Typen verwendet werden, die zur Kompilierzeit generiert wurden. Es gibt keinen Laufzeit-Overhead.

Das einzige Problem mit Type Alias ​​Rebinding, das ich sehe, könnte die Binärverteilung sein.

@dc0d Leistungsschaden bedeutet normalerweise, dass der Anweisungscache aufgrund unterschiedlicher Implementierungen von Klassenvorlagen gefüllt wird. Wie genau das mit der realen Leistung zusammenhängt, ist eine offene Frage, mir sind keine Benchmarks bekannt, aber theoretisch ist es ein Problem.

Wie für binäre Größe. Es ist ein weiteres theoretisches Problem, das die Leute normalerweise ansprechen (wie ich es früher getan habe), aber wie der echte Code darunter leiden wird, ist wiederum eine offene Frage. Zum Beispiel könnte die Spezialisierung für alle Zeiger- und Schnittstellentypen gleich sein, denke ich. Eine Spezialisierung für alle Werttypen wäre jedoch einzigartig. Und dazu gehören auch Strukturen. Die Verwendung generischer Container zum Speichern ist üblich und würde zu einer erheblichen Aufblähung des Codes führen, da generische Containerimplementierungen nicht klein sind.

Das einzige Problem mit Type Alias ​​Rebinding, das ich sehe, könnte die Binärverteilung sein.

Hier bin ich mir noch nicht sicher. Muss der Generika-Vorschlag Nur-Binär-Pakete unterstützen oder wir könnten einfach erwähnen, dass Nur-Binär-Pakete keine Generika unterstützen. Es wäre viel einfacher, das ist sicher.

Wie bereits erwähnt, wenn man Debugging nicht unterstützen muss, kann man
können identische Template-Instanziierungen kombinieren.

Am Di, 10. April 2018, 5:46 Uhr Kaveh Shahbazian [email protected]
schrieb:

Zunächst wäre es hilfreich, über die 5 Programmierregeln von Rob Pike nachzudenken
https://users.ece.utexas.edu/%7Eadnan/pike.html noch einmal.

Zweitens (IMHO):

Über langsame Kompilierung und Binärgröße, wie viele generische Typen verwendet werden
allgemeine Arten von Anwendungen, die mit Go entwickelt werden ( n isnormalerweise klein aus Regel 3)? Es sei denn, das Problem erfordert ein hohes Maß an
Kardinalität in konkreten Konzepten (hohe Anzahl von Typen), die Overhead können
übersehen werden. Selbst dann würde ich behaupten, dass etwas daran nicht stimmt
sich nähern. Bei der Implementierung eines E-Commerce-Systems definiert niemand ein separates
Typ für jede Art von Produkt und seine Variationen und vielleicht das Mögliche
Anpassungen.

Ausführlichkeit ist eine gute Form der Einfachheit und Vertrautheit (zum Beispiel in
Syntax), was die Dinge offensichtlicher und sauberer macht. Wobei ich das bezweifle
Code-Bloat wäre mit Type Alias ​​Rebinding höher, ich mag das
vertraute Go-ish-Syntax und die damit einhergehende offensichtliche Ausführlichkeit. Einer von
Das Ziel von Go ist es, leicht lesbar zu sein (obwohl ich es persönlich finde
auch relativ einfach und angenehm zu schreiben).

Ich verstehe nicht, wie es die Leistung beeinträchtigen kann, weil nur zur Laufzeit
Es werden konkret begrenzte Typen verwendet, die bei generiert wurden
Kompilierzeit. Es gibt keinen Laufzeit-Overhead.

Das einzige Problem mit Type Alias ​​Rebinding, das ich sehe, könnte die Binärdatei sein
Verteilung.


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/15292#issuecomment-380040032 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AGGWB6aDfoHz2wbsmu8mCGEt652G_VE9ks5tnH9xgaJpZM4IG-xv
.

Die Instanziierungen müssen nicht einmal „identisch“ im Sinne von „die gleichen Argumente verwenden“ oder gar „Argumente mit demselben zugrunde liegenden Typ verwenden“ sein. Sie müssen nur nahe genug beieinander liegen, um zu demselben generierten Code zu führen. (Für Go impliziert das auch „die gleichen Zeigermasken“.)

@kreker

Nach dem, was ich gelesen habe, führt C# JIT eine Spezialisierung zur Laufzeit für jeden Werttyp und einmal für alle Referenztypen durch. Es gibt keine Spezialisierung zur Kompilierzeit (IL-Zeit).

Nun, das ist manchmal etwas kompliziert, weil ihr Bytecode gerade rechtzeitig interpretiert wird, bevor der Code ausgeführt wird. Die Codegenerierung erfolgt also vor der Ausführung des Programms, aber nach der Kompilierung. Sie haben also Recht im Sinne des vm, das während des Codes ausgeführt wird generiert wird.

Ich denke, dass das generische System von c# für Go in Ordnung wäre, wenn wir stattdessen Code zur Kompilierzeit generieren.
Laufzeitcodegenerierung im Sinne von c# ist mit go nicht möglich, da go keine vm ist.

@dc0d

Das einzige Problem mit Type Alias ​​Rebinding, das ich sehe, könnte die Binärverteilung sein.

Kannst du das etwas näher erläutern.

@sighoya Mein Fehler; Ich meinte nicht die Binärdistribution, sondern Binärpakete - von denen ich persönlich keine Ahnung habe, wie wichtig sie sind.

@creker Schöne Zusammenfassung! (MO) Sofern kein triftiger Grund gefunden wird, muss jede Form der Überladung der Go-Sprachkonstrukte vermieden werden. Ein Grund für Type Alias ​​Rebinding besteht darin, das Überladen integrierter zusammengesetzter Typen wie Slices oder Maps zu vermeiden.

Ausführlichkeit ist eine gute Form der Einfachheit und Vertrautheit (z. B. in der Syntax), die die Dinge offensichtlicher und sauberer macht. Obwohl ich bezweifle, dass das Aufblähen des Codes mit Type Alias ​​Rebinding höher wäre, mag ich die vertraute Go-ish-Syntax und die damit einhergehende offensichtliche Ausführlichkeit. Eines der Ziele von Go ist es, leicht zu lesen zu sein (obwohl ich persönlich es relativ einfach und angenehm finde, darin zu schreiben).

Ich bin mit dieser Vorstellung nicht einverstanden. Ihr Vorschlag wird Benutzer dazu zwingen, das Schwierigste zu tun, was Programmierern bekannt ist – Dinge zu benennen. Am Ende haben wir Code, der mit ungarischer Notation durchsetzt ist, was nicht nur schlecht aussieht, sondern auch unnötig ausführlich ist und Stottern verursacht. Darüber hinaus bringen auch andere Vorschläge eine Go-ish-Syntax ein und haben gleichzeitig diese Probleme nicht.

Es gibt drei Kategorien von Namen, die wir uns täglich ausdenken müssen:

  • Für Domänenentitäten/Logik
  • Programm-Workflow-Datentypen/Logik
  • Dienste/Schnittstellendatentypen/Logik

Wie oft war es einem Programmierer jemals gelungen, es zu vermeiden, irgendetwas in seinem/ihrem Code zu benennen?

Schwer oder nicht, es muss täglich gemacht werden. Und die meisten Hürden kommen von der Inkompetenz bei der Strukturierung einer Codebasis - nicht von den Schwierigkeiten des Benennungsprozesses selbst. Dieses Zitat – zumindest in seiner jetzigen Form – hat der Welt der Programmierung bisher einen großen Bärendienst erwiesen. Es versucht lediglich, die Wichtigkeit der Namensgebung hervorzuheben. Weil wir in unserem Code über Namen kommunizieren.

Und Namen werden so viel mächtiger, wenn sie eine Codestrukturierungspraxis begleiten; sowohl in Bezug auf das Code-Layout (eine Datei, Verzeichnisstruktur, Pakete/Module) als auch in Bezug auf Praktiken (Entwurfsmuster, Dienstabstraktionen – wie REST, Ressourcenverwaltung – gleichzeitige Programmierung, Zugriff auf die Festplatte, Durchsatz/Latenz).

Was Syntax und Ausführlichkeit betrifft, bevorzuge ich Ausführlichkeit gegenüber cleverer Prägnanz (zumindest im Kontext von Go) - auch hier soll Go leicht zu lesen sein, nicht unbedingt leicht zu schreiben (was ich seltsamerweise auch gut finde). .

Ich habe viele Erfahrungsberichte und Vorschläge gelesen, warum und wie man Generika in Go implementiert.

Stört es Sie, wenn ich versuche, sie tatsächlich in meinem Go-Interpreter gomacro zu implementieren?

Ich habe einige Erfahrung mit dem Thema, da ich in der Vergangenheit Generika zu zwei Sprachen hinzugefügt habe

  1. eine jetzt aufgegebene Sprache , die ich damals erstellt habe, als ich naiv war :) Sie wurde in C-Quellcode transpiliert
  2. Common Lisp mit meiner Bibliothek cl-parametric-types - es unterstützt auch teilweise und vollständige Spezialisierungen von generischen Typen und Funktionen

@ cosmos72 Es wäre ein schöner Erfahrungsbericht, einen Prototyp einer Technik zu sehen, die die Typsicherheit bewahrt.

Habe gerade angefangen daran zu arbeiten. Sie können den Fortschritt auf https://github.com/cosmos72/gomacro/tree/generics-v1 verfolgen

Im Moment beginne ich mit einer (leicht modifizierten) Mischung aus dem dritten und vierten Vorschlag von Ian, der unter https://github.com/golang/proposal/blob/master/design/15292-generics.md#Proposal aufgeführt ist

@cosmos72 Es gibt eine Zusammenfassung der Vorschläge unter dem Link unten. Gehört Ihre Mischung dazu?
https://docs.google.com/document/d/1vrAy9gMpMoS3uaVphB32uVXX4pi-HnNjkMEgyAHX4N4

Ich habe dieses Dokument gelesen, es fasst viele verschiedene Herangehensweisen an Generics durch verschiedene Programmiersprachen zusammen.

Im Moment gehe ich in Richtung der Technik der "Typspezialisierung", die von C++, Rust und anderen verwendet wird, möglicherweise mit ein wenig "parametrisierten Vorlagenbereichen", da die allgemeinste Go-Syntax für neue Typen type ( Foo ...; Bar ...) ist und ich erweitere es zu template[T1,T2...] type ( Foo ...; Bar ...) .
Außerdem halte ich die Tür für "eingeschränkte Spezialisierung" offen.

Ich möchte auch die "Spezialisierung polymorpher Funktionen" implementieren, dh dafür sorgen, dass die Spezialisierung automatisch von der Sprache an der Aufrufstelle abgeleitet wird, wenn sie nicht vom Programmierer angegeben wird, aber ich denke, die Implementierung kann etwas komplex sein. Wir werden sehen.

Die Mischung, auf die ich mich bezog, liegt zwischen https://github.com/golang/proposal/blob/master/design/15292/2013-10-gen.md und https://github.com/golang/proposal/blob/ master/design/15292/2013-12-type-params.md

Update: Um zu vermeiden, dass diese offizielle Go-Ausgabe über die ursprüngliche Ankündigung hinaus zugespammt wird, ist es wahrscheinlich besser, die gomacro-spezifische Diskussion in gomacro-Ausgabe Nr. 24 fortzusetzen: Generika hinzufügen

Update 2: erste Template-Funktionen kompiliert und erfolgreich ausgeführt. Siehe https://github.com/cosmos72/gomacro/tree/generics-v1

Nur fürs Protokoll, es ist möglich, meine Meinung (zu Generika und Type Alias ​​Rebinding) umzuformulieren:

Generics sollten als Compiler-Funktion (Codegenerierung, Vorlagen usw.) hinzugefügt werden, nicht als Sprachfunktion (Einmischung in das Typsystem von Go auf allen Ebenen).

@dc0d
Aber sind C++-Vorlagen nicht ein Compiler- und Sprachfeature?

@sighoya Das letzte Mal, als ich C++ professionell geschrieben habe, war ungefähr 2001. Also könnte ich mich irren. Aber vorausgesetzt, die Implikationen der Benennung sind korrekt - der "Vorlagen" -Teil - ja (oder eher nein); es könnte ein Compiler-Feature (und kein Sprachfeature) sein, begleitet von einigen Sprachkonstrukten, die höchstwahrscheinlich keine Sprachkonstrukte überladen, die am Typsystem beteiligt sind.

Ich unterstütze @dc0d. Wenn Sie es bedenken, wäre dieses Feature nichts anderes als ein integrierter Codegenerator.

Ja: Die Binärgröße kann und WIRD zunehmen, aber im Moment verwenden wir Codegeneratoren, die ziemlich gleich sind, aber als externes Feature. Wenn ich meine Vorlage erstellen muss als:

type BinaryTreeOfStrings struct {
    left, right *BinaryTreeOfStrings;
    string content;
}

// Its methods here

type BinaryTreeOfBigInts struct {
    left, right *BinaryTreeOfBigInts;
    uint64 content;
}

// AGAIN the same methods but different type

... Ich würde es ernsthaft begrüßen, wenn diese Funktion Teil des Compilers selbst wird, anstatt sie zu kopieren oder ein externes Tool zu verwenden.

Bitte beachten Sie:

  • Ja, der Endcode würde dupliziert. Genauso, als ob wir einen Generator benutzen würden. Und die Binärdatei wäre größer.
  • Ja, die Idee ist nicht originell, sondern von C++ entlehnt.
  • Ja, Funktionen von MyTypenichts mit Typ T (direkt oder indirekt) involviert, würde ebenfalls wiederholt werden. Das könnte optimiert werden (z. B. werden Methoden, die auf etwas vom Typ T verweisen – außer dem Zeiger auf das Nachrichtenempfangsobjekt – für jedes T generiert; Methoden, die Aufrufe von Methoden enthalten, die dies tun würden für jedes T generiert werden, werden auch rekursiv für jedes T generiert - während Methoden, bei denen ihre einzige Referenz auf T *T ist, und andere Methoden, die nur diese sicheren Methoden aufrufen und dieselben Kriterien erfüllen, könnten nur einmal durchgeführt werden). Wie auch immer, IMO ist dieser Punkt groß und weniger auf den Punkt gebracht: Ich wäre ziemlich glücklich, auch wenn diese Optimierung nicht existiert.
  • Typargumente sollten meiner Meinung nach explizit sein. Besonders wenn ein Objekt potenziell unendliche Schnittstellen erfüllt. Nochmal: ein Codegenerator.

Bisher in meinem Kommentar schlage ich vor, es so zu implementieren, wie es ist: als Compiler-unterstützter Codegenerator anstelle eines externen Tools.

Es wäre unglücklich, wenn Go der C++-Route folgen würde. Viele Leute betrachten den C++-Ansatz als ein Durcheinander, das Programmierer gegen die ganze Idee von Generika aufgebracht hat: Schwierigkeiten beim Debuggen, Mangel an Modularität, aufgeblähter Code. Alle „Code-Generator“-Lösungen sind eigentlich nur Makro-Ersetzungen – wenn Sie Code so schreiben wollen, warum brauchen wir dann überhaupt Compiler-Unterstützung?

@andrewcmyers Ich hatte diesen Vorschlag Type Alias ​​Rebinding, in dem wir nur normale Pakete schreiben und anstatt interface{} explizit zu verwenden, verwenden wir es einfach als type T = interface{} als generischen Parameter auf Paketebene. Und das ist alles.

  • Wir debuggen es wie ein normales Paket – es ist tatsächlicher Code, nicht irgendeine Kreatur mit halber Lebensdauer.
  • Es besteht keine Notwendigkeit, sich auf allen Ebenen in das Go-Typ-System einzumischen - denken Sie nur an die Zuweisbarkeit.
  • Es ist explizit. Kein verstecktes Mojo. Natürlich kann es ein Nachteil sein, dass generische Aufrufe nicht nahtlos verkettet werden können. Ich sehe es als Draw-Forward! Änderungstyp in zwei aufeinanderfolgenden Aufrufen, in einer Aussage ist nicht Goish (IMO).
  • Und das Beste ist, dass es abwärtskompatibel mit der Go 1.x (x >= 8) Serie ist.

Obwohl die Idee nicht neu ist, ist die Art und Weise, wie Go sie umsetzt, pragmatisch und klar.

Weiterer Bonus: In Go gibt es keine Operatorüberladung. Aber durch die Definition des Standardwerts des Typalias als (zum Beispiel) type T = int sind nicht die einzigen gültigen Typen, die zum Anpassen dieses generischen Pakets verwendet werden können, numerische Typen, die eine interne Implementierung für + haben

Auch der Alias-Typ-Parameter kann gezwungen werden, mehr als eine Schnittstelle zu erfüllen, indem einfach einige Validator-Typen und -Anweisungen hinzugefügt werden.

Nun, das wäre super hässlich, wenn Sie eine explizite Notation für einen generischen Typ verwenden würden, der einen Parameter hat, der Error - und Stringer -Schnittstellen implementiert, und auch ein numerischer Typ ist, der den + -Operator unterstützt !

Im Moment verwenden wir Codegeneratoren, die ziemlich gleich sind, aber als externes Feature.

Der Unterschied besteht darin, dass die allgemein akzeptierte Art der Codegenerierung (über go generate ) zur Commit-/Entwicklungszeit und nicht zur Kompilierzeit erfolgt. Wenn Sie dies zur Kompilierzeit tun, müssen Sie die Ausführung beliebigen Codes im Compiler zulassen, Bibliotheken können die Kompilierungszeiten um Größenordnungen sprengen und/oder Sie haben separate Build-Abhängigkeiten (dh Code kann nicht mehr nur mit Go Werkzeug). Ich mag Go dafür, dass es den Aufruf der Metaprogrammierung an den Upstream-Entwickler weiterleitet.

Das heißt, wie alle Ansätze zur Lösung dieser Probleme hat auch dieser Ansatz Nachteile und bringt Kompromisse mit sich. Persönlich würde ich argumentieren, dass tatsächliche Generika mit Unterstützung im Typsystem nicht nur besser sind (dh einen leistungsfähigeren Funktionssatz haben), sondern auch den Vorteil einer vorhersagbaren und sicheren Kompilierung behalten können.

Ich werde alles oben Gesagte lesen, das verspreche ich, und doch werde ich auch ein bisschen hinzufügen - GoLang SDK für Apache Beam scheint ein ziemlich leuchtendes Beispiel/Schaufenster für Probleme zu sein, die Bibliotheksdesigner ertragen müssen, um irgendetwas _richtig_ auf hohem Niveau zu erreichen.

Es gibt mindestens zwei experimentelle Implementierungen für Go-Generika. Anfang dieser Woche verbrachte ich einige Zeit mit (1). Ich war erfreut festzustellen, dass die Auswirkungen auf die Lesbarkeit des Codes minimal waren. Und ich fand, dass die Verwendung anonymer Funktionen zur Bereitstellung von Gleichheitstests gut funktionierte; Daher bin ich überzeugt, dass das Überladen von Operatoren nicht erforderlich ist. Das einzige Problem, das ich gefunden habe, war die Fehlerbehandlung. Die gebräuchliche Redewendung „return nil,err“ funktioniert nicht, wenn der Typ beispielsweise eine Ganzzahl oder ein String ist. Es gibt eine Reihe von Möglichkeiten, dies zu umgehen, die alle mit Komplexitätskosten verbunden sind. Ich mag etwas seltsam sein, aber ich mag die Fehlerbehandlung von Go. Dies führt mich zu der Beobachtung, dass eine Go-Generika-Lösung ein universelles Schlüsselwort für den Nullwert eines Typs haben sollte. Der Compiler würde es einfach durch Null für numerische Typen, eine leere Zeichenfolge für Zeichenfolgentypen und nil für Strukturen ersetzen.

Obwohl diese Implementierung keinen Ansatz auf Paketebene erzwang, wäre es sicherlich natürlich, dies zu tun. Und natürlich ging diese Implementierung nicht auf alle technischen Details darüber ein, wohin der vom Compiler instanziierte Code gehen sollte (falls irgendwo), wie Code-Debugger funktionieren würden usw.

Es war ganz nett, denselben Algorithmuscode für ganze Zahlen und so etwas wie einen Punkt zu verwenden:

type Point struct {
    x,y int
}

Siehe (2) für meine Tests und Beobachtungen.

(1) https://github.com/albrow/fo; die andere ist die oben erwähnte https://github.com/cosmos72/gomacro#generics
(2) https://github.com/mandolyte/fo-experiments

@mandolyte Sie können *new(T) verwenden, um den Nullwert eines beliebigen Typs zu erhalten.

Ein Sprachkonstrukt wie default(T) oder zero(T) (das erste ist das one
in C# IIRC) wäre klar, aber OTOH länger als *new(T) (obwohl mehr
leistungsfähig).

2018-07-06 9:15 GMT-05:00 Tom Thorogood [email protected] :

@mandolyte https://github.com/mandolyte Sie können *new(T) verwenden, um die
Nullwert jeglicher Art.


Sie erhalten dies, weil Sie kommentiert haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/15292#issuecomment-403046735 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AlhWhQ5cQwnc3x_XUldyJXCHYzmr6aN3ks5uD3ETgaJpZM4IG-xv
.

--
Dies ist ein Test für E-Mail-Signaturen, die in TripleMint verwendet werden sollen

19642 dient zur Diskussion eines generischen Nullwerts

@tmthrgd Irgendwie habe ich diesen kleinen Leckerbissen verpasst. Danke!

Auftakt

Bei Generics dreht sich alles um die Spezialisierung anpassbarer Konstrukte. Drei Kategorien von Spezialisierungen sind:

  • Spezialisierende Typen, Type<T> - ein _array_;
  • Spezialisierung von Berechnungen, F<T>(T) oder F<T>(Type<T>) - ein _sortierbares Array_;
  • Spezialisierte Notation, _LINQ_ zum Beispiel - select oder for Anweisungen in Go;

Natürlich gibt es Programmiersprachen, die noch generischere Konstrukte darstellen. Herkömmliche Programmiersprachen wie _C++_, _C#_ oder _Java_ bieten jedoch mehr oder weniger auf diese Liste beschränkte Sprachkonstrukte.

die Gedanken

Die erste Kategorie generischer Typen/Konstrukte sollte typunabhängig sein.

Die zweite Kategorie generischer Typen/Konstrukte muss auf eine _Eigenschaft_ des Typparameters _wirken_. Zum Beispiel muss ein _sortierbares Array_ in der Lage sein, die _vergleichbare Eigenschaft_ seiner Elemente zu _vergleichen_. Angenommen, T.(P) ist eine Eigenschaft von T und A(T.(P)) ist eine Berechnung/Aktion, die auf diese Eigenschaft einwirkt, kann (A, .(P)) entweder auf jedes einzelne Element angewendet werden oder als spezialisierte Berechnung deklariert werden, die an die ursprüngliche anpassbare Berechnung übergeben wird. Ein Beispiel für den letzteren Fall in Go ist die Schnittstelle sort.Interface , die auch die separate Funktion sort.Reverse als Gegenstück hat.

Die dritte Kategorie generischer Typen/Konstrukte sind _typ-spezialisierte_ Sprachnotationen - scheint _allgemein_ kein Go-Ding zu sein.

Fragen

Fortsetzung folgt ...

Jedes Feedback, das aussagekräftiger als ein Emoji ist, ist sehr willkommen!

@dc0d Ich würde empfehlen, Sepanovs "Elements of Programming" zu studieren, bevor Sie versuchen, Generics zu definieren. Das TL;DR ist, dass wir zunächst konkreten Code schreiben, sagen wir einen Algorithmus, der ein Array sortiert. Später fügen wir andere Sammlungstypen wie einen Btree usw. hinzu. Wir stellen fest, dass wir viele Kopien des Sortieralgorithmus schreiben, die im Wesentlichen gleich sind, also definieren wir ein Konzept, sagen wir „sortierbar“. Jetzt wollen wir die Sortieralgorithmen kategorisieren, vielleicht nach dem erforderlichen Zugriffsmuster, sagen wir nur vorwärts, Single Pass (ein Stream), nur Forward Multiple Pass (eine einfach verknüpfte Liste), bidirektional (eine doppelt verknüpfte Liste), wahlfreier Zugriff (ein Reihe). Wenn wir einen neuen Sammlungstyp hinzufügen, müssen wir nur angeben, in welche Kategorie von "Koordinaten" er fällt, um Zugriff auf alle relevanten Sortieralgorithmen zu erhalten. Diese Algorithmus-Kategorien sind „Go“-Schnittstellen sehr ähnlich. Ich würde versuchen, Schnittstellen in Go zu erweitern, um mehrere Typparameter und abstrakte/assoziierte Typen zu unterstützen. Ich glaube nicht, dass Funktionen eine Ad-hoc-Typparametrierung benötigen.

@dc0d Als Versuch, Generika in Bestandteile zu zerlegen, hatte ich 3, "spezialisierende Notation", zuvor nicht als eigenen separaten Teil betrachtet. Vielleicht könnte es als Definition von DSLs durch Verwendung von Typbeschränkungen charakterisiert werden.

Ich könnte argumentieren, dass Ihre 1 und 2 "Datenstrukturen" bzw. "Algorithmen" sind. Mit dieser Terminologie wird etwas klarer, warum es schwierig sein könnte, sie sauber zu trennen, da sie oft stark voneinander abhängig sind. Aber sort.Interface ist ein ziemlich gutes Beispiel dafür, wo Sie eine Grenze zwischen Speicherung und Verhalten ziehen können (mit ein wenig frischem Zucker, um es schöner zu machen), da es die Anforderungen Indizierbar und Vergleichbar in das Mindestverhalten codiert, das zur Implementierung des Sortieralgorithmus erforderlich ist mit "swap" und "less" (und len). Dies scheint jedoch an komplizierteren Datenstrukturen wie Bäumen oder Haufen zusammenzubrechen, die beide derzeit einige Verzerrungen erfordern, um sie in reines Verhalten als Go-Schnittstellen abzubilden.

Ich könnte mir eine relativ kleine generische Ergänzung zu Schnittstellen (oder auf andere Weise) vorstellen, mit der die meisten Lehrbuchdatenstrukturen und Algorithmen relativ sauber und ohne Verzerrungen implementiert werden könnten (wie es heute bei sort.Interface der Fall ist), die jedoch nicht leistungsfähig genug ist, um DSLs zu entwerfen. Ob wir uns auf eine so eingeschränkte Generika-Implementierung beschränken wollen , wenn wir uns überhaupt die Mühe machen, Generika hinzuzufügen, ist eine andere Frage.

@infogulch- Koordinatenstrukturen für Binärbäume sind "Gabelungskoordinaten", und es gibt Äquivalente für andere Bäume. Sie können jedoch auch die Bestellung eines Baums durch eine von drei Bestellungen projizieren: Vorbestellung, In-Bestellung und Nachbestellung. Wenn man sich für eines davon entschieden hat, kann der Baum als bidirektionale Koordinate adressiert werden, und die Familie von Sortieralgorithmen, die auf bidirektionalen Koordinaten definiert ist, wäre optimal effizient.

Der Punkt ist, dass Sie die Sortieralgorithmen nach ihren Zugriffsmustern kategorisieren. Für jedes Zugriffsmuster gibt es nur eine endliche Anzahl optimaler Sortieralgorithmen. Sie kümmern sich an dieser Stelle nicht um die Datenstrukturen. Von komplexeren Strukturen zu sprechen verfehlt den Punkt, wir wollen die Familie der Sortieralgorithmen kategorisieren, nicht die Datenstrukturen. Unabhängig davon, welche Daten Sie haben, müssen Sie einen der vorhandenen Algorithmen verwenden, um sie zu sortieren. Daher stellt sich die Frage, welche der verfügbaren Datenzugriffsmuster-Kategorisierungen von Sortieralgorithmen für die vorhandenen Datenstrukturen optimal ist.

(MEINER BESCHEIDENEN MEINUNG NACH)

@infogulch

Vielleicht könnte es als Definition von DSLs durch Verwendung von Typbeschränkungen charakterisiert werden

Sie haben Recht. Aber da sie Teil des Satzes von Sprachkonstrukten sind, wäre es meiner Meinung nach etwas ungenau, sie DSLs zu nennen.

1 und 2 ... sind oft stark abhängig

Wieder wahr. Aber es gibt viele Fälle, in denen ein Containertyp weitergegeben werden muss, während die tatsächliche Verwendung noch nicht entschieden ist - an diesem Punkt in einem Programm. Deshalb muss 1 alleine studiert werden.

sort.Interface ist ein ziemlich gutes Beispiel dafür, wo man eine Grenze zwischen _Speicher_ und _Verhalten_ ziehen kann.

Gut gesagt;

Dies scheint bei komplizierteren Datenstrukturen zusammenzubrechen

Das ist eine meiner Fragen: den Typparameter zu verallgemeinern und ihn in Form von Einschränkungen (wie List<T> where T:new, IDisposable ) zu beschreiben oder ein verallgemeinertes _Protokoll_ bereitzustellen, das auf alle Elemente (eines Satzes; eines bestimmten Typs) anwendbar ist?

@Keean

Es stellt sich die Frage, welche der verfügbaren Kategorisierungen von Datenzugriffsmustern von Sortieralgorithmen für die vorhandenen Datenstrukturen optimal ist

Wahr. Der Zugriff über den Index ist eine _Eigenschaft_ eines Slice (oder Arrays). Die erste Anforderung für einen sortierbaren Container (oder einen _baum_-fähigen Container, was auch immer der _Baum_-Algorithmus ist) ist die Bereitstellung eines Dienstprogramms für _Zugriff und Mutation (Swap)_. Die zweite Voraussetzung ist, dass die Artikel vergleichbar sein müssen. Das ist (für mich) der verwirrende Teil dessen, was Sie Algorithmen nennen: Anforderungen müssen auf beiden Seiten erfüllt werden (auf dem Container und auf dem Typparameter). Das ist der Punkt, an dem ich mir eine pragmatische Umsetzung von Generika in Go nicht vorstellen kann. Jede Seite des Problems lässt sich in Bezug auf Schnittstellen perfekt beschreiben. Aber wie kombiniert man diese beiden in einer effektiven Notation?

@dc0d -Algorithmen benötigen Schnittstellen, Datenstrukturen stellen sie bereit. Dies reicht für die volle Allgemeingültigkeit aus, vorausgesetzt, die Schnittstellen sind ausreichend leistungsfähig. Schnittstellen werden nach Typen parametrisiert, aber Sie benötigen Typvariablen.

Nehmen wir das Beispiel „sort“, „Ord“ ist eine Eigenschaft des Typs, der im Container gespeichert ist, nicht der Container selbst. Das Zugriffsmuster ist eine Eigenschaft des Containers. Einfache Zugriffsmuster sind "Iteratoren", aber dieser Name kommt von C++, Stepanov bevorzugte "Koordinaten", da er auf komplexere mehrdimensionale Container angewendet werden kann.

Beim Versuch, sort zu definieren, wollen wir so etwas:

bubble_sort : forall T U I => T U -> T U requires
   ForwardIterator<T>, Readable<T>, Writable<T>,
   Ord<U>,  ValueType(T) == U, Distance type(T) == I

Hinweis: Ich schlage diese Notation nicht vor, sondern versuche nur, andere verwandte Arbeiten einzubeziehen, die require-Klausel ist in der von Stepanov bevorzugten Syntax, der Funktionstyp stammt von Haskell, dessen Typklassen wahrscheinlich eine gute Implementierung dieser Konzepte darstellen.

@Keean
Vielleicht verstehe ich Sie falsch, aber ich glaube nicht, dass Sie Algorithmen einfach nur auf Schnittstellen beschränken können, zumindest nicht in der Art und Weise, wie Schnittstellen derzeit definiert werden.
Betrachten Sie zum Beispiel sort.Slice, wir sind daran interessiert, Slices zu sortieren, und ich sehe nicht, wie man eine Schnittstelle konstruieren würde, die alle Slices darstellen würde.

@urandom Sie abstrahieren die Algorithmen, nicht die Sammlungen. Sie fragen also, welche Datenzugriffsmuster in "Sortier"-Algorithmen existieren, und klassifizieren diese dann. Es spielt also keine Rolle, ob der Container ein "Slice" ist, wir versuchen nicht, alle Operationen zu definieren, die Sie möglicherweise auf einem Slice ausführen möchten, wir versuchen, die Anforderungen eines Algorithmus zu bestimmen und diese zu verwenden, um eine Schnittstelle zu definieren. Ein Slice ist nichts Besonderes, es ist nur ein Typ T, für den wir eine Reihe von Operationen definieren können.

Schnittstellen beziehen sich also auf Bibliotheken von Algorithmen, und Sie können Ihre eigenen Schnittstellen für Ihre eigenen Datenstrukturen definieren, um diese Algorithmen verwenden zu können. Die Bibliotheken könnten mit vordefinierten Schnittstellen für die eingebauten Typen geliefert werden.

@Keean
Ich dachte, das meinst du. Aber im Kontext von Go würde dies wahrscheinlich bedeuten, dass eine grundlegende Überarbeitung dessen erforderlich wäre, was Schnittstellen definieren können. Ich könnte mir vorstellen, dass verschiedene integrierte Operationen, wie Iterationen oder Operatoren, über Methoden verfügbar gemacht werden müssten, damit Dinge wie sort.Slice oder math.Max über Schnittstellen generisch gemacht werden können.

Sie müssten also Unterstützung für die folgende Schnittstelle (Pseudocode) haben:

type [T] OrderedIterator interface {
   Len() int
   ValueAt(i int) *T
}

...
package sort

func [T] Slice(s [T]OrderedIterator, func(i, j int) bool) {
   ...
}

und alle Slices hätten dann diese Methoden?

@urandom Ein Iterator ist keine Abstraktion einer Sammlung, sondern eine Abstraktion der Referenz/des Zeigers in eine Sammlung. Zum Beispiel könnte der Forward-Iterator eine einzelne Methode „successor“ (manchmal „next“) haben. Auf Daten am Speicherort eines Iterators zugreifen zu können, ist keine Eigenschaft des Iterators (andernfalls erhalten Sie am Ende Lese-/Schreib-/änderbare Varianten des Iterators). Es ist am besten, "Referenzen" separat als lesbare, beschreibbare und veränderliche Schnittstellen zu definieren:

type T ForwardIterator interface {
   type DistanceType D
   successor(x T) T
}

type T Readable interface {
   type ValueType U 
   source(x T) U
}

Hinweis: Der Typ 'T' ist nicht der Slice, sondern der Typ des Iterators auf dem Slice. Dies könnte nur ein einfacher Zeiger sein, wenn wir den C++-Stil übernehmen, einen Start- und End-Iterator an Funktionen wie sort zu übergeben.

Für einen Iterator mit wahlfreiem Zugriff würden wir am Ende so etwas wie:

type T RandomIterator interface {
   type DistanceType D
   setPosition(x DistanceType)
}

Ein Iterator/eine Koordinate ist also eine Abstraktion des Verweises auf eine Sammlung, nicht die Sammlung selbst. Der Name „Koordinate“ drückt dies recht gut aus, wenn man sich den Iterator als Koordinate und die Sammlung als Karte vorstellt.

Verkaufen wir nicht Go short, indem wir Funktionsschließungen und anonyme Funktionen nicht nutzen? Funktionen/Methoden als erstklassigen Typ in Go zu haben, kann hilfreich sein. Unter Verwendung der Syntax von albrow/fo könnte eine Blasensortierung beispielsweise so aussehen:

type SortableContainer[C,T] struct {
    Less func(C,T,T) bool
    Swap func(C,int,int)
    Next func(C) (T,bool)
}

func (bs *SortableContainer[C,T]) BubbleSort(container C, e1,e2 T) {    
    swapCount := 1
    var item1, item2 T
    item1, ok1 = bs.Next()
    if !ok1 {return}
    item2, ok2 = bs.Next()
    if !ok2 {return}
    for swapCount > 0 {
        swapCount = 0
        for {
            if Less(item2, item1) { 
                bs.Swap(C,item2,item1)
                swapCount += 1
            }
        }
    }
}

Bitte übersehen Sie eventuelle Fehler ... völlig ungetestet!

@mandolyte Ich bin mir nicht sicher, ob dies an mich adressiert war? Ich sehe keinen wirklichen Unterschied zwischen dem, was ich vorgeschlagen habe, und Ihrem Beispiel, außer dass Sie Schnittstellen mit mehreren Parametern verwenden und ich Beispiele mit abstrakten/assoziierten Typen gegeben habe. Um es klar zu sagen, ich denke, Sie brauchen sowohl Multiparameter-Schnittstellen als auch abstrakte/assoziierte Typen für die volle Allgemeingültigkeit, die beide derzeit nicht von Go unterstützt werden.

Ich würde vorschlagen, dass Ihre Schnittstellen weniger allgemein sind als die von mir vorgeschlagenen, da sie die Sortierreihenfolge, das Zugriffsmuster und die Zugänglichkeit an dieselbe Schnittstelle binden, was natürlich zu einer Vermehrung von Schnittstellen führen wird, zum Beispiel zwei Ordnungen (weniger , größer), drei Zugriffsarten (read-only, write-only, mutable) und fünf Zugriffsmuster (forward-single-pass, forward-multi-pass, bidirectional, indexed, random) würden zu 36 statt nur 11 Schnittstellen führen wenn die Bedenken getrennt gehalten werden.

Sie könnten die von mir vorgeschlagenen Schnittstellen mit Multiparameter-Schnittstellen anstelle von abstrakten Typen wie diesen definieren:

type I ForwardIterator interface {
   successor(x I) I
}
type R V Readable interface {
   source(x R) V
}
type V Ord interface {
   less(x V, y V) : bool
}

Beachten Sie, dass die einzige, die zwei Typparameter benötigt, die Readable-Schnittstelle ist. Wir verlieren jedoch die Fähigkeit eines Iteratorobjekts, den Typ der iterierten Objekte zu 'enthalten', was ein großes Problem darstellt, da wir jetzt den 'Wert'-Typ im Typsystem verschieben müssen, und wir müssen es richtig machen . Dies führt zu einer unerwünschten Vermehrung von Typparametern und erhöht die Möglichkeit von Codierungsfehlern. Wir verlieren auch die Möglichkeit, den „DistanceType“ auf dem Iterator zu definieren, der der kleinste Zahlentyp ist, der zum Zählen der Elemente in der Sammlung erforderlich ist, was für die Zuordnung zu int8, int16, int32 usw. nützlich ist, um den Typ zu erhalten, den Sie benötigen Elemente ohne Überlauf zählen.

Dies ist eng mit dem Konzept der „funktionalen Abhängigkeit“ verbunden. Wenn ein Typ funktional von einem anderen Typ abhängig ist, sollte es sich um einen abstrakten/assoziierten Typ handeln. Nur wenn die beiden Typen unabhängig sind, sollten sie separate Typparameter sein.

Einige Probleme:

  1. Die aktuelle f(x I)-Syntax kann für Schnittstellen mit mehreren Parametern nicht verwendet werden. Ich mag es nicht, dass diese Syntax Schnittstellen (die Einschränkungen für Typen sind) sowieso mit Typen verwechselt.
  2. Es bräuchte eine Möglichkeit, parametrisierte Typen zu deklarieren.
  3. Es müsste eine Möglichkeit geben, zugeordnete Typen für Schnittstellen mit einem bestimmten Satz von Typparametern zu deklarieren.

@keean Ich bin mir nicht sicher, ob ich verstehe, wie oder warum die Anzahl der Schnittstellen so hoch wird. Hier ist ein vollständiges Arbeitsbeispiel: https://play.folang.org/p/BZa6BdsfBgZ (slice-basiert, kein allgemeiner Container, daher keine Next()-Methode erforderlich).

Es verwendet nur eine Typstruktur, überhaupt keine Schnittstellen. Ich muss alle anonymen Funktionen und Schließungen bereitstellen (das ist wahrscheinlich der Kompromiss?). Das Beispiel verwendet den gleichen Blasensortierungsalgorithmus, um sowohl ein Segment von Ganzzahlen als auch ein Segment von „(x,y)“-Punkten zu sortieren, wobei die Entfernung vom Ursprung die Grundlage der Less()-Funktion ist.

Auf jeden Fall wollte ich zeigen, wie es helfen kann, Funktionen im Typsystem zu haben.

@mandolyte Ich glaube, ich habe deinen Vorschlag falsch verstanden. Ich sehe, wovon Sie sprechen, ist "folang", das Go bereits einige nette funktionale Programmierfunktionen hinzugefügt hat. Was Sie implementiert haben, ist im Grunde genommen, eine Typklasse mit mehreren Parametern von Hand auszufüllen. Sie übergeben ein sogenanntes Funktionswörterbuch an die Sortierfunktion. Dies tut explizit das, was eine Schnittstelle implizit tun würde. Diese Art von Funktionen werden wahrscheinlich vor Schnittstellen mit mehreren Parametern und zugehörigen Typen benötigt, aber irgendwann stoßen Sie auf Probleme, wenn Sie all diese Wörterbücher herumgeben. Ich denke, Schnittstellen sorgen für saubereren, besser lesbaren Code.

Das Sortieren eines Slice ist ein gelöstes Problem. Hier ist der Code für ein Slice quicksort.go , das mit der go-li-Sprache (golang-verbessert) implementiert wurde .

func main(){
    var data = []int{5,3,1,8,9}

    Sort(data, func(a *int, b *int) int {
        return *a - *b
    })

    fmt.Println(data)
}

Auf dem Spielplatz kann man damit experimentieren

Das vollständige Beispiel können Sie in den Spielplatz einfügen , da das Importieren des Quicksort-Pakets auf dem Spielplatz nicht funktioniert.

@go-li Ich bin sicher, Sie können ein Stück sortieren, es wäre ein bisschen schlecht, wenn Sie es nicht könnten. Der Punkt ist im Allgemeinen, dass Sie in der Lage sein möchten, jeden linearen Container mit demselben Code zu sortieren, sodass Sie immer nur einmal einen Sortieralgorithmus schreiben müssen, egal welchen Container (Datenstruktur) Sie sortieren und egal was Inhalt ist.

Wenn Sie dies tun können, kann die Standardbibliothek universelle Sortierfunktionen bereitstellen, und niemand muss jemals wieder eine schreiben. Dies hat zwei Vorteile, weniger Fehler, da es schwieriger ist, einen korrekten Sortieralgorithmus zu schreiben, als Sie denken. Stepanov verwendet das Beispiel, dass die meisten Programmierer das Paar „min“ und „max“ nicht richtig definieren können. Welche Hoffnung müssen wir also haben? korrekt für komplexere Algorithmen. Der andere Vorteil besteht darin, dass, wenn es nur eine Definition für jeden Sortieralgorithmus gibt, alle Verbesserungen der Klarheit oder Leistung, die vorgenommen werden können, allen Programmen zugute kommen, die ihn verwenden. Die Leute können ihre Zeit damit verbringen, den gemeinsamen Algorithmus zu verbessern, anstatt für jeden anderen Datentyp einen eigenen schreiben zu müssen.

@Keean
Eine weitere Frage bezieht sich auf unsere vorherige Diskussion. Ich kann nicht herausfinden, wie man eine Zuordnungsfunktion definieren könnte, die Elemente von einem iterierbaren ändert und einen neuen konkreten iterierbaren Typ zurückgibt, dessen Elemente möglicherweise einen anderen Typ als den ursprünglichen haben.

Und ich kann mir vorstellen, dass ein Benutzer einer solchen Funktion einen konkreten Typ zurückgeben möchte, nicht eine andere Schnittstelle.

@urandom Angenommen, wir wollen es nicht an Ort und Stelle tun, was unsicher wäre, was Sie wollen, ist eine Kartenfunktion, die einen „Lese-Iterator“ eines Typs und einen „Schreib-Iterator“ eines anderen Typs hat. was etwa so definiert werden kann:

map<I, O, U>(first I, last I, out O, fn U) requires
   ForwardIterator<I>, Readable<I>,
   ForwardIterator<O>, Writable<O>,
   UnaryFunction<U>, Domain(U) == ValueType(I), Codomain(U) == ValueType(O)

Der Übersichtlichkeit halber ist "ValueType" ein assoziierter Typ der Schnittstellen "Readable" und "Writable", "Domain" und "Codomain" sind assoziierte Typen der Schnittstelle "UnaryFunction". Es hilft natürlich sehr, wenn der Compiler die Schnittstellen für Datentypen wie "UnaryFunction" automatisch ableiten kann. Während diese Art von Reflektion aussieht, ist sie es nicht, und alles geschieht zur Kompilierzeit mit statischen Typen.

@keean Wie kann man diese Lese- und Schreibbeschränkungen im Kontext der aktuellen Go-Schnittstellen modellieren?

Ich meine, wenn wir einen Typ A haben und in den Typ B konvertieren wollen, wäre die Signatur dieser UnaryFunction func (input A) B (richtig?), aber wie kann das sein nur mit Schnittstellen modelliert werden und wie dieses generische map (oder filter , reduce usw.) modelliert werden würde, um die Pipeline von Typen beizubehalten?

@geovanisouza92 Ich denke, "Typfamilien" würden gut funktionieren, da sie als orthogonaler Mechanismus im Typsystem implementiert und dann wie in Haskell in die Syntax für Schnittstellen integriert werden können.

Eine Typfamilie ist wie eine eingeschränkte Funktion für Typen (eine Zuordnung). Da Schnittstellenimplementierungen nach Typ ausgewählt werden, können wir für jede Implementierung eine Typzuordnung bereitstellen.

Wenn wir also definieren:

ValueType MyIntArrayIterator -> Int

Funktionen sind ein wenig kniffliger, aber eine Funktion hat einen Typ, zum Beispiel:

fn(x : Int) Float

Wir würden diesen Typ schreiben:

Int -> Float

Es ist wichtig zu erkennen, dass -> nur ein Infix-Typkonstruktor ist, so wie '[]' für ein Array ein Typkonstruktor ist, wir könnten dies genauso gut schreiben;

Fn Int Float
Or
Fn<Int, Float>

Abhängig von unserer Präferenz für die Typsyntax. Jetzt können wir klar sehen, wie wir definieren können:

Domain  Fn<Int, Float> -> Int
Codomain Fn<Int, Float> -> Float

Während wir alle diese Definitionen von Hand bereitstellen könnten, können sie vom Compiler leicht abgeleitet werden.

Angesichts dieser Typfamilien können wir sehen, dass die oben angegebene Definition von map nur die Typen IO und U erfordert, um das Generikum zu instanziieren, da alle anderen Typen funktional von diesen abhängig sind. Wir können sehen, dass diese Typen direkt von den Argumenten bereitgestellt werden.

Danke, @keean.

Dies würde für eingebaute/vordefinierte Funktionen gut funktionieren. Wollen Sie damit sagen, dass dasselbe Konzept für benutzerdefinierte Funktionen oder Userland-Bibliotheken angewendet wird?

Diese "Typfamilien" werden im Falle eines Fehlerkontexts zur Laufzeit mitgeführt?

Wie wäre es mit leeren Schnittstellen, Typschaltern und Reflektion?


EDIT: Ich bin nur neugierig und beschwere mich nicht.

@giovanisouza92 Nun , niemand hat Go verpflichtet, Generika zu haben, also erwarte ich Skepsis. Mein Ansatz ist, dass, wenn Sie Generika herstellen, Sie sie richtig machen sollten.

In meinem Beispiel ist 'map' benutzerdefiniert. Daran ist nichts Besonderes, und innerhalb der Funktion verwenden Sie einfach die Methoden der Schnittstellen, die Sie für diese Typen benötigt haben, genau wie Sie es jetzt in Go tun. Der einzige Unterschied besteht darin, dass ein Typ erforderlich sein kann, um mehrere Schnittstellen zu erfüllen, Schnittstellen können mehrere Typparameter haben (obwohl das Kartenbeispiel dies nicht verwendet) und es auch zugeordnete Typen gibt (und Einschränkungen für Typen wie die Typgleichheit '==' aber das ist wie eine Prolog-Gleichheit und vereinheitlicht die Typen). Aus diesem Grund gibt es die unterschiedliche Syntax zur Angabe der Schnittstellen, die von einer Funktion benötigt werden. Beachten Sie, dass es einen weiteren wichtigen Unterschied gibt:

f(x I, y I) requires ForwardIterator<I>

Vs

f(x ForwardIterator, y ForwardIterator)

Beachten Sie, dass es im letzteren einen Unterschied gibt, dass 'x' und 'y' unterschiedliche Typen sein können, die die ForwardIterator-Schnittstelle erfüllen, während in der früheren Syntax 'x' und 'y' beide vom gleichen Typ sein müssen (der den Forward-Iterator erfüllt). Dies ist wichtig, damit Funktionen nicht unterbeschränkt werden und konkrete Typen viel weiter während der Kompilierung weitergegeben werden können.

Ich glaube nicht, dass sich bezüglich Type Switches und Reflection etwas ändert, da wir lediglich das Interface-Konzept erweitern. Da go über Laufzeittypinformationen verfügt, geraten Sie nicht in das gleiche Problem wie Haskell und benötigen existentielle Typen.

Wenn wir an Go, Laufzeitpolymorphismus und Typfamilien denken, möchten wir wahrscheinlich die Typfamilie selbst auf eine Schnittstelle beschränken, um zu vermeiden, dass jeder zugeordnete Typ zur Laufzeit als leere Schnittstelle behandelt werden muss, was langsam wäre.

Angesichts dieser Gedanken würde ich meinen obigen Vorschlag so ändern, dass Sie beim Deklarieren einer Schnittstelle eine Schnittstelle/einen Typ für jeden zugeordneten Typ deklarieren, dass alle Implementierungen dieser Schnittstelle einen zugeordneten Typ bereitstellen müssten, der diese Schnittstelle erfüllt. Auf diese Weise können wir wissen, dass es sicher ist, alle Methoden von dieser Schnittstelle auf den zugehörigen Typen zur Laufzeit aufzurufen, ohne von einer leeren Schnittstelle aus den Typ wechseln zu müssen.

@Keean
Um die Debatte voranzutreiben, lassen Sie mich ein Missverständnis klarstellen, das meines Erachtens dem nicht erfundenen Syndrom ähnelt.

Bidirektionaler Iterator (in T-Syntax func (*T) *[2]*T ) hat den Typ func (*) *[2]* in go-li-Syntax. In Worten, es nimmt einen Zeiger auf einen Typ und gibt einen Zeiger auf zwei Zeiger auf das nächste und vorherige Element desselben Typs zurück. Es ist der grundlegende konkrete Fundamentaltyp, der von einer doppelt verknüpften Liste verwendet wird.

Jetzt können Sie schreiben, was Sie Map nennen, was ich die generische Funktion foreach nenne. Machen Sie keinen Fehler, dies funktioniert nicht nur über verknüpfte Listen, sondern über alles, was einen bidirektionalen Iterator offenlegt!

func Foreach(link func(*) *[2]*, list **, direction byte, f func(*)) {

    if nil == *list {
        return
    }

    var end *
    end = *list

    var e *
    e = (*link(*list))[direction]
    f(end)

    for (e != end) && ((*link(e))[direction] != nil) {
        var newe = (*link(e))[direction]
        f(e)
        e = newe
    }
    return
}

Der Foreach kann auf zwei Arten verwendet werden, Sie verwenden ihn mit einem Lambda in einer for-schleifenähnlichen Iteration über Listen- oder Sammlungselementen.

const forward = 1
const backwards = 0
Foreach(iterator, collection, forward, func(element *element_type){
    // do something with every element
})

Oder Sie können es verwenden, um jedem Sammlungselement eine Funktion funktional zuzuordnen.

Foreach(iterator, collection, backwards, function_to_be_mapped_on_elements)

Bidirektionaler Iterator kann natürlich auch über Schnittstellen in go 1 modelliert werden.
interface Iterator { Iter() [2]Iterator } Sie müssen es mithilfe von Schnittstellen modellieren, um den zugrunde liegenden Typ zu umschließen ("boxen"). Der Benutzer des Iterators bestätigt dann den bekannten Typ, sobald er ein bestimmtes Sammlungselement gefunden hat und besuchen möchte. Dies ist möglicherweise eine unsichere Kompilierzeit.

Was Sie als Nächstes beschreiben, sind die Unterschiede zwischen dem Legacy-Ansatz und dem auf Generika basierenden Ansatz.

func modern(x func  (*) *[2]*, y func  (*) *[2]*){}

Dieser Ansatz zur Kompilierzeit überprüft, ob die beiden Sammlungen denselben zugrunde liegenden Typ haben, mit anderen Worten, ob die Iteratoren tatsächlich dieselben konkreten Typen zurückgeben

func modern_T_syntax<T>(x func  (*T) *[2]*T, y func  (*T) *[2]*T){}

Dasselbe wie oben, aber unter Verwendung der bekannten Platzhaltersyntax für T bedeutet Typ

func legacy(x Iterator, y Iterator){}

In diesem Fall kann der Benutzer beispielsweise eine ganzzahlige verkettete Liste als x und eine verkettete Float-Liste als y übergeben. Dies könnte zu potenziellen Laufzeitfehlern, Panik oder anderen internen Dekohärenzen führen, aber alles hängt davon ab, was Legacy mit den beiden Iteratoren machen würde.

Jetzt der Denkfehler. Sie behaupten, dass das Ausführen von Iteratoren und das Ausführen generischer Sortierungen zum Sortieren dieser Iteratoren der richtige Weg wäre. Das wäre eine wirklich schlechte Sache, hier ist der Grund

Iterator und verkettete Liste sind zwei Seiten derselben Medaille. Beweis: Jede Sammlung, die Iterator verfügbar macht, bewirbt sich einfach als verknüpfte Liste. Nehmen wir an, Sie müssen das sortieren. Was tun?

Offensichtlich löschen Sie die verknüpfte Liste aus Ihrer Codebasis und ersetzen sie durch einen binären Baum. Oder wenn Sie schick sein wollen, verwenden Sie einen ausgewogenen Suchbaum wie avl, rot-schwarz, wie von Ian et all vorgeschlagen, ich weiß nicht, wie viele Jahre her. Dies wurde jedoch noch nicht generisch in Golang durchgeführt. Das wäre jetzt der richtige Weg.

Eine andere Lösung besteht darin, den Iterator schnell in einer O(N)-Zeitschleife zu durchlaufen, die Zeiger auf Elemente in einem Slice generischer Zeiger mit der Bezeichnung []*T zu sammeln und diese generischen Zeiger mit der schlechten Slice-Sortierung zu sortieren

Bitte geben Sie den Ideen anderer eine Chance

@go-li Wenn wir das Not-invented-here-Syndrom vermeiden wollen, sollten wir Alex Stepanov nach einer Definition fragen, da er so ziemlich die generische Programmierung erfunden hat. So würde ich es definieren, entnommen aus Stepanovs "Elements of Programming" Seite 111:

Bidirectional iterator<T> =
    ForwardIterator<T>
/\ predecessor : T -> T
/\ predecessor takes constant time
/\ (forall i in T) successor(i) is defined =>
        predecessor(successor(i)) is defined and equals i
/\ (forall i in T) predecessor(i) is defined =>
        successor(predecessor(i)) is defined and equals i

Dies hängt von der Definition von ForwardIterator ab:

ForwardIterator<T> =
    Iterator<T>
/\ regular_unary_function(successor)

Im Wesentlichen haben wir also eine Schnittstelle, die eine successor -Funktion und eine predecessor -Funktion deklariert, zusammen mit einigen Axiomen, denen sie entsprechen müssen, um gültig zu sein.

In Bezug auf legacy ist es nicht so, dass Legacy schief gehen wird, in Go geht es offensichtlich derzeit nicht schief, aber dem Compiler fehlen Optimierungsmöglichkeiten und dem Typsystem fehlt die Möglichkeit, konkrete Typen weiter zu verbreiten. Es schränkt auch die Fähigkeit der Programmierer ein, ihre Absicht genau zu spezifizieren. Ein Beispiel wäre eine Identitätsfunktion, die genau den Typ zurückgeben soll, an den sie übergeben wird:

id(x T) T

Erwähnenswert ist vielleicht auch der Unterschied zwischen einem parametrischen Typ und einem universell quantifizierten Typ. Ein parametrischer Typ wäre id<T>(x T) T , während der universell quantifizierte Typ id(x T) T ist (normalerweise lassen wir in diesem Fall den äußersten universellen Quantor forall T ). Bei parametrischen Typen muss das Typsystem einen Typ für T haben, der auf der Aufrufseite für id bereitgestellt wird, wobei eine universelle Quantifizierung nicht erforderlich ist, solange T mit einem konkreten Typ vereinheitlicht wird, bevor die Kompilierung abgeschlossen ist. Eine andere Möglichkeit, dies zu verstehen, ist, dass die parametrische Funktion kein Typ, sondern eine Vorlage für einen Typ ist, und sie ist nur dann ein gültiger Typ, nachdem T durch einen konkreten Typ ersetzt wurde. Bei der universell quantifizierten Funktion hat id tatsächlich einen Typ forall T . T -> T , der vom Compiler genauso wie Int übergeben werden kann.

@go-li

Offensichtlich löschen Sie die verknüpfte Liste aus Ihrer Codebasis und ersetzen sie durch einen binären Baum. Oder wenn Sie schick sein wollen, verwenden Sie einen ausgewogenen Suchbaum wie avl, rot-schwarz, wie von Ian et all vorgeschlagen, ich weiß nicht, wie viele Jahre her. Dies wurde jedoch noch nicht generisch in Golang durchgeführt. Das wäre jetzt der richtige Weg.

Geordnete Datenstrukturen zu haben bedeutet nicht, dass Sie niemals Daten sortieren müssen.

Wenn wir das Not-invented-here-Syndrom vermeiden wollen, sollten wir Alex Stepanov nach einer Definition fragen, da er so ziemlich die generische Programmierung erfunden hat.

Ich würde jede Behauptung bestreiten, dass die generische Programmierung von C++ erfunden wurde. Lesen Sie die Liskov et al. CACM-Papier von 1977, wenn Sie ein frühes Modell generischer Programmierung sehen möchten, das tatsächlich funktioniert (typsicher, modular, kein aufgeblähter Code): https://dl.acm.org/citation.cfm?id=359789 (siehe Abschnitt 4 )

Ich denke, wir sollten diese Diskussion beenden und warten, bis das golang-Team (russ) mit einigen Blog-Beiträgen kommt, und dann eine Lösung implementieren 👍 (siehe vgo). Sie werden es einfach tun 🎉

https://peter.bourgon.org/blog/2018/07/27/a-response-about-dep-and-vgo.html

Ich hoffe, diese Geschichte dient anderen als Warnung: Wenn Sie daran interessiert sind, wesentliche Beiträge zum Go-Projekt zu leisten, kann keine noch so große unabhängige Due Diligence ein Design kompensieren, das nicht vom Kernteam stammt.

Dieser Thread zeigt, dass das Kernteam kein Interesse daran hat, sich aktiv an der Lösungsfindung mit der Community zu beteiligen.

Aber am Ende, wenn sie wieder selbst eine Lösung finden können, ist das in Ordnung für mich, mach es einfach 👍

@andrewcmyers Nun, vielleicht war "erfunden" ein bisschen weit hergeholt, das ist wahrscheinlich eher wie David Musser im Jahr 1971, der später mit Stepanov an einigen generischen Bibliotheken für Ada arbeitete.

Elements of Programming ist kein Buch über C++, die Beispiele mögen in C++ sein, aber das ist etwas ganz anderes. Ich denke, dieses Buch ist eine unverzichtbare Lektüre für jeden, der Generika in jeder Sprache implementieren möchte. Bevor Sie Stepanov entlassen, sollten Sie das Buch wirklich lesen, um zu sehen, worum es eigentlich geht.

Dieses Problem stößt bereits an die Grenzen der GitHub-Skalierbarkeit. Bitte konzentrieren Sie die Diskussion hier auf konkrete Themen für Go-Vorschläge.

Es wäre unglücklich, wenn Go der C++-Route folgen würde.

@andrewcmyers Ja, ich stimme voll und ganz zu, bitte verwenden Sie C++ nicht für Syntaxvorschläge oder als Maßstab für die richtige Vorgehensweise. Schauen Sie sich stattdessen bitte D an, um sich inspirieren zu lassen .

@nomad-software

Ich mag D sehr, aber braucht go die mächtigen Meta-Programmierfunktionen zur Kompilierzeit, die D bietet?

Auch in C++ gefällt mir die Template-Syntax nicht, die aus der Steinzeit stammt.

Aber was ist mit dem normalen ParametricTypeStandard in Java oder C# zu finden, bei Bedarf kann man diesen auch mit ParametricType überladen

Außerdem mag ich die Template-Call-Syntax in D mit ihrem Bang-Symbol nicht, das Bang-Symbol wird heutzutage eher verwendet, um veränderlichen oder unveränderlichen Zugriff auf die Parameter einer Funktion zu bezeichnen.

@nomad-software Ich habe nicht vorgeschlagen, dass die C ++ - Syntax oder der Vorlagenmechanismus der richtige Weg ist, um Generika zu erstellen. Mehr noch, dass "Konzepte" im Sinne von Stepanov Typen als Algebra behandeln, was sehr genau der richtige Weg ist, Generika zu erstellen. Sehen Sie sich Haskell-Typklassen an, um zu sehen, wie dies aussehen könnte. Haskell-Klassen sind semantisch sehr nah an C++-Vorlagen und -Konzepten, wenn Sie verstehen, was vor sich geht.

Also +1 für die Nichtbefolgung der C++-Syntax und +1 für die Nichtimplementierung eines typunsicheren Vorlagensystems :-)

@keean Der Grund für die D-Syntax besteht darin, <,> insgesamt zu vermeiden und die kontextfreie Grammatik einzuhalten. Dies ist Teil meines Punktes, D als Inspiration zu verwenden. <,> ist eine wirklich schlechte Wahl für die Syntax generischer Parameter.

@nomad-software Wie ich oben (in einem jetzt versteckten Kommentar) darauf hingewiesen habe, müssen Sie die Typparameter für parametrische Typen angeben, nicht jedoch für universell quantifizierte Typen (daher der Unterschied zwischen Rust und Haskell, die Art und Weise, wie Typen behandelt werden, ist tatsächlich unterschiedlich im Typensystem). Auch C++-Konzepte == Haskell-Typklassen == Go-Schnittstellen, zumindest auf konzeptioneller Ebene.

Ist D Syntax wirklich vorzuziehen:

auto add(T)(T lhs, T rhs) {
    return lhs + rhs;

Warum ist das besser als C++/Java/Rust-Stil:

T add<T>(T lhs, T rhs) {
    return lhs + rhs;
}

Oder Scala-Stil:

T add[T](T lhs, T rhs) {
    return lhs + rhs;
}

Ich habe über die Syntax für Typparameter nachgedacht. Ich war noch nie ein Fan von "spitzen Klammern" in C++ und Java, weil sie das Parsen ziemlich schwierig machen und damit die Entwicklung von Tools behindern. Eckige Klammern sind eigentlich eine klassische Wahl (von CLU, System F und anderen frühen Sprachen mit parametrischem Polymorphismus).

Allerdings ist die Syntax von Go ziemlich heikel, vielleicht weil sie schon so knapp ist. Mögliche Syntaxen, die auf eckigen Klammern oder runden Klammern basieren, erzeugen grammatikalische Mehrdeutigkeiten, die noch schlimmer sind als die, die durch spitze Klammern eingeführt werden. Also trotz meiner Veranlagung scheinen spitze Klammern eigentlich die beste Wahl für Go zu sein. (Natürlich gibt es auch echte spitze Klammern, die keine Mehrdeutigkeit erzeugen würden – ⟨⟩ – aber sie würden die Verwendung von Unicode-Zeichen erfordern).

Natürlich ist die genaue Syntax, die für Typparameter verwendet wird, weniger wichtig als die richtige Semantik . In diesem Punkt ist die Sprache C++ ein schlechtes Modell. Die Arbeit meiner Forschungsgruppe zu Generika in Genus (PLDI 2015) und Familia (OOPSLA 2017) bietet einen weiteren Ansatz, der Typklassen erweitert und sie mit Schnittstellen vereinheitlicht.

@andrewcmyers Ich denke, dass beide Papiere interessant sind, aber ich würde sagen, keine gute Richtung für Go, da Genus objektorientiert ist und Go nicht, und Familia vereinheitlicht Subtypisierung und parametrischen Polymorphismus, und Go hat keines von beidem. Ich denke, Go sollte einfach entweder parametrischen Polymorphismus oder universelle Quantifizierung übernehmen, es braucht keine Untertypisierung und meiner Meinung nach ist es eine bessere Sprache, um es nicht zu haben.

Ich denke, Go sollte nach Generika suchen, die keine Objektorientierung und keine Untertypisierung erfordern. Go hat bereits Schnittstellen, die meiner Meinung nach ein guter Mechanismus für Generika sind. Wenn Sie sehen können, dass Go-Schnittstellen == C++-Konzepte == Haskell-Typklassen, scheint es mir, dass der Weg, Generika hinzuzufügen, während der Geschmack von 'Go' beibehalten wird, darin besteht, Schnittstellen zu erweitern, um mehrere Typparameter zu akzeptieren (ich würde wie assoziierte Typen auch auf Schnittstellen, aber das könnte eine separate Erweiterung davon sein, hilft dabei, dass mehrere Typparameter akzeptiert werden). Das wäre die Schlüsseländerung, aber um dies zu ermöglichen, müsste es eine „alternative“ Syntax für Schnittstellen in Funktionssignaturen geben, damit Sie die mehreren Typparameter an die Schnittstellen übertragen können, wo die gesamte Syntax der spitzen Klammern ins Spiel kommt .

Go-Schnittstellen sind keine Typklassen – sie sind lediglich Typen – aber Familia zeigt einen Weg, Schnittstellen mit Typklassen zu vereinheitlichen. Die Mechanismen von Genus und Familia sind nicht daran gebunden, dass die Sprachen vollständig objektorientiert sind. Go-Schnittstellen machen Go bereits in der wichtigen Weise "objektorientiert", daher denke ich, dass die Ideen in leicht vereinfachter Form angepasst werden könnten.

@andrewcmyers

Go-Schnittstellen sind keine Typklassen – sie sind lediglich Typen

Sie verhalten sich für mich nicht wie Typen, da sie Polymorphismus zulassen. Das Objekt in einem polymorphen Array wie Addable[] hat immer noch seinen tatsächlichen Typ (sichtbar durch Reflektion zur Laufzeit), daher verhalten sie sich genau wie Klassen mit Einzelparametertyp. Die Tatsache, dass sie in Typsignaturen an die Stelle eines Typs gesetzt werden, ist einfach eine Kurzschreibweise, bei der die Typvariable weggelassen wird. Verwechseln Sie die Notation nicht mit der Semantik.

f(x : Addable) == f<T>(x : T) requires Addable<T>

Diese Identität gilt natürlich nur für Einzelparameterschnittstellen.

Der einzige signifikante Unterschied zwischen Schnittstellen und Einzelparameter-Typklassen besteht darin, dass Schnittstellen lokal definiert werden, aber das ist nützlich, weil es das globale Kohärenzproblem vermeidet, das Haskell mit seinen Typklassen hat. Ich denke, das ist ein interessanter Punkt im Designbereich. Schnittstellen mit mehreren Parametern würden Ihnen die gesamte Leistungsfähigkeit von Typklassen mit mehreren Parametern mit dem Vorteil geben, lokal zu sein. Es besteht keine Notwendigkeit, der Go-Sprache irgendeine Vererbung oder Untertypisierung hinzuzufügen (was meiner Meinung nach die beiden Hauptmerkmale sind, die OO definieren).

MEINER BESCHEIDENEN MEINUNG NACH:

Immer noch einen Standardtyp zu haben, wäre einer DSL vorzuziehen, die dazu bestimmt ist, Typbeschränkungen auszudrücken. Als hätte man eine Funktion f(s T fmt.Stringer) , die eine generische Funktion ist, die jeden Typ akzeptiert, der auch die Schnittstelle fmt.Stringer erfüllt/befriedigt.

Auf diese Weise ist es möglich, eine generische Funktion zu haben wie:

func add(a, b T int) T int {
    return a + b
}

Jetzt funktioniert die Funktion add() mit jedem Typ T , der wie int #$ den Operator + unterstützt.

@dc0d Ich stimme zu, dass dies angesichts der aktuellen Go-Syntax attraktiv erscheint. Es ist jedoch nicht „vollständig“, da es nicht alle für Generika notwendigen Einschränkungen darstellen kann, und es wird noch Bestrebungen geben, dies weiter auszubauen. Dies wird zu einer Vermehrung verschiedener Syntaxen führen, die meiner Ansicht nach im Widerspruch zum Ziel der Einfachheit stehen. Ich bin der Meinung, dass Einfachheit nicht einfach ist, sie muss die einfachste sein, aber dennoch die erforderliche Ausdruckskraft bieten. Derzeit sehe ich die größte Einschränkung von Go in Bezug auf die generische Ausdruckskraft im Fehlen von Schnittstellen mit mehreren Parametern. Beispielsweise könnte eine Collection-Schnittstelle wie folgt definiert werden:

type T U Collection interface {
   member(c T, v U) Bool
   insert(c T, v U) T
}

Das macht also Sinn oder? Wir würden gerne Schnittstellen über Dinge wie Sammlungen schreiben. Die Frage ist also, wie Sie diese Schnittstelle in einer Funktion verwenden. Mein Vorschlag wäre so etwas wie:

func[T, U] f(c T, e U) (Bool, T) requires Collection[T, U] {
   a := member(c, e)
   d := insert(c, e)
   return a, d
}

Die Syntax ist nur ein Vorschlag, aber ich habe nichts dagegen, was die Syntax ist, solange Sie diese Konzepte in der Sprache ausdrücken können.

@keean Es wäre nicht korrekt, wenn ich sagen würde, dass mir die Syntax überhaupt nichts ausmacht. Aber der Punkt war die Betonung darauf, einen Standardtyp für jeden generischen Parameter zu haben. In diesem Sinne wird das bereitgestellte Beispiel für die Schnittstelle zu:

type Collection interface (T interface{}, U interface{}) {
   member(c T, v U) Bool
   insert(c T, v U) T
}

Jetzt hilft der Teil (T interface{}, U interface{}) beim Definieren von Einschränkungen. Wenn die Mitglieder zum Beispiel fmt.Stringer erfüllen sollen, dann wäre die Definition:

type Collection interface (T fmt.Stringer, U fmt.Stringer) {
   member(c T, v U) Bool
   insert(c T, v U) T
}

@dc0d Dies wäre wiederum in dem Sinne restriktiv, dass Sie durch mehr als einen Typparameter einschränken möchten, beachten Sie:

type OrderedCollection[T, U] interface
   requires Collection[T, U], Ord[U] {...}

Ich glaube, ich sehe, woher Sie mit der Parameterplatzierung kommen, Sie könnten Folgendes haben:

type OrderedCollection interface(T, U)
   requires Collection(T, U), Ord(U) {...}

Wie gesagt, mich stört die Syntax nicht allzu sehr, da ich mich an die meisten Syntaxen gewöhnen kann. Aus dem Obigen nehme ich an, dass Sie Klammern '()' für Schnittstellen mit mehreren Parametern bevorzugen.

@keean Betrachten wir die Schnittstelle heap.Interface . Aktuelle Definition in der Standardbibliothek ist:

type Interface interface {
    sort.Interface
    Push(x interface{}) // add x as element Len()
    Pop() interface{}   // remove and return element Len() - 1.
}

Lassen Sie uns es jetzt als generische Schnittstelle umschreiben und den Standardtyp verwenden:

type Interface interface (T interface{}) {
    sort.Interface
    Push(x T) // add x as element Len()
    Pop() T   // remove and return element Len() - 1.
}

Dies bricht keine der Go 1.x-Codeserien da draußen. Eine Implementierung wäre mein Vorschlag für Type Alias ​​Rebinding. Aber ich bin mir sicher, dass es bessere Implementierungen geben kann.

Mit Standardtypen können wir generischen Code schreiben, der mit Code im Go 1.x-Stil verwendet werden kann. Und die Standardbibliothek kann zu einer generischen Bibliothek werden, ohne etwas zu beschädigen. Das ist großer Gewinn IMO.

@dc0d Sie schlagen also eine schrittweise Verbesserung vor? Was Sie vorschlagen, sieht für mich als inkrementelle Verbesserung gut aus, hat jedoch immer noch eine begrenzte generische Ausdruckskraft. Wie würden Sie die Schnittstellen "Collection" und "OrderedCollection" implementieren?

Bedenken Sie, dass mehrere partielle Spracherweiterungen zu einem komplexeren Endprodukt (mit mehreren alternativen Syntaxen) führen können, als wenn Sie die vollständige Lösung so einfach wie möglich implementieren.

@keean Ich verstehe den Teil requires Collection[T, U], Ord[U] nicht. Wie beschränken sie die Typparameter T und U ?

@dc0d Sie funktionieren genauso wie in einer Funktion, gelten aber für alles. Für jedes Paar von Typen TU, die eine OrderedCollection sind, müssen wir also TU auch eine Instanz von Collection und U Ord sein. Überall dort, wo wir also OrderedCollection verwenden, können wir gegebenenfalls Methoden aus Collection und Ord verwenden.

Wenn wir minimalistisch sind, sind diese nicht erforderlich, da wir die zusätzlichen Schnittstellen in die Funktionstypen aufnehmen können, wo wir sie benötigen, zum Beispiel:

type OrderedCollection interface(T, U)
{
   first(c T) U
}

func[T] first(c T[]) T requires Collection(T[], T), Ord T
{...}

func[T] f(c T[]) requires OrderedCollection(T[], T), Collection(T[], T), Ord(T)
{...}

Aber vielleicht besser lesbar:

type OrderedCollection interface(T, U) 
   requires Collection(T, U), Ord(U)
{
   first(c T) U
}

func[T] first(c T[]) T
{...}

func[T] f(c T[]) requires OrderedCollection(T[], T)
{...}

@keean (IMO) Solange es einen obligatorischen Standardwert für die Typparameter gibt, bin ich glücklich. Auf diese Weise ist es möglich, die Abwärtskompatibilität mit der Codeserie Go 1.x aufrechtzuerhalten. Das ist der Hauptpunkt, den ich versucht habe zu machen.

@Keean

Go-Schnittstellen sind keine Typklassen – sie sind lediglich Typen

Sie verhalten sich für mich nicht wie Typen, da sie Polymorphismus zulassen.

Ja, sie erlauben Subtyp-Polymorphismus . Go hat Subtyping über Schnittstellentypen. Es hat keine explizit deklarierten Subtyp-Hierarchien, aber das ist weitgehend orthogonal. Was Go nicht vollständig objektorientiert macht, ist die fehlende Vererbung.

Alternativ können Sie Schnittstellen als existenziell quantifizierte Anwendungen von Typklassen betrachten. Ich glaube, das haben Sie im Sinn. Das haben wir in Genus und Familia gemacht.

@andrewcmyers

Ja, sie erlauben Subtyp-Polymorphismus.

Soweit ich weiß, ist es invariant, es gibt keine Kovarianz oder Kontravarianz, dies spricht stark dafür, dass dies keine Subtypisierung ist. Polymorphe Typsysteme sind unveränderlich, daher scheint mir Go diesem Modell näher zu sein, und die Behandlung von Schnittstellen als Einzelparameter-Typklassen scheint eher der Einfachheit von Go zu entsprechen. Das Fehlen von Kovarianz und Kontravarianz ist ein großer Vorteil für Generika, sehen Sie sich nur die Verwirrung an, die solche Dinge in Sprachen wie C# verursachen:

https://docs.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance

Ich denke, Go sollte diese Art von Komplexität vollständig vermeiden. Für mich bedeutet dies, dass wir Generics und Subtyping nicht im selben Typsystem haben wollen.

Alternativ können Sie Schnittstellen als existenziell quantifizierte Anwendungen von Typklassen betrachten. Ich glaube, das haben Sie im Sinn. Das haben wir in Genus und Familia gemacht.

Da Go zur Laufzeit über Typinformationen verfügt, ist keine existenzielle Quantifizierung erforderlich. In Haskell sind Typen unboxed (wie native 'C'-Typen) und das bedeutet, sobald wir etwas in eine existenzielle Sammlung eingefügt haben, können wir den Typ des Inhalts nicht (leicht) wiederherstellen, alles, was wir tun können, ist die bereitgestellten Schnittstellen (Typ-Klassen ). Dies wird dadurch realisiert, dass neben den Rohdaten ein Zeiger auf die Schnittstellen gespeichert wird. In Go wird stattdessen der Datentyp gespeichert, die Daten sind „Boxed“ (wie in C# Boxed- und Unboxed-Daten). Als solches ist Go nicht nur auf die mit den Daten gespeicherten Schnittstellen beschränkt, da es möglich ist (durch Verwendung eines Type-Case), den Typ der Daten in der Sammlung wiederherzustellen, was nur in Haskell möglich ist, indem eine „Reflection“ implementiert wird. typeclass (obwohl es umständlich ist, die Daten herauszubekommen, ist es möglich, den Typ und die Daten zu serialisieren, um Strings zu sagen, und dann außerhalb der existentiellen Box zu deserialisieren). Meine Schlussfolgerung ist also, dass sich Go-Schnittstellen genau so verhalten, wie es Typklassen tun würden, wenn Haskell die Typklasse „Reflection“ als eingebaute Funktion bereitstellen würde. Als solches gibt es keine existenzielle Box, und wir können immer noch den Inhalt von Sammlungen typisieren, aber Schnittstellen verhalten sich genau wie Typklassen. Der Unterschied zwischen Haskell und Go liegt in der Semantik von geschachtelten und nicht geschachtelten Daten, und Schnittstellen sind Typklassen mit einzelnen Parametern. Wenn 'Go' eine Schnittstelle als Typ behandelt, tut es tatsächlich Folgendes:

Addable[] == exists T . T[] requires Addable[T], Reflection[T]

Es ist wahrscheinlich erwähnenswert, dass dies die gleiche Art und Weise ist, wie "Eigenschaftsobjekte" in Rust funktionieren.

Go kann Existenzialien (für den Programmierer sichtbar sein), Kovarianz und Kontravarianz vollständig vermeiden, was eine gute Sache ist, und das wird meiner Meinung nach Generika viel einfacher und leistungsfähiger machen.

Soweit ich weiß, ist es invariant, es gibt keine Kovarianz oder Kontravarianz, dies spricht stark dafür, dass dies keine Subtypisierung ist.

Polymorphe Typsysteme sind unveränderlich, daher scheint es mir näher an diesem Modell zu liegen, und die Behandlung von Schnittstellen als Einzelparameter-Typklassen scheint eher der Einfachheit von Go zu entsprechen.

Darf ich vorschlagen, dass Sie beide Recht haben? Insofern sind Schnittstellen äquivalent zu Typklassen, aber Typklassen sind eine Form der Untertypisierung. Die Definitionen der Subtypisierung, die ich bisher gefunden habe, sind alle ziemlich vage und ungenau und laufen auf "A ist ein Subtyp von B, wenn einer durch den anderen ersetzt werden kann" hinaus. Was meiner Meinung nach ziemlich leicht durch Typklassen befriedigt werden kann.

Beachten Sie, dass das Varianz-Argument an sich meiner Meinung nach nicht wirklich funktioniert. Varianz ist eine Eigenschaft von Typkonstruktoren, keine Sprache. Und es ist ziemlich normal, dass nicht alle Typkonstruktoren in einer Sprache variabel sind (z. B. haben viele Sprachen mit Subtypisierung veränderliche Arrays, die invariant sein müssen, um typsicher zu sein). Ich verstehe also nicht, warum Sie ohne Variantentypkonstruktoren keine Subtypisierung haben könnten.

Außerdem glaube ich, dass diese Diskussion für ein Problem im Go-Repository etwas zu weit gefasst ist. Hier sollte es nicht darum gehen, die Feinheiten von Typtheorien zu diskutieren, sondern darum, ob und wie Generika zu Go hinzugefügt werden können.

@Merovius- Varianz ist eine Eigenschaft, die der Untertypisierung zugeordnet ist. In Sprachen ohne Subtypisierung gibt es keine Varianz. Damit es überhaupt Varianz gibt, müssen Sie Subtyping haben, was das Kovarianz/Kontravarianz-Problem für Typkonstruktoren einführt. Sie haben jedoch Recht, dass es in einer Sprache mit Subtypisierung möglich ist, dass alle Typkonstruktoren invariant sind.

Typklassen sind definitiv keine Subtypen, da eine Typklasse kein Typ ist. Wir können 'Schnittstellentypen' in Go jedoch als das betrachten, was Rust ein 'Eigenschaftsobjekt' nennt, effektiv ein Typ, der von der Typklasse abgeleitet ist.

Die Semantik von Go scheint im Moment zu beiden Modellen zu passen, da es keine Varianz und implizite „Eigenschaftsobjekte“ gibt. Vielleicht ist Go also an einem Wendepunkt, Generika und das Typsystem könnten entlang der Linien der Subtypisierung entwickelt werden, Varianz einführen und am Ende so etwas wie Generika in C# haben. Alternativ könnte Go Schnittstellen mit mehreren Parametern einführen, die Schnittstellen für Sammlungen ermöglichen, und dies würde die unmittelbare Verbindung zwischen Schnittstellen und „Schnittstellentypen“ aufheben. Zum Beispiel, wenn Sie haben:

type (T, U) Collection interface {
    member : (c T, e U) Bool
    insert: (c T, e U) T
}

member(c int32[], e int32) Bool {...}
insert(c int32[], e int32) int32[] {...}

member(c float32[], e float32) Bool {...}
insert(c float32[], e float32) float32[] {...}

Es gibt keine offensichtliche Untertypbeziehung mehr zwischen den Typen T, U und der Schnittstelle Collection. Daher können Sie die Beziehung zwischen dem Instanztyp und den Schnittstellentypen nur als Untertypisierung für den Spezialfall von Einzelparameterschnittstellen betrachten, und wir können keine Abstraktionen von Dingen wie Sammlungen mit Einzelparameterschnittstellen ausdrücken.

Ich denke, für Generika muss man eindeutig in der Lage sein, Dinge wie Sammlungen zu modellieren, daher sind Schnittstellen mit mehreren Parametern ein Muss für mich. Ich denke jedoch, dass die Wechselwirkung zwischen Kovarianz und Kontravarianz in Generika ein übermäßig komplexes Typsystem schafft, daher möchte ich Subtypen vermeiden.

@keean Da Schnittstellen als Typen verwendet werden können und Typklassen keine Typen sind, ist die natürlichste Erklärung der Go-Semantik, dass Schnittstellen keine Typklassen sind. Ich verstehe, dass Sie argumentieren, Schnittstellen als Typklassen zu verallgemeinern; Ich denke, es ist eine vernünftige Richtung, die Sprache zu nehmen, und tatsächlich haben wir diesen Ansatz bereits ausführlich in unserer veröffentlichten Arbeit untersucht.

Um festzustellen, ob Go Subtyping hat, beachten Sie bitte den folgenden Code:

package main

type Cloneable interface {
    Clone() Cloneable
}

type CloneableZ interface {
    Clone() Cloneable
    zero() int
}

type S struct {}

func (t S) Clone() Cloneable {
    c := t
    return c
}

func (t S) zero() int {
    return 0
}

var x CloneableZ = S{}
var y Cloneable = x

func main() {
    print("ok\n")
}

Die Zuordnung von x zu y zeigt, dass der Typ von y verwendet werden kann, wo der Typ von x erwartet wird. Dies ist eine Subtypisierungsbeziehung, nämlich: CloneableZ <: Cloneable und auch S <: CloneableZ . Selbst wenn Sie Schnittstellen in Bezug auf Typklassen erklärt haben, wäre hier immer noch eine Subtypisierungsbeziehung im Spiel, etwa S <: ∃T.CloneableZ[T] <: ∃T.Cloneable[T] .

Beachten Sie, dass es für Go absolut sicher wäre, der Funktion Clone zu erlauben, ein S zurückzugeben, aber Go erzwingt zufälligerweise unnötig restriktive Regeln für die Konformität mit Schnittstellen: tatsächlich dieselben Regeln wie Java ursprünglich durchgesetzt. Die Untertypisierung erfordert keine nicht-invarianten Typkonstruktoren, wie @Merovius beobachtet hat.

@andrewcmyers Was passiert mit Schnittstellen mit mehreren Parametern, wie sie zum Abstrahieren von Sammlungen erforderlich sind?

Darüber hinaus kann die Zuweisung von x nach y als Demonstration der Schnittstellenvererbung ohne jegliche Untertypisierung angesehen werden. In Haskell (das eindeutig keine Untertypisierung hat) würden Sie schreiben:

class Cloneable t => CloneableZ t where...

Wo wir x haben, ist ein Typ, der CloneableZ implementiert, der per Definition auch Cloneable implementiert, also offensichtlich y zugewiesen werden kann.

Zusammenfassend lässt sich sagen, dass Sie entweder eine Schnittstelle als Typ und Go betrachten können, um eine begrenzte Untertypisierung ohne kovariante oder kontravariante Typkonstruktoren zu haben, oder Sie können sie als "Eigenschaftsobjekt" betrachten, oder vielleicht würden wir sie in Go als " Schnittstellenobjekt", das effektiv ein polymorpher Container ist, der durch eine Schnittstellen-"Typklasse" eingeschränkt wird. Im Typklassenmodell gibt es keine Subtypisierung und daher keinen Grund, sich Gedanken über Kovarianz und Kontravarianz zu machen.

Wenn wir beim Subtyping-Modell bleiben, können wir keine Sammlungstypen haben, deshalb musste C++ Templates einführen, weil die objektorientierte Subtypisierung nicht ausreicht, um Konzepte wie Container generisch zu definieren. Am Ende haben wir zwei Mechanismen für die Abstraktion, Objekte und Subtypisierung sowie Templates/Traits und Generika, und die Interaktionen zwischen den beiden werden komplex, sehen Sie sich zum Beispiel C++, C# und Scala an. Es wird weiterhin Forderungen geben, kovariante und kontravariante Konstruktoren einzuführen, um die Leistungsfähigkeit von Generika im Einklang mit diesen anderen Sprachen zu erhöhen.

Wenn wir generische Sammlungen wollen, ohne ein separates generisches System einzuführen, dann sollten wir an Schnittstellen wie Typklassen denken. Schnittstellen mit mehreren Parametern würden bedeuten, nicht mehr über Subtypisierung nachzudenken und stattdessen über Schnittstellenvererbung nachzudenken. Wenn wir Generika in Go verbessern und Abstraktionen von Dingen wie Sammlungen zulassen wollen und wir nicht die Komplexität der Typsysteme von Sprachen wie C++, C#, Scala usw. wollen, dann sind Multiparameter-Schnittstellen und Schnittstellenvererbung der Weg gehen.

@Keean

Was passiert mit Schnittstellen mit mehreren Parametern, wie sie zum Abstrahieren von Sammlungen erforderlich sind?

Bitte lesen Sie unsere Artikel zu Genus und Familia, die Multiparameter-Type Constraints unterstützen. Familia vereinheitlicht diese Einschränkungen mit Schnittstellen und ermöglicht Schnittstellen, mehrere Typen einzuschränken.

Wenn wir beim Subtyping-Modell bleiben, können wir keine Sammlungstypen haben

Ich bin mir nicht ganz sicher, was Sie mit "dem Subtyping-Modell" meinen, aber es ist ziemlich klar, dass Java und C # Sammlungstypen haben, daher ergibt diese Behauptung für mich keinen Sinn.

Wo wir haben, ist x ein Typ, der CloneableZ implementiert, das per Definition auch Cloneable implementiert, also offensichtlich y zugewiesen werden kann.

Nein, in meinem Beispiel ist x eine Variable und y eine andere Variable. Wenn ich weiß, dass y ein Typ CloneableZ ist und x ein Typ Cloneable ist, bedeutet das nicht, dass ich von y zu x zuweisen kann. Das tut mein Beispiel.

Um zu verdeutlichen, dass zum Modellieren von Go eine Untertypisierung erforderlich ist, finden Sie unten eine geschärfte Version des Beispiels, dessen moralisches Äquivalent in Haskell keine Typprüfung durchführt. Das Beispiel zeigt, dass die Subtypisierung die Erstellung heterogener Sammlungen ermöglicht, in denen verschiedene Elemente unterschiedliche Implementierungen haben. Darüber hinaus ist die Menge möglicher Implementierungen offen.

type Cloneable interface {
    Clone() Cloneable
}

type CloneableZ interface {
    Clone() Cloneable
    zero() int
}

type S struct {}

func (t S) Clone() Cloneable {
    c := t
    return c
}

type T struct { x int }

func (t T) Clone() Cloneable {
    c := t
    return c
}

func (t S) zero() int {
    return 0
}

var x CloneableZ = S{}
var y Cloneable = T{}
var a [2]Cloneable = [2]Cloneable{x, y}

@andrewcmyers

Ich bin mir nicht ganz sicher, was Sie mit "dem Subtyping-Modell" meinen, aber es ist ziemlich klar, dass Java und C # Sammlungstypen haben, daher ergibt diese Behauptung für mich keinen Sinn.

Sehen Sie sich an, warum C++ Vorlagen entwickelt hat, das OO-Subtypisierungsmodell war nicht in der Lage, die generischen Konzepte auszudrücken, die für die Verallgemeinerung von Dingen wie Sammlungen erforderlich sind. C# und Java mussten auch ein vollständiges generisches System getrennt von Objekten, Subtypisierung und Vererbung einführen und dann das Durcheinander der komplexen Interaktionen der beiden Systeme mit Dingen wie kovarianten und kontravarianten Typkonstruktoren aufräumen. Im Nachhinein können wir die OO-Subtypisierung vermeiden und uns stattdessen ansehen, was passiert, wenn wir Schnittstellen (Typklassen) zu einer einfach typisierten Sprache hinzufügen. Dies ist, was Rust getan hat, also lohnt es sich, einen Blick darauf zu werfen, aber natürlich wird es durch die ganze Lebenszeit kompliziert. Go hat GC, also hätte es nicht diese Komplexität. Mein Vorschlag ist, dass Go erweitert werden kann, um Schnittstellen mit mehreren Parametern zu ermöglichen und diese Komplexität zu vermeiden.

In Bezug auf Ihre Behauptung, dass Sie dieses Beispiel nicht in Haskell ausführen können, hier ist der Code:

{-# LANGUAGE ExistentialQuantification #-}

class ICloneable t where
    clone :: t -> t

class ICloneable t => ICloneableZ t where
    zero :: t

data S = S deriving Show

instance ICloneable S where
    clone x = x

data T = T Int deriving Show

instance ICloneable T where
    clone x = x

instance ICloneableZ T where
    zero = T 0

data Cloneable = forall a . (ICloneable a, Show a) => ToCloneable a

instance Show Cloneable where
    show (ToCloneable x) = show x

main = do
    x <- return S
    y <- return (T 27)
    a <- return [ToCloneable x, ToCloneable y]
    putStrLn (show a)

Einige interessante Unterschiede, Go leitet diesen Typ data Cloneable = forall a . (ICloneable a, Show a) => ToCloneable a automatisch ab, da Sie auf diese Weise eine Schnittstelle (die keinen Speicher hat) in einen Typ (der Speicher hat) umwandeln. Rust leitet diese Typen ebenfalls ab und nennt sie "Trait-Objekte". . In anderen Sprachen wie Java, C# und Scala stellen wir fest, dass Sie Schnittstellen nicht instanziieren können, was eigentlich "richtig" ist, Schnittstellen sind keine Typen, sie haben keinen Speicher, Go leitet den Typ eines existenziellen Containers automatisch für Sie ab, damit Sie ihn behandeln können die Schnittstelle wie einen Typ, und Go verbirgt dies vor Ihnen, indem es dem existenziellen Container denselben Namen gibt wie der Schnittstelle, von der er abgeleitet ist. Die andere zu beachtende Sache ist, dass dieses [2]Cloneable{x, y} alle Mitglieder zu Cloneable $ zwingt, während Haskell keine solchen impliziten Zwänge hat und wir die Mitglieder explizit mit ToCloneable zwingen müssen .

Ich wurde auch darauf hingewiesen, dass wir S und T nicht als Subtypen von Cloneable betrachten sollten, da S und T keine sind baulich kompatibel. Wir können buchstäblich jeden Typ als Instanz von Cloneable deklarieren (indem Sie einfach die relevante Definition der Funktion clone in Go deklarieren), und diese Typen müssen überhaupt keine Beziehung zueinander haben.

Die meisten Vorschläge für Generics scheinen zusätzliche Token zu enthalten, was meiner Meinung nach die Lesbarkeit und das einfache Gefühl von Go beeinträchtigt. Ich würde gerne eine andere Syntax vorschlagen, von der ich denke, dass sie möglicherweise gut mit der vorhandenen Grammatik von Go funktionieren könnte (es passiert sogar, dass die Syntaxhervorhebung in Github Markdown ziemlich gut funktioniert).

Die wichtigsten Punkte des Vorschlags:

  • Die Grammatik von Go scheint immer eine einfache Möglichkeit zu haben, um festzustellen, wann eine Typdeklaration beendet ist, weil es ein bestimmtes Token oder Schlüsselwort gibt, nach dem wir suchen. Wenn dies in allen Fällen zutrifft, können Typargumente einfach nach den Typnamen selbst hinzugefügt werden.
  • Wie bei den meisten Vorschlägen bedeutet derselbe Bezeichner in jeder Funktionsdeklaration denselben Typ. Diese Bezeichner entkommen niemals der Deklaration.
  • In den meisten Vorschlägen müssen Sie generische Typargumente deklarieren, aber in diesem Vorschlag ist dies implizit. Einige Leute werden behaupten, dass dies die Lesbarkeit oder Klarheit beeinträchtigt (Implizitheit ist schlecht) oder die Fähigkeit einschränkt, einen Typ zu benennen, es folgen Widerlegungen:

    • Wenn es darum geht, die Lesbarkeit zu beeinträchtigen, kann man meiner Meinung nach so oder so argumentieren, das Extraoder [T] beeinträchtigt die Lesbarkeit genauso, indem es viel syntaktisches Rauschen verursacht.

    • Implizitheit kann bei richtiger Anwendung dazu beitragen, dass eine Sprache weniger ausführlich ist. Wir verzichten ständig auf Typdeklarationen mit := , weil die dadurch verborgenen Informationen einfach nicht wichtig genug sind, um sie jedes Mal zu buchstabieren.

    • Die Benennung eines konkreten (nicht generischen) Typs a oder t ist wahrscheinlich eine schlechte Praxis, daher geht dieser Vorschlag davon aus, dass es sicher ist, diese Bezeichner als generische Typargumente zu reservieren. Obwohl dies vielleicht eine Go-Fix-Migration erfordern würde?

package main

import "fmt"

type LinkedList a struct {
  Head *Node a
  Tail *Node a
}

type Node a {
  Next *Node a
  Prev *Node a

  Value a
}

func main() {
  // Not sure about how recursive we could get with the inference
  ll := LinkedList string {
    // The string bit could be inferred
    Head: Node string { Value: "hello world" },
  }
}

func (l *LinkedList a) Append(value a) {
  newNode := &Node{Value: value}

  if l.Tail == nil {
    l.Head = newNode
    l.Tail = l.Head
    return
  }

  l.Tail.Next = newNode
  l.Tail = l.Tail.Next
}

Dies stammt aus einem Gist, der etwas mehr Details sowie hier vorgeschlagene Summentypen enthält: https://gist.github.com/aarondl/9b950373642fcf5072942cf0fca2c3a2

Dies ist kein vollständig ausgespülter Generika-Vorschlag und soll es auch nicht sein, es gibt eine Menge Probleme zu lösen, um Generika zu Go hinzufügen zu können. Dieser befasst sich nur mit der Syntax, und ich hoffe, wir können uns darüber unterhalten, ob das, was vorgeschlagen wird, machbar / wünschenswert ist oder nicht.

@aarondl
Sieht für mich gut aus, mit dieser Syntax hätten wir:

type Collection a b interface {
   member(c a, e b) Bool
   insert(c a, e b) a
}

func insert(c *LinkedList a, e a) *LinkedList a {
   c.Append(e)
   return c
}

@keean Würden Sie bitte den Typ Collection etwas erklären. Ich verstehe es nicht:

type Collection a b interface {
   member(c a, e b) Bool
   insert(c a, e b) a
}

@dc0d Collection ist eine Schnittstelle, die _alle_ Sammlungen abstrahiert, also Bäume, Listen, Slices usw., sodass wir generische Operationen wie member und insert haben können, die mit jeder Sammlung funktionieren, die einen beliebigen Datentyp enthält. Oben habe ich das Beispiel für die Definition von „insert“ für den LinkedList-Typ im vorherigen Beispiel gegeben:

func insert(c *LinkedList a, e a) *LinkedList a {
   c.Append(e)
   return c
}

Wir könnten es auch für ein Slice definieren

func insert(c []a, e a) []a {
   return append(c, e)
}

Wir brauchen jedoch nicht einmal die Art von parametrischen Funktionen mit Typvariablen, wie sie von @aarondl mit dem polymorphen Typ a veranschaulicht werden, damit dies funktioniert, da Sie einfach für konkrete Typen definieren können:

func insert(c *LinkedList int, e int) *LinkedList int {
   c.Append(e)
   return c
}

func insert(c *LinkedList float, e float) *LinkedList float {
   c.Append(e)
   return c
}

func insert(c int[], e int) int[] {
   return append(c, e)
}

func insert(c float[], e float) float[] {
   return append(c, e)
}

Collection ist also eine Schnittstelle zur Verallgemeinerung sowohl des Typs eines Containers als auch des Typs seines Inhalts, wodurch generische Funktionen geschrieben werden können, die auf alle Kombinationen von Container und Inhalt wirken.

Es gibt keinen Grund, warum Sie nicht auch einen Teil der Sammlungen []Collection haben könnten, in denen die Inhalte alle unterschiedliche Sammlungstypen mit unterschiedlichen Wertetypen wären, vorausgesetzt, dass member und insert für jede Kombination definiert wurden .

@aarondl Angesichts der Tatsache, dass type LinkedList a bereits eine gültige Typdeklaration ist, sehe ich nur zwei Möglichkeiten, dies eindeutig parsbar zu machen: Die Grammatik kontextsensitiv machen (in die Probleme beim Analysieren von C einsteigen, ugh) oder unbegrenztes Lookahead verwenden ( was die Go-Grammatik aufgrund schlechter Fehlermeldungen im Fehlerfall eher vermeidet). Ich verstehe vielleicht etwas falsch, aber meiner Meinung nach spricht das gegen einen tokenlosen Ansatz.

@keean Schnittstellen in Go verwenden Methoden, keine Funktionen. In der von Ihnen vorgeschlagenen spezifischen Syntax gibt es nichts, was insert *LinkedList anfügt (in Haskell geschieht dies über instance -Deklarationen). Es ist auch normal, dass Methoden den Wert verändern, mit dem sie arbeiten. Nichts davon ist ein Show-Stopper, sondern weist nur darauf hin, dass die von Ihnen vorgeschlagene Syntax mit Go nicht gut funktioniert. Wahrscheinlich eher so etwas wie

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

Dies zeigt auch ein paar weitere Fragen in Bezug darauf, wie die Typparameter abgegrenzt sind und wie diese analysiert werden sollten.

@aarondl , es gibt auch noch weitere Fragen, die ich zu deinem Vorschlag hätte. Beispielsweise sind keine Einschränkungen zulässig, sodass Sie nur uneingeschränkten Polymorphismus erhalten. Was im Allgemeinen nicht wirklich nützlich ist , da Sie mit den Werten, die Sie erhalten, nichts tun dürfen (zB Sie könnten Collection nicht mit einer Map implementieren, da nicht alle Typen gültige Map-Schlüssel sind). Was soll passieren, wenn jemand versucht, so etwas zu tun? Wenn es sich um einen Kompilierungsfehler handelt, beschwert er sich über die Instanziierung (C++-Fehlermeldungen voraus) oder über die Definition (man kann im Grunde nichts tun, weil es nichts gibt, das mit allen Typen funktioniert)?

@keean Ich verstehe immer noch nicht, wie a darauf beschränkt ist, eine Liste (oder ein Slice oder eine andere Sammlung) zu sein. Ist das eine kontextabhängige Spezialgrammatik für Sammlungen? Wenn ja, welchen Wert hat es? Es ist nicht möglich, benutzerdefinierte Typen auf diese Weise zu deklarieren.

@Merovius Bedeutet das, dass Go keinen Mehrfachversand durchführen kann und das erste Argument einer 'Funktion' zu etwas Besonderem macht? Dies deutet darauf hin, dass zugeordnete Typen besser geeignet wären als Schnittstellen mit mehreren Parametern. Etwas wie das:

type Collection interface {
   type Element
   Member(e Element) Bool
   Insert(e Element) Collection
}

type IntSlice struct {
    value []Int,
}

type IntSlice.Element = Int

func (IntSlice) Member(e Int) Bool {...}
func (IntSlice) Insert(e Int) IntSlice {...}

func useIt(c Collection, e Collection.Element) {...}

Dies hat jedoch immer noch Probleme, da es nichts gibt, was die beiden Sammlungen dazu zwingt, derselbe Typ zu sein ... Sie würden am Ende so etwas brauchen wie:

func[A] useIt(c A, e A.Element) requires A:Collection

Um zu versuchen, den Unterschied zu erklären, Multiparameter-Schnittstellen haben zusätzliche _Eingabe_-Typen, die an der Instanzauswahl teilnehmen (daher die Verbindung mit Mehrfachversand), während assoziierte Typen _Ausgabe_-Typen sind, nur der Empfängertyp an der Instanzauswahl teilnimmt, und dann die zugehörigen Typen hängen vom Typ des Empfängers ab.

@dc0d a und b sind Typparameter der Schnittstelle, genau wie in einer Haskell-Typklasse. Damit etwas als Collection betrachtet werden kann, muss es die Methoden definieren, die mit den Typen in der Schnittstelle übereinstimmen, wobei a und b ein beliebiger Typ sein können. Wie @Merovius jedoch betont hat, sind Go-Schnittstellen methodenbasiert und unterstützen keinen Mehrfachversand, sodass Schnittstellen mit mehreren Parametern möglicherweise nicht gut geeignet sind. Mit dem Single-Dispatch-Methodenmodell von Go scheint es besser zu passen, zugeordnete Typen in Schnittstellen anstelle von mehreren Parametern zu haben. Das Fehlen von Mehrfachversand erschwert jedoch die Implementierung von Funktionen wie unify(x, y) , und Sie müssen das Doppelversandmuster verwenden, was nicht sehr schön ist.

Um das Multi-Parameter-Ding etwas weiter zu erklären:

type Cloneable[A] interface {
   clone(x A) A
}

Hier steht a für einen beliebigen Typ, es ist uns egal, was es ist, solange die richtigen Funktionen definiert sind, betrachten wir es als Cloneable . Wir würden Schnittstellen als Einschränkungen für Typen und nicht als Typen selbst betrachten.

func clone(x int) int {...}

im Fall von „Klon“ ersetzen wir in der Schnittstellendefinition a durch int , und wir können Klon aufrufen, wenn die Ersetzung erfolgreich ist. Das passt gut zu dieser Notation:

func[A] test(x A) A requires Cloneable[A] {...}

Dies ist äquivalent zu:

type Cloneable interface {
   clone() Cloneable
}

aber deklariert eine Funktion, keine Methode, und kann mit mehreren Parametern erweitert werden. Wenn Sie eine Sprache mit Mehrfachversand haben, ist das erste Argument einer Funktion/Methode nichts Besonderes, also warum schreiben Sie es an einer anderen Stelle.

Da Go keinen Mehrfachversand hat, fühlt sich das alles so an, als wäre es zu viel, um es auf einmal zu ändern. Es scheint, als würden assoziierte Typen besser passen, wenn auch eingeschränkter. Dies würde abstrakte Sammlungen ermöglichen, aber keine eleganten Lösungen für Dinge wie die Vereinheitlichung.

@Merovius Vielen Dank, dass Sie sich den Vorschlag angesehen haben. Lassen Sie mich versuchen, auf Ihre Bedenken einzugehen. Ich bin traurig, dass Sie den Vorschlag abgelehnt haben, bevor wir ihn weiter diskutiert haben. Ich hoffe, ich kann Ihre Meinung ändern - oder vielleicht können Sie meine ändern :)

Unbegrenzte Vorausschau:
Wie ich im Vorschlag erwähnt habe, scheint es derzeit so, als ob die Go-Grammatik eine gute Möglichkeit hat, das "Ende" von so ziemlich allem syntaktisch zu erkennen. Und wir würden es aufgrund der impliziten generischen Argumente immer noch tun. Ein einzelner Kleinbuchstabe ist das syntaktische Konstrukt, das dieses generische Argument erzeugt - oder was auch immer wir entscheiden, um dieses Inline-Token zu erstellen, vielleicht greifen wir sogar auf ein tokenisiertes Ding wie @a im Vorschlag zurück, wenn uns die Syntax genug gefällt, aber das ist es nicht Mögliche Compiler-Schwierigkeit ohne Token, obwohl der Vorschlag viel Charme verliert, sobald Sie das tun.

Unabhängig davon ist das Problem mit type LinkedList a unter diesem Vorschlag nicht so schwer, da wir wissen, dass a ein generisches Typargument ist und dies daher mit einem Compiler-Fehler fehlschlagen würde, der dem von type LinkedList schlägt heute fehl mit: prog.go:3:16: expected type, found newline (and 1 more errors) . Der ursprüngliche Beitrag kam nicht wirklich heraus und sagte es, aber Sie dürfen keinen konkreten Typ [a-z]{1} mehr nennen, der meiner Meinung nach dieses Problem löst und ein Opfer ist, mit dem wir alle einverstanden wären machen (ich kann heute nur Nachteile beim Erstellen echter Typen mit Einzelbuchstabennamen im Go-Code sehen).

Es ist nur unbeschränkter Polymorphismus
Der Grund, warum ich jegliche Art von Merkmalen oder Einschränkungen für generische Argumente weggelassen habe, ist, dass ich der Meinung bin, dass dies die Rolle von Schnittstellen in Go ist. Wenn Sie etwas mit einem Wert tun möchten, sollte dieser Wert ein Schnittstellentyp und kein vollständig generischer Typ sein. Ich denke, dieser Vorschlag spielt auch gut mit Schnittstellen.

Bei diesem Vorschlag hätten wir immer noch das gleiche Problem wie jetzt mit Operatoren wie + , sodass Sie keine generische Addierfunktion für alle numerischen Typen erstellen könnten, aber Sie könnten eine generische Addierfunktion als Argument akzeptieren. Folgendes berücksichtigen:

func Sort(slice []a, compare func (a, a) bool) { ... }

Fragen zum Geltungsbereich

Du hast hier ein Beispiel gegeben:

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

Der Geltungsbereich dieser Bezeichner ist in der Regel an die jeweilige Deklaration/Definition gebunden, in der sie sich befinden. Sie werden nirgendwo geteilt, und ich sehe keinen Grund dafür.

@keean Das ist sehr interessant, obwohl Sie, wie andere darauf hingewiesen haben, ändern müssten, was Sie dort gezeigt haben, um die Schnittstellen tatsächlich implementieren zu können (derzeit gibt es in Ihrem Beispiel keine Methoden mit Empfängern, nur Funktionen). Ich versuche, mehr darüber nachzudenken, wie sich dies auf meinen ursprünglichen Vorschlag auswirkt.

Ein einzelner Kleinbuchstabe ist das syntaktische Konstrukt, das dieses generische Argument erzeugt

Ich fühle mich dabei nicht gut; Es erfordert je nach Kontext separate Produktionen für das, was ein Bezeichner ist, und bedeutet auch, bestimmte Bezeichner für Typen willkürlich zu verbieten. Aber es ist nicht wirklich die Zeit, über diese Details zu sprechen.

Bei diesem Vorschlag hätten wir immer noch das gleiche Problem wie jetzt mit Operatoren wie +

Ich verstehe diesen Satz nicht. Derzeit hat der +-Operator keines dieser Probleme, da die Typen seiner Operanden lokal bekannt sind und die Fehlermeldung klar und eindeutig ist und auf die Ursache des Problems hinweist. Gehe ich richtig in der Annahme, dass Sie sagen, dass Sie die Verwendung von generischen Werten verbieten möchten, die nicht für alle möglichen Typen zulässig sind (ich kann mir nicht viele solcher Operationen vorstellen)? Und einen Compilerfehler für den anstößigen Ausdruck in der generischen Funktion erstellen? Meiner Meinung nach würde das den Wert von Generika zu sehr einschränken.

Wenn Sie etwas mit einem Wert machen möchten, sollte dieser Wert ein Schnittstellentyp und kein vollständig generischer Typ sein.

Die beiden Hauptgründe, warum Menschen Generika wollen, sind Leistung (vermeiden Sie das Umschließen von Schnittstellen) und Typsicherheit (stellen Sie sicher, dass derselbe Typ an verschiedenen Stellen verwendet wird, ohne sich darum zu kümmern, um welchen es sich handelt). Dies scheint diese Gründe zu ignorieren.

Sie könnten eine generische add-Funktion als Argument akzeptieren.

Wahr. Aber ziemlich unergonomisch. Bedenken Sie, wie viele Beschwerden es über die sort -API gab. Für viele generische Container scheint die Menge an Funktionen, die der Aufrufer implementieren und übergeben müsste, unerschwinglich zu sein. Überlegen Sie, wie würde eine container/heap -Implementierung unter diesem Vorschlag aussehen und wie wäre sie in Bezug auf die Ergonomie besser als die aktuelle Implementierung? Es scheint, dass die Gewinne hier bestenfalls vernachlässigbar sind. Sie müssten mehr triviale Funktionen implementieren (und an jeder Verwendungsstelle duplizieren/referenzieren), nicht weniger.

@Merovius

Denken Sie über diesen Punkt von @aarondl nach

Sie könnten eine generische add-Funktion als Argument akzeptieren.

Es wäre besser, eine Addable-Schnittstelle zu haben, um das Überladen von Additionen zu ermöglichen, vorausgesetzt, es gibt eine Syntax zum Definieren von Infix-Operatoren:

type Addable interface {
   + (x Addable, y Addable) Addable
}

Leider funktioniert das nicht, weil es nicht ausdrückt, dass wir erwarten, dass alle Typen gleich sind. Um Addable zu definieren, bräuchten wir so etwas wie die Multi-Parameter-Schnittstellen:

type Addable[A] interface {
   + (x A, y A) A
}

Dann müssten Sie auch Go für den Mehrfachversand benötigen, was bedeuten würde, dass alle Argumente in einer Funktion wie ein Empfänger für den Schnittstellenabgleich behandelt werden. Im obigen Beispiel ist also jeder Typ Addable , wenn eine Funktion + darauf definiert ist, die die Funktionsdefinitionen in der Schnittstellendefinition erfüllt.

Aber angesichts dieser Änderungen könnte man jetzt schreiben:

type S struct {
   value: int
}

func (+) (x S, y S) S {
   return S {
      value: x.value + y.value
   }
}

func main() {
    println(S {value: 27} + S {value: 5})
}

Natürlich ist das Überladen von Funktionen und das mehrfache Dispatch etwas, das die Leute in Go nie wollen, aber dann werden Dinge wie das Definieren grundlegender Arithmetik für benutzerdefinierte Typen wie Vektoren, Matrizen, komplexe Zahlen usw. immer unmöglich sein. Wie ich oben sagte, würden "assoziierte Typen" auf Schnittstellen eine gewisse Steigerung der generischen Programmierfähigkeit ermöglichen, aber keine vollständige Allgemeingültigkeit. Ist Mehrfachversand (und vermutlich Funktionsüberlastung) etwas, das jemals in Go passieren könnte?

Dinge wie das Definieren grundlegender Arithmetik für benutzerdefinierte Typen wie Vektoren, Matrizen, komplexe Zahlen usw. werden immer unmöglich sein.

Manche halten das vielleicht für ein Feature :) AFAIR, irgendwo schwirrt ein Vorschlag oder Thread herum, in dem diskutiert wird, ob es so sein sollte. FWIW, ich denke, das wandert - wieder - vom Thema ab. Das Überladen von Operatoren (oder allgemeine Ideen, wie man Go zu mehr Haskell macht) ist nicht wirklich der Sinn dieser Ausgabe :)

Ist Mehrfachversand (und vermutlich Funktionsüberlastung) etwas, das jemals in Go passieren könnte?

Sag niemals nie. Ich persönlich würde es aber nicht erwarten.

@Merovius

Manche halten das vielleicht für ein Feature :)

Sicher, und wenn Go es nicht tut, gibt es andere Sprachen, die es tun werden :-) Go muss nicht alles für alle sein. Ich habe nur versucht, in Go einen Spielraum für Generika zu schaffen. Mein Fokus liegt auf der Erstellung vollständig generischer Sprachen, da ich eine Abneigung dagegen habe, mich selbst und Textbausteine ​​zu wiederholen (und ich mag keine Makros). Wenn ich einen Cent für jedes Mal hätte, wenn ich eine verknüpfte Liste oder einen Baum in 'C' für einen bestimmten Datentyp schreiben müsste. Es macht einige Projekte für ein kleines Team tatsächlich unmöglich, da die Menge an Code im Kopf behalten werden muss, um ihn zu verstehen, und dann durch Änderungen beibehalten werden muss. Manchmal denke ich, dass Leute, die keinen Bedarf an Generika haben, einfach noch kein ausreichend großes Programm geschrieben haben. Natürlich können Sie stattdessen ein großes Team von Entwicklern an etwas arbeiten lassen und jeden Entwickler nur für einen kleinen Teil des gesamten Codes verantwortlich machen, aber ich bin daran interessiert, einen einzelnen Entwickler (oder ein kleines Team) so effektiv wie möglich zu machen.

Angesichts der Tatsache, dass das Überladen von Funktionen und der mehrfache Versand außerhalb des Geltungsbereichs liegen, und angesichts der Parsing-Probleme mit dem Vorschlag von @aarondl scheint es, dass das Hinzufügen zugeordneter Typen zu Schnittstellen und Typparametern zu Funktionen so weit ist, wie Sie möchten go mit Generika in Go.

So etwas scheint das Richtige zu sein:

type Collection interface {
   type Element
   Member(e Element) Bool
   Insert(e Element) Collection
}

type IntSlice struct {
    value []Int,
}

type IntSlice.Element = Int

func (IntSlice) Member(e Int) Bool {...}
func (IntSlice) Insert(e Int) IntSlice {...}

func useIt<T>(c T, e T.Element) requires T:Collection {...}

Dann würde in der Implementierung entschieden, ob parametrische Typen oder universell quantifizierte Typen verwendet werden. Bei parametrischen Typen (wie Java) ist eine "generische" Funktion eigentlich keine Funktion, sondern eine Art typsichere Funktionsvorlage und kann als solche nicht als Argument übergeben werden, es sei denn, ihr Typparameter ist so angegeben:

f(useIt) // not okay with parametric types
f(useIt<List>) // okay with parametric types

Bei universell quantifizierten Typen können Sie useIt als Argument übergeben und es kann dann innerhalb f mit einem Typparameter versehen werden. Der Grund, parametrische Typen zu bevorzugen, liegt darin, dass Sie den Polymorphismus zur Kompilierzeit monomorphisieren können, was bedeutet, dass zur Laufzeit keine polymorphen Funktionen entwickelt werden müssen. Ich bin mir nicht sicher, ob dies bei Go ein Problem darstellt, da Go bereits Laufzeit-Dispatching auf Schnittstellen durchführt. Solange also der Typparameter für useIt Collection implementiert, können Sie zur Laufzeit an den richtigen Empfänger senden, also universell Quantifizierung ist wahrscheinlich der richtige Weg für Go.

Ich frage mich, SFINAE wird nur von @bcmills erwähnt. Nicht einmal im Vorschlag erwähnt (obwohl Sort als Beispiel da ist).
Wie könnte dann das Sort für Slice und Linkedlist aussehen?

@Keean
Ich kann nicht herausfinden, wie man mit Ihrem Vorschlag eine generische 'Slice'-Sammlung definieren würde. Sie scheinen ein 'IntSlice' zu definieren, das möglicherweise 'Collection' implementiert (obwohl Insert einen anderen Typ als den von der Schnittstelle gewünschten zurückgibt), aber das ist kein generisches 'Slice', da es nur für Ints zu sein scheint , und die Methodenimplementierungen sind nur für ints. Müssen wir eine spezifische Implementierung pro Typ definieren?

Manchmal denke ich, dass Leute, die keinen Bedarf an Generika haben, einfach noch kein ausreichend großes Programm geschrieben haben.

Ich kann Ihnen versichern, dass dieser Eindruck falsch ist. Und FWIW, ISTM, dass „die andere Seite“ „die Notwendigkeit nicht sieht“ in denselben Eimer stellt wie „den Nutzen nicht sieht“. Ich sehe den Nutzen und widerlege ihn nicht. Allerdings sehe ich die Notwendigkeit nicht wirklich. Mir geht es gut ohne, auch in großen Codebasen.

Und verwechseln Sie „zu wollen, dass sie richtig gemacht werden, und darauf hinzuweisen, wo bestehende Vorschläge nicht sind“ auch nicht mit „grundsätzlich gegen die eigentliche Idee“.

auch angesichts der Parsing-Probleme mit dem Vorschlag von @aarondl .

Wie gesagt, ich glaube nicht, dass es im Moment wirklich produktiv ist, über das Parsing-Problem zu sprechen. Parsing-Probleme können gelöst werden. Semantisch gesehen ist das Problem des eingeschränkten Polymorphismus viel schwerwiegender. IMO, das Hinzufügen von Generika ohne das ist die Mühe nicht wirklich wert.

@urandom

Ich kann nicht herausfinden, wie man mit Ihrem Vorschlag eine generische 'Slice'-Sammlung definieren würde.

Wie oben angegeben, müssten Sie immer noch eine separate Implementierung für jeden Slice-Typ definieren, aber Sie würden immer noch davon profitieren, Algorithmen in Bezug auf die generische Schnittstelle schreiben zu können. Wenn Sie eine generische Implementierung für alle Slices zulassen wollten, müssten Sie parametrisch zugeordnete Typen und Methoden zulassen. Hinweis: Ich habe den Typparameter nach dem Schlüsselwort verschoben, sodass er vor dem Empfängertyp auftritt.

type<T> []T.Element = Int

func<T> ([]T) Member(e T) Bool {...}
func<T> ([]T) Insert(e T) Collection {...}

Allerdings müssen Sie sich jetzt auch mit der Spezialisierung befassen, da jemand den zugehörigen Typ und die Methoden für das spezialisiertere []int definieren könnte und Sie sich damit befassen müssten, welche Sie verwenden. Normalerweise würden Sie sich für die spezifischere Instanz entscheiden, aber es fügt eine weitere Ebene der Komplexität hinzu.

Ich bin mir nicht sicher, wie viel Ihnen das tatsächlich bringt. Mit meinem ursprünglichen Beispiel oben können Sie generische Algorithmen schreiben, um mit der Schnittstelle auf allgemeine Sammlungen einzuwirken, und Sie müssten nur die Methoden und zugehörigen Typen für die Typen bereitstellen, die Sie tatsächlich verwenden. Der größte Vorteil für mich besteht darin, dass ich Algorithmen wie das Sortieren beliebiger Sammlungen definieren und diese Algorithmen in einer Bibliothek ablegen kann. Wenn ich dann eine Liste von "Formen" habe, muss ich nur die Sammlungsschnittstellenmethoden für meine Liste von Formen definieren, und ich kann dann jeden Algorithmus in der Bibliothek darauf anwenden. Die Schnittstellenmethoden für alle Slice-Typen definieren zu können, interessiert mich weniger und ist für Go möglicherweise zu komplex?

@Merovius

Allerdings sehe ich die Notwendigkeit nicht wirklich. Mir geht es gut ohne, auch in großen Codebasen.

Wenn Sie mit einem Programm mit 100.000 Zeilen zurechtkommen, können Sie mit 100.000 generischen Zeilen mehr erreichen als mit 100.000 nicht-generischen Zeilen (aufgrund der Wiederholung). Sie sind also vielleicht ein Super-Star-Entwickler, der mit sehr großen Codebasen umgehen kann, aber Sie würden mit einer sehr großen generischen Codebasis immer noch mehr erreichen, da Sie die Redundanz eliminieren würden. Dieses generische Programm würde sich zu einem noch größeren nicht-generischen Programm erweitern. Mir scheint nur, dass Sie Ihre Komplexitätsgrenze noch nicht erreicht haben.

Ich denke jedoch, dass Sie Recht haben, dass "Bedürfnis" zu stark ist. Ich schreibe glücklich Go-Code, mit nur gelegentlicher Frustration über den Mangel an Generika, und ich kann dies umgehen, indem ich einfach mehr Code schreibe, und in Go ist dieser Code angenehm direkt und wörtlich.

Das Fehlen eines eingeschränkten Polymorphismus ist semantisch weit schwerwiegender. IMO, das Hinzufügen von Generika ohne das ist die Mühe nicht wirklich wert.

Ich stimme dem zu.

Sie können mit 100.000 generischen Zeilen mehr erreichen als mit 100.000 nicht generischen Zeilen (aufgrund der Wiederholung)

Ich bin neugierig, wie viel % dieser Zeilen aus Ihrem hypothetischen Beispiel eine generische Funktion wären?
Meiner Erfahrung nach sind dies weniger als 2% (aus einer Codebasis mit 115k LOC), daher denke ich nicht, dass dies ein gutes Argument ist, es sei denn, Sie schreiben eine Bibliothek für "Sammlungen".

Ich wünschte, wir würden irgendwann Generika bekommen

@Keean

In Bezug auf Ihre Behauptung, dass Sie dieses Beispiel nicht in Haskell ausführen können, hier ist der Code:

Dieser Kodex ist moralisch nicht gleichwertig mit dem Kodex, den ich geschrieben habe. Es führt zusätzlich zur ICloneable-Schnittstelle einen neuen Cloneable-Wrapper-Typ ein. Der Go-Code benötigte keinen Wrapper; noch würden andere Sprachen, die Subtyping unterstützen.

@andrewcmyers

Dieser Kodex ist moralisch nicht gleichwertig mit dem Kodex, den ich geschrieben habe. Es führt zusätzlich zur ICloneable-Schnittstelle einen neuen Cloneable-Wrapper-Typ ein.

Ist es nicht das, was dieser Code tut:

type Cloneable interface {...}

Es führt einen von der Schnittstelle abgeleiteten Datentyp 'Cloneable' ein. Sie sehen das 'ICloneable' nicht, weil Sie keine Instanzdeklarationen für Schnittstellen haben, Sie deklarieren nur die Methoden.

Können Sie es als Subtypisierung betrachten, wenn die Typen, die eine Schnittstelle implementieren, nicht strukturell kompatibel sein müssen?

@keean Ich würde Cloneable nur als Typ betrachten, nicht wirklich als "Datentyp". In einer Sprache wie Java würden der Cloneable -Abstraktion im Wesentlichen keine zusätzlichen Kosten entstehen, da es im Gegensatz zu Ihrem Code keinen Wrapper geben würde.

Es scheint mir einschränkend und unerwünscht, eine strukturelle Ähnlichkeit zwischen Typen zu verlangen, die eine Schnittstelle implementieren, daher bin ich verwirrt darüber, was Sie hier denken.

@andrewcmyers
Ich verwende Typ und Datentyp austauschbar. Jeder Typ, der Daten enthalten kann, ist ein Datentyp.

weil es anders als in Ihrem Code keinen Wrapper geben würde.

Es gibt immer einen Wrapper, weil Go-Typen immer verpackt sind, also existiert der Wrapper um alles herum. Haskell benötigt den Wrapper, um explizit zu sein, da er unverpackte Typen hat.

strukturelle Ähnlichkeit zwischen Typen, die eine Schnittstelle implementieren, daher bin ich verwirrt darüber, was Sie hier denken.

Strukturelle Subtypisierung erfordert, dass die Typen „strukturell kompatibel“ sind. Da es keine explizite Typhierarchie wie in einer OO-Sprache mit Vererbung gibt, kann die Untertypisierung nicht nominal sein, also muss sie strukturell sein, wenn sie überhaupt vorhanden ist.

Ich verstehe jedoch, was Sie meinen, was ich so beschreiben würde, dass eine Schnittstelle eine abstrakte Basisklasse ist, keine Schnittstelle, mit einer Art impliziter nomineller Subtyp-Beziehung zu jedem Typ, der die erforderlichen Methoden implementiert.

Ich denke tatsächlich, dass Go im Moment zu beiden Modellen passt, und es könnte von hier aus in beide Richtungen gehen, aber ich würde vorschlagen, dass die Bezeichnung als Schnittstelle und nicht als Klasse eine Denkweise ohne Subtypisierung nahelegt.

@keean Ich verstehe deinen Kommentar nicht. Zuerst sagst du mir, dass du anderer Meinung bist und dass ich "meine Komplexitätsgrenze noch nicht erreicht habe", und dann sagst du mir, dass du zustimmst (wobei "Bedürfnis" ein zu starkes Wort ist). Ich denke auch, dass Ihr Argument trügerisch ist (Sie gehen davon aus, dass LOC das primäre Maß für die Komplexität ist und dass jede Codezeile gleich ist). Aber vor allem glaube ich nicht, dass die Frage „wer schreibt kompliziertere Programme“ wirklich eine produktive Diskussionslinie ist. Ich wollte nur klarstellen, dass das Argument "Wenn Sie nicht meiner Meinung sind, muss das bedeuten, dass Sie nicht an so schwierigen oder interessanten Problemen arbeiten" nicht überzeugend ist und nicht in gutem Glauben rüberkommt. Ich hoffe, Sie können einfach darauf vertrauen, dass die Leute Ihnen in Bezug auf die Bedeutung dieser Funktion widersprechen können, während sie gleichermaßen kompetent sind und genauso interessante Dinge tun.

@merovius
Ich sagte, Sie sind wahrscheinlich ein fähigerer Programmierer als ich und daher in der Lage, mit mehr Komplexität zu arbeiten. Ich glaube sicherlich nicht, dass Sie an weniger interessanten oder weniger komplexen Problemen arbeiten, und es tut mir leid, dass das so rübergekommen ist. Ich habe gestern versucht, einen Scanner zum Laufen zu bringen, was ein sehr uninteressantes Problem war.

Ich kann mir vorstellen, dass Generika mir helfen, komplexere Programme mit meiner begrenzten Intelligenz zu schreiben, und ich gebe auch zu, dass ich keine Generika "brauche". Es ist eine Frage des Grades. Ich kann immer noch ohne Generika programmieren, aber ich kann nicht unbedingt Software mit der gleichen Komplexität schreiben.

Ich hoffe, das beruhigt Sie, dass ich in gutem Glauben handele, ich habe hier keine versteckten Absichten, und wenn Go keine Generika einführt, werde ich sie trotzdem verwenden. Ich habe eine Meinung darüber, wie man Generika am besten herstellt, aber das ist nicht die einzige Meinung, ich kann nur aus meiner eigenen Erfahrung sprechen. Wenn ich nicht helfe, gibt es viele andere Dinge, mit denen ich meine Zeit verbringen kann, also sagen Sie einfach ein Wort, und ich werde mich auf etwas anderes konzentrieren.

@Merovius Danke für den fortgesetzten Dialog.

| Die beiden Hauptgründe, warum Menschen Generika wollen, sind Leistung (vermeiden Sie das Umschließen von Schnittstellen) und Typsicherheit (stellen Sie sicher, dass derselbe Typ an verschiedenen Stellen verwendet wird, ohne sich darum zu kümmern, um welchen es sich handelt). Dies scheint diese Gründe zu ignorieren.

Vielleicht betrachten wir das, was ich vorgeschlagen habe, ganz anders, da es aus meiner Sicht beides tut, soweit ich das beurteilen kann? Im Beispiel der verknüpften Liste gibt es kein Wrapping mit Schnittstellen und sollte daher so performant sein, als wäre es für einen bestimmten Typ von Hand geschrieben. Auf der Typsicherheitsseite ist es genauso. Können Sie hier ein Gegenbeispiel geben, damit ich verstehe, woher Sie kommen?

| Wahr. Aber ziemlich unergonomisch. Überlegen Sie, wie viele Beschwerden es über die Sortier-API gab. Für viele generische Container scheint die Menge an Funktionen, die der Aufrufer implementieren und übergeben müsste, unerschwinglich zu sein. Überlegen Sie, wie würde eine Container/Haufen-Implementierung unter diesem Vorschlag aussehen und wie wäre sie in Bezug auf die Ergonomie besser als die aktuelle Implementierung? Es scheint, dass die Gewinne hier bestenfalls vernachlässigbar sind. Sie müssten mehr triviale Funktionen implementieren (und an jeder Verwendungsstelle duplizieren/referenzieren), nicht weniger.

Mich stört das eigentlich überhaupt nicht. Ich glaube nicht, dass die Menge an Funktionen unerschwinglich wäre, aber ich bin definitiv offen für einige Gegenbeispiele. Denken Sie daran, dass die API, über die sich die Leute beschwert haben, keine Funktion war, für die Sie eine Funktion bereitstellen mussten, sondern die ursprüngliche hier: https://golang.org/pkg/sort/#Interface , wo Sie einen neuen Typ erstellen mussten, der einfach war Ihr Slice + Typ, und implementieren Sie dann 3 Methoden darauf. Angesichts der Beschwerden und der mit dieser Schnittstelle verbundenen Schmerzen wurde Folgendes erstellt: https://golang.org/pkg/sort/#Slice , ich jedenfalls habe kein Problem mit dieser API und wir würden die Leistungseinbußen davon ausgleichen Unter dem Vorschlag, den wir diskutieren, ändern wir einfach die Definition in func Slice(slice []a, less func(a, a) bool) .

In Bezug auf die container/heap -Datenstruktur muss jeder generische Vorschlag, den Sie akzeptieren, vollständig neu geschrieben werden. container/heap genau wie das Paket sort bietet nur Algorithmen auf Ihrer eigenen Datenstruktur, aber keines der Pakete besitzt jemals die Datenstruktur, weil wir sonst []interface{} und die damit verbundene Kosten. Vermutlich würden wir sie ändern, da Sie dank Generika einen Heap haben könnten, der einen Slice mit einem konkreten Typ besitzt, und dies gilt für alle Vorschläge, die ich hier gesehen habe (einschließlich meiner eigenen). .

Ich versuche, die Unterschiede in unseren Perspektiven auf das, was ich vorgeschlagen habe, auseinanderzuhalten. Und ich denke, die Wurzel der Meinungsverschiedenheit (jenseits aller persönlichen syntaktischen Vorlieben) ist, dass es keine Einschränkungen für die generischen Typen gibt. Aber ich versuche immer noch herauszufinden, was uns das bringt. Wenn die Antwort lautet, dass nichts in Bezug auf die Leistung eine Schnittstelle verwenden darf, dann kann ich hier nicht viel sagen.

Betrachten Sie die folgende Hash-Tabellendefinition:

// Hasher turns a key into a hash
type Hasher interface {
  func Hash() []byte
}

type HashTable v struct {
   Keys   []Hasher
   Values []v
}

// Note that the generic arguments must be repeated here and immediately
// understood without reading another line of code, which to me
// is a readability win over the sudden appearance of the K and V which are
// defined elsewhere in the code in the example below. This is of course because
// the tokenized type declarations with constraints are fairly painful in general
// and repeating them everywhere is simply too much.
func (h (*HashTable v)) Insert(key Hasher, value v) { ... }

Wollen wir damit sagen, dass []Hasher aufgrund von Leistungs-/Speicherproblemen ein Nichtstarter ist und dass wir für eine erfolgreiche Generika-Implementierung in Go unbedingt etwas wie das Folgende haben müssen?

// Without selecting another proposal I have no idea how the constraint might be defined or implemented so let's just pretend
type [K: Hasher, V] HashTable a struct {
   Keys   []K
   Values []V
}

func (h *HashTable) Insert(key K, value V) { ... }

Hoffentlich sehen Sie, wo ich herkomme. Aber es ist definitiv möglich, dass ich die Einschränkungen nicht verstehe, die Sie bestimmten Codes auferlegen möchten. Vielleicht gibt es Anwendungsfälle, die ich nicht berücksichtigt habe, trotzdem hoffe ich, ein umfassenderes Verständnis dafür zu bekommen, was die Anforderungen sind und wie der Vorschlag sie nicht erfüllt.

Vielleicht betrachten wir das, was ich vorgeschlagen habe, ganz anders, da es aus meiner Sicht beides tut, soweit ich das beurteilen kann?

Das "this" in dem Abschnitt, den Sie zitieren, bezieht sich auf die Verwendung von Schnittstellen. Das Problem ist nicht, dass Ihr Vorschlag dies auch nicht tut, sondern dass Ihr Vorschlag keine eingeschränkte Polymorphie zulässt, was die meisten Verwendungen für sie ausschließt. Und die Alternative, die Sie dafür vorgeschlagen haben, waren Schnittstellen, die den Kernanwendungsfall für Generika auch nicht wirklich ansprechen (wegen der beiden Dinge, die ich erwähnt habe).

Zum Beispiel erlaubte Ihr Vorschlag (wie ursprünglich geschrieben) nicht das Schreiben einer generischen Karte jeglicher Art, da dies erfordern würde, zumindest Schlüssel mit == vergleichen zu können (was eine Einschränkung darstellt, also implementieren Sie a Karte erfordert eingeschränkten Polymorphismus).

Angesichts der Beschwerden und der mit dieser Schnittstelle verbundenen Schmerzen wurde Folgendes erstellt: https://golang.org/pkg/sort/#Slice

Beachten Sie, dass diese Schnittstelle in Ihrem Vorschlag für Generika immer noch nicht möglich ist, da sie auf Reflektion für Länge und Austausch angewiesen ist (also haben Sie auch hier eine Einschränkung für Slice-Operationen). Selbst wenn wir diese API als die untere Grenze dessen akzeptieren, was Generika leisten können sollten (viele Leute würden das nicht tun. Es gibt immer noch viele Beschwerden über die mangelnde Typsicherheit in dieser API), würde Ihr Vorschlag nicht durchkommen diese Stange.

Aber Sie zitieren auch wieder eine Antwort auf einen bestimmten Punkt, den Sie gemacht haben, nämlich dass Sie eingeschränkten Polymorphismus erhalten könnten, indem Sie Funktionsliterale in der API übergeben. Und diese spezifische Methode, die Sie vorgeschlagen haben, um den Mangel an eingeschränktem Polymorphismus zu umgehen, würde die Implementierung der alten API mehr oder weniger erfordern. dh Sie zitieren meine Antwort auf dieses Argument, das Sie dann nur wiederholen:

Wir würden die Leistungseinbußen gemäß dem Vorschlag, den wir diskutieren, ausgleichen, indem wir einfach die Definition in func Slice(slice []a, less func(a, a) bool) ändern.

Das ist aber die alte API. Sie sagen: "Mein Vorschlag erlaubt keinen eingeschränkten Polymorphismus, aber das ist kein Problem, da wir einfach keine Generika verwenden können und stattdessen die vorhandenen Lösungen (Reflektion/Schnittstellen) verwenden". Nun, auf „Ihr Vorschlag lässt die grundlegendsten Anwendungsfälle, für die die Leute Generika wollen, nicht zu“ mit „Wir können einfach die Dinge tun, die die Leute ohne Generika für diese grundlegendsten Anwendungsfälle tun“ zu antworten, scheint uns nicht zu verstehen überall, TBH. Ein Generika-Vorschlag, der Ihnen nicht einmal hilft, grundlegende Containertypen zu schreiben, Sort, Max ... scheint es einfach nicht wert zu sein.

Dies gilt für alle Vorschläge, die ich hier gesehen habe (einschließlich meiner eigenen).

Die meisten Generika-Vorschläge enthalten eine Möglichkeit, Typparameter einzuschränken. dh um auszudrücken "der Typparameter muss eine Less-Methode haben" oder "der Typparameter muss vergleichbar sein". Ihre – AFAICT – nicht.

Betrachten Sie die folgende Hash-Tabellendefinition:

Ihre Definition ist unvollständig. a) Der Schlüsseltyp muss auch gleich sein und b) Sie verhindern nicht, dass Sie unterschiedliche Schlüsseltypen verwenden. dh das wäre legal:

type hasherA uint64

func (a hasherA) Hash() []byte {
    b := make([]byte, 8)
    binary.BigEndian.PutUint64(b, uint64(a))
    return b
}

type hasherB string

func (b hasherB) Hash() []byte {
    return []byte(b)
}

h := new(HashTable int)
h.Insert(hasherA(42), 1)
h.Insert(hasherB("Hello world"), 2)

Es sollte jedoch nicht legal sein, da Sie verschiedene Schlüsseltypen verwenden. dh der Container ist nicht im gewünschten Umfang typgeprüft. Sie müssen die Hashtabelle sowohl über den Schlüssel- als auch über den Werttyp parametrisieren

type HashTable k v struct {
    Keys []k
    Values []v
}

func (h *(HashTable k v)) Insert(key k, value v) {
    // You can't actually do anything with k, as it's unconstrained. i.e. you can't hash it, compare it…
    // Implementing this is impossible in your proposal.
}

// If it weren't impossible, you'd get this:
h := new(HashTable hasherA int)
h[hasherA(42)] = 1
h[hasherB("Hello world")] = 2 // compile error - can't use hasherB as hasherA

Oder, wenn es hilft, stellen Sie sich vor, Sie versuchen, ein Hash-Set zu implementieren. Sie würden das gleiche Problem bekommen, aber jetzt hat der resultierende Container keine zusätzliche Typprüfung über interface{} .

Aus diesem Grund geht Ihr Vorschlag nicht auf die grundlegendsten Anwendungsfälle ein: Er stützt sich auf Schnittstellen, um den Polymorphismus einzuschränken, bietet dann aber keine Möglichkeit, diese Schnittstellen auf Konsistenz zu überprüfen. Sie können entweder eine konsistente Typprüfung oder einen eingeschränkten Polymorphismus haben, aber nicht beides. Aber man braucht beides.

dass wir für eine erfolgreiche Generika-Implementierung in Go unbedingt etwas wie das Folgende haben müssen?

So denke ich zumindest, ja, ziemlich. Wenn ein Vorschlag es nicht erlaubt, typsichere Container zu schreiben oder zu sortieren oder …, fügt er der bestehenden Sprache nicht wirklich etwas hinzu, das signifikant genug ist, um die Kosten zu rechtfertigen.

@ Merovius Okay. Ich glaube, ich verstehe, was Sie wollen. Denken Sie daran, dass Ihre Anwendungsfälle sehr weit von dem entfernt sind, was ich möchte. Ich jucke nicht wirklich nach typsicheren Containern, obwohl ich vermute, dass dies - wie Sie sagten - eine Minderheitsmeinung sein könnte. Einige der größten Dinge, die ich sehen möchte, sind Ergebnistypen anstelle von Fehlern und eine einfache Slice-Manipulation ohne Duplizierung oder Reflexion überall, die mein Vorschlag angemessen angeht. Ich kann jedoch sehen, dass es aus Ihrer Sicht "nicht die grundlegendsten Anwendungsfälle anspricht", wenn Ihr grundlegender Anwendungsfall darin besteht, generische Container ohne die Verwendung von Schnittstellen zu schreiben.

Beachten Sie, dass diese Schnittstelle in Ihrem Vorschlag für Generika immer noch nicht möglich ist, da sie auf Reflektion für Länge und Austausch angewiesen ist (also haben Sie auch hier eine Einschränkung für Slice-Operationen). Selbst wenn wir diese API als die untere Grenze dessen akzeptieren, was Generika leisten können sollten (viele Leute würden das nicht tun. Es gibt immer noch viele Beschwerden über die mangelnde Typsicherheit in dieser API), würde Ihr Vorschlag nicht durchkommen diese Stange.

Wenn Sie dies lesen, ist klar, dass Sie die Art und Weise, wie die generischen Slices unter diesem Vorschlag funktionieren würden/sollten, gründlich missverstanden haben. Durch dieses Missverständnis sind Sie zu dem falschen Schluss gekommen, dass „diese Schnittstelle in Ihrem Vorschlag immer noch nicht möglich ist“. Unter jedem Vorschlag muss ein generisches Slice möglich sein, das ist meine Meinung. Und len() in der Welt, wie ich es gesehen habe, würde definiert werden als: func len(slice []a) , was ein generisches Slice-Argument ist, was bedeutet, dass es die Länge für jedes Slice auf nicht reflektierende Weise zählen kann. Dies ist der Hauptpunkt dieses Vorschlags, wie ich oben sagte (einfache Slice-Manipulation), und es tut mir leid, dass ich dies durch die von mir gegebenen Beispiele und den von mir gemachten Kern nicht so gut vermitteln konnte. Ein generisches Slice sollte genauso einfach verwendet werden können wie ein []int heute, ich sage noch einmal, dass jeder Vorschlag, der dies nicht anspricht (Slice/Array-Swaps, Zuweisung, Len, Cap usw. ) greift meiner Meinung nach zu kurz.

Alles in allem sind wir uns jetzt wirklich darüber im Klaren, was die Ziele des anderen sind. Als ich vorschlug, was ich tat, sagte ich sehr deutlich, dass es sich lediglich um einen syntaktischen Vorschlag handele und dass die Details sehr unscharf seien. Aber wir sind sowieso in die Details gegangen und eines dieser Details war das Fehlen von Einschränkungen. Als ich es schrieb, hatte ich sie einfach nicht im Sinn, weil sie für das, was ich gerne tun würde, nicht wichtig sind , heißt das nicht, dass wir sie nicht hinzufügen könnten oder dass sie nicht wünschenswert sind. Das Hauptproblem bei der Fortführung der vorgeschlagenen Syntax und dem Versuch, Einschränkungen einzubauen, wäre, dass sich die Definition eines generischen Arguments derzeit (absichtlich) wiederholt, sodass nicht auf Code an anderer Stelle verwiesen wird, um Einschränkungen usw. zu bestimmen. Wenn wir Einschränkungen einführen würden I sehe nicht, wie wir das halten könnten.

Das beste Gegenbeispiel ist die zuvor besprochene Sortierfunktion.

type Sort(slice []a:Lesser, less func(a:Lesser, a:Lesser)) { ... }

Wie Sie sehen können, gibt es keinen netten Weg, dies zu erreichen, und die Token-Spam-Ansätze für Generika klingen wieder besser. Um Einschränkungen für diese zu definieren, müssen wir zwei Dinge gegenüber dem ursprünglichen Vorschlag ändern:

  • Es muss eine Möglichkeit geben, auf ein Typargument zu zeigen und ihm Einschränkungen zu geben.
  • Die Einschränkungen müssen länger als eine einzelne Definition dauern, vielleicht ist dieser Bereich ein Typ, vielleicht ist dieser Bereich eine Datei (Datei klingt eigentlich ziemlich vernünftig).

Haftungsausschluss: Das Folgende ist keine tatsächliche Änderung des Vorschlags, da ich nur zufällige Symbole da draußen werfe. Ich verwende diese Syntaxen nur als Beispiele, um zu veranschaulichen, was wir tun könnten, um den Vorschlag in seiner ursprünglichen Form zu ändern

// Decorator style, follows the definition of the type thorugh all
// of it's methods.
<strong i="14">@a</strong>: Lesser, Hasher, Equaler
func Sort(slice []a) { ... }
<strong i="15">@k</strong>: Equaler, Hasher
type HashTable k v struct

// Inline, follows the definition of the type through
// all of it's methods.
func [a: Hasher, Equaler] Sort(slice []a) { ... }
type [k: Hasher, Equaler] HashTable k v struct

// File-scope global style, if k appears as a generic argument
// it's constrained by this that appears at the top of the file underneath
// the imports but before any other code.
<strong i="16">@k</strong>: Equaler, Hasher

Beachten Sie noch einmal, dass ich dem Vorschlag nichts von dem oben Gesagten wirklich hinzufügen möchte. Ich zeige nur, welche Art von Konstrukten wir verwenden könnten, um das Problem zu lösen, und wie sie aussehen, ist im Moment etwas irrelevant.

Die Frage, die wir dann beantworten müssen, lautet: Gewinnen wir immer noch Wert aus den impliziten generischen Argumenten? Der Hauptpunkt des Vorschlags war, das saubere Go-ähnliche Gefühl der Sprache beizubehalten, die Dinge einfach zu halten, die Dinge ausreichend leise zu halten, indem übermäßige Token eliminiert wurden. In den vielen Fällen, in denen keine Einschränkungen erforderlich sind, z. B. eine Kartenfunktion oder die Definition eines Ergebnistyps, sieht es gut aus, fühlt es sich wie Go an, ist es nützlich? Vorausgesetzt, es gibt auch Constraints in irgendeiner Form.

func map(slice []a, mapper func(a) b) {
  for i := range slice {
    slice[i] = mapper(slice[i])
  }
}

type Result a b struct {
  Ok  a
  Err b
}

@aarondl Ich werde versuchen, es zu erklären. Der Grund, warum Sie Typbeschränkungen benötigen, ist, dass Sie nur so Funktionen oder Methoden für einen Typ aufrufen können. Betrachten Sie den uneingeschränkten Typ a , welcher Typ kann das sein, nun, es könnte ein String oder ein Int oder irgendetwas sein. Wir können also keine Funktionen oder Methoden darauf aufrufen, da wir den Typ nicht kennen. Wir könnten einen Typschalter und eine Laufzeitreflektion verwenden, um den Typ abzurufen, und dann einige Funktionen oder Methoden darauf aufrufen, aber das möchten wir mit Generika vermeiden. Wenn Sie einen Typ einschränken, zum Beispiel ist a ein Tier, können wir dann jede für ein Tier definierte Methode auf a aufrufen.

In Ihrem Beispiel können Sie zwar eine Mapper-Funktion übergeben, aber dies führt dazu, dass Funktionen viele Argumente annehmen, und ist im Grunde wie eine Sprache ohne Schnittstellen, nur erstklassige Funktionen. Um jede Funktion, die Sie verwenden werden, auf Typ a zu übergeben, wird in jedem echten Programm eine sehr lange Liste von Funktionen erstellt, insbesondere wenn Sie hauptsächlich generischen Code für die Abhängigkeitsinjektion schreiben, was Sie tun möchten Kopplung minimieren.

Was ist zum Beispiel, wenn die Funktion, die map aufruft, auch generisch ist? Was ist, wenn die Funktion, die das aufruft, generisch ist usw. Wie definieren wir Mapper, wenn wir den Typ von a noch nicht kennen?

func m(slice []a) []b {
   mapper := func(x a) b {...}
   return map(slice, mapper)
}

Welche Funktionen können wir für x aufrufen, wenn wir versuchen, mapper zu definieren?

@keean Ich verstehe den Zweck und die Funktion der Einschränkungen. Ich schätze sie einfach nicht so hoch wie einfache Dinge wie generische Containerstrukturen (sozusagen keine generischen Container) und generische Slices und habe sie daher nicht einmal in den ursprünglichen Vorschlag aufgenommen.

Ich glaube immer noch größtenteils, dass Schnittstellen die richtige Antwort auf Probleme wie das sind, von dem Sie sprechen, wo Sie Abhängigkeitsinjektion durchführen, das scheint einfach nicht der richtige Ort für Generika zu sein, aber wer soll ich sagen. Die Überschneidung zwischen ihren Verantwortlichkeiten ist in meinen Augen ziemlich groß, daher mussten @Merovius und ich die Diskussion führen, ob wir ohne sie leben könnten oder nicht, und er hat mich ziemlich davon überzeugt, dass sie in einigen Anwendungsfällen nützlich sein würden, daher ich ein wenig untersucht, was wir tun könnten, um die Funktion zu dem Vorschlag hinzuzufügen, den ich ursprünglich gemacht habe.

In Ihrem Beispiel können Sie keine Funktionen auf x aufrufen. Sie können das Slice jedoch wie jedes andere Slice bearbeiten, was für sich genommen enorm nützlich ist. Ich bin mir auch nicht sicher, was die Funktion in der Funktion ist ... vielleicht wollten Sie eine Variable zuweisen?

@aarondl
Danke, ich habe die Syntax korrigiert, aber ich denke, die Bedeutung war immer noch klar.

In den Beispielen, die ich oben gegeben habe, wurden sowohl parametrischer Polymorphismus als auch Schnittstellen verwendet, um ein gewisses Maß an generischer Programmierung zu erreichen. Das Fehlen von Mehrfachversand wird jedoch immer eine Obergrenze für das erreichbare Maß an Allgemeingültigkeit setzen. Als solches scheint Go nicht die Funktionen zu bieten, die ich in einer Sprache suche, das bedeutet nicht, dass ich Go nicht für einige Aufgaben verwenden kann, und tatsächlich bin ich es bereits und es funktioniert gut, auch wenn ich es getan habe zum Ausschneiden und Einfügen von Code, der wirklich nur eine Definition benötigt. Ich hoffe nur, dass der Entwickler in Zukunft alle eingefügten Instanzen finden kann, wenn dieser Code geändert werden muss.

Ich bin mir dann nicht sicher, ob die begrenzte Allgemeingültigkeit, die ohne solch große Änderungen an der Sprache möglich ist, eine gute Idee ist, wenn man bedenkt, wie kompliziert sie sein wird. Vielleicht ist Go besser einfach, und die Leute können Makros wie die Vorverarbeitung oder andere Sprachen hinzufügen, die zu Go kompiliert werden, um diese Funktionen bereitzustellen? Andererseits wäre das Hinzufügen von parametrischem Polymorphismus ein guter erster Schritt. Es wäre ein guter nächster Schritt, zuzulassen, dass diese Typparameter eingeschränkt werden. Dann könnten Sie Schnittstellen zugeordnete Typparameter hinzufügen, und Sie hätten etwas einigermaßen Generisches, aber das ist wahrscheinlich alles, was Sie ohne Mehrfachdispatch erreichen können. Durch die Aufteilung in separate kleinere Funktionen würden Sie die Chance erhöhen, dass sie akzeptiert werden?

@Keean
Ist Mehrfachversand alles nötig? Nur sehr wenige Sprachen unterstützen es nativ. Selbst C++ unterstützt es nicht. C# unterstützt es irgendwie über dynamic , aber ich habe es nie in der Praxis verwendet und das Schlüsselwort im Allgemeinen ist in echtem Code sehr, sehr selten. Beispiele, an die ich mich erinnere, befassen sich mit etwas wie JSON-Parsing und nicht mit dem Schreiben von Generika.

Ist Mehrfachversand alles nötig?

IMHO, ich denke, @keean spricht über statischen Mehrfachversand, der von Typklassen/Schnittstellen bereitgestellt wird.
Dies wird sogar in C++ durch Methodenüberladung bereitgestellt (ich weiß es nicht für C#)

Was Sie meinen, ist der dynamische Mehrfachversand, der in statischen Sprachen ohne Union-Typen ziemlich umständlich ist. Dynamische Sprachen umgehen dieses Problem, indem sie die statische Typprüfung weglassen (partieller Typrückschluss für dynamische Sprachen, dasselbe für den „dynamischen“ Typ von C#).

Könnte ein Typ "nur" als Parameter bereitgestellt werden?

func Append(t, t2 type, arr []t, value t2) []t {
    v := t(value) // conversion
    return append(arr, v)
}

var arr []float64
v := 0

arr = Append(float64, int, arr, v)

@Inuart schrieb:

Könnte ein Typ "nur" als Parameter bereitgestellt werden?

Fraglich, inwieweit dies in go möglich oder erwünscht wäre

Was Sie wollen, könnte stattdessen erreicht werden, wenn generische Einschränkungen unterstützt werden:

func Append(arr []t, value s) []t  requires Convertible<s,t>{
    v := t(value) // conversion
    return append(arr, v)
}

var arr []int64
v := 0.5

arr = Append(arr, v)

Auch dies sollte mit Einschränkungen möglich sein:

func convert(value s) t requires Convertible<s,t>{
    return t(value);
}

f:float64:=2.0

i:int64=convert(f)

Für das, was es wert ist, unterstützt unsere Genus-Sprache den Mehrfachversand. Modelle für eine Einschränkung können mehrere Implementierungen bereitstellen, an die gesendet wird.

Ich verstehe, dass die Notation Convertible<s,t> für die Kompilierzeitsicherheit benötigt wird, aber möglicherweise zu einer Laufzeitprüfung degradiert werden könnte

func Append(t, t2 type, arr []t, value t2) []t {
    v, ok := t(value) // conversion
    if !ok {
        panic(...) // or return an err
    }
    return append(arr, v)
}

var arr []float64
v := 0

arr = Append(float64, int, arr, v)

Aber das sieht eher nach Syntaxzucker für reflect aus.

@Inuart Der Punkt ist, dass der Compiler überprüfen kann, ob der Typ die Typklasse zur Kompilierzeit implementiert, sodass die Laufzeitprüfung nicht erforderlich ist. Der Vorteil ist eine bessere Leistung (sogenannte Nullkosten-Abstraktion). Wenn es sich um eine Laufzeitprüfung handelt, können Sie auch reflect verwenden.

@kreker

Ist Mehrfachversand alles nötig?

Ich mache mir zu viele Gedanken darüber. Einerseits funktioniert Multiple-Dispatch (mit Klassen vom Typ Multi-Parameter) nicht gut mit Existentials, was 'Go' 'Schnittstellenwerte' nennt.

type Equals<T> interface {eq(right T) bool}
(left I) eq(right I) bool {return left == right}
(left I) eq(right F) bool {return false}
(left F) eq(right I) bool {return false}
(left F) eq(right F) bool {return left == right}

func main() {
    x := []Equals<?>{I{2}, F{4.0}, I{2}, F{4.0}}
}

Wir können den Abschnitt von Equals nicht definieren, da wir keine Möglichkeit haben, anzugeben, dass der rechte Parameter aus derselben Sammlung stammt. Wir können dies nicht einmal in Haskell tun:

data Equals = forall a . IEquals a a => Equals a

Das ist nicht gut, weil es nur erlaubt, einen Typ mit sich selbst zu vergleichen

data Equals = forall a b . IEquals a b => Equals a

Das ist nicht gut, weil wir keine Möglichkeit haben, b darauf zu beschränken, ein weiteres Existential in derselben Sammlung wie a zu sein (wenn a überhaupt in einer Sammlung ist).

Es macht es jedoch sehr einfach, mit einem neuen Typ zu erweitern:

(left K) eq(right I) bool {return false}
(left K) eq(right F) bool {return false}
(left I) eq(right K) bool {return false}
(left F) eq(right K) bool {return false}
(left K) eq(right K) bool {return left == right}

Und das wäre noch prägnanter mit Standardinstanzen oder Spezialisierungen.

Andererseits können wir dies in 'Go' umschreiben, das jetzt funktioniert:

package main

type I struct {v int}
type F struct {v float32}

type EqualsInt interface {eqInt(left I) bool}
func (right I) eqInt (left I) bool {return left == right}
func (right F) eqInt (left I) bool {return false}

type EqualsFloat interface {eqFloat(left F) bool}
func (right I) eqFloat (left F) bool {return false}
func (right F) eqFloat (left F) bool {return left == right}

type EqualsRight interface {
    EqualsInt
    EqualsFloat
}

type EqualsLeft interface {eq(right EqualsRight) bool}
func (left I) eq (right EqualsRight) bool {return right.eqInt(left)}
func (left F) eq (right EqualsRight) bool {return right.eqFloat(left)}

type Equals interface {
    EqualsLeft
    EqualsRight
}

func main() {
    x := []Equals{I{2}, F{4.0}, I{2}, F{4.0}}
    println(x[0].eq(x[1]))
    println(x[1].eq(x[0]))
    println(x[0].eq(x[2]))
    println(x[1].eq(x[3]))
}

Dies funktioniert gut mit dem existenziellen Wert (Schnittstellenwert), ist jedoch viel komplexer, schwerer zu erkennen, was vor sich geht und wie es funktioniert, und es hat die große Einschränkung, dass wir eine Schnittstelle pro Typ benötigen und das Akzeptable fest codieren müssen Typen auf der rechten Seite wie folgt:

type EqualsRight interface {
    EqualsInt
    EqualsFloat
}

Das bedeutet, dass wir die Bibliotheksquelle ändern müssten, um einen neuen Typ hinzuzufügen, da die Schnittstelle EqualsRight nicht erweiterbar ist.

Ohne Schnittstellen mit mehreren Parametern können wir also keine erweiterbaren generischen Operatoren wie Gleichheit definieren. Bei Multiparameter-Schnittstellen werden Existentiale (Schnittstellenwerte) problematisch.

Mein Hauptproblem bei vielen der vorgeschlagenen Syntaxen (Syntaxen?) Blah[E] ist, dass der zugrunde liegende Typ keine Informationen über enthaltende Generika anzeigt.

Zum Beispiel:

type Comparer[C] interface {
    Compare(other C) bool
}
// or
type Comparer c interface {
    Compare(other c) bool
}
...

Dies bedeutet, dass wir einen neuen Typ deklarieren, der dem zugrunde liegenden Typ weitere Informationen hinzufügt. Ist es nicht der Sinn der type -Deklaration, einen Namen basierend auf einem anderen Typ zu definieren?

Ich würde eine Syntax eher in Richtung von vorschlagen

type Comparer interface[C] {
    Compare(other C) bool
}

Das bedeutet, dass Comparer eigentlich nur ein Typ ist, der auf interface[C] { ... } basiert, und interface[C] { ... } natürlich ein eigener Typ von interface { ... } ist. Dadurch können Sie eine generische Schnittstelle verwenden, ohne sie zu benennen, wenn Sie möchten (was bei normalen Schnittstellen zulässig ist). Ich denke, diese Lösung ist etwas intuitiver und funktioniert gut mit dem Typsystem von Go, obwohl ich mich bitte korrigieren sollte, wenn ich falsch liege.

Hinweis: Das Deklarieren eines generischen Typs wäre nur für Schnittstellen, Strukturen und Funktionen mit den folgenden Syntaxen zulässig:
interface[G] { ... }
struct[G] { ... }
func[G] (vars...) { ... }

Dann hätte die "Implementierung" der Generika die folgenden Syntaxen:
interface[G] { ... }[string]
struct[G] { ... }[string]
func[G] (vars...) { ... }[int](args...)

Und mit einigen Beispielen, um es etwas klarer zu machen:

Schnittstellen

package add

type Adder interface[E] {
    // Adds the element and returns the size
    Add(elem E) int
}

// Adds the integer 5 to any implementation of Adder[int].
func AddFiveTo(a Adder[int]) int {
    return a.Add(5)
}

Strukturen

package heap

type List struct[T] {
    slice []T
}

func (l *List) Add(elem T) { // T is a type defined by the receiver
    l.slice = append(l.slice, elem)
}

Funktionen

func[A] AddManyTo(a Adder[A], many ...A) {
    for _, each := range a {
        a.Add(each)
    }
}

Dies ist eine Reaktion auf den Go2-Vertragsentwurf und ich werde seine Syntax verwenden, aber ich poste sie hier, da sie für jeden Vorschlag für parametrischen Polymorphismus gilt.

Das Einbetten von Typparametern sollte nicht erlaubt sein.

Erwägen

type X(type T C) struct {
  R // A regular type with method Foo()
  T // Some type parameter
}
// X defines some methods other than Foo(),
// some of which invoke Foo.

für einen beliebigen Typ R und einen beliebigen Vertrag C , der Foo() nicht enthält.

T hat alle Selektoren, die von C benötigt werden, aber eine bestimmte Instanziierung von T kann auch beliebige andere Selektoren haben, einschließlich Foo .

Nehmen wir an, Bar ist eine Struktur, zulässig unter C , die ein Feld namens Foo hat.

X(Bar) könnte eine illegale Instantiierung sein. Ohne eine Möglichkeit, den Vertrag anzugeben, dass ein Typ keinen Selektor hat, müsste dies eine abgeleitete Eigenschaft sein.

Methoden von X(Bar) könnten Verweise auf Foo weiterhin als X(Bar).R.Foo auflösen. Dies ermöglicht das Schreiben des generischen Typs, könnte jedoch für einen Leser, der mit der Spitzfindigkeit der Auflösungsregeln nicht vertraut ist, verwirrend sein. Außerhalb der Methoden von X würde der Selektor mehrdeutig bleiben, während interface { Foo() } nicht von den Parametern von X abhängt, einige Instanziierungen von X würden dies tun nicht befriedigen.

Das Verbieten der Einbettung eines Typparameters ist einfacher.

(Wenn dies jedoch zulässig sein soll, wäre der Feldname T aus demselben Grund, aus dem der Feldname eines eingebetteten S , das als type S = io.Reader definiert ist, S ist Reader , sondern auch, weil der Typ, der T instanziiert, nicht unbedingt einen Namen haben muss.)

@jimmyfrasche Ich denke, dass eingebettete Felder mit generischen Typen nützlich genug sind, dass es gut wäre, sie zuzulassen, auch wenn es an einigen Stellen etwas unangenehm sein könnte. Mein Vorschlag wäre, im gesamten generischen Code anzunehmen, dass der eingebettete Typ alle möglichen Felder und Methoden auf jeder möglichen Ebene definiert hat , sodass im generischen Code alle eingebetteten Methoden und Felder nicht generischer Typen gelöscht werden.

Also gegeben:

type R struct(type T) {
    io.Reader
    T
}

Methoden auf R wären nicht in der Lage, Read auf R aufzurufen, ohne über Reader umzuleiten. Beispielsweise:

func (r R) Do() {
     r.Read(buf)     // Illegal
     r.Reader.Read(buf)  // ok
}

Der einzige Nachteil, den ich sehen kann, ist, dass der dynamische Typ möglicherweise mehr Mitglieder enthält als der statische Typ. Beispielsweise:

func (r R) Do() {
    var x interface{} = r
    x.(io.Reader)    // Succeeds
}

@rogpeppe

Der einzige Nachteil, den ich sehen kann, ist, dass der dynamische Typ möglicherweise mehr Mitglieder enthält als der statische Typ.

Dies ist bei Typparametern direkt der Fall, daher sollte es meiner Meinung nach auch bei parametrischen Typen in Ordnung sein. Ich denke, die Lösung für das von @jimmyfrasche vorgestellte Problem könnte darin bestehen, den gewünschten Methodensatz des parametrisierten Typs in den Vertrag aufzunehmen.

contract C(t T) {
  interface { Foo() } (X(T){})
  // ...
}

type X(type T C) struct {
  R // A regular type with method Foo()
  T // Some type parameter
}
// X defines some methods other than Foo(),
// some of which invoke Foo.

Dadurch könnte Foo direkt auf X aufgerufen werden. Das würde natürlich gegen die Regel "keine lokalen Namen in Verträgen" verstoßen ...

@stevenblenkinsop Hmm , es ist möglich, wenn es umständlich ist, dies zu tun, ohne sich auf X zu beziehen

contract C(t T) {
  struct{ R; T }{}.Foo
}

C ist immer noch an die Implementierung von X gebunden, wenn auch etwas lockerer.

Wenn Sie das nicht tun, und Sie schreiben

func (x X(T)) Fooer() interface { Foo() } {
  return x
}

kompiliert es? Es würde nicht unter die Regel von @rogpeppe fallen , die anscheinend auch übernommen werden müsste, wenn Sie die Garantie im Vertrag nicht übernehmen. Aber gilt es dann nur, wenn Sie ein Typargument ohne ausreichenden Vertrag einbetten oder für alle Einbettungen?

Es wäre einfacher, es einfach zu verbieten.

Ich habe mit der Arbeit an diesem Vorschlag begonnen, bevor der Go2-Entwurf angekündigt wurde.

Als ich die Ankündigung sah, war ich bereit, meinen zu verschrotten, aber ich bin immer noch verunsichert wegen der Komplexität des Entwurfs, also habe ich meinen fertiggestellt. Es ist weniger leistungsfähig, aber einfacher. Wenn nichts anderes, kann es einige Bits geben, die es wert sind, gestohlen zu werden.

Es erweitert die Syntax der früheren Vorschläge von @ianlancetaylor , da diese verfügbar war, als ich anfing. Das ist nicht grundlegend. Es könnte durch eine (type T etc. Syntax oder etwas Äquivalentes ersetzt werden. Ich brauchte nur etwas Syntax als Notation für die Semantik.

Es befindet sich hier: https://gist.github.com/jimmyfrasche/656f3f47f2496e6b49e041cd8ac716e4

Die Regel müsste lauten, dass jede Methode, die aus einer größeren Tiefe als der eines eingebetteten Typparameters heraufgestuft wird, nicht aufgerufen werden kann, es sei denn, (1) die Identität des Typarguments ist bekannt oder (2) die Methode wird als aufrufbar nach außen bestätigt Typ durch den Vertrag, der den Typparameter einschränkt. Der Compiler könnte auch Ober- und Untergrenzen für die Tiefe bestimmen, die eine hochgestufte Methode innerhalb des äußeren Typs O haben muss, und sie verwenden, um zu bestimmen, ob die Methode für einen Typ aufrufbar ist, der O einbettet. dh ob ein Konfliktpotential mit anderen geförderten Methoden besteht oder nicht. Etwas Ähnliches würde auch für alle Typparameter gelten, von denen behauptet wird, dass sie über aufrufbare Methoden verfügen, wobei die Tiefenbereiche der Methoden innerhalb des Typparameters [0, inf] wären.

Das Einbetten von Typparametern scheint einfach zu nützlich, um es vollständig zu verbieten. Zum einen ermöglicht es eine transparente Komposition, die das Muster der Einbettung von Schnittstellen nicht zulässt.

Ich fand auch eine potenzielle Verwendung bei der Definition von Verträgen. Wenn Sie in der Lage sein möchten, einen Wert vom Typ T (der ein Zeigertyp sein könnte) zu akzeptieren, für den möglicherweise Methoden auf *T definiert sind, und Sie diesen Wert einfügen können möchten eine Schnittstelle, können Sie nicht unbedingt T in die Schnittstelle einfügen, da die Methoden möglicherweise auf *T , und Sie können nicht unbedingt *T in die Schnittstelle einfügen, weil T könnte selbst ein Zeigertyp sein (und daher könnte *T einen leeren Methodensatz haben). Wenn Sie jedoch eine Verpackung mögen

type Wrapper(type T) { T }

Sie könnten in jedem Fall *Wrapper(T) in die Schnittstelle einfügen, wenn Ihr Vertrag besagt, dass er die Schnittstelle erfüllt.

Kannst du nicht einfach tun

type Interface interface {
  SomeMethod(int) error
}

contract MightBeAPointer(t T) {
  Interface(t)
}

func Example(type T MightBeAPointer)(v T) {
  var i Interface = v
  // ...
}

Ich versuche, den Fall zu behandeln, wo jemand anruft

type S struct{}
func (s *S) SomeMethod(int) error { ... }
...
var s S
Example(S)(s)

Dies funktioniert nicht, da S nicht in Interface konvertiert werden kann, sondern nur *S .

Offensichtlich könnte die Antwort "Tu das nicht" lauten. Der Vertragsvorschlag beschreibt jedoch Verträge wie:

contract Contract(t T) {
    var _ error = t.SomeMethod(int(0))
}

S würde diesen Vertrag aufgrund der automatischen Adressierung erfüllen, ebenso wie *S . Was ich anzugehen versuche, ist die Fähigkeitslücke zwischen Methodenaufrufen und Schnittstellenkonvertierungen in Verträgen.

Wie auch immer, dies ist ein bisschen eine Tangente, die eine mögliche Verwendung für das Einbetten von Typparametern zeigt.

Beim Einbetten denke ich, dass „kann in eine Struktur eingebettet werden“ eine weitere Einschränkung ist, die die Verträge erfassen müssten, wenn dies zulässig wäre.

Erwägen:

contract Embeddable(type X, Y) {
    type S struct {
        X
        Y
    }
}

type Embedded(type First, Second Embeddable) struct {
        First
        Second
}

// Error: First and Second both provide method Read.
// That must be diagnosed to the Embeddable contract, not the definition of Embedded itself.
type Boom = Embedded(*bytes.Buffer, *strings.Reader)

Das Einbetten von @bcmills- Typen mit mehrdeutigen Selektoren ist zulässig, daher bin ich mir nicht sicher, wie dieser Vertrag interpretiert werden soll.

Auf jeden Fall ist es in Ordnung, wenn Sie nur bekannte Typen einbetten. Wenn Sie nur Typparameter einbetten, ist das in Ordnung. Der einzige Fall, der seltsam wird, ist, wenn Sie einen oder mehrere bekannte Typen UND einen oder mehrere Typparameter einbetten, und dann nur, wenn die Selektoren der bekannten Typen und die Typargumente nicht disjunkt sind

Das Einbetten von @bcmills- Typen mit mehrdeutigen Selektoren ist zulässig, daher bin ich mir nicht sicher, wie dieser Vertrag interpretiert werden soll.

Hm, guter Punkt. Mir fehlt eine weitere Einschränkung, um den Fehler auszulösen.¹

contract Embeddable(type X, Y) {
    type S struct {
        X
        Y
    }
    var _ io.Reader = S{}
}

¹ https://play.golang.org/p/3wSg5aRjcQc

Dazu muss einer von X oder Y aber nicht beide ein io.Reader sein. Es ist interessant, dass das Vertragssystem ausdrucksstark genug ist, um dies zuzulassen. Ich bin froh, dass ich nicht die Typschlussregeln für solch ein Biest herausfinden muss.

Aber das ist nicht wirklich das Problem.

Es ist, wenn Sie es tun

type S (type T C) struct {
  io.Reader
  T
}
func (s *S(T)) X() io.Reader {
  return s
}

Das sollte nicht kompiliert werden, da T einen Read -Selektor haben könnte, es sei denn, C hat einen

struct{ io.Reader; T }.Read

Aber was sind dann die Regeln, wenn C nicht sicherstellt, dass die Selektorsätze disjunkt sind und S nicht auf die Selektoren verweist? Ist es möglich, dass jede Instanziierung S eine Schnittstelle erfüllt, mit Ausnahme von Typen, die einen mehrdeutigen Selektor erstellen?

Ist es möglich, dass jede Instanziierung S eine Schnittstelle erfüllt, mit Ausnahme von Typen, die einen mehrdeutigen Selektor erstellen?

Ja, das scheint so zu sein. Ich frage mich, ob das etwas Tieferes impliziert... 🤔

Ich konnte nichts unwiderruflich Böses konstruieren, aber die Asymmetrie ist ziemlich unangenehm und macht mich unruhig:

type I interface { /* ... */ }
a := G(A) // ok, A satisfies contract
var _ I = a // ok, no selector overlap
b := G(B) // ok, B satisfies contract
var _ = b // error, selector overlap

Ich mache mir Sorgen über die Fehlermeldungen, wenn G0(B) ein G1(B) verwendet ein . . . verwendet ein Gn(B) und Gn ist derjenige, der den Fehler verursacht. . . .

FTR, Sie müssen sich nicht mit mehrdeutigen Selektoren herumschlagen, um Typfehler beim Einbetten auszulösen.

// Error: Duplicate field name Reader
type Boom = Embedded(*bytes.Reader, *strings.Reader)

Sie gehen davon aus, dass der eingebettete Feldname auf dem Argumenttyp basiert, während es sich eher um den Namen des eingebetteten Typparameters handelt. Dies ist so, als ob Sie einen Typalias einbetten und der Feldname der Alias ​​ist und nicht der Name des Typs, den er aliasiert.

Dies ist tatsächlich im Entwurf im Abschnitt über parametrisierte Typen angegeben :

Wenn ein parametrisierter Typ eine Struktur ist und der Typparameter als Feld in die Struktur eingebettet ist, ist der Name des Felds der Name des Typparameters, nicht der Name des Typarguments.

type Lockable(type T) struct {
    T
    mu sync.Mutex
}

func (l *Lockable(T)) Get() T {
    l.mu.Lock()
    defer l.mu.Unlock()
    return l.T
}

(Hinweis: Dies funktioniert schlecht, wenn Sie Lockable(X) in die Methodendeklaration schreiben: Soll die Methode lT oder lX zurückgeben? Vielleicht sollten wir einfach das Einbetten eines Typparameters in eine Struktur verbieten.)

Ich sitze nur hier hinten an der Seitenlinie und beobachte. Aber mache mir auch ein bisschen Sorgen.

Ich schäme mich nicht zu sagen, dass 90 % dieser Diskussion über meinen Kopf geht.

Es scheint, dass 20 Jahre, in denen ich meinen Lebensunterhalt mit dem Schreiben von Software verdient habe, ohne zu wissen, was Generika oder parametrischer Polymorphismus sind, mich nicht davon abgehalten haben, die Arbeit zu erledigen.

Leider habe ich mir erst vor etwa einem Jahr die Zeit genommen, Go zu lernen. Ich ging fälschlicherweise davon aus, dass es eine steile Lernkurve sei und es zu lange dauern würde, bis ich produktiv werde.

Ich hätte nicht falscher liegen können.

Ich konnte genug Go lernen, um einen Microservice zu erstellen, der den node.js-Dienst, mit dem ich Leistungsprobleme hatte, in weniger als einem Wochenende vollständig zerstörte.

Ironischerweise habe ich nur herumgespielt. Es war mir nicht besonders ernst damit, mit Go die Welt zu erobern.

Und doch stellte ich fest, dass ich mich innerhalb weniger Stunden aus meiner zusammengesunkenen, besiegten Haltung aufsetzte, als ob ich auf der Kante meines Sitzes sitzen würde und einen Action-Thriller ansehen würde. Die API, die ich erstellte, kam so schnell zusammen. Mir wurde klar, dass es sich tatsächlich lohnt, in diese Sprache meine kostbare Zeit zu investieren, weil sie offensichtlich so pragmatisch in ihrem Design war.

Und das liebe ich an Go. Es ist sehr schnell..... Zu lernen. Wir alle hier kennen seine Leistungsfähigkeit. Aber die Geschwindigkeit, mit der es gelernt werden kann, ist unübertroffen von den 8 anderen Sprachen, die ich im Laufe der Jahre gelernt habe.

Seitdem singe ich Gos Loblieder und habe 4 weitere Entwickler dazu gebracht, sich in es zu verlieben. Ich sitze einfach ein paar Stunden mit ihnen zusammen und baue etwas. Ergebnisse sprechen für sich.

Einfachheit und Schnelligkeit des Lernens. Dies sind die wahren Killer-Features der Sprache.

Programmiersprachen, die monatelanges hartes Lernen erfordern, halten oft nicht genau die Entwickler, die sie anziehen wollen. Wir haben Arbeit zu erledigen und Arbeitgeber, die täglich Fortschritte sehen wollen (danke agile, weiß es zu schätzen)

Ich hoffe also, dass das Go-Team zwei Dinge berücksichtigen kann:

1) Welches alltägliche Problem wollen wir lösen?

Ich kann anscheinend kein Beispiel aus der realen Welt finden, mit einem Showstopper, der durch Generika oder wie auch immer sie heißen werden, gelöst werden würde.

Kochbuchartige Beispiele für alltägliche Aufgaben, die problematisch sind, mit einem Beispiel dafür, wie sie mit diesen Sprachänderungsvorschlägen verbessert werden könnten.

2) Halten Sie es einfach, wie alle anderen großartigen Funktionen von Go

Es gibt einige unglaublich intelligente Kommentare hier. Aber ich bin mir sicher, dass die Mehrheit der Entwickler, die Go täglich für die allgemeine Programmierung verwenden, wie ich selbst, mit den Dingen, wie sie sind, vollkommen zufrieden und produktiv sind.

Vielleicht ein Compiler-Argument, um solche erweiterten Funktionen zu aktivieren? '--hardcore'

Ich wäre wirklich traurig, wenn wir die Compilerleistung negativ beeinflussen würden. Sag's einfach

Und das liebe ich an Go. Es ist sehr schnell..... Zu lernen. Wir alle hier kennen seine Leistungsfähigkeit. Aber die Geschwindigkeit, mit der es gelernt werden kann, ist unübertroffen von den 8 anderen Sprachen, die ich im Laufe der Jahre gelernt habe.

Ich stimme vollkommen zu. Die Kombination von Leistung und Einfachheit in einer vollständig kompilierten Sprache ist etwas völlig Einzigartiges. Ich möchte definitiv nicht, dass Go das verliert, und so sehr ich Generika will, glaube ich nicht, dass sie es auf diese Kosten wert sind. Ich glaube aber nicht, dass es notwendig ist, das zu verlieren.

Ich kann anscheinend kein Beispiel aus der realen Welt finden, mit einem Showstopper, der durch Generika oder wie auch immer sie heißen werden, gelöst werden würde.

Ich habe zwei hauptsächliche Anwendungsfälle für Generika: Typsichere Boilerplate-Eliminierung komplexer Datenstrukturen wie Binärbäume, Mengen und sync.Map und die Fähigkeit, typsichere Funktionen zur _Kompilierungszeit_ zu schreiben, die basierend arbeiten ausschließlich auf die Funktionalität ihrer Argumente und nicht auf ihr Layout im Speicher. Es gibt einige ausgefallenere Dinge, die ich gerne tun könnte, aber es würde mir nichts ausmachen, sie zu tun, wenn es unmöglich ist, Unterstützung für sie hinzuzufügen, ohne die Einfachheit der Sprache vollständig zu brechen.

Um ehrlich zu sein, gibt es bereits Funktionen in der Sprache, die ziemlich missbrauchbar sind. Der Hauptgrund dafür, dass sie _nicht_ so oft missbraucht werden, ist meiner Meinung nach die Go-Kultur des Schreibens von „idiomatischem“ Code, kombiniert mit der Standardbibliothek, die größtenteils saubere, leicht zu findende Beispiele für solchen Code bereitstellt. Eine gute Verwendung von Generika in der Standardbibliothek sollte definitiv eine Priorität sein, wenn sie implementiert werden.

@camstuart

Ich kann anscheinend kein Beispiel aus der realen Welt finden, mit einem Showstopper, der durch Generika oder wie auch immer sie heißen werden, gelöst werden würde.

Generics sind so, dass Sie den Code nicht selbst schreiben müssen. Sie müssen also nie wieder eine andere verknüpfte Liste, einen Binärbaum, eine Deque oder eine Priority-Queue selbst implementieren. Sie müssen niemals einen Sortieralgorithmus, einen Partitionierungsalgorithmus oder einen Rotationsalgorithmus usw. implementieren. Datenstrukturen werden zu komponierenden Standardsammlungen (z. und drehen). Wenn Sie diese Komponenten wiederverwenden können, sinkt die Fehlerrate, denn jedes Mal, wenn Sie eine Prioritätswarteschlange oder einen Partitionierungsalgorithmus neu implementieren, besteht die Möglichkeit, dass Sie etwas falsch machen und einen Fehler einführen.

Generics bedeutet, dass Sie weniger Code schreiben und mehr wiederverwenden. Sie bedeuten, dass gut gewartete Standardbibliotheksfunktionen und abstrakte Datentypen in mehr Situationen verwendet werden können, sodass Sie keine eigenen schreiben müssen.

Noch besser ist, dass all dies technisch bereits jetzt in Go möglich ist, aber nur mit einem nahezu vollständigen Verlust der Typsicherheit während der Kompilierzeit _und_ mit einem möglicherweise erheblichen Laufzeit-Overhead. Mit Generika können Sie dies ohne diese Nachteile tun.

Generische Funktionsimplementierung:

/*

* "generic" is a KIND of types, just like "struct", "map", "interface", etc...
* "T" is a generic type (a type of kind generic).
* var t = T{int} is a value of type T, values of generic types looks like a "normal" type

*/

type T generic {
    int
    float64
    string
}

func Sum(a, b T{}) T{} {
    return a + b
}

Funktionsaufrufer:

Sum(1, 1) // 2
// same as:
Sum(T{int}(1), T{int}(1)) // 2

Generische Strukturimplementierung:

type ItemT generic {
    interface{}
}

type List struct {
    l []ItemT{}
}

func NewList(t ItemT) *List {
    l := make([]t)
    return &List{l}
}

func (p *List) Push(item ItemT{}) {
    p.l = append(p.l, item)
}

Anrufer:

list := NewList(ItemT{int})
list.Push(42)

Als jemand, der gerade Swift lernt und es nicht mag, aber viel Erfahrung in anderen Sprachen wie Go, C, Java usw. hat; Ich glaube wirklich, dass Generics (oder Templating oder wie auch immer Sie es nennen wollen) keine gute Sache sind, um sie der Go-Sprache hinzuzufügen.

Vielleicht habe ich einfach mehr Erfahrung mit der aktuellen Version von Go, aber für mich fühlt sich das wie eine Regression zu C++ an, da es schwieriger ist, Code zu verstehen, den andere Leute geschrieben haben. Der klassische T-Platzhalter für Typen macht es so schwierig zu verstehen, was eine Funktion zu tun versucht.

Ich weiß, dass dies eine beliebte Feature-Anfrage ist, also kann ich damit umgehen, wenn sie landet, aber ich wollte meine 2 Cent (Meinung) hinzufügen.

@jlubawy
Kennen Sie eine andere Möglichkeit, dass ich niemals eine verknüpfte Liste oder einen Quicksort-Algorithmus implementieren muss? Wie Alexander Stepanov betont, können die meisten Programmierer die „min“- und „max“-Funktionen nicht korrekt definieren. Welche Hoffnung haben wir also, komplexere Algorithmen ohne viel Debugging-Zeit korrekt zu implementieren? Ich würde viel lieber Standardversionen dieser Algorithmen aus einer Bibliothek ziehen und sie einfach auf die Typen anwenden, die ich habe. Welche Alternative gibt es?

@jlubawy

oder Templating, oder wie auch immer Sie es nennen wollen

Alles hängt von der Umsetzung ab. Wenn wir über C++-Vorlagen sprechen, dann ja, sie sind im Allgemeinen schwer zu verstehen. Selbst das Schreiben ist schwierig. Wenn wir andererseits C#-Generika nehmen, dann ist das eine ganz andere Sache. Das Konzept an sich ist hier kein Problem.

Falls Sie es noch nicht wussten, das Go-Team hat einen Entwurf von Go 2.0 angekündigt:
https://golang.org/s/go2designs

Es gibt einen Entwurf zum Generics-Design in Go 2.0 (Vertrag). Vielleicht möchten Sie einen Blick auf ihr Wiki werfen und Feedback geben.

Dies ist der relevante Abschnitt:

Generika

Nachdem ich den Entwurf gelesen habe, frage ich:

Warum

T: Addierbar

bedeutet "ein Typ T, der den Vertrag Addable umsetzt"? Warum eine neue hinzufügen
Konzept, wenn wir dafür schon SCHNITTSTELLEN haben? Schnittstellenbelegung ist
in der Bauzeit eingecheckt, sodass wir bereits die Mittel haben, keine zu benötigen
zusätzliches Konzept hier. Wir können diesen Begriff verwenden, um so etwas zu sagen wie: Beliebig
Typ T, der die Schnittstelle Addable implementiert. Zusätzlich T:_ oder T:Any
(Any ist ein spezielles Schlüsselwort oder ein eingebauter Alias ​​von interface{}).
der Trick.

Ich weiß nur nicht, warum ich die meisten Sachen so neu implementieren soll. Macht nein
sinnvoll und WERDEN redundant sein (so redundant ist die neue Behandlung von Fehlern bzgl
Umgang mit Panik).

2018-09-14 6:15 GMT-05:00 Koala Yeung [email protected] :

Falls Sie es noch nicht wussten, das Go-Team hat einen Entwurf von Go 2.0 angekündigt:
https://golang.org/s/go2designs

Es gibt einen Entwurf zum Generics-Design in Go 2.0 (Vertrag). Du möchtest vielleicht
mal reinschauen und Feedback geben
https://github.com/golang/go/wiki/Go2GenericsFeedback zu ihrem Wiki
https://github.com/golang/go/wiki/Go2GenericsFeedback .

Dies ist der relevante Abschnitt:

Generika


Sie erhalten dies, weil Sie kommentiert haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/15292#issuecomment-421326634 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AlhWhS8xmN5Y85_aUKT5VnutoOKUAaLLks5ua4_agaJpZM4IG-xv
.

--
Dies ist ein Test für E-Mail-Signaturen, die in TripleMint verwendet werden sollen

Bearbeiten: "[...] würde den Zweck erfüllen, WENN SIE KEINE BESTIMMTE ANFORDERUNG BRAUCHEN
DAS TYP-ARGUMENT".

17.09.2018 11:10 GMT-05:00 Luis Masuelli [email protected] :

Nachdem ich den Entwurf gelesen habe, frage ich:

Warum

T: Addierbar

bedeutet "ein Typ T, der den Vertrag Addable umsetzt"? Warum eine neue hinzufügen
Konzept, wenn wir dafür schon SCHNITTSTELLEN haben? Schnittstellenbelegung ist
in der Bauzeit eingecheckt, sodass wir bereits die Mittel haben, keine zu benötigen
zusätzliches Konzept hier. Wir können diesen Begriff verwenden, um so etwas zu sagen wie: Beliebig
Typ T, der die Schnittstelle Addable implementiert. Zusätzlich T:_ oder T:Any
(Any ist ein spezielles Schlüsselwort oder ein eingebauter Alias ​​von interface{}).
der Trick.

Ich weiß nur nicht, warum ich die meisten Sachen so neu implementieren soll. Macht nein
sinnvoll und WERDEN redundant sein (so redundant ist die neue Behandlung von Fehlern bzgl
Umgang mit Panik).

2018-09-14 6:15 GMT-05:00 Koala Yeung [email protected] :

Falls Sie es noch nicht wussten, das Go-Team hat einen Entwurf von Go 2.0 angekündigt:
https://golang.org/s/go2designs

Es gibt einen Entwurf zum Generics-Design in Go 2.0 (Vertrag). Sie können
möchte mal reinschauen und Feedback geben
https://github.com/golang/go/wiki/Go2GenericsFeedback zu ihrem Wiki
https://github.com/golang/go/wiki/Go2GenericsFeedback .

Dies ist der relevante Abschnitt:

Generika


Sie erhalten dies, weil Sie kommentiert haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/15292#issuecomment-421326634 oder stumm
der Faden
https://github.com/notifications/unsubscribe-auth/AlhWhS8xmN5Y85_aUKT5VnutoOKUAaLLks5ua4_agaJpZM4IG-xv
.

--
Dies ist ein Test für E-Mail-Signaturen, die in TripleMint verwendet werden sollen

--
Dies ist ein Test für E-Mail-Signaturen, die in TripleMint verwendet werden sollen

@luismasuelli-jobsity Wenn ich die Geschichte der generischen Implementierungen in Go richtig gelesen habe, dann sieht es so aus, als ob der Grund für die Einführung von Contracts darin besteht, dass sie keine Operatorüberladung in Interfaces wollten.

Ein früherer Vorschlag, der schließlich abgelehnt wurde, verwendete Schnittstellen, um parametrischen Polymorphismus einzuschränken, scheint jedoch abgelehnt worden zu sein, weil Sie in solchen Funktionen keine allgemeinen Operatoren wie '+' verwenden konnten, da es in einer Schnittstelle nicht definierbar ist. Verträge ermöglichen es Ihnen, t == t oder t + t zu schreiben, damit Sie angeben können, dass der Typ Gleichheit oder Addition usw. unterstützen muss.

Bearbeiten: Auch Go unterstützt keine Schnittstellen mit mehreren Typparametern, also hat Go in gewisser Weise die Typklasse in zwei separate Dinge getrennt, Verträge, die die Funktionstypparameter miteinander in Beziehung setzen, und Schnittstellen, die Methoden bereitstellen. Was dabei verloren geht, ist die Möglichkeit, eine Typklassenimplementierung basierend auf mehreren Typen auszuwählen. Es ist wohl einfacher, wenn Sie nur Schnittstellen oder Verträge verwenden müssen, aber komplexer, wenn Sie beide zusammen verwenden müssen.

Warum bedeutet T:Addable "ein Typ T, der den Vertrag Addable implementiert"?

Das ist eigentlich nicht gemeint; es sieht nur so aus für ein Typargument. An anderer Stelle im Entwurf wird kommentiert, dass Sie nur einen Vertrag pro Funktion haben können, und hier kommt der Hauptunterschied ins Spiel. Verträge sind eigentlich Aussagen über die Typen der Funktion, nicht nur die Typen unabhängig voneinander. Zum Beispiel, wenn Sie haben

func Example(type K, V someContract)(k K, v V) V

du kannst sowas machen

contract someContract(k K, v V) {
  k.someMethod(v)
}

Dies vereinfacht die Koordination mehrerer Typen erheblich, ohne dass die Typen in der Funktionssignatur redundant angegeben werden müssen. Denken Sie daran, dass sie versuchen, das „merkwürdig wiederholende generische Muster“ zu vermeiden. Dieselbe Funktion mit parametrisierten Schnittstellen, die zum Einschränken der Typen verwendet wird, wäre beispielsweise so etwas wie

type someMethoder(V) interface {
  someMethod(V)
}

func Example(type K: someMethoder(V), V)(k K, v V) V

Das ist irgendwie umständlich. Die Vertragssyntax ermöglicht Ihnen dies jedoch bei Bedarf, da die „Argumente“ des Vertrags vom Compiler automatisch ausgefüllt werden, wenn der Vertrag die gleiche Anzahl von Argumenten hat wie die Funktion Parameter typisiert. Sie können sie jedoch manuell angeben, wenn Sie möchten, was bedeutet, dass Sie func Example(type K, V someContract(K, V))(k K, v V) V tun _könnten_, wenn Sie es wirklich wollten, obwohl dies in dieser Situation nicht besonders nützlich ist.

Eine Möglichkeit, klarer zu machen, dass es bei Verträgen um ganze Funktionen und nicht um einzelne Argumente geht, wäre, sie einfach anhand des Namens zuzuordnen. Beispielsweise,

contract Example(k K, v V) {
  k.someMethod(v)
}

func Example(type K, V)(k K, v V) V

wäre das gleiche wie oben. Der Nachteil ist jedoch, dass Verträge nicht wiederverwendbar sind und Sie die Möglichkeit verlieren, die Vertragsargumente manuell anzugeben.

Bearbeiten: Um weiter zu zeigen, warum sie das merkwürdig sich wiederholende Muster lösen wollen, betrachten Sie das Problem des kürzesten Pfads, auf das sie sich immer wieder bezogen. Bei parametrisierten Schnittstellen sieht die Definition am Ende so aus

type E(Node) interface {
  Nodes() []Node
}

type N(Edge) interface {
  Edges() (from, to Edge)
}

type Graph(type Node: N(Edge), Edge: E(Node)) struct { ... }
func New(type Node: N(Edge), Edge: E(Node))(nodes []Node) *Graph(Node, Edge) { ... }
func (*Graph(Node, Edge)) ShortestPath(from, to Node) []Edge { ... }

Mir persönlich gefällt eher die Art und Weise, wie Verträge für Funktionen spezifiziert werden. Ich bin nicht _zu_ scharf darauf, nur 'normale' Funktionskörper als eigentliche Vertragsspezifikation zu haben, aber ich denke, viele der potenziellen Probleme könnten gelöst werden, indem man eine Art gofmt-ähnlichen Vereinfacher einführt, der Verträge für Sie automatisch vereinfacht und entfernt Fremdteile. Dann _könnten_ Sie einfach einen Funktionskörper hineinkopieren, vereinfachen und von dort aus modifizieren. Ich bin mir aber leider nicht sicher, wie es möglich sein wird, dies zu implementieren.

Einige Dinge werden jedoch immer noch etwas umständlich zu spezifizieren sein, und die offensichtliche Überschneidung zwischen Verträgen und Schnittstellen erscheint immer noch etwas seltsam.

Ich finde die "CRTP"-Version viel klarer, expliziter und einfacher zu handhaben (keine Notwendigkeit, Verträge zu erstellen, die nur existieren, um die Beziehung zwischen bereits bestehenden Verträgen über eine Reihe von Variablen zu definieren). Zugegeben, das könnte auch nur an der langjährigen Vertrautheit mit der Idee liegen.

Erläuterungen. Durch den Entwurfsentwurf kann Vertrag auf beide Funktionen und Typen angewendet werden.

"""
Es ist wohl einfacher, wenn Sie nur Schnittstellen oder Verträge verwenden müssen, aber komplexer, wenn Sie beide zusammen verwenden müssen.
"""

Solange sie es Ihnen erlauben, innerhalb eines Vertrags auf eine oder mehrere Schnittstellen zu verweisen (anstatt nur auf Operatoren und Funktionen, wodurch DRY zugelassen wird), wird dieses Problem (und meine Behauptung) gelöst. Es besteht die Möglichkeit, dass ich das Vertragsmaterial falsch gelesen oder nicht vollständig gelesen habe, und es besteht auch die Möglichkeit, dass die genannte Funktion unterstützt wird und ich es nicht bemerkt habe. Wenn nicht, sollte es sein.

Können Sie Folgendes nicht tun?

contract Example(t T, v V) {
  t.(interface{
    SomeMethod() V
  })
}

Sie können keine an anderer Stelle deklarierte Schnittstelle verwenden, da Sie nicht auf Bezeichner aus demselben Paket verweisen können, in dem der Vertrag deklariert ist, aber Sie können dies tun. Oder sie könnten diese Einschränkung einfach aufheben; es wirkt etwas willkürlich.

@DeedleFake Nein, weil jeder Schnittstellentyp typgesichert werden kann (und dann zur Laufzeit möglicherweise nur in Panik gerät, aber Verträge nicht ausgeführt werden). Aber Sie können stattdessen eine Zuweisung verwenden.

t.(someInterface) würde auch bedeuten, dass es sich um eine Schnittstelle handeln muss

Guter Punkt. Hoppla.

Je mehr Beispiele dafür ich sehe, desto fehleranfälliger scheint "es aus einem Funktionskörper herausfinden" zu sein.

Es gibt viele Fälle, in denen es für eine Person verwirrend ist, dieselbe Syntax für verschiedene Operationen, Implikationsschattierungen von verschiedenen Konstrukten usw., aber ein Tool wäre in der Lage, dies zu nehmen und auf eine normale Form zu reduzieren. Aber dann wird die Ausgabe eines solchen Tools de facto zu einer Subsprache zum Ausdrücken von Typbeschränkungen, die wir auswendig lernen müssen, was es umso überraschender macht, wenn jemand davon abweicht und einen Vertrag von Hand schreibt.

Das werde ich auch anmerken

contract I(t T) {
  var i interface { Foo() }
  i = t
  t.(interface{})
}

drückt aus, dass T eine Schnittstelle mit mindestens Foo() sein muss, aber es könnte auch eine beliebige Anzahl anderer zusätzlicher Methoden haben.

T muss eine Schnittstelle mit mindestens Foo() sein, kann aber auch beliebig viele zusätzliche Methoden haben

Ist das aber ein Problem? Wollen Sie normalerweise Dinge nicht so einschränken, dass sie bestimmte Funktionen zulassen, sich aber nicht um andere Funktionen kümmern? Ansonsten ein Vertrag wie

contract Example(t T) {
  t + t
}

würde zum Beispiel keine Subtraktion zulassen. Aber was auch immer ich implementiere, es ist mir egal, ob ein Typ eine Subtraktion zulässt oder nicht. Wenn ich es daran hindern würde, Subtraktionen durchzuführen, wären die Leute einfach willkürlich nicht in der Lage, zum Beispiel irgendetwas zu übergeben, das an eine Sum() -Funktion oder so etwas tut. Das wirkt willkürlich restriktiv.

Nein, das ist überhaupt kein Problem. Es war nur eine (für mich) nicht intuitive Eigenschaft, aber vielleicht lag das an unzureichendem Kaffee.

Es ist fair zu sagen, dass die aktuelle Vertragserklärung bessere Compiler-Nachrichten haben muss, um damit zu arbeiten. Und die Regeln für einen gültigen Vertrag sollten streng sein.

Hallo
Ich habe einen Vorschlag für Einschränkungen für Generika gemacht, den ich vor ungefähr einem halben Jahr in diesem Thread gepostet habe.
Jetzt habe ich eine Version 2 gemacht. Die wichtigsten Änderungen sind:

  • Die Syntax wurde an die vom go-Team vorgeschlagene angepasst.
  • Auf die Einschränkung durch Felder wurde verzichtet, was einige Vereinfachungen ermöglicht.
  • Als nicht unbedingt notwendig erachtete Absätze wurden herausgenommen.

Ich dachte kürzlich an eine interessante (aber vielleicht detailliertere als in diesem Stadium des Designs angemessene?) Frage zur Typidentität:

func Foo() interface{} {
    type S struct {}
    return S{}
}

func Bar(type T)() interface{} {
    type S struct {}
    return S{}
}

func Baz(type T)() interface{} {
    type S struct{t T}
    return S{}
}

func main() {
    fmt.Println(Foo() == Foo()) // 1
    fmt.Println(Bar(int)() == Bar(string)()) // 2
    fmt.Println(Baz(int)() == Baz(string)()) // 3
}
  1. Gibt true aus, da die Typen der zurückgegebenen Werte aus derselben Typdeklaration stammen.
  2. Drucke…?
  3. Druckt false , nehme ich an.

dh die Frage ist, wann zwei in einer generischen Funktion deklarierte Typen identisch sind und wann nicht. Ich glaube nicht, dass dies im ~spec~-Design beschrieben ist? Jedenfalls finde ich es gerade nicht :)

@merovius Ich nehme an, der mittlere Fall sollte sein:

fmt.Println(Bar(int)() == Bar(int)()) // 2

Dies ist ein interessanter Fall, und es hängt davon ab, ob Typen "generativ" oder "applikativ" sind. Es gibt tatsächlich zwei Varianten von ML, die unterschiedliche Ansätze verfolgen. Applikative Typen betrachten das Generische als eine Typfunktion und daher f(int) == f(int). Generative Typen betrachten das Generische als eine Typvorlage, die jedes Mal, wenn sie verwendet wird, einen neuen eindeutigen „Instanztyp“ erstellt, also t<int> != t<int>. Dies muss auf der Ebene des gesamten Typsystems angegangen werden, da es subtile Auswirkungen auf die Vereinheitlichung, Schlussfolgerung und Solidität hat. Für weitere Details und Beispiele für diese Art von Problemen empfehle ich, Andreas Rossbergs "F-ing-Module"-Papier zu lesen: https://people.mpi-sws.org/~rossberg/f-ing/ , obwohl das Papier über ML spricht " Funktoren" liegt daran, dass ML sein Typsystem in zwei Ebenen unterteilt und Funktoren MLs Äquivalent zu einem Generikum sind und nur auf Modulebene verfügbar sind.

@keean Du nimmst falsch an.

@merovius Ja, mein Fehler, ich sehe, die Frage ist, weil der Typparameter nicht verwendet wird (ein Phantomtyp).

Bei generativen Typen würde jede Instanziierung zu einem anderen eindeutigen Typ für „S“ führen, sodass sie nicht gleich sind, obwohl der Parameter nicht verwendet wird.

Bei applikativen Typen wäre das 'S' von jeder Instanziierung derselbe Typ, und daher wären sie gleich.

Es wäre seltsam, wenn sich das Ergebnis in Fall 2 aufgrund von Compiler-Optimierungen ändern würde. Klingt nach UB.

Es ist 2018, Leute, ich kann nicht glauben, dass ich das tatsächlich wie 1982 tippen muss:

func min(x, y int) int {
wenn x < y {
Rückgabe x
}
gib y zurück
}

func max(x, y int) int {
wenn x > y {
Rückgabe x
}
gib y zurück
}

Ich meine, im Ernst, Leute MIN(INT,INT) INT, wie ist das NICHT in der Sprache?
Ich bin verärgert.

@dataf3l Wenn Sie möchten, dass diese mit Vorbestellungen wie erwartet funktionieren, dann:

func min(x, y int) int {
   if x <= y {
      return x
   }
   return y
}

Daher ist das Paar (min(x, y), max(x, y)) immer verschieden und ist entweder (x, y) oder (y, x), und das ist daher eine stabile Art von zwei Elementen.

Ein weiterer Grund, warum diese in der Sprache oder einer Bibliothek enthalten sein sollten, ist, dass die Leute sie meistens falsch verstehen :-)

Ich habe über < vs <= nachgedacht, für ganze Zahlen bin ich mir nicht sicher, ob ich den Unterschied sehe.
Vielleicht bin ich einfach dumm...

Ich bin mir nicht sicher, ob ich den Unterschied wirklich sehe.

In diesem Fall gibt es keine.

@cznic stimmt in diesem Fall, da es sich um Ganzzahlen handelt, aber da es in dem Thread um Generika ging, ging ich davon aus, dass es im Bibliothekskommentar um generische Definitionen von min und max ging, damit Benutzer sie nicht selbst deklarieren müssen. Beim erneuten Lesen des OP kann ich sehen, dass sie nur einfache Min- und Max-Werte für Ganzzahlen wollen, also mein Fehler, aber sie waren nicht zum Thema und haben in einem Thread über Generika nach einfachen Integrationsfunktionen gefragt :-)

Generics sind eine entscheidende Ergänzung zu dieser Sprache, insbesondere angesichts des Mangels an eingebauten Datenstrukturen. Meine bisherige Erfahrung mit Go ist, dass es eine großartige und leicht zu erlernende Sprache ist. Es hat jedoch einen großen Nachteil, nämlich dass Sie immer und immer wieder dieselben Dinge codieren müssen.

Vielleicht fehlt mir etwas, aber das scheint ein ziemlich großer Fehler in der Sprache zu sein. Unterm Strich gibt es nur wenige eingebaute Datenstrukturen, und jedes Mal, wenn wir eine Datenstruktur erstellen, müssen wir den Code kopieren und einfügen, um jeden T zu unterstützen.

Ich bin mir nicht sicher, was ich beitragen soll, außer meine Beobachtung hier als "Benutzer" zu posten. Ich bin kein erfahrener Programmierer, um zum Design oder zur Implementierung beizutragen, daher kann ich nur sagen, dass Generika die Produktivität in der Sprache erheblich verbessern würden (solange die Build-Zeit und die Tools so großartig bleiben, wie sie jetzt sind).

@webern Danke. Siehe https://go.googlesource.com/proposal/+/master/design/go2draft.md .

@ianlancetaylor , nach dem Posten kam mir eine ziemlich radikale/einzigartige Idee in den Sinn, die meiner Meinung nach in Bezug auf Sprache und Werkzeuge „leicht“ wäre. Ich habe deinen Link noch nicht vollständig gelesen, das werde ich tun. Aber wenn ich eine Idee/einen Vorschlag für eine generische Programmierung im MD-Format einreichen möchte, wie würde ich das tun?

Danke.

@webern Schreiben Sie es auf (die meisten Leute haben Gists für das Markdown-Format verwendet) und aktualisieren Sie das Wiki hier https://github.com/golang/go/wiki/Go2GenericsFeedback

Viele andere haben dies bereits getan.

Ich habe die CL unserer Pre-Gophercon-Prototypenimplementierung eines Parsers (und Druckers) zusammengeführt (gegen den neuesten Tipp) und hochgeladen, der den Vertragsentwurf implementiert. Wenn Sie daran interessiert sind, die Syntax auszuprobieren, schauen Sie bitte: https://golang.org/cl/149638 .

Damit zu spielen:

1) Wählen Sie die CL in einem aktuellen Repo aus:
git fetch https://go.googlesource.com/go refs/changes/38/149638/2 && git cherry-pick FETCH_HEAD

2) Erstellen Sie den Compiler neu und installieren Sie ihn:
gehe cmd installieren/kompilieren

3) Verwenden Sie den Compiler:
go-Tool kompilieren foo.go

Einzelheiten finden Sie in der CL-Beschreibung. Genießen!

contract Addable(t T) {
    t + t
}

func Sum(type T Addable)(x []T) T {
    var total T
    for _, v := range x {
        total += v
    }
    return total
}

Dieses Generika-Design, func Sum(type T Addable)(x []T) T , ist SEHR SEHR SEHR HÄSSLICH!!!

Um mit func Sum(type T Addable)(x []T) T verglichen zu werden, denke ich, dass func Sum<T: Addable> (x []T) T klarer ist und keine Belastung für den Programmierer darstellt, der aus anderen Programmiersprachen kommt.

Sie meinen, die Syntax ist ausführlicher?
Es muss einen Grund geben, warum es nicht func Sum(T Addable)(x []T) T ist.

Ohne das Schlüsselwort type gibt es keine Möglichkeit, zwischen einer generischen Funktion und einer Funktion zu unterscheiden, die eine andere Funktion zurückgibt, die selbst aufgerufen wird.

@urandom Das ist nur ein Problem zur Instanziierungszeit und dort benötigen wir nicht das Schlüsselwort type , sondern leben einfach mit der Mehrdeutigkeit AIUI.

Das Problem ist, dass type #$2$#$ ohne das Schlüsselwort func Foo(x T) (y T) geparst werden könnte, indem es entweder eine generische Funktion deklariert, die ein T nimmt und nichts zurückgibt, oder eine nicht generische Funktion, die ein T nimmt T .

func Summe(x []T)T

Ich stimme zu, ich bevorzuge etwas in dieser Richtung. Angesichts der Erweiterung des sprachlichen Umfangs, der durch Generika dargestellt wird, halte ich es für sinnvoll, diese Syntax einzuführen, um auf eine generische Funktion "aufmerksam zu machen".

Ich denke auch, dass dies das Analysieren von Code für menschliche Leser etwas einfacher (sprich: weniger Lisp-y) machen und die Wahrscheinlichkeit verringern würde, später auf einige obskure Parsing-Mehrdeutigkeiten zu stoßen (siehe C++s "Most Vexing Parse", um eine Fülle von Vorsicht zu motivieren).

Es ist 2018, Leute, ich kann nicht glauben, dass ich das tatsächlich wie 1982 tippen muss:

func min(x, y int) int {
wenn x < y {
Rückgabe x
}
gib y zurück
}

func max(x, y int) int {
wenn x > y {
Rückgabe x
}
gib y zurück
}

Ich meine, im Ernst, Leute MIN(INT,INT) INT, wie ist das NICHT in der Sprache?
Ich bin verärgert.

Es gibt einen Grund dafür.
Wenn du es nicht verstehst, kannst du lernen oder weggehen.
Deine Entscheidung.

Ich hoffe sehr, dass sie es besser machen.
Aber Ihre „Sie können lernen oder weggehen“-Einstellung ist kein gutes Beispiel, dem andere folgen können. es liest sich unnötig aggressiv. Ich glaube nicht, dass es in dieser Community um @petar-dambovaliev geht. Es ist jedoch nicht an mir, Ihnen zu sagen, was Sie tun oder wie Sie sich online verhalten sollen, das ist nicht meine Aufgabe.

Ich weiß, dass es viele starke Gefühle gegenüber Generika gibt, aber denken Sie bitte an unsere Gopher-Werte . Bitte führen Sie das Gespräch auf allen Seiten respektvoll und einladend.

@bcmills Danke, du machst die Community zu einem besseren Ort.

@katzdm stimmte zu, die Sprache hat schon so viele Klammern, dieses neue Zeug sieht für mich wirklich mehrdeutig aus

Die Definition generics scheint unvermeidlich, Dinge wie type's type einzuführen, was Go ziemlich kompliziert macht.

Hoffe, das ist nicht zu off-topic, aber ein Feature von function overload scheint mir ausreichend zu sein.

Übrigens, ich weiß, dass es einige Diskussionen über das Überladen von .

@xgfone Stimmen Sie zu , dass die Sprache bereits so viele Klammern hat, was den Code unklar macht.
func Sum<T: Addable> (x []T) T oder func Sum<type T Addable> (x []T) T ist besser und klarer.

Aus Gründen der Konsistenz (mit integrierten Generika) ist func Sum[T: Addable] (x []T) T besser als func Sum<T: Addable> (x []T) T .

Ich bin vielleicht von früheren Arbeiten in anderen Sprachen beeinflusst, aber Sum<T: Addable> (x []T) T erscheint auf den ersten Blick deutlicher und lesbarer.

Ich stimme auch @katzdm dahingehend zu, dass es besser ist, die Aufmerksamkeit auf etwas Neues in der Sprache zu lenken. Es ist auch Nicht-Go-Entwicklern, die in Go einsteigen, ziemlich vertraut.

FWIW, es besteht eine Wahrscheinlichkeit von ungefähr 0 %, dass Go spitze Klammern für Generika verwendet. Die Grammatik von C++ ist nicht parsbar, da Sie a < b > c (eine zulässige, aber bedeutungslose Reihe von Vergleichen) nicht von einem generischen Aufruf unterscheiden können, ohne die Typen von a, b und c zu verstehen. Andere Sprachen vermeiden aus diesem Grund die Verwendung von spitzen Klammern für Generika.

func a < b Addable> (...
Ich denke, Sie können das, wenn Sie erkennen, dass Sie nach func nur entweder den Funktionsnamen, ein ( oder ein < haben können.

@carlmjohnson Ich hoffe du hast Recht

f := sum<int>(10)

Aber hier wissen Sie, dass sum ein Vertrag ist.

Die Grammatik von C++ ist nicht parsbar, da Sie a < b > c (eine zulässige, aber bedeutungslose Reihe von Vergleichen) nicht von einem generischen Aufruf unterscheiden können, ohne die Typen von a, b und c zu verstehen.

Ich denke, es ist erwähnenswert, dass Go, anders als C++, dies im Typsystem verbietet, da die Operatoren < und > bool s in Go und < zurückgeben > können nicht mit bool s verwendet werden, es _ist_ syntaktisch legal, also ist dies immer noch ein Problem.

Ein weiteres Problem mit spitzen Klammern ist List<List<int>> , in dem >> als Rechtsverschiebungsoperator tokenisiert wird.

Welche Probleme gab es bei der Verwendung [] ? Es scheint mir, dass die meisten der oben genannten Probleme gelöst werden, indem sie verwendet werden:

  • Syntaktisch ist f := sum[int](10) , um das obige Beispiel zu verwenden, eindeutig, weil es die gleiche Syntax wie ein Array- oder Map-Zugriff hat, und das Typsystem kann es später herausfinden, genauso wie es es bereits tun muss zum Beispiel der Unterschied zwischen Array- und Map-Zugriffen. Dies unterscheidet sich von <> , da ein einzelnes < legal ist, was zu Mehrdeutigkeiten führt, ein einzelnes [ jedoch nicht.
  • func Example[T](v T) T ist ebenfalls eindeutig.
  • ]] ist kein eigener Token, sodass dieses Problem ebenfalls vermieden wird.

Der Designentwurf erwähnt eine Mehrdeutigkeit in Typdeklarationen , wie z. B. in type A [T] int , aber ich denke, dass dies auf verschiedene Arten relativ einfach gelöst werden könnte. Beispielsweise könnte die generische Definition in das Schlüsselwort selbst verschoben werden, anstatt in den Typnamen, d. h.:

  • func[T] Example(v T) T
  • type[T] A int

Die Komplikation hier könnte durch die Verwendung von Typdeklarationsblöcken entstehen, wie z

type (
  A int
)

Aber ich denke, das ist selten genug, dass es in Ordnung ist, im Grunde zu sagen, dass Sie keinen dieser Blöcke verwenden können, wenn Sie Generika benötigen.

Ich denke, es wäre sehr unglücklich zu schreiben

type[T] A []T
var s A[int]

weil sich die eckigen Klammern von einer Seite von A zur anderen bewegen. Natürlich könnte man das machen, aber wir sollten nach Besserem streben.

Allerdings bedeutet die Verwendung des Schlüsselworts type in der aktuellen Syntax, dass wir Klammern durch eckige Klammern ersetzen könnten.

Dies scheint sich nicht so sehr von der Array-Typ- vs. Ausdruckssyntax zu unterscheiden, die [N]T vs. arr[i] ist, in Bezug darauf, wie etwas deklariert wird, das nicht mit seiner Verwendung übereinstimmt. Ja, in var arr [N]T enden die eckigen Klammern auf der gleichen Seite von arr wie bei der Verwendung arr , aber wir denken normalerweise an die Syntax in Bezug auf Typ- vs. Ausdruckssyntax gegenüber sein.

Ich habe einige meiner alten unausgereiften Ideen erweitert und verbessert, um zu versuchen, benutzerdefinierte und eingebaute Generika zu vereinheitlichen.

Ich bin mir nicht sicher, ob die Diskussion über ( vs. < vs. [ und die Verwendung von type Bikeshedding ist oder ob es wirklich ein Problem mit der Syntax gibt

@ianlancetaylor ... fragte sich, ob das Feedback irgendwelche Änderungen am vorgeschlagenen Design rechtfertigte? Mein eigener Eindruck des Feedbacks war, dass viele der Meinung waren, dass Schnittstellen und Verträge zumindest anfänglich kombiniert werden könnten. Nach einer Weile schien es eine Verschiebung zu geben, dass die beiden Konzepte getrennt gehalten werden sollten. Aber ich könnte die Trends falsch lesen. Würde gerne eine experimentelle Option in einer Veröffentlichung dieses Jahr sehen!

Ja, wir erwägen Änderungen am Designentwurf, einschließlich der Betrachtung der vielen Gegenvorschläge, die von den Leuten gemacht wurden. Nichts ist abgeschlossen.

Nur um noch einen Erfahrungsbericht aus der Praxis hinzuzufügen:
Ich habe Generika als Spracherweiterung in meinem Go-Interpreter https://github.com/cosmos72/gomacro implementiert. Interessanterweise sind beide Syntaxen

type[T] Pair struct { First T; Second T }
type Pair[T] struct { First T; Second T }

Es stellte sich heraus, dass der Parser viele Mehrdeutigkeiten einführte: Die zweite könnte als Deklaration geparst werden, dass Pair ein Array von T -Strukturen ist, wobei T eine konstante Ganzzahl ist. Wenn Pair verwendet wird, gibt es auch Mehrdeutigkeiten: Pair[int] könnte auch als Ausdruck statt als Typ geparst werden: Es könnte ein Array/Slice/Map mit dem Namen Pair indizieren der Indexausdruck int (Anmerkung: int und andere Grundtypen sind KEINE reservierten Schlüsselwörter in Go), also musste ich auf eine neue Syntax zurückgreifen - zugegebenermaßen hässlich, aber funktioniert:

template[T] type Pair struct { First T; Second T }
type pairOfInt = Pair#[int]
var p Pair#[int]

und ähnlich für Funktionen:

template[T] func Sum(args ...T) T { /*...*/ }
Sum#[int] (1,2,3)

Obwohl ich theoretisch zustimme, dass Syntax eine oberflächliche Angelegenheit ist, muss ich darauf hinweisen, dass:
1) Einerseits ist es die Syntax, der Go-Programmierer ausgesetzt sein werden - also muss sie ausdrucksstark, einfach und möglicherweise schmackhaft sein
2) Auf der anderen Seite wird eine schlechte Wahl der Syntax den Parser, Typechecker und Compiler erschweren, um die eingeführten Mehrdeutigkeiten aufzulösen

Pair[int] könnte auch als Ausdruck statt als Typ geparst werden: Es könnte ein Array/Slice/Map namens Pair mit dem Indexausdruck int indizieren

Dies ist keine Parsing-Mehrdeutigkeit, sondern nur eine semantische (bis nach der Namensauflösung); die syntaktische Struktur ist in beiden Fällen gleich. Beachten Sie, dass Sum#[int] auch entweder ein Typ oder ein Ausdruck sein kann, je nachdem, was Sum ist. Dasselbe gilt für (*T) in vorhandenem Code. Solange die Namensauflösung die Struktur dessen, was analysiert wird, nicht beeinflusst, ist alles in Ordnung.

Vergleichen Sie dies mit den Problemen mit <> :

f ( a < b , c < d >> (e) )

Sie können dies nicht einmal in Tokens umwandeln, da >> ein oder zwei Tokens sein können. Dann können Sie nicht sagen, ob es ein oder zwei Argumente für f gibt ... die Struktur des Ausdrucks ändert sich erheblich, je nachdem, was mit a bezeichnet wird.

Wie auch immer, ich bin interessiert zu sehen, was die aktuelle Meinung im Team zu Generika ist, insbesondere, ob "Einschränkungen-sind-nur-Code" iteriert oder aufgegeben wurden. Ich kann verstehen, dass ich vermeiden möchte, eine eindeutige Einschränkungssprache zu definieren, aber es stellt sich heraus, dass das Schreiben von Code, der die beteiligten Typen ausreichend einschränkt, einen unnatürlichen Stil erzwingt, und Sie müssen auch Grenzen setzen, was der Compiler tatsächlich über Typen basierend auf dem Code ableiten kann denn sonst können diese Schlüsse beliebig komplex werden oder sich auf Tatsachen über die Sprache stützen, die sich in Zukunft ändern könnten.

@ cosmos72

Vielleicht irre ich mich, aber neben dem, was @stevenblenkinsop gesagt hat, ist es alles möglich, dass ein Begriff:

a b

könnte auch implizieren, dass b kein Typ ist, wenn b als alphanumerisch (kein Operator/kein Trennzeichen) mit optional angehängtem [identifier] bekannt ist und a kein spezielles Schlüsselwort/spezieller alphanumerischer Wert ist (z. B. kein Import/ Paket/Typ/Funktion)?.

Kenne die Grammatik von go too much nicht.

In gewisser Weise würden Typen wie int und Sum[int] sowieso als Ausdrücke behandelt:

type (
    nodeList = []*Node  // nodeList and []*Node are identical types
    Polar    = polar    // Polar and polar denote identical types
)

Wenn go Infix-Funktionen zulassen würde, dann wäre a type tag in der Tat mehrdeutig, da type eine Infix-Funktion oder ein Typ sein könnte.

Mir ist heute aufgefallen, dass die Problemübersicht dieses Vorschlags Ansprüche von Swift hat:

Die Erklärung, dass T das Equatable -Protokoll erfüllt, macht die Verwendung von == im Funktionsrumpf gültig. Equatable scheint in Swift integriert zu sein und kann nicht anders definiert werden.

Dies scheint eher eine Nebensache zu sein als etwas, das die zu diesem Thema getroffenen Entscheidungen tief beeinflusst, aber für den unwahrscheinlichen Fall, dass es Menschen, die viel klüger sind als ich, etwas Inspiration gibt, wollte ich darauf hinweisen, dass es eigentlich nichts Besonderes gibt über Equatable , außer dass es in der Sprache vordefiniert ist (hauptsächlich, damit viele andere eingebaute Typen sich daran "anpassen"). Es ist durchaus möglich, ähnliche Protokolle zu erstellen:

protocol Equatable2 {
    static func == (lhs: Self, rhs: Self) -> Bool
}

class uniq: Equatable2 {
    static func == (lhs: uniq, rhs: uniq) -> Bool {
        return false
    }
}

let narf = uniq(), poit = uniq()

func !=<T: Equatable2> (lhs: T, rhs: T) -> Bool {
    return !(lhs == rhs)
}

print(narf != poit)

@sighoya
Ich sprach über Mehrdeutigkeiten der für Generika vorgeschlagenen Syntax a[b] , da sie bereits zum Indizieren von Slices und Maps verwendet wird - nicht über a b .

In der Zwischenzeit habe ich Haskell studiert, und obwohl ich vorher wusste, dass es ausgiebig Typinferenz verwendet, hat mich die Ausdruckskraft und Raffinesse seiner Generika überrascht.

Leider hat es ein ziemlich eigenartiges Namensschema, so dass es auf den ersten Blick nicht immer leicht zu verstehen ist. Zum Beispiel ist ein class eigentlich eine Einschränkung für Typen (generisch oder nicht). Die Klasse Eq ist die Einschränkung für Typen, deren Werte mit '==' und '/=' verglichen werden können:

class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool

bedeutet, dass ein Typ a die Einschränkung Eq $ erfüllt, wenn eine "Spezialisierung" (eigentlich eine "Instanz" im Haskell-Jargon) der Infix-Funktionen == und /= existiert a akzeptiert und ein Bool -Ergebnis zurückgibt.

Ich versuche derzeit, einige der in Haskell-Generika gefundenen Ideen an einen Vorschlag für Go-Generika anzupassen und zu sehen, wie gut sie passen. Ich bin wirklich froh zu sehen, dass Untersuchungen mit anderen Sprachen als C++ und Java im Gange sind:

Das obige Swift-Beispiel und mein Haskell-Beispiel zeigen, dass Einschränkungen für generische Typen bereits in der Praxis von mehreren Programmiersprachen verwendet werden und dass eine nicht triviale Menge an Erfahrung mit verschiedenen Ansätzen für Generics und Einschränkungen existiert und unter Programmierern verfügbar ist (und andere) Sprachen.

Meiner Meinung nach lohnt es sich auf jeden Fall, solche Erfahrungen zu studieren, bevor man einen Vorschlag für Go-Generika abschließt.

Verirrter Gedanke: Wenn die Form der Einschränkung, die der generische Typ erfüllen soll, zufällig mehr oder weniger kongruent zu einer Schnittstellendefinition ist, können Sie die vorhandene Typzusicherungssyntax verwenden, an die wir bereits gewöhnt sind:

type Comparer interface {
  Compare(v interface{}) (*int, error)
}
type PriorityQueue<T.(Comparer)> struct {
  things []T
}

Entschuldigung, wenn dies bereits an anderer Stelle ausführlich diskutiert wurde; Ich habe es nicht gesehen, aber ich beschäftige mich immer noch mit der Literatur. Ich habe es eine Weile ignoriert, weil ich in keiner Version von Go Generika haben möchte. Aber die Idee scheint in der Community insgesamt an Fahrt zu gewinnen und ein Gefühl der Unvermeidlichkeit zu bekommen.

@jesse-amano Es ist interessant, dass Sie in keiner Version von Go Generika wollen. Ich finde das schwer zu verstehen, weil ich mich als Programmierer wirklich nicht gerne wiederhole. Immer wenn ich in 'C' programmiere, muss ich die gleichen grundlegenden Dinge wie eine Liste oder einen Baum auf einem neuen Datentyp implementieren, und meine Implementierungen sind zwangsläufig voller Fehler. Bei Generika können wir von jedem Algorithmus nur eine Version haben, und die gesamte Community kann dazu beitragen, dass diese eine Version die beste ist. Was ist Ihre Lösung, um sich nicht zu wiederholen?

In Bezug auf den anderen Punkt scheint Go eine neue Syntax für generische Einschränkungen einzuführen, da Schnittstellen keine überladenden Operatoren (wie '==' und '+') zulassen. Es gibt zwei Möglichkeiten, einen neuen Mechanismus für generische Einschränkungen zu definieren, was der Weg ist, den Go zu gehen scheint, oder Schnittstellen zu erlauben, Operatoren zu überladen, was der Weg ist, den ich bevorzuge.

Ich bevorzuge die zweite Option, weil sie die Sprachsyntax kleiner und einfacher hält und es ermöglicht, neue numerische Typen zu deklarieren, die die üblichen Operatoren verwenden können, zum Beispiel komplexe Zahlen, die Sie mit '+' hinzufügen können. Das Argument dagegen scheint zu sein, dass Leute das Überladen von Operatoren missbrauchen könnten, um '+' dazu zu bringen, seltsame Dinge zu tun, aber das scheint mir kein Argument zu sein, weil ich bereits jeden Funktionsnamen missbrauchen kann, zum Beispiel kann ich eine Funktion namens 'print' schreiben ', das alle Daten auf meiner Festplatte löscht und das Programm beendet. Ich hätte gerne die Möglichkeit, Überladungen von Operatoren und Funktionen einzuschränken, um bestimmten axiomatischen Eigenschaften wie Kommutativität oder Assoziativität zu entsprechen, aber wenn dies nicht sowohl für Operatoren als auch für Funktionen gilt, sehe ich keinen Sinn. Ein Operator ist nur eine Infix-Funktion, und eine Funktion ist schließlich nur ein Präfix-Operator.

Ein weiterer zu erwähnender Punkt ist, dass generische Einschränkungen, die auf mehrere Typparameter verweisen, sehr nützlich sind, wenn generische Einschränkungen für einzelne Parameter Prädikate für Typen sind, Einschränkungen für mehrere Parameter Relationen für Typen sind. Go-Schnittstellen können nicht mehr als einen Typparameter haben, also muss wieder entweder eine neue Syntax eingeführt oder Schnittstellen neu gestaltet werden.

In gewisser Weise stimme ich Ihnen also zu, dass Go nicht als generische Sprache konzipiert wurde, und jeder Versuch, Generika anzubauen, wird suboptimal sein. Vielleicht ist es besser, Go ohne Generika zu belassen und eine neue Sprache von Grund auf um Generika herum zu entwerfen, um die Sprache mit einer einfachen Syntax klein zu halten.

@keean Ich habe keine so starke Abneigung dagegen, mich bei Bedarf ein paar Mal zu wiederholen, und Gos Ansatz zur Fehlerbehandlung, Methodenempfänger usw. scheint im Allgemeinen gute Arbeit zu leisten, um die meisten Fehler in Schach zu halten.

In einer Handvoll Fällen in den letzten vier Jahren habe ich mich in Situationen wiedergefunden, in denen ein komplexer, aber verallgemeinerbarer Algorithmus auf mehr als zwei komplexe, aber in sich konsistente Datenstrukturen angewendet werden musste, und zwar in allen Fällen – und ich sage das mit allen Ernstes -- ich fand die Codegenerierung über go:generate mehr als ausreichend.

Wenn ich Erfahrungsberichte durchlese, denke ich in vielen Fällen, dass go:generate oder ein ähnliches Tool das Problem hätte lösen können, und in einigen anderen Fällen habe ich das Gefühl, dass Go1 vielleicht einfach nicht die richtige Sprache war, und etwas anderes hätte es sein können stattdessen verwendet (vielleicht mit einem Plugin-Wrapper, wenn etwas Go-Code benötigt wird, um davon Gebrauch zu machen). Aber ich bin mir bewusst, dass es leicht genug für mich ist, zu spekulieren, was ich getan _hätte_ getan haben könnte, was _möglicherweise_ funktioniert hat; Ich hatte bisher keine praktischen Erfahrungen, die mich wünschten, Go1 hätte mehr Möglichkeiten, generische Typen auszudrücken, aber es könnte sein, dass ich eine seltsame Denkweise über Dinge habe, oder es könnte sein, dass ich einfach nur großes Glück hatte, nur zu arbeiten bei Projekten, die eigentlich keine Generika brauchten.

Ich hoffe, dass Go2, wenn es eine generische Syntax unterstützt, eine ziemlich einfache Zuordnung zur generierten Logik hat, ohne dass seltsame Randfälle möglicherweise durch Boxen / Unboxing, "Verdinglichung", Vererbungsketten usw. entstehen. um die sich andere Sprachen kümmern müssen.

@jesse-amano Meiner Erfahrung nach ist es nicht nur ein paar Mal, jedes Programm ist eine Zusammensetzung bekannter Algorithmen. Ich kann mich nicht erinnern, wann ich das letzte Mal einen Originalalgorithmus geschrieben habe, vielleicht ein komplexes Optimierungsproblem, das Domänenkenntnisse erforderte.

Wenn ich ein Programm schreibe, versuche ich als Erstes, das Problem in bekannte Teile zu zerlegen, die ich zusammenstellen kann, einen Argument-Parser, etwas Datei-Streaming, ein Constraint-basiertes Layout der Benutzeroberfläche. Es sind nicht nur komplexe Algorithmen, in denen Menschen Fehler machen, kaum jemand kann beim ersten Mal eine korrekte Implementierung von "min" und "max" schreiben (siehe: http://componentsprogramming.com/writing-min-function-part5/ ).

Das Problem mit go:generate ist, dass es im Grunde nur ein Makroprozessor ist, es hat keine Typsicherheit, Sie müssen den generierten Code irgendwie auf Typprüfung und Fehlerprüfung überprüfen, was Sie nicht tun können, bis Sie die Generierung ausgeführt haben. Diese Art der Metaprogrammierung ist sehr schwer zu debuggen. Ich möchte kein Programm schreiben, um das Programm zu schreiben, ich möchte nur das Programm schreiben :-)

Der Unterschied zu Generika besteht also darin, dass ich ein einfaches _direktes_ Programm schreiben kann, das nach meinem Verständnis der Bedeutung auf Fehler überprüft und typgeprüft werden kann, ohne den Code generieren und diesen debuggen und die Fehler zum Generator zurückführen zu müssen.

Ein wirklich einfaches Beispiel ist "swap", ich möchte nur zwei Werte tauschen, es ist mir egal, was sie sind:

swap<A>(x: *A, y: *A) {
   let tmp = *x
   *x = *y
   *y = tmp
}

Nun, ich denke, es ist trivial zu sehen, ob diese Funktion korrekt ist, und es ist trivial zu sehen, dass sie generisch ist und auf jeden Typ angewendet werden kann. Warum sollte ich diese Funktion immer und immer wieder für jeden Zeigertyp auf einen Wert eingeben, für den ich möglicherweise einen Swap verwenden möchte? Natürlich kann ich daraus größere generische Algorithmen wie eine In-Place-Sortierung erstellen. Ich glaube nicht, dass der go:generate-Code selbst für einen einfachen Algorithmus leicht zu erkennen wäre, ob er korrekt ist.

Ich könnte leicht einen Fehler machen wie:

let tmp = *x
*y = *x
*x = tmp

Ich tippe dies jedes Mal von Hand ein, wenn ich den Inhalt von zwei Zeigern austauschen wollte.

Ich verstehe, dass der idiomatische Weg, so etwas in Go zu tun, darin besteht, eine leere Schnittstelle zu verwenden, aber das ist nicht typsicher und langsam. Mir scheint jedoch, dass Go nicht über die richtigen Funktionen verfügt, um diese Art der generischen Programmierung elegant zu unterstützen, und leere Schnittstellen bieten einen Ausweg, um die Probleme zu umgehen. Anstatt den Go-Stil komplett zu ändern, scheint es besser, eine für diese Art von Generika geeignete Sprache von Grund auf neu zu entwickeln. Interessanterweise macht 'Rust' viele der generischen Dinge richtig, aber weil es statische Speicherverwaltung anstelle von Garbage Collection verwendet, fügt es eine ganze Menge Komplexität hinzu, die für die meisten Programmierungen nicht wirklich notwendig ist. Ich denke, zwischen Haskell, Go und Rust gibt es wahrscheinlich alle Teile, die notwendig sind, um eine anständige generische Mainstream-Sprache zu machen, nur alles durcheinander.

Zur Information: Ich schreibe gerade eine Wunschliste zu Go-Generika,

mit der Absicht, es tatsächlich in meinem Go-Interpreter gomacro zu implementieren, der bereits eine andere Implementierung von Go-Generika hat (nach dem Vorbild von C++-Vorlagen).

Es ist noch nicht fertig, Feedback ist willkommen :)

@Keean

Ich habe den von Ihnen verlinkten Blogbeitrag über die min-Funktion und die vier Beiträge gelesen, die dazu geführt haben. Ich habe nicht einmal den Versuch beobachtet, zu argumentieren, dass "kaum jemand eine korrekte Implementierung von 'min' schreiben kann ...". Der Verfasser scheint tatsächlich anzuerkennen, dass ihre erste Implementierung korrekt _ist_ ... solange die Domäne auf Zahlen beschränkt ist. Es ist die Einführung von Objekten und Klassen und die Anforderung, dass sie nur entlang einer Dimension verglichen werden, es sei denn, die Werte in dieser Dimension sind dieselben, außer wann – und so weiter, was die zusätzliche Komplexität erzeugt. Die subtilen versteckten Anforderungen, die mit der Notwendigkeit verbunden sind, die Komparator- und Sortierfunktionen für ein komplexes Objekt sorgfältig zu definieren, sind genau der Grund, warum ich Generika als Konzept _nicht_ mag (zumindest in Go; Java mit Spring scheint bereits eine ausreichend gute Umgebung zum Komponieren zu sein eine Reihe ausgereifter Bibliotheken zu einer Anwendung zusammenfügen).

Ich persönlich sehe keinen Bedarf an Typsicherheit in Makrogeneratoren; Wenn sie lesbaren Code generieren ( gofmt hilft dabei, die Messlatte dafür ziemlich niedrig zu legen), sollte die Fehlerprüfung zur Kompilierzeit ausreichen. Es sollte dem Benutzer des Generators (oder des Codes, der ihn aufruft) für die Produktion sowieso egal sein; in den zugegebenermaßen wenigen Fällen, in denen ich aufgefordert wurde, einen generischen Algorithmus als Makro zu schreiben, eine Handvoll Unit-Tests (normalerweise Float, String und Pointer-to-struct – falls es irgendwelche hartcodierten Typen gibt, die das tun sollten nicht fest codiert sein, einer dieser drei wird damit inkompatibel sein; wenn einer dieser drei nicht im generischen Algorithmus verwendet werden kann, dann ist es kein generischer Algorithmus) ausreichend war, um sicherzustellen, dass das Makro ordnungsgemäß funktioniert.

swap ist ein schlechtes Beispiel. Entschuldigung, aber es ist so. Es ist bereits ein Einzeiler in Go, es ist keine generische Funktion erforderlich, um es zu umschließen, und kein Platz für einen Programmierer, um einen nicht offensichtlichen Fehler zu machen.

*y, *x = *x, *y

Es gibt auch bereits ein in-place sort in der Standardbibliothek . Es nutzt Schnittstellen. Um eine für Ihren Typ spezifische Version zu erstellen, definieren Sie:

type myslice []mytype
func (s myslice) Len() int { return len(s) }
func (s myslice) Less(i, j int) bool { return s[i].whatWouldAlsoBeNeededInAGenericImpl(s[j]) }
func (s myslice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

Es sind zugegebenermaßen mehrere Bytes mehr zu tippen als SortableList<mytype>(myThings).Sort() , aber es ist _viel_ weniger dicht zu lesen, es ist nicht so wahrscheinlich, dass es während des Rests einer Anwendung "stottert", und wenn Fehler auftreten, bin ich unwahrscheinlich etwas so Schweres wie einen Stack-Trace zu brauchen, um die Ursache zu finden. Der aktuelle Ansatz hat mehrere Vorteile, und ich befürchte, dass wir sie verlieren würden, wenn wir uns zu sehr auf Generika stützen würden.

@jesse-amano
Die Probleme mit 'min/max' gelten auch dann, wenn Sie die Notwendigkeit einer stabilen Sortierung nicht verstehen. Zum Beispiel implementiert ein Entwickler Min/Max für einen Datentyp in einem Modul, und dann wird es von einem anderen Teammitglied in einer Sortierung oder einem anderen Algorithmus verwendet, ohne die Annahmen ordnungsgemäß zu überprüfen, und führt zu seltsamen Fehlern, weil es nicht stabil ist.

Ich denke, Programmieren besteht hauptsächlich darin, Standardalgorithmen zu erstellen, Programmierer erstellen sehr selten neue innovative Algorithmen, daher sind Min/Max und Sort nur Beispiele. Das Picken von Löchern in den spezifischen Beispielen, die ich ausgewählt habe, zeigt nur, dass ich keine sehr guten Beispiele ausgewählt habe, es spricht nicht den eigentlichen Punkt an. Ich habe "swap" gewählt, weil es sehr einfach und schnell zu tippen ist. Ich hätte viele andere auswählen können, sortieren, rotieren, partitionieren, die sehr allgemeine Algorithmen sind. Wenn Sie ein Programm schreiben, das eine Sammlung wie einen rot/schwarzen Baum verwendet, dauert es nicht lange, bis Sie es satt haben, den Baum für jeden anderen Datentyp, von dem Sie eine Sammlung haben möchten, neu erstellen zu müssen, weil Sie Typsicherheit wollen, und eine leere Schnittstelle ist kaum besser als "void*" in 'C'. Dann müssten Sie dasselbe noch einmal für jeden Algorithmus tun, der jeden dieser Bäume verwendet, wie z Mengen, Halden, Minimum Spanning Trees, Shortest Paths, Flows etc.)

Ich denke, Codegeneratoren haben ihren Platz, zum Beispiel einen Validator aus einem JSON-Schema oder einen Parser aus einer Grammatikdefinition zu generieren, aber ich denke nicht, dass sie einen geeigneten Ersatz für Generika darstellen. Für die generische Programmierung möchte ich in der Lage sein, jeden Algorithmus einmal zu schreiben und ihn klar, einfach und direkt zu haben.

Auf jeden Fall stimme ich Ihnen in Bezug auf „Go“ zu, ich glaube nicht, dass ein „Go“ von Anfang an als gute generische Sprache konzipiert wurde, und das Hinzufügen von Generika jetzt wird wahrscheinlich nicht zu einer guten generischen Sprache führen, und wird etwas von der Direktheit und Einfachheit verlieren, die es bereits hat. Wenn Sie persönlich nach einem Codegenerator greifen müssen (über Dinge wie das Generieren von Validatoren aus JSON-Schema oder Parser aus einer Grammatikdatei hinaus), verwenden Sie wahrscheinlich sowieso die falsche Sprache.

Bearbeiten: In Bezug auf das Testen von Generika mit "float" "string" "pointer-to-struct" glaube ich nicht, dass es viele generische Algorithmen gibt, die mit so unterschiedlichen Typen arbeiten, außer vielleicht "swap". Echte „generische“ Funktionen sind wirklich auf Shuffles beschränkt und kommen nicht sehr oft vor. Eingeschränkte Generika sind viel interessanter, wenn die generischen Typen durch eine Schnittstelle eingeschränkt werden. Wie Sie sehen können, können Sie mit dem In-Place-Sortierungsbeispiel aus der Standardbibliothek einige eingeschränkte Generika in begrenzten Fällen in 'Go' zum Laufen bringen. Ich mag die Art und Weise, wie Go-Schnittstellen funktionieren, und man kann viel damit machen. Ich mag echte Generika mit Einschränkungen noch mehr. Ich mag es nicht wirklich, einen zweiten Beschränkungsmechanismus hinzuzufügen, wie es der aktuelle Generika-Vorschlag tut. Eine Sprache, in der Schnittstellen Typen direkt einschränken, wäre viel eleganter.

Es ist interessant, dass, soweit ich das beurteilen kann, der einzige Grund, warum die neuen Einschränkungen eingeführt wurden, darin besteht, dass Go es nicht zulässt, dass Operatoren in Schnittstellen definiert werden. Frühere Generika-Vorschläge erlaubten es zwar, Typen durch Schnittstellen einzuschränken, wurden aber verworfen, weil sie Operatoren wie „+“ nicht zuließen.

@Keean
Vielleicht gibt es einen besseren Ort für eine langwierige Diskussion. (Vielleicht nicht; ich habe mich umgesehen und dies scheint _der_ Ort zu sein, um Generika in Go2 zu diskutieren.)

Ich verstehe auf jeden Fall die Notwendigkeit einer stabilen Sorte! Ich vermute, dass die Autoren der ursprünglichen Go1-Standardbibliothek es auch verstanden haben, da sort.Stable dort seit der Veröffentlichung enthalten ist.

Ich denke, das Tolle am sort -Paket der Standardbibliothek ist, dass es _nicht_ nur auf Slices funktioniert. Es ist sicherlich am einfachsten, wenn der Empfänger ein Slice ist, aber alles, was Sie wirklich brauchen, ist eine Möglichkeit zu wissen, wie viele Werte sich im Container befinden (die Len() int -Methode), wie man sie vergleicht (die Less(int, int) bool -Methode) und wie man sie vertauscht (natürlich die Swap(int, int) -Methode). Sie können sort.Interface mithilfe von Kanälen implementieren! Es ist natürlich langsam, weil Kanäle nicht für eine effiziente Indizierung ausgelegt sind, aber es kann sich bei einem großzügigen Budget für die Ausführungszeit als richtig erweisen.

Ich will nicht pingelig sein, aber das Problem mit einem schlechten Beispiel ist, dass … es schlecht ist. Dinge wie sort und min sind nur _keine_ Argumente für ein wirkungsvolles Sprachfeature wie Generika. Ich bin ziemlich stark davon überzeugt, dass das Stechen von Löchern in diesen Beispielen den eigentlichen Punkt anspricht; _mein_ Punkt ist, dass Generika nicht nötig sind, wenn es bereits eine bessere Lösung in der Sprache gibt.

@jesse-amano

bessere Lösung existiert bereits in der Sprache

Welcher? Ich sehe nichts Besseres als typsichere eingeschränkte Generika. Generatoren sind nicht Go, schlicht und einfach. Schnittstellen und Reflektion erzeugen unsicheren, langsamen und panikanfälligen Code. Diese Lösungen sind gut genug, weil es nichts anderes gibt. Generics würden das Problem mit Boilerplates, unsicheren leeren Schnittstellenkonstrukten lösen und, was am schlimmsten ist, viele Verwendungen von Reflektion eliminieren, die noch anfälliger für Laufzeitpaniken sind. Sogar der neue Fehlerpaketvorschlag leidet unter dem Mangel an Generika und seine API würde stark davon profitieren. Sie können sich As als Beispiel ansehen - nicht idiomatisch, anfällig für Panik, schwer zu verwenden, erfordert eine tierärztliche Überprüfung, um richtig verwendet zu werden. Alles nur, weil Go keinerlei Generika enthält.

sort , min und andere generische Algorithmen sind großartige Beispiele, da sie den Hauptvorteil von Generika zeigen – die Zusammensetzbarkeit. Sie ermöglichen den Aufbau einer umfangreichen Bibliothek generischer Transformationsroutinen, die miteinander verkettet werden können. Und am wichtigsten wäre es einfach zu bedienen, sicher, schnell (zumindest ist es mit Generika möglich), keine Notwendigkeit für Boilerplate, Generatoren, Interface {}, Reflexion und andere obskure Sprachfunktionen, die nur verwendet werden, weil es keinen anderen Weg gibt.

@kreker

Welcher?

Zum Sortieren von Sachen das Paket sort . Alles, was sort.Interface implementiert, kann sortiert werden (mit einem stabilen oder instabilen Algorithmus Ihrer Wahl; einige In-Place-Versionen werden über das Paket sort bereitgestellt, aber es steht Ihnen frei, Ihre eigenen mit einem zu schreiben ähnliche oder andere API). Da die Standardbibliotheken sort.Sort und sort.Stable beide mit dem Wert arbeiten, der durch die Argumentliste übergeben wird, ist der zurückgegebene Wert derselbe wie der Wert, mit dem Sie begonnen haben – und daher notwendigerweise der Typ Sie erhalten den gleichen Typ zurück, mit dem Sie begonnen haben. Es ist absolut typsicher, und der Compiler erledigt die ganze Arbeit, um abzuleiten, ob Ihr Typ die benötigte Schnittstelle implementiert und zu _mindestens_ so vielen Optimierungen zur Kompilierzeit fähig ist, wie es mit einer generischen sort<T> -Funktion möglich wäre .

Zum Austauschen von Sachen der Einzeiler x, y = y, x . Auch hier sind keine Typzusicherungen, Schnittstellenumwandlungen oder Reflexionen erforderlich. Es werden nur zwei Werte vertauscht. Der Compiler kann leicht sicherstellen, dass Ihre Operationen typsicher sind.

Es gibt kein einziges spezifisches Tool, das ich in allen Fällen für eine bessere Lösung als Generika halten würde, aber für jedes gegebene Problem, das Generika lösen sollen, gibt es meines Erachtens eine bessere Lösung. Ich könnte mich hier irren; Ich bin immer noch offen für ein Beispiel dafür, was Generika tun können, wo alle bestehenden Lösungen schrecklich gewesen wären. Aber wenn ich Löcher hineinstecken kann, dann ist es nicht eines dieser Beispiele.

Ich mag das xerrors -Paket auch nicht besonders, aber xerrors.As erscheint mir nicht als unidiomatisch; es ist immerhin eine sehr ähnliche API wie json.Unmarshal . Möglicherweise ist eine bessere Dokumentation und/oder Beispielcode erforderlich, aber ansonsten ist es in Ordnung.

Aber nein, sort und min sind für sich genommen ziemlich schreckliche Beispiele. Ersteres existiert bereits in Go und ist perfekt kombinierbar, alles ohne Generika. Letzteres ist im weitesten Sinne eine der Ausgaben von sort (die wir bereits gelöst haben), und in Fällen, in denen eine spezialisiertere oder optimiertere Lösung erforderlich sein könnte, würden Sie die spezialisierte Lösung sowieso schreiben, anstatt sich darauf zu stützen Generika. Auch hier werden im sort -Paket der Standardbibliothek keine Generatoren, Schnittstellen{}, Reflexionen oder "obskuren" Sprachfunktionen verwendet. Es gibt nicht leere Schnittstellen (die in der API gut definiert sind, sodass Sie bei falscher Verwendung Kompilierzeitfehler erhalten, abgeleitet, sodass Sie keine Umwandlungen benötigen, und zur Kompilierzeit überprüft werden, damit Sie sie nicht benötigen Behauptungen). Es könnte einige Standardbausteine ​​geben, _wenn_ die Sammlung, die Sie sortieren, ein Slice ist, aber wenn es zufällig eine Struktur ist (wie eine, die den Wurzelknoten eines binären Suchbaums darstellt?), können Sie dafür sorgen, dass dies die sort.Interface erfüllt

@jesse-amano

Mein Punkt ist, dass es keine Notwendigkeit für Generika gibt, wenn eine bessere Lösung bereits in der Sprache existiert

Ich denke, eine bessere Lösung basiert wirklich relativ darauf, wie Sie es sehen. Wenn wir eine bessere Sprache hätten, könnten wir eine bessere Lösung haben, deshalb wollen wir diese Sprache besser machen. Wenn zum Beispiel ein besseres Generikum existiert, könnten wir besseres sort in unserer stdlib haben, zumindest ist die aktuelle Art und Weise, die Sortierschnittstelle zu implementieren, keine gute Benutzererfahrung für mich, ich muss immer noch eine Menge ähnlichen Code eingeben von dem ich stark glaube, dass wir es abstrahieren könnten.

@jesse-amano

Ich denke, das Tolle am Sort-Paket der Standardbibliothek ist, dass es nicht nur auf Slices funktioniert.

Ich stimme zu, ich mag die Standard-Sorte.

Ersteres existiert bereits in Go und ist perfekt kombinierbar, alles ohne Generika.

Dies ist eine falsche Dichotomie. Schnittstellen in Go sind bereits eine Form von Generika. Der Mechanismus ist nicht das Ding selbst. Schauen Sie über die Syntax hinaus und sehen Sie das Ziel, nämlich die Fähigkeit, jeden Algorithmus ohne Einschränkungen auf generische Weise auszudrücken. Die Interface-Abstraktion von 'sort' ist generisch, sie erlaubt es, jeden Datentyp zu sortieren, der die erforderlichen Methoden implementieren kann. Die Notation ist einfach anders. Wir könnten schreiben:

f<T>(x: T) requires Sortable(T)

Das würde bedeuten, dass der Typ 'T' die Schnittstelle 'Sortable' implementieren muss. In 'Go' könnte dies func f(x Sortable) geschrieben werden. So kann zumindest die Funktionsanwendung in Go generisch gehandhabt werden, aber es gibt Operationen, die keine Arithmetik oder Dereferenzierung mögen. Go schneidet ziemlich gut ab, da Schnittstellen als Typprädikate betrachtet werden können, aber Go hat keine Antwort auf Beziehungen zu Typen.

Es ist leicht, die Einschränkungen mit Go zu erkennen, bedenken Sie:

func merge(x, y Sortable)

wo wir zwei sortierbare Dinge zusammenführen werden, Go lässt uns jedoch nicht erzwingen, dass diese beiden Dinge gleich sein müssen. Vergleichen Sie dies mit:

merge<T>(x: T, y: T) requires Sortable(T)

Hier ist uns klar, dass wir zwei sortierbare Typen zusammenführen, die gleich sind. 'Go' wirft die zugrunde liegenden Typinformationen weg und behandelt einfach alles, was "sortierbar" ist, als gleich.

Versuchen wir es mit einem besseren Beispiel: Nehmen wir an, ich möchte einen rot/schwarzen Baum schreiben, der jeden Datentyp als Bibliothek enthalten kann, damit andere ihn verwenden können.

Schnittstellen in Go sind bereits eine Form von Generika.

Wenn ja, dann kann dieses Problem als bereits gelöst geschlossen werden, denn die ursprüngliche Aussage lautete:

Diese Ausgabe schlägt vor, dass Go irgendeine Form von generischer Programmierung unterstützen sollte.

Äquivokation tut allen Parteien einen Bärendienst. Schnittstellen sind in der Tat _eine_ Form der generischen Programmierung, und sie lösen in der Tat _nicht_ unbedingt alle Probleme, die andere Formen der generischen Programmierung lösen können. Lassen Sie uns also der Einfachheit halber zulassen, dass jedes Problem, das mit Tools außerhalb des Anwendungsbereichs dieses Vorschlags/Problems gelöst werden kann, als "ohne Generika gelöst" betrachtet werden kann. (Ich glaube, dass eine überwältigende Mehrheit der lösbaren Probleme, die in der realen Welt auftreten, wenn nicht alle, in diesem Satz enthalten sind, aber dies soll nur sicherstellen, dass wir alle dieselbe Sprache sprechen.)

Betrachten Sie: func merge(x, y Sortable)

Es ist mir unklar, warum sich das Zusammenführen von zwei sortierbaren Dingen (oder Dingen, die sort.Interface implementieren) in irgendeiner Weise vom Zusammenführen zweier Sammlungen _allgemein_ unterscheidet. Für Slices sind das append ; für Karten sind das for k, v := range m { n[k] = v } ; und für komplexere Datenstrukturen gibt es abhängig von der Struktur notwendigerweise komplexere Zusammenführungsstrategien (deren Inhalt möglicherweise erforderlich ist, um einige Methoden zu implementieren, die die Struktur benötigt). Angenommen, Sie sprechen von einem komplizierteren Sortieralgorithmus, der Unteralgorithmen für die Partitionen partitioniert und auswählt, bevor Sie sie wieder zusammenführen. Was Sie brauchen, ist nicht, dass die Partitionen "sortierbar" sind, sondern eine Art Garantie dafür, dass Ihre Partitionen sind bereits _sortiert_ vor dem Zusammenführen. Das ist eine ganz andere Art von Problem, und keines, bei dessen Lösung die Template-Syntax offensichtlich hilft; Natürlich möchten Sie einige ziemlich rigorose Unit-Tests, um die Zuverlässigkeit Ihres/Ihrer Merge-Sort-Algorithmus(s) zu garantieren, aber Sie möchten sicherlich keine _exportierte_ API offenlegen, die den Entwickler mit dieser Art von Zeug belastet.

Sie sprechen einen interessanten Punkt darüber an, dass Go keine gute Möglichkeit hat, ohne Reflektion, interface{} usw. zu überprüfen, ob zwei Werte vom gleichen Typ sind Im Falle von Allzweckcontainern (z. B. einer kreisförmigen verknüpften Liste), da die beim Umhüllen der API für die Typsicherheit erforderliche Boilerplate absolut trivial ist:

type MyStack struct { stack Stack }
func (s *MyStack) Push(v MyType) error { return s.stack.Push(v) }
func (s *MyStack) Pop() (MyType, error) {
  v, err := s.stack.Pop()
  var m MyType
  if v != nil {
    if m, ok := v.(MyType); ok { return m, err; }
    panic("this code should be unreachable from the exported API")
  }
  return nil, err
}

Ich kann mir kaum vorstellen, warum diese Boilerplate ein Problem darstellen würde, aber wenn dies der Fall ist, könnte eine (Text-/) Vorlage eine vernünftige Alternative sein. Sie könnten Typen, für die Sie Stacks definieren möchten, mit einem //go:generate stackify MyType github.com/me/myproject/mytype -Kommentar kommentieren und go generate die Boilerplate für Sie erstellen lassen. Solange cmd/stackify/stackify_test.go es mit mindestens einer Struktur und mindestens einem eingebauten Typ ausprobiert und es kompiliert und durchläuft, sehe ich nicht, warum dies ein Problem sein sollte - und es ist wahrscheinlich ziemlich nah dran zu dem, was jeder Compiler "unter der Haube" getan hätte, wenn Sie eine Vorlage definiert hätten. Der einzige Unterschied besteht darin, dass die Fehler hilfreicher sind, weil sie weniger dicht sind.

(Es kann auch Fälle geben, in denen wir ein generisches _etwas_ wollen, das sich mehr darum kümmert, dass zwei Dinge vom gleichen Typ sind, als sich um ihr Verhalten zu kümmern, die nicht in die Kategorie "Container von Sachen" fallen. Das wäre sehr interessant, aber Das Hinzufügen einer generischen Vorlagenkonstruktionssyntax zur Sprache ist möglicherweise immer noch nicht die einzig mögliche Lösung.)

Unter der Annahme, dass die Boilerplate _kein_ Problem darstellt, bin ich daran interessiert, das Problem anzugehen, einen rot/schwarzen Baum zu erstellen, der für Anrufer so einfach zu verwenden ist wie Pakete wie sort oder encoding/json . Ich werde sicherlich scheitern, weil ... nun, ich bin einfach nicht so schlau. Aber ich bin gespannt, wie nah ich ihm kommen könnte.

Bearbeiten: Die Anfänge eines Beispiels sind hier zu sehen, obwohl es bei weitem nicht vollständig ist (das Beste, was ich in ein paar Stunden zusammenwerfen könnte). Natürlich gibt es auch andere Versuche ähnlicher Datenstrukturen.

@jesse-amano

Wenn ja, dann kann dieses Problem als bereits > gelöst geschlossen werden, denn die ursprüngliche Aussage lautete:

Es ist nicht nur so, dass Schnittstellen eine Form von Generika sind, sondern dass die Verbesserung des Schnittstellenansatzes uns bei Generika ganz weit bringen kann. Beispielsweise würden Multiparameter-Schnittstellen (wo Sie mehr als einen "Empfänger" haben können) Beziehungen zu Typen zulassen. Schnittstellen Operatoren wie Addition und Dereferenzierung außer Kraft setzen zu lassen, würde jede andere Form der Beschränkung von Typen überflüssig machen. Schnittstellen _können_ alle Typbeschränkungen sein, die Sie benötigen, wenn sie mit einem Verständnis des Endpunkts vollständig allgemeiner Generika entworfen werden.

Schnittstellen ähneln semantisch den Typklassen von Haskell und den Merkmalen von Rust, die diese generischen Probleme _tun_ lösen. Typklassen und Traits lösen dieselben generischen Probleme wie C++-Templates, aber auf typsichere Weise (aber vielleicht nicht alle Verwendungen der Metaprogrammierung, was ich für eine gute Sache halte).

Ich kann mir kaum vorstellen, warum diese Boilerplate ein Problem darstellen würde, aber wenn dies der Fall ist, könnte eine (Text-/) Vorlage eine vernünftige Alternative sein.

Ich persönlich habe kein Problem mit so vielen Boilerplates, aber ich verstehe den Wunsch, überhaupt keine Boilerplates zu haben, als Programmierer ist es langweilig und repetitiv, und es ist genau die Art von Aufgabe, die wir beim Schreiben von Programmen vermeiden wollen. Also noch einmal, ich persönlich denke, dass das Schreiben einer Implementierung für eine 'Stack'-Schnittstelle/Typ-Klasse genau der _richtige_ Weg ist, Ihren Datentyp 'stapelbar' zu machen.

Es gibt zwei Einschränkungen bei Go, die eine weitere generische Programmierung vereiteln. Das 'Typ'-Äquivalenzproblem, zum Beispiel das Definieren mathematischer Funktionen, sodass das Ergebnis und alle Argumente gleich sein müssen. Wir könnten uns vorstellen:

mul<T>(x, y T) T requires Addable(T) {
    r := 0
    for i := 0; i < y; ++i  {
        r = r + x
    }
    return r
}

Um die Einschränkungen für '+' zu erfüllen, müssen wir sicherstellen, dass x und y numerisch sind, aber auch beide den gleichen zugrunde liegenden Typ haben.

Die andere ist die Begrenzung der Schnittstellen auf nur einen einzigen „Empfänger“-Typ. Diese Einschränkung bedeutet, dass Sie den obigen Boilerplate nicht nur einmal eingeben müssen (was ich für vernünftig halte), sondern für jeden anderen Typ, den Sie in MyStack einfügen möchten. Wir wollen den enthaltenen Typ als Teil der Schnittstelle deklarieren:

type Stack<T> interface {...}

Dies würde unter anderem ermöglichen, eine Implementierung zu deklarieren, die parametrisch in T ist, sodass wir beliebige T über die Stack-Schnittstelle in MyStack einfügen können, solange alle Verwendungen von Push and Pop auf derselben Instanz von MyStack arbeitet mit demselben „Wert“-Typ.

Mit diesen beiden Änderungen sollten wir in der Lage sein, einen generischen rot/schwarzen Baum zu erstellen. Es sollte ohne sie möglich sein, aber wie beim Stack müssen Sie für jeden Typ, den Sie in den rot/schwarzen Baum einfügen möchten, eine neue Instanz der Schnittstelle deklarieren.

Aus meiner Sicht sind die beiden obigen Erweiterungen für Schnittstellen alles, was Go benötigt, um "Generika" vollständig zu unterstützen.

@jesse-amano
Wenn wir uns das Rot/Schwarz-Baum-Beispiel ansehen, wollen wir allgemein die Definition einer „Karte“. Der Rot/Schwarz-Baum ist nur eine mögliche Implementierung. Als solches könnten wir eine Schnittstelle wie diese erwarten:

type Map<Key, Value> interface {
   put(x Key, y Value) 
   get(x Key) Value
}

Dann könnte der Rot/Schwarz-Baum als Implementierung bereitgestellt werden. Idealerweise möchten wir Code schreiben, der nicht von der Implementierung abhängt, sodass Sie eine Hash-Tabelle, einen Rot-Schwarz-Baum oder einen BTree bereitstellen könnten. Wir würden dann unseren Code schreiben:

f<K, V, T>(index T) T requires Map<K, V> {
   ...
}

Was auch immer f ist, es kann unabhängig von der Implementierung der Map funktionieren, f kann eine Bibliotheksfunktion sein, die von jemand anderem geschrieben wurde, der nicht wissen muss, ob meine Anwendung ein rotes/ Black Tree oder eine Hash-Map.

So wie es jetzt ist, müssten wir eine bestimmte Karte wie folgt definieren:

type MapIntString interface {
   put(x Int, y String)
   get(x Int) String
}

Was nicht so schlimm ist, aber es bedeutet, dass die 'Bibliotheks'-Funktion f für jede mögliche Kombination von Schlüssel- und Werttypen geschrieben werden muss, wenn wir sie in einer Anwendung verwenden können, in der wir es nicht tun. Wir kennen die Typen der Schlüssel und Werte nicht, wenn wir die Bibliothek schreiben.

Während ich dem letzten Kommentar von @keean zustimme, besteht die Schwierigkeit darin, einen rot/schwarzen Baum in Go zu schreiben , der eine bekannte Schnittstelle implementiert, wie zum Beispiel die gerade vorgeschlagene.

Ohne Generika ist bekannt, dass man zur Implementierung typunabhängiger Container interface{} und/oder Reflektion verwenden muss – leider sind beide Ansätze langsam und fehleranfällig.

@Keean

Es ist nicht nur so, dass Schnittstellen eine Form von Generika sind, sondern dass die Verbesserung des Schnittstellenansatzes uns bei Generika ganz weit bringen kann.

Ich sehe keinen der bisherigen Vorschläge zu diesem Thema als Verbesserung an. Es scheint ziemlich unumstritten zu sagen, dass sie alle in irgendeiner Weise fehlerhaft sind. Ich glaube, dass diese Mängel jeden Nutzen bei weitem aufwiegen, und viele der _behaupteten_ Vorteile werden tatsächlich bereits von bestehenden Funktionen unterstützt. Mein Glaube basiert auf praktischer Erfahrung, nicht auf Spekulation, aber es ist immer noch anekdotisch.

Ich persönlich habe kein Problem mit so vielen Boilerplates, aber ich verstehe den Wunsch, überhaupt keine Boilerplates zu haben, als Programmierer ist es langweilig und repetitiv, und es ist genau die Art von Aufgabe, die wir beim Schreiben von Programmen vermeiden wollen.

Auch hiermit bin ich nicht einverstanden. Als bezahlter Fachmann ist es mein Ziel, die Zeit-/Aufwandskosten _für mich und andere_ zu reduzieren und gleichzeitig die Gewinne meines Arbeitgebers zu steigern, wie auch immer diese gemessen werden mögen. Eine Aufgabe, die „langweilig“ ist, ist nur dann schlecht, wenn sie auch zeitaufwändig ist; es kann nicht schwierig sein, sonst wäre es nicht langweilig. Wenn es im Vorfeld nur ein wenig zeitaufwändig ist, aber zukünftige zeitaufwändige Aktivitäten eliminiert und/oder das Produkt früher veröffentlicht wird, dann lohnt es sich trotzdem.

Dann könnte der Rot/Schwarz-Baum als Implementierung bereitgestellt werden.

Ich glaube, ich habe in den letzten Tagen anständige Fortschritte bei der Implementierung eines rot/schwarzen Baums gemacht (es ist unvollendet; es fehlt sogar eine Readme-Datei), aber ich mache mir Sorgen, dass ich es bereits versäumt habe, meinen Standpunkt zu veranschaulichen, wenn es nicht reichlich ist klar, dass mein Ziel nicht darin besteht, auf eine Schnittstelle hinzuarbeiten, sondern eher auf eine Implementierung. Ich schreibe einen Rot/Schwarz-Baum, und natürlich möchte ich, dass er _nützlich_ ist, aber es ist mir egal, für welche _spezifischen_ Dinge andere Entwickler ihn verwenden möchten.

Ich weiß, dass die minimale Schnittstelle, die von einer Rot/Schwarz-Baumbibliothek benötigt wird, eine ist, bei der eine "schwache" Ordnung für ihre Elemente existiert, also brauche ich etwas _wie_ eine Funktion namens Less(v interface{}) bool , aber wenn der Aufrufer eine Methode hat, die tut etwas Ähnliches, heißt aber nicht Less(v interface{}) bool , es liegt an ihnen, die Boilerplate-Wrapper/Shims zu schreiben, damit es funktioniert.

Wenn Sie auf Elemente zugreifen, die im rot/schwarzen Baum enthalten sind, erhalten Sie interface{} , aber wenn Sie bereit sind, meiner Garantie zu vertrauen, dass die bereitgestellte Bibliothek ein rot/schwarzer Baum ist, verstehe ich nicht, warum Sie das tun sollten. Vertrauen Sie nicht darauf, dass die Typen von Elementen, die Sie einfügen, genau die Typen von Elementen sind, die Sie herausbekommen. Wenn Sie diesen beiden Garantien _vertrauen_, dann ist die Bibliothek überhaupt nicht fehleranfällig. Schreiben (oder fügen Sie) einfach etwa ein Dutzend Codezeilen ein, um die Typzusicherungen abzudecken.

Jetzt haben Sie eine vollkommen sichere Bibliothek (wieder unter der Annahme, dass Sie nicht mehr als das Maß an Vertrauen geben müssen, das Sie bereit sein müssten, um die Bibliothek überhaupt herunterzuladen), das sogar die genauen Funktionsnamen hat, die Sie wollen. Das ist wichtig. In einem Ökosystem im Java-Stil, in dem Bibliotheksautoren sich nach hinten beugen, um gegen eine _exakte_ Schnittstellendefinition zu programmieren (sie _müssen_ fast, weil die Sprache dies durch class MyClassImpl extends AbstractMyClass implements IMyClass -Syntax erzwingt) und es eine Menge zusätzlicher Bürokratie gibt, Sie müssen sich alle Mühe geben, eine Fassade für die Drittanbieterbibliothek zu erstellen, die in die Codierungsstandards Ihrer Organisation passt (was die gleiche Menge an Boilerplate ist, wenn nicht mehr), oder dies als "Ausnahme" zulassen die Codierungsstandards Ihrer Organisation (und schließlich hat Ihre Organisation genauso viele Ausnahmen in ihren Standards wie in ihren Codebasen), oder geben Sie die Verwendung einer vollkommen guten Bibliothek auf (angenommen, um der Argumentation willen, dass die Bibliothek tatsächlich gut ist).

Idealerweise möchten wir Code schreiben, der nicht von der Implementierung abhängt, sodass Sie eine Hash-Tabelle, einen Rot-Schwarz-Baum oder einen BTree bereitstellen könnten.

Ich stimme diesem Ideal zu, aber ich denke, Go erfüllt es bereits. Mit einer Schnittstelle wie:

type MyStorage interface {
  Get(KeyType) (ValueType, error)
  Put(KeyType, ValueType) error
}

Das einzige, was fehlt, ist die Möglichkeit, zu parametrisieren, was KeyType und ValueType sind, und ich bin nicht davon überzeugt, dass dies besonders wichtig ist.

Als (hypothetischer) Betreuer einer Red/Black-Tree-Bibliothek ist es mir egal, welche Typen Sie sind. Ich werde einfach interface{} für alle meine Kernfunktionen verwenden, die "einige Daten" verarbeiten, und _vielleicht_ einige exportierte Beispielfunktionen bereitstellen, mit denen Sie sie einfacher mit gängigen Typen wie string und int verwenden können

Als (hypothetischer) Aufrufer einer Rot/Schwarz-Baum-Bibliothek möchte ich sie wahrscheinlich nur für eine schnelle Speicher- und Suchzeit. Es ist mir egal, dass es ein rot/schwarzer Baum ist. Es ist mir wichtig, dass ich Get Dinge daraus und Put Dinge darin bekomme, und – was wichtig ist – es ist mir wichtig, was diese Dinge sind. Wenn die Bibliothek keine Funktionen namens Get und Put anbietet oder nicht perfekt mit den von mir definierten Typen interagieren kann, macht mir das nichts aus, solange es für mich einfach ist um die Methoden Get und Put selbst zu schreiben und meinen eigenen Typ zu erstellen, der die Schnittstelle erfüllt, die die Bibliothek benötigt, wenn ich schon dabei bin. Wenn es nicht einfach ist, finde ich normalerweise, dass es die Schuld des Autors der Bibliothek ist, nicht die Sprache, aber es ist wieder möglich, dass es Gegenbeispiele gibt, die mir einfach nicht bekannt sind.

Übrigens könnte der Code viel verworrener werden, wenn es nicht so wäre. Wie Sie sagen, gibt es viele mögliche Implementierungen eines Schlüssel/Wert-Speichers. Das Herumreichen eines abstrakten Schlüssel/Wert-Speicher-"Konzepts" verbirgt die Komplexität, wie die Schlüssel/Wert-Speicherung bewerkstelligt wird, und ein Entwickler in meinem Team könnte das falsche für seine Aufgabe auswählen (einschließlich einer zukünftigen Version von mir, deren Kenntnis des Schlüssels /Wert Speicherimplementierung hat ausgelagerten Speicher!). Die Anwendung oder ihre Komponententests können trotz unserer Bemühungen bei der Codeüberprüfung subtilen implementierungsabhängigen Code enthalten, der nicht mehr zuverlässig funktioniert, wenn einige Schlüssel/Wert-Speicher von einer Verbindung zu einer DB abhängen und andere nicht. Es ist ärgerlich, wenn der Fehlerbericht mit einem großen Stack-Trace kommt und die einzige Zeile im Stack-Trace, die auf etwas in der _echten_ Codebasis verweist, auf eine Zeile zeigt, die einen Schnittstellenwert verwendet, weil die Implementierung dieser Schnittstelle generierter Code ist (der Sie können es nur zur Laufzeit sehen) anstelle einer gewöhnlichen Struktur, mit Methoden, die lesbare Fehlerwerte zurückgeben.

@jesse-amano
Ich stimme Ihnen zu, und ich mag die „Go“-Methode, bei der der „Benutzer“-Code eine Schnittstelle deklariert, die die Funktionsweise abstrahiert, und dann schreiben Sie die Implementierung dieser Schnittstelle für die Bibliothek/Abhängigkeit. Dies ist anders als die Art und Weise, wie die meisten anderen Sprachen über Schnittstellen denken. aber sobald Sie es bekommen, ist es sehr mächtig.

Ich würde immer noch gerne die folgenden Dinge in einer generischen Sprache sehen:

  • parametrische Typen wie: RBTree<Int, String> , da dies die Typsicherheit von Benutzersammlungen erzwingen würde.
  • Typvariablen wie: f<T>(x, y T) T , da dies notwendig ist, um Familien verwandter Funktionen wie Addition, Subtraktion usw. zu definieren, bei denen die Funktion polymorph ist, aber wir verlangen, dass alle Argumente vom gleichen zugrunde liegenden Typ sind.
  • Typbeschränkungen, wie: f<T: Addable>(x, y T) T , das Schnittstellen auf Typvariablen anwendet, denn sobald wir Typvariablen eingeführt haben, brauchen wir eine Möglichkeit, diese Typvariablen einzuschränken, anstatt Addable als Typ zu behandeln. Wenn wir Addable als Typ betrachten und f(x, y Addable) Addable schreiben, haben wir keine Möglichkeit zu sagen, ob die ursprünglichen zugrunde liegenden Typen von x und y die gleichen sind wie einander oder den zurückgegebenen Typ.
  • Schnittstellen mit mehreren Parametern, wie: type<K, V> Map<K, V> interface {...} , die wie merge<K, V, T: Map<K, V>>(x, y T) T verwendet werden könnten, die es uns ermöglichen, Schnittstellen zu deklarieren, die nicht nur durch den Containertyp, sondern in diesem Fall auch durch den Schlüssel und den Wert parametrisiert werden Arten der Karte.

Ich denke, jedes davon würde die Abstraktionskraft der Sprache erhöhen.

Jeglicher Fortschritt oder Zeitplan diesbezüglich?

@leaxoy Auf der GopherCon ist ein Vortrag über „Generika in Go“ von @ianlancetaylor geplant . Ich würde erwarten, in diesem Vortrag mehr über den aktuellen Stand der Dinge zu erfahren.

@griesemer Danke für den Link.

@keean Ich würde gerne auch die Where-Klausel von Rust hier sehen, die möglicherweise eine Verbesserung Ihres type constraints -Vorschlags darstellt. Es ermöglicht die Verwendung des Typsystems, um Verhaltensweisen wie "Starten einer Transaktion vor der Abfrage" einzuschränken, die ohne Laufzeitreflexion typgeprüft werden sollen. Sehen Sie sich dazu dieses Video an: https://www.youtube.com/watch?v=jSpio0x7024

@jadbox Entschuldigung, wenn meine Erklärung nicht klar war, aber die Klausel "Wo" ist fast genau das, was ich vorgeschlagen habe. Die Dinge nach „where“ in rust sind Typbeschränkungen, aber ich glaube, ich habe stattdessen das Schlüsselwort „requires“ in einem früheren Beitrag verwendet. Dieses Zeug wurde vor mindestens einem Jahrzehnt in Haskell gemacht, außer dass Haskell den Operator „=>“ in Typsignaturen verwendet, um Typbeschränkungen anzuzeigen, aber es ist derselbe zugrunde liegende Mechanismus.

Ich habe das oben in meinem zusammenfassenden Beitrag weggelassen, weil ich die Dinge einfach halten wollte, aber ich hätte gerne so etwas:

merge<K, V, T>(x, y T) T requires T: Map<K, V>

Aber es fügt nicht wirklich etwas zu dem hinzu, was Sie tun können, abgesehen von einer Syntax, die für lange Constraint-Sets besser lesbar sein kann. Sie können alles, was Sie können, mit der 'where'-Klausel darstellen, indem Sie die Einschränkung wie folgt in die ursprüngliche Deklaration einfügen, nachdem sie eine Variable eingegeben haben:

merge<K, V, T: Map<K, V>>(x, y T) T

Vorausgesetzt, Sie können auf die Typvariablen verweisen, bevor sie deklariert werden, können Sie beliebige Einschränkungen dort einfügen, und Sie würden eine durch Kommas getrennte Liste verwenden, um mehrere Einschränkungen auf dieselbe Typvariable anzuwenden.

Soweit mir bekannt ist, besteht der einzige Vorteil einer 'where'/'requires'-Klausel darin, dass alle Typvariablen bereits im Voraus deklariert sind, was es für den Parser und die Art-Inferenz einfacher machen kann.

Ist dies immer noch der richtige Thread für Feedback/Diskussionen zum aktuellen/neuesten funktionierenden Go 2 Generics-Vorschlag , der kürzlich angekündigt wurde?

Kurz gesagt, ich mag die Richtung, in die der Vorschlag im Allgemeinen geht, und den Vertragsmechanismus im Besonderen. Aber ich mache mir Sorgen um die scheinbar zielstrebige Annahme, dass generische Parameter zur Kompilierzeit (immer) Typparameter sein müssen. Ich habe hier ein Feedback zu diesem Thema geschrieben:

Sind nur Typparameter generisch genug für Go 2-Generika?

Natürlich sind Kommentare hier in Ordnung, aber im Allgemeinen denke ich nicht, dass GitHub-Probleme ein gutes Diskussionsformat sind, da sie keinerlei Threading vorsehen. Ich denke, die Mailinglisten sind besser.

Ich glaube, es ist noch nicht klar, wie oft Leute Funktionen mit konstanten Werten parametrisieren wollen. Der naheliegendste Fall wären Array-Dimensionen – aber Sie können das bereits tun, indem Sie den gewünschten Array-Typ als Typargument übergeben. Abgesehen von diesem Fall, was gewinnen wir wirklich, wenn wir eine Konstante als Argument zur Kompilierzeit und nicht als Argument zur Laufzeit übergeben?

Go bietet bereits viele verschiedene und großartige Möglichkeiten, um Probleme zu lösen, und wir sollten niemals etwas Neues hinzufügen, es sei denn, es behebt ein wirklich großes Problem und einen Mangel, was dies eindeutig nicht tut, und selbst unter solchen Umständen ist die zusätzliche Komplexität, die folgt, sehr hoher Preis zu zahlen.

Go ist genau wegen seiner Art einzigartig. Wenn es nicht kaputt ist, dann versuchen Sie bitte nicht, es zu reparieren!

Leute, die mit der Art und Weise, wie Go entworfen wurde, unzufrieden sind, sollten eine der vielen anderen Sprachen verwenden, die bereits diese zusätzliche und lästige Komplexität besitzen.

Go ist genau wegen seiner Art einzigartig. Wenn es nicht kaputt ist, dann versuchen Sie bitte nicht, es zu reparieren!

Es ist kaputt, also sollte es repariert werden.

Es ist kaputt, also sollte es repariert werden.

Es funktioniert vielleicht nicht so, wie Sie denken, dass es sollte – aber eine Sprache kann es nie. Es ist auf keinen Fall kaputt. Es ist immer die beste Option, die verfügbaren Informationen und Debatten zu berücksichtigen und sich dann Zeit zu nehmen, um eine fundierte und vernünftige Entscheidung zu treffen. Viele andere Sprachen haben meiner Meinung nach gelitten, weil immer mehr Funktionen hinzugefügt wurden, um immer mehr potenzielle Probleme zu lösen. Denken Sie daran, dass „Nein“ vorübergehend ist, „Ja“ für immer.

Nachdem ich an früheren Mega-Ausgaben teilgenommen habe, darf ich vorschlagen, dass ein Kanal auf Gopher Slack für diejenigen geöffnet wird, die dies diskutieren möchten, die Ausgabe vorübergehend gesperrt wird und dann Zeiten gepostet werden, wann die Ausgabe für alle, die sie konsolidieren möchten, wieder freigegeben wird Diskussion von Slack? Github Issues funktionieren nicht mehr als Forum, sobald der gefürchtete Link „478 versteckte Elemente Mehr laden…“ erscheint.

Darf ich vorschlagen, dass ein Kanal auf Gopher Slack für diejenigen geöffnet wird, die dies diskutieren möchten
Die Mailinglisten sind besser, weil sie ein durchsuchbares Archiv bieten. Zu diesem Thema kann noch eine Zusammenfassung gepostet werden.

Nachdem ich an vergangenen Mega-Themen teilgenommen habe, darf ich vorschlagen, dass ein Kanal auf Gopher Slack für diejenigen eröffnet wird, die dies diskutieren möchten

Bitte verlegen Sie die Diskussion nicht komplett auf geschlossene Plattformen. Wenn irgendwo, ist Golang-Nuss für alle verfügbar (ish? Ich weiß nicht, ob das auch ohne Google-Konto funktioniert, aber zumindest ist es eine Standard-Kommunikationsmethode, die jeder hat oder bekommen kann) und es sollte dorthin verschoben werden . GitHub ist schlimm genug, aber ich akzeptiere widerwillig, dass wir für die Kommunikation daran festhalten, nicht jeder kann ein Slack-Konto bekommen oder seine schrecklichen Clients verwenden.

Nicht jeder kann ein Slack-Konto bekommen oder seine schrecklichen Clients verwenden

Was heißt hier „können“? Gibt es wirkliche Einschränkungen für Slack, von denen ich nichts weiß, oder verwenden die Leute es einfach nicht gerne? Letzteres ist in Ordnung, denke ich, aber einige Leute boykottieren Github auch, weil sie Microsoft nicht mögen, also verliert man einige Leute, gewinnt aber andere.

Nicht jeder kann ein Slack-Konto bekommen oder seine schrecklichen Clients verwenden

Was heißt hier „können“? Gibt es wirkliche Einschränkungen für Slack, von denen ich nichts weiß, oder verwenden die Leute es einfach nicht gerne? Letzteres ist in Ordnung, denke ich, aber einige Leute boykottieren Github auch, weil sie Microsoft nicht mögen, also verliert man einige Leute, gewinnt aber andere.

Slack ist ein US-Unternehmen und wird als solches jede von den USA auferlegte Außenpolitik befolgen.

Github hat das gleiche Problem und war gerade in den Nachrichten, weil er Iraner ohne Vorwarnung rausgeschmissen hat. Es ist bedauerlich, aber wenn wir nicht Tor oder IPFS oder ähnliches verwenden, müssen wir US-amerikanisches/europäisches Recht für jedes praktische Diskussionsforum respektieren.

Github hat das gleiche Problem und war gerade in den Nachrichten, weil er Iraner ohne Vorwarnung rausgeschmissen hat. Es ist bedauerlich, aber wenn wir nicht Tor oder IPFS oder ähnliches verwenden, müssen wir US-amerikanisches/europäisches Recht für jedes praktische Diskussionsforum respektieren.

Ja, wir stecken bei GitHub und Google Groups fest. Fügen wir der Liste keine problematischeren Dienste hinzu. Außerdem ist Chat einfach kein gutes Archiv; Es ist schon schwer genug, diese Diskussionen zu durchforsten, wenn sie schön aufgefädelt und auf Golang-Nüssen sind (wo sie direkt in Ihren Posteingang gelangen). Slack bedeutet, wenn Sie sich nicht in derselben Zeitzone wie alle anderen befinden, müssen Sie sich durch Massen von Chat-Archiven, einmalige Non-Sequiter usw. wühlen. Mailinglisten bedeuten, dass Sie es zumindest einigermaßen in Threads organisiert haben, und die Leute neigen dazu, zu nehmen mehr Zeit in ihren Antworten, damit Sie nicht zufällig Tonnen von zufälligen einmaligen Kommentaren hinterlassen. Außerdem habe ich einfach kein Slack-Konto und ihre dummen Clients funktionieren auf keinem der Computer, die ich verwende. Mutt hingegen (oder Ihr bevorzugter E-Mail-Client, yay Standards) funktioniert überall.

Bitte bewahren Sie diese Ausgabe über Generika auf. Dass der GitHub Issue Tracker nicht ideal für groß angelegte Diskussionen wie Generika ist, ist diskussionswürdig, aber nicht zu diesem Thema. Ich habe einige Kommentare oben als "Off-Topic" markiert.

Was die Einzigartigkeit von Go betrifft: Go hat einige nette Features, aber es ist nicht so einzigartig, wie manche zu glauben scheinen. Als zwei Beispiele haben CLU und Modula-3 ähnliche Ziele und ähnliche Auszahlungen, und beide unterstützen Generika in irgendeiner Form (seit ca. 1975 im Fall von CLU!). Sie haben derzeit keine industrielle Unterstützung, aber FWIW, es ist möglich, eine zu bekommen Compiler, der für beide funktioniert.

ein paar Anfragen zur Syntax, ist das Schlüsselwort type in den Typparametern erforderlich? und wäre es sinnvoller, <> für die Typparameter wie andere Sprachen zu übernehmen? Dies könnte die Dinge lesbarer und vertrauter machen ...

Obwohl ich nicht dagegen bin, wie es in dem Vorschlag steht, stelle ich dies nur zur Prüfung

anstatt:

type Vector(type Element) []Element
var v Vector(int)
func (v *Vector(Element)) Push(x Element) { *v = append(*v, x) }
type VectorInt = Vector(int)

wir könnten haben

type Vector<Element> []Element
var v Vector<int>
func (v *Vector<Element>) Push(x Element) { *v = append(*v, x) }
type VectorInt = Vector<int>

Die Syntax <> wird im Entwurf erwähnt, @jnericks (Ihr Benutzername ist perfekt für diese Diskussion...). Das Hauptargument dagegen ist, dass es die Komplexität des Parsers massiv erhöht. Allgemeiner gesagt macht es Go zu einer wesentlich schwierigeren Sprache, die wenig Nutzen bringt. Die meisten Leute sind sich einig, dass es die Lesbarkeit verbessert, aber es gibt Meinungsverschiedenheiten darüber, ob es den Kompromiss wert ist oder nicht. Ich persönlich glaube nicht, dass es so ist.

Die Verwendung des Schlüsselworts type ist zur Unterscheidung erforderlich. Andernfalls ist es schwierig, den Unterschied zwischen func Example(T)(arg int) {} und func Example(arg int) (int) {} zu erkennen.

Ich habe mir den neuesten Vorschlag über die Go-Generika durchgelesen. alle treffen meinen geschmack bis auf die grammatik der vertragserklärung.

Wie wir wissen, deklarieren wir in go struct oder interface immer so:

type MyStruct struct {
        a int
        s string
}

type MyInterface inteface {
    Method1() err
    Method2() string
}

aber die Vertragserklärung im letzten Vorschlag ist wie folgt:

contract Ordered(T) {
    T int, int8
}

contract G(Node, Edge) {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

Meiner Meinung nach widerspricht die Vertragsgrammatik der Form dem traditionellen Ansatz. wie wäre es mit der Grammatik wie folgt:

type Ordered(T) contract {
    T int, int8
}

if there is only one type parameter, the declaration above can be also wrote like this:

type Ordered contract {
    int , int8
}


if there are more than one type parameter, we have to use named parameter:

type G(Node, Edge) contract {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

Jetzt entspricht die Vertragsform der traditionellen. Wir können einen Vertrag in einem Typblock mit struct, interface deklarieren:

type (
        Sequence contract {
                string, []byte
        }

    Stringer(T) contract {
        T String() string
    }

    Stringer contract { // equivalent with the above Stringer(T), single type parameter could be omitted
        String() string
    }

        MyStruct struct {
                a int
                b string
        }

    G(Node, Edge) contract {
        Node Edges() []Edge
        Edge Nodes() (from Node, to Node)
    }
)

Der "Vertrag" wird also zum Schlüsselwort der gleichen Ebene wie struct, interface. Der Unterschied besteht darin, dass der Vertrag verwendet wird, um den Metatyp für Typ zu deklarieren.

@bigwhite Wir diskutieren immer noch über diese Notation. Für die im Entwurfsentwurf vorgeschlagene Notation spricht, dass ein Vertrag kein Typ ist (zB kann man keine Variable eines Vertragstyps deklarieren), ein Vertrag also eine neue Art von Entität ist, ebenso wie eine Konstante , Funktion, Variable oder Typ. Das Argument für Ihren Vorschlag ist, dass ein Vertrag einfach ein "Typtyp" (oder ein Metatyp) ist und daher einer konsistenten Notation folgen sollte. Ein weiteres Argument für Ihren Vorschlag ist, dass er die Verwendung "anonymer" Vertragsliterale erlauben würde, ohne dass sie explizit deklariert werden müssen. Kurz gesagt, IMHO ist dies noch nicht geklärt. Aber es ist auch einfach, später zu wechseln.

FWIW, CL 187317 unterstützt derzeit beide Notationen (obwohl der Vertragsparameter mit dem Vertrag geschrieben werden muss), z. B.:

type C contract(X) { ... }

und

contract C (X) { ... }

werden intern akzeptiert und gleich vertreten. Der konsequentere Ansatz wäre:

type C(type X) contract { ... }

Ein Vertrag ist kein Typ. Es ist nicht einmal ein Meta-Typ, da es nur von ihm typisiert wird
beschäftigt sich mit sind seine Parameter. Es gibt keinen separaten Empfängertyp
deren Metatyp der Vertrag betrachtet werden könnte.

Go hat auch Funktionsdeklarationen:

func Name(args) { body }

die die vorgeschlagene Vertragssyntax direkter widerspiegelt.

Wie auch immer, diese Art von Syntax-Diskussionen scheint weit unten auf der Prioritätenliste zu stehen
dieser Punkt. Es ist wichtiger, sich die Semantik des Entwurfs anzusehen und
wie sie sich auf den Code auswirken, welche Art von Code kann darauf basierend geschrieben werden
Semantik und welcher Code dies nicht kann.

Bearbeiten: In Bezug auf Inline-Verträge hat Go Funktionsliterale. Ich sehe keinen Grund, warum es keine Vertragsliterale geben kann. Es gäbe nur eine begrenztere Anzahl von Orten, an denen sie erscheinen könnten, da es sich nicht um Typen oder Werte handelt.

@stevenblenkinsop Ich würde nicht so weit gehen, sachlich zu sagen, dass ein Vertrag kein Typ (oder Metatyp) ist. Ich denke, es gibt sehr vernünftige Argumente für beide Standpunkte. Beispielsweise dient ein einzelner Parametervertrag, der nur Methoden angibt, im Wesentlichen als „Obergrenze“ für einen Typparameter: Jedes gültige Typargument muss diese Methoden implementieren. Dafür verwenden wir normalerweise Schnittstellen. Es kann sehr sinnvoll sein, in diesen Fällen anstelle eines Vertrags Schnittstellen zuzulassen, a) weil diese Fälle häufig vorkommen; und b) weil Vertragserfüllung in diesem Fall lediglich die Erfüllung der als Vertrag ausbuchstabierten Schnittstelle bedeutet. Das heißt, ein solcher Vertrag verhält sich sehr ähnlich wie ein Typ, mit dem ein anderer Typ "verglichen" wird.

@griesemer Verträge als Typen zu betrachten, kann zu Problemen mit dem Russel-Paradoxon führen (wie beim Typ aller Typen, die keine „Mitglieder“ von sich selbst sind). Ich denke, sie sind besser als "Einschränkungen für Typen" zu betrachten. Wenn wir ein Typensystem als eine Form von „Logik“ betrachten, können wir dies in Prolog prototypisieren. Typvariablen werden zu logischen Variablen, Typen werden zu Atomen und Verträge/Einschränkungen können durch Constraint Logic Programming gelöst werden. Es ist alles sehr ordentlich und nicht paradox. In Bezug auf die Syntax könnten wir einen Vertrag als eine Funktion für Typen betrachten, die einen booleschen Wert zurückgibt.

@keean Jede Schnittstelle dient bereits als "Einschränkung für Typen", dennoch handelt es sich um Typen. Typtheoretische Leute betrachten Beschränkungen von Typen sehr formal als Typen. Wie ich oben erwähnt habe, gibt es vernünftige Argumente, die für beide Standpunkte vorgebracht werden können. Hier gibt es keine "logischen Paradoxien" - tatsächlich modelliert der aktuelle Work-in-Progress-Prototyp intern einen Vertrag als Typ, da dies die Dinge im Moment vereinfacht.

@griesemer- Schnittstellen in Go sind "Untertypen", keine Einschränkungen für Typen. Ich finde jedoch, dass sowohl Verträge als auch Schnittstellen erforderlich sind, einen Nachteil für das Design von Go, jedoch kann es zu spät sein, Schnittstellen in Typbeschränkungen anstatt in Untertypen umzuwandeln. Ich habe oben argumentiert, dass Go-Schnittstellen nicht unbedingt Subtypen sein müssen, aber ich sehe nicht viel Unterstützung für diese Idee. Dies würde ermöglichen, dass Schnittstellen und Verträge dasselbe sind – wenn Schnittstellen auch für Betreiber deklariert werden könnten.

Hier gibt es Paradoxe, also gehen Sie vorsichtig vor, Girards Paradox ist die häufigste „Kodierung“ von Russels Paradox in die Typentheorie. Die Typentheorie führt das Konzept von Universen ein, um diese Paradoxien zu verhindern, und Sie dürfen nur auf Typen in Universum „U“ aus Universum „U+1“ verweisen. Intern werden diese Typtheorien als Logik höherer Ordnung implementiert (z. B. verwendet Elf Lambda-Prolog). Dies reduziert sich wiederum auf das Lösen von Beschränkungen für die entscheidbare Teilmenge der Logik höherer Ordnung.

Während Sie sie sich also als Typen vorstellen können, müssen Sie eine Reihe von Verwendungsbeschränkungen (syntaktisch oder anderweitig) hinzufügen, die Sie effektiv zu den Beschränkungen von Typen zurückbringen. Ich persönlich finde es einfacher, direkt mit den Einschränkungen zu arbeiten und die beiden weiteren Ebenen der Abstraktion, der Logik höherer Ordnung und der abhängigen Typen zu vermeiden. Diese Abstraktionen tragen nichts zur Ausdruckskraft des Typensystems bei und erfordern weitere Regeln oder Einschränkungen, um Paradoxien zu vermeiden.

In Bezug auf den aktuellen Prototyp, der Constraints als Typen behandelt, besteht die Gefahr, wenn Sie diesen "Constraint-Type" als normalen Typ verwenden und dann einen anderen "Constraint-Type" auf diesem Typ erstellen können. Sie werden Prüfungen benötigen, um Selbstreferenzen (dies ist normalerweise trivial) und gegenseitige Referenzschleifen zu verhindern. Diese Art von Prototyp sollte wirklich in Prolog geschrieben werden, da Sie sich so auf die Implementierungsregeln konzentrieren können. Ich glaube, die Rust-Entwickler haben das vor einiger Zeit endlich erkannt (siehe Chalk).

@griesemer Interessant, Verträge als Typen neu zu modellieren. Ausgehend von meinem eigenen mentalen Modell würde ich Einschränkungen als Metatypen und Verträge als eine Art Struktur auf Typebene betrachten.

type A int
func (a A) Foo() int {
    return int(a)
}

type C contract(T, U) {
    T int
    U int, uint
    U Foo() int
}

var B (int, uint; Foo() int).type = A
var C1 C = C(A, B)

Dies deutet für mich darauf hin, dass die aktuelle Syntax im Typdeklarationsstil für Verträge die korrektere der beiden ist. Ich denke jedoch, dass die im Entwurf dargelegte Syntax immer noch besser ist, da sie nicht die Frage „Wenn es sich um einen Typ handelt, wie sehen seine Werte aus“ ansprechen muss.

@stevenblenkinsop du hast mich verloren, warum übergibst du T an C contract , wenn es nicht verwendet wird, und was versuchen die var Zeilen zu tun?

@griesemer Danke für deine Antwort. Eines der Designprinzipien von Go ist „biete nur eine Möglichkeit, etwas zu tun“. Es ist besser, nur ein Vertragserklärungsformular aufzubewahren. Typ C (Typ X) Vertrag { ... } ist besser.

@Goodwine Ich habe die Typen umbenannt, um sie von den Vertragsparametern zu unterscheiden. Vielleicht hilft das? (int, uint; Foo() int).type soll der Metatyp jedes Typs sein, der einen zugrunde liegenden Typ von int oder uint hat und der Foo() int implementiert. var B soll zeigen, wie man einen Typ als Wert verwendet und ihn einer Variablen zuweist, deren Typ ein Metatyp ist (da ein Metatyp wie ein Typ ist, dessen Werte Typen sind). var C1 soll eine Variable zeigen, deren Typ ein Vertrag ist, und ein Beispiel für etwas zeigen, das einer solchen Variable zugewiesen werden könnte. Im Grunde versucht man, die Frage zu beantworten: "Wenn ein Vertrag ein Typ ist, wie sehen seine Werte aus?". Der Punkt ist zu zeigen, dass dieser Wert selbst kein Typ zu sein scheint.

Ich habe ein Problem mit Verträgen mit mehreren Typen.

Sie können es für den Typ-Parameter-Vertrag hinzufügen oder belassen, beides
type Graph (type Node, Edge) struct { ... }
und
type Graph (type Node, Edge G) struct { ... } sind in Ordnung.

Aber was ist, wenn ich nur einen Vertrag zu einem der beiden Typparameter hinzufügen möchte?

contract G(Node, Edge) {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

VS

contract G(Edge) {
    Edge Nodes() (from Node, to Node)
}

@themez Das steht im Entwurf. Sie können beispielsweise die Syntax (type T, U comparable(T)) verwenden, um nur einen Typparameter einzuschränken.

@stevenblenkinsop Ich verstehe, danke.

@themez Das ist jetzt ein paar Mal aufgetaucht. Ich denke, es gibt einige Verwirrung durch die Tatsache, dass die Verwendung wie ein Typ für eine Variablendefinition aussieht. Das ist es aber wirklich nicht; Ein Vertrag ist eher ein Detail der gesamten Funktion als eine Argumentdefinition. Ich denke, die Annahme ist, dass Sie im Wesentlichen für jede generische Funktion/Typ, die Sie erstellen, einen neuen Vertrag schreiben würden, der möglicherweise aus anderen Verträgen besteht, um bei der Wiederholung zu helfen. Dinge wie das, was @stevenblenkinsop erwähnt hat, sind wirklich dazu da, die Grenzfälle zu erfassen, in denen diese Annahme keinen Sinn ergibt.

Zumindest habe ich diesen Eindruck gewonnen, vor allem dadurch, dass sie „Verträge“ heißen.

@keean Ich denke, wir interpretieren das Wort "Einschränkung" anders; Ich benutze es eher informell. Per Definition von Schnittstellen können I bei einer Schnittstelle I und einer Variablen x vom Typ I nur Werte mit Typen zugewiesen werden, die I x . Somit kann I als „Einschränkung“ für diese Typen angesehen werden (natürlich gibt es immer noch unendlich viele Typen, die diese „Einschränkung“ erfüllen). Ebenso könnte man I als Einschränkung für einen Typparameter P einer generischen Funktion verwenden; nur tatsächliche Typargumente mit Methodensätzen, die I implementieren, wären zulässig. Somit schränkt I auch die Menge möglicher tatsächlicher Argumenttypen ein.

In beiden Fällen besteht der Grund dafür darin, die verfügbaren Operationen (Methoden) innerhalb der Funktion zu beschreiben. Wenn I als Typ eines (Wert-)Parameters verwendet wird, wissen wir, dass dieser Parameter diese Methoden bereitstellt. Wenn die I als "Einschränkung" (anstelle eines Vertrags) verwendet werden, wissen wir, dass alle Werte des so eingeschränkten Typparameters diese Methoden bereitstellen. Es ist offensichtlich ziemlich geradlinig.

Ich hätte gerne ein konkretes Beispiel dafür, warum diese spezielle Idee, Schnittstellen für Einzelparameterverträge zu verwenden, die nur Methoden deklarieren, ohne einige Einschränkungen "zusammenbricht", wie Sie in Ihrem Kommentar angedeutet haben.

Wie wird der Vertragsvorschlag eingeführt? Verwenden Sie den go1.14 -Parameter des go-Moduls? Eine GO114CONTRACTS Umgebungsvariable? Beide? Etwas anderes..?

Entschuldigung, falls dies schon einmal angesprochen wurde, leiten Sie mich gerne dorthin weiter.

Eine Sache, die ich am aktuellen Designentwurf für Generika besonders mag, ist, dass er klares Wasser zwischen contracts und interfaces setzt. Ich halte das für wichtig, weil die beiden Konzepte leicht verwechselt werden, obwohl es drei grundlegende Unterschiede zwischen ihnen gibt:

  1. Contracts beschreiben die Anforderungen an eine _Menge_ von Typen, während interfaces die Methoden beschreiben, die ein _einzelner_ Typ haben muss, um ihn zu erfüllen.

  2. Contracts kann mit eingebauten Operationen, Umwandlungen usw. umgehen, indem es Typen auflistet, die sie unterstützen; interfaces kann nur mit Methoden umgehen, die die eingebauten Typen selbst nicht haben.

  3. Was auch immer sie in typtheoretischer Hinsicht sein mögen, contracts sind keine Typen in dem Sinne, wie wir sie normalerweise in Go sehen, dh Sie können Variablen von contract -Typen nicht deklarieren und ihnen irgendeinen Wert zuweisen. Andererseits sind interfaces Typen, Sie können Variablen dieser Typen deklarieren und ihnen entsprechende Werte zuweisen.

Obwohl ich den Sinn eines contract sehen kann, der einen einzelnen Typparameter erfordert, um bestimmte Methoden zu haben, stattdessen durch ein interface dargestellt zu werden (das ist etwas, das ich sogar in meiner eigenen Vergangenheit befürwortet habe Vorschläge), denke ich jetzt, dass es ein unglücklicher Schritt wäre, weil es das Wasser zwischen contracts und interfaces wieder trüben würde.

Es war mir vorher nicht wirklich in den Sinn gekommen, dass contracts plausibel so deklariert werden könnte, wie @bigwhite vorgeschlagen hat, das vorhandene 'Typ'-Muster zu verwenden. Aber auch hier bin ich nicht begeistert von der Idee, weil ich das Gefühl habe, dass sie (3) oben gefährden würde. Wenn es (aus Analysegründen) notwendig ist, das Schlüsselwort type zu wiederholen, wenn eine generische Struktur wie folgt deklariert wird:

type List(type Element) struct {
    next *List(Element)
    val  Element
}

Vermutlich wäre es auch notwendig, es zu wiederholen, wenn contracts auf ähnliche Weise deklariert würden, was im Vergleich zum Draft-Design-Ansatz etwas "stotternd" ist.

Eine andere Idee, die mir nicht gefällt, sind `Vertragsliterale', die es ermöglichen würden, contracts 'an Ort und Stelle' und nicht als separate Konstrukte zu schreiben. Dies würde generische Funktions- und Typdefinitionen schwieriger lesbar machen, und da einige Leute denken, dass sie es bereits sind, wird es nicht dazu beitragen, diese Leute davon zu überzeugen, dass Generika eine gute Sache sind.

Es tut mir leid, dass ich gegenüber vorgeschlagenen Änderungen am Generika-Entwurf (der zugegebenermaßen einige Probleme hat) so widerspenstig erscheint, aber als begeisterter Verfechter einfacher Generika für Go denke ich, dass es sich lohnt, diese Punkte anzusprechen.

Ich möchte vorschlagen, Prädikate nicht über Typen "Verträge" zu nennen. Es gibt zwei Gründe:

  • Der Begriff „Verträge“ wird in der Informatik bereits unterschiedlich verwendet. Siehe zum Beispiel: (https://scholar.google.com/scholar?hl=en&as_sdt=0%2C33&q=contracts+languages&btnG=)
  • In der Informatikliteratur gibt es für diese Idee bereits mehrere Namen. Ich kenne mindestens ~drei~ vier: "Schriftsätze", "Typklassen", "Konzepte" und "Einschränkungen". Das Hinzufügen eines weiteren wird die Dinge nur weiter verwirren.

@griesemer "Einschränkungen für Typen" sind eine reine Kompilierzeitsache, da Typen vor der Laufzeit gelöscht werden. Die Beschränkungen bewirken, dass generischer Code in nicht generischen Code ausgearbeitet wird, der ausgeführt werden kann. Untertypen existieren zur Laufzeit und sind keine Einschränkungen in dem Sinne, dass eine Einschränkung für Typen mindestens Typgleichheit oder Typungleichheit wäre, wobei Einschränkungen wie „ist ein Untertyp von“ optional je nach Typsystem verfügbar sind.

Für mich ist die Laufzeitnatur von Untertypen der entscheidende Unterschied, wenn X <: Y können wir X übergeben, wo Y erwartet wird, aber wir kennen den Typ nur als Y ohne unsichere Laufzeitoperationen. In diesem Sinne schränkt es den Typ Y nicht ein, Y ist immer Y. Die Untertypisierung ist auch 'gerichtet' und kann daher kovariant oder kontravariant sein, je nachdem, ob sie auf ein Eingabe- oder Ausgabeargument angewendet wird.

Bei einer Typbeschränkung „pred(X)“ beginnen wir mit einem vollständig polymorphen X und beschränken dann die zulässigen Werte. Sagen Sie also nur X, das 'print' implementiert. Dies ist ungerichtet und weist daher keine Kovarianz oder Kontravarianz auf. Es ist in der Tat invariant, da wir den Grundtyp von X zur Kompilierzeit kennen.

Daher halte ich es für gefährlich, Schnittstellen als Einschränkungen für Typen zu betrachten, da wichtige Unterschiede wie Kovarianz und Kontravarianz ignoriert werden.

Beantwortet das deine Frage oder habe ich das Thema verfehlt?

Bearbeiten: Ich sollte darauf hinweisen, dass ich mich oben speziell auf 'Go'-Schnittstellen beziehe. Die Punkte zum Subtyping gelten für alle Sprachen, die Subtypen haben, aber Go macht Schnittstellen zu einem Typ und hat daher eine Subtyping-Beziehung. In anderen Sprachen wie Java ist eine Schnittstelle ausdrücklich kein Typ (eine Klasse ist ein Typ), daher _sind_ Schnittstellen eine Einschränkung für Typen. Während es also im Allgemeinen richtig ist, Schnittstellen als Einschränkungen für Typen zu betrachten, ist es speziell für „Go“ falsch.

@Inuart Es ist viel zu früh, um zu sagen, wie dies zur Implementierung hinzugefügt werden würde. Es gibt noch keinen Vorschlag, nur einen Designentwurf. Es wird sicherlich nicht in 1.14 sein.

@andrewcmyers Ich mag das Wort "Vertrag", weil es eine Beziehung zwischen dem Autor der generischen Funktion und ihrem Aufrufer beschreibt.

Wörter wie „Schriftsätze“ und „Typklassen“ legen nahe, dass wir von einem Metatyp sprechen, was wir natürlich sind, aber Verträge beschreiben auch eine Beziehung zwischen mehreren Typen. Ich weiß, dass Typklassen, zB in Haskell, mehrere Typparameter haben können, aber mir scheint, dass der Name schlecht zu der beschriebenen Idee passt.

Ich habe nie verstanden, warum C++ dies ein "Konzept" nennt. Was bedeutet das überhaupt?

"Einschränkung" oder "Einschränkungen" wäre mir recht. Im Moment stelle ich mir einen Vertrag so vor, dass er mehrere Einschränkungen enthält. Aber wir könnten dieses Denken ändern.

Ich bin nicht allzu besorgt über die Tatsache, dass es ein vorhandenes Programmiersprachenkonstrukt namens "Vertrag" gibt. Ich denke, dass diese Idee der Idee, die wir ausdrücken möchten, relativ ähnlich ist, da es sich um eine Beziehung zwischen einer Funktion und ihren Aufrufern handelt. Ich verstehe, dass die Art und Weise, wie diese Beziehung ausgedrückt wird, ziemlich unterschiedlich ist, aber ich habe das Gefühl, dass es eine zugrunde liegende Ähnlichkeit gibt.

Ich habe nie verstanden, warum C++ dies ein "Konzept" nennt. Was bedeutet das überhaupt?

Ein Konzept ist eine Abstraktion von Instanziierungen, die einige Gemeinsamkeiten haben, zB Signaturen.

Der Begriff Konzept passt bei weitem besser zu Schnittstellen, da letztere auch verwendet werden, um eine gemeinsame Grenze zwischen zwei Komponenten zu bezeichnen.

@sighoya Ich wollte auch erwähnen, dass „Konzepte“ konzeptionell sind, weil sie „Axiome“ enthalten, die unerlässlich sind, um den Missbrauch von Operatoren zu verhindern. Zum Beispiel sollte der Zusatz '+' assoziativ und kommutativ sein. Diese Axiome können in C++ nicht dargestellt werden, daher existieren sie als abstrakte Ideen, also „Konzepte“. Ein Konzept ist also der syntaktische „Vertrag“ plus die semantischen Axiome.

@ianlancetaylor "Constraint" nennen wir es in Genus (http://www.cs.cornell.edu/~yizhou/papers/genus-pldi2015.pdf), also bin ich von dieser Terminologie abhängig. Der Begriff "Vertrag" wäre eine völlig vernünftige Wahl, außer dass er in der PL-Community sehr aktiv verwendet wird, um sich auf die Beziehung zwischen Schnittstellen und Implementierungen zu beziehen, die auch einen vertraglichen Charakter hat.

@keean Ohne ein Experte zu sein, glaube ich nicht, dass die Dichotomie, die Sie malen, die Realität sehr gut widerspiegelt. Ob der Compiler beispielsweise instanziierte Versionen generischer Funktionen generiert, ist ausschließlich eine Frage der Implementierung, daher ist es durchaus sinnvoll, eine Laufzeitdarstellung von Einschränkungen zu haben, beispielsweise in Form einer Tabelle mit Funktionszeigern für jede erforderliche Operation. Eigentlich genau wie die Methodentabellen der Schnittstelle. Ebenso passen Schnittstellen in Go nicht zu Ihrer Subtyp-Definition, weil Sie sie sicher wieder nach unten projizieren können (über Typzusicherungen) und weil Sie weder Ko- noch Kontravarianz für Typkonstruktoren in Go haben.

Zu guter Letzt: Ob die Dichotomie, die Sie malen, realistisch und genau ist oder nicht, ändert nichts daran, dass eine Schnittstelle letztendlich nur eine Liste von Methoden ist - und selbst in Ihrer Dichotomie gibt es keinen Grund, warum diese Liste dies kann nicht als von der Laufzeit repräsentierte Tabelle oder als Einschränkung nur zur Kompilierzeit wiederverwendet werden, je nach Kontext, in dem sie verwendet wird.

Wie wäre es mit etwas wie:

typeConstraint C(T) {
}

oder

TypVertrag C(T) {
}

Es unterscheidet sich von anderen Typdeklarationen, um zu betonen, dass dies kein Laufzeitkonstrukt ist.

Zur neuen Vertragsgestaltung habe ich einige Fragen.

1.

Wenn ein generischer Typ A einen anderen generischen Typ B einbettet,
oder eine generische Funktion A ruft eine andere generische Funktion B auf,
Müssen wir auch die Verträge von B auf A spezifizieren?

Wenn die Antwort wahr ist, dann wenn ein generischer Typ viele andere generische Typen einbettet,
oder eine generische Funktion ruft viele andere generische Funktionen auf,
dann müssen wir viele Verträge zu einem als Vertrag des Einbettungstyps oder der Anruferfunktion kombinieren.
Dies kann das const-poisoning-alike-Problem verursachen.

  1. Benötigen wir neben den aktuellen Typ- und Methodensatzbeschränkungen noch andere Beschränkungen?
    Wie konvertierbar von einem Typ zum anderen, zuweisbar von einem Typ zum anderen,
    vergleichbar zwischen zwei Typen, ist ein sendbarer Kanal, ist ein empfangbarer Kanal,
    hat einen bestimmten Feldsatz, ...

3.

Wenn eine generische Funktion eine Zeile wie die folgende verwendet

v.Foo()

Wie können wir einen Vertrag schreiben, der Foo entweder eine Methode oder ein Feld eines Funktionstyps sein lässt?

@merovius -Typbeschränkungen müssen zur Kompilierzeit aufgelöst werden, oder das Typsystem kann unzuverlässig sein. Dies liegt daran, dass Sie einen Typ haben können, der von einem anderen abhängt, der bis zur Laufzeit nicht bekannt ist. Sie haben dann zwei Möglichkeiten: Sie müssen ein vollständig abhängiges Typsystem implementieren (das eine Typüberprüfung zur Laufzeit ermöglicht, wenn Typen bekannt werden) oder Sie müssen dem Typsystem existentielle Typen hinzufügen. Existentials codieren die Phasendifferenz von statisch bekannten Typen und Typen, die nur zur Laufzeit bekannt sind (Typen, die beispielsweise vom Lesen aus IO abhängen).

Subtypen, wie oben erwähnt, sind normalerweise bis zur Laufzeit nicht bekannt, obwohl viele Sprachen Optimierungen für den Fall haben, dass der Typ statisch bekannt ist.

Wenn wir davon ausgehen, dass eine der oben genannten Änderungen in die Sprache eingeführt wird (abhängige Typen oder existenzielle Typen), müssen wir die Konzepte der Subtypisierung und Typbeschränkungen noch trennen. Für Go sind insbesondere Typkonstriktoren unveränderlich. Wir können diese Unterschiede ignorieren, und wir können davon ausgehen, dass Go-Schnittstellen (statisch) Beschränkungen für Typen sind.

Wir können daher eine Go-Schnittstelle als einen einzigen Parametervertrag betrachten, bei dem der Parameter der Empfänger aller Funktionen/Methoden ist. Warum also hat go sowohl Schnittstellen als auch Verträge? Es scheint mir, weil Go keine Schnittstellen für Operatoren (wie '+') zulassen will und weil Go weder abhängige Typen noch existentielle Typen hat.

Es gibt also zwei Faktoren, die einen wirklichen Unterschied zwischen Typbeschränkungen und Subtypisierung ausmachen. Die eine ist die Ko/Kontra-Varianz, die wir in Go möglicherweise aufgrund der Typkonstruktor-Invarianz ignorieren können, und die andere ist die Notwendigkeit abhängiger Typen oder existentieller Typen, um ein Typsystem mit Typbeschränkungen stichhaltig zu machen Es gibt einen Laufzeitpolymorphismus der Typparameter für die Typbeschränkungen.

@keean Cool, also AIUI sind wir uns zumindest einig, dass Schnittstellen in Go als Einschränkungen betrachtet werden können :)

Zum Rest: Oben hast du behauptet:

"Einschränkungen für Typen" sind eine reine Kompilierzeitsache, da Typen vor der Laufzeit gelöscht werden. Die Beschränkungen bewirken, dass generischer Code in nicht generischen Code ausgearbeitet wird, der ausgeführt werden kann.

Diese Behauptung ist spezifischer als Ihre letzte, dass Einschränkungen zur Kompilierzeit aufgelöst werden müssen. Alles, was ich sagen wollte, ist, dass der Compiler diese Auflösung (und alle die gleichen Typprüfungen) durchführen kann, aber dann immer noch generischen Code generiert. Es wäre immer noch vernünftig, weil die Semantik des Typsystems dieselbe ist. Aber die Beschränkungen hätten immer noch eine Laufzeitdarstellung. Das ist ein bisschen wählerisch - aber deshalb denke ich, dass es nicht der beste Weg ist, diese auf der Grundlage der Laufzeit vs. der Kompilierzeit zu definieren. Es mischt Implementierungsbedenken in eine Diskussion über die abstrakte Semantik eines Typsystems.

FWIW, ich habe schon früher argumentiert, dass ich es vorziehen würde, Schnittstellen zum Ausdrücken von Einschränkungen zu verwenden - und bin auch zu dem Schluss gekommen, dass das Erlauben der Verwendung von Operatoren in generischem Code das Haupthindernis dafür ist und daher der Hauptgrund, ein separates einzuführen Konzept in Form von Verträgen.

@keean Danke, aber nein, deine Antwort hat meine Frage nicht beantwortet. Beachten Sie, dass ich in meinem Kommentar ein sehr einfaches Beispiel für die Verwendung einer Schnittstelle anstelle eines entsprechenden Vertrags/einer „Einschränkung“ beschrieben habe. Ich bat um ein _einfaches_ _konkretes_ Beispiel, warum dieses Szenario nicht "ohne einige Einschränkungen" funktionieren würde, wie Sie in Ihrem früheren Kommentar angedeutet haben. Ein solches Beispiel haben Sie nicht gegeben.

Beachten Sie, dass ich Subtypen, Co- oder Contra-Varianz (die wir in Go sowieso nicht zulassen, Signaturen müssen immer übereinstimmen) usw. nicht erwähnt habe. Stattdessen habe ich die elementare und etablierte Go-Terminologie verwendet (Schnittstellen, Implementierungen, Typparameter usw.), um zu erklären, was ich mit "Einschränkung" meine, denn das ist die gemeinsame Sprache, die jeder hier versteht und daher kann jeder mitmachen. (Im Gegensatz zu Ihrer Behauptung hier sieht eine Schnittstelle in Java gemäß der Java-Spezifikation für mich wie ein Typ aus: "Eine Schnittstellendeklaration gibt einen neuen benannten Referenztyp an". Wenn dies nicht besagt, dass eine Schnittstelle ein Typ ist dann die Java-Spec-Leute haben einiges zu tun.)

Aber es sieht so aus, als hätten Sie meine Frage mit Ihrem letzten Kommentar indirekt beantwortet, wie @Merovius bereits bemerkte, wenn Sie sagen: "Wir können daher eine Go-Schnittstelle als einen einzigen Parametervertrag betrachten, bei dem der Parameter der Empfänger aller Funktionen/Methoden ist .". Das ist genau der Punkt, den ich am Anfang gemacht habe, also danke für die Bestätigung dessen, was ich die ganze Zeit gesagt habe.

@dotaheor

Wenn ein generischer Typ A einen anderen generischen Typ B einbettet oder eine generische Funktion A eine andere generische Funktion B aufruft, müssen wir dann auch die Verträge von B auf A spezifizieren?

Wenn ein generischer Typ A einen anderen generischen Typ B einbettet, müssen die an B übergebenen Typparameter jeden von B verwendeten Vertrag erfüllen. Dazu muss der von A verwendete Vertrag den von B verwendeten Vertrag implizieren. Das heißt, alle Einschränkungen über die an B übergebenen Typparameter müssen in dem von A verwendeten Vertrag ausgedrückt werden. Dies gilt auch, wenn eine generische Funktion eine andere generische Funktion aufruft.

Wenn die Antwort wahr ist, dann müssen wir, wenn ein generischer Typ viele andere geneirc-Typen einbettet oder eine generische Funktion viele andere generische Funktionen aufruft, viele Verträge zu einem als Vertrag des Einbettungstyps oder der aufrufenden Funktion kombinieren. Dies kann das const-poisoning-alike-Problem verursachen.

Ich denke, was Sie sagen, ist wahr, aber es ist nicht das Const-Poisoning-Problem. Das Const-Poisoning-Problem besteht darin, dass Sie const überall dort verteilen müssen, wo ein Argument übergeben wird, und dann, wenn Sie eine Stelle entdecken, an der das Argument geändert werden muss, müssen Sie const überall entfernen. Der Fall mit Generika ist eher so: "Wenn Sie mehrere Funktionen aufrufen, müssen Sie Werte des richtigen Typs an jede dieser Funktionen übergeben."

Auf jeden Fall scheint es mir äußerst unwahrscheinlich, dass Leute generische Funktionen schreiben, die viele andere generische Funktionen aufrufen, die alle unterschiedliche Verträge verwenden. Wie würde das natürlich passieren?

Benötigen wir neben den aktuellen Typ- und Methodensatzbeschränkungen noch andere Beschränkungen? Zum Beispiel von einem Typ zum anderen umwandelbar, von einem Typ zum anderen zuweisbar, zwischen zwei Typen vergleichbar, ist ein sendbarer Kanal, ist ein empfangbarer Kanal, hat einen bestimmten Feldsatz, ...

Randbedingungen wie Konvertierbarkeit und Zuordenbarkeit und Vergleichbarkeit werden in Form von Typen ausgedrückt, wie der Designentwurf erläutert. Einschränkungen wie Sende- oder Empfangskanal können nur in Form von chan T ausgedrückt werden, wobei T ein Typparameter ist, wie der Designentwurf erklärt. Es gibt keine Möglichkeit, die Einschränkung auszudrücken, dass ein Typ einen bestimmten Feldsatz hat, aber ich bezweifle, dass dies sehr oft vorkommen wird. Wir müssen sehen, wie das funktioniert, indem wir echten Code schreiben, um zu sehen, was passiert.

Wenn eine generische Funktion eine Zeile wie die folgende verwendet

v.Foo()
Wie können wir einen Vertrag schreiben, der es Foo erlaubt, entweder eine Methode oder ein Feld eines Funktionstyps zu sein?

Im aktuellen Designentwurf ist das nicht möglich. Scheint das ein wichtiger Anwendungsfall zu sein? (Ich weiß, dass der vorherige Designentwurf dies unterstützt hat.)

@griesemer Sie haben den Punkt verpasst, an dem ich sagte, dass dies nur gültig ist, wenn Sie abhängige Typen oder Existenztypen in das Typsystem einführen.

Wenn Sie andernfalls einen Vertrag als Schnittstelle verwenden, können Sie zur Laufzeit fehlschlagen, da Sie die Typprüfung verschieben müssen, bis Sie die Typen kennen, und die Typprüfung fehlschlagen kann, was daher nicht typsicher ist.

Ich habe auch gesehen, dass Schnittstellen als Untertypen erklärt wurden, also müssen Sie vorsichtig sein, dass jemand in Zukunft nicht versucht, Ko-/Kontra-Varianz in Typkonstruktoren einzuführen. Besser keine Schnittstellen als Typen haben, dann gibt es dazu keine Möglichkeit, und die Intention der Designer, dass es sich nicht um Untertypen handelt, ist klar.

Für mich wäre es ein besseres Design, Schnittstellen und Verträge zusammenzuführen und sie explizit zu Typbeschränkungen (Prädikaten für Typen) zu machen.

@ianlancetaylor

Auf jeden Fall scheint es mir äußerst unwahrscheinlich, dass Leute generische Funktionen schreiben, die viele andere generische Funktionen aufrufen, die alle unterschiedliche Verträge verwenden. Wie würde das natürlich passieren?

Warum sollte das ungewöhnlich sein? Wenn ich eine Funktion für den Typ 'T' definiere, möchte ich Funktionen für 'T' aufrufen. Zum Beispiel, wenn ich per Vertrag eine 'Summen'-Funktion über 'addierbare Typen' definiere. Jetzt möchte ich eine generische Multiplikationsfunktion erstellen, die Summe aufruft? Viele Dinge in der Programmierung haben eine Summe/Produkt-Struktur (alles, was eine 'Gruppe' ist).

Ich verstehe nicht, was der Zweck der Schnittstelle sein wird, nachdem Verträge in der Sprache sind, es sieht so aus, als würden Verträge demselben Zweck dienen, um sicherzustellen, dass für einen Typ eine Reihe von Methoden definiert sind.

@keean Der ungewöhnliche Fall sind Funktionen, die viele andere generische Funktionen aufrufen, die alle unterschiedliche Verträge verwenden. Ihr Gegenbeispiel ruft nur eine Funktion auf. Denken Sie daran, dass ich gegen die Ähnlichkeit mit const-poisoning argumentiere.

@mrkaspa Der einfachste Weg, darüber nachzudenken, ist, dass Verträge wie C++-Vorlagenfunktionen und Schnittstellen wie virtuelle C++-Methoden sind. Für beides gibt es einen Nutzen und Zweck.

@ianlancetaylor Aus Erfahrung treten zwei Probleme auf, die der Const-Vergiftung ähneln. Beide treten aufgrund der baumähnlichen Natur verschachtelter Funktionsaufrufe auf. Das erste ist, wenn Sie Debugging zu einer tief verschachtelten Funktion hinzufügen möchten, müssen Sie Druckbare vom Blatt bis zum Stamm hinzufügen, was das Berühren mehrerer Bibliotheken von Drittanbietern beinhalten könnte. Zweitens können Sie eine große Anzahl von Verträgen an der Wurzel ansammeln, wodurch Funktionssignaturen schwer lesbar werden. Es ist oft besser, den Compiler die Einschränkungen ableiten zu lassen, wie es Haskell mit Typklassen tut, um diese beiden Probleme zu vermeiden.

@ianlancetaylor Ich weiß nicht allzu viel über C++, was werden die Anwendungsfälle für Schnittstellen und Verträge in Golang sein? Wann sollte ich Schnittstelle oder Vertrag verwenden?

@keean In diesem Unterthread geht es um einen bestimmten Designentwurf für die Go-Sprache. In Go sind alle Werte druckbar. Es ist nicht etwas, das in einem Vertrag zum Ausdruck gebracht werden muss. Und obwohl ich bereit bin, Beweise dafür zu sehen, dass sich viele Verträge für eine einzelne generische Funktion oder einen einzigen Typ ansammeln können, bin ich nicht bereit, eine Behauptung zu akzeptieren, dass dies passieren wird. Der Zweck des Designentwurfs besteht darin, zu versuchen, echten Code zu schreiben, der ihn verwendet.

Der Designentwurf erklärt so klar wie möglich, warum ich denke, dass das Ableiten der Einschränkungen eine schlechte Wahl für eine Sprache wie Go ist, die für die Programmierung in großem Maßstab entwickelt wurde.

@mrkaspa Wenn Sie beispielsweise ein []io.Reader haben, möchten Sie einen Schnittstellenwert, keinen Vertrag. Ein Vertrag würde erfordern, dass alle Elemente im Slice denselben Typ haben. Eine Schnittstelle lässt zu, dass es sich um unterschiedliche Typen handelt, solange alle Typen io.Reader implementieren.

@ianlancetaylor , soweit ich Schnittstelle habe, erstellt einen neuen Typ, während Verträge einen Typ einschränken, aber nein, erstellt einen neuen, habe ich recht?

@ianlancetaylor :

Könnten Sie nicht etwas wie das Folgende tun?

contract Reader(T) {
  T Read([]byte) (int, error)
}

func ReadAll(type T Reader)(readers []T) ([]byte, error) {
  // Use the readers...
}

Jetzt sollte ReadAll() ein []io.Reader genauso gut akzeptieren wie ein []*os.File , nicht wahr? io.Reader scheint den Vertrag zu erfüllen, und ich erinnere mich an nichts im Entwurf darüber, dass Schnittstellenwerte nicht als Typargumente verwendet werden können.

Bearbeiten: Macht nichts. Ich habe es falsch verstanden. Dies ist immer noch ein Ort, an dem Sie eine Schnittstelle verwenden würden, also ist es eine Antwort auf die Frage von @mrkaspa . Sie verwenden die Schnittstelle in der Funktionssignatur einfach nicht. Sie verwenden es nur dort, wo es aufgerufen wird.

@mrkaspa Ja, das stimmt.

@ianlancetaylor wenn ich eine Liste von []io.Reader und diesen Vertrag hätte:

contract Reader(T) {
  T Read([]byte) (int, error)
}

func ReadAll(type T Reader)(readers []T) ([]byte, error) {
  // Use the readers...
}

Ich könnte ReadAll über jede Schnittstelle anrufen, weil sie den Vertrag erfüllen?

@ianlancetaylor sicher, dass die Dinge druckbar sind, aber es ist einfach, andere Beispiele zu finden, zum Beispiel die Protokollierung in eine Datei oder in ein Netzwerk. Wir möchten, dass die Protokollierung generisch ist, damit wir das Protokollziel zwischen null, lokaler Datei, Netzwerkdienst usw. ändern können. Hinzufügen Das Protokollieren in eine Blattfunktion erfordert das Hinzufügen der Einschränkungen bis zum gesamten Stamm, einschließlich der Änderung der verwendeten Bibliotheken von Drittanbietern.

Code ist nicht statisch, Sie müssen auch Wartungsarbeiten einplanen. Tatsächlich befindet sich der Code viel länger in der „Wartung“, als es dauert, ihn ursprünglich zu schreiben, also gibt es ein gutes Argument dafür, dass wir Sprachen entwerfen sollten, um die Wartung, das Refactoring, das Hinzufügen von Funktionen usw. einfacher zu machen.

Wirklich manifestieren sich diese Probleme nur in einer großen Codebasis, die im Laufe der Zeit gepflegt wird. Es ist nicht etwas, das Sie schnell ein kleines Beispiel schreiben können, um es zu demonstrieren.

Diese Probleme gibt es auch in anderen generischen Sprachen, zum Beispiel Ada. Sie könnten eine große Ada-Anwendung auf Go portieren, die in großem Umfang Generika verwendet, aber wenn das Problem in Ada besteht, sehe ich in Go nichts, was dieses Problem entschärfen würde.

@mrkaspa Ja.

An dieser Stelle schlage ich vor, dass dieser Konversationsthread zu golang-nuts wechseln sollte. Der GitHub Issue Tracker ist ein schlechter Ort für diese Art von Diskussion.

@keean Vielleicht hast du recht. Wir werden sehen. Wir bitten die Leute ausdrücklich, zu versuchen, Code für den Designentwurf zu schreiben. Rein hypothetische Diskussionen haben wenig Wert.

@keean Ich verstehe dein Protokollierungsbeispiel nicht. Das Problem, das Sie beschreiben, können Sie mit Schnittstellen zur Laufzeit lösen, nicht mit Generika zur Kompilierzeit.

@bserdar- Schnittstellen haben nur einen Typparameter, sodass Sie nichts tun können, wo ein Parameter das zu protokollierende Objekt und ein zweiter Typparameter der Typ des Protokolls ist.

@keean IMO In diesem Beispiel würden Sie dasselbe tun, was Sie heute tun, ohne jegliche Typparameter: Verwenden Sie Reflektion, um das zu protokollierende Objekt zu untersuchen, und verwenden Sie context.Context , um den Wert des Protokolls zu übergeben. Ich weiß, dass diese Ideen für Tipp-Enthusiasten abstoßend sind, aber sie erweisen sich als ziemlich praktisch. Natürlich haben eingeschränkte Typparameter einen Wert, weshalb wir dieses Gespräch führen - aber ich würde sagen, dass der Grund, warum Ihnen die Fälle in den Sinn kommen, die Fälle sind, die in aktuellen Go-Codebasen in großem Umfang bereits ziemlich gut funktionieren , dass dies nicht die Fälle sind, die wirklich von einer zusätzlichen strengen Typprüfung profitieren. Womit wir auf Ians Punkt zurückkommen – es bleibt abzuwarten, ob sich dies in der Praxis als Problem manifestiert.

@merovius Wenn es nach mir ginge, würde jede Laufzeitreflexion verboten, da ich keine gelieferte Software möchte, die zur Laufzeit Tippfehler generiert, die den Benutzer beeinträchtigen könnten. Dies ermöglicht aggressivere Compileroptimierungen, da Sie sich keine Gedanken über die Ausrichtung des Laufzeitmodells am statischen Modell machen müssen.

Nachdem ich mich mit der Migration großer Projekte in großem Maßstab von JavaScript zu TypeScript befasst habe, wird meiner Erfahrung nach die strikte Typisierung umso wichtiger, je größer das Projekt und je größer das Team, das daran arbeitet. Dies liegt daran, dass Sie sich auf die Schnittstelle/den Vertrag eines Codeblocks verlassen müssen, ohne sich um die Implementierung kümmern zu müssen, um die Effizienz bei der Arbeit mit einem großen Team aufrechtzuerhalten.

Beiseite: Natürlich hängt es davon ab, wie Sie die Skalierung erreichen, im Moment bevorzuge ich einen API-First-Ansatz, beginnend mit einer OpenAPI/Swagger-JSON-Datei und dann die Verwendung von Code-Generierung, um die Server-Stubs und das Client-SDK zu erstellen. Als solche fungiert OpenAPI tatsächlich als Ihr Typsystem für Mikrodienste.

@ianlancetaylor

Einschränkungen wie Konvertierbarkeit und Zuordenbarkeit und Vergleichbarkeit werden in Form von Typen ausgedrückt

In Anbetracht der vielen Details in den Konvertierungsregeln für Go-Typen ist es wirklich schwierig, einen benutzerdefinierten Vertrag C zu schreiben, um die folgende allgemeine Slice-Konvertierungsfunktion zu erfüllen:

func ConvertSlice(type In, Out C(In, Out)) (x []In) []Out {
    o := make([]Out, len(x))
    for i := range x {
        o[i] = Out(x[i])
    }
    return o
}

Ein perfektes C sollte Konvertierungen ermöglichen:

  • zwischen allen ganzzahligen, numerischen Fließkommatypen
  • zwischen beliebigen komplexen numerischen Typen
  • zwischen zwei Typen, deren zugrunde liegende Typen identisch sind
  • von einem Typ Out , der In implementiert
  • von einem Kanaltyp zu einem bidirektionalen Kanaltyp und die beiden Kanaltypen haben einen identischen Elementtyp
  • struct tag bezogen, ...
  • ...

Nach meinem Verständnis kann ich einen solchen Vertrag nicht schreiben. Brauchen wir also einen eingebauten convertible -Vertrag?

Es gibt keine Möglichkeit, die Einschränkung auszudrücken, dass ein Typ einen bestimmten Feldsatz hat, aber ich bezweifle, dass dies sehr oft vorkommen wird

Wenn man bedenkt, dass Type Embedding oft in der Go-Programmierung verwendet wird, denke ich, dass die Bedürfnisse nicht selten sind.

@keean Das ist eine gültige Meinung, aber sie ist offensichtlich nicht diejenige, die das Design und die Entwicklung von Go leitet. Um konstruktiv mitzuwirken, akzeptieren Sie das bitte und beginnen Sie dort zu arbeiten, wo wir sind , und unter der Annahme, dass jede Entwicklung der Sprache eine allmähliche Änderung des Status quo sein muss. Wenn Sie das nicht können, dann gibt es Sprachen, die Ihren Vorlieben besser entsprechen, und ich glaube, jeder – insbesondere Sie – wäre glücklicher, wenn Sie Ihre Energie dort einbringen würden.

@merovius Ich bin bereit zu akzeptieren, dass Änderungen an Go schrittweise erfolgen müssen, und akzeptiere den Status quo.

Ich habe gerade im Rahmen eines Gesprächs auf Ihren Kommentar geantwortet und zugestimmt, dass ich ein Tipp-Enthusiast bin. Ich habe eine Meinung zur Laufzeitreflexion geäußert, ich habe nicht vorgeschlagen, dass Go die Laufzeitreflexion aufgeben sollte. Ich arbeite an anderen Sprachen, verwende viele Sprachen in meiner Arbeit. Ich entwickle (langsam) meine eigene Sprache, aber ich hoffe immer, dass Entwicklungen in anderen Sprachen dies überflüssig machen werden.

@dotaheor Ich stimme zu, dass wir heute keinen Rahmenvertrag zur Konvertibilität schreiben können. Ob das in der Praxis ein Problem darstellt, müssen wir sehen.

Als Antwort auf @ianlancetaylor

Ich glaube, es ist noch nicht klar, wie oft Leute Funktionen mit konstanten Werten parametrisieren wollen. Der naheliegendste Fall wären Array-Dimensionen – aber Sie können das bereits tun, indem Sie den gewünschten Array-Typ als Typargument übergeben. Abgesehen von diesem Fall, was gewinnen wir wirklich, wenn wir eine Konstante als Argument zur Kompilierzeit und nicht als Argument zur Laufzeit übergeben?

Im Fall von Arrays scheint es extrem einschränkend zu sein, nur den (ganzen) Array-Typ als Typargument zu übergeben, da der Vertrag weder die Array-Dimension noch den Elementtyp zerlegen und ihnen Einschränkungen auferlegen könnte. Könnte beispielsweise ein Vertrag, der einen "ganzen Array-Typ" annimmt, erfordern, dass der Elementtyp des Array-Typs bestimmte Methoden implementiert?

Aber Ihre Forderung nach spezifischeren Beispielen dafür, wie generische Nicht-Typ-Parameter nützlich wären, wird gut aufgenommen, daher habe ich den Blog-Beitrag erweitert, um einen Abschnitt einzuschließen, der einige wichtige Klassen von Beispielanwendungsfällen und einige spezifische Beispiele für jeden abdeckt. Da es schon ein paar Tage her ist, gibt es hier wieder den Blogbeitrag:

Sind nur Typparameter generisch genug für Go 2-Generika?

Der neue Abschnitt trägt den Titel „Beispiele, wie Generika gegenüber Nicht-Typen nützlich sind“.

Als kurze Zusammenfassung können Verträge für Matrix- und Vektoroperationen sowohl für die Dimensionalität als auch für die Elementtypen von Arrays geeignete Einschränkungen auferlegen. Beispielsweise könnte die Matrixmultiplikation einer nxm-Matrix mit einer mxp-Matrix, die jeweils als zweidimensionales Array dargestellt werden, die Anzahl der Zeilen der ersten Matrix korrekt auf die Anzahl der Spalten der zweiten Matrix beschränken usw.

Allgemeiner könnten Generika Nicht-Typ-Parameter verwenden, um die Konfiguration zur Kompilierzeit und die Spezialisierung von Code und Algorithmen auf vielfältige Weise zu ermöglichen. Beispielsweise könnte eine generische Variante von math/big.Int zur Kompilierzeit auf ein bestimmtes Bit mit und/oder Vorzeichen konfiguriert werden, wodurch die Anforderungen für 128-Bit-Ganzzahlen und andere nicht native Ganzzahlen mit fester Breite mit angemessener Effizienz wahrscheinlich viel besser erfüllt werden als das bestehende big.Int, wo alles dynamisch ist. Eine generische Variante von big.Float könnte in ähnlicher Weise zur Kompilierzeit auf eine bestimmte Genauigkeit und/oder andere Parameter zur Kompilierzeit spezialisiert werden, z. B. um einigermaßen effiziente generische Implementierungen der Formate „binary16“, „binary128“ und „binary256“ aus IEEE 754-2008 bereitzustellen dass Go nicht nativ unterstützt. Viele Bibliotheksalgorithmen, die ihren Betrieb basierend auf dem Wissen über die Bedürfnisse des Benutzers oder bestimmte Aspekte der verarbeiteten Daten optimieren können – z sich darauf verlassen, dass Matrizen obere oder untere Dreiecke sind, oder Arithmetik für große Ganzzahlen für die Kryptografie, die manchmal in konstanter Zeit implementiert werden müssen und manchmal nicht - könnten Generika verwenden, um sich zur Kompilierzeit konfigurierbar zu machen, um von optionalen deklarativen Informationen wie z Dies, während sichergestellt wird, dass alle Tests dieser Optionen zur Kompilierzeit in der Implementierung normalerweise über konstante Weitergabe kompiliert werden.

@bford schrieb:

nämlich, dass die Parameter für Generika zur Kompilierzeit an Konstanten gebunden werden.

Das ist der Punkt, den ich nicht verstehe. Warum Sie diese Bedingung benötigen.
Theoretisch könnte man Variablen/Parameter im Körper neu definieren. Es spielt keine Rolle.
Intuitiv nehme ich an, dass Sie angeben möchten, dass die erste Funktionsanwendung zur Kompilierzeit erfolgen muss.

Aber für diese Anforderung wäre ein Schlüsselwort wie comp oder comptime besser geeignet.
Wenn die Grammatik von Golang außerdem höchstens zwei Parametertupel für eine Funktion zulassen würde, kann diese Schlüsselwortannotation weggelassen werden, da immer das erste Parametertupel eines Typs und einer Funktion (im Fall von zwei Parametertupeln) ausgewertet wird zur Kompilierzeit.

Ein weiterer Punkt: Was ist, wenn const erweitert wird, um Laufzeitausdrücke zu ermöglichen (echtes einmaliges Anmelden)?

Auf Zeiger vs. Wertmethoden :

Wenn eine Methode in einem Vertrag mit einem einfachen T anstelle von *T aufgeführt ist, kann es sich entweder um eine Zeigermethode oder um eine Wertmethode von T . Um sich über diese Unterscheidung keine Gedanken zu machen, sind in einem generischen Funktionsrumpf alle Methodenaufrufe Zeigermethodenaufrufe. ...

Wie passt das zur Schnittstellenimplementierung? Wenn T eine Zeigermethode hat (wie MyInt im Beispiel), kann T der Schnittstelle mit dieser Methode zugewiesen werden ( Stringer in der Beispiel)?

Das Zulassen bedeutet, dass eine weitere versteckte Adressoperation & vorhanden ist, das Nichtzulassen bedeutet, dass Verträge und Schnittstellen nur über einen expliziten Typwechsel interagieren können. Beide Lösungen erscheinen mir nicht gut.

(Hinweis: Wir sollten diese Entscheidung überdenken, wenn sie zu Verwirrung oder falschem Code führt.)

Wie ich sehe, hat das Team bereits einige Vorbehalte gegen diese Mehrdeutigkeit in der Zeigermethodensyntax. Ich füge nur hinzu, dass die Mehrdeutigkeit auch die Schnittstellenimplementierung betrifft (und füge implizit auch meine Vorbehalte hinzu).

@fJavierZunzunegui Sie haben Recht, der aktuelle Text impliziert, dass beim Zuweisen eines Werts eines Typparameters zu einem Schnittstellentyp möglicherweise eine implizite Adressoperation erforderlich ist. Dies kann ein weiterer Grund sein, beim Aufrufen von Methoden keine impliziten Adressen zu verwenden. Wir müssen sehen.

Zu parametrisierten Typen , insbesondere in Bezug auf Typparameter, die als Feld in eine Struktur eingebettet sind:

Erwägen

type Lockable(type T) struct {
    T
    sync.Locker
}

Was wäre, wenn T eine Methode namens Lock oder Unlock hätte? Die Struktur ließ sich nicht kompilieren. Dass es keine Methode-X -Bedingung gibt, wird von Verträgen nicht unterstützt, daher haben wir ungültigen Code, der den Vertrag nicht bricht (was den gesamten Zweck von Verträgen zunichte macht).

Noch komplizierter wird es, wenn Sie mehrere eingebettete Parameter haben (z. B. T1 und T2 ), da diese keine gemeinsamen Methoden haben dürfen (wiederum nicht durch Verträge erzwungen). Darüber hinaus trägt die Unterstützung beliebiger Methoden in Abhängigkeit von den eingebetteten Typen zu sehr begrenzten Einschränkungen der Kompilierzeit bei Typwechseln für diese Strukturen bei (sehr ähnlich wie Type Assertions and Switches ).

Aus meiner Sicht gibt es 2 gute Alternativen:

  • das Einbetten von Typparametern insgesamt verbieten: einfach, aber zu geringen Kosten (wenn die Methode benötigt wird, muss man sie explizit in die Struktur mit dem Feld schreiben).
  • Beschränken Sie aufrufbare Methoden auf die Vertragsmethoden: ähnlich wie beim Einbetten einer Schnittstelle. Dies weicht vom normalen Go (kein Ziel) ab, ist jedoch kostenlos (Methoden müssen nicht explizit in die Struktur mit dem Feld geschrieben werden).

Die Struktur ließ sich nicht kompilieren.

Es würde kompilieren. Versuch es. Was nicht kompiliert werden kann, ist ein Aufruf der mehrdeutigen Methode. Ihr Punkt ist jedoch immer noch gültig.

Ihre zweite Lösung, die aufrufbare Methoden auf die im Vertrag genannten beschränkt, funktioniert nicht: Selbst wenn der Vertrag auf T Lock und Unlock spezifiziert hat, könnten Sie es trotzdem tun. Rufen Sie sie nicht auf Lockable an.

@jba danke für die Einblicke in die Kompilierung.

Mit der zweiten Lösung meine ich die Behandlung eingebetteter Typparameter, wie wir es jetzt mit Schnittstellen tun, sodass, wenn die Methode nicht im Vertrag enthalten ist, sie nach dem Einbetten nicht sofort zugänglich ist. Da T keinen Vertrag hat, wird es in diesem Szenario effektiv als interface{} behandelt, daher würde es nicht mit sync.Locker in Konflikt geraten, selbst wenn T damit instanziiert würde ein Typ mit diesen Methoden. Dies könnte helfen, meinen Punkt zu erklären .

In jedem Fall bevorzuge ich die erste Lösung (das Einbetten insgesamt verbieten). Wenn Sie dies also bevorzugen, hat es wenig Sinn, die zweite Lösung zu diskutieren! :smiley:

Das Beispiel von @JavierZunzunegui deckt auch einen anderen Fall ab. Was ist, wenn T eine Struktur ist, die ein Feld noCopy noCopy hat? Compiler sollte in der Lage sein, diesen Fall auch zu behandeln.

Ich bin mir nicht sicher, ob dies genau der richtige Ort dafür ist, aber ich wollte einen konkreten Anwendungsfall aus der Praxis für generische Typen kommentieren, die eine "Parametrisierung von Nicht-Typ-Werten wie Konstanten" ermöglichen, und speziell für den Fall von Arrays . Ich hoffe, das ist hilfreich.

In meiner Welt ohne Generika schreibe ich viel Code, der so aussieht:

import "math/bits"

// SigEl is the element type used in variable length bit vectors, 
// can be any unsigned integer type
type SigEl = uint

// SigElBits is the number of bits storable in each SigEl
const SigElBits = 8 << uint((^SigEl(0)>>32&1)+(^SigEl(0)>>16&1)+(^SigEl(0)>>8&1))

// HammingDist counts the number bitwise differences between two
// bit vectors b1 and b2. I want this to be generic
// Function will panic at runtime if b1 and b2 aren't of equal length.
func HammingDist(b1, b2 []SigEl) (sum int) {
    // Give the compiler a hint so it won't need to bounds check the slices in loops
    _ = b1[len(b2)-1]  
        // This switch is optimized away because SigElBits is const
    switch SigElBits {   // Yay no golang generics!
    case 64:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount64(uint64(b1[x] ^ b2[x]))
        }
    case 32:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount32(uint32(b1[x] ^ b2[x]))
        }
    case 16:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount16(uint16(b1[x] ^ b2[x]))
        }
    case 8:
        _ = b2[len(b1)-1]
        for x := range b1 {
            sum += bits.OnesCount8(uint8(b1[x] ^ b2[x]))
        }
    }
    return sum
}

Das funktioniert gut genug, mit einer Falte. Ich brauche oft Hunderte von Millionen von []SigEl s, und ihre Länge beträgt oft 128-384 Gesamtbits. Da Slices zusätzlich zur Größe des zugrunde liegenden Arrays einen festen 192-Bit-Overhead auferlegen, führt dies, wenn das Array selbst 384 Bit oder weniger groß ist, zu einem unnötigen Speicher-Overhead von 50-150 %, was offensichtlich schrecklich ist.

Meine Lösung besteht darin, ein Stück von Sig _arrays_ zuzuweisen und sie dann spontan als Parameter für HammingDist oben zu schneiden:

const SigBits = 256  // Any multiple of SigElBits is valid

// Sig is the bit vector array type
type Sig [SigBits/SigElBits]SigEl

bitVects := make([]Sig, 100000000)
// stuff happens ... 

// Note slicing below, just to make the arrays "generic" for the call 
dist := HammingDist(bitVects[x][:], bitVects[y][:])

Was ich _gerne_ stattdessen tun könnte, ist, einen generischen Signaturtyp zu definieren und alles oben Gesagte umzuschreiben als (etwas wie):

contract UnsignedInteger(T) {
    T uint, uint8, uint16, uint32, uint64
}

type Signature (type Element UnsignedInteger, n int) [n]Element

// HammingDist counts the number bitwise differences between two bit vectors
func HammingDist(b1, b2 *Signature) (sum int) {
    for x := range *b1 {
        // Assuming the std lib bits.OnesCount becomes generic over 
        // all UnsignedInteger types
        sum += bits.OnesCount(*b1[x] ^ *b2[x])
    }
    return sum
}

Also, um diese Bibliothek zu verwenden:

type sigEl = uint   // Any unsigned int type
const sigElBits = 8 << uint((^SigEl(0)>>32&1)+(^SigEl(0)>>16&1)+(^SigEl(0)>>8&1))
const sigBits = 256  // Any multiple of SigElBits is valid
type sig Signature(sigEl, sigBits/sigElBits)

bitVects := make([]sig, 100000000)
// stuff happens ... 

dist := HammingDist(&bitVects[x], &bitVects[y])

Ein Ingenieur kann träumen... 🤖

Wenn Sie wissen, wie groß die maximale Bitlänge sein könnte, können Sie stattdessen so etwas verwenden:

contract uintArrayOfFixedLength(ElemType,ArrayType)
{
    ArrayType [1]ElemType,[2]ElemType,...,[maxBit]ElemType
    ElemType uint8,uint16,uint32,uint64
}

func HammingDist(type ElemType,ArrayType uintArrayOfFixedLength)(t1,t2 ArrayType) (sum int)
{

}

@vsivsi Ich bin mir nicht sicher, ob ich verstehe, wie Sie denken, dass dies die Dinge verbessern wird - gehen Sie vielleicht davon aus, dass der Compiler eine instanziierte Version dieser Funktion für jede mögliche Array-Länge generieren würde? Weil ISTM, dass a) das nicht sehr wahrscheinlich ist, also b) Sie am Ende genau die gleichen Leistungsmerkmale haben würden wie jetzt. Die wahrscheinlichste Implementierung, meiner Meinung nach, wäre immer noch, dass der Compiler die Länge und einen Zeiger auf das erste Element übergibt, sodass Sie effektiv immer noch ein Slice im generierten Code übergeben würden (ich meine, Sie würden die Kapazität nicht übergeben, aber ich denke nicht, dass ein zusätzliches Wort auf dem Stapel wirklich wichtig ist).

Ehrlich gesagt, IMO ist das, was Sie sagen, ein ziemlich gutes Beispiel für die übermäßige Verwendung von Generika, wo sie nicht benötigt werden - "ein Array von unbestimmter Länge" ist genau das, wofür Slices da sind.

@Merovius Danke, ich denke, Ihr Kommentar enthüllt einige interessante Diskussionspunkte.

"ein Array von unbestimmter Länge" ist genau das, wofür Slices da sind.

Richtig, aber in meinem Beispiel gibt es keine Arrays unbestimmter Länge. Die Array-Länge ist eine bekannte Konstante zur _Kompilierungszeit_. Genau dafür sind Arrays da, aber sie werden in Golang meiner Meinung nach zu wenig genutzt, weil sie so unflexibel sind.

Um klar zu sein, ich schlage nicht vor

type Signature (type Element UnsignedInteger, n int) [n]Element

bedeutet, dass n eine Laufzeitvariable ist. Es muss im gleichen Sinne wie heute noch eine Konstante sein:

const n = 10
type nArray [n]uint               // works
type nSigInt Signature(uint, n)   // works 

var m = int(n)
type mArray [m]uint               // error
type mSigInt Signature(uint, m)   // error 

Schauen wir uns also die "Kosten" der Slice-basierten Funktion HammingDist an. Ich stimme zu, dass der Unterschied zwischen der Übergabe eines Arrays als bitVects[x][:] vs. &bitVects[x] klein ist (-ish, ein Faktor von maximal 3). Der wirkliche Unterschied liegt in der Code- und Laufzeitprüfung, die innerhalb dieser Funktion stattfinden muss.

In der Slice-basierten Version muss der Laufzeitcode die Slice-Zugriffe überprüfen, um die Speichersicherheit zu gewährleisten. Dies bedeutet, dass diese Version des Codes in Panik geraten kann (oder dass ein expliziter Fehlerprüfungs- und Rückgabemechanismus erforderlich ist, um dies zu verhindern). Die NOP-Zuweisungen ( _ = b1[len(b2)-1] ) bewirken einen bedeutenden Leistungsunterschied, indem sie dem Compiler-Optimierer einen Hinweis geben, dass er nicht jeden Slice-Zugriff in der Schleife auf Grenzen prüfen muss. Aber diese minimalen Begrenzungsprüfungen sind immer noch notwendig, obwohl die übergebenen zugrunde liegenden Arrays immer dieselbe Länge haben. Darüber hinaus kann der Compiler Schwierigkeiten haben, die For/Range-Schleife gewinnbringend zu optimieren (z. B. über Unrolling ).

Im Gegensatz dazu kann die generische Array-basierte Version der Funktion zur Laufzeit nicht in Panik geraten (erfordert keine Fehlerbehandlung) und umgeht die Notwendigkeit einer bedingten Begrenzungsprüfungslogik. Ich bezweifle sehr, dass eine kompilierte generische Version der Funktion die Arraylänge wie von Ihnen vorgeschlagen "übergeben" müsste, da es sich buchstäblich um einen konstanten Wert handelt, der zur Kompilierzeit Teil des instanziierten Typs ist.

Darüber hinaus wäre es für kleine Array-Dimensionen (wichtig in meinem Fall) für den Compiler einfach, die For/Range-Schleife für einen anständigen Leistungsgewinn gewinnbringend aufzurollen oder sogar vollständig zu optimieren, da er zur Kompilierzeit weiß, was diese Dimensionen sind .

Der andere große Vorteil der generischen Version des Codes besteht darin, dass der Benutzer des HammingDist -Moduls den unsigned int-Typ in seinem eigenen Code bestimmen kann. Die nicht generische Version erfordert, dass das Modul selbst modifiziert wird, um den definierten Typ SigEl zu ändern, da es keine Möglichkeit gibt, einen Typ an ein Modul zu "übergeben". Eine Folge dieses Unterschieds besteht darin, dass die Implementierung der Abstandsfunktion einfacher wird, wenn es nicht erforderlich ist, separaten Code für jeden der {8,16,32,64}-Bit-uint-Fälle zu schreiben.

Die Kosten der Slice-basierten Version der Funktion und die Notwendigkeit, den Bibliothekscode zu modifizieren, um den Elementtyp festzulegen, sind höchst suboptimale Zugeständnisse, die erforderlich sind, um zu vermeiden, dass "NxM"-Versionen dieser Funktion implementiert und gewartet werden müssen. Generische Unterstützung für (konstante) parametrisierte Array-Typen würde dieses Problem lösen:

// With generics + parameterized constant array lengths:
type Signature (type Element UnsignedInteger, n int) [n]Element
func HammingDist(b1, b2 *Signature) (sum int) { ... }

// Without generics
func HammingDistL1Uint(b1, b2 [1]uint) (sum int) { ... }
func HammingDistL1Uint8(b1, b2 [1]uint8) (sum int) { ... }
func HammingDistL1Uint16(b1, b2 [1]uint16) (sum int) { ... }
func HammingDistL1Uint32(b1, b2 [1]uint32) (sum int) { ... }
func HammingDistL1Uint64(b1, b2 [1]uint64) (sum int) { ... }

func HammingDistL2Uint(b1, b2 [2]uint) (sum int) { ... }
func HammingDistL2Uint8(b1, b2 [2]uint8) (sum int) { ... }
func HammingDistL2Uint16(b1, b2 [2]uint16) (sum int) { ... }
func HammingDistL2Uint32(b1, b2 [2]uint32) (sum int) { ... }
func HammingDistL2Uint64(b1, b2 [2]uint64) (sum int) { ... }

func HammingDistL3Uint(b1, b2 [3]uint) (sum int) { ... }
func HammingDistL3Uint8(b1, b2 [3]uint8) (sum int) { ... }
func HammingDistL3Uint16(b1, b2 [3]uint16) (sum int) { ... }
func HammingDistL3Uint32(b1, b2 [3]uint32) (sum int) { ... }
func HammingDistL3Uint64(b1, b2 [3]uint64) (sum int) { ... }

// and L4, L5, L6 ... ad nauseum

Die Vermeidung des oben genannten Albtraums oder der sehr realen Kosten der aktuellen Alternativen erscheint mir wie das _Gegenteil_ von "allgemeiner Überbeanspruchung". Ich stimme @sighoya zu, dass die Aufzählung aller zulässigen Array-Längen im Vertrag für eine sehr begrenzte Anzahl von Fällen funktionieren könnte, aber ich glaube, dass dies selbst für meinen Fall zu begrenzt ist, da selbst wenn ich die obere Grenze der Unterstützung auf a setze Niedrige 384 Bits insgesamt, was fast 50 Bedingungen in der ArrayType [1]ElemType,[2]ElemType,...,[maxBit]ElemType -Klausel des Vertrags erfordern würde, um den uint8 -Fall abzudecken.

Richtig, aber in meinem Beispiel gibt es keine Arrays unbestimmter Länge. Die Arraylänge ist zur Kompilierzeit eine bekannte Konstante.

Ich verstehe das, aber beachten Sie, dass ich auch nicht "zur Laufzeit" gesagt habe. Sie möchten Code schreiben, der die Länge des Arrays nicht beachtet. Slices können das schon.

Ich bezweifle sehr, dass eine kompilierte generische Version der Funktion die Arraylänge wie von Ihnen vorgeschlagen "übergeben" müsste, da es sich buchstäblich um einen konstanten Wert handelt, der zur Kompilierzeit Teil des instanziierten Typs ist.

Eine generische Version der Funktion würde - da jede Instanziierung dieses Typs eine andere Konstante verwendet. Deshalb habe ich den Eindruck, dass Sie davon ausgehen, dass der generierte Code nicht generisch, sondern für jeden Typ erweitert wird. dh Sie scheinen davon auszugehen, dass mehrere Instanziierungen dieser Funktion generiert werden, für [1]Element , [2]Element usw. Ich sage, dass mir das unwahrscheinlich erscheint, dass es wahrscheinlicher erscheint dass eine Version generiert wird, die im Wesentlichen der Slice-Version entspricht.

Natürlich muss es nicht so sein. Also, ja, Sie haben Recht, dass Sie die Array-Länge nicht übergeben müssen . Ich sage nur stark voraus, dass es auf diese Weise implementiert werden würde , und es scheint eine fragwürdige Annahme zu sein, dass dies nicht der Fall sein wird. (FWIW, ich würde auch argumentieren, dass, wenn Sie bereit sind, dass der Compiler spezialisierte Funktionskörper für separate Längen generiert, er dies genauso gut auch für Slices transparent tun könnte, aber das ist eine andere Diskussion).

Der andere große Vorteil der generischen Version des Codes

Zur Verdeutlichung: Beziehen Sie sich mit "der generischen Version" auf die allgemeine Idee von Generika, wie sie beispielsweise im aktuellen Vertragsdesignentwurf implementiert ist, oder beziehen Sie sich spezifischer auf Generika mit Nicht-Typ-Parametern? Denn die Vorteile, die Sie in diesem Absatz nennen, gelten auch für den aktuellen Entwurf der Vertragsgestaltung.

Ich versuche hier nicht, allgemein gegen Generika zu argumentieren. Ich erkläre nur, warum ich nicht glaube, dass Ihr Beispiel zeigt, dass wir andere Parameterarten als Typen brauchen.

// With generics + parameterized constant array lengths:
// Without generics

Das ist eine falsche Dichotomie (und eine so offensichtliche, dass ich ein bisschen frustriert von dir bin). Es gibt auch "mit Typparametern, aber ohne Ganzzahlparameter":

contract Unsigned(T) {
    T uint, uint8, uint16, uint32, uint64
}
func HammingDist(type T Unsigned) (b1, b2 []T) (sum int) {
    if len(b1) != len(b2) {
        panic("slices of different lengths passed to HammingDist")
    }
    for i := range b1 {
        sum += bits.OnesCount(b1[i]^b2[i]) // Same assumption about OnesCount being generic you made above
    }
    return sum
}

Was mir gut erscheint. Es ist etwas weniger typsicher, da eine Laufzeitpanik erforderlich ist, wenn die Typen nicht übereinstimmen. Aber, und das ist irgendwie mein Punkt, das ist der einzige Vorteil des Hinzufügens von generischen Nicht-Typ-Parametern in Ihrem Beispiel (und es ist ein Vorteil, der meiner Meinung nach bereits klar war). Die von Ihnen prognostizierten Leistungssteigerungen beruhen auf ziemlich starken Annahmen darüber, wie Generika im Allgemeinen und Generika gegenüber Nichttypparametern im Besonderen implementiert werden. Das halte ich persönlich nach dem, was ich bisher vom Go-Team gehört habe, für nicht sehr wahrscheinlich.

Ich bezweifle sehr, dass eine kompilierte generische Version der Funktion die Arraylänge wie von Ihnen vorgeschlagen "übergeben" müsste, da es sich buchstäblich um einen konstanten Wert handelt, der zur Kompilierzeit Teil des instanziierten Typs ist.

Sie gehen nur davon aus, dass Generika wie C++-Vorlagen und doppelte Funktionsimplementierungen funktionieren würden, aber das ist einfach nicht richtig. Der Vorschlag erlaubt explizit einzelne Implementierungen mit versteckten Parametern.

Ich denke, wenn Sie wirklich vorlagenbasierten Code für eine kleine Anzahl numerischer Typen benötigen, ist es keine große Belastung, einen Codegenerator zu verwenden. Generika sind die Codekomplexität wirklich nur für Dinge wie Containertypen wert, bei denen es einen messbaren Leistungsvorteil durch die Verwendung primitiver Typen gibt, Sie aber vernünftigerweise nicht erwarten können, nur eine kleine Anzahl von Codevorlagen im Voraus zu generieren.

Ich habe offensichtlich keine Ahnung, wie die Golang-Maintainer letztendlich irgendetwas implementieren werden, also werde ich auf weitere Spekulationen verzichten und mich glücklicherweise auf diejenigen mit mehr Insiderwissen verlassen.

Was ich weiß, ist, dass für das oben beschriebene reale Beispielproblem der potenzielle Leistungsunterschied zwischen der aktuellen Slice-basierten Implementierung und einer gut optimierten generischen Array-basierten Implementierung erheblich ist.

BenchmarkHD/256-bit_unrolled_array_HD-20            2000000000           1.05 ns/op        0 B/op          0 allocs/op
BenchmarkHD/256-bit_slice_HD-20                     300000000            5.10 ns/op        0 B/op          0 allocs/op

Code unter: https://github.com/vsivsi/hdtest

Das ist ein 5-facher potenzieller Leistungsunterschied für den 4x64-Bit-Fall (ein Sweetspot in meiner Arbeit) mit nur einem kleinen Loop-Unrolling (und im Wesentlichen keinem zusätzlichen ausgegebenen Code) im Array-Fall. Diese Berechnungen befinden sich in den inneren Schleifen meiner Algorithmen und werden buchstäblich viele Billionen Mal durchgeführt, sodass ein 5-facher Leistungsunterschied ziemlich groß ist. Aber um diese Effizienzgewinne heute zu realisieren, muss ich jede Version der Funktion für jeden benötigten Elementtyp und jede Array-Länge schreiben.

Aber ja, wenn Optimierungen wie diese niemals von den Betreuern implementiert werden, dann wäre die ganze Übung, parametrisierte Array-Längen zu Generika hinzuzufügen, sinnlos, zumindest da es diesem Beispielfall zugute kommen könnte.

Jedenfalls interessante Diskussion. Ich weiß, dass dies umstrittene Themen sind, also danke, dass du höflich bleibst!

@vsivsi FWIW, die Gewinne, die Sie beobachten, verschwinden, wenn Sie Ihre Loops nicht manuell entrollen (oder wenn Sie die Schleife auch über ein Slice entrollen) - dies unterstützt also immer noch nicht Ihr Argument, dass Integer-Parameter helfen, weil sie dies zulassen der Compiler, der das Ausrollen für Sie erledigt. Es scheint mir schlechte Wissenschaft zu sein, X über Y zu argumentieren, basierend darauf, dass der Compiler für X beliebig schlau wird und für Y beliebig dumm bleibt. Es ist mir nicht klar, warum eine andere Unrolling-Heuristik im Fall einer Schleife über ein Array auslösen würde , aber nicht auslösen, wenn ein Slice mit zur Kompilierzeit bekannter Länge durchlaufen wird. Sie zeigen nicht die Vorteile einer bestimmten Sorte von Generika gegenüber einer anderen, Sie zeigen die Vorteile dieser anderen entfaltenden Heuristik.

Aber auf jeden Fall hat niemand wirklich argumentiert, dass das Generieren von spezialisiertem Code für jede Instanziierung einer generischen Funktion nicht potenziell schneller sein würde - nur, dass es andere Kompromisse zu berücksichtigen gilt , wenn Sie entscheiden, ob Sie dies tun möchten.

@Merovius Ich denke, der stärkste Fall für Generika in dieser Art von Beispiel ist die Ausarbeitung zur Kompilierzeit (also das Ausgeben einer eindeutigen Funktion für jede Ganzzahl auf Typebene), bei der sich der zu spezialisierende Code in einer Bibliothek befindet. Wenn der Bibliotheksbenutzer eine begrenzte Anzahl von Instanziierungen der Funktion verwendet, erhält er den Vorteil einer optimierten Version. Wenn mein Code also nur Arrays der Länge 64 verwendet, kann ich optimierte Ausarbeitungen der Bibliotheksfunktionen für die Länge 64 verwenden.

In diesem speziellen Fall hängt dies von der Häufigkeitsverteilung der Array-Längen ab, da wir möglicherweise nicht alle möglichen Funktionen ausarbeiten möchten, wenn es aufgrund von Speicherbeschränkungen und Seiten-Cache-Löschen, die die Dinge verlangsamen könnten, Tausende von Funktionen gibt. Wenn zum Beispiel kleine Größen üblich sind, aber größere möglich sind (eine Long-Tail-Verteilung in der Größe), dann können wir spezialisierte Funktionen für die kleinen ganzen Zahlen mit entrollten Schleifen (z. B. 1 bis 64) ausarbeiten und dann eine einzige verallgemeinerte Version mit einem Versteck bereitstellen -Parameter für den Rest.

Ich mag die Idee des "willkürlich schlauen Compilers" nicht und halte das für ein schlechtes Argument. Wie lange muss ich auf diesen willkürlich schlauen Compiler warten? Ich mag besonders die Idee nicht, dass der Compiler Typen ändert, zum Beispiel ein Slice zu einem Array optimiert und versteckte Spezialisierungen in einer Sprache mit Reflektion macht, da wenn Sie über dieses Slice nachdenken, etwas Unerwartetes passieren könnte.

In Bezug auf das "allgemeine Dilemma" würde ich persönlich "den Compiler langsamer machen / mehr Arbeit erledigen", aber versuchen, es so schnell wie möglich zu machen, indem Sie eine gute Implementierung und eine separate Kompilierung verwenden. Rust scheint recht gut abzuschneiden, und nach Intels jüngster Ankündigung scheint es, als könnte es schließlich „C“ als Hauptprogrammiersprache für Systeme ersetzen. Die Kompilierzeit schien bei Intels Entscheidung nicht einmal ein Faktor zu sein, da der Laufzeitspeicher und die Parallelitätssicherheit mit "C"-ähnlicher Geschwindigkeit die Schlüsselfaktoren zu sein schienen. Rusts "Traits" sind eine vernünftige Implementierung generischer Typklassen, sie haben einige lästige Eckfälle, die meiner Meinung nach von ihrem Typsystemdesign herrühren.

Um auf unsere frühere Diskussion zurückzukommen, muss ich darauf achten, die Diskussion über Generika im Allgemeinen und darüber, wie sie sich speziell auf Go beziehen könnten, zu trennen. Daher bin ich mir nicht sicher, ob Go überhaupt Generika haben sollte, da es eine einfache und elegante Sprache verkompliziert, ähnlich wie 'C' keine Generika hat. Ich denke immer noch, dass es eine Marktlücke für eine Sprache gibt, die generische Implementierungen als Kernfunktion hat, aber einfach und elegant bleibt.

Ich frage mich, ob es diesbezüglich Fortschritte gibt.

Wie lange kann ich Generika ausprobieren. Ich warte schon lange

@Nsgj Sie können diese CL auschecken: https://go-review.googlesource.com/c/go/+/187317/

Ist dies in der aktuellen Spezifikation möglich?

contract Point(T) {
  T struct { X, Y float64 }
}

Mit anderen Worten, der Typ muss eine Struktur mit zwei Feldern, X und Y, vom Typ float64 sein.

Bearbeiten: mit Beispielverwendung

func generate(type T Point)() T {
  return T{X: randomFloat64(), Y: randomFloat64()}
}

@abuchanan-nr Ja, der aktuelle Designentwurf würde das zulassen, obwohl es schwer zu erkennen ist, wie nützlich es wäre.

Ich bin mir auch nicht sicher, ob es nützlich ist, aber ich habe kein klares Beispiel für die Verwendung eines benutzerdefinierten Strukturtyps in einer Typenliste eines Vertrags gesehen. Die meisten Beispiele verwenden eingebaute Typen.

FWIW, ich stellte mir eine 2D-Grafikbibliothek vor. Möglicherweise möchten Sie, dass jeder Scheitelpunkt eine Reihe von anwendungsspezifischen Feldern wie Farbe, Kraft usw. enthält. Möglicherweise möchten Sie jedoch auch eine generische Bibliothek mit Methoden und Algorithmen nur für den Geometrieteil, der sich nur auf X-, Y-Koordinaten stützt. Es könnte nett sein, Ihren benutzerdefinierten Scheitelpunkttyp an diese Bibliothek zu übergeben, z

type MyVertex struct {
  X, Y float64
  Color color.Color
  OtherAttr int
}
p := geo.RandomPolygon(MyVertex)()

for _, vert := range p.Vertices() {
  p.Color = randColor()
}

Auch hier bin ich mir nicht sicher, ob sich das in der Praxis als gutes Design herausstellt, aber da war meine Vorstellungskraft damals :)

Unter https://godoc.org/image#Image erfahren Sie, wie dies heute im standardmäßigen Go durchgeführt wird.

In Bezug auf Betreiber/Arten in Verträgen :

Dies führt zu einer Duplizierung vieler generischer Methoden, da wir sie im Operatorformat ( + , == , < , ...) und Methodenformat ( Plus(T) T , Equal(T) bool , LessThan(T) bool , ...).

Ich schlage vor, wir vereinen diese beiden Ansätze in einem, dem Methodenformat. Um dies zu erreichen, müssten die vordeklarierten Typen ( int , int64 , string , ...) mit beliebigen Methoden in Typen umgewandelt werden. Für den (trivialen) einfachen Fall ist das schon möglich ( type MyInt int; func (i MyInt) LessThan(o MyInt) bool {return int(i) < int(o)} ), aber der eigentliche Wert liegt in zusammengesetzten Typen ( []int -> []MyInt , map[int]struct{} -> map[MyInt]struct{} , usw. für Kanal, Zeiger, ...), was nicht erlaubt ist (siehe FAQ ). Das Zulassen dieser Konvertierungen ist an sich schon eine bedeutende Änderung, daher habe ich die technischen Einzelheiten in Relaxed Type Conversion Proposal erweitert. Das würde es generischen Funktionen ermöglichen, sich nicht mit Operatoren zu befassen und dennoch alle Typen zu unterstützen, einschließlich vorab deklarierter.

Beachten Sie, dass diese Änderung auch nicht vordeklarierten Typen zugute kommt. Unter dem aktuellen Vorschlag, gegeben type X struct{S string} (das aus einer externen Bibliothek stammt, sodass Sie keine Methoden hinzufügen können), sagen Sie, Sie haben ein []X und möchten es an eine generische Funktion übergeben erwartet []T , für T die Erfüllung des Stringer Vertrages. Das würde type X2 X; func(x X2) String() string {return x.S} und eine tiefe Kopie von []X in []X2 erfordern. Unter diesen vorgeschlagenen Änderungen an diesem Vorschlag speichern Sie die tiefe Kopie vollständig.

HINWEIS : Der erwähnte gelockerte Typumwandlungsvorschlag erfordert eine Herausforderung.

@JavierZunzunegui Das Bereitstellen eines "Methodenformats" (oder Operatorformats) für einfache unäre/binäre Operatoren ist nicht das Problem. Es ist ziemlich einfach, Methoden wie +(x int) int einzuführen, indem man einfach Operatorsymbole als Methodennamen zulässt, und dies auf eingebaute Typen zu erweitern (obwohl selbst dies für Verschiebungen zusammenbricht, da der rechte Operator sein kann ein beliebiger Integer-Typ - wir haben im Moment keine Möglichkeit, dies auszudrücken). Das Problem ist, dass das nicht ausreicht. Eines der Dinge, die ein Vertrag ausdrücken muss, ist, ob ein Wert x vom Typ X in den Typ eines Typparameters T wie in T(x) konvertiert werden kann c einer Variablen vom Typ Parametertyp T zugewiesen (oder in diese konvertiert) werden kann: Ist es zulässig, sagen wir, 256 bis t vom Typ T ? Was ist, wenn T gleich byte ist? Es gibt noch ein paar mehr solcher Dinge. Man kann für diese Dinge eine "Methodenformat" -Notation erfinden, aber es wird schnell kompliziert, und es ist nicht klar, ob es verständlicher oder lesbarer ist.

Ich sage nicht, dass es nicht geht, aber wir haben noch keinen zufriedenstellenden und klaren Ansatz gefunden. Der aktuelle Designentwurf, der einfach Typen aufzählt, ist dagegen ziemlich einfach zu verstehen.

@griesemer Das mag in Go aufgrund anderer Prioritäten schwierig sein, aber im Allgemeinen ist es ein ziemlich gut gelöstes Problem. Das ist einer der Gründe, warum ich implizite Konvertierungen für schlecht halte. Es gibt andere Gründe, wie z. B. magische Ereignisse, die für jemanden, der den Code liest, nicht sichtbar sind.

Wenn im Typsystem keine impliziten Konvertierungen vorhanden sind, kann ich das Überladen verwenden, um den Bereich der akzeptierten Typen genau zu steuern, und Schnittstellen steuern das Überladen.

Ich würde dazu neigen, die Ähnlichkeit zwischen Typen mithilfe von Schnittstellen auszudrücken, daher würden Operationen wie „+“ generisch als Operationen auf einer numerischen Schnittstelle und nicht als Typ ausgedrückt. Sie müssen sowohl Typvariablen als auch Schnittstellen haben, um die Einschränkung auszudrücken, dass sowohl die Argumente für als auch das Ergebnis der Addition denselben Typ haben müssen.

Hier wird also der Additionsoperator deklariert, um über Typen mit einer numerischen Schnittstelle zu arbeiten. Das passt gut zur Mathematik, wo zum Beispiel „Ganzzahlen“ und „Addition“ eine „Gruppe“ bilden.

Sie würden am Ende so etwas wie:

+(T Addable)(x T, y T) T

Wenn Sie die implizite Schnittstellenauswahl zulassen, kann der '+'-Operator nur eine Methode der numerischen Schnittstelle sein, aber ich denke, das würde Probleme mit der Methodenauswahl in Go verursachen?

@griesemer zu deinem Punkt zu Conversions:

Ein Vertrag muss unter anderem ausdrücken, ob ein Wert x vom Typ X in den Typ eines Typparameters T wie in T(x) konvertiert werden kann (und umgekehrt). Das heißt, man muss ein "Methodenformat" für zulässige Konvertierungen erfinden

Ich kann sehen, wie das eine Komplikation wäre, aber ich denke nicht, dass es nötig ist. So wie ich es sehe, würden solche Konvertierungen außerhalb des generischen Codes durch den Aufrufer erfolgen. Ein Beispiel (unter Verwendung von Stringify gemäß dem Entwurfsdesign):

Stringify(int)([]int{1,2}) // does not compile
type MyInt int
func (i MyInt) String() string {...}
Stringify(MyInt)([]MyInt([]int{1,2})) // OK. Generic type MyInt could be inferred

Oben, was Stringify betrifft, ist das Argument vom Typ []MyInt und erfüllt den Vertrag. Generischer Code kann generische Typen nicht in etwas anderes konvertieren (außer Schnittstellen, die sie gemäß dem Vertrag implementieren), gerade weil ihr Vertrag nichts darüber aussagt.

@JavierZunzunegui Ich sehe nicht, wie der Anrufer solche Konvertierungen durchführen kann, ohne sie in der Schnittstelle/im Vertrag offenzulegen. Zum Beispiel möchte ich vielleicht einen generischen numerischen Algorithmus (eine parametrisierte Funktion) implementieren, der mit verschiedenen Ganzzahl- oder Gleitkommatypen arbeitet. Als Teil dieses Algorithmus muss der Funktionscode den Werten des Parametertyps T konstante Werte c1 , c2 usw. zuweisen. Ich sehe nicht, wie der Code dies tun kann, ohne zu wissen, dass es in Ordnung ist, diese Konstanten einer Variablen vom Typ T zuzuweisen. (Man möchte diese Konstanten sicherlich nicht an die Funktion übergeben müssen.)

func NumericAlgorithm(type T SomeContract)(vector []T) T {
   ...
   vector[i] = 3.1415  // <<< how do we know this is valid without the contract telling us?
   ...
}

muss Werten des Parametertyps T konstante Werte c1 , c2 usw. zuweisen

@griesemer Ich würde (aus meiner Sicht, wie Generika sind / sein sollten) sagen, dass das obige die falsche Problemstellung ist. Sie verlangen, dass T als float32 definiert wird, aber ein Vertrag gibt nur an, welche Methoden T zur Verfügung stehen, nicht wie es definiert ist. Wenn Sie dies benötigen, können Sie entweder vector als []T beibehalten und ein func(float32) T -Argument ( vector[i] = f(c1) ) benötigen oder viel besser vector . []float32 und erfordern T vertraglich eine Methode DoSomething(float32) oder DoSomething([]float32) , da ich davon ausgehe, dass T und die Schwimmer müssen irgendwann interagieren. Das bedeutet, dass T als type T float32 definiert werden kann oder nicht, wir können nur sagen, dass es die vom Vertrag geforderten Methoden hat.

@JavierZunzunegui Ich sage überhaupt nicht, dass T als float32 definiert werden - es könnte ein float32 , ein float64 oder sogar einer der sein komplexe Typen. Allgemeiner gesagt, wenn die Konstante eine ganze Zahl wäre, könnte es eine Vielzahl von ganzzahligen Typen geben, die gültig wären, um an diese Funktion übergeben zu werden, und einige, die es nicht sind. Es ist sicherlich keine "falsche Problemstellung". Das Problem ist real - es ist sicherlich überhaupt nicht erfunden, solche Funktionen schreiben zu wollen - und das Problem verschwindet nicht, indem man es für "falsch" erklärt.

@griesemer Ich verstehe, ich dachte, Sie würden sich nur mit der Konvertierung befassen, ich habe das Schlüsselelement nicht registriert, dass es sich um nicht typisierte Konstanten handelt.

Sie können wie in meiner obigen Antwort vorgehen, wobei T eine Methode DoSomething(X) hat und die Funktion ein zusätzliches Argument func(float64) X , sodass die generische Form durch zwei Typen definiert wird ( T,X ). Die Art und Weise, wie Sie das Problem X beschreiben, ist normalerweise float32 oder float64 und das Funktionsargument ist func(f float64) float32 {return float32(f)} oder func(f float64) float64 {return f} .

Noch wichtiger ist, wie Sie hervorheben, für den Ganzzahlfall das Problem, dass weniger genaue Ganzzahlformate für eine bestimmte Konstante möglicherweise nicht ausreichen. Der sicherste Ansatz besteht darin, die generische Funktion mit zwei Typen ( T,X ) privat zu halten und nur MyFunc32 / MyFunc64 /etc öffentlich verfügbar zu machen.

Ich gebe zu, dass MyFunc32(int32) / MyFunc64(int64) /etc. ist weniger praktisch als ein einzelnes MyFunc(type T Numeric) (das Gegenteil ist nicht zu rechtfertigen!). Dies gilt jedoch nur für generische Implementierungen, die sich auf eine Konstante und hauptsächlich eine ganzzahlige Konstante verlassen - wie viele davon gibt es? Für den Rest erhalten Sie die zusätzliche Freiheit, nicht auf ein paar eingebaute Typen für T beschränkt zu sein.

Und natürlich, wenn die Funktion nicht teuer ist, könnten Sie vollkommen in Ordnung sein, wenn Sie die Berechnung als int64 / float64 durchführen und nur dies offenlegen und es sowohl einfach als auch uneingeschränkt auf T halten.

Wir können den Leuten wirklich nicht sagen "Sie können generische Funktionen für jeden Typ T schreiben, aber diese generischen Funktionen dürfen keine untypisierten Konstanten verwenden." Go ist vor allem eine einfache Sprache. Sprachen mit solchen bizarren Beschränkungen sind nicht einfach.

Jedes Mal, wenn es schwierig wird, einen vorgeschlagenen Ansatz für Generika auf einfache Weise zu erklären, müssen wir diesen Ansatz verwerfen. Es ist wichtiger, die Sprache einfach zu halten, als der Sprache Generika hinzuzufügen.

@JavierZunzunegui Eine der interessanten Eigenschaften von parametrisiertem (generischem) Code ist, dass der Compiler ihn basierend auf den Typen anpassen kann, mit denen der Code instanziiert wird. Beispielsweise könnte man lieber einen byte -Typ anstelle von int verwenden, da dies zu erheblichen Platzeinsparungen führt (stellen Sie sich eine Funktion vor, die riesige Slices des generischen Typs zuweist). Daher ist es eine unbefriedigende Antwort, den Code einfach auf einen Typ zu beschränken, der „groß genug“ ist, selbst für eine „meinungsstarke“ Sprache wie Go.

Darüber hinaus geht es nicht nur um Algorithmen, die "große" nicht typisierte Konstanten verwenden, die möglicherweise nicht so häufig vorkommen: Solche Algorithmen mit der Frage "wie viele davon gibt es überhaupt" abzutun, ist einfach eine Handbewegung, um ein vorhandenes Problem abzulenken. Nur zur Überlegung: Es scheint für eine große Anzahl von Algorithmen nicht unvernünftig zu sein, ganzzahlige Konstanten wie -1, 0, 1 zu verwenden. Beachten Sie, dass man -1 nicht in Verbindung mit untypisierten Ganzzahlen verwenden könnte, nur um Ihnen ein einfaches Beispiel zu geben. Das können wir natürlich nicht einfach ignorieren. Wir müssen dies in einem Vertrag festlegen können.

@ianlancetaylor @griesemer danke für das Feedback - ich sehe, dass meine vorgeschlagene Änderung einen erheblichen Konflikt mit nicht typisierten Konstanten und negativen Ganzzahlen aufweist, ich werde es hinter mir lassen.

Kann ich Ihre Aufmerksamkeit auf den zweiten Punkt in https://github.com/golang/go/issues/15292#issuecomment -546313279 lenken:

Beachten Sie, dass diese Änderung auch nicht vordeklarierten Typen zugute kommt. Sagen Sie unter dem aktuellen Vorschlag, gegebener Typ X struct{S string} (der aus einer externen Bibliothek stammt, sodass Sie ihm keine Methoden hinzufügen können), sagen Sie, Sie haben ein []X und möchten es an eine generische Funktion übergeben, die [ ]T, für T, das den Stringer-Vertrag erfüllt. Das würde einen Typ X2 X erfordern; func(x X2) String() string {return xS} und eine tiefe Kopie von []X in []X2. Unter diesen vorgeschlagenen Änderungen an diesem Vorschlag speichern Sie die tiefe Kopie vollständig.

Die Lockerung der Umrechnungsregeln (sofern technisch machbar) wäre weiterhin sinnvoll.

@JavierZunzunegui Das Diskutieren von Konvertierungen der Art []B([]A) wenn B(a) (mit a vom Typ A ) erlaubt ist, scheint größtenteils orthogonal zu generischen Funktionen zu sein. Ich denke, wir müssen das hier nicht einbringen.

@ianlancetaylor Ich bin mir nicht sicher, wie relevant das für Go ist, aber ich glaube nicht, dass Konstanten wirklich untypisiert sind, sie müssen einen Typ haben, da der Compiler eine Maschinendarstellung auswählen muss. Ich denke, ein besserer Begriff sind Konstanten unbestimmten Typs, da die Konstante durch mehrere verschiedene Typen darstellbar sein kann. Eine Lösung besteht darin, einen Union-Typ zu verwenden, sodass eine Konstante wie 27 einen Typ wie int16|int32|float16|float32 hätte, eine Vereinigung aller möglichen Typen . Dann kann T in einem generischen Typ dieser Vereinigungstyp sein. Die einzige Voraussetzung ist, dass wir irgendwann die Vereinigung in einen einzigen Typ auflösen müssen. Der problematischste Fall wäre so etwas wie print(27) , da es nie einen einzigen Typ gibt, in den aufgelöst werden kann, in solchen Fällen würde jeder Typ in der Vereinigung ausreichen, und wir könnten basierend auf einem Optimierungsparameter wie Speicherplatz/Geschwindigkeit usw. auswählen .

@keean Der genaue Name und die Handhabung dessen, was die Spezifikation als "nicht typisierte Konstanten" bezeichnet, ist für dieses Problem kein Thema. Lassen Sie uns diese Diskussion bitte an anderer Stelle führen. Danke.

@ianlancetaylor Gerne , aber dies ist einer der Gründe, warum ich denke, dass Go keine saubere/einfache generische Implementierung haben kann, all diese Probleme sind miteinander verbunden und die ursprünglichen Entscheidungen für Go wurden nicht im Hinblick auf die generische Programmierung getroffen. Ich denke, dass eine andere Sprache benötigt wird, die darauf ausgelegt ist, Generika durch Design einfach zu machen, für Go werden Generika später immer etwas zur Sprache hinzugefügt, und die beste Option, um die Sprache sauber und einfach zu halten, besteht möglicherweise darin, sie überhaupt nicht zu haben.

Wenn ich heute eine einfache Sprache mit schnellen Kompilierzeiten und vergleichbarer Flexibilität entwerfen würde, würde ich Methodenüberladung und strukturelle Polymorphie (Subtyping) über Golang-Schnittstellen und keine Generika wählen. Tatsächlich würde es das Überladen verschiedener anonymer Schnittstellen mit verschiedenen Feldern ermöglichen.

Die Wahl von Generika hat den Vorteil der sauberen Wiederverwendbarkeit von Code, führt jedoch zu mehr Rauschen, was kompliziert wird, wenn Einschränkungen hinzugefügt werden, was manchmal zu kaum verständlichem Code führt.
Wenn wir dann Generika haben, warum nicht ein erweitertes Beschränkungssystem wie eine Where-Klausel, höherwertige Typen oder vielleicht höherrangige Typen und auch abhängige Typisierung verwenden?
All diese Fragen werden irgendwann auftauchen, wenn Sie früher oder später Generika einführen.

Um es klar zu sagen, ich bin nicht gegen Generika, aber ich überlege, ob es der richtige Weg ist, um die Einfachheit von Go zu erhalten.

Wenn die Einführung von Generika in go unvermeidlich ist, dann wäre es vernünftig, über die Auswirkungen auf die Kompilierzeiten nachzudenken, wenn generische Funktionen monomorphisiert werden.
Wäre es nicht ein guter Standard, Generika zu verpacken, dh eine Kopie für alle Eingabetypen zusammen zu erstellen und sich nur zu spezialisieren, wenn der Benutzer dies ausdrücklich mit einigen Anmerkungen auf der Definitions- oder Aufrufseite fordert?

In Bezug auf die Auswirkungen auf die Laufzeitleistung würde dies die Leistung aufgrund des Boxing/Unboxing-Problems verringern, andernfalls gibt es C++-Ingenieure auf Expertenebene, die Boxing-Generika wie Java empfehlen, um Cache-Fehler zu mindern.

@ianlancetaylor @griesemer Ich habe das Problem der nicht typisierten Konstanten und „Nicht-Operator“-Generika (https://github.com/golang/go/issues/15292#issuecomment-547166519) neu überdacht und einen besseren Weg gefunden, damit umzugehen damit.

Geben Sie die nummetischen Typen ( type MyInt32 int32 , type MyInt64 int64 , ...) an, diese haben viele Methoden, die denselben Vertrag erfüllen ( Add(T) T , ...), andere jedoch nicht würde func(MyInt64) FromI64(int64) MyInt64 überlaufen riskieren, aber nein ~ func(MyInt32) FromI64(int64) MyInt32 ~. Dies ermöglicht die sichere Verwendung numerischer Konstanten (die explizit dem niedrigsten erforderlichen Genauigkeitswert zugewiesen sind) (1) , da numerische Typen mit niedriger Genauigkeit den erforderlichen Vertrag nicht erfüllen, alle höheren jedoch. Siehe Spielplatz , wobei Schnittstellen anstelle von Generika verwendet werden.

Ein Vorteil der Entspannung numerischer Generika über die eingebauten Typen hinaus (nicht spezifisch für diese neueste Revision, also hätte ich sie letzte Woche teilen sollen) ist, dass sie die Instanziierung generischer Methoden mit Typen zur Überprüfung des Überlaufs ermöglicht - siehe Spielplatz . Overflow-Checking ist selbst ein sehr beliebter Wunsch/Vorschlag (https://github.com/golang/go/issues/31500 und verwandte Themen).


(1) : Die Kompilierzeit-Garantie ohne Überlauf für nicht typisierte Konstanten ist stark innerhalb desselben 'Zweigs' ( int[8/16/32/64] und uint[8/16/32/64] ). Beim Überqueren von Zweigen wird eine uint[X] -Konstante nur sicher in int[2X+] instanziiert, und eine int[X] -Konstante kann überhaupt nicht sicher instanziiert werden durch uint[X] . Sogar diese zu lockern ( int[X]<->uint[X] zuzulassen) wäre nach einigen Mindeststandards einfach und sicher, und entscheidend ist, dass jede Komplexität auf den Autor des generischen Codes fällt, nicht auf den Benutzer des generischen Codes (der sich nur um den Vertrag kümmert). , und kann davon ausgehen, dass jeder numerische Typ, der diesem entspricht, gültig ist).

Generische Methoden - war der Untergang von Java!

@ianlancetaylor Gerne , aber dies ist einer der Gründe, warum ich denke, dass Go keine saubere/einfache generische Implementierung haben kann, all diese Probleme sind miteinander verbunden und die ursprünglichen Entscheidungen für Go wurden nicht im Hinblick auf die generische Programmierung getroffen. Ich denke, dass eine andere Sprache benötigt wird, die darauf ausgelegt ist, Generika durch Design einfach zu machen, für Go werden Generika später immer etwas zur Sprache hinzugefügt, und die beste Option, um die Sprache sauber und einfach zu halten, besteht möglicherweise darin, sie überhaupt nicht zu haben.

Ich stimme zu 100% zu%. So sehr ich es lieben würde, eine Art von Generika implementiert zu sehen, denke ich, dass das, was Sie gerade kochen, die Einfachheit der Go-Sprache zerstören wird.

Die aktuelle Idee zur Erweiterung von Schnittstellen sieht so aus:

type I1(type P1) interface {
        m1(x P1)
}

type I2(type P1, P2) interface {
        m2(x P1) P2
        type int, float64
}

func f(type P1 I1(P1), P2 I2(P1, P2)) (x P1, y P2) P2

Tut mir leid, aber bitte tut das nicht! Es macht die Schönheit von Go big time hässlich.

Nachdem ich jetzt fast 100.000 Zeilen Go-Code geschrieben habe, bin ich damit einverstanden, keine Generika zu haben.

Allerdings Kleinigkeiten wie Unterstützung

// Allow mulitple types in Slices and Maps declarations
func Reverse(s []<int,string>) {
    first := 0
    last := len(s) - 1
    for first < last {
        s[first], s[last] = s[last], s[first]
        first++
        last--
    }
}

//  Allow multiple types in variable declarations
func Index (s <string, []byte>, b byte) int {
    for i := 0; i < len(s); i++ {
        if s[i] == b {
            return i
        }
    }
    return -1
}

// Allow slices and maps declarations with interface values
func ToStrings (s []Stringer) []string {
    r := make([]string, len(s))
    for i, v := range s {
        r[i] = v.String()
    }
    return r
}

würde helfen.

Syntaxvorschlag, um Generika vollständig von regulärem Go-Code trennen zu können

package graph

// Example how you would define generics completely separat from Go 1 code
contract (Node, Edge)G {
    Node Edges() []Edge
    Edge Nodes() (from, to Node)
}

type (type Node, Edge G) ( Graph )
func (type Node, Edge G) ( New )
const _ = (Node, Edge) Graph

// Unmodified Go 1 code
type Graph struct { ... }
func New(nodes []Node) *Graph { ... }
func (g *Graph) ShortestPath(from, to Node) []Edge { ... }

@martinrode Allerdings Kleinigkeiten wie Unterstützung
... mehrere Typen in Slices- und Maps-Deklarationen zulassen

Dies erfüllt nicht die Anforderungen einiger funktionaler generischer Slice-Funktionen, z. B. head() , tail() , map(slice, func) , filter(slice, func)

Sie könnten das einfach selbst für jedes Projekt schreiben, in dem Sie es benötigen, aber an diesem Punkt besteht die Gefahr, dass es aufgrund von Wiederholungen beim Kopieren und Einfügen veraltet wird, und fördert die Komplexität des Go-Codes, um die Einfachheit der Sprache zu sparen.

(Auf persönlicher Ebene ist es auch etwas ermüdend zu wissen, dass ich eine Reihe von Funktionen habe, die ich implementieren möchte, und keine saubere Möglichkeit habe, diese auszudrücken, ohne auch auf sprachliche Einschränkungen zu reagieren.)

Berücksichtigen Sie Folgendes in aktuellem, nicht generischem go:

Ich habe eine Variable x vom Typ externallib.Foo , die ich aus einer Bibliothek externallib erhalten habe, die ich nicht kontrolliere.
Ich möchte es an eine Funktion SomeFunc(fmt.Stringer) übergeben, aber externallib.Foo hat keine Methode String() string . Ich kann einfach:

type MyFoo externallib.Foo
func (mf MyFoo) String() string {...}
// ...
SomeFunc(MyFoo(x))

Betrachten Sie dasselbe mit Generika.

Ich habe eine Variable x vom Typ []externallib.Foo . Ich möchte es an AnotherFunc(type T Stringer)(s []T) übergeben. Es geht nicht ohne ein teures tiefes Kopieren des Slice in ein neues []MyFoo . Wenn es sich anstelle eines Slice um einen komplexeren Typ handelt (z. B. einen Kanal oder eine Karte) oder das Verfahren den Empfänger modifiziert, wird es noch ineffizienter und mühsamer, wenn dies überhaupt möglich ist.

Dies ist möglicherweise kein Problem innerhalb der Standardbibliothek, aber das liegt nur daran, dass es keine externen Abhängigkeiten gibt. Das ist ein Luxus, den so gut wie kein anderes Projekt haben wird.

Mein Vorschlag ist, die Konvertierung zu lockern, um []Foo([]Bar{}) für alle Foo zuzulassen, die als type Foo Bar definiert sind, oder umgekehrt, und gleichermaßen für Karten, Arrays, Kanäle und Zeiger rekursiv. Beachten Sie, dass dies alles billige flache Kopien sind. Weitere technische Details finden Sie im Relaxed Type Conversion Proposal .


Dies wurde zuerst als sekundäres Feature in https://github.com/golang/go/issues/15292#issuecomment -546313279 angesprochen.

@JavierZunzunegui Ich glaube nicht, dass das wirklich etwas mit Generika zu tun hat. Ja, Sie können ein Beispiel unter Verwendung von Generika bereitstellen, aber Sie können ein ähnliches Beispiel ohne die Verwendung von Generika bereitstellen. Ich denke, diese Frage sollte separat diskutiert werden, nicht hier. Siehe auch https://golang.org/doc/faq#convert_slice_with_same_underlying_type. Danke.

Ohne Generika hat eine solche Konvertierung so gut wie keinen Wert, da []Foo im Allgemeinen keine Schnittstelle treffen wird, oder zumindest keine Schnittstelle, die davon Gebrauch macht, ein Slice zu sein. Die Ausnahme sind Schnittstellen, die ein sehr spezifisches Muster haben, um sie zu verwenden, wie sort.Interface , für die Sie das Slice sowieso nicht konvertieren müssen.

Die nicht-generische Version des Obigen ( func AnotherFunc(type T Stringer)(s []T) ) ist

type SliceOfStringers interface {
  Len() int
  Get(int) fmt.Stringer
}
func AnotherFunc(s SliceOfStringers) {...}

Es mag weniger praktisch sein als der generische Ansatz, aber es kann so gemacht werden, dass es jeden Slice gut handhabt und dies tut, ohne ihn zu kopieren, unabhängig davon, ob der zugrunde liegende Typ tatsächlich ein fmt.Stringer ist. So wie es aussieht, können Generika das nicht, obwohl sie im Prinzip ein viel geeigneteres Werkzeug für den Job sind. Und sicherlich, wenn wir Generika hinzufügen, ist es genau das, um Slices, Maps usw. in APIs häufiger zu machen und sie mit weniger Boilerplate zu manipulieren. Dennoch führen sie ein neues Problem ein, ohne Äquivalenz in einer reinen Schnittstellenwelt, das _vielleicht_ nicht einmal unvermeidlich ist, sondern künstlich durch die Sprache auferlegt wird.

Die von Ihnen erwähnte Typkonvertierung kommt oft genug in nicht generischem Code vor, dass es sich um eine FAQ handelt. Lassen Sie uns diese Diskussion bitte woanders hin verschieben. Danke.

Was ist der Stand davon? Irgendein AKTUALISIERTER Entwurf? Seitdem warte ich auf Generika
vor fast 2 jahren. Wann werden wir Generika haben?

El März, 4 de feb. de 2020 um 13:28 Uhr, Ian Lance Taylor (
[email protected]) Beschreibung:

Die von Ihnen erwähnte Typkonvertierung kommt oft genug in nicht generischem Code vor
dass es sich um eine FAQ handelt. Lassen Sie uns diese Diskussion bitte woanders hin verschieben. Danke.


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/15292?email_source=notifications&email_token=AJMFNBN3MFHDMENAFXIKBLDRBGXUTA5CNFSM4CA35RX2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEKYV5RI#issuecomment-582049477 ,
oder abbestellen
https://github.com/notifications/unsubscribe-auth/AJMFNBO5UTKNPL3MSA3NESLRBGXUTANCNFSM4CA35RXQ
.

--
Dies ist ein Test für E-Mail-Signaturen, die in TripleMint verwendet werden sollen

Wir arbeiten daran. Manche Dinge brauchen Zeit.

Wird die Arbeit offline erledigt? Ich würde gerne sehen, wie es sich im Laufe der Zeit so entwickelt, dass die "allgemeine Öffentlichkeit" wie ich keinen Kommentar abgeben kann, um Lärm zu vermeiden.

Obwohl es inzwischen geschlossen wurde, um die Generika-Diskussion an einem Ort zu halten, sehen Sie sich #36177 an, wo @Griesemer auf einen Prototyp verweist, an dem er arbeitet, und einige interessante Kommentare zu seinen bisherigen Gedanken zu diesem Thema macht.

Ich glaube, ich liege richtig, wenn ich sage, dass der Prototyp derzeit nur die Typprüfungsaspekte des Entwurfs des „Vertrags“-Vorschlags behandelt, aber die Arbeit klingt für mich auf jeden Fall vielversprechend.

@ianlancetaylor Jedes Mal, wenn es schwierig wird, einen vorgeschlagenen Ansatz für Generika auf einfache Weise zu erklären, müssen wir diesen Ansatz verwerfen. Es ist wichtiger, die Sprache einfach zu halten, als der Sprache Generika hinzuzufügen.

Das ist ein großartiges Ideal, das man anstreben sollte, aber in Wirklichkeit ist Softwareentwicklung manchmal von Natur aus nicht _einfach zu erklären_.

Wenn die Sprache daran gehindert wird, solche _nicht einfach auszudrückenden_ Ideen auszudrücken, müssen Softwareingenieure diese Möglichkeiten immer wieder neu erfinden, weil diese verdammten _schwer auszudrückenden_ Ideen manchmal wesentlich für die Programmlogik sind.

Schauen Sie sich Istio, Kubernetes, operator-sdk und in gewissem Umfang Terraform und sogar die protobuf-Bibliothek an. Sie alle entkommen dem Go-Typsystem, indem sie Reflektion verwenden, ein neues Typsystem auf Go implementieren, indem sie Schnittstellen und Codegenerierung verwenden, oder eine Kombination davon.

@omeid

Schauen Sie sich Istio, Kubernetes an

Ist Ihnen jemals in den Sinn gekommen, dass der Grund, warum sie dieses absurde Zeug machen, darin besteht, dass ihr Kerndesign keinen Sinn ergibt und sie daher zu den reflect -Spielen führen mussten, um es zu erfüllen? ?

Ich behaupte, dass bessere Designs für Golang-Programme (sowohl in der Designphase als auch in der API) keine Generika _erfordern_.

Bitte fügen Sie sie nicht zu golang hinzu.

Programmieren ist schwer. Kubelet ist ein dunkler Ort. Generika spalten die Menschen mehr als die amerikanische Politik. Ich möchte glauben.

Wenn die Sprache daran gehindert wird, solche nicht einfach auszudrückenden Ideen auszudrücken, müssen Softwareingenieure diese Möglichkeiten immer wieder neu erfinden, weil diese verdammt schwer auszudrückenden Ideen manchmal für die Programmlogik wesentlich sind.

Schauen Sie sich Istio, Kubernetes, operator-sdk und in gewissem Umfang Terraform und sogar die protobuf-Bibliothek an. Sie alle entkommen dem Go-Typsystem, indem sie Reflektion verwenden, ein neues Typsystem auf Go implementieren, indem sie Schnittstellen und Codegenerierung verwenden, oder eine Kombination davon.

Ich finde das kein überzeugendes Argument. Die Go-Sprache sollte idealerweise leicht zu lesen, zu schreiben und zu verstehen sein, während es dennoch möglich ist, beliebig komplexe Operationen auszuführen. Das stimmt mit dem überein, was Sie sagen: Die Tools, die Sie erwähnen, müssen etwas Komplexes tun, und Go gibt ihnen eine Möglichkeit, dies zu tun.

Die Go-Sprache sollte idealerweise leicht zu lesen, zu schreiben und zu verstehen sein, während es dennoch möglich ist, beliebig komplexe Operationen auszuführen.

Ich stimme dem zu, aber da es sich um mehrere Ziele handelt, werden sie manchmal miteinander in Spannung geraten. Code, der von Natur aus in einem generischen Stil geschrieben werden „möchte“, wird oft weniger leicht lesbar, als es sonst der Fall wäre, wenn er auf Techniken wie Reflektion zurückgreifen muss.

Code, der von Natur aus in einem generischen Stil geschrieben werden „möchte“, wird oft weniger leicht lesbar, als es sonst der Fall wäre, wenn er auf Techniken wie Reflektion zurückgreifen muss.

Deshalb bleibt dieser Vorschlag offen und wir haben einen Designentwurf für eine mögliche Implementierung von Generika (https://blog.golang.org/why-generics).

Schauen Sie sich ... sogar die Protobuf-Bibliothek an. Sie alle entkommen dem Go-Typsystem, indem sie Reflektion verwenden, ein neues Typsystem auf Go implementieren, indem sie Schnittstellen und Codegenerierung verwenden, oder eine Kombination davon.

Aus Erfahrung mit Protobufs sprechend, gibt es einige Fälle, in denen Generika die Benutzerfreundlichkeit und/oder Implementierung der API verbessern können, aber die überwiegende Mehrheit der Logik wird nicht von Generika profitieren . Generics setzen voraus, dass konkrete Typinformationen zur Kompilierzeit bekannt sind . Bei Protobufs betreffen die meisten Situationen Fälle, in denen die Typinformationen nur zur Laufzeit bekannt sind.

Im Allgemeinen bemerke ich, dass die Leute oft auf jegliche Verwendung von Reflexion hinweisen und dies als Beweis für die Notwendigkeit von Generika behaupten. Es ist nicht so einfach. Ein entscheidender Unterschied besteht darin, ob die Typinformationen zur Kompilierzeit bekannt sind oder nicht. In einer Reihe von Fällen ist dies grundsätzlich nicht der Fall.

@dsnet Interessanter Dank, ich habe nie darüber nachgedacht, dass protobuf nicht generisch kompatibel ist. Immer davon ausgegangen, dass jedes Tool, das Boilerplate-Go-Code wie zum Beispiel protoc basierend auf einem vordefinierten Schema generiert, in der Lage wäre, generischen Code ohne Reflexion mit dem aktuellen generischen Vorschlag zu generieren. Würde es Ihnen etwas ausmachen, dies in der Spezifikation mit einem Beispiel oder in einem neuen Go-Blog-Beitrag zu aktualisieren, in dem Sie dieses Problem ausführlicher beschreiben?

Die Tools, die Sie erwähnen, müssen etwas Komplexes tun, und Go gibt ihnen eine Möglichkeit, dies zu tun.

Die Verwendung von Textvorlagen zum Generieren von Go-Code ist kaum eine Einrichtung, ich würde argumentieren, dass es sich um ein Ad-hoc-Pflaster handelt. Idealerweise sollten zumindest die Standardpakete ast und parser das Generieren von beliebigem Go-Code ermöglichen.

Das einzige, womit man argumentieren kann, dass Go einem den Umgang mit komplexer Logik bietet, ist vielleicht Reflection, aber das zeigt schnell seine Grenzen, ganz zu schweigen von leistungskritischem Code, selbst wenn es in der Standardbibliothek verwendet wird, zum Beispiel ist Gos JSON-Handhabung primitiv bestenfalls.

Es ist schwer zu argumentieren, dass die Verwendung von Textvorlagen oder Reflexionen, um _etwas bereits Komplexes_ zu tun, dem Ideal entspricht von:

Jedes Mal, wenn ein vorgeschlagener Ansatz für ~Generika~ etwas Komplexes schwierig auf einfache Weise zu erklären ist, müssen wir diesen Ansatz verwerfen.

Ich denke, die Lösung, die die erwähnten Projekte gefunden haben, um ihr Problem zu lösen, ist zu komplex und nicht leicht zu verstehen. In dieser Hinsicht fehlt Go also die Möglichkeiten, die es Benutzern ermöglichen, komplexe Probleme so einfach und direkt wie möglich auszudrücken.

Im Allgemeinen bemerke ich, dass die Leute oft auf jegliche Verwendung von Reflexion hinweisen und dies als Beweis für die Notwendigkeit von Generika behaupten.

Vielleicht gibt es so ein allgemeines Missverständnis, aber die Protobuf-Bibliothek, insbesondere die neue API, könnte mit _Generika_ oder einer Art _Summentyp_ sprunghaft viel einfacher sein.

Einer der Autoren dieser neuen Protobuf-API sagte nur: „Die überwiegende Mehrheit der Logik wird nicht von Generika profitieren“, also bin ich mir nicht sicher, woher Sie das bekommen, „insbesondere die neue API könnte viel mehr sein einfach mit Generika". Worauf basiert das? Können Sie beweisen, dass es viel einfacher wäre?

Als jemand, der die protobuf-APIs in einigen Sprachen verwendet hat, die Generika (Java, C++) enthalten, kann ich nicht sagen, dass ich signifikante Unterschiede in der Benutzerfreundlichkeit mit der Go-API und ihren APIs festgestellt habe. Wenn Ihre Behauptung wahr wäre, würde ich erwarten, dass es einen solchen Unterschied gibt.

@dsnet Sagte auch: "Es gibt einige Fälle, in denen Generika die Benutzerfreundlichkeit und / oder Implementierung der API verbessern können".

Aber wenn Sie ein Beispiel dafür wollen, wie die Dinge einfacher sein können, beginnen Sie damit, den Typ Value fallen zu lassen, da es sich größtenteils um einen Ad-hoc-Summentyp handelt.

@omeid In dieser Ausgabe geht es um Generika, nicht um Summentypen. Ich bin mir also nicht sicher, wie relevant dieses Beispiel ist.

Meine Frage lautet insbesondere: Wie würde Generika zu einer Protobuf-Implementierung oder API führen, die "sprunghaft viel einfacher" ist als die neue (oder alte) API?

Dies scheint weder mit meiner Lektüre von dem, was @dsnet oben gesagt hat, noch mit meiner Erfahrung mit den Java- und C++-Protobuf-APIs übereinzustimmen.

Auch Ihr Kommentar zur primitiven JSON-Verarbeitung in Go erscheint mir ebenso seltsam. Können Sie erklären, wie Ihrer Meinung nach die API von encoding/json durch Generika verbessert werden würde?

AFAIK, Implementierungen von JSON-Parsing in Java verwenden Reflektion (keine Generika). Es stimmt, dass die Top-Level-API in den meisten JSON-Bibliotheken wahrscheinlich eine generische Methode (z. B. Gson ) verwendet, aber eine Methode, die einen uneingeschränkten generischen Parameter T akzeptiert und einen Wert vom Typ T zurückgibt bietet im Vergleich zu json.Unmarshal nur sehr wenig zusätzliche Typprüfung. Tatsächlich denke ich, dass der einzige Fehler, den das einzige zusätzliche Fehlerszenario, das von json.Unmarshal zur Kompilierzeit nicht abgefangen wird, darin besteht, dass Sie einen Nicht-Zeiger-Wert übergeben. (Beachten Sie auch die Vorbehalte in der API-Dokumentation von Gson, eine andere Funktion für generische und nicht generische Typen zu verwenden. Auch dies spricht dafür, dass Generika ihre API komplizierten, anstatt sie zu vereinfachen; in diesem Fall soll die Serialisierung/Deserialisierung von Generics unterstützt werden Typen).

(JSON-Unterstützung in C++ ist AFAICT schlechter; die verschiedenen Ansätze, die ich kenne, verwenden entweder erhebliche Mengen an Makros oder beinhalten das manuelle Schreiben von Analyse-/Serialisierungsfunktionen. Auch dies nicht)

Wenn Sie erwarten, dass Generika die Unterstützung von Go für JSON erheblich verbessern, werden Sie leider enttäuscht sein.


@gertcuykens Jede Protobuf-Implementierung in jeder Sprache, die ich kenne, verwendet Codegenerierung, unabhängig davon, ob sie Generika hat oder nicht. Dazu gehören Java, C++, Swift, Rust, JS (und TS). Ich glaube nicht, dass Generika automatisch alle Verwendungen der Codegenerierung entfernen (als Existenzbeweis habe ich Codegeneratoren geschrieben, die Java-Code und C++-Code generieren); Es erscheint unlogisch zu erwarten, dass irgendeine Lösung für Generika diese Messlatte erfüllen wird.


Nur um es ganz klar zu sagen: Ich unterstütze das Hinzufügen von Generika zu Go. Aber ich denke, wir sollten klar im Auge behalten, was wir davon haben werden. Ich glaube nicht, dass wir signifikante Verbesserungen an Protobuf- oder JSON-APIs erhalten werden.

Ich glaube nicht, dass Protobuf ein besonders guter Fall für Generika ist. Sie benötigen keine Generika in der Zielsprache, da Sie einfach spezialisierten Code direkt generieren können. Dies würde auch für andere ähnliche Systeme wie Swagger/OpenAPI gelten.

Wo Generika für mich nützlich zu sein scheinen und sowohl Vereinfachung als auch Typsicherheit bieten könnten, wäre das Schreiben des Protobuf-Compilers selbst.

Was Sie brauchen, ist eine Sprache, die in der Lage ist, ihren eigenen abstrakten Syntaxbaum typsicher darzustellen. Aus meiner eigenen Erfahrung erfordert dies mindestens Generika und verallgemeinerte abstrakte Datentypen. Sie könnten dann einen typsicheren Protobuf-Compiler für eine Sprache in der Sprache selbst schreiben.

Wo Generika für mich nützlich zu sein scheinen und sowohl Vereinfachung als auch Typsicherheit bieten könnten, wäre das Schreiben des Protobuf-Compilers selbst.

Ich sehe nicht wirklich wie. Das go/ast -Paket bietet bereits eine Darstellung von Gos AST. Der Go-Protobuf-Compiler verwendet es nicht, weil das Arbeiten mit einem AST viel umständlicher ist, als nur Strings auszugeben, auch wenn es typsicherer ist.

Vielleicht haben Sie ein Beispiel aus dem protobuf-Compiler für eine andere Sprache?

@neild Ich habe zunächst gesagt, dass ich nicht denke, dass Protobuf ein sehr gutes Beispiel ist. Mit Generika lassen sich Vorteile erzielen, die jedoch stark davon abhängen, wie wichtig Typsicherheit für Sie ist, und dies würde dadurch ausgeglichen, wie aufdringlich die Implementierung von Generika ist. Eine ideale Implementierung würde Ihnen aus dem Weg gehen, es sei denn, Sie machen einen Fehler, und in diesem Fall würden die Vorteile die Kosten für mehr Anwendungsfälle überwiegen.

Wenn Sie sich das go/ast-Paket ansehen, hat es keine typisierte Darstellung des AST, da dies Generics und GADTs erfordert. Beispielsweise müsste ein „Hinzufügen“-Knoten in Bezug auf die Art der hinzuzufügenden Begriffe generisch sein. Bei einem nicht typsicheren AST muss die gesamte Typprüfungslogik von Hand codiert werden, was dies umständlich machen würde.

Mit einer guten Vorlagensyntax und typsicheren Ausdrücken könnten Sie es so einfach machen wie das Ausgeben von Zeichenfolgen, aber auch typsicher. Siehe zum Beispiel (hier geht es mehr um die Parsing-Seite): https://stackoverflow.com/questions/11104536/how-to-parse-strings-to-syntax-tree-using-gadts

Betrachten Sie beispielsweise JSX als wörtliche Syntax für den HTML-Dom in JavaScript Vs TSX als wörtliche Syntax für den Dom in TypeScript.

Wir können typisierte generische Ausdrücke schreiben, die sich auf den endgültigen Code spezialisieren. So einfach zu schreiben wie Strings, aber typgeprüft (in ihrer generischen Form).

Eines der Hauptprobleme bei Codegeneratoren besteht darin, dass die Typprüfung nur für den ausgegebenen Code erfolgt, was das Schreiben korrekter Vorlagen erschwert. Mit Generika können Sie die Vorlagen als tatsächliche typgeprüfte Ausdrücke schreiben, sodass die Überprüfung direkt auf der Vorlage erfolgt, nicht auf dem ausgegebenen Code, was es viel einfacher macht, sie richtig zu machen und zu warten.

Die variadischen Typparameter fehlen im aktuellen Design, was wie ein großes Fehlen der Funktionalität von Generika aussieht. Ein Add-On-Design (vielleicht) folgt dem aktuellen Vertragsdesign:

contract Comparables(Ts...) {
    if  len(Ts) > 0 {
        Comparables(Ts[1:]...)
    } else {
        Comparable(Ts[0])
    }
}

contract Comparable(T) {
    T int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string
}

type Keys(type Ts ...Comparables) struct {
    fs ...Ts
}

type Metric(type Ts ...Comparables) struct {
    mu sync.Mutex
    m  map[Keys(Ts...)]int
}

func (m *Metric(Ts...)) Add(vs ...Ts) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if m.m == nil {
        m.m = make(map[Keys(Ts...))]int)
    }
    m[Keys(Ts...){vs...}]++
}


// To use the metric

m := Metric(int, float64, string){m: make(map[Keys(int, float64, string)]int}
m.Add(1, 2.0, "variadic")

Beispiel inspiriert von hier .

Es ist mir nicht klar, wie das die Sicherheit erhöht, wenn man nur interface{} verwendet. Gibt es ein echtes Problem damit, dass Menschen Nicht-Vergleichbares in eine Metrik einfließen lassen?

Es ist mir nicht klar, wie das die Sicherheit erhöht, wenn man nur interface{} verwendet. Gibt es ein echtes Problem damit, dass Menschen Nicht-Vergleichbares in eine Metrik einfließen lassen?

Comparables in diesem Beispiel erfordert, dass Keys aus einer Reihe vergleichbarer Typen bestehen muss. Die Schlüsselidee besteht darin, das Design von variadischen Typparametern zu zeigen, nicht die Bedeutung des Typs selbst.

Ich möchte mich nicht zu sehr mit dem Beispiel aufhalten, aber ich greife es auf, weil ich denke, dass viele Beispiele für "Typerweiterungen" nur dazu führen, dass die Buchhaltung herumgeschubst wird, ohne dass praktische Sicherheit hinzugefügt wird. In diesem Fall können Sie sich beschweren, wenn Sie zur Laufzeit oder möglicherweise beim Tierarztbesuch einen schlechten Typ sehen.

Außerdem mache ich mir ein wenig Sorgen, dass das Zulassen offener Typen von Typen wie diesem zu dem Problem paradoxer Referenzen führen würde, wie es in der Logik zweiter Ordnung auftritt. Könnten Sie C als den Vertrag aller Typen definieren, die nicht in C enthalten sind?

Außerdem mache ich mir ein wenig Sorgen, dass das Zulassen offener Typen von Typen wie diesem zu dem Problem paradoxer Referenzen führen würde, wie es in der Logik zweiter Ordnung auftritt. Könnten Sie C als den Vertrag aller Typen definieren, die nicht in C enthalten sind?

Tut mir leid, aber ich verstehe nicht, wie dieses Beispiel offene Typen zulässt und sich auf Russells Paradoxon bezieht, Comparables wird durch eine Liste von Comparable definiert.

Ich mag die Idee nicht, Go-Code in einen Vertrag zu schreiben. Wenn ich eine if -Anweisung schreiben kann, kann ich dann auch eine for -Anweisung schreiben? Kann ich eine Funktion aufrufen? Kann ich Variablen deklarieren? Warum nicht?

Es scheint auch unnötig. func F(a ...int) bedeutet, dass a gleich []int ist. Analog würde func F(type Ts ...comparable) bedeuten, dass jeder Typ in der Liste comparable ist.

In diesen Zeilen

type Keys(type Ts ...Comparables) struct {
    fs ...Ts
}

Sie scheinen eine Struktur mit mehreren Feldern zu definieren, die alle fs heißen. Ich bin mir nicht sicher, wie das funktionieren soll. Gibt es eine andere Möglichkeit, Verweise auf Felder in dieser Struktur zu verwenden, als Reflektion zu verwenden?

Die Frage ist also: Was kann man mit variadischen Typparametern tun? Was möchte man tun?

Ich denke, Sie verwenden hier variadische Typparameter, um einen Tupeltyp mit einer beliebigen Anzahl von Feldern zu definieren.

Was will man sonst noch machen?

Ich mag die Idee nicht, Go-Code in einen Vertrag zu schreiben. Wenn ich eine if -Anweisung schreiben kann, kann ich dann auch eine for -Anweisung schreiben? Kann ich eine Funktion aufrufen? Kann ich Variablen deklarieren? Warum nicht?

Es scheint auch unnötig. func F(a ...int) bedeutet, dass a gleich []int ist. Analog würde func F(type Ts ...comparable) bedeuten, dass jeder Typ in der Liste comparable ist.

Nachdem ich mir das Beispiel einen Tag später angeschaut habe, gebe ich Ihnen absolut Recht. Die Comparables ist eine dumme Idee. Das Beispiel soll nur die Botschaft vermitteln, mit len(args) die Anzahl der Parameter zu bestimmen. Es stellt sich heraus, dass für Funktionen func F(type Ts ...Comparable) gut genug ist.

Das getrimmte Beispiel:

contract Comparable(T) {
    T int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string
}

type Keys(type Ts ...Comparable) struct {
    fs ...Ts
}

type Metric(type Ts ...Comparable) struct {
    mu sync.Mutex
    m  map[Keys(Ts...)]int
}

func (m *Metric(Ts...)) Add(vs ...Ts) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if m.m == nil {
        m.m = make(map[Keys(Ts...))]int)
    }
    m[Keys(Ts...){vs...}]++
}


// To use the metric

m := Metric(int, float64, string){m: make(map[Keys(int, float64, string)]int}
m.Add(1, 2.0, "variadic")

Sie scheinen eine Struktur mit mehreren Feldern zu definieren, die alle fs heißen. Ich bin mir nicht sicher, wie das funktionieren soll. Gibt es eine andere Möglichkeit, Verweise auf Felder in dieser Struktur zu verwenden, als Reflektion zu verwenden?

Die Frage ist also: Was kann man mit variadischen Typparametern tun? Was möchte man tun?

Ich denke, Sie verwenden hier variadische Typparameter, um einen Tupeltyp mit einer beliebigen Anzahl von Feldern zu definieren.

Was will man sonst noch machen?

Variadische Typparameter zielen per Definition auf Tupel ab, wenn wir ... dafür verwenden, was nicht bedeutet, dass Tupel der einzige Anwendungsfall sind, aber man kann sie in allen Strukturen und Funktionen verwenden.

Da es nur zwei Stellen gibt, die mit variadischen Typparametern erscheinen: struct oder function, haben wir leicht, was vorher für Funktionen klar ist:

func F(type Ts ...Comparable) (args ...Ts) {
    if len(args) > 1 {
        F(args[1:])
        return
    }
    // ... do stuff with args[0]
}

Beispielsweise ist die Funktion variadic Min im aktuellen Design nicht möglich, aber mit variadic-Typparametern möglich:

func Min(type T ...Comparable)(p1 T, pn ...T) T {
    switch l := len(pn); {
    case l > 1:
        return Min(pn[0], pn[1:]...)
    case l == 1:
        if p1 >= pn[0] { return pn[0] }
        return p1
    case l < 1:
        return p1
    }
}

So definieren Sie ein Tuple mit variadischen Typparametern:

type Tuple(type Ts ...Comparable) struct {
    fs ...Ts
}

Wenn drei Typparameter durch 'Ts' instanziiert werden, kann es übersetzt werden

type Tuple(type T1, T2, T3 Comparable) struct {
    fs_1 T1
    fs_2 T2
    fs_3 T3
}

als Zwischendarstellung. Um fs zu verwenden, gibt es mehrere Möglichkeiten:

  1. Parameter entpacken
k := Tuple(int, float64, string){1, 2.0, "variadic"}
fs1, fs2, fs3 := k.fs // translated to fs1, fs2, fs3 := k.fs_1, k.fs_2, k.fs_3
println(fs1) // 1
println(fs2) // 2.0
println(fs3) // variadic
  1. Verwenden Sie die Schleife for
for idx, f := range k.fs {
    println(idx, ": ", f)
}
// Output:
// 0: 1
// 1: 2.0
// 2: variadic
  1. Index verwenden (nicht sicher, ob die Leute sehen, dass dies eine Mehrdeutigkeit für Array/Slice oder Map ist)
k.fs[0] = ... // translated to k.fs_1 = ...
f2 := k.fs[1] // translated to f2 := k.fs_2
  1. Verwenden Sie das Paket reflect , funktioniert im Grunde wie ein Array
t := Tuple(int, float64, string){1, 2.0, "variadic"}

fs := reflect.ValueOf(t).Elem().FieldByName("fs")
val := reflect.ValueOf(fs)
if val.Kind() == reflect.VariadicTypes {
    for i := 0; i < val.Len(); i++ {
        e := val.Index(i)
        switch e.Kind() {
        case reflect.Int:
            fmt.Printf("%v, ", e.Int())
        case reflect.Float64:
            fmt.Printf("%v, ", e.Float())
        case reflect.String:
            fmt.Printf("%v, ", e.String())
        }
    }
}

Nichts wirklich Neues im Vergleich zur Verwendung eines Arrays.

Beispielsweise ist die variadische Min-Funktion im aktuellen Design nicht möglich, aber mit variadischen Typparametern möglich:

func Min(type T ...Comparable)(p1 T, pn ...T) T {
    switch l := len(pn); {
    case l > 1:
        return Min(pn[0], pn[1:]...)
    case l == 1:
        if p1 >= pn[0] { return pn[0] }
        return p1
    case l < 1:
        return p1
    }
}

Das ergibt für mich keinen Sinn. Variadische Typparameter sind nur sinnvoll, wenn es sich bei den Typen um unterschiedliche Typen handeln kann. Aber das Aufrufen Min auf einer Liste mit verschiedenen Typen macht keinen Sinn. Go unterstützt die Verwendung >= für Werte unterschiedlicher Typen nicht. Selbst wenn wir das irgendwie zugelassen hätten, könnten wir nach Min(int, string)(1, "a") gefragt werden. Darauf gibt es keine Antwort.

Es stimmt zwar, dass das aktuelle Design Min einer variablen Anzahl verschiedener Typen nicht zulässt, aber es unterstützt den Aufruf Min für eine unterschiedliche Anzahl von Werten desselben Typs. Was meiner Meinung nach sowieso der einzig vernünftige Weg ist, Min zu verwenden.

func Min(type T comparable)(s ...T) T {
    if len(s) == 0 {
        panic("Min of no elements")
    }
    r := s[0]
    for _, v := range s[1:] {
        if v < r {
            r = v
        }
    }
    return r
}

Für einige der anderen Beispiele in https://github.com/golang/go/issues/15292#issuecomment -599040081 ist es wichtig zu beachten, dass Slices und Arrays in Go Elemente haben, die alle denselben Typ haben. Bei der Verwendung von Parametern unterschiedlicher Typen sind die Elemente unterschiedliche Typen. Es ist also wirklich nicht dasselbe wie ein Slice oder Array.

Es stimmt zwar, dass das aktuelle Design Min einer variablen Anzahl verschiedener Typen nicht zulässt, aber es unterstützt den Aufruf Min für eine unterschiedliche Anzahl von Werten desselben Typs. Was meiner Meinung nach sowieso der einzig vernünftige Weg ist, Min zu verwenden.

func Min(type T comparable)(s ...T) T {
    if len(s) == 0 {
        panic("Min of no elements")
    }
    r := s[0]
    for _, v := range s[1:] {
        if v < r {
            r = v
        }
    }
    return r
}

Wahr. Min war ein schlechtes Beispiel. Es wurde spät hinzugefügt und hatte keinen klaren Gedanken, wie Sie dem Kommentarbearbeitungsverlauf entnehmen können. Ein echtes Beispiel ist Metric , das Sie ignoriert haben.

Es ist wichtig zu beachten, dass Slices und Arrays in Go Elemente haben, die alle denselben Typ haben. Bei der Verwendung von Parametern unterschiedlicher Typen sind die Elemente unterschiedliche Typen. Es ist also wirklich nicht dasselbe wie ein Slice oder Array.

Sehen? Sie sind die Leute, die sehen, dass dies eine Mehrdeutigkeit für Array/Slice oder Map ist. Wie ich in https://github.com/golang/go/issues/15292#issuecomment -599040081 sagte, ist die Syntax recht ähnlich zu array/slice und map, aber sie greift auf Elemente mit unterschiedlichen Typen zu. Ist es wirklich wichtig? Oder kann man beweisen, dass dies Mehrdeutigkeit ist? Was in Go 1 möglich ist, ist:

m := map[interface{}]int{1: 2, "2": 3, 3.0: 4}
for i, e := range m {
    println(i, e)
}

Wird i als derselbe Typ betrachtet? Anscheinend sagen wir, i ist interface{} , gleicher Typ. Aber drückt eine Schnittstelle wirklich den Typ aus? Programmierer müssen manuell prüfen, welche Typen möglich sind. Wenn Sie for , [] und unpack verwenden, ist es für den Benutzer wirklich wichtig, dass sie nicht auf denselben Typ zugreifen? Was spricht dagegen? Gleiches gilt für fs :

for idx, f := range k.fs {
    switch f.(type) { // compare to interface{}, here is zero overhead.
    case int:
        // ...
    case float64:
        // ...
    case string:
        // ...
    }
}

Wenn Sie einen Typschalter verwenden müssen, um auf ein Element eines variadischen generischen Typs zuzugreifen, sehe ich keinen Vorteil. Ich kann sehen, wie es mit einigen Kompilierungstechniken zur Laufzeit möglicherweise etwas effizienter sein könnte als die Verwendung interface{} . Aber ich denke, der Unterschied wäre ziemlich gering, und ich sehe nicht ein, warum es typsicherer sein sollte. Es ist nicht sofort ersichtlich, dass es sich lohnt, die Sprache komplexer zu machen.

Ich wollte das Metric -Beispiel nicht ignorieren, ich sehe nur noch nicht, wie man generische Variadic-Typen verwendet, um das Schreiben zu vereinfachen. Wenn ich einen Typschalter im Körper von Metric verwenden muss, dann würde ich eher Metric2 und Metric3 schreiben.

Was ist die Definition von "Sprache komplexer machen"? Wir sind uns alle einig, dass Generika eine komplexe Sache sind und die Sprache niemals einfacher machen werden als Go 1. Sie haben bereits große Anstrengungen unternommen, um es zu entwerfen und zu implementieren, aber für Go-Benutzer ist es ziemlich unklar: Was ist die Definition von "fühlt sich an wie Schreiben... Los"? Gibt es eine quantifizierte Metrik, um es zu messen? Wie könnte ein Sprachvorschlag argumentieren, dass er die Sprache nicht komplexer macht? In der Sprachvorschlagsvorlage Go 2 sind die Ziele auf den ersten Blick recht einfach:

  1. ein wichtiges Thema für viele Menschen ansprechen,
  2. haben nur minimale Auswirkungen auf alle anderen, und
  3. kommen mit einer klaren und gut verständlichen Lösung.

Aber Fragen könnten sein: Wie viele sind "viele"? Was steht für "wichtig"? Wie misst man die Auswirkungen auf eine unbekannte Population? Wann ist ein Problem gut verstanden? Go dominiert die Cloud, aber wird die Dominanz anderer Bereiche wie wissenschaftliche numerische Berechnungen (z. B. maschinelles Lernen), grafisches Rendering (z. B. riesiger 3D-Markt) eines der Ziele von Go werden? Passt das Problem eher in „Ich mache lieber A als B in Go & es gibt keinen Anwendungsfall, weil wir es anders machen können“ oder „B wird nicht angeboten, daher nutzen wir Go & den Anwendungsfall nicht ist noch nicht da, weil die Sprache es nicht leicht ausdrücken kann"? ... Ich fand diese Fragen schmerzhaft und endlos, und manchmal sogar nicht wert, sie zu beantworten.

Zurück zum Beispiel Metric , es zeigt keine Notwendigkeit für den Zugriff auf Personen. Das Entpacken des Parametersatzes scheint hier keine wirkliche Notwendigkeit zu sein, obwohl Lösungen, die mit der vorhandenen Sprache "übereinstimmen", [ ] Indizierung und Typableitung verwenden, das Problem der Typsicherheit lösen können:

f2 := k.fs[1] // f2 is a float64

@changkun Wenn es klare und objektive Metriken gäbe, um zu entscheiden, welche Sprachfunktionen gut und schlecht sind, bräuchten wir keine Sprachdesigner - wir könnten einfach ein Programm schreiben, um eine optimale Sprache für uns zu entwerfen. Aber das gibt es nicht - es kommt immer auf die persönlichen Vorlieben einiger Leute an. Das ist übrigens auch der Grund, warum es keinen Sinn macht, darüber zu streiten, ob eine Sprache "gut" ist oder nicht - die Frage ist nur, ob Sie sie persönlich mögen. Im Fall von Go sind die Leute, die über die Präferenzen entscheiden, die Leute im Go-Team, und die Dinge, die Sie zitieren, sind keine Metriken, sondern Leitfragen, die Ihnen helfen sollen, sie zu überzeugen.

Persönlich, FWIW, habe ich das Gefühl, dass variadische Typparameter bei zwei dieser drei versagen. Ich glaube nicht, dass sie für viele Menschen ein wichtiges Thema ansprechen - das Metrikbeispiel könnte davon profitieren, aber meiner Meinung nach nur geringfügig und es ist ein sehr spezialisierter Anwendungsfall. Und ich glaube nicht, dass sie mit einer klaren und gut verständlichen Lösung kommen. Mir ist keine Sprache bekannt, die so etwas unterstützt. Aber ich kann mich irren. Es wäre auf jeden Fall hilfreich, wenn jemand Beispiele für andere Sprachen hat, die dies unterstützen - es könnte Informationen darüber liefern, wie es normalerweise implementiert und, was noch wichtiger ist, wie es verwendet wird. Vielleicht wird es breiter verwendet, als ich mir vorstellen kann.

@Merovius Haskell hat polyvariadische Funktionen, wie wir im HList-Papier gezeigt haben: http://okmij.org/ftp/Haskell/polyvariadic.html#polyvar -fn
Es ist eindeutig komplex, dies in Haskell zu tun, aber nicht unmöglich.

Das motivierende Beispiel ist der typsichere Datenbankzugriff, bei dem Dinge wie typsichere Joins und Projektionen durchgeführt und das Datenbankschema in der Sprache deklariert werden können.

Beispielsweise sieht eine Datenbanktabelle sehr nach einem Datensatz aus, in dem es Spaltennamen und -typen gibt. Die relationale Verknüpfungsoperation nimmt zwei beliebige Datensätze und erzeugt einen Datensatz mit den Typen von beiden. Sie können dies natürlich von Hand tun, aber es ist fehleranfällig, sehr mühsam, verschleiert die Bedeutung des Codes mit all den handdeklarierten Datensatztypen, und natürlich ist das große Merkmal einer SQL-Datenbank, dass sie Ad-hoc unterstützt Abfragen, sodass Sie nicht alle möglichen Datensatztypen vorab erstellen können, da Sie nicht unbedingt wissen, welche Abfragen Sie möchten, bis Sie sie ausführen.

Ein typsicherer relationaler Verknüpfungsoperator für Datensätze und Tupel wäre also ein guter Anwendungsfall. Wir denken hier nur über den Typ der Funktion nach - es liegt am Programmierer, was die Funktion tatsächlich tut, ob es sich um eine Verknüpfung von zwei Arrays von Tupeln im Speicher handelt oder ob sie SQL generiert, um sie auf einer externen DB auszuführen und die Ergebnisse zu marshallieren typsicher zurück.

So etwas lässt sich mit LINQ viel besser in C# einbetten. Die meisten Leute scheinen LINQ als Hinzufügen von Lambda-Funktionen und Monaden zu C# zu betrachten, aber ohne Polyvariadics würde es für seinen primären Anwendungsfall nicht funktionieren, da Sie einfach keinen typsicheren Join-Operator ohne ähnliche Funktionalität definieren können.

Ich denke, dass Vergleichsoperatoren wichtig sind. Nach grundlegenden Operatoren für boolesche, binäre, int-, Float- und String-Typen kommen wahrscheinlich als nächstes Mengen und dann Relationen.

Übrigens, C++ bietet es auch an, obwohl wir nicht argumentieren wollen, dass wir dieses Feature in Go wollen, weil XXX es hat :)

Ich denke, es wäre sehr seltsam, wenn k.fs[0] und k.fs[1] unterschiedliche Typen hätten. So funktionieren andere indexierbare Werte in Go nicht.

Das Messwertbeispiel basiert auf https://medium.com/@sameer_74231/go -experience-report-for-generics-google-metrics-api-b019d597aaa4. Ich denke, dass dieser Code Reflexion erfordert, um die Werte abzurufen. Ich denke, wenn wir variadische Generika zu Go hinzufügen, sollten wir etwas Besseres als Reflektion bekommen, um die Werte abzurufen. Sonst scheint es nicht so viel zu helfen.

Ich denke, es wäre sehr seltsam, wenn k.fs[0] und k.fs[1] unterschiedliche Typen hätten. So funktionieren andere indexierbare Werte in Go nicht.

Das Messwertbeispiel basiert auf https://medium.com/@sameer_74231/go -experience-report-for-generics-google-metrics-api-b019d597aaa4. Ich denke, dass dieser Code Reflexion erfordert, um die Werte abzurufen. Ich denke, wenn wir variadische Generika zu Go hinzufügen, sollten wir etwas Besseres als Reflektion bekommen, um die Werte abzurufen. Sonst scheint es nicht so viel zu helfen.

Brunnen. Sie fordern an, dass etwas nicht existiert. Wenn Sie [``] nicht mögen, bleiben zwei Optionen übrig: ( ) oder {``} , und ich sehe, Sie können argumentieren, dass Klammern wie ein Funktionsaufruf und aussehen Die geschweiften Klammern sehen aus wie eine Variableninitialisierung. Niemand mag args.0 args.1 , da es sich nicht wie Go anfühlt. Die Syntax ist trivial.

Eigentlich verbringe ich einige Wochenendzeit damit, das Buch "The Design and Evolution of C++" zu lesen, es gibt viele interessante Einblicke in Entscheidungen und Lektionen, obwohl es 1994 geschrieben wurde:

_"[...] Im Nachhinein habe ich die Bedeutung von Einschränkungen bei der Lesbarkeit und frühen Fehlererkennung unterschätzt."_ ==> Tolles Vertragsdesign

"_die Funktionssyntax sieht auf den ersten Blick auch ohne extra Schlüsselwort schöner aus:_

T& index<class T>(vector<T>& v, int i) { /*...*/ }
int i = index(v1, 10);

_Es scheint nagende Probleme mit dieser einfacheren Syntax zu geben. Es ist zu schlau. Es ist relativ schwer, eine Template-Deklaration in einem Programm zu erkennen, weil [...] Die <...> -Klammern wurden gegenüber runden Klammern bevorzugt gewählt, weil Benutzer sie leichter lesbar fanden. [...] Zufällig hat Tom Pennello bewiesen, dass Klammern einfacher zu analysieren gewesen wären, aber das ändert nichts an der Schlüsselbeobachtung, dass (menschliche) Leser <...> _ bevorzugen
" ==> ist es nicht ähnlich wie func F(type T C)(v T) T ?

_"Ich denke jedoch, dass ich zu vorsichtig und konservativ war, als es darum ging, Template-Features zu spezifizieren. Ich hätte Features wie [...] einbeziehen können. Diese Features hätten die Implementierer nicht sehr belastet, und den Benutzern wäre geholfen worden."_

Warum fühlt es sich so vertraut an?

Die Indizierung von variadischen Typparametern (oder Tupeln) muss von der Laufzeitindizierung und der Kompilierzeitindizierung getrennt sein. Ich schätze, Sie könnten einfach argumentieren, dass die fehlende Unterstützung für die Indizierung zur Laufzeit Benutzer verwirren kann, weil sie nicht mit der Indizierung zur Kompilierzeit konsistent ist. Selbst für die Indexierung während der Kompilierung fehlt im aktuellen Design ein Nicht-Typ-Parameter „Vorlage“.

Mit allen Beweisen versucht der Vorschlag (außer dem Erfahrungsbericht) zu vermeiden, diese Funktion zu diskutieren, und ich fange an zu glauben, dass es nicht darum geht, verschiedene Generika zu Go hinzuzufügen, sondern nur absichtlich zu entfernen.

Ich stimme zu, dass Design and Evolution of C++ ein gutes Buch ist, aber C++ und Go haben unterschiedliche Ziele. Das letzte Zitat dort ist gut; Stroustrup erwähnt nicht einmal die Kosten der Sprachkomplexität für die Benutzer der Sprache. Bei Go versuchen wir immer, diese Kosten zu berücksichtigen. Go soll eine einfache Sprache sein. Wenn wir jede Funktion hinzufügen würden, die den Benutzern helfen würde, wäre es nicht einfach. Denn C++ ist nicht einfach.

Mit allen Beweisen versucht der Vorschlag (außer dem Erfahrungsbericht) zu vermeiden, diese Funktion zu diskutieren, und ich fange an zu glauben, dass es nicht darum geht, verschiedene Generika zu Go hinzuzufügen, sondern nur absichtlich zu entfernen.

Tut mir leid, ich weiß nicht, was Sie hier meinen.

Persönlich habe ich immer die Möglichkeit verschiedener generischer Typen in Betracht gezogen, aber ich habe mir nie die Zeit genommen, herauszufinden, wie es funktionieren würde. Die Funktionsweise in C++ ist sehr subtil. Ich würde gerne sehen, ob wir zunächst nicht-variadische Generika zum Laufen bringen können. Es ist sicherlich Zeit, später, wenn möglich, variadische Generika hinzuzufügen.

Wenn ich die früheren Gedanken kritisiere, sage ich nicht, dass variadische Typen nicht gemacht werden können. Ich weise auf Probleme hin, die meiner Meinung nach gelöst werden müssen. Wenn sie nicht gelöst werden können, bin ich nicht davon überzeugt, dass sich Variadic-Typen lohnen.

Stroustrup erwähnt nicht einmal die Kosten der Sprachkomplexität für die Benutzer der Sprache. Bei Go versuchen wir immer, diese Kosten zu berücksichtigen. Go soll eine einfache Sprache sein. Wenn wir jede Funktion hinzufügen würden, die den Benutzern helfen würde, wäre es nicht einfach. Denn C++ ist nicht einfach.

Stimmt IMO nicht. Man muss beachten, dass C++ der erste Praktiker ist, der Generika weiterführt (Nun, ML ist die erste Sprache). Aus dem, was ich aus dem Buch lese, erhalte ich die Nachricht, dass C++ als einfache Sprache gedacht war (am Anfang keine Generika anbieten, Experiment-Simplify-Ship-Schleife für Sprachdesign, gleiche Geschichte). C++ hatte auch mehrere Jahre lang eine Feature-Freeze-Phase, was wir in Go "The Compatability Promise" haben. Aber es gerät aus vielen vernünftigen Gründen mit der Zeit ein wenig außer Kontrolle, was Go nicht klar ist, wenn es nach der Veröffentlichung von Generika den alten Pfad von C++ einschlägt.

Es ist sicherlich Zeit, später, wenn möglich, variadische Generika hinzuzufügen.

Dasselbe Gefühl für mich. Variadic Generics fehlen auch in der ersten standardisierten Version von Templates.

Ich weise auf Probleme hin, die meiner Meinung nach gelöst werden müssen. Wenn sie nicht gelöst werden können, bin ich nicht davon überzeugt, dass sich Variadic-Typen lohnen.

Ich verstehe Ihre Bedenken. Aber das Problem ist im Grunde genommen gelöst, muss aber nur richtig in Go übersetzt werden (und ich denke, niemand mag das Wort "übersetzen"). Was ich aus Ihrem historischen Generika-Vorschlag gelesen habe, folgen sie im Grunde dem, was im frühen Vorschlag von C++ fehlgeschlagen ist, und kompromittieren das, was Stroustrup bereut. Mich interessieren Ihre Gegenargumente dazu.

Wir werden uns über die Ziele von C++ nicht einig sein müssen. Vielleicht waren die ursprünglichen Ziele ähnlicher, aber wenn ich mir C++ heute anschaue, denke ich, dass es klar ist, dass ihre Ziele sich sehr von denen für Go unterscheiden, und ich denke, das ist seit mindestens 25 Jahren so.

Beim Schreiben verschiedener Vorschläge zum Hinzufügen von Generika zu Go habe ich mir natürlich angesehen, wie C++-Templates funktionieren, sowie viele andere Sprachen (schließlich hat C++ keine Generika erfunden). Ich habe mir nicht angesehen, was Stroustrup bedauert hat, also wenn wir an denselben Ort gekommen sind, dann ist das großartig. Meiner Meinung nach sind Generika in Go eher wie Generika in Ada oder D als in C++. Auch heute hat C++ keine Verträge, die sie Konzepte nennen, aber noch nicht zur Sprache hinzugefügt haben. Außerdem erlaubt C++ absichtlich eine komplexe Programmierung zum Zeitpunkt der Kompilierung, und tatsächlich sind C++-Vorlagen selbst eine vollständige Turing-Sprache (obwohl ich nicht weiß, ob das beabsichtigt war). Ich habe das immer als etwas angesehen, das man für Go vermeiden sollte, da die Komplexität extrem ist (obwohl es in C++ komplexer ist als in Go, weil Methoden überladen und aufgelöst werden, was Go nicht hat).

Nachdem ich die aktuelle Vertragsimplementierung etwa einen Monat lang ausprobiert habe, frage ich mich ein wenig, was das Schicksal der vorhandenen integrierten Funktionen ist. Alle von ihnen können auf generische Weise implementiert werden:

func Append(type T)(slice []T, elems ...T) []T {...}
func Copy(type T)(dst, src []T) int {...}
func Delete(type K, V)(m map[K]V, k K) {...}
func Make(type T, I Integer(I))(siz ...I) T {...}
func New(type T)() *T {...}
func Close(type T)(c chan<- T) {...}
func Panic(type T)(v T) {...}
func Recover(type T)() T {...}
func Print(type ...T)(args ...T) {...}
func Println(type ...T)(args ...T) {...}

Werden sie in Go2 weg sein? Wie konnte Go 2 mit solch enormen Auswirkungen auf die bestehende Codebasis von Go 1 fertig werden? Das scheinen offene Fragen zu sein.

Außerdem sind diese beiden etwas Besonderes:

func Len(type T C)(t T) int {...}
func Cap(type T C)(t T) int {...}

Wie man einen solchen Vertrag C mit dem aktuellen Design implementiert, sodass ein Typparameter nur generischer Slice []Ts , Map map[Tk]Tv und Channel chan Tc sein kann wo sind T Ts Tk Tv Tc anders?

@changkun Ich denke nicht, dass "sie mit Generika implementiert werden können" ein überzeugender Grund ist, sie zu entfernen. Und Sie nennen einen ziemlich klaren und starken Grund, warum sie nicht entfernt werden sollten. Also ich glaube nicht, dass sie es sein werden. Ich denke, das macht die restlichen Fragen obsolet.

@changkun Ich denke nicht, dass "sie mit Generika implementiert werden können" ein überzeugender Grund ist, sie zu entfernen. Und Sie nennen einen ziemlich klaren und starken Grund, warum sie nicht entfernt werden sollten.

Ja, ich stimme zu, dass es nicht überzeugt, sie zu entfernen, deshalb habe ich es ausdrücklich gesagt. Sie zusammen mit Generika zu halten, "verstößt" jedoch gegen die bestehende Philosophie von Go, deren Sprachmerkmale orthogonal sind. Die Kompatibilität ist das Hauptanliegen, aber das Hinzufügen von Verträgen wird wahrscheinlich einen großen aktuellen "veralteten" Code zerstören.

Also ich glaube nicht, dass sie es sein werden. Ich denke, das macht die restlichen Fragen obsolet.

Lassen Sie uns versuchen, die Frage nicht zu ignorieren und sie als realen Anwendungsfall von Verträgen zu betrachten. Wenn man auf ähnliche Anforderungen stößt, wie könnten wir diese mit dem aktuellen Design umsetzen?

Natürlich werden wir die vorhandenen vordeklarierten Funktionen nicht los.

Es ist zwar möglich, eine parametrisierte Funktionssignatur für delete , close , panic , recover , print und println zu schreiben

Es gibt Teilversionen von Append und Copy unter https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-contracts.md#append. Es ist nicht vollständig, da append und copy Sonderfälle für ein zweites Argument vom Typ string haben, was vom aktuellen Designentwurf nicht unterstützt wird.

Beachten Sie, dass die Signatur für Make oben gemäß dem aktuellen Designentwurf nicht gültig ist. New ist nicht ganz dasselbe wie new , aber nah genug dran.

Mit dem aktuellen Entwurfsentwurf müssten Len und Cap ein Argument vom Typ interface{} annehmen und wären daher nicht typsicher zur Kompilierzeit.

https://go-review.googlesource.com/c/go/+/187317

Bitte verwenden Sie keine .go2 Dateierweiterungen, wir haben Module, um diese Art von Versionssache zu machen? Ich verstehe, wenn Sie es als vorübergehende Lösung tun, um sich das Leben beim Experimentieren mit Verträgen zu erleichtern, aber stellen Sie bitte sicher, dass sich die go.mod -Datei am Ende darum kümmert, go -Pakete ohne die zu mischen benötigt .go2 Dateierweiterungen. Es wäre ein Schlag gegen die Modulentwickler, die sich bemühen sicherzustellen, dass Module so gut wie möglich funktionieren. Die Verwendung .go2 Dateierweiterungen ist wie zu sagen, nein, es ist mir egal, ob Ihr Modulzeug es auf meine Weise macht, weil ich nicht möchte, dass mein 10 Jahre alter Pre-Modul-Dinosaurier go Compiler kaputt geht .

@gertcuykens .go2-Dateien sind nur für das Experiment; Sie werden nicht verwendet, wenn Generika im Compiler landen.

(Ich werde unsere Kommentare verbergen, da sie nicht wirklich zur Diskussion beitragen und sie so wie sie sind lang genug sind.)

Kürzlich habe ich eine neue generische Syntax in der von mir entworfenen K -Sprache untersucht, weil K viel Grammatik von Go entlehnt hat, sodass diese generische Grammatik möglicherweise auch einen gewissen Referenzwert für Go hat.

Das identifier<T> Problem ist, dass es mit Vergleichsoperatoren und auch Bitoperatoren kollidiert, also stimme ich diesem Design nicht zu.

Scalas identifier[T] hat ein besseres Erscheinungsbild als das vorherige Design, aber nachdem der obige Konflikt gelöst wurde, hat es einen neuen Konflikt mit dem Indexdesign identifier[index] .
Aus diesem Grund wurde das Indexdesign von Scala auf identifier(index) geändert. Dies funktioniert nicht gut für Sprachen, die bereits [] als Index verwenden.

In Gos Entwurf wurde erklärt, dass Generika (type T) verwenden, was keine Konflikte verursachen wird, da type ein Schlüsselwort ist, aber der Compiler braucht immer noch mehr Urteilsvermögen, wenn er aufgerufen wird, um das identifier(type)(params) aufzulösen.

Durch Zufall erinnerte ich mich an das spezielle Design des Methodenaufrufs in OC, was mich zu einem neuen Design inspirierte.

Was wäre, wenn wir den Bezeichner und das Generikum als Ganzes zusammenfassen und in [] zusammenfassen?
Wir können die [identifier T] bekommen. Dieses Design steht nicht im Widerspruch zum Index, da es mindestens zwei Elemente haben muss, die durch Leerzeichen getrennt sind.
Wenn es mehrere Generika gibt, können wir [identifier T V] so schreiben, und es wird nicht mit dem bestehenden Design in Konflikt geraten.

Wenn wir dieses Design in Go einsetzen, erhalten wir das folgende Beispiel.
Z.B

type [Item T] struct {
    Value T
}

func (it [Item T]) Print() {
    println(it.Value)
}

func [TestGenerics T V]() {
    var a = [Item T]{}
    a.Print()
    var b = [Item V]{}
    b.Print()
}

func main() {
    [TestGenerics int string]()
}

Das sieht sehr übersichtlich aus.

Ein weiterer Vorteil der Verwendung von [] besteht darin, dass es eine gewisse Vererbung von Gos ursprünglichem Slice-and-Map-Design hat und kein Gefühl der Fragmentierung hervorruft.

[]int  ->  [slice int]

map[string]int  ->  [map string int]

Wir können ein komplizierteres Beispiel machen

var a map[int][]map[string]map[string][]string

var b [map int [slice [map string [map string [slice string]]]]]

Dieses Beispiel behält noch eine relativ klare Wirkung bei und hat gleichzeitig einen geringen Einfluss auf die Kompilierung.

Ich habe dieses Design in K implementiert und getestet und es funktioniert gut.

Ich denke, dieses Design hat einen gewissen Referenzwert und ist vielleicht diskussionswürdig.

Kürzlich habe ich eine neue generische Syntax in der von mir entworfenen K -Sprache untersucht, weil K viel Grammatik von Go entlehnt hat, sodass diese generische Grammatik möglicherweise auch einen gewissen Referenzwert für Go hat.

Das identifier<T> Problem ist, dass es mit Vergleichsoperatoren und auch Bitoperatoren kollidiert, also stimme ich diesem Design nicht zu.

Scalas identifier[T] hat ein besseres Erscheinungsbild als das vorherige Design, aber nachdem der obige Konflikt gelöst wurde, hat es einen neuen Konflikt mit dem Indexdesign identifier[index] .
Aus diesem Grund wurde das Indexdesign von Scala auf identifier(index) geändert. Dies funktioniert nicht gut für Sprachen, die bereits [] als Index verwenden.

In Gos Entwurf wurde erklärt, dass Generika (type T) verwenden, was keine Konflikte verursachen wird, da type ein Schlüsselwort ist, aber der Compiler braucht immer noch mehr Urteilsvermögen, wenn er aufgerufen wird, um das identifier(type)(params) aufzulösen.

Durch Zufall erinnerte ich mich an das spezielle Design des Methodenaufrufs in OC, was mich zu einem neuen Design inspirierte.

Was wäre, wenn wir den Bezeichner und das Generikum als Ganzes zusammenfassen und in [] zusammenfassen?
Wir können die [identifier T] bekommen. Dieses Design steht nicht im Widerspruch zum Index, da es mindestens zwei Elemente haben muss, die durch Leerzeichen getrennt sind.
Wenn es mehrere Generika gibt, können wir [identifier T V] so schreiben, und es wird nicht mit dem bestehenden Design in Konflikt geraten.

Wenn wir dieses Design in Go einsetzen, erhalten wir das folgende Beispiel.
Z.B

type [Item T] struct {
    Value T
}

func (it [Item T]) Print() {
    println(it.Value)
}

func [TestGenerics T V]() {
    var a = [Item T]{}
    a.Print()
    var b = [Item V]{}
    b.Print()
}

func main() {
    [TestGenerics int string]()
}

Das sieht sehr übersichtlich aus.

Ein weiterer Vorteil der Verwendung von [] besteht darin, dass es eine gewisse Vererbung von Gos ursprünglichem Slice-and-Map-Design hat und kein Gefühl der Fragmentierung hervorruft.

[]int  ->  [slice int]

map[string]int  ->  [map string int]

Wir können ein komplizierteres Beispiel machen

var a map[int][]map[string]map[string][]string

var b [map int [slice [map string [map string [slice string]]]]]

Dieses Beispiel behält noch eine relativ klare Wirkung bei und hat gleichzeitig einen geringen Einfluss auf die Kompilierung.

Ich habe dieses Design in K implementiert und getestet und es funktioniert gut.

Ich denke, dieses Design hat einen gewissen Referenzwert und ist vielleicht diskussionswürdig.

Großartig

Nach einigem Hin und Her und mehrfachem erneuten Lesen unterstütze ich insgesamt den aktuellen Designentwurf für Contracts in Go. Ich weiß die Menge an Zeit und Mühe zu schätzen, die darin investiert wurde. Obwohl der Umfang, die Konzepte, die Implementierung und die meisten Kompromisse solide erscheinen, ist meine Sorge, dass die Syntax überarbeitet werden muss, um die Lesbarkeit zu verbessern.

Ich habe eine Reihe von vorgeschlagenen Änderungen aufgeschrieben, um dies zu beheben:

Die wichtigsten Punkte sind:

  • Methodenaufruf/Type-Assert-Syntax für Vertragserklärung
  • Der „leere Vertrag“
  • Trennzeichen ohne Klammern

Auf die Gefahr hin, dem Essay vorzugreifen, werde ich ein paar Syntax-Stücke ohne Erklärung geben, die aus Beispielen im aktuellen Contracts-Entwurfsentwurf konvertiert wurden. Beachten Sie, dass die F«T» Form der Trennzeichen illustrativ und nicht präskriptiv ist; Einzelheiten finden Sie in der Beschreibung.

type List«type Element contract{}» struct {
    next *List«Element»
    val  Element
}

und

contract viaStrings«To, From» {
    To.Set(string)
    From.String() string
}

func SetViaStrings«type To, From viaStrings»(s []From) []To {
    r := make([]To, len(s))
    for i, v := range s {
        r[i].Set(v.String())
    }
    return r
}

und

func Keys«type K comparable, V contract{}»(m map[K]V) []K {
    r := make([]K, 0, len(m))
    for k := range m {
        r = append(r, k)
    }
    return r
}

k := maps.Keys(map[int]int{1:2, 2:4})

und

contract Numeric«T» {
    T.(int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        complex64, complex128)
}

func DotProduct«type T Numeric»(s1, s2 []T) T {
    if len(s1) != len(s2) {
        panic("DotProduct: slices of unequal length")
    }
    var r T
    for i := range s1 {
        r += s1[i] * s2[i]
    }
    return r
}

Ohne Verträge unter der Haube wirklich zu ändern, ist dies für mich als Go-Entwickler viel besser lesbar. Ich fühle mich auch viel sicherer, diese Form jemandem beizubringen, der Go lernt (wenn auch spät im Lehrplan ).

@ianlancetaylor Basierend auf Ihrem Kommentar unter https://github.com/golang/go/issues/36533#issuecomment -579484523 poste ich in diesem Thread, anstatt ein neues Problem zu beginnen. Es ist auch auf der Generics Feedback Page aufgeführt. Ich bin mir nicht sicher, ob ich noch etwas tun muss, um es "offiziell in Betracht zu ziehen" (dh Go 2 Proposal Review Group ?) oder ob noch aktiv Feedback gesammelt wird.

Aus dem Entwurf der Vertragsgestaltung:

Warum nicht die Syntax F<T> wie C++ und Java verwenden?
Beim Analysieren von Code innerhalb einer Funktion, z. B. v := F<T> , ist es an dem Punkt, an dem < angezeigt wird, nicht eindeutig, ob wir eine Typinstanziierung oder einen Ausdruck sehen, der den < -Operator verwendet. Um dies zu lösen, ist eine effektiv unbegrenzte Vorausschau erforderlich. Im Allgemeinen bemühen wir uns, den Go-Parser einfach zu halten.

Steht nicht besonders im Widerspruch zu meinem letzten Post: Angle Brace Delimiters for Go Contracts

Nur einige Ideen, wie man diesen Punkt umgehen kann, an dem der Parser verwirrt wird. Paar Proben:

// Lifted from the design draft
func New<type K, V>(compare func(K, K) int) *Map<K, V> {
    return &Map{<K, V> compare: compare}
}

// ...

func (m *Map<K, V>) InOrder() *Iterator<K, V> {
    sender, receiver := chans.Ranger(<keyValue<K, V>>)
    var f func(*node<K, V>) bool
    f = func(n *node<K, V>) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue{<K, V> n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Im Wesentlichen nur eine andere Position für die Typparameter in Szenarien, in denen < mehrdeutig sein könnte.

@toolbox In Bezug auf Ihren Kommentar in spitzen Klammern. Danke, aber für mich persönlich liest sich diese Syntax so, als würde man zuerst eine Entscheidung treffen, dass wir spitze Klammern für Typparameter und Typargumente verwenden müssen, und dann einen Weg finden, sie einzuhämmern. Ich denke, wenn wir Generics zu Go hinzufügen, müssen wir zielen für etwas, das sich sauber und einfach in die bestehende Sprache einfügt. Ich glaube nicht, dass das Verschieben von spitzen Klammern in geschweiften Klammern dieses Ziel erreicht.

Ja, das ist ein kleines Detail, aber ich denke, dass bei der Syntax kleine Details sehr wichtig sind. Ich denke, wenn wir Typargumente und Parameter hinzufügen, müssen sie auf einfache und intuitive Weise funktionieren.

Ich behaupte sicher nicht, dass die Syntax im aktuellen Designentwurf perfekt ist, aber ich behaupte, dass sie sich problemlos in die bestehende Sprache einfügt. Was wir jetzt tun müssen, ist mehr Beispielcode zu schreiben, um zu sehen, wie gut es in der Praxis funktioniert. Ein wichtiger Punkt ist: Wie oft müssen Menschen tatsächlich Typargumente außerhalb von Funktionsdeklarationen schreiben, und wie verwirrend sind diese Fälle? Ich glaube nicht, dass wir es wissen.

Ist es eine gute Idee, [] für generische Typen und () für generische Funktionen zu verwenden? Dies würde eher mit den aktuellen Core-Generika übereinstimmen.

Könnte die Community darüber abstimmen? Persönlich würde ich _alles_ vorziehen, mehr Klammern hinzuzufügen, es ist schon schwierig, einige Funktionsdefinitionen für Closures usw. zu lesen, das fügt mehr Unordnung hinzu

Ich glaube nicht, dass eine Abstimmung eine gute Möglichkeit ist, eine Sprache zu entwerfen. Vor allem bei einer sehr schwer (wahrscheinlich unmöglich) zu bestimmenden und unglaublich großen Gruppe von Wahlberechtigten.

Ich vertraue darauf, dass die Go-Designer und die Community gemeinsam die beste Lösung finden und
Ich habe also nicht das Bedürfnis verspürt, irgendetwas in diesem Gespräch zu berücksichtigen.
Ich musste jedoch nur sagen, wie unerwartet erfreut ich von der war
Vorschlag der F«T»-Syntax.

(Andere Unicode-Klammern:
https://unicode-search.net/unicode-namesearch.pl?term=BRACKET.)

Beifall,

  • Bob

Am Freitag, den 1. Mai 2020 um 19:43 Uhr schrieb Matt Mc [email protected] :

Nach einigem Hin und Her und mehreren erneuten Lesungen unterstütze ich insgesamt die
aktueller Designentwurf für Contracts in Go. Ich schätze die Menge an Zeit
und der Aufwand, der darin steckt. Während Umfang, Konzepte,
Implementierung, und die meisten Kompromisse scheinen vernünftig zu sein, meine Sorge ist, dass die
Die Syntax muss überarbeitet werden, um die Lesbarkeit zu verbessern.

Ich habe eine Reihe von vorgeschlagenen Änderungen aufgeschrieben, um dies zu beheben:

Die wichtigsten Punkte sind:

  • Methodenaufruf/Type-Assert-Syntax für Vertragserklärung
  • Der „leere Vertrag“
  • Trennzeichen ohne Klammern

Auf die Gefahr hin, dem Essay zuvorzukommen, gebe ich ein paar Stücke ohne Unterstützung
Syntax, konvertiert aus Beispielen im aktuellen Contracts-Designentwurf. Notiz
dass die F«T»-Form von Trennzeichen veranschaulichend und nicht präskriptiv ist; sehen
die Beschreibung für Details.

type List«type element contract{}» struct {
nächstes *Liste«Element»
val-Element
}

und

Vertrag viaStrings«An, Von» {
To.Set(string)
From.String()-String
}
func SetViaStrings«type To, From viaStrings»(s []From) []To {
r := make([]To, len(s))
für i, v := Bereich s {
r[i].Set(v.String())
}
Rückkehr r
}

und

func Keys«Typ K vergleichbar, V Vertrag{}»(m map[K]V) []K {
r := make([]K, 0, len(m))
für k := Bereich m {
r = anhängen (r, k)
}
Rückkehr r
}
k := maps.Keys(map[int]int{1:2, 2:4})

und

Vertrag Numerisch«T» {
T.(int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64,
komplex64, komplex128)
}
func DotProduct«Typ T Numerisch»(s1, s2 []T) T {
if len(s1) != len(s2) {
panic("DotProduct: Slices ungleicher Länge")
}
var r T
für i := Bereich s1 {
r += s1[i] * s2[i]
}
Rückkehr r
}

Ohne die Verträge unter der Haube wirklich zu ändern, ist dies weit mehr
für mich als Go-Entwickler lesbar. Ich fühle mich auch viel selbstbewusster
diese Form jemandem beizubringen, der Go lernt (wenn auch spät in der
Lehrplan).

@ianlancetaylor https://github.com/ianlancetaylor Basierend auf Ihrem Kommentar
bei #36533 (Kommentar)
https://github.com/golang/go/issues/36533#issuecomment-579484523 Ich bin
in diesem Thread posten, anstatt ein neues Thema zu eröffnen. Es ist auch aufgeführt
auf der Generika-Feedback-Seite
https://github.com/golang/go/wiki/Go2GenericsFeedback . Nicht sicher, ob ich
etwas anderes tun müssen, um es "offiziell in Betracht zu ziehen" (dh Go 2
Proposal Review Group https://github.com/golang/go/issues/33892 ?) oder wenn
Feedback wird noch aktiv gesammelt.


Sie erhalten dies, weil Sie diesen Thread abonniert haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/15292#issuecomment-622657596 oder
Abmelden
https://github.com/notifications/unsubscribe-auth/AACQ2NJRBNLLDGY2XGCCQCLRPOCEHANCNFSM4CA35RXQ
.

Wir alle wollen die bestmögliche Syntax für Go. Der Designentwurf verwendet Klammern, da er mit dem Rest von Go funktionierte, ohne dass es zu erheblichen Parsing-Mehrdeutigkeiten kam. Wir sind bei ihnen geblieben, weil sie damals unserer Meinung nach die beste Lösung waren und weil es größere Fische zu braten gab. Bisher haben sie (Klammern) ziemlich gut gehalten.

Wenn am Ende des Tages eine viel bessere Notation gefunden wird, kann diese sehr einfach geändert werden, solange wir keine Kompatibilitätsgarantie einhalten müssen (der Parser ist trivial angepasst, und jeder Code kann konvertiert werden einfach mit gofmt).

@ianlancetaylor Vielen Dank für die Antwort, es wird geschätzt.

Sie haben Recht; Diese Syntax lautete "keine Klammern für Typargumente verwenden" und das auszuwählen, was meiner Meinung nach der beste Kandidat war, und dann Änderungen vorzunehmen, um zu versuchen, die Implementierungsprobleme mit dem Parser zu lösen.

Wenn die Syntax schwer zu lesen ist (auf einen Blick schwer zu erkennen, was vor sich geht), passt sie wirklich problemlos in die vorhandene Sprache? Da greift meiner Meinung nach die Haltung zu kurz.

Es ist wahr, dass der Typrückschluss die Menge an Typargumenten, die im Clientcode übergeben werden müssen, erheblich reduzieren könnte. Ich persönlich glaube, dass ein Bibliotheksautor danach streben sollte, dass Argumente vom Typ Null übergeben werden, wenn er seinen Code verwendet, und dennoch wird dies in der Praxis vorkommen.

Letzte Nacht bin ich zufällig auf die Template-Syntax für D gestoßen, die in mancher Hinsicht überraschend ähnlich ist:

template Square(T) {
    T Square(T t) {
        return t * t;
    }
}

writefln("The square of %s is %s", 3, Square!(int)(3));

template TCopy(T) {
    void copy(out T to, T from) {
        to = from;
    }
}

int i;
TCopy!(int).copy(i, 3);

Es gibt zwei wesentliche Unterschiede, die ich sehe:

  1. Sie haben ! als Instanziierungsoperator , um die Vorlagen zu verwenden.
  2. Ihr Deklarationsstil (keine mehrfachen Rückgabewerte, in Klassen verschachtelte Methoden) bedeutet, dass gewöhnlicher Code weniger Klammern enthält, sodass die Verwendung von Klammern für Typparameter nicht die gleiche visuelle Mehrdeutigkeit erzeugt.

Instanziierungsoperator

Bei der Verwendung von Contracts besteht die primäre visuelle Mehrdeutigkeit zwischen einer Instanziierung und einem Funktionsaufruf (oder einer Typkonvertierung oder ...?). Dies ist unter anderem deshalb problematisch, weil Instanziierungen zur Kompilierzeit und Funktionsaufrufe zur Laufzeit erfolgen. Go hat viele visuelle Hinweise, die einem Leser sagen, zu welchem ​​Lager jede Klausel gehört, aber die neue Syntax trübt diese, sodass es nicht offensichtlich ist, ob Sie sich Typen oder Programmablauf ansehen.

Ein erfundenes Beispiel:

// Instantiation with unexported types and then function call,
// or chained method call?
a := draw(square, ellipse)(canvas, color)

Vorschlag: Verwenden Sie einen Instanziierungsoperator , um Typparameter anzugeben. Die ! , die D verwendet, scheinen vollkommen akzeptabel zu sein. Einige Beispielsyntax:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map!(K, V) {
    return &Map!(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator!(K, V) {
    sender, receiver := chans.Ranger!(keyValue!(K, V))()
    var f func(*node!(K, V)) bool
    f = func(n *node!(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue!(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Aus meiner persönlichen Sicht ist der obige Code um eine Größenordnung einfacher zu lesen. Ich denke, das klärt alle Unklarheiten, sowohl visuell als auch für den Parser. Außerdem frage ich mich, ob dies die wichtigste Änderung sein könnte, die an Verträgen vorgenommen werden könnte.

Deklarationsstil

Beim Deklarieren von Typen, Funktionen und Methoden gibt es weniger "Laufzeit oder Kompilierzeit?" Problem. Ein Gopher sieht eine Zeile, die mit type oder func beginnt, und weiß, dass er eine Deklaration und kein Programmverhalten betrachtet.

Es bestehen jedoch noch einige visuelle Unklarheiten:

// Type-parameterized function,
// or function with multiple return values?
func Draw(cvs canvas, t tool)(canvas, tool) {
    // ...
}
func Draw(type canvas, tool)(cvs canvas, t tool) {
    // ...
}

// Type-parameterized struct, or function call?
func Set(elem constructible) rect {
    // ...
}
type Set(type Elem comparable) struct{
    // ...
}

// Method call, or type-parameterized function?
func Map(type Element)(s []Element, f func(Element) Element) (results []Element) {
    // ...
}
func (t Element) Map(s []Element, f func(Element) Element) (results []Element) {
    // ...
}

Die Gedanken:

  • Ich denke, dass diese Probleme weniger wichtig sind als das Instantiierungsproblem.
  • Die naheliegendste Lösung wäre, die für Typargumente verwendeten Trennzeichen zu ändern.
  • Möglicherweise könnte das Einfügen einer anderen Art von Operator oder Zeichen ( ! könnte verloren gehen, was ist mit # ?) die Dinge eindeutig machen.

EDIT: @griesemer danke für die zusätzliche Klarstellung!

Danke. Um nur die natürliche Frage zu stellen: Warum ist es wichtig zu wissen, ob ein bestimmter Aufruf zur Laufzeit oder zur Kompilierzeit ausgewertet wird? Warum ist das die entscheidende Frage?

@toolbox

// Instantiation with unexported types and then function call,
// or chained method call?
a := draw(square, ellipse)(canvas, color)

Warum sollte es so oder so wichtig sein? Für einen gelegentlichen Leser wäre es egal, ob dies ein Stück Code wäre, der während der Kompilierung oder Laufzeit ausgeführt wurde. Alle anderen können nur einen Blick auf die Definition der Funktion werfen, um zu wissen, was los ist. Ihre späteren Beispiele scheinen überhaupt nicht zweideutig zu sein.

Tatsächlich ist die Verwendung () für Typparameter sinnvoll, da es so aussieht, als würden Sie eine Funktion aufrufen, die eine Funktion zurückgibt - und das ist mehr oder weniger richtig. Der Unterschied besteht darin, dass die erste Funktion Typen akzeptiert, die normalerweise in Großbuchstaben geschrieben oder sehr bekannt sind.

In diesem Stadium ist es viel wichtiger, die Abmessungen des Schuppens herauszufinden, nicht seine Farbe.

Ich glaube nicht, dass das, wovon @tooolbox spricht, wirklich ein Unterschied zwischen Kompilierzeit und Laufzeit ist. Ja, das ist ein Unterschied, aber nicht der entscheidende. Wichtig ist: Ist das ein Funktionsaufruf oder eine Typdeklaration? Sie möchten es wissen, weil sie sich unterschiedlich verhalten, und Sie möchten nicht ableiten müssen, ob ein Ausdruck zwei oder einen Funktionsaufruf ausführt, denn das ist ein großer Unterschied. Das heißt, ein Ausdruck wie a := draw(square, ellipse)(canvas, color) ist mehrdeutig, ohne die Umgebung zu untersuchen.

Es ist wichtig, den Kontrollfluss des Programms visuell analysieren zu können. Ich denke, Go war ein großartiges Beispiel dafür.

Danke. Um nur die natürliche Frage zu stellen: Warum ist es wichtig zu wissen, ob ein bestimmter Aufruf zur Laufzeit oder zur Kompilierzeit ausgewertet wird? Warum ist das die entscheidende Frage?

Entschuldigung, anscheinend habe ich meine Kommunikation vermasselt. Das ist der entscheidende Punkt, den ich zu vermitteln versuchte:

Es ist nicht offensichtlich, ob Sie sich Typen oder den Programmablauf ansehen

(Im Moment wird eines während der Kompilierung aussortiert und das andere tritt zur Laufzeit auf, aber das sind ... Merkmale, nicht der Schlüsselpunkt, den @infogulch zu Recht aufgegriffen hat - danke!)


Ich habe an einigen Stellen die Meinung gehört, dass die Generika im Entwurf mit Funktionsaufrufen verglichen werden können: Es ist eine Art Funktion zur Kompilierzeit, die die tatsächliche Funktion oder den tatsächlichen Typ zurückgibt. Das ist zwar hilfreich als mentales Modell dessen, was während der Kompilierung passiert, aber es lässt sich nicht syntaktisch übersetzen. Syntaktisch sollten sie dann wie Funktionen benannt werden. Hier ist ein Beispiel:

// Example from the Contracts draft
Print(int)([]int{1, 2, 3})

// New naming that communicates behavior and intent
MakePrintFunc(int)([]int{1, 2, 3}) // Chained function call, great!

Dort sieht das tatsächlich wie eine Funktion aus, die eine Funktion zurückgibt; Ich finde das durchaus lesenswert.

Eine andere Möglichkeit wäre, alles mit Type , sodass aus dem Namen hervorgeht, dass Sie beim "Aufrufen" der Funktion einen Typ erhalten. Andernfalls ist es nicht offensichtlich, dass (zum Beispiel) Pair(...) eher einen Strukturtyp als eine Struktur erzeugt. Aber wenn diese Konvention vorhanden ist, wird dieser Code klar: a := drawType(square, ellipse)(canvas, color)

(Mir ist klar, dass ein Präzedenzfall die "-er"-Konvention für Schnittstellen ist.)

Beachten Sie, dass ich das Obige nicht besonders als Lösung unterstütze, ich veranschauliche nur, wie ich denke, dass "Generika als Funktionen" durch die aktuelle Syntax nicht vollständig und eindeutig ausgedrückt werden.


Auch hier hat @infogulch meinen Punkt sehr gut zusammengefasst. Ich unterstütze die visuelle Unterscheidung von Typargumenten, damit klar ist, dass sie Teil des Typs sind.

Vielleicht wird der visuelle Teil davon durch die Syntaxhervorhebung des Editors verbessert.

Ich weiß nicht viel über Parser und wie man nicht zu viel Vorausschau machen kann.

Aus Benutzersicht möchte ich kein weiteres Zeichen in meinem Code sehen, also würde «» meine Unterstützung nicht bekommen (ich habe sie nicht auf meiner Tastatur gefunden!).

Es ist jedoch auch nicht sehr angenehm, runde Klammern gefolgt von runden Klammern zu sehen.

Wie wäre es einfach mit geschweiften Klammern?

a := draw{square, ellipse}(canvas, color)

In Print(int)([]int{1,2,3}) ist der einzige Verhaltensunterschied jedoch "Kompilierzeit vs. Laufzeit". Ja, MakePrintFunc anstelle von Print würde diese Ähnlichkeit stärker betonen, aber… ist das nicht ein Argument dafür, MakePrintFunc nicht zu verwenden? Weil es tatsächlich den wirklichen Verhaltensunterschied verbirgt.

FWIW, wenn überhaupt, scheinen Sie zu argumentieren, unterschiedliche Trennzeichen für parametrische Funktionen und parametrische Typen zu verwenden. Weil Print(int) tatsächlich als Äquivalent zu einer Funktion betrachtet werden kann , die eine Funktion zurückgibt (ausgewertet zur Kompilierzeit), während Pair(int, string) dies nicht kann - es ist eine Funktion, die einen Typ zurückgibt . Print(int) ist tatsächlich ein gültiger Ausdruck, der zu einem func -Wert ausgewertet wird, während Pair(int, string) kein gültiger Ausdruck ist, sondern eine Typspezifikation. Der wirkliche Unterschied in der Verwendung ist also nicht "generische vs. nicht generische Funktionen", sondern "generische Funktionen vs. generische Typen". Und aus diesem Gesichtspunkt denke ich, dass es ein starkes Argument dafür gibt, () zumindest für parametrische Funktionen zu verwenden, weil es die Natur von parametrischen Funktionen betont, um tatsächlich Werte darzustellen - und vielleicht sollten wir <> verwenden

Ich denke, das Argument für () für parametrische Typen stammt aus der funktionalen Programmierung, wo diese funktionsrückgebenden Typen ein echtes Konzept namens Typkonstruktoren sind und tatsächlich als Funktionen verwendet und referenziert werden können. Und FWIW, das ist auch der Grund, warum ich nicht argumentieren würde, () nicht für parametrische Typen zu verwenden. Ich persönlich fühle mich mit diesem Konzept sehr wohl und würde den Vorteil weniger unterschiedlicher Trennzeichen dem Vorteil der Disambiguierung parametrischer Funktionen von parametrischen Typen vorziehen - schließlich haben wir kein Problem mit reinen Bezeichnern , die sich sowohl auf Typen als auch auf Werte beziehen .

Ich glaube nicht, dass das, wovon @tooolbox spricht, wirklich ein Unterschied zwischen Kompilierzeit und Laufzeit ist. Ja, das ist ein Unterschied, aber nicht der entscheidende. Wichtig ist: Ist das ein Funktionsaufruf oder eine Typdeklaration? Sie _wollen_ es wissen, weil sie sich unterschiedlich verhalten, und Sie möchten nicht ableiten müssen, ob ein Ausdruck zwei oder einen Funktionsaufruf ausführt, denn das ist ein großer Unterschied. Das heißt, ein Ausdruck wie a := draw(square, ellipse)(canvas, color) ist mehrdeutig, ohne die Umgebung zu untersuchen.

Es ist wichtig, den Kontrollfluss des Programms visuell analysieren zu können. Ich denke, Go war ein großartiges Beispiel dafür.

Typdeklarationen wären sehr leicht zu erkennen, da sie alle mit dem Schlüsselwort type beginnen. Ihr Beispiel gehört offensichtlich nicht dazu.

Vielleicht wird der visuelle Teil davon durch die Syntaxhervorhebung des Editors verbessert.

Ich denke, im Idealfall sollte die Syntax klar sein, egal welche Farbe sie hat. Das war bei Go der Fall, und ich glaube nicht, dass es gut wäre, von diesem Standard abzufallen.

Wie wäre es einfach mit geschweiften Klammern?

Ich glaube, dies steht leider im Konflikt mit einem Struct-Literal.

In Print(int)([]int{1,2,3}) ist der einzige Verhaltensunterschied jedoch "Kompilierzeit vs. Laufzeit". Ja, MakePrintFunc anstelle von Print würde diese Ähnlichkeit stärker betonen, aber… ist das nicht ein Argument dafür, MakePrintFunc nicht zu verwenden? Weil es tatsächlich den wirklichen Verhaltensunterschied verbirgt.

Nun, das ist zum einen der Grund, warum ich Print!(int)([]int{1,2,3}) statt MakePrintFunc(int)([]int{1,2,3}) unterstützen würde. Es ist klar, dass etwas Einzigartiges passiert.

Aber noch einmal die Frage, die @ianlancetaylor zuvor gestellt hat: Warum spielt es eine Rolle, ob der Typ Instanziierung/Funktionsrückgabefunktion Kompilierzeit oder Laufzeit ist?

Wenn Sie darüber nachdenken, wenn Sie einige Funktionsaufrufe geschrieben haben und der Compiler in der Lage war, sie zu optimieren und ihr Ergebnis zur Kompilierzeit zu berechnen, würden Sie sich über den Leistungsgewinn freuen! Der wichtige Aspekt ist vielmehr, was der Code tut, wie ist das Verhalten? Das sollte auf einen Blick ersichtlich sein.

Wenn ich Print(...) sehe, ist mein erster Instinkt "das ist ein Funktionsaufruf, der irgendwohin schreibt". Es kommuniziert nicht "dies wird eine Funktion zurückgeben". Meiner Meinung nach ist jedes davon besser, weil es das Verhalten und die Absicht kommunizieren kann:

  • MakePrintFunc(...)
  • Print!(...)
  • Print<...>

Mit anderen Worten, dieses Stück Code "verweist" oder "gibt mir in gewisser Weise" eine Funktion, die nun im folgenden Codestück aufgerufen werden kann.

FWIW, wenn überhaupt, scheinen Sie zu argumentieren, unterschiedliche Trennzeichen für parametrische Funktionen und parametrische Typen zu verwenden. ...

Nein, ich weiß, dass es in den letzten paar Beispielen um Funktionen ging, aber ich würde eine konsistente Syntax für parametrische Funktionen und parametrische Typen befürworten. Ich glaube nicht, dass das Go-Team Generics zu Go hinzufügen würde, es sei denn, es handelt sich um ein einheitliches Konzept mit einer einheitlichen Syntax.

Wenn ich Print(...) sehe, ist mein erster Instinkt "das ist ein Funktionsaufruf, der irgendwohin schreibt". Es kommuniziert nicht "dies wird eine Funktion zurückgeben".

func Print(…) func(…) auch nicht, wenn es als Print(…) aufgerufen wird. Trotzdem sind wir kollektiv damit einverstanden. Ohne eine spezielle Aufruf-Syntax, wenn eine Funktion ein func zurückgibt.
Die Print(…) -Syntax sagt Ihnen ziemlich genau, was sie heute tut: Dass Print eine Funktion ist, die einen Wert zurückgibt, was Print(…) auswertet. Wenn Sie an dem Typ interessiert sind, den die Funktion zurückgibt, sehen Sie sich ihre Definition an.
Oder, viel wahrscheinlicher, verwenden Sie die Tatsache, dass es sich tatsächlich um Print(…)(…) handelt, als Indikator dafür, dass es eine Funktion zurückgibt.

Wenn Sie darüber nachdenken, wenn Sie einige Funktionsaufrufe geschrieben haben und der Compiler in der Lage war, sie zu optimieren und ihr Ergebnis zur Kompilierzeit zu berechnen, würden Sie sich über den Leistungsgewinn freuen!

Sicher. Das haben wir schon. Und ich bin sehr froh, dass ich keine spezifischen syntaktischen Anmerkungen benötige, um sie besonders zu machen, sondern einfach darauf vertrauen kann, dass der Compiler kontinuierlich verbesserte Heuristiken darüber bereitstellt, um welche Funktionen es sich handelt.

Meiner Meinung nach ist jedes davon besser, weil es das Verhalten und die Absicht kommunizieren kann:

Beachten Sie, dass zumindest der erste zu 100 % mit dem Design kompatibel ist. Es schreibt keine Form für die verwendeten Identifikatoren vor, und ich hoffe, Sie schlagen nicht vor, dies vorzuschreiben (und wenn Sie dies tun, würde mich interessieren, warum die gleichen Regeln nicht für die Rückgabe von func gelten ).

Nein, ich weiß, dass es in den letzten paar Beispielen um Funktionen ging, aber ich würde eine konsistente Syntax für parametrische Funktionen und parametrische Typen befürworten.

Nun, ich stimme zu, wie gesagt :) Ich sage nur, dass ich nicht verstehe, wie die Argumente, die Sie vorbringen, entlang der Achse "generisch vs. nicht generisch" angewendet werden können, da es keine wichtigen Verhaltensänderungen dazwischen gibt die Zwei. Sie würden entlang der Achse "Typ vs. Funktion" Sinn machen, denn ob etwas eine Typspezifikation oder ein Ausdruck ist, ist sehr wichtig für den Kontext, in dem es verwendet werden kann. Ich würde immer noch nicht zustimmen, aber zumindest würde ich es verstehen Ihnen :)

@Merovius danke für deinen Kommentar.

func Print(…) func(…) auch nicht, wenn es als Print(…) aufgerufen wird. Trotzdem sind wir kollektiv damit einverstanden. Ohne spezielle Aufrufsyntax, wenn eine Funktion eine func.
Die Print(…) -Syntax sagt Ihnen ziemlich genau, was sie heute tut: Dass Print eine Funktion ist, die einen Wert zurückgibt, was Print(…) auswertet. Wenn Sie an dem Typ interessiert sind, den die Funktion zurückgibt, sehen Sie sich ihre Definition an.

Ich bin der Ansicht, dass der Name einer Funktion mit dem zusammenhängen sollte, was sie tut. Daher erwarte ich, dass Print(...) etwas ausgibt, unabhängig davon, was es zurückgibt. Ich glaube, dass dies eine vernünftige Erwartung ist, und eine, die in einem Großteil des bestehenden Go-Codes erfüllt werden könnte.

Wenn ich Print(...)(...) sehe, teilt dies mit, dass das erste () etwas ausgegeben hat und dass die Funktion eine Art Funktion zurückgegeben hat, und das zweite () dieses zusätzliche Verhalten ausführt .

(Ich wäre überrascht, wenn dies eine ungewöhnliche oder seltene Meinung wäre, aber ich würde einigen Umfrageergebnissen nicht widersprechen.)

Beachten Sie, dass zumindest der erste zu 100 % mit dem Design kompatibel ist. Es schreibt keine Form für die verwendeten Bezeichner vor, und ich hoffe, Sie schlagen nicht vor, dies vorzuschreiben (und wenn Sie dies tun, würde mich interessieren, warum die gleichen Regeln nicht für die einfache Rückgabe einer Funktion gelten).

Du hast verdammt Recht, dass ich das vorgeschlagen habe :)

Sehen Sie, ich habe die 3 Möglichkeiten aufgelistet, die mir einfallen, um die visuelle Mehrdeutigkeit zu beheben, die durch Typparameter für Funktionen und Typen eingeführt wird. Wenn Sie keine Mehrdeutigkeit sehen, wird Ihnen keiner der Vorschläge gefallen!

Ich sage nur, dass ich nicht verstehe, wie die Argumente, die Sie vorbringen, entlang der Achse "generisch vs. nicht generisch" angewendet werden können, da es keine wichtigen Verhaltensänderungen zwischen den beiden gibt. Sie wären entlang der Achse "Typ vs. Funktion" sinnvoll, denn ob etwas eine Typspezifikation oder ein Ausdruck ist, ist sehr wichtig für den Kontext, in dem es verwendet werden kann.

Siehe obige Punkte zur Mehrdeutigkeit und 3 Lösungsvorschläge.

Typparameter sind eine neue Sache.

  • Wenn wir sie als etwas Neues betrachten wollen, schlage ich vor, Trennzeichen zu ändern oder einen Instanziierungsoperator hinzuzufügen, um sie vollständig von normalem Code zu unterscheiden: Funktionsaufrufe, Typkonvertierungen usw.
  • Wenn wir sie nur als eine weitere Funktion betrachten wollen, schlage ich vor, diese Funktionen klar zu benennen, so dass identifier in identifier(...) das Verhalten und den Rückgabewert kommuniziert.

Ich bevorzuge ersteres. In beiden Fällen wären die Änderungen wie besprochen global über die Typparametersyntax hinweg.

Es gibt ein paar andere Möglichkeiten, Licht ins Dunkel zu bringen:

  1. Umfrage
  2. Lernprogramm

1. Umfrage

Vorwort: Dies ist keine Demokratie. Ich denke, dass Entscheidungen auf Daten basieren, und sowohl artikulierte Logik als auch umfassende Umfragedaten können den Entscheidungsprozess unterstützen.

Ich habe nicht die Mittel dazu, aber ich würde gerne wissen, was passieren würde, wenn Sie ein paar tausend Gophers auf "Ordnen Sie diese nach Klarheit" befragen würden.

Grundlinie:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map(K, V) {
    return &Map(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator(K, V) {
    sender, receiver := chans.Ranger(keyValue(K, V))()
    var f func(*node(K, V)) bool
    f = func(n *node(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Instanziierungsoperator:

// Lifted from the design draft
func New(type K, V)(compare func(K, K) int) *Map!(K, V) {
    return &Map!(K, V){compare: compare}
}

// ...

func (m *Map(K, V)) InOrder() *Iterator!(K, V) {
    sender, receiver := chans.Ranger!(keyValue!(K, V))()
    var f func(*node!(K, V)) bool
    f = func(n *node!(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue!(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Winkelstreben: (oder doppelte Winkelstreben, so oder so)

// Lifted from the design draft
func New<type K, V>(compare func(K, K) int) *Map<K, V> {
    return &Map<K, V>{compare: compare}
}

// ...

func (m *Map<K, V>) InOrder() *Iterator<K, V> {
    sender, receiver := chans.Ranger<keyValue<K, V>>()
    var f func(*node<K, V>) bool
    f = func(n *node<K, V>) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValue<K, V>{n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

Entsprechend benannte Funktionen:

// Lifted from the design draft
func NewConstructor(type K, V)(compare func(K, K) int) *MapType(K, V) {
    return &MapType(K, V){compare: compare}
}

// ...

func (m *MapType(K, V)) InOrder() *IteratorType(K, V) {
    sender, receiver := chans.RangerType(keyValueType(K, V))()
    var f func(*nodeType(K, V)) bool
    f = func(n *nodeType(K, V)) bool {
        if n == nil {
            return true
        }
        // Stop sending values if sender.Send returns false,
        // meaning that nothing is listening at the receiver end.
        return f(n.left) &&
            sender.Send(keyValueType(K, V){n.key, n.val}) &&
            f(n.right)
    }
    go func() {
        f(m.root)
        sender.Close()
    }()
    return &Iterator{receiver}
}

// ...

... Komisch, das letzte gefällt mir eigentlich ganz gut.

(Wie denken Sie, würden diese in der weiten Welt von Gophers @Merovius abschneiden ?)

2. Lernprogramm

Ich denke, das wäre eine sehr nützliche Übung: Schreiben Sie ein anfängerfreundliches Tutorial für Ihre Lieblingssyntax und lassen Sie es von einigen Leuten lesen und anwenden. Wie gut lassen sich die Konzepte vermitteln? Was sind die FAQ und wie beantworten Sie sie?

Der Designentwurf soll das Konzept erfahrenen Gophern vermitteln. Es folgt der Kette der Logik und taucht Sie langsam ein. Was ist die Kurzfassung? Wie erklären Sie die Goldenen Vertragsregeln in einem leicht verständlichen Blogbeitrag?

Dies könnte einen anderen Blickwinkel oder Datenausschnitt darstellen als typische Feedback-Berichte.

@tooolbox Ich denke, was Sie noch nicht beantwortet haben, ist: Warum ist dies ein Problem für parametrische Funktionen, aber nicht für nicht parametrische Funktionen, die ein func zurückgeben? Heute kann ich schreiben

func Print(a string) func(string) {
    return func(b string) {
        fmt.Println(a+b)
    }
}

func main() {
    Print("foo")("bar")
}

Warum ist das in Ordnung und führt nicht dazu, dass Sie durch die Mehrdeutigkeit super verwirrt sind, aber sobald Print einen Typparameter anstelle eines Wertparameters verwendet, wird dies unerträglich? Und würden Sie (abgesehen von den offensichtlichen Kompatibilitätsfragen) auch vorschlagen, dass wir eine Einschränkung hinzufügen, damit dies richtig funktioniert, dass dies nicht möglich sein sollte, es sei denn, Print wird für einige X in MakeXFunc umbenannt? X ? Wenn nein, warum nicht?

@tooolbox wäre dies wirklich ein Problem, wenn davon ausgegangen wird, dass die Typinferenz möglicherweise die Notwendigkeit beseitigt, die parametrischen Typen für Funktionen anzugeben, und nur ein einfach aussehender Funktionsaufruf übrig bleibt?

@Merovius Ich glaube nicht, dass das Problem bei der Syntax Print("foo")("bar") selbst liegt, da dies bereits in Go 1 möglich ist, gerade weil es eine einzige mögliche Interpretation gibt . Das Problem ist, dass mit dem unveränderten Vorschlag der Ausdruck Foo(X)(Y) jetzt mehrdeutig ist und bedeuten könnte, dass Sie zwei Funktionsaufrufe machen (wie in Go 1), oder es könnte bedeuten, dass Sie einen Funktionsaufruf mit Typargumenten machen . Das Problem besteht darin, lokal ableiten zu können, was das Programm tut, und diese beiden möglichen semantischen Interpretationen sind sehr unterschiedlich .

@urandom Ich stimme zu, dass der Typrückschluss möglicherweise den Großteil der explizit bereitgestellten Typparameter eliminieren kann, aber ich denke nicht, dass es eine gute Idee ist, die gesamte kognitive Komplexität in die dunklen Ecken der Sprache zu schieben, nur weil sie nur selten verwendet werden entweder. Auch wenn es selten genug vorkommt, dass die meisten Leute ihnen normalerweise nicht begegnen, werden sie trotzdem manchmal darauf stoßen, und zuzulassen, dass Code einen verwirrenden Kontrollfluss hat, solange es nicht "der meiste" Code ist, hinterlässt einen schlechten Geschmack in meinem Mund. Zumal Go derzeit so zugänglich ist, wenn "Sanitär" -Code einschließlich stdlib gelesen wird. Vielleicht ist Typinferenz so gut, dass „selten“ zu „nie“ wird und Go-Programmierer sehr diszipliniert bleiben und niemals ein System entwerfen, bei dem Typparameter erforderlich sind; dann ist dieses ganze Thema im Grunde strittig. Aber darauf wetten würde ich nicht.

Ich denke, das Hauptargument von @tooolbox ist, dass wir die vorhandene Syntax nicht unbekümmert mit kontextsensitiver Semantik überladen sollten und stattdessen eine andere Syntax finden sollten, die nicht mehrdeutig ist (Auch wenn es nur eine kleine Ergänzung ist, wie z Foo(X)!(Y) .) Ich denke, dies ist eine wichtige Maßnahme, wenn es um Syntaxoptionen geht.

Ich habe damals (~2008-2009) ein bisschen D -Code verwendet und gelesen, und ich muss sagen, dass ! mich immer zum Stolpern gebracht hat.

Lassen Sie mich diesen Schuppen stattdessen mit # , $ oder @ bemalen (da sie in Go oder C keine Bedeutung haben).
Dies könnte dann die Möglichkeit eröffnen, geschweifte Klammern ohne Verwirrung mit Maps, Slices oder Structs zu verwenden.

  • Foo@{X}(Y)
  • Foo${X}(Y)
  • Foo#{X}(Y)
    oder eckige Klammern.

In Diskussionen wie dieser ist es wichtig, sich echten Code anzusehen.

Bedenken Sie zum Beispiel, dass nur wenige Leute Foo(X)(Y) schreiben. In Go sehen Typnamen, Variablennamen und Funktionsnamen genau gleich aus, aber die Leute sind selten verwirrt darüber, was sie sehen. Die Leute verstehen, dass int64(v) eine Typkonvertierung und F(v) ein Funktionsaufruf ist, obwohl sie genau gleich aussehen.

Wir müssen uns echten Code ansehen, um zu sehen, ob Typargumente in der Praxis wirklich verwirrend sind. Wenn ja, müssen wir die Syntax anpassen. In Ermangelung von echtem Code wissen wir es einfach nicht.

Am Mittwoch, den 6. Mai 2020 um 13:00 Uhr schrieb Ian Lance Taylor:

Die Leute verstehen, dass int64(v) eine Typumwandlung ist und F(v) eine
Funktionsaufruf, obwohl sie genau gleich aussehen.

Ich habe im Moment weder eine Meinung noch eine Meinung zu dem Vorschlag
Syntax, aber ich denke nicht, dass dieses spezielle Beispiel sehr gut ist. Es kann
für eingebaute Typen wahr sein, aber das hat mich tatsächlich verwirrt
Genaues Problem selbst mehrmals (ich habe nach einer Funktion gesucht
Definition und sehr verwirrt darüber, wie der Code vorher funktioniert hat
Mir wurde klar, dass es sich wahrscheinlich um einen Typ handelte, und ich konnte die Funktion nicht finden, weil
es war überhaupt kein Funktionsaufruf). Nicht das Ende der Welt, und
wahrscheinlich überhaupt kein Problem für Leute, die ausgefallene IDEs mögen, aber ich habe
verschwendete ungefähr 5 Minuten damit, mehrmals danach zu suchen.

-Sam

--
Sam Whited

@ianlancetaylor Eine Sache, die mir an Ihrem Beispiel aufgefallen ist, ist, dass Sie eine Funktion schreiben können, die einen Typ akzeptiert und einen anderen Typ mit derselben Bedeutung int64(v) genauso sinnvoll wie strconv.Atoi(v) .

Aber während Sie UseConverter(strconv.Atoi) machen können, ist UseConverter(int64) in Go 1 nicht möglich. Die Klammer für den Typparameter könnte einige Möglichkeiten eröffnen, wenn das Generikum für Casting wie verwendet werden kann:

func StrToNumber(type K)(s string) K {
  asInt := strconb.Atoi(s)
  return K(asInt)
}

Warum ist das in Ordnung und führt nicht dazu, dass Sie durch die Mehrdeutigkeit super verwirrt sind?

Dein Beispiel ist nicht in Ordnung. Es ist mir egal, ob der erste Aufruf Argumente oder Typparameter übernimmt. Sie haben eine Print -Funktion, die nichts ausgibt. Können Sie sich vorstellen, diesen Code zu lesen/zu überprüfen? Print("foo") ohne den zweiten Satz von Klammern sieht gut aus, ist aber insgeheim ein no-op.

Wenn Sie mir diesen Code in einer PR übermittelt haben, würde ich Ihnen sagen, dass Sie den Namen in PrintFunc oder MakePrintFunc oder PrintPlusFunc oder etwas ändern sollen, das sein Verhalten kommuniziert.

Ich habe damals (~ 2008-2009) ein bisschen D-Code verwendet und gelesen, und ich muss sagen, das ! brachte mich immer wieder zum Stolpern.

Ha, interessant. Ich habe keine besondere Vorliebe für einen Instanziierungsoperator; das scheinen anständige Optionen zu sein.

In Go sehen Typnamen, Variablennamen und Funktionsnamen genau gleich aus, aber die Leute sind selten verwirrt darüber, was sie sehen. Die Leute verstehen, dass int64(v) eine Typkonvertierung und F(v) ein Funktionsaufruf ist, obwohl sie genau gleich aussehen.

Ich stimme zu, Leute können normalerweise schnell zwischen Typumwandlungen und Funktionsaufrufen unterscheiden. Warum denkst Du, das ist?

Meine persönliche Theorie ist, dass Typen normalerweise Substantive und Funktionen normalerweise Verben sind. Wenn Sie also Noun(...) sehen, ist es ziemlich klar, dass es sich um eine Typkonvertierung handelt, und wenn Sie Verb(...) sehen, handelt es sich um einen Funktionsaufruf.

Wir müssen uns echten Code ansehen, um zu sehen, ob Typargumente in der Praxis wirklich verwirrend sind. Wenn ja, müssen wir die Syntax anpassen. In Ermangelung von echtem Code wissen wir es einfach nicht.

Das macht Sinn.

Persönlich bin ich auf diesen Thread gestoßen, weil ich den Vertragsentwurf gelesen habe (wahrscheinlich 5 Mal, jedes Mal abprallend und dann weitergekommen, als ich später zurückkam) und die Syntax verwirrend und ungewohnt fand. Ich mochte die Konzepte, als ich sie schließlich grokkte, aber es gab eine große Barriere wegen der mehrdeutigen Syntax.

Am Ende des Vertragsentwurfs befindet sich eine Menge "echter Code", der all diese häufigen Anwendungsfälle behandelt, was großartig ist! Allerdings finde ich es schwierig, visuell zu analysieren; Ich lese und verstehe den Code langsamer. Es scheint mir, dass ich mir die Argumente der Dinge und den breiteren Kontext ansehen muss, um zu wissen, was die Dinge sind und was der Kontrollfluss ist, und es scheint, als wäre das ein Schritt nach unten vom regulären Code.

Nehmen wir diesen echten Code:

import "container/orderedmap"

var m = orderedmap.New(string, string)(strings.Compare)

func Add(a, b string) {
    m.Insert(a, b)
}

Wenn ich orderedmap.New( lese, erwarte ich, dass das Folgende die Argumente für die Funktion New sind, jene Schlüsselinformationen, die die geordnete Karte benötigt, um zu funktionieren. Aber die stehen eigentlich im zweiten Satz von Klammern. Ich bin davon geflasht. Es macht den Code schwieriger zu groken.

(Dies ist nur ein Beispiel, es ist nicht alles , was ich sehe, mehrdeutig, aber es ist schwierig, eine detaillierte Diskussion über eine breite Palette von Punkten zu führen.)

Folgendes würde ich vorschlagen:

// Instantiation operator
var m = orderedmap.New!(string, string)(strings.Compare)
// Alternate delimiters -- notice I don't insist on any particular kind
var m = orderedmap.New<|string, string|>(strings.Compare)
// Appropriately named function
var m = orderedmap.MakeConstructor(string, string)(strings.Compare)

In den ersten beiden Beispielen dient eine andere Syntax dazu, meine Annahme zu widerlegen, dass der erste Klammersatz die Argumente für New() enthält, sodass der Code weniger überraschend ist und der Fluss von einer hohen Ebene aus besser beobachtbar ist.

Die dritte Option verwendet Benennung, um den Fluss nicht überraschend zu machen. Ich erwarte jetzt, dass der erste Klammersatz die Argumente enthält, die zum Erstellen einer Konstruktorfunktion erforderlich sind, und ich erwarte, dass der Rückgabewert eine Konstruktorfunktion ist, die wiederum aufgerufen werden kann, um eine geordnete Karte zu erstellen.


Ich kann sicher Code im aktuellen Stil lesen. Ich konnte den gesamten Code im Vertragsentwurf lesen. Es ist nur langsamer, weil ich länger brauche, um es zu verarbeiten. Ich habe mein Bestes versucht, um zu analysieren, warum das so ist, und es zu melden: Zusätzlich zu dem orderedmap.New -Beispiel hat https://github.com/golang/go/issues/15292#issuecomment -623649521 eine gute Zusammenfassung , obwohl mir wahrscheinlich noch mehr einfallen könnte. Der Grad der Mehrdeutigkeit variiert zwischen den verschiedenen Beispielen.

Ich gebe zu, dass ich nicht die Zustimmung aller bekommen werde, da Lesbarkeit und Klarheit etwas subjektiv sind und möglicherweise vom Hintergrund und den bevorzugten Sprachen der Person beeinflusst werden. Ich denke jedoch, dass 4 Arten von Parsing-Mehrdeutigkeiten ein guter Indikator dafür sind, dass wir ein Problem haben.

import "container/orderedmap"

var m = orderedmap.NewOf(string, string)(strings.Compare)

func Add(a, b string) {
    m.Insert(a, b)
}

Ich denke, NewOf liest sich besser als New , weil New normalerweise eine Instanz zurückgibt, kein Generikum, das eine Instanz erstellt.


Sie haben eine Print -Funktion, die nichts ausgibt.

Um es klar zu sagen, da es eine gewisse automatische Typinferenz gibt, wäre das generische Print(foo) entweder ein echter Druckaufruf per Inferenz oder ein Fehler. In Go Today sind bloße Identifikatoren nicht zulässig :

package main

import (
    "fmt"
)

func main() {
    fmt.Println
}

./prog.go:8:5: fmt.Println evaluated but not used

Ich frage mich, ob es eine Möglichkeit gibt, die allgemeine Schlussfolgerung weniger verwirrend zu machen.

@toolbox

Dein Beispiel ist nicht in Ordnung. Es ist mir egal, ob der erste Aufruf Argumente oder Typparameter übernimmt. Sie haben eine Druckfunktion, die nichts druckt. Können Sie sich vorstellen, diesen Code zu lesen/zu überprüfen?

Sie haben hier die entsprechenden Anschlussfragen ausgelassen. Ich stimme dir zu, dass es nicht wirklich lesbar ist. Aber Sie plädieren für eine sprachliche Durchsetzung dieser Einschränkung. Ich habe nicht gesagt "Sie sind damit einverstanden", was "Sie sind mit diesem Code einverstanden", sondern mit "Sie sind mit der Sprache einverstanden , die diesen Code zulässt".

Das war meine Anschlussfrage. Glauben Sie, dass Go eine schlechtere Sprache ist, weil es keine Namensbeschränkung für Funktionen, die func zurückgeben, eingeführt hat? Wenn nicht, warum wäre es eine schlechtere Sprache, wenn wir solche Funktionen nicht einschränken, wenn sie ein Typargument anstelle eines Wertarguments annehmen?

@Merovius

Aber Sie plädieren für eine sprachliche Durchsetzung dieser Einschränkung.

Nein, er argumentiert, dass das Verlassen auf Benennungsstandards eine mögliche gültige Lösung für das Problem ist. Eine informelle Regel wie "Typautoren werden ermutigt, ihre generischen Typen so zu benennen, dass sie weniger leicht mit dem Namen einer Funktion verwechselt werden können" ist eine gültige Lösung für das Mehrdeutigkeitsproblem, da sie das Problem in Einzelfällen buchstäblich lösen würde.

Er weist nirgendwo darauf hin, dass diese Lösung durch die Sprache erzwungen werden muss , er sagt, dass selbst dann , wenn die Betreuer entscheiden, den aktuellen Vorschlag unverändert zu lassen, es potenzielle praktische Lösungen für das Mehrdeutigkeitsproblem gibt. Und er behauptet, dass das Mehrdeutigkeitsproblem real und wichtig zu berücksichtigen ist.

Edit: Ich glaube, wir weichen etwas vom Kurs ab. Ich denke, mehr "echter" Beispielcode wäre für die Konversation an dieser Stelle sehr vorteilhaft.

Nein, er argumentiert, dass das Verlassen auf Benennungsstandards eine mögliche gültige Lösung für das Problem ist.

Sind sie? Ich habe versucht, konkret zu fragen:

Beachten Sie, dass zumindest der erste zu 100 % mit dem Design kompatibel ist. Es schreibt keine Form für die verwendeten Bezeichner vor, und ich hoffe, Sie schlagen nicht vor, dies vorzuschreiben (und wenn Sie dies tun, würde mich interessieren, warum die gleichen Regeln nicht für die einfache Rückgabe einer Funktion gelten).

Du hast verdammt Recht, dass ich das vorgeschlagen habe :)

Ich stimme zu, dass "vorschreiben" hier nicht sehr spezifisch ist, aber das ist zumindest die Frage, die ich beabsichtigt habe. Wenn sie tatsächlich nicht für eine in das Design integrierte Anforderung des Sprachniveaus argumentieren, entschuldige ich mich natürlich für das Missverständnis. Aber ich fühle mich berechtigt anzunehmen, dass "vorschreiben" zumindest stärker ist als "eine informelle Regel". Vor allem, wenn sie in den Kontext der anderen beiden Vorschläge gestellt werden, die sie (auf der gleichen Grundlage) vorbringen, bei denen es sich um Konstrukte auf Sprachebene handelt, da sie nicht einmal derzeit gültige Identifikatoren verwenden.

Wird es einen vgo -ähnlichen Plan geben, der es der Community ermöglicht, den neuesten generischen Vorschlag auszuprobieren?

Nachdem ich ein bisschen mit dem vertragsfähigen Spielplatz herumgespielt habe, verstehe ich nicht wirklich, was die ganze Aufregung darüber ist, zwischen den Typargumenten und den regulären unterscheiden zu müssen.

Betrachten Sie dieses Beispiel . Ich habe die Typinitialisierer bei allen Funktionen belassen, obwohl ich sie alle weglassen könnte und es trotzdem gut kompilieren würde. Dies scheint darauf hinzudeuten, dass die überwiegende Mehrheit eines solchen potenziellen Codes sie nicht einmal enthalten würde, was wiederum keine Verwirrung stiften würde.

Falls diese Typparameter enthalten sind, können jedoch bestimmte Beobachtungen gemacht werden:
a) die Typen sind entweder die eingebauten, die jeder kennt und sofort identifizieren kann
b) Die Typen sind von Drittanbietern und in diesem Fall TitleCased, wodurch sie ziemlich auffallen würden. Ja, es wäre möglich, wenn auch unwahrscheinlich, dass es sich um eine Funktion handelt, die eine andere Funktion zurückgibt, und der erste Aufruf verbraucht exportierte Variablen von Drittanbietern, aber ich denke, das ist äußerst selten.
c) die Typen sind einige private Typen. In diesem Fall würden sie eher wie normale Variablenbezeichner aussehen. Da sie jedoch nicht exportiert werden, würde dies bedeuten, dass der Code, den der Leser betrachtet, nicht Teil einer Dokumentation ist, die er zu entschlüsseln versucht, und, was noch wichtiger ist, er liest den Code bereits. Daher können sie den zusätzlichen Schritt ausführen und einfach zur Definition der Funktion springen, um Unklarheiten zu beseitigen.

Die Aufregung dreht sich darum, wie es ohne Generika aussieht https://play.golang.org/p/7BRdM2S5dwQ und für jemanden, der neu darin ist, einen separaten Stack für jeden Typ zu programmieren, wie StackString, StackInt, ... ist viel einfacher zu programmieren dann ein Stack(T) im aktuellen generischen Syntaxvorschlag. Ich habe keinen Zweifel, dass der aktuelle Vorschlag gut durchdacht ist, wie Ihr Beispiel zeigt, aber der Wert von Einfachheit und Klarheit wird erheblich verringert. Ich verstehe, dass die erste Priorität darin besteht, durch Testen herauszufinden, ob es funktioniert, aber sobald wir uns einig sind, dass der aktuelle Vorschlag die meisten Fälle abdeckt und es keine technischen Compiler-Schwierigkeiten gibt, ist es eine noch höhere Priorität, ihn für alle verständlich zu machen, was immer der Hauptgrund dafür war Gehen Sie von Anfang an erfolgreich.

@Merovius Nein, es ist wie @infogulch sagte, ich meinte, eine Konvention à la -er über Schnittstellen zu erstellen. Das habe ich oben erwähnt, sorry für die Verwirrung. (Ich bin übrigens ein „er“.)

Betrachten Sie dieses Beispiel. Ich habe die Typinitialisierer bei allen Funktionen belassen, obwohl ich sie alle weglassen könnte und es trotzdem gut kompilieren würde. Dies scheint darauf hinzudeuten, dass die überwiegende Mehrheit eines solchen potenziellen Codes sie nicht einmal enthalten würde, was wiederum keine Verwirrung stiften würde.

Wie wäre es mit demselben Beispiel in einer gegabelten Version des Generika-Spielplatzes?

Ich habe ::<> für die Typparameterklausel verwendet, und wenn es einen einzelnen Typ gibt, können Sie das <> weglassen. Die eckigen geschweiften Klammern sollten keine Mehrdeutigkeit des Parsers aufweisen, und es erleichtert mir das Lesen des Codes – sowohl der Generika als auch des Codes, der die Generika verwendet. (Und wenn die Typparameter abgeleitet werden, umso besser.)

Wie ich bereits sagte, blieb ich nicht bei ! für die Typeninstanziierung hängen (und ich denke, dass :: nach Überprüfung besser aussieht). Und es hilft nur dabei, wo die Generika verwendet werden, nicht so sehr bei den Deklarationen. Das kombiniert also etwas die beiden, wobei <> weggelassen wird, wo es unnötig ist, ähnlich wie das Weglassen () für Funktionsrückgabeparameter, wenn es nur einen gibt.

Beispielauszug:

type Stack::<type E> []E

func (s Stack::E) Peek() E {
    return s[len(s)-1]
}

func (s *Stack::E) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack::E) Push(value E) {
    *s = append(*s, value)
}

type StackIterator::<type E> struct{
    stack Stack::E
    current int
}

func (s *Stack::E) Iter() Iterator::E {
    it := StackIterator::E{stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator::E) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator::E) Value() E { 
    if i.current < 0 {
        var zero E
        return zero
    }

    return i.stack[i.current]
}

// ...

var it Iterator::string = stack.Iter()

it = Filter::string(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::<string, string>(it, func(s string) string {
    return s + ":1"
})

it = Distinct::string(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

Für dieses Beispiel habe ich auch die Variablennamen angepasst, ich denke, E für "Element" ist besser lesbar als T für "Type".

Wie ich bereits sagte, wird der zugrunde liegende Go-Code sichtbar, indem das Generika-Zeug anders aussieht. Sie wissen, was Sie sehen, der Kontrollfluss ist offensichtlich, es gibt keine Mehrdeutigkeit usw.

Es ist auch gut mit mehr Typrückschluss:

var it Iterator::string = stack.Iter()

it = Filter(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::<string, string>(it, func(s string) string {
    return s + ":1"
})

it = Distinct(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

@tooolbox Entschuldigung, dann haben wir aneinander vorbei geredet :)

jemand, der neu darin ist, einen separaten Stack für jeden Typ zu programmieren, wie StackString, StackInt, ..., ist viel einfacher zu programmieren als ein Stack (T)

Es würde mich wirklich wundern, wenn das so wäre. Niemand ist unfehlbar, und der erste Fehler, der sich auch nur in ein einfaches Stück Code einschleicht, zeigt, wie falsch diese Aussage auf lange Sicht ist.

Der Sinn meines Beispiels bestand darin, die Verwendung parametrischer Funktionen und ihre Instanziierung mit konkreten Typen zu veranschaulichen, was der springende Punkt dieser Diskussion ist, und nicht, ob die beispielhafte Stack -Implementierung gut war oder nicht.

Der Sinn meines Beispiels bestand darin, die Verwendung parametrischer Funktionen und ihre Instanziierung mit konkreten Typen zu veranschaulichen, was der springende Punkt dieser Diskussion ist, und nicht, ob die Beispiel-Stack-Implementierung gut war oder nicht.

Ich glaube nicht, dass @gertcuykens beabsichtigte, Ihre Stack-Implementierung zu klopfen, es scheint, als ob er das Gefühl hatte, dass die generische Syntax ungewohnt und schwer zu verstehen ist.

Falls diese Typparameter enthalten sind, können jedoch bestimmte Beobachtungen gemacht werden:
(A B C D)...

Ich sehe alle Ihre Punkte, schätze Ihre Analyse, und sie sind nicht falsch. Sie haben Recht, dass Sie in den meisten Fällen durch genaues Untersuchen des Codes feststellen können, was er tut. Ich denke nicht, dass dies die Berichte von Go-Entwicklern widerlegt , die sagen, dass die Syntax verwirrend oder mehrdeutig ist oder länger zum Lesen braucht, selbst wenn sie sie schließlich lesen können.

Im Allgemeinen befindet sich die Syntax in einem unheimlichen Tal. Der Code macht etwas anderes, aber er sieht den vorhandenen Konstrukten so ähnlich, dass Ihre Erwartungen über den Haufen geworfen werden und die Übersichtlichkeit sinkt. Sie können auch keine neuen Erwartungen wecken, weil diese Elemente (sinnvollerweise) optional sind, sowohl im Ganzen als auch in Teilen.

Für diese spezifischeren pathologischen Fälle hat @infogulch es gut ausgedrückt:

Ich denke auch nicht, dass es eine gute Idee ist, die ganze kognitive Komplexität in die dunklen Ecken der Sprache zu schieben, nur weil sie nur selten verwendet werden. Auch wenn es selten genug vorkommt, dass die meisten Leute ihnen normalerweise nicht begegnen, werden sie trotzdem manchmal darauf stoßen, und zuzulassen, dass Code einen verwirrenden Kontrollfluss hat, solange es nicht "der meiste" Code ist, hinterlässt einen schlechten Geschmack in meinem Mund.

Ich denke, an diesem Punkt erreichen wir die Artikulationssättigung zu diesem speziellen Teil des Themas. Egal, wie viel wir darüber reden, der Härtetest wird sein, wie schnell und wie gut Go-Entwickler es lernen, lesen und schreiben können.

(Und ja, bevor darauf hingewiesen wird, die Last sollte beim Bibliotheksautor liegen, nicht beim Client-Entwickler, aber ich glaube nicht, dass wir den Boost-Effekt wollen, bei dem generische Bibliotheken für den Mann auf der Straße unverständlich sind. Ich auch nicht Ich möchte nicht, dass Go zu einem generischen Jamboree wird, aber teilweise vertraue ich darauf, dass die Auslassungen des Designs die Verbreitung einschränken werden.)

Wir haben einen Spielplatz und wir können Forks für andere Syntaxen erstellen , was fantastisch ist. Vielleicht brauchen wir noch mehr Tools!

Die Leute haben Feedback gegeben . Ich bin sicher, dass mehr Feedback benötigt wird, und vielleicht brauchen wir bessere oder optimiertere Feedbacksysteme.

@tooolbox Glaubst du, es ist möglich, den Code zu parsen, wenn du <> und type immer so weglässt? Erfordert vielleicht einen strengeren Vorschlag, was getan werden kann, aber vielleicht ist es den Kompromiss wert?

type Stack::E []E

func (s Stack::E) Peek() E {
    return s[len(s)-1]
}

func (s *Stack::E) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack::E) Push(value E) {
    *s = append(*s, value)
}

type StackIterator::E struct{
    stack Stack::E
    current int
}

func (s *Stack::E) Iter() Iterator::E {
    it := StackIterator::E{stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator::E) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator::E) Value() E { 
    if i.current < 0 {
        var zero E
        return zero
    }

    return i.stack[i.current]
}

// ...

var it Iterator::string = stack.Iter()

it = Filter::string(it, func(s string) bool {
    return s == "foo" || s == "beta" || s == "delta"
})

it = Map::string, string (it, func(s string) string {
    return s + ":1"
})

it = Distinct::string(it)

println(Reduce(it, "", func(a, b string) string {
    if a == "" {
        return b
    }
        return a + ":" + b
}))

Ich weiß nicht warum, aber dieses Map::string, string (... fühlt sich einfach komisch an. Es sieht so aus, als ob dies 2 Token erstellt, einen Map::string und einen string Funktionsaufruf.

Auch wenn dies in Go nicht verwendet wird, kann die Verwendung von „Identifier :: Identifier“ bei Erstbenutzern den falschen Eindruck erwecken, dass sie denken, dass es eine Filter -Klasse/einen Namensraum mit string gibt

Glauben Sie, dass es möglich ist, den Code zu analysieren, wenn Sie <> immer weglassen und so eingeben? Erfordert vielleicht einen strengeren Vorschlag, was getan werden kann, aber vielleicht ist es den Kompromiss wert?

Nein, ich glaube nicht. Ich stimme @urandom zu, dass das Leerzeichen ohne etwas Umschließendes wie zwei Token aussieht. Ich persönlich mag auch den Umfang von Contracts und bin nicht daran interessiert, seine Fähigkeiten zu ändern.

Auch wenn dies in Go nicht verwendet wird, kann die Verwendung von "Identifier::Identifier" bei Erstbenutzern den falschen Eindruck erwecken, dass sie denken, dass es eine Filterklasse/einen Namensraum mit einer Zeichenfolgenfunktion darin gibt. Die Wiederverwendung von Token aus anderen weit verbreiteten Sprachen für etwas völlig anderes wird viel Verwirrung stiften.

Ich habe noch keine Sprache mit :: verwendet, aber ich habe sie überall gesehen. Vielleicht ist ! dann besser, weil es zu D passen würde, obwohl ich finde, dass :: visuell besser aussieht.

Wenn wir diesen Weg einschlagen würden, kann es viele Diskussionen darüber geben, welche Zeichen wir verwenden sollen. Hier ist ein Versuch, das Gesuchte einzugrenzen:

  • Etwas anderes als bloßes identifier() , damit es nicht wie ein Funktionsaufruf aussieht.
  • Etwas, das mehrere Typparameter einschließen kann, um sie visuell so zu vereinen, wie es Klammern können.
  • Etwas, das mit der Kennung verbunden aussieht, also wie eine Einheit aussieht.
  • Etwas, das für den Parser nicht mehrdeutig ist.
  • Etwas, das nicht mit einem anderen Konzept kollidiert, das eine starke Entwicklermeinung hat.
  • Wenn möglich, etwas, das sowohl die Definitionen als auch die Verwendung von Generika beeinflusst, damit diese auch leichter zu lesen sind.

Es gibt viele Dinge, die passen könnten.

  • identifier!(a, b) ( Spielplatz )
  • identifier@(a, b)
  • identifier#(a, b)
  • identifier$(a, b)
  • identifier<:a, b:>
  • identifier.<a, b> es ist wie eine Typaussage!
  • identifier:<a, b>
  • etc.

Hat jemand eine Idee, wie man die Menge der Potenziale weiter eingrenzen kann?

Nur eine kurze Anmerkung, dass wir all diese Ideen in Betracht gezogen haben, und wir haben auch Ideen wie in Betracht gezogen

func F(T : a, b T) { }
func G() { F(int : 1, 2) }

Aber auch hier gilt: Probieren geht über Studieren. Abstrakte Diskussionen ohne Code sind sinnvoll, führen aber nicht zu endgültigen Schlussfolgerungen.

(Nicht sicher, ob darüber schon gesprochen wurde) Ich sehe, dass wir in Fällen, in denen wir eine Struktur erhalten, nicht in der Lage sein werden, eine vorhandene API zu "erweitern", um generische Typen zu verarbeiten, ohne vorhandenen Aufrufcode zu beschädigen.

Zum Beispiel angesichts dieser nicht generischen Funktion

func Repeat(v, n int) []int {
    var r []int
    for i := n; i > 0; i-- {
        r = append(r, v)
    }
    return r
}

Repeat(4, 4)

Wir können es generisch machen, ohne die Abwärtskompatibilität zu brechen

func Repeat(type T)(v T, n int) []T {
    var r []T
    for i := n; i > 0; i-- {
        r = append(r, v)
    }
    return r
}

Repeat("a", 5)

Aber wenn wir dasselbe mit einer Funktion machen wollen, die ein generisches struct erhält

type XY struct {
    X, Y int
}

func RangeRepeat(arr []XY) []int {
    var r []int
    for _, n := range arr {
        for i := n.Y; i > 0; i-- {
            r = append(r, n.X)
        }
    }
    return r
}

RangeRepeat([]XY{{1, 1}, {2, 2}, {3, 3}})

Es scheint, als müsste der Aufrufcode aktualisiert werden

type XY(type T) struct {
    X T
    Y int
}

func RangeRepeat(type T)(arr []XY(T)) []T {
    var r []T
    for _, n := range arr {
        for i := n.Y; i > 0; i-- {
            r = append(r, n.X)
        }
    }
    return r
}

// error: cannot use generic type XY(type T any) without instantiation
// RangeRepeat([]XY{{1, 1}, {2, 2}, {3, 3}}) // error in old code
RangeRepeat([](XY(int)){{1, 1}, {2, 2}, {3, 3}}) // API changed
// RangeRepeat([]XY{{"1", 1}, {"2", 2}, {"3", 3}}) // error
RangeRepeat([](XY(string)){{"1", 1}, {"2", 2}, {"3", 3}}) // ok

Es wäre großartig, Typen auch von Strukturen ableiten zu können.

@ianlancetaylor

Der Vertragsentwurf erwähnt, dass methods may not take additional type arguments . Es wird jedoch nicht erwähnt, dass der Vertrag für bestimmte Methoden ersetzt wird. Eine solche Funktion wäre sehr nützlich, um Schnittstellen zu implementieren, die davon abhängen, an welchen Vertrag ein parametrischer Typ gebunden ist.

Haben Sie eine solche Möglichkeit besprochen?

Noch eine Frage zum Vertragsentwurf. Werden Typdisjunktionen auf eingebaute Typen beschränkt? Wenn nicht, wäre es möglich, parametrisierte Typen zu verwenden, insbesondere Schnittstellen in der Disjunktionsliste?

Etwas wie

type Getter(T) interface {
    Get() T
}

contract(G, T) {
    G Getter(T)
}

wäre sehr nützlich, nicht nur um das Duplizieren des Methodensatzes von der Schnittstelle zum Vertrag zu vermeiden, sondern auch um einen parametrisierten Typ zu instanziieren, wenn der Typrückschluss fehlschlägt und Sie keinen Zugriff auf den konkreten Typ haben (z. B. er wird nicht exportiert).

@ianlancetaylor Ich bin mir nicht sicher, ob dies bereits besprochen wurde, aber ist es in Bezug auf die Syntax für Typargumente für eine Funktion möglich, die Argumentliste mit der Typargumentliste zu verketten? Also für das Diagrammbeispiel, anstatt

var g = graph.New(*Vertex, *FromTo)([]*Vertex{ ... })

du würdest verwenden

var g = graph.New(*Vertex, *FromTo, []*Vertex{ ... })

Im Wesentlichen entsprechen die ersten K Argumente der Argumentliste einer Typenargumentliste der Länge K. Der Rest der Argumentliste entspricht den regulären Argumenten der Funktion. Dies hat den Vorteil, dass die Syntax von gespiegelt wird

make(Type, size)

die einen Typ als erstes Argument nimmt.

Dies würde die Grammatik vereinfachen, benötigt jedoch Typinformationen, um zu wissen, wo die Typargumente enden und die regulären Argumente beginnen.

@ smasher164 Er sagte ein paar Kommentare zurück, dass sie es in Betracht gezogen haben (was impliziert, dass sie es verworfen haben, obwohl ich neugierig bin, warum).

func F(T : a, b T) { }
func G() { F(int : 1, 2) }

Das schlagen Sie vor, aber mit einem Doppelpunkt, um die beiden Arten von Argumenten zu trennen. Mir persönlich gefällt es mäßig, obwohl es ein unvollständiges Bild ist; Was ist mit Typdeklaration, Methoden, Instanziierung usw.

Ich möchte auf etwas zurückkommen, das @Inuart gesagt hat:

Wir können es generisch machen, ohne die Abwärtskompatibilität zu brechen

Würde das Go-Team erwägen, die Standardbibliothek auf diese Weise zu ändern, um mit der Kompatibilitätsgarantie von Go 1 vereinbar zu sein? Was wäre zum Beispiel, wenn strings.Repeat(s string, count int) string durch Repeat(type S stringlike)(s S, count int) S ersetzt würde? Sie könnten auch einen //Deprecated -Kommentar zu bytes.Repeat hinzufügen, aber ihn dort belassen, damit alter Code verwendet werden kann. Ist das etwas, was das Go-Team in Betracht ziehen würde?

Bearbeiten: Um es klar zu sagen, ich meine, würde dies innerhalb von Go1Compat im Allgemeinen berücksichtigt werden? Ignorieren Sie das spezifische Beispiel, wenn es Ihnen nicht gefällt.

@carlmjohnson Nein. Dieser Code würde brechen: f := strings.Repeat , da polymorphe Funktionen nicht referenziert werden können, ohne sie zuerst zu instanziieren.

Und von da an denke ich, dass die Verkettung von Typargumenten und Wertargumenten ein Fehler wäre, da es eine natürliche Syntax zum Verweisen auf eine instanziierte Version einer Funktion verhindert. Es wäre natürlicher, wenn Go schon Curry hätte, aber das tut es nicht. Es sieht seltsam aus, wenn foo(int, 42) und foo(int) Ausdrücke sind und beide sehr unterschiedliche Typen haben.

@urandom Ja, wir haben die Möglichkeit diskutiert, zusätzliche Einschränkungen für die Typparameter einer einzelnen Methode hinzuzufügen. Dies würde dazu führen, dass der Methodensatz des parametrisierten Typs basierend auf den Typargumenten variiert. Dies kann nützlich oder verwirrend sein, aber eines scheint sicher: Wir können es später hinzufügen, ohne etwas zu beschädigen. Also haben wir die Idee auf später verschoben. Danke, dass du es angesprochen hast.

Was genau in der zulässigen Typenliste aufgeführt werden kann, ist nicht so klar wie möglich. Ich glaube, da haben wir noch mehr zu tun. Beachten Sie, dass zumindest im aktuellen Entwurfsentwurf die Auflistung eines Schnittstellentyps in der Liste der Typen derzeit bedeutet, dass das Typargument dieser Schnittstellentyp sein kann. Dies bedeutet nicht, dass das Typargument ein Typ sein kann, der diesen Schnittstellentyp implementiert. Ich denke, es ist derzeit unklar, ob es sich um eine instanziierte Instanz eines parametrisierten Typs handeln kann. Es ist aber eine gute Frage.

@smasher164 @tooolbox Die zu berücksichtigenden Fälle beim Kombinieren von Typparametern und regulären Parametern in einer einzigen Liste sind, wie sie getrennt werden (falls sie getrennt sind) und wie mit dem Fall umgegangen wird, in dem es keine regulären Parameter gibt (vermutlich können wir ausschließen im Fall ohne Typparameter). Wenn es beispielsweise keine regulären Parameter gibt, wie unterscheiden Sie zwischen dem Instanziieren der Funktion, ohne sie aufzurufen, und dem Instanziieren der Funktion und ihrem Aufruf? Während letzteres eindeutig der häufigere Fall ist, ist es vernünftig, dass die Leute in der Lage sein möchten, den ersteren Fall zu schreiben.

Wenn die Typparameter in die gleichen Klammern gesetzt werden sollten wie die regulären Parameter, dann sagte @griesemer in #36177 (seinem zweiten Beitrag), dass er die Verwendung eines Semikolons anstelle eines Doppelpunkts als Trennzeichen sehr mochte, weil (als Ergebnis des automatischen Einfügens von Semikolons) ermöglichte es, die Parameter auf nette Weise auf mehrere Zeilen zu verteilen.

Ich persönlich mag auch die Verwendung vertikaler Balken ( |..| ) zum Einschließen der Typparameter, da Sie diese manchmal in anderen Sprachen (Ruby, Crystal usw.) zum Einschließen eines Parameterblocks sehen. Also hätten wir Sachen wie:

func F(|T| a, b T) { }
func G() { F(|int| 1, 2) }

Zu den Vorteilen gehören:

  • Sie bieten eine schöne visuelle Unterscheidung (zumindest in meinen Augen) zwischen Typ und regulären Parametern.
  • Sie müssten das Schlüsselwort type nicht verwenden.
  • Keine regulären Parameter zu haben, ist kein Problem.
  • Das vertikale Strichzeichen ist natürlich im ASCII-Satz enthalten und sollte daher auf den meisten Tastaturen verfügbar sein.

Sie können es möglicherweise sogar außerhalb der Klammern verwenden, aber vermutlich hätten Sie dann die gleichen Parsing-Schwierigkeiten wie bei <...> oder [...] , da es möglicherweise mit dem bitweisen 'or'-Operator verwechselt werden könnte die Schwierigkeiten wären weniger akut.

Ich verstehe nicht, wie vertikale Balken bei fehlenden regulären Parametern helfen. Ich verstehe nicht, wie Sie eine Funktionsinstanziierung von einem Funktionsaufruf unterscheiden können.

Eine Möglichkeit, zwischen diesen beiden Fällen zu unterscheiden, wäre, das Schlüsselwort type zu verlangen, wenn Sie die Funktion instanziieren, aber nicht, wenn Sie sie aufrufen, was, wie Sie bereits sagten, der häufigere Fall ist.

Ich stimme zu, dass das funktionieren könnte, aber es scheint sehr subtil zu sein. Ich glaube nicht, dass es für den Leser offensichtlich sein wird, was passiert.

Ich denke, dass wir in Go höher zielen müssen, als nur einen Weg zu haben, etwas zu tun. Wir müssen nach Ansätzen streben, die einfach und intuitiv sind und gut zum Rest der Sprache passen. Die Person, die den Code liest, sollte leicht verstehen können, was passiert. Natürlich können wir diese Ziele nicht immer erreichen, aber wir sollten unser Bestes geben.

@ianlancetaylor Abgesehen von der Syntaxdebatte, die an sich schon interessant ist, frage ich mich, ob wir als Community irgendetwas tun können, um Ihnen und dem Team bei diesem Thema zu helfen.

Ich habe zum Beispiel die Idee, dass Sie mehr Code im Stil des Vorschlags schreiben möchten, um den Vorschlag sowohl syntaktisch als auch anderweitig besser zu bewerten? Und/oder andere Dinge?

@toolbox Ja. Wir arbeiten an einem Tool, um dies zu vereinfachen, aber es ist noch nicht fertig. Jetzt wirklich bald.

Kannst du mehr über das Tool sagen? Würde es das Ausführen von Code erlauben?

Ist dieses Problem der bevorzugte Ort für Generika-Feedback? Es scheint aktiver zu sein als das Wiki. Eine Beobachtung ist, dass der Vorschlag viele Aspekte hat, aber das GitHub-Problem reduziert die Diskussion auf ein lineares Format.

Die Syntax F(T:) / G() { F(T:)} sieht für mich gut aus. Ich glaube nicht, dass eine Instanziierung, die wie ein Funktionsaufruf aussieht, für unerfahrene Leser intuitiv sein wird.

Ich verstehe nicht genau, was die Bedenken bezüglich der Abwärtskompatibilität sind. Ich denke, es gibt eine Einschränkung im Entwurf gegen die Erklärung eines Vertrags außer auf höchster Ebene. Es könnte sich lohnen abzuwägen (und zu messen), wie viel Code tatsächlich brechen würde, wenn dies erlaubt wäre. Mein Verständnis ist nur Code, der das Schlüsselwort contract verwendet, was nicht viel Code zu sein scheint (was sowieso durch die Angabe von go1 am Anfang alter Dateien unterstützt werden könnte). Wägen Sie das gegen Jahrzehnte mehr Leistung für Programmierer ab. Im Allgemeinen scheint es ziemlich einfach zu sein, alten Code mit solchen Mechanismen zu schützen, insbesondere bei weit verbreiteter Verwendung der berühmten Tools von go.

In Bezug auf diese Einschränkung vermute ich, dass das Verbot, Methoden innerhalb von Funktionskörpern zu deklarieren, ein Grund dafür ist, dass Schnittstellen nicht häufiger verwendet werden - sie sind viel umständlicher als das Herumreichen einzelner Funktionen. Es ist schwer zu sagen, ob die Beschränkung der Verträge auf oberster Ebene so irritierend wäre wie die Beschränkung der Methoden – das wäre es wahrscheinlich nicht – aber bitte verwenden Sie die Beschränkung der Methoden nicht als Präzedenzfall. Das ist für mich ein Sprachfehler.

Ich würde auch gerne Beispiele dafür sehen, wie Verträge dazu beitragen könnten, die Ausführlichkeit von if err != nil zu reduzieren, und was noch wichtiger ist, wo sie unzureichend wären. Ist so etwas wie F() (X, error) {return IfError(foo(), func(i, j int) X { return X(i*j}), Identity )} möglich?

Ich frage mich auch, ob das Go-Team davon ausgeht, dass sich implizite Funktionssignaturen wie ein fehlendes Feature anfühlen werden, sobald Map, Filter und Co. verfügbar sind. Ist dies etwas, das berücksichtigt werden muss, wenn der Sprache für Verträge neue implizite Typisierungsfunktionen hinzugefügt werden? Oder kann es später hinzugefügt werden? Oder wird es nie Teil der Sprache sein?

Ich freue mich darauf, den Vorschlag auszuprobieren. Sorry für so viele Themen.

Ich persönlich bin ziemlich skeptisch, dass viele Leute Methoden innerhalb von Funktionskörpern schreiben möchten. Es ist heute sehr selten, Typen innerhalb von Funktionskörpern zu definieren; Methoden zu deklarieren wäre noch seltener. Siehe jedoch #25860 (nicht im Zusammenhang mit Generika).

Ich sehe nicht, wie Generika bei der Fehlerbehandlung helfen (an sich schon ein sehr ausführliches Thema). Ich verstehe dein Beispiel nicht, sorry.

Eine kürzere Funktionsliteral-Syntax, die ebenfalls nicht mit Generika verbunden ist, ist #21498.

Als ich gestern Abend gepostet habe, war mir nicht klar, dass es möglich ist, mit dem Entwurf zu spielen
Implementierung (!!). Wow, es ist großartig, endlich abstrakteren Code schreiben zu können. Ich habe keine Probleme mit der Entwurfssyntax.

Fortsetzung der obigen Diskussion...


Ein Teil des Grundes, warum Menschen keine Typen in Funktionskörper schreiben, liegt darin, dass sie es tun
kann keine Methoden für sie schreiben. Diese Einschränkung kann den Typ innerhalb der abfangen
Block, wo es definiert wurde, da es nicht kurz in einen transformiert werden kann
Schnittstelle zur anderweitigen Verwendung. Java erlaubt anonymen Klassen, seine Version zu erfüllen
von Schnittstellen, und sie werden ziemlich oft verwendet.

Wir können die Schnittstellendiskussion in #25860 führen. Ich würde das nur in der Ära sagen
von Verträgen werden Methoden wichtiger, daher schlage ich vor, auf dem zu irren
Seite der Stärkung lokaler Typen und Menschen, die gerne Abschlüsse schreiben, nicht
schwächt sie.

(Und um es noch einmal zu wiederholen, verwenden Sie bitte keine strikte go1-Kompatibilität [vs virtual
99,999 % Kompatibilität, wie ich es verstehe] als Faktor bei der Entscheidung darüber
Merkmal.)


In Bezug auf die Fehlerbehandlung hatte ich vermutet, dass Generika eine Abstraktion ermöglichen könnten
allgemeine Muster für den Umgang mit (T1, T2, ..., error) -Rückgabetupeln. Ich nicht
etwas genaues im Sinn haben. Etwas wie type ErrPair(type T) struct{T T; Err Error} könnte nützlich sein, um Aktionen wie Promise in zu verketten
Java/TypeScript. Vielleicht hat sich das jemand genauer überlegt. Ein Versuch an
Es lohnt sich, eine Hilfsbibliothek und Code zu schreiben, der die Bibliothek verwendet
at, wenn Sie nach einer echten Nutzung suchen.

Nach einigem Probieren bin ich zu folgendem Ergebnis gekommen. Ich möchte das versuchen
Technik an einem größeren Beispiel, um zu sehen, ob die Verwendung ErrPair(T) tatsächlich hilft.

type result struct {min, max point}

// with a generic ErrPair type and generic function errMap2 (like Java's Optional#map() function).
func minMax2(msg *inputTimeSeries) (result, error) {
    return errMap2(
        MakeErrPair(time.Parse(layout, msg.start)).withMessage("bad start"),
        MakeErrPair(time.Parse(layout, msg.end)).withMessage("bad end"),
        func(start, end time.Time) (result, error) {
            min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
                return float64(p.value)
            })
            mkPoint := func(ip inputPoint) point {
                return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
            }
            return result{mkPoint(*min), mkPoint(*max)}, nil
        }).tuple()
}

// without generics, lots of if err != nil 
func minMax(msg *inputTimeSeries) (result, error) { 
    start, err := time.Parse(layout, msg.start)
    if err != nil {
        return result{}, fmt.Errorf("bad start: %w", err)
    }
    end, err := time.Parse(layout, msg.end)
    if err != nil {
        return result{}, fmt.Errorf("bad end: %w", err)
    }
    min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
        return float64(p.value)
    })
    mkPoint := func(ip inputPoint) point {
        return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
    }
    return result{mkPoint(*min), mkPoint(*max)}, nil
}

// Most languages look more like this.
func minMaxWithThrowing(msg *inputTimeSeries) result {
    start := time.Parse(layout, msg.start)) // might throw
    end := time.Parse(layout, msg.end)) // might throw
    min, max := argminmax(msg.inputPoints, func(p inputPoint) float64 {
        return float64(p.value)
    })
    mkPoint := func(ip inputPoint) point {
        return point{interpTime(start, end, ip.interp).Format(layout), ip.value}
    }
    return result{mkPoint(*min), mkPoint(*max)}
}

(vollständiger Beispielcode hier verfügbar)


Für allgemeine Experimente habe ich versucht, ein S-Expression-Paket zu schreiben
hier .
Ich erlebte einige Panik bei der experimentellen Implementierung, als ich es versuchte
Arbeiten Sie mit zusammengesetzten Typen wie Form([]*Form(T)) . Ich kann mehr Feedback geben
nachdem Sie das umgangen haben, wenn es nützlich wäre.

Ich war mir auch nicht ganz sicher, wie man einen primitiven Typ -> String-Funktion schreibt:

contract PrimitiveType(T) {
    T bool, int, int8, int16, int32, int64, string, uint, uint8, uint16, uint32, uint64, float32, float64, complex64, complex128
    // string(T) is not a contract
}

func primitiveString(type T PrimitiveType(T))(t T) string  {
    // I'm not sure if this is an artifact of the experimental implementation or not.
    return string(t) // error: `cannot convert t (variable of type T) to string`
}

Die eigentliche Funktion, die ich schreiben wollte, war diese:

// basicFormAdapter implements FormAdapter() for the primitive types.
type basicFormAdapter(type T PrimitiveType) struct{}


func (a *basicFormAdapter(T)) Format(e T, fc *FormatContext) error {
    //This doesn't work: fc.Print(string(e)) -- cannot convert e (variable of type T) to string
    // This also doesn't work: cannot type switch on non-interface value e (type int)
    // switch ee := e.(type) {
    // case int: fc.Print(string(ee))
    // default: fc.Print(fmt.Sprintf("!!! unsupported type %v", e))
    // }
    // IMO, the proposal to allow switching on T is most natural:
    // switch T.(type) {
    //  case int: fc.Print(string(e))
    //  default: fc.Print(fmt.Sprintf("!!! unsupported type %v", e))
    // }

    // This can't be the only way, right?
    rv := reflect.ValueOf(e)
    switch rv.Kind() {
    case reflect.Bool: fc.Print(fmt.Sprintf("%v", e))
    case reflect.Int:fc.Print(fmt.Sprintf("%v", e))
    case reflect.Int8: fc.Print(fmt.Sprintf("int8:%v", e))
    case reflect.Int16: fc.Print(fmt.Sprintf("int16:%v", e))
    case reflect.Int32: fc.Print(fmt.Sprintf("int32:%v", e))
    case reflect.Int64: fc.Print(fmt.Sprintf("int64:%v", e))
    case reflect.Uint: fc.Print(fmt.Sprintf("uint:%v", e))
    case reflect.Uint8: fc.Print(fmt.Sprintf("uint8:%v", e))
    case reflect.Uint16: fc.Print(fmt.Sprintf("uint16:%v", e))
    case reflect.Uint32: fc.Print(fmt.Sprintf("uint32:%v", e))
    case reflect.Uint64: fc.Print(fmt.Sprintf("uint64:%v", e))
    case reflect.Uintptr: fc.Print(fmt.Sprintf("uintptr:%v", e))
    case reflect.Float32: fc.Print(fmt.Sprintf("float32:%v", e))
    case reflect.Float64: fc.Print(fmt.Sprintf("float64:%v", e))
    case reflect.Complex64: fc.Print(fmt.Sprintf("(complex64 %f %f)", real(rv.Complex()), imag(rv.Complex())))
    case reflect.Complex128:
         fc.Print(fmt.Sprintf("(complex128 %f %f)", real(rv.Complex()), imag(rv.Complex())))
    case reflect.String:
        fc.Print(fmt.Sprintf("%q", rv.String()))
    }
    return nil
}

Ich habe auch versucht, ein 'Ergebnis' wie eine Art von Sortierung zu erstellen

type Result(type T) struct {
    Value T
    Err error
}

func NewResult(type T)(value T, err error) Result(T) {
    return Result(T){
        Value: value,
        Err: err,
    }
}

func then(type T, R)(r Result(T), f func(T) R) Result(R) {
    if r.Err != nil {
        return Result(R){Err: r.Err}
    }

    v := f(r.Value)
    return  Result(R){
        Value: v,
        Err: nil,
    }
}

func thenTry(type T, R)(r Result(T), f func(T)(R, error)) Result(R) {
    if r.Err != nil {
        return Result(R){Err: r.Err}
    }

    v, err := f(r.Value)
    return  Result(R){
        Value: v,
        Err: err,
    }
}

z.B

    r := NewResult(GetInput())
    r2 := thenTry(r, UppercaseAndErr)
    r3 := thenTry(r2, strconv.Atoi)
    r4 := then(r3, Add5)
    if r4.Err != nil {
        // handle err
    }
    return r4.Value, nil

Idealerweise haben Sie die then -Funktionen als Methoden für den Ergebnistyp.

Auch das absolute Differenzbeispiel im Entwurf scheint nicht zu kompilieren.
Ich denke folgendes:

func (a ComplexAbs(T)) Abs() T {
    r := float64(real(a))
    i := float64(imag(a))
    d := math.Sqrt(r * r + i * i)
    return T(complex(d, 0))
}

sollte sein:

func (a ComplexAbs(T)) Abs() ComplexAbs(T) {
    r := float64(real(a))
    i := float64(imag(a))
    d := math.Sqrt(r * r + i * i)
    return ComplexAbs(T)(complex(d, 0))
}

Ich habe ein wenig Bedenken hinsichtlich der Möglichkeit, mehrere contract zu verwenden, um einen Typparameter zu binden.

In Scala ist es üblich, eine Funktion wie folgt zu definieren:

def compute[A: PointLike: HasTime: IsWGS](points: Vector[A]): Map[Int, A] = ???

PointLike , HasTime und IsWGS sind kleine contract (Scala nennt sie type class ).

Rust hat auch einen ähnlichen Mechanismus:

fn f<F: A + B>(a F) {}

Und wir können eine anonyme Schnittstelle verwenden, wenn wir eine Funktion definieren.

type I1 interface {
    A()
}
type I2 interface {
    B()
}
func f(a interface{
    I1
    I2
})

IMO, die anonyme Schnittstelle ist eine schlechte Praxis, da ein interface ein echter Typ ist, der Aufrufer dieser Funktion muss möglicherweise eine Variable mit diesem Typ deklarieren. Aber contract ist nur eine Einschränkung für den Typparameter, der Aufrufer spielt immer mit einem echten Typ oder nur einem anderen Typparameter. Ich denke, es ist sicher, einen anonymen Vertrag in einer Funktionsdefinition zuzulassen.

Für Bibliotheksentwickler ist es unpraktisch, ein neues contract zu definieren, wenn die Kombination einiger Verträge nur an wenigen Stellen verwendet wird, da dies die Codebasis durcheinander bringt. Benutzer von Bibliotheken müssen sich mit den Definitionen befassen, um die tatsächlichen Anforderungen zu kennen. Wenn der Benutzer viele Funktionen zum Aufrufen der Funktion in der Bibliothek definiert, kann er einen benannten Vertrag zur einfachen Verwendung definieren und diesem neuen Vertrag bei Bedarf sogar weitere Verträge hinzufügen, da dieser gültig ist

contract C1(T) {
    T A()
}
contract C2(T) {
    T B()
}
contract C3(T) {
    T C()
}

contract PART(T) {
    C1(T)
    C2(T)
}

contract ALL(T) {
    C1(T)
    C2(T)
    C3(T)
}

func f1(type A PART) (a A) {}

func f2(type A ALL) (a A) {
    f1(a)
}

Ich habe diese auf dem Draft-Compiler ausprobiert, alle können nicht typgeprüft werden.

func f(type A C1, C2)(x A)

func f1(type A contract C(A1) {
    C1(A)
    C2(A)
}) (x A)

func f2(type A ((type A1) interface {
    I1(A1)
    I2(A1)
})(A)) (x A)

Laut den Anmerkungen in CL

Ein Typparameter, der durch mehrere Verträge eingeschränkt ist, erhält nicht die richtige Typbindung.

Ich denke, dieses seltsame Snippet ist gültig, nachdem dieses Problem behoben wurde

func f1(type A C1, _ C2(A)) (x A)

Hier sind einige meiner Gedanken:

  • Wenn wir contract als Typ eines Typparameters behandeln, type a A <=> var a A , können wir einen Syntaxzucker wie type a { A1(a); A2(a) } hinzufügen, um einen anonymen Parameter zu definieren Vertrag schnell.
  • Andernfalls können wir den letzten Teil der Typliste als eine Liste von Anforderungen behandeln, type a, b, A1(a), A2(a), A3(a, b) , dieser Stil ist genau wie interface , um Typparameter einzuschränken.

@bobotu In Go ist es üblich, Funktionen mithilfe der Einbettung zu verfassen. Es scheint natürlich, Verträge so zu erstellen, wie Sie es mit Strukturen oder Schnittstellen tun würden.

@azunymous Persönlich weiß ich nicht, wie ich darüber denke, dass die gesamte Go-Community von mehreren Rückgaben auf Result umgestellt hat, obwohl es scheint, dass der Vertragsvorschlag dies bis zu einem gewissen Grad ermöglichen würde. Das Go-Team scheint Sprachänderungen zu scheuen, die das "Gefühl" der Sprache beeinträchtigen, dem ich zustimme, aber das scheint eine dieser Änderungen zu sein.

Nur ein Gedanke; Ich frage mich, ob es zu diesem Punkt irgendwelche Annahmen gibt.

@tooolbox Ich glaube nicht, dass es tatsächlich möglich ist, so etwas wie einen einzelnen Result -Typ ausgiebig außerhalb des Falls zu verwenden, in dem Sie nur Werte übergeben, es sei denn, Sie haben eine Menge generischer Result s und Funktionen jeder Kombination aus Parameteranzahl und Rückgabetypen. Mit vielen nummerierten Funktionen oder der Verwendung von Closures würden Sie die Lesbarkeit verlieren.

Ich denke, es ist wahrscheinlicher, dass Sie etwas sehen, das einem errWriter entspricht, wo Sie so etwas gelegentlich verwenden würden, wenn es passt, benannt nach dem Anwendungsfall.

Ich persönlich weiß nicht, wie ich darüber denke, dass die gesamte Go-Community von mehreren Rücksendungen zu Result wechselt

Ich glaube nicht, dass das passieren würde. Wie @azunymous sagte, haben viele Funktionen mehrere Rückgabetypen und einen Fehler, aber ein Ergebnis konnte nicht alle diese anderen zurückgegebenen Werte gleichzeitig enthalten. Parametrischer Polymorphismus ist nicht die einzige Funktion, die für so etwas benötigt wird; Sie würden auch Tupel und Destrukturierung benötigen.

Danke! Wie ich schon sagte, nicht etwas, worüber ich tief nachgedacht hatte, aber gut zu wissen, dass meine Besorgnis fehl am Platz war.

@tooolbox Ich beabsichtige nicht, eine neue Syntax einzuführen, das Hauptproblem hier ist die fehlende Möglichkeit, anonyme Verträge wie anonyme Schnittstellen zu verwenden.

Im Draft-Compiler scheint es unmöglich, so etwas zu schreiben. Wir können eine anonyme Schnittstelle in der Funktionsdefinition verwenden, aber wir können das Gleiche nicht einmal im ausführlichen Stil für den Vertrag tun.

func f1(type A, B, C, D contract {
    C1(A)
    C2(A, B)
    C3(A, C)
}) (a A, b B, c C, d D)

// Or a more verbose style

func f2(type A, B, C, D (contract (_A, _B, _C) {
    C1(_A)
    C2(_A, _B)
    C3(_A, _C)
})(A, B, C)) (a A, b B, c C, d D)

Meiner Meinung nach ist dies eine natürliche Erweiterung der bestehenden Syntax. Dies ist immer noch ein Vertrag am Ende der Typparameterliste, und wir verwenden immer noch die Einbettung, um die Funktionalität zu erstellen. Wenn Go etwas Zucker bereitstellen kann, um die Typparameter des Vertrags wie das erste Snippet automatisch zu generieren, wird der Code einfacher zu lesen und zu schreiben sein.

func fff(type A C1(A), B C2(B, A), C C3(B, C, A)) (a A, b B, c C)

// is more verbose than

func fff(type A, B, C contract {
    C1(A)
    C2(B, A)
    C3(B, C, A)
}) (a A, b B, c C)

Ich stoße auf einige Probleme, wenn ich versuche, einen faulen Iterator ohne den dynamischen Methodenaufruf zu implementieren, genau wie Rusts Iterator.

Ich möchte einen einfachen Iterator Vertrag definieren

contract Iterator(T, E) {
    T Next() (E, bool)
}

Da Go nicht über das Konzept von type member verfügt, muss ich E als Eingabetypparameter deklarieren.

Eine Funktion zum Sammeln der Ergebnisse

func Collect(type I, E Iterator) (input I) []E {
    var results []E
    for {
        e, ok := input.Next()
        if !ok {
            return results
        }
        results = append(results, e)
    }
}

Eine Funktion zum Zuordnen von Elementen

contract MapIO(I, E, O, R) {
    Iterator(I, E)
    Iterator(O, R)
}

func Map(type I, E, O, R MapIO) (input I, f func (e E) R) O {
    return &lazyIterator(I, E, R){
        parent: input,
        f:      f,
    }
}

Ich habe hier zwei Probleme:

  1. Ich kann hier kein lazyIterator zurückgeben, der Compiler sagt cannot convert &(lazyIterator(I, E, R) literal) (value of type *lazyIterator(I, E, R)) to O .
  2. Ich muss einen neuen Vertrag mit dem Namen MapIO deklarieren, der 4 Zeilen benötigt, während der Map nur 6 Zeilen benötigt. Es ist schwierig für Benutzer, den Code zu lesen.

Angenommen, Map kann typgeprüft werden, ich hoffe, ich kann so etwas schreiben

type staticIterator(type E) struct {
    elem []E
}

func (it *(staticIterator(E))) Next() (E, bool) { panic("todo") }

func main() {
    inpuit := &staticIterator{
        elem: []int{1, 2, 3, 4},
    }
    mapped := Map(input, func (i int) float32 { return float32(i + 1) })
    fmt.Printf("%v\n", Collect(mapped))
}

Leider beschwert sich der Compiler darüber, dass er keine Typen ableiten kann. Es hört auf, sich darüber zu beschweren, nachdem ich den Code geändert habe

func main() {
    input := &staticIterator(int){
        elem: []int{1, 2, 3, 4},
    }
    mapped := Map(*staticIterator(int), int, *lazyIterator(*staticIterator(int), int, float32), float32)(input, func (i int) float32 { return float32(i + 1) })
    result := Collect(*lazyIterator(*staticIterator(int), int, float32), float32)(mapped)
    fmt.Printf("%v\n", result)
}

Der Code ist sehr schwer zu lesen und zu schreiben, und es gibt zu viele doppelte Typhinweise.

Übrigens, der Compiler gerät in Panik mit:

panic: interface conversion: ast.Expr is *ast.ParenExpr, not *ast.CallExpr

goroutine 1 [running]:
go/go2go.(*translator).instantiateTypeDecl(0xc000251950, 0x0, 0xc0001af860, 0xc0001a5dd0, 0xc00018ac90, 0x1, 0x1, 0xc00018bca0, 0x1, 0x1, ...)
        /home/tuzi/go-tip/src/go/go2go/instantiate.go:191 +0xd49
go/go2go.(*translator).translateTypeInstantiation(0xc000251950, 0xc000189380)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:671 +0x3f3
go/go2go.(*translator).translateExpr(0xc000251950, 0xc000189380)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:518 +0x501
go/go2go.(*translator).translateExpr(0xc000251950, 0xc0001af990)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:496 +0xe3
go/go2go.(*translator).translateExpr(0xc000251950, 0xc00018ace0)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:524 +0x1c3
go/go2go.(*translator).translateExprList(0xc000251950, 0xc00018ace0, 0x1, 0x1)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:593 +0x45
go/go2go.(*translator).translateStmt(0xc000251950, 0xc000189840)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:419 +0x26a
go/go2go.(*translator).translateBlockStmt(0xc000251950, 0xc00018d830)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:380 +0x52
go/go2go.(*translator).translateFuncDecl(0xc000251950, 0xc0001c0390)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:373 +0xbc
go/go2go.(*translator).translate(0xc000251950, 0xc0001b0400)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:301 +0x35c
go/go2go.rewriteAST(0xc000188280, 0xc000188240, 0x0, 0x0, 0xc0001f6280, 0xc0001b0400, 0x1, 0xc000195360, 0xc0001f6280)
        /home/tuzi/go-tip/src/go/go2go/rewrite.go:122 +0x101
go/go2go.RewriteBuffer(0xc000188240, 0x7ffe07d6c027, 0xa, 0xc0001ec000, 0x4fe, 0x6fe, 0x0, 0xc00011ed58, 0x40d288, 0x30, ...)
        /home/tuzi/go-tip/src/go/go2go/go2go.go:132 +0x2c6
main.translateFile(0xc000188240, 0x7ffe07d6c027, 0xa)
        /home/tuzi/go-tip/src/cmd/go2go/translate.go:26 +0xa9
main.main()
        /home/tuzi/go-tip/src/cmd/go2go/main.go:64 +0x434

Ich finde auch, dass es unmöglich ist, eine Funktion zu definieren, die mit einem Iterator arbeitet, der einen bestimmten Typ zurückgibt.

type User struct {}

func UpdateUsers(type A Iterator(A, User)) (it A) bool { 
    // Access `User`'s field.
}

// And I found this may be possible

contract checkInts(A, B) {
    Iterator(A, B)
    B int
}

func CheckInts(type A, B checkInts) (it A) bool { panic("todo") }

Das zweite Snippet kann in einigen Szenarien funktionieren, aber es ist schwer zu verstehen, und der nicht verwendete Typ B erscheint seltsam.

Tatsächlich können wir eine Schnittstelle verwenden, um diese Aufgabe zu erledigen.

type Iterator(type E) interface {
    Next() (E, bool)
}

Ich versuche nur herauszufinden, wie ausdrucksstark das Design des Go ist.

Übrigens, der Rust-Code, auf den ich mich beziehe, ist

fn main() {
    let input = vec![1, 2, 3, 4];
    let mapped = input.iter().map(|x| x * 3);
    let result = f(mapped);
    println!("{:?}", result.collect::<Vec<_>>());
}

fn f<I: Iterator<Item = i32>>(it: I) -> impl Iterator<Item = f32> {
    it.map(|i| i as f32 * 2.0)
}

// The definition of `map` in stdlib is
pub struct Map<I, F> {
    iter: I,
    f: F,
}

fn map<B, F: FnMut(Self::Item) -> B>(self, f: F) -> Map<Self, F>

Hier ist eine Zusammenfassung für https://github.com/golang/go/issues/15292#issuecomment -633233479

  1. Wir brauchen vielleicht etwas, um existential type für func Collect(type I, E Iterator) (input I) []E auszudrücken

    • Auf den tatsächlichen Typ des universellen quantifizierten Parameters E kann nicht geschlossen werden, da er nur in der Rückgabeliste auftauchte. Aufgrund des Fehlens von type member , um E standardmäßig existentiell zu machen, denke ich, dass wir dieses Problem an vielen Stellen treffen können.

    • Vielleicht können wir das einfachste existential type wie Javas Platzhalter ? verwenden, um die Typinferenz von func Consume(type I, E Iterator) (input I) aufzulösen. Wir können _ verwenden, um E , func Consume(type I Iterator(I, _)) (input I) $ zu ersetzen.

    • Aber es kann immer noch nicht das Typrückschlussproblem für Collect lösen, ich weiß nicht, ob es schwer ist, auf E zu schließen, aber Rust scheint dazu in der Lage zu sein.

    • Oder wir können _ als Platzhalter für Typen verwenden, die der Compiler ableiten kann, und die fehlenden Typen manuell füllen, z. B. Collect(_, float32) (...) , um auf einem Iterator von float32 zu sammeln.

  1. Aufgrund der fehlenden Möglichkeit, existential type zurückzugeben, haben wir auch Probleme mit Dingen wie func Map(type I, E, O, R MapIO) (input I, f func (e E) R) O

    • Rust unterstützt dies durch die Verwendung von impl Iterator<E> . Wenn Go so etwas bereitstellen kann, können wir einen neuen Iterator ohne Boxen zurückgeben, was für leistungskritischen Code nützlich sein kann.

    • Oder wir können einfach ein Box-Objekt zurückgeben, so löst Rust dieses Problem, bevor es existential type an der Rückgabeposition unterstützt. Aber die Frage ist die Beziehung zwischen contract und interface , vielleicht müssen wir einige Konvertierungsregeln definieren und sie vom Compiler automatisch konvertieren lassen. Andernfalls müssen wir für diesen Fall möglicherweise ein contract und ein interface mit identischen Methoden definieren.

    • Andernfalls können wir nur CPS verwenden, um den Typparameter von der Rückgabeposition in die Eingabeliste zu verschieben. zB func Map(type I, E, O, R MapIO) (input I, f func (e E) R, f1 func (outout O)) . Aber das ist in der Praxis nutzlos, einfach weil wir den tatsächlichen Typ von O schreiben müssen, wenn wir eine Funktion an Map übergeben.

Ich habe diese Diskussion gerade ein wenig nachgeholt, und es scheint ziemlich klar zu sein, dass die syntaktischen Schwierigkeiten mit Typparametern eine Hauptschwierigkeit des Vorschlagsentwurfs bleiben. Es gibt eine Möglichkeit, Typparameter vollständig zu vermeiden und die meisten generischen Funktionen zu erreichen: #32863 - vielleicht ist dies ein guter Zeitpunkt, diese Alternative angesichts einiger dieser weiteren Diskussionen in Betracht zu ziehen? Wenn es eine Chance gäbe, dass so etwas wie dieses Design übernommen wird, würde ich gerne versuchen, den Web-Assembly-Playground zu modifizieren, um ihn testen zu können.

Meines Erachtens liegt der Schwerpunkt derzeit darauf, die Korrektheit der Semantik des aktuellen Vorschlags unabhängig von der Syntax festzunageln, da die Semantik sehr schwer zu ändern ist.

Ich habe gerade gesehen, dass ein Artikel über Featherweight Go auf Arxiv veröffentlicht wurde und eine Zusammenarbeit zwischen dem Go-Team und mehreren Experten für Typentheorie ist. Es sieht so aus, als gäbe es weitere geplante Papiere in dieser Richtung.

Um meinen vorherigen Kommentar weiterzuverfolgen, hat Phil Wadler von Haskell und einer der Autoren der Zeitung am Montag, den 8. Juni um 7:00 Uhr PDT / 10:00 Uhr EDT einen Vortrag zum Thema „Featherweight Go“ geplant: http://chalmersfp.org/ . YouTube-Link

@rcoreilly Ich denke, wir werden erst wissen, ob die "syntaktischen Schwierigkeiten" ein großes Problem sind, wenn die Leute mehr Erfahrung mit dem Schreiben und, was noch wichtiger ist, dem Lesen von Code haben, der gemäß dem Designentwurf geschrieben wurde. Wir arbeiten an Möglichkeiten, wie die Leute das ausprobieren können.

In Ermangelung dessen denke ich, dass die Syntax einfach das ist, was die Leute zuerst sehen und zuerst kommentieren. Es kann ein großes Problem sein, es kann nicht sein. Wir wissen es noch nicht.

Um an meinen vorherigen Kommentar anzuknüpfen, hat Phil Wadler von Haskell und einer der Autoren der Zeitung am Montag einen Vortrag zum Thema „Featherweight Go“ geplant

Der Vortrag von Phil Wadler war sehr zugänglich und interessant. Ich ärgerte mich über das scheinbar sinnlose Zeitlimit von einer Stunde, das ihn daran hinderte, in die Monomorphisierung zu geraten.

Bemerkenswert, dass Wadler von Pike gebeten wurde, vorbeizuschauen; Anscheinend kennen sie sich von Bell Labs. Für mich hat Haskell ganz andere Werte und Paradigmen, und es ist interessant zu sehen, wie sein (Schöpfer? Hauptdesigner?) über Go und Generika in Go denkt.

Der Vorschlag selbst hat eine Syntax, die Contracts sehr nahe kommt, lässt aber Contracts selbst weg und verwendet nur Typparameter und Schnittstellen. Ein wichtiger Unterschied, der genannt wird, ist die Möglichkeit, einen generischen Typ zu nehmen und Methoden darauf zu definieren, die spezifischere Einschränkungen als der Typ selbst haben.

Anscheinend arbeitet das Go-Team daran oder hat einen Prototyp davon! Das wird interessant sein. Wie würde das in der Zwischenzeit aussehen?

package graph

type Node(type e) interface{
    Edges() []e
}

type Edge(type n) interface{
    Nodes() (from n, to n)
}

type Graph(type n Node(e), e Edge(n)) struct { ... }
func New(type n Node(e), e Edge(n))(nodes []n) *Graph(n, e) { ... }
func (g *Graph(type n Node(e), e Edge(n))) ShortestPath(from, to n) []e { ... }

Habe ich das Recht? Ich glaube schon. Wenn ich das tue ... eigentlich nicht schlecht. Löst das Problem mit den stotternden Klammern nicht ganz, aber es scheint irgendwie verbessert zu werden. Ein namenloser Aufruhr in mir wird beruhigt.

Was ist mit dem Stack-Beispiel von @urandom ? (Aliasing interface{} zu Any und Verwendung einer bestimmten Menge an Typrückschlüssen.)

package main

type Any interface{}

type Stack(type t Any) []t

func (s Stack(type t Any)) Peek() t {
    return s[len(s)-1]
}

func (s *Stack(type t Any)) Pop() {
    *s = (*s)[:len(*s)-1]
}

func (s *Stack(type t Any)) Push(value t) {
    *s = append(*s, value)
}

type StackIterator(type t Any) struct{
    stack Stack(t)
    current int
}

func (s *Stack(type t Any)) Iter() *StackIterator(t) {
    it := StackIterator(t){stack: *s, current: len(*s)}

    return &it
}

func (i *StackIterator(type t Any)) Next() (bool) { 
    i.current--

    if i.current < 0 { 
        return false
    }

    return true
}

func (i *StackIterator(type t Any)) Value() t {
    if i.current < 0 {
        var zero t
        return zero
    }

    return i.stack[i.current]
}

type Iterator(type t Any) interface {
    Next() bool
    Value() t
}

func Map(type t Any, u Any)(it Iterator(t), mapF func(t) u) Iterator(u) {
    return mapIt(t, u){it, mapF}
}

type mapIt(type t Any, u Any) struct {
    parent Iterator(t)
    mapF func(t) u
}

func (i mapIt(type t Any, u Any)) Next() bool {
    return i.parent.Next()
}

func (i mapIt(type t Any, u Any)) Value() u {
    return i.mapF(i.parent.Value())
}

func Filter(type t Any)(it Iterator(t), predicate func(t) bool) Iterator(t) {
    return filter(t){it, predicate}
}

type filter(type t Any) struct {
    parent Iterator(t)
    predicateF func(t) bool
}

func (i filter(type t Any)) Next() bool {
    if !i.parent.Next() {
        return false
    }

    n := true
    for n && !i.predicateF(i.parent.Value()) {
        n = i.parent.Next()
    }

    return n
}

func (i filter(type t Any)) Value() t {
    return i.parent.Value()
}

func Distinct(type t comparable)(it Iterator(t)) Iterator(t) {
    return distinct(t){it, map[t]struct{}{}}
}

type distinct(type t comparable) struct {
    parent Iterator(t)
    set map[t]struct{}
}

func (i distinct(type t Any)) Next() bool {
    if !i.parent.Next() {
        return false
    }

    n := true
    for n {
        _, ok := i.set[i.parent.Value()]
        if !ok {
            i.set[i.parent.Value()] = struct{}{}
            break
        }
        n = i.parent.Next()
    }


    return n
}

func (i distinct(type t Any)) Value() t {
    return i.parent.Value()
}

func ToSlice(type t Any)(it Iterator(t)) []t {
    var res []t

    for it.Next() {
        res = append(res, it.Value())
    }

    return res
}

func ToSet(type t comparable)(it Iterator(t)) map[t]struct{} {
    var res map[t]struct{}

    for it.Next() {
        res[it.Value()] = struct{}{}
    }

    return res
}

func Reduce(type t Any)(it Iterator(t), id t, acc func(a, b t) t) t {
    for it.Next() {
        id = acc(id, it.Value())
    }

    return id
}

func main() {
    var stack Stack(string)
    stack.Push("foo")
    stack.Push("bar")
    stack.Pop()
    stack.Push("alpha")
    stack.Push("beta")
    stack.Push("foo")
    stack.Push("gamma")
    stack.Push("beta")
    stack.Push("delta")


    var it Iterator(string) = stack.Iter()

    it = Filter(string)(it, func(s string) bool {
        return s == "foo" || s == "beta" || s == "delta"
    })

    it = Map(string, string)(it, func(s string) string {
        return s + ":1"
    })

    it = Distinct(string)(it)

    println(Reduce(it, "", func(a, b string) string {
        if a == "" {
            return b
        }
        return a + ":" + b
    }))


}

So etwas in der Art, nehme ich an. Mir ist klar, dass es in diesem Code eigentlich keine Verträge gibt, also ist es keine gute Darstellung dessen, wie das im FGG-Stil gehandhabt wird, aber ich kann das gleich angehen.

Impressionen:

  • Ich mag es, wenn der Stil von Typparametern in Methoden dem von Typdeklarationen entspricht. Das heißt, "Typ" zu sagen und die Typen explizit anzugeben, ("type" param paramType, param paramType...) statt (param, param) . Es macht es visuell konsistent, sodass der Code übersichtlicher ist.
  • Ich mag es, wenn die Typparameter in Kleinbuchstaben geschrieben werden. Einzelbuchstaben-Variablen in Go weisen auf eine extrem lokale Verwendung hin, aber Großschreibung bedeutet, dass sie exportiert wird, und sie scheinen zusammengenommen gegensätzlich zu sein. Kleinbuchstaben fühlen sich besser an, da Typparameter auf die Funktion/den Typ beschränkt sind.

Okay, was ist mit Verträgen?

Nun, eine Sache, die ich mag, ist, dass Stringer unberührt ist; Sie werden keine Stringer Schnittstelle und keinen Stringer Vertrag haben.

type Stringer interface {
    String() string
}

func Stringify(type t Stringer)(s []t) (ret []string) {
    for _, v := range s {
        ret = append(ret, v.String())
    }
    return ret
}

Wir haben auch das viaStrings Beispiel:

type ToString interface {
    Set(string)
}

type FromString interface {
    String() string
}

func SetViaStrings(type to ToString, from FromString)(s []from) []to {
    r := make([]to, len(s))
    for i, v := range s {
        r[i].Set(v.String())
    }
    return r
}

Interessant. Ich bin mir nicht 100% sicher, was uns der Vertrag in diesem Fall gebracht hat. Vielleicht war ein Teil davon die Regel, dass eine Funktion mehrere Typparameter haben konnte, aber nur einen Vertrag.

Gleiches wird in der Arbeit/dem Vortrag behandelt:

contract equal(T) {
    T Equal(T) bool
}

// becomes

type equal(type t equal(t)) interface{
    Equal(t) bool
}

Und so weiter. Ich bin ziemlich angetan von der Semantik. Typparameter sind Schnittstellen, daher werden die gleichen Regeln zur Implementierung einer Schnittstelle auf das angewendet, was als Typparameter verwendet werden kann. Es ist zur Laufzeit einfach nicht "verpackt" - es sei denn, Sie übergeben ihm explizit eine Schnittstelle, nehme ich an, die Ihnen freisteht.

Das Größte, was ich als nicht abgedeckt bemerke, ist ein Ersatz für die Fähigkeit von Contracts, eine Reihe primitiver Typen anzugeben. Nun, ich bin mir sicher, dass eine Strategie dafür und viele andere Dinge kommen wird :

8 - SCHLUSSFOLGERUNG

Dies ist der Anfang der Geschichte, nicht das Ende. In der zukünftigen Arbeit planen wir, neben der Monomorphisierung andere Methoden der Implementierung zu untersuchen und insbesondere eine Implementierung in Betracht zu ziehen, die auf der Weitergabe von Laufzeitdarstellungen von Typen basiert, ähnlich der für .NET-Generika verwendeten. Ein gemischter Ansatz, der manchmal Monomorphisierung verwendet und manchmal Laufzeitdarstellungen übergibt, könnte am besten sein, wieder ähnlich dem, der für .NET-Generika verwendet wird.

Featherweight Go ist auf eine kleine Teilmenge von Go beschränkt. Wir planen ein Modell anderer wichtiger Funktionen wie Zuweisungen, Arrays, Slices und Pakete, die wir Bantamweight Go nennen werden; und ein Modell des innovativen Nebenläufigkeitsmechanismus von Go, der auf „Goroutinen“ und Nachrichtenübermittlung basiert und das wir Cruiserweight Go nennen werden.

Featherweight Go sieht für mich großartig aus. Ausgezeichnete Idee, einige Typentheorie-Experten einzubeziehen. Das sieht viel mehr nach dem aus, was ich weiter oben in diesem Thema befürwortet habe.

Gut zu hören, dass Typentheorie-Experten aktiv daran arbeiten!

Es sieht sogar ähnlich aus (bis auf die etwas andere Syntax) zu meinem alten Vorschlag "Verträge sind Schnittstellen" https://github.com/cosmos72/gomacro/blob/master/doc/generics-cti.md

@toolbox
Durch das Zulassen von Methoden mit anderen Einschränkungen als dem tatsächlichen Typ (sowie anderen Typen insgesamt) eröffnet FGG eine Reihe von Möglichkeiten, die mit dem aktuellen Vertragsentwurf nicht realisierbar waren. Beispielsweise sollte man mit FGG in der Lage sein, sowohl einen Iterator als auch einen ReversibleIterator zu definieren, und die Zwischen- und End-Iteratoren (Map, Filter Reduce) beide unterstützen (z. B. mit Next() und NextFromBack() für Reversibles) , je nachdem, was der übergeordnete Iterator ist.

Ich denke, es ist wichtig, im Hinterkopf zu behalten, dass FGG nicht definitiv dort ist, wo Generika in Go landen werden. Es ist eine Sicht auf sie, von außen. Und es ignoriert ausdrücklich eine Reihe von Dingen, die das Endprodukt verkomplizieren. Außerdem habe ich die Zeitung nicht gelesen, sondern nur den Vortrag gesehen. In diesem Sinne: Soweit ich das beurteilen kann, gibt es zwei wesentliche Möglichkeiten, wie FGG dem Vertragsentwurf Ausdrucksmacht verleiht:

  1. Es ermöglicht das Hinzufügen neuer Typparameter zu Methoden (wie im Beispiel "List and Maps" im Vortrag gezeigt). AFAICT, dies würde die Implementierung Functor (tatsächlich ist das sein Listenbeispiel, wenn ich mich nicht irre), Monad und ihren Freunden ermöglichen. Ich glaube nicht, dass diese spezifischen Typen für Gophers interessant sind, aber es gibt interessante Anwendungsfälle dafür (zum Beispiel würde ein Go-Port von Flume oder ähnlichen Konzepten wahrscheinlich davon profitieren). Ich persönlich empfinde es als positive Veränderung, obwohl ich noch nicht sehe, welche Auswirkungen dies auf Reflexion und dergleichen hat. Ich habe das Gefühl, dass Methodendeklarationen, die dies verwenden, langsam schwer lesbar werden - insbesondere, wenn Typparameter eines generischen Typs auch im Empfänger aufgelistet werden müssen.
  2. Es ermöglicht, dass Typparameter strengere Grenzen für Methoden generischer Typen haben als für den Typ selbst. Wie von anderen erwähnt, können Sie damit denselben generischen Typ verschiedene Methoden implementieren lassen, je nachdem, mit welchen Typen er instanziiert wurde. Ich persönlich bin mir nicht sicher, ob das eine gute Veränderung ist. Es scheint ein Rezept für Verwirrung zu sein, wenn Map(int, T) mit Methoden endet, die Map(string, T) nicht hat. Zumindest muss der Compiler hervorragende Fehlermeldungen liefern, wenn so etwas passiert. Der Nutzen scheint derweil vergleichsweise gering - zumal der Motivationsfaktor des Vortrags (separate Zusammenstellung) für Go nicht so sehr relevant ist: Da Methoden im selben Paket wie ihr Empfängertyp deklariert werden müssen und Pakete die Einheit sind der Kompilierung können Sie den Typ nicht wirklich separat erweitern. Ich weiß, dass es eher eine konkrete Art ist, über Kompilierung zu sprechen, um über einen abstrakteren Nutzen zu sprechen, aber ich glaube trotzdem nicht, dass dieser Nutzen Go viel hilft.

Ich freue mich auf jeden Fall auf die nächsten Schritte :)

Ich denke, es ist wichtig, im Hinterkopf zu behalten, dass FGG nicht definitiv dort ist, wo Generika in Go landen werden.

@ Merovius warum sagst du das?

@arl
FG ist eher eine Forschungsarbeit darüber, was getan _könnte_. Niemand hat explizit gesagt, dass Polymorphismus in Go in Zukunft so funktionieren wird. Auch wenn 2 Go-Core-Entwickler in dem Papier als Autoren aufgeführt sind, bedeutet das nicht, dass dies in Go implementiert wird.

Ich denke, es ist wichtig, im Hinterkopf zu behalten, dass FGG nicht definitiv dort ist, wo Generika in Go landen werden. Es ist eine Sicht auf sie, von außen. Und es ignoriert ausdrücklich eine Reihe von Dingen, die das Endprodukt verkomplizieren.

Ja, sehr guter Punkt.

Außerdem möchte ich anmerken, dass Wadler als Teil eines Teams arbeitet und das resultierende Produkt auf dem Contracts-Vorschlag aufbaut und diesem sehr nahe kommt, der das Ergebnis jahrelanger Arbeit der Kernentwickler ist.

Durch das Zulassen von Methoden mit anderen Einschränkungen als dem tatsächlichen Typ (sowie anderen Typen insgesamt) eröffnet FGG eine Reihe von Möglichkeiten, die mit dem aktuellen Vertragsentwurf nicht realisierbar waren. ...

@urandom Ich bin gespannt, wie dieses Iterator-Beispiel aussieht. Würdest du etwas zusammen werfen?

Unabhängig davon interessiere ich mich dafür, was Generics über Karten und Filter und funktionale Dinge hinaus tun können, und neugieriger, wie sie einem Projekt wie k8s zugute kommen könnten. (Nicht, dass sie an dieser Stelle umgestalten würden, aber ich habe anekdotisch gehört, dass der Mangel an Generika einige ausgefallene Beinarbeit erfordert hat, denke ich, mit benutzerdefinierten Ressourcen? Jemand, der mit dem Projekt besser vertraut ist, kann mich korrigieren.)

Ich habe das Gefühl, dass Methodendeklarationen, die dies verwenden, langsam schwer lesbar werden - insbesondere, wenn Typparameter eines generischen Typs auch im Empfänger aufgelistet werden müssen.

Vielleicht könnte gofmt irgendwie helfen? Vielleicht müssen wir mehrzeilig gehen. Es lohnt sich vielleicht, damit herumzuspielen.

Wie von anderen erwähnt, können Sie damit denselben generischen Typ verschiedene Methoden implementieren lassen, je nachdem, mit welchen Typen er instanziiert wurde.

Ich verstehe, was du sagst @Merovius

Es wurde von Wadler als Unterschied bezeichnet und lässt ihn sein Ausdrucksproblem lösen, aber Sie machen deutlich, dass Gos Art von hermetischen Paketen anscheinend einschränken, was Sie damit tun können / sollten. Können Sie sich einen konkreten Fall vorstellen, in dem Sie das tun möchten?

Wie von anderen erwähnt, können Sie damit denselben generischen Typ verschiedene Methoden implementieren lassen, je nachdem, mit welchen Typen er instanziiert wurde.

Ich verstehe, was du sagst @Merovius

Es wurde von Wadler als Unterschied bezeichnet und lässt ihn sein Ausdrucksproblem lösen, aber Sie machen deutlich, dass Gos Art von hermetischen Paketen anscheinend einschränken, was Sie damit tun können / sollten. Können Sie sich einen konkreten Fall vorstellen, in dem Sie das tun möchten?

Ironischerweise war mein erster Gedanke, dass damit einige der in diesem Artikel beschriebenen Herausforderungen gelöst werden könnten: https://blog.merovius.de/2017/07/30/the-trouble-with-optional-interfaces.html

@Werkzeugkasten

Unabhängig davon interessiert mich, was Generika über Karten und Filter und funktionale Dinge hinaus tun können.

FWIW, es sollte klargestellt werden, dass dies eine Art Verkauf von "Karten und Filtern und funktionalen Dingen" ist. Ich persönlich möchte zum Beispiel keine map und filter über eingebauten Datenstrukturen in meinem Code (ich bevorzuge for-Schleifen). Kann aber auch bedeuten

  1. Bereitstellung eines allgemeinen Zugriffs auf beliebige Datenstrukturen von Drittanbietern. dh map und filter können so gemacht werden, dass sie über generische Bäume oder sortierte Maps oder … auch funktionieren. So können Sie das, was abgebildet ist, für mehr Leistung austauschen. Und noch wichtiger
  2. Sie können austauschen, wie es zugeordnet ist. Beispielsweise könnten Sie eine Version von Compose erstellen, die mehrere Goroutinen für jede Funktion hervorbringen und sie gleichzeitig mithilfe von Kanälen ausführen kann. Dies würde es einfach machen, gleichzeitige Datenverarbeitungspipelines auszuführen und den Engpass automatisch zu skalieren, während nur func(A) B s geschrieben werden müssten. Oder Sie könnten die gleichen Funktionen in ein Framework packen, das Tausende von Kopien des Programms in einem Cluster ausführt und Stapel der Daten über sie verteilt (darauf habe ich angespielt, als ich oben auf Flume verlinkt habe).

Obwohl die Möglichkeit, Map und Filter und Reduce zu schreiben, oberflächlich langweilig erscheinen mag, eröffnen dieselben Techniken einige wirklich aufregende Möglichkeiten, um skalierbare Berechnungen einfacher zu machen.

@ChrisHines

Ironischerweise war mein erster Gedanke, dass damit einige der in diesem Artikel beschriebenen Herausforderungen gelöst werden könnten: https://blog.merovius.de/2017/07/30/the-trouble-with-optional-interfaces.html

Es ist ein interessanter Gedanke und es fühlt sich sicherlich so an, wie es sollte. Aber ich sehe noch nicht wie. Wenn Sie das ResponseWriter -Beispiel nehmen, scheint dies es Ihnen zu ermöglichen, generische, typsichere Wrapper mit unterschiedlichen Methoden zu schreiben, je nachdem, was das umschlossene ResponseWriter unterstützt. Aber selbst wenn Sie unterschiedliche Grenzen für verschiedene Methoden verwenden können, müssen Sie sie dennoch aufschreiben. Obwohl es die Situation typsicher machen kann, in dem Sinne, dass Sie keine Methoden hinzufügen, die Sie nicht unterstützen, müssen Sie dennoch alle Methoden aufzählen, die Sie unterstützen könnten , sodass Middleware möglicherweise immer noch einige optionale Schnittstellen maskiert nur indem man sie nicht kennt. Mittlerweile kann man auch (auch ohne diese Funktion) darauf verzichten

type Middleware (type RW http.ResponseWriter) struct {
    RW
}

und überschreiben Sie ausgewählte Methoden, die Ihnen wichtig sind - und lassen Sie alle anderen Methoden von RW promoten. Sie müssen also nicht einmal Wrapper schreiben und erhalten transparent sogar die Methoden, von denen Sie nichts wussten.

Unter der Annahme, dass wir geförderte Methoden für Typparameter erhalten, die in generische Strukturen eingebettet sind (und ich hoffe, dass wir das tun), scheinen die Probleme mit dieser Methode besser gelöst zu sein.

Ich denke, die spezifische Lösung für http.ResponseWriter ist so etwas wie errors.Is/As . Es ist keine Sprachänderung erforderlich, nur eine Bibliothekserweiterung, um eine Standardmethode für das Umschließen von ResponseWriter und eine Möglichkeit zum Abfragen zu erstellen, ob einer der ResponseWriter in einer Kette mit egwPush umgehen kann. Ich bin skeptisch, ob Generika für so etwas gut geeignet wären, weil der springende Punkt darin besteht, Laufzeitwahl zwischen optionalen Schnittstellen zu haben, z. B. Push ist nur in http2 verfügbar und nicht, wenn ich einen lokalen http1-Entwicklungsserver hochfahre.

Wenn ich Github durchsehe, glaube ich nicht, dass ich jemals ein Problem für diese Idee erstellt habe, also werde ich das vielleicht jetzt tun.

Bearbeiten: #39558.

@toolbox
Ich vermute, dass es zusammen mit seinem internen Monomorphisierungscode ungefähr so ​​​​aussehen würde:

package iter

type Any interface{}

type Iterator(type T Any) interface {
    Next() bool
    Value() T
}

type ReversibleIterator(type T Any) interface {
    Iterator(T)
    NextBack() bool
}

type mapIt(type I Iterator(T), T Any, U Any) struct {
    parent I
    mapF func(T) U
}

func (i mapIt(type I Iterator(T))) Next() bool {
    return i.parent.Next()
}

func (i mapIt(type I Iterator(T), T Any, U Any)) Value() U { 
    return i.mapF(i.parent.Value())
}

func (i mapIt(type I ReversibleIterator(T))) NextBack() bool { 
    return i.parent.NextBack()
}

// Monomorphisation
type mapIt<OnlyForward, int, float64> struct {
    parent OnlyForward,
    mapF func(int) float64
}

func (i mapIt<OnlyForward, int, float64>) Next() bool {
    return i.parent.Next()
}

func (i mapIt<OnlyForward, int, float64>) Value() float64 {
    return i.mapF(i.parent.Value())
}

type mapIt<Slice, int, string> struct {
    parent Slice,
    mapF func(int) string
}

func (i mapIt<Slice, int, string>) Next() bool {
    return i.parent.Next()
}

func (i mapIt<Slice, int, string>) Value() string {
    return i.mapF(i.parent.Value())
}

func (i mapIt<Slice, int, string>) NextBack() bool {
    return i.parent.NextBack()
}



Ich vermute, dass es zusammen mit seinem internen Monomorphisierungscode ungefähr so ​​​​aussehen würde:

FWIW, hier ist ein Tweet von mir vor einigen Jahren, in dem untersucht wird, wie Iteratoren in Go mit Generika funktionieren könnten. Wenn Sie eine globale Substitution vornehmen, um <T> durch (type T) zu ersetzen, haben Sie etwas, das nicht weit vom aktuellen Vorschlag entfernt ist: https://twitter.com/rogpeppe/status/425035488425037824

FWIW, es sollte klargestellt werden, dass dies eine Art Verkauf von "Karten und Filtern und funktionalen Dingen" ist. Ich persönlich möchte zum Beispiel keine eingebauten Datenstrukturen in meinem Code abbilden und filtern (ich bevorzuge for-Schleifen). Kann aber auch bedeuten...

Ich verstehe Ihren Standpunkt und bin nicht anderer Meinung, und ja, wir werden von den Dingen profitieren, die Ihre Beispiele abdecken.
Aber ich frage mich immer noch, wie etwas wie k8s betroffen wäre, oder eine andere Codebasis mit "generischen" Datentypen, bei denen die Art der ausgeführten Aktionen keine Karten oder Filter sind oder zumindest darüber hinausgehen. Ich frage mich, wie effektiv Contracts oder FGG in solchen Kontexten die Typensicherheit und Leistung erhöhen.

Sie fragen sich, ob jemand auf eine Codebasis verweisen kann, die hoffentlich einfacher als k8s ist und in diese Art von Kategorie passt?

@urandom wow. Wenn Sie also ein mapIt mit einem parent instanziieren, das ReversibleIterator implementiert, dann hat mapIt eine NextBack() -Methode, und wenn nicht, hat es eine NextBack()-Methode. T. Lese ich das richtig?

Wenn ich darüber nachdenke, scheint es aus Sicht der Bibliothek nützlich zu sein. Sie haben einige generische Strukturtypen, die ziemlich offen sind ( Any Typparameter) und sie haben viele Methoden, die durch verschiedene Schnittstellen eingeschränkt sind. Wenn Sie also die Bibliothek in Ihrem eigenen Code verwenden, gibt Ihnen der Typ, den Sie in die Struktur einbetten, die Möglichkeit, einen bestimmten Satz von Methoden aufzurufen, sodass Sie einen bestimmten Satz der Funktionalität der Bibliothek erhalten. Was diese Funktionalität ist, wird zur Kompilierzeit basierend auf den Methoden Ihres Typs ermittelt.

... Es scheint ein bisschen wie das zu sein, was @ChrisHines angesprochen hat, dass Sie irgendwie Code schreiben könnten, der mehr oder weniger Funktionalität hat, je nachdem, was Ihr Typ implementiert, aber andererseits ist es wirklich eine Frage des verfügbaren Methodensatzes, der zunimmt oder abnimmt. nicht das Verhalten einer einzelnen Methode, also ja, ich sehe nicht, wie der http2-Hijacker-Sache dabei geholfen wird.

Jedenfalls sehr interessant.

Nicht, dass ich das tun würde, aber ich denke, das wäre möglich:

type OverrideX interface {
    GetX() int
}

type OverrideY interface {
    GetY() int
}

type Inheritor(type child Any) struct {
    Parent
    c child
}

func (i Inheritor(type child OverrideX)) GetX() int {
    return i.c.GetX()
}

func (i Inheritor(type child OverrideY)) GetY() int {
    return i.c.GetY()
}

type Parent struct {
    x, y int
}

func (p Parent) GetX() int {
    return p.x
}

func (p Parent) GetY() int {
    return p.y
}

type Child struct {
    x int
}

func (c Child) GetX() int {
    return c.x
}

func main() {
    i := Inheritor(Child){Parent{5, 6}, Child{3}}
    x, y := i.GetX(), i.GetY() // 3, 6
}

Wieder hauptsächlich ein Scherz, aber ich finde es gut, die Grenzen des Möglichen auszuloten.

Bearbeiten: Hm, zeigt, wie Sie je nach Typparameter unterschiedliche Methodensätze haben können, erzeugt jedoch genau den gleichen Effekt wie das einfache Einbetten Parent in Child . Wieder blödes Beispiel ;)

Ich bin kein großer Fan von Methoden, die nur bei einem bestimmten Typ aufgerufen werden können. Angesichts des Beispiels von @tooolbox wäre das Testen wahrscheinlich mühsam, da einige Methoden nur bei einem bestimmten Kind aufrufbar sind - der Tester wird wahrscheinlich einen Fall übersehen. Es ist auch ziemlich unklar, welche Methoden verfügbar sind, und eine IDE zu verlangen, um Vorschläge zu machen, ist nicht das, was Go erfordern sollte. Sie können dies jedoch nur mit dem von der Struktur angegebenen Typ implementieren, indem Sie eine Typzusicherung in der Methode durchführen.

func (i Inheritor(type child Any)) GetX() int {
    if c, ok := i.c.(OverrideX); ok {
        return c.GetX()
    }
    return i.Parent.GetX()
}

func (i Inheritor(type child Any)) GetY() int {
    if c, ok := i.c.(OverrideY); ok {
        return c.GetY()
    }
    return i.Parent.GetY()
} 

Dieser Code ist außerdem typsicher, klar, leicht zu testen und läuft wahrscheinlich ohne Verwirrung identisch mit dem Original.

@TotallyGamerJet
Dieses spezielle Beispiel ist typsicher, andere jedoch nicht und erfordern Laufzeitpaniken mit inkompatiblen Typen.

Außerdem bin ich mir nicht sicher, wie der Tester möglicherweise Fälle übersehen könnte, da sie höchstwahrscheinlich diejenigen sind, die den generischen Code überhaupt geschrieben haben. Ob es klar ist oder nicht, ist ein bisschen subjektiv, obwohl es definitiv keine IDE erfordert, um es abzuleiten. Denken Sie daran, dass dies keine Funktionsüberladung ist, die Methode kann entweder aufgerufen werden oder nicht, es ist also nicht so, dass ein Fall versehentlich übersprungen werden kann. Jeder kann sehen, dass diese Methode für einen bestimmten Typ existiert, und er muss sie möglicherweise noch einmal lesen, um zu verstehen, welcher Typ erforderlich ist, aber das war es auch schon.

@urandom Ich meinte nicht unbedingt, dass jemand mit diesem speziellen Beispiel einen Fall verpassen würde - es ist sehr kurz. Ich meinte, dass, wenn Sie Tonnen von Methoden haben, die nur bei bestimmten Typen aufrufbar sind. Also stehe ich dazu, kein Subtyping zu verwenden (wie ich es gerne nenne). Es ist sogar möglich, das "Ausdrucksproblem" zu lösen, ohne Typzusicherungen oder Subtyping zu verwenden. Hier ist wie:

type Any interface {}

type Evaler(type t Any) interface {
    Eval() t
}

type Num struct {
    value int
}

func (n Num) Eval() int {
    return n.value
}

type Plus(type a Evaler(type t Any)) struct {
    left a
    right a
}

func (p Plus(type a Evaler(type t Any)) Eval() t {
    return p.left.Eval() + p.right.Eval()
}

func (p Plus(type a Evaler(type t Any)) String() string {
    return fmt.Sprintf("(%s+%s)", p.left, p.right)
}

type Expr interface {
    Evaler
    fmt.Stringer
}

func main() {
    var e Expr = Plus(Num){Num{1}, Num{2}}
    var v int = e.Eval() // 3
    var s string = e.String() // "(1+2)"
}

Jeder Missbrauch der Eval-Methode sollte zur Kompilierzeit abgefangen werden, da es nicht erlaubt ist, Eval auf Plus mit einem Typ aufzurufen, der keine Addition implementiert. Obwohl es möglich ist, String() unsachgemäß zu verwenden (möglicherweise durch Hinzufügen von Strukturen), sollten gute Tests diese Fälle abfangen. Und Go stellt normalerweise Einfachheit über "Korrektheit". Das einzige, was mit Subtyping gewonnen wird, ist mehr Verwirrung in den Dokumenten und in der Verwendung. Wenn Sie ein Beispiel geben können, das eine Untertypisierung erfordert, bin ich vielleicht eher geneigt, es für eine gute Idee zu halten, aber derzeit bin ich nicht überzeugt.
EDIT: Fehler behoben und verbessert

@TotallyGamerJet In Ihrem Beispiel sollte die String-Methode String rekursiv aufrufen, nicht Eval

@TotallyGamerJet In Ihrem Beispiel sollte die String-Methode String rekursiv aufrufen, nicht Eval

@magisch
Ich bin mir nicht sicher, was du meinst. Der Typ der Plus-Struktur ist ein Evaler, der nicht sicherstellt, dass fmt.Stringer erfüllt ist. Das Aufrufen von String() auf beiden Evalern würde eine Typzusicherung erfordern und wäre daher nicht typsicher.

@TotallyGamerJet
Leider ist das die Idee der String-Methode. Es sollte alle String-Methoden für seine Mitglieder rekursiv aufrufen, sonst hat es keinen Sinn. Aber Sie sehen bereits, dass es eine Typzusicherung und eine Panik erfordern würde, wenn Sie nicht sicherstellen können, dass die Methode des Plug-Typs einen Typ a erfordert, der eine String-Methode hat

@urandom
Du hast Recht! Überraschenderweise erledigt der Sprintf diese Art von Behauptung für Sie. Sie können also einfach sowohl das linke als auch das rechte Feld einsenden. Es kann zwar immer noch in Panik geraten, wenn die Typen in Plus Stringer nicht implementieren, aber ich bin damit einverstanden, da es möglich ist, Panik zu vermeiden, indem das Verb %v verwendet wird, um die Struktur auszugeben (es wird String( ) wenn verfügbar). Ich denke, diese Lösung ist klar und alle anderen Unsicherheiten sollten im Code dokumentiert werden. Ich bin also immer noch nicht überzeugt, warum eine Untertypisierung notwendig ist.

@TotallyGamerJet
Ich persönlich sehe immer noch nicht ein, welche Probleme entstehen können, wenn es Methoden mit unterschiedlichen Einschränkungen geben darf. Die Methode ist immer noch da, und der Code beschreibt klar, welche Argumente (und Empfänger im speziellen Fall) erforderlich sind.
Genauso wie das Vorhandensein einer Methode, das Akzeptieren eines string -Arguments oder eines MyType -Empfängers klar lesbar und eindeutig ist, wäre dies auch die folgende Definition:

func (rec MyType(type T SomeInterface(T)) Foo() T

Die Anforderungen sind in der Signatur selbst deutlich gekennzeichnet. IE ist es MyType(type T SomeInterface(T)) und sonst nichts.

Änderung https://golang.org/cl/238003 erwähnt dieses Problem: design: add go2draft-type-parameters.md

Änderung https://golang.org/cl/238241 erwähnt dieses Problem: content: add generics-next-step article

Weihnachten ist früh!

  • Ich kann sehen, dass viel Mühe darauf verwendet wurde, das Designdokument zugänglich zu machen, es zeigt und es ist großartig und wird sehr geschätzt.
  • Diese Iteration ist in meinen Augen eine große Verbesserung und ich konnte sehen, dass dies so wie es ist implementiert wird.
  • Stimme so ziemlich der gesamten Argumentation und Logik zu.
  • Wenn Sie also eine Einschränkung für einen einzelnen Typparameter angeben, müssen Sie dies für alle tun.
  • Vergleichbar klingt gut.
  • Typenlisten in Schnittstellen sind nicht schlecht; Ich stimme zu, dass es besser ist als Operatormethoden, aber meiner Meinung nach ist es wahrscheinlich der größte Bereich für weitere Diskussionen.
  • Typinferenz ist (noch) großartig.
  • Die Inferenz für typparametrisierte Einschränkungen mit einem Argument scheint wie Cleverness über Klarheit zu sein.
  • Ich mag "Wir behaupten nicht, dass dies einfach ist" im Diagrammbeispiel. Das ist gut.
  • (type *T constraint) scheint eine gute Lösung für das Zeigerproblem zu sein.
  • Völlig einverstanden mit der Änderung func(x(T)) .
  • Ich denke, wir wollen Typrückschluss für zusammengesetzte Literale auf Anhieb? 😄

Vielen Dank an das Go-Team! 🎉

https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#comparable -types-in-constraints

Ich glaube, vergleichbar ist eher ein eingebauter Typ als eine Schnittstelle. Ich glaube, es ist ein kleiner Fehler im Vorschlagsentwurf.

type ComparableHasher interface {
    comparable
    Hash() uintptr
}

muss sein

type ComparableHasher interface {
    type comparable
    Hash() uintptr
}

Der Spielplatz scheint auch anzuzeigen, dass es type comparable sein muss
https://go2goplay.golang.org/p/mhrl0xYsMyj

BEARBEITEN: Ian Lance Taylor und Robert Griesemer reparieren das go2go-Tool (war ein kleiner Fehler im go2go-Übersetzer, nicht im Entwurf. Der Designentwurf war korrekt)

Gab es Überlegungen, es den Leuten zu ermöglichen, ihre eigenen generischen Hash-Tabellen und dergleichen zu schreiben? ISTM, das ist derzeit sehr begrenzt (insbesondere im Vergleich zur integrierten Karte). Grundsätzlich hat die eingebaute Karte comparable als Key-Constraint, aber natürlich reichen == und != nicht aus, um eine Hash-Tabelle zu implementieren. Eine Schnittstelle wie ComparableHasher übergibt nur die Verantwortung, eine Hash-Funktion zu schreiben, an den Aufrufer, sie beantwortet nicht die Frage, wie sie tatsächlich aussehen würde (auch sollte der Aufrufer wahrscheinlich nicht dafür verantwortlich sein; gute Hash-Funktionen zu schreiben ist schwierig). Schließlich könnte die Verwendung von Zeigern als Schlüssel grundsätzlich unmöglich sein - das Konvertieren eines Zeigers in uintptr zur Verwendung als Index würde riskieren, dass der GC den Pointee bewegt und somit den Bucket ändert (mit Ausnahme dieses Problems wird ein vordeklariertes func hash(type T comparable)(v T) uintptr könnte eine - wahrscheinlich nicht ideale - Lösung sein).

Ich kann "es ist nicht wirklich machbar" als Antwort gut akzeptieren, ich bin nur neugierig zu wissen, ob Sie darüber nachgedacht haben :)

@gertcuykens Ich habe eine Korrektur für das go2go-Tool vorgenommen, um comparable wie beabsichtigt zu handhaben.

@Merovius Wir erwarten, dass Personen, die eine generische Hash-Tabelle schreiben, ihre eigene Hash-Funktion und möglicherweise ihre eigene Vergleichsfunktion bereitstellen. Wenn Sie Ihre eigene Hash-Funktion schreiben, kann das Paket https://golang.org/pkg/hash/maphash/ nützlich sein. Sie haben Recht, dass der Hash eines Zeigerwerts von dem Wert abhängen muss, auf den dieser Zeiger zeigt. es kann nicht vom Wert des in uintptr umgewandelten Zeigers abhängen.

Ich bin mir nicht sicher, ob dies eine Einschränkung der aktuellen Implementierung des Tools ist, aber ein Versuch, einen generischen Typ zurückzugeben, der durch eine Schnittstelle eingeschränkt ist, gibt einen Fehler zurück:
https://go2goplay.golang.org/p/KYRFL-vrcUF

Ich habe einen realen Anwendungsfall implementiert, den ich gestern für Generika hatte . Es ist eine generische Pipeline-Abstraktion, die es ermöglicht, Phasen der Pipeline unabhängig voneinander zu skalieren, und die Abbruch- und Fehlerbehandlung unterstützt (sie läuft nicht im Playground, weil sie von errgroup abhängt, aber die Ausführung mit dem go2go-Tool scheint es zu tun arbeiten). Einige Beobachtungen:

  • Es hat ziemlich viel Spaß gemacht. Ein funktionierender Type-Checker hat beim Iterieren des Designs tatsächlich sehr geholfen, indem Designfehler in Typfehler übersetzt wurden. Das Endergebnis ist ~100 LOC einschließlich Kommentare. Insgesamt ist die Erfahrung beim Schreiben von generischem Code meiner Meinung nach angenehm.
  • Dieser Anwendungsfall funktioniert zumindest reibungslos mit Typinferenz, es sind keine expliziten Instanziierungen erforderlich. Ich denke, das ist ein gutes Zeichen für das Inferenzdesign.
  • Ich denke, dieses Beispiel würde von der Möglichkeit profitieren, Methoden mit zusätzlichen Typparametern zu haben. Das Erfordernis einer Top-Level-Funktion für Compose bedeutet, dass die Konstruktion der Pipeline umgekehrt erfolgt – die letzten Stufen der Pipeline müssen konstruiert werden, um sie an die Funktionen weiterzugeben, die die früheren Stufen aufbauen. Wenn Methoden Typparameter haben könnten, könnten Sie Stage einen konkreten Typ haben und func (s *Stage(A, B)) Compose(type C)(n int, f func(B) C) *Stage(A, C) . Und der Bau der Pipeline würde in der gleichen Reihenfolge erfolgen, in der sie verlegt wird (siehe Kommentar auf dem Spielplatz). Es könnte natürlich auch eine elegantere API im bestehenden Entwurf geben, die ich nicht sehe - es ist schwer, etwas Negatives zu beweisen. Mich würde ein funktionierendes Beispiel dafür interessieren.

Insgesamt gefällt mir der neue Entwurf, FWIW :) IMO das Löschen von Verträgen ist eine Verbesserung, ebenso wie die neue Möglichkeit, erforderliche Operatoren über Typenlisten anzugeben.

[Bearbeiten: Fehler in meinem Code behoben, bei dem ein Deadlock auftreten konnte, wenn eine Pipeline-Phase fehlschlug. Parallelität ist schwierig]

Eine Frage an die Tool-Branche: Wird es mit dem letzten Go-Release (also v1.15, v1.15.1, ...) mithalten?

@urandom : Beachten Sie, dass der Wert, den Sie in Ihrem Code zurückgeben, vom Typ Foo (T) ist. Jeder
eine solche Typinstanziierung erzeugt einen neu definierten Typ, in diesem Fall Foo(T).
(Wenn Sie mehrere Foo(T) im Code haben, sind sie natürlich alle gleich
definierter Typ).

Aber der Ergebnistyp Ihrer Funktion ist V, was ein Typparameter ist. Notiz
dass der Typparameter durch die Valuer-Schnittstelle eingeschränkt wird, aber das ist er
_nicht_ eine Schnittstelle (oder sogar diese Schnittstelle). V ist ein Typparameter, der ist
eine neue Art von Typ, über den wir Dinge wissen, die durch seine Beschränkung beschrieben werden.
Hinsichtlich der Zuweisbarkeit verhält es sich wie ein definierter Typ namens V.

Sie versuchen also, einer Variablen vom Typ V einen Wert vom Typ Foo(T) zuzuweisen
(was weder Foo(T) noch Valuer(T) ist), es hat nur Eigenschaften, die von beschrieben werden
Gutachter(T)). Damit schlägt die Zuordnung fehl.

(Nebenbei verfeinern wir noch unser Verständnis von Typparametern
und schließlich müssen wir es genau genug buchstabieren, damit wir a schreiben können
spez. Denken Sie jedoch daran, dass jeder Typparameter praktisch ein neuer ist
über einen definierten Typ wissen wir nur so viel, wie seine Typbeschränkung angibt.)

Vielleicht wollten Sie Folgendes schreiben: https://go2goplay.golang.org/p/8Hz6eWSn8Ek?

@Inuart Wenn Sie mit Tool-Zweig den dev.go2go-Zweig meinen: Dies ist ein Prototyp, der mit Blick auf die Zweckmäßigkeit und zu Experimentierzwecken erstellt wurde. Wir möchten, dass die Leute damit spielen und versuchen, Code zu schreiben, aber es ist keine gute Idee, sich für Produktionssoftware auf den Übersetzer zu _verlassen_. Viele Dinge können sich ändern (sogar die Syntax, wenn nötig). Wir werden Fehler beheben und das Design anpassen, wenn wir aus dem Feedback lernen. Es scheint weniger wichtig zu sein, mit den neuesten Go-Versionen Schritt zu halten.

Ich habe gestern einen realen Anwendungsfall für Generika implementiert. Es ist eine generische Pipeline-Abstraktion, die es ermöglicht, Phasen der Pipeline unabhängig zu skalieren, und die Abbruch- und Fehlerbehandlung unterstützt (sie läuft nicht im Playground, weil sie von errgroup abhängt, aber die Ausführung mit dem go2go-Tool scheint zu funktionieren).

Ich mag das Beispiel. Ich habe es gerade vollständig durchgelesen und die Sache, die mich am meisten stolperte (nicht einmal der Erklärung wert), hatte nichts mit den beteiligten Generika zu tun. Ich denke, das gleiche Konstrukt ohne Generika wäre nicht viel einfacher zu verstehen. Es ist auch definitiv eines dieser Dinge, die Sie einmal geschrieben haben möchten, mit Tests, und sich später nicht noch einmal damit herumschlagen müssen.

Eine Sache, die zur Lesbarkeit und Überprüfung beitragen könnte, wäre, wenn das Go-Tool eine Möglichkeit hätte, die monomorphisierte Version des generischen Codes anzuzeigen, damit Sie sehen können, wie sich die Dinge entwickeln. Könnte undurchführbar sein, teilweise weil Funktionen in der endgültigen Compiler-Implementierung möglicherweise nicht einmal monomorphisiert sind, aber ich denke, es wäre wertvoll, wenn es überhaupt erreichbar wäre.

Ich denke, dieses Beispiel würde von der Möglichkeit profitieren, Methoden mit zusätzlichen Typparametern zu haben.

Ich habe diesen Kommentar auch auf Ihrem Playground gesehen; Auf jeden Fall scheint die alternative Aufrufsyntax lesbarer und unkomplizierter zu sein. Können Sie das näher erläutern? Nachdem ich meinen Kopf kaum um Ihren Beispielcode gewickelt habe, habe ich Probleme, den Sprung zu machen :)

Sie versuchen also, einer Variablen vom Typ V einen Wert vom Typ Foo(T) zuzuweisen
(was weder Foo(T) noch Valuer(T) ist), es hat nur Eigenschaften, die von beschrieben werden
Gutachter(T)). Damit schlägt die Zuordnung fehl.

Tolle Erklärung.

... Ansonsten ist es traurig zu sehen, dass der HN-Post von der Rust-Crowd gekapert wurde. Es wäre schön gewesen, mehr Feedback von Gophers zu dem Vorschlag zu bekommen.

Zwei Fragen an das Go-Team:

Gibt es einen Unterschied zwischen diesen beiden oder handelt es sich um einen Fehler im go2-Spielplatz? Der erste kompiliert, der zweite gibt einen Fehler aus

type Addable interface {
    type int, float64
}

func Add(type T Addable)(a, b T) T {
  return a + b
}
type Addable interface {
    type int, float64, string
}

func Add(type T Addable)(a, b T) T {
  return a + b
}

Schlägt fehl mit: invalid operation: operator + not defined for a (variable of type T)

Nun, das war eine höchst unerwartete und angenehme Überraschung. Ich hatte auf eine Möglichkeit gehofft, dies irgendwann tatsächlich auszuprobieren, aber ich hatte nicht so bald damit gerechnet.

Zuerst einen Fehler gefunden: https://go2goplay.golang.org/p/1r0NQnJE-NZ

Zweitens habe ich ein Iterator-Beispiel erstellt und war etwas überrascht, dass diese Typinferenz nicht funktioniert. Ich kann es einfach einen Schnittstellentyp direkt zurückgeben lassen, aber ich hätte nicht gedacht, dass es nicht in der Lage wäre, darauf zu schließen, da alle erforderlichen Typinformationen durch das Argument kommen.

Bearbeiten: Außerdem denke ich, wie mehrere Leute gesagt haben, dass das Hinzufügen neuer Typen während der Methodendeklaration sehr nützlich wäre. Was die Schnittstellenimplementierung betrifft, könnten Sie entweder die Schnittstellenimplementierung einfach nicht zulassen, die Implementierung nur zulassen, wenn die Schnittstelle dort auch Generika aufruft ( type Example interface { Method(type T someConstraint)(v T) bool } ), oder möglicherweise die Schnittstelle implementieren lassen, wenn _beliebig_ möglich Variante davon implementiert die Schnittstelle und muss dann den Aufruf auf das beschränken, was die Schnittstelle will, wenn sie über die Schnittstelle aufgerufen wird. Beispielsweise,

„Geh
Typ Schnittstelle Schnittstelle {
Hole (Zeichenfolge) Zeichenfolge
}

Typ Beispiel (Typ T) Struktur {
vT
}

// Dies funktioniert nur, weil Interface.Get spezifischer ist als Example.Get.
func (e Beispiel(T)) Get(type R)(v R) T {
return fmt.Sprintf("%v: %v", v, ev)
}

func DoSomething(inter Interface) {
// Basiswert ist Example(string) und Example(string).Get(string) wird angenommen, weil es erforderlich ist.
fmt.Println(inter.Get("Beispiel"))
}

func main() {
// Zugelassen, weil Example(string).Get(string) möglich ist.
DoSomething(Example(string){v: "Ein Beispiel."})
}

@DeedleFake Das erste, was Sie melden, ist kein Fehler. Sie müssen im Moment https://go2goplay.golang.org/p/qo3hnviiN4k schreiben. Dies ist im Konstruktionsentwurf dokumentiert. In einer Parameterliste wird das Schreiben a(b) aus Gründen der Abwärtskompatibilität als a (b) ( a vom Typ b $ in Klammern) interpretiert. Wir können das in Zukunft ändern.

Das Iterator-Beispiel ist interessant - es sieht auf den ersten Blick wie ein Fehler aus. Bitte melden Sie einen Fehler (Anweisungen im Blogbeitrag) und weisen Sie ihn mir zu. Danke.

@Kashomon Der Blog-Beitrag (https://blog.golang.org/generics-next-step) schlägt die Mailingliste für Diskussionen und das Einreichen separater Probleme für Fehler vor. Danke.

Ich denke, das Problem mit + wurde bereits behoben.

@toolbox

Eine Sache, die zur Lesbarkeit und Überprüfung beitragen könnte, wäre, wenn das Go-Tool eine Möglichkeit hätte, die monomorphisierte Version des generischen Codes anzuzeigen, damit Sie sehen können, wie sich die Dinge entwickeln. Könnte undurchführbar sein, teilweise weil Funktionen in der endgültigen Compiler-Implementierung möglicherweise nicht einmal monomorphisiert sind, aber ich denke, es wäre wertvoll, wenn es überhaupt erreichbar wäre.

Das go2go-Tool kann dies tun. Statt go tool go2go run x.go2 zu verwenden, schreiben Sie go tool go2go translate x.go2 . Dadurch wird eine Datei x.go mit dem übersetzten Code erstellt.

Allerdings muss ich sagen, dass es ziemlich anspruchsvoll zu lesen ist. Nicht unmöglich, aber nicht einfach.

@griesemer

Ich verstehe, dass das Rückgabeargument stattdessen eine Schnittstelle sein kann, aber ich verstehe nicht wirklich, warum es nicht der generische Typ selbst sein kann.

Sie können beispielsweise denselben generischen Typ als Eingabeparameter verwenden, und das funktioniert einwandfrei:
https://go2goplay.golang.org/p/LuDrlT3zLRb
Funktioniert das, weil der Typ bereits instanziiert wurde?

@urandom schrieb:

Ich verstehe, dass das Rückgabeargument stattdessen eine Schnittstelle sein kann, aber ich verstehe nicht wirklich, warum es nicht der generische Typ selbst sein kann.

Theoretisch könnte es, aber es macht keinen Sinn, einen Rückgabetyp generisch zu machen, wenn der Rückgabetyp nicht generisch ist, da er durch den Funktionsblock bestimmt wird, dh durch den Rückgabewert.

Normalerweise werden generische Parameter entweder vollständig durch das Parameterwert-Tupel oder durch den Typ der Funktionsanwendung an der Aufrufstelle bestimmt (bestimmt die Instanziierung des generischen Rückgabetyps).

Theoretisch könnten Sie auch generische Typparameter zulassen, die nicht durch das Parameterwerttupel bestimmt werden und explizit angegeben werden müssen, z.

func f(type S)(i int) int
{
    s S =...
    return 2
}

weiß nicht wie viel sinn das macht.

@urandom Ich meinte nicht unbedingt, dass jemand mit diesem speziellen Beispiel einen Fall verpassen würde - es ist sehr kurz. Ich meinte, dass, wenn Sie Tonnen von Methoden haben, die nur bei bestimmten Typen aufrufbar sind. Also stehe ich dazu, kein Subtyping zu verwenden (wie ich es gerne nenne). Es ist sogar möglich, das "Ausdrucksproblem" zu lösen, ohne Typzusicherungen oder Subtyping zu verwenden. Hier ist wie:

type Any interface {}

type Evaler(type t Any) interface {
  Eval() t
}

type Num struct {
  value int
}

func (n Num) Eval() int {
  return n.value
}

type Plus(type a Evaler(type t Any)) struct {
  left a
  right a
}

func (p Plus(type a Evaler(type t Any)) Eval() t {
  return p.left.Eval() + p.right.Eval()
}

func (p Plus(type a Evaler(type t Any)) String() string {
  return fmt.Sprintf("(%s+%s)", p.left, p.right)
}

type Expr interface {
  Evaler
  fmt.Stringer
}

func main() {
  var e Expr = Plus(Num){Num{1}, Num{2}}
  var v int = e.Eval() // 3
  var s string = e.String() // "(1+2)"
}

Jeder Missbrauch der Eval-Methode sollte zur Kompilierzeit abgefangen werden, da es nicht erlaubt ist, Eval auf Plus mit einem Typ aufzurufen, der keine Addition implementiert. Obwohl es möglich ist, String() unsachgemäß zu verwenden (möglicherweise durch Hinzufügen von Strukturen), sollten gute Tests diese Fälle abfangen. Und Go stellt normalerweise Einfachheit über "Korrektheit". Das einzige, was mit Subtyping gewonnen wird, ist mehr Verwirrung in den Dokumenten und in der Verwendung. Wenn Sie ein Beispiel geben können, das eine Untertypisierung erfordert, bin ich vielleicht eher geneigt, es für eine gute Idee zu halten, aber derzeit bin ich nicht überzeugt.
EDIT: Fehler behoben und verbessert

Ich weiß nicht, warum verwendest du nicht '<> '?

@99yun
Bitte sehen Sie sich die FAQ an, die dem aktualisierten Entwurf beiliegen

Warum nicht die Syntax F\ verwendenwie C++ und Java?
Beim Analysieren von Code innerhalb einer Funktion, z. B. v := F\, an dem Punkt, an dem das < angezeigt wird, ist es nicht eindeutig, ob wir eine Typinstanziierung oder einen Ausdruck mit dem Operator < sehen. Um dies zu lösen, ist eine effektiv unbegrenzte Vorausschau erforderlich. Im Allgemeinen bemühen wir uns, den Go-Parser effizient zu halten.

@urandom Ein generischer Funktionskörper wird immer ohne Instanziierung typgeprüft (*); im Allgemeinen (wenn es zum Beispiel exportiert wird) können wir nicht wissen, wie es instanziiert wird. Bei der Typprüfung kann es sich nur auf die verfügbaren Informationen verlassen. Wenn der Ergebnistyp ein Typparameter ist und der Rückgabeausdruck einen anderen Typ hat, der nicht zuweisungskompatibel ist, kann die Rückgabe nicht funktionieren. Oder mit anderen Worten, wenn eine generische Funktion mit (möglicherweise abgeleiteten) Typargumenten aufgerufen wird, wird der Funktionskörper nicht erneut mit diesen Typargumenten typgeprüft. Es überprüft nur, ob die Typargumente die Einschränkungen der generischen Funktion erfüllen (nachdem die Funktionssignatur mit diesen Typargumenten instanziiert wurde). Hoffentlich hilft das.

(*) Genauer gesagt wird die generische Funktion typgeprüft, als wäre sie mit ihren eigenen Typparametern instanziiert worden; die Typparameter sind echte Typen; wir wissen nur so viel über sie, wie ihre Zwänge uns sagen.

Bitte lassen Sie uns diese Diskussion an anderer Stelle fortsetzen. Wenn Sie weitere Fragen zu einem Code haben, der Ihrer Meinung nach funktionieren sollte, reichen Sie bitte ein Problem ein, damit wir es dort besprechen können. Danke.

Es scheint keine Möglichkeit zu geben, eine Funktion zu verwenden, um einen Nullwert einer generischen Struktur zu erstellen. Nehmen Sie zum Beispiel diese Funktion:

func zero(type T)() T {
    var zero T
    return zero
}

Es scheint für die Grundtypen (int, float32 usw.) zu funktionieren. Wenn Sie jedoch eine Struktur mit einem generischen Feld haben, werden die Dinge seltsam. Nehmen Sie zum Beispiel:

type Opt(type T) struct {
    val T
}

func (o Opt(T)) Do() { /*stuff*/ }

Alles scheint gut. Wenn Sie jedoch Folgendes tun:

opt := zero(Opt(int))
opt.Do() 

Es wird nicht kompiliert und gibt den Fehler aus: opt.Do undefined (type func() Opt(int) has no field or method Do) Ich kann verstehen, wenn dies nicht möglich ist, aber es ist seltsam zu glauben, dass es sich um eine Funktion handelt, wenn int Teil des Opt-Typs sein soll. Aber was noch seltsamer ist, ist, dass es möglich ist, dies zu tun:

opt := zero(Opt)      //  But somehow this line compiles
opt(int).Do()         // This will panic

Ich bin mir nicht sicher, welcher Teil ein Fehler ist und welcher Teil beabsichtigt ist.
Code: https://go2goplay.golang.org/p/M0VvyEYwbQU

@TotallyGamerJet

Ihre Funktion zero() hat keine Argumente, daher findet kein Typrückschluss statt. Sie müssen die Funktion zero instanziieren und dann aufrufen.

opt := zero(Opt(int))()
opt.Do()

https://go2goplay.golang.org/p/N6ip-nm1BP-

@toolbox
Ah ja. Ich dachte, ich würde den Typ bereitstellen, aber ich habe die zweite Klammer vergessen, um die Funktion tatsächlich aufzurufen. Ich muss mich immer noch an diese Generika gewöhnen.

Ich habe immer verstanden, dass es eine Designentscheidung und kein Versehen war, keine Generika in Go zu haben. Es hat Go so viel einfacher gemacht, und ich kann die übertriebene Paranoia gegen eine einfache Kopienkopie nicht nachvollziehen. In unserem Unternehmen haben wir Tonnen von Go-Code erstellt und nie einen einzigen Fall gefunden, in dem wir Generika bevorzugen würden.

Für uns wird sich Go definitiv weniger nach Go anfühlen und es sieht so aus, als ob die Hype-Crowd es endlich geschafft hat, die Entwicklung von Go in eine falsche Richtung zu beeinflussen. Sie konnten Go nicht einfach in seiner einfachen Schönheit lassen, nein, sie mussten sich beschweren und beschweren, bis sie sich endlich durchgesetzt hatten.

Es tut mir leid, es soll niemanden herabwürdigen, aber so beginnt die Zerstörung einer schön gestalteten Sprache. Was kommt als nächstes? Wenn wir ständig Dinge ändern, wie so viele Leute es gerne hätten, landen wir bei "C++" oder "JavaScript".

Gehen Sie einfach so, wie es sein sollte!

@iio7 Ich habe den niedrigsten IQ von allen hier, meine Zukunft hängt davon ab sicherzustellen, dass ich den Code anderer Leute lesen kann. Der Hype begann nicht nur wegen Generika, sondern weil das neue Design keine Sprachänderung im aktuellen Vorschlag erfordert. Wir freuen uns alle, dass es ein Fenster gibt, um die Dinge einfach zu halten und dennoch einige generische und funktionale Goodies zu haben. Verstehen Sie mich nicht falsch, ich weiß, dass es immer eine Person im Team geben wird, die Code schreibt wie ein Raketenwissenschaftler, und ich, der Affe, nehme an, dass ich es einfach so verstehe? Die Beispiele, die Sie jetzt sehen, stammen also von den Raketenwissenschaftlern, und um ehrlich zu sein, ja, ich brauche einige Zeit, um sie zu lesen, aber am Ende mit etwas Versuch und Irrtum weiß ich, was sie zu programmieren versuchen. Ich sage nur, vertraue Ian und Robert und den anderen, sie sind mit dem Design noch nicht fertig. Wäre nicht überrascht, dass es in einem Jahr oder so Tools gibt, die dem Compiler helfen, eine perfekte einfache Affensprache zu sprechen, egal wie schwierig der generische Raketencode ist, den Sie darauf werfen. Das beste Feedback, das Sie geben können, ist, einige Beispiele neu zu schreiben und darauf hinzuweisen, wenn etwas zu überentwickelt ist, damit sie sicherstellen können, dass sich der Compiler darüber beschwert oder automatisch von etwas wie dem vet-Tool neu geschrieben wird.

Ich habe die FAQ zu <> gelesen, aber für eine dumme Person wie mich, wie ist es für den Parser schwieriger festzustellen, ob es sich um einen generischen Aufruf handelt, wenn er so aussieht wie v := F<T> und nicht wie v := F(T) ? Ist es mit den Klammern nicht schwieriger, da es nicht weiß, ob es sich um einen Funktionsaufruf mit T als regulärem Argument handelt?

Darüber hinaus denke ich, dass der Parser natürlich schnell gehalten werden sollte, aber vergessen wir nicht, was für den Programmierer am einfachsten zu lesen ist, was meiner Meinung nach ebenso wichtig ist. Ist es einfacher zu verstehen, was v := F(T) auf Anhieb macht? Oder ist v := F<T> einfacher? Auch wichtig zu berücksichtigen :)

Nicht für oder gegen v := F<T> argumentieren, sondern nur einige Gedanken aufwerfen, die eine Überlegung wert sein könnten.

Das ist heute legal Go :

    f, c, d, e := 1, 2, 3, 4
    a, b := f < c, d > (e)
    fmt.Println(a, b) // true false

Es hat keinen Sinn, über spitze Klammern zu diskutieren, es sei denn, Sie machen einen Vorschlag, was dagegen zu tun ist (Breakback Compat?). Es ist in jeder Hinsicht ein totes Thema. Es besteht praktisch keine Chance, dass spitze Klammern vom Go-Team übernommen werden. Bitte besprechen Sie alles andere.

Bearbeiten zum Hinzufügen: Entschuldigung, wenn dieser Kommentar zu knapp war. Es gibt viele Diskussionen über spitze Klammern auf Reddit und HN, was für mich sehr frustrierend ist, da das Problem der Rückkompatibilität bei Leuten, die sich für Generika interessieren, seit langem bekannt ist. Ich verstehe, warum die Leute spitze Klammern bevorzugen, aber es geht nicht ohne Breaking Change.

Danke für deinen Kommentar @iio7. Es besteht immer ein Risiko ungleich Null, dass die Dinge außer Kontrolle geraten. Deshalb sind wir auf dem Weg dorthin mit äußerster Vorsicht vorgegangen. Ich glaube, wir haben jetzt ein viel saubereres und orthogonaleres Design als im letzten Jahr; und ich persönlich hoffe, dass wir es noch einfacher machen können, besonders wenn es um Typenlisten geht - aber wir werden es herausfinden, wenn wir mehr erfahren. (Etwas Ironie, je orthogonaler und sauberer das Design wird, desto mächtiger wird es und desto komplexer kann man Code schreiben.) Die letzten Worte sind noch nicht gesprochen. Als wir letztes Jahr das erste potenziell realisierbare Design hatten, war die Reaktion vieler Leute ähnlich wie bei Ihnen: „Wollen wir das wirklich?“ Das ist eine ausgezeichnete Frage, und wir sollten versuchen, sie so gut wie möglich zu beantworten.

Die Beobachtung von @gertcuykens ist auch richtig - natürlich loten die Leute, die mit dem go2go-Prototyp spielen, seine Grenzen so weit wie möglich aus (was wir wollen), produzieren dabei aber auch Code, der in einer richtigen Produktion wahrscheinlich nicht durchkommen würde Einstellung. Inzwischen habe ich viel generischen Code gesehen, der wirklich schwer zu entziffern ist.

Es gibt Situationen, in denen generischer Code eindeutig ein Gewinn wäre; Ich denke an generische gleichzeitige Algorithmen, die es uns ermöglichen würden, etwas subtilen Code in eine Bibliothek einzufügen. Es gibt natürlich verschiedene Container-Datenstrukturen und Dinge wie sort usw. Wahrscheinlich benötigt die überwiegende Mehrheit des Codes überhaupt keine Generika. Im Gegensatz zu anderen Sprachen, in denen generische Funktionen für vieles, was man in der Sprache tut, von zentraler Bedeutung sind, sind generische Funktionen in Go nur ein weiteres Werkzeug im Go-Werkzeugsatz; nicht der grundlegende Baustein, auf dem alles andere aufbaut.

Zum Vergleich: In den frühen Tagen von Go neigten wir alle dazu, Goroutinen und Kanäle zu überbeanspruchen. Es dauerte eine Weile, um zu lernen, wann sie angemessen waren und wann nicht. Jetzt haben wir einige mehr oder weniger etablierte Richtlinien und wir verwenden sie nur, wenn es wirklich angebracht ist. Ich hoffe, dass dasselbe passieren würde, wenn wir Generika hätten.

Danke.

Aus dem Abschnitt des Designentwurfs zu [T] -basierten Syntaxen:

Die Sprache erlaubt im Allgemeinen ein abschließendes Komma in einer durch Kommas getrennten Liste, daher sollte A[T,] zulässig sein, wenn A ein generischer Typ ist, aber normalerweise wäre dies für einen Indexausdruck nicht zulässig. Der Parser kann jedoch nicht wissen, ob A ein generischer Typ oder ein Wert vom Slice-, Array- oder Map-Typ ist, sodass dieser Analysefehler erst gemeldet werden kann, nachdem die Typprüfung abgeschlossen ist. Wieder lösbar, aber kompliziert.

Könnte das nicht ziemlich einfach gelöst werden, indem man einfach das abschließende Komma in Indexausdrücken völlig legal macht und es dann einfach gofmt entfernen lässt?

@DeedleFake Möglicherweise. Das wäre sicherlich ein einfacher Ausweg; aber es scheint syntaktisch auch ein bisschen hässlich zu sein. Ich erinnere mich nicht mehr an alle Details, aber eine frühere Version hatte Unterstützung für [Typ T]-Stiltypparameter. Siehe den dev.go2go-Zweig, Commit 3d4810b5ba, wo die Unterstützung entfernt wurde. Das könnte man wieder ausgraben und untersuchen.

Kann die Länge der generischen Argumente in jeder [] -Liste auf die meisten begrenzt werden, um dieses Problem zu vermeiden, genau wie bei den eingebauten generischen Typen:

  • [N]T
  • []T
  • Karte[K]T
  • Chan T

Bitte beachten Sie, dass die letzten Argumente in eingebauten generischen Typen nicht in [] eingeschlossen sind.
Die generische Deklarationssyntax ist wie folgt: https://github.com/dotaheor/unify-Go-builtin-and-custom-generics#the-generic-declaration-syntax

@dotaheor Ich bin mir nicht sicher, was Sie genau fragen, aber es ist eindeutig notwendig, mehrere Typargumente für einen generischen Typ zu unterstützen. Beispiel: https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#containers .

@ianlancetaylor
Was ich meine, ist, dass jeder Typparameter von einem [] umschlossen ist, sodass der Typ in Ihrem Link wie folgt deklariert werden kann:

type Map[type K][type V] struct

Wenn es verwendet wird, ist es wie folgt:

var m Map[string]int

Ein Typargument, das nicht von [] eingeschlossen ist, zeigt das Ende der Verwendung eines generischen Typs an.

Als ich über die Bestellung von Arrays #39355 in Verbindung mit Generika nachdachte, stellte ich fest, dass "vergleichbar" im aktuellen Generika-Entwurf als vordeklarierte Typbeschränkung besonders behandelt wird (vermutlich weil nicht alle vergleichbaren Typen in einer Typliste einfach aufgelistet werden können). .

Es wäre schön, wenn der Generika-Entwurf geändert würde, um auch „bestellt“/„bestellbar“ zu definieren, ähnlich wie „vergleichbar“ vordefiniert ist. Es handelt sich um eine verwandte, häufig verwendete Beziehung zu Werten desselben Typs, und dies würde es zukünftigen Erweiterungen der Go-Sprache ermöglichen, die Reihenfolge für mehr Typen (Arrays, Strukturen, Slices, Summentypen, überprüfte Aufzählungen usw.) zu definieren, ohne dass dies zu Komplikationen führt dass nicht alle bestellten Typen in einer Typenliste wie "vergleichbar" auflistebar wären.

Ich schlage nicht vor, dass entschieden werden sollte, dass mehr Typen in der Sprachspezifikation bestellt werden sollten, aber diese Änderung an Generika macht es mit einer solchen Änderung aufwärtskompatibel (eine Einschränkung). wäre veraltet, wenn eine Typenliste verwendet wird). Das Sortieren von Paketen könnte mit der vordeklarierten Typbeschränkung "ordered" beginnen und später "nur" mit z. B. Arrays arbeiten, wenn sie jemals geändert werden, und es gibt keine Korrektur für die verwendete Einschränkung.

@martisch Ich denke, das müsste nur passieren, wenn bestellte Typen erweitert werden. Derzeit könnte constraints.Ordered alle Typen auflisten (das funktioniert nicht für comparable , da Zeiger, Strukturen, Arrays usw. vergleichbar sind, also muss das magisch sein. Aber ordered ist derzeit auf eine endliche Menge eingebauter zugrunde liegender Typen beschränkt) und Benutzer können sich darauf verlassen. Wenn wir zum Beispiel Ordnungen auf Arrays erweitern, können wir immer noch eine neue magische ordered Einschränkung hinzufügen und sie in constraints.Ordered einbetten. Das bedeutet, dass alle Benutzer von constraints.Ordered automatisch von der neuen Einschränkung profitieren würden. Natürlich würden Benutzer, die ihre eigene explizite Typliste schreiben, nicht davon profitieren - aber es ist dasselbe, wenn wir jetzt ordered hinzufügen, für Benutzer, die das nicht einbetten.

Meiner Meinung nach ist es also nicht verloren, dies zu verzögern, bis es tatsächlich sinnvoll ist. Wir sollten keinen möglichen Constraint-Set als vordeklarierten Bezeichner hinzufügen - geschweige denn einen potenziellen zukünftigen Constraint-Set :)

Wenn wir zum Beispiel Ordnungen auf Arrays erweitern, können wir immer noch eine neue magische ordered Einschränkung hinzufügen und sie in constraints.Ordered einbetten.

@Merovius Das ist ein guter Punkt, an den ich nicht gedacht hatte. Dadurch kann constraints.Ordered in Zukunft konsequent erweitert werden. Wenn dann noch ein constraints.Comparable dabei ist, dann passt das gut ins Gesamtgefüge.

@martisch , beachten Sie, dass ordered – im Gegensatz zu comparable – als Schnittstellentyp nicht kohärent ist, es sei denn, wir definieren auch eine (globale) Gesamtreihenfolge zwischen konkreten Typen oder verbieten nicht-generischem Code die Verwendung von < für Variablen des Typs ordered oder verbieten Sie die Verwendung von comparable als allgemeinem Laufzeitschnittstellentyp.

Andernfalls bricht die Transitivität von „implements“ zusammen. Betrachten Sie dieses Programmfragment:

    var x constraints.Ordered = int(0)
    var y constraints.Ordered = string("0")
    fmt.Println(x < y)

Was soll es ausgeben? (Ist die Antwort intuitiv oder willkürlich?)

@bcmills
Was ist mit fun (<)(type T Ordered)(t1 T,t2 T) Bool?

Um arithmetische Typen verschiedener Art zu vergleichen:

Wenn irgendeine Arithmetik S nur Ordered(T) für S<:T $ implementiert, dann:

//Isn't possible I think
interface SorT(S,T)
{ 
type S,T
}

fun (<)(type R SorT(S,T), S Ordered(R), T Ordered(R))(s S, t T) Bool

sollte einzigartig sein.

Für Laufzeitpolymorphismus müsste Ordered parametrierbar sein.
Oder:
Sie partitionieren Ordered in Tupeltypen und schreiben dann (<) um:

//but isn't supported that either
fun(<)(type R Ordered)(s R.0,t R.1)

Hallo!
Ich habe eine Frage.

Gibt es eine Möglichkeit, eine Typbeschränkung zu erstellen, die nur generische Typen mit einem Typparameter übergibt?
Etwas, das nur Result(T) / Option(T) /etc übergibt, aber nicht nur T .
Ich habe es versucht

type Box(type T) interface {
    Val() (T, bool)
}

aber es erfordert Val() Methode

type Box(type T) interface{}

ist ähnlich wie interface{} , also Any

auch versucht https://go2goplay.golang.org/p/lkbTI7yppmh -> Kompilierung schlägt fehl

type Box(type T) interface {
       type Box(T)
}

https://go2goplay.golang.org/p/5NsKWNa3E1k -> Kompilierung schlägt fehl

type Box(type T) interface{}

type Generic(type T) interface {
    type Box(T)
}

https://go2goplay.golang.org/p/CKzE2J-YOpD -> funktioniert nicht

type Box(type T) interface{}

type Generic(type T Box(T)) interface {}

Ist dieses Verhalten zu erwarten oder handelt es sich nur um einen Typüberprüfungsfehler?

@tdakkota- Einschränkungen gelten für Typargumente und sie gelten für die vollständig instanziierte Form von Typargumenten. Es gibt keine Möglichkeit, eine Typbeschränkung zu schreiben, die Anforderungen an die nicht instanziierte Form eines Typarguments stellt.

Bitte sehen Sie sich die FAQ an, die dem aktualisierten Entwurf beiliegen

Warum nicht die Syntax F verwendenwie C++ und Java?
Beim Analysieren von Code innerhalb einer Funktion, z. B. v := F, an dem Punkt, an dem das < angezeigt wird, ist es nicht eindeutig, ob wir eine Typinstanziierung oder einen Ausdruck mit dem Operator < sehen. Um dies zu lösen, ist eine effektiv unbegrenzte Vorausschau erforderlich. Im Allgemeinen bemühen wir uns, den Go-Parser effizient zu halten.

@TotallyGamerJet Was auch immer!

Wie geht man mit dem Nullwert des generischen Typs um? Wie können wir ohne Enum mit optionalem Wert umgehen?
Zum Beispiel: Die generische Version von vector und eine Funktion namens First geben das erste Element zurück, wenn seine Länge > 0 ist, sonst Nullwert des generischen Typs.
Wie schreiben wir solchen Code? Da wir nicht wissen, welche Art von Vektor, wenn chan/slice/map , können wir return (nil, false) sein, wenn struct oder primitive type wie string sein , int , bool , wie geht man damit um?

@leaxoy

var zero T sollte reichen

@leaxoy

var zero T sollte reichen

Eine globale magische Variable wie nil ?

@leaxoy
var zero T sollte reichen

Eine globale magische Variable wie nil ?

Zu diesem Thema wird ein Vorschlag diskutiert - siehe Vorschlag: Go 2: Universal Zero Value with type inference #35966 .

Es untersucht mehrere neue alternative Syntaxen für einen Ausdruck (keine Anweisung wie var zero T ), der immer den Nullwert eines Typs zurückgibt.

Der Nullwert sieht derzeit machbar aus, aber kann er Platz auf dem Stack oder Heap einnehmen? Sollten wir in Betracht ziehen, enum Option zu verwenden, um dies in einem Schritt abzuschließen.
Andernfalls, wenn der Nullwert keinen Platz benötigt, wäre es besser und keine notwendige Aufzählung hinzufügen.

Der Nullwert sieht derzeit machbar aus, aber kann er Platz auf dem Stack oder Heap einnehmen?

Ich glaube, historisch hat der Go-Compiler diese Art von Fällen optimiert. Ich mache mir keine Sorgen.

In C++-Vorlagen kann ein Standardtypwert angegeben werden. Wurde ein ähnliches Konstrukt für go-generische Typparameter in Betracht gezogen? Dies würde es potenziell ermöglichen, vorhandene Typen nachzurüsten, ohne vorhandenen Code zu brechen.

Betrachten Sie beispielsweise den vorhandenen Typ asn1.ObjectIdentifier , der ein []int ist. Ein Problem mit diesem Typ ist, dass er nicht mit der ASN.1-Spezifikation konform ist, die besagt, dass jeder Sub-Oid ein INTEGER beliebiger Länge sein kann (z. B. *big.Int ). Potenziell könnte ObjectIdentifier modifiziert werden, um einen generischen Parameter zu akzeptieren, aber das würde eine Menge existierenden Codes beschädigen. Wenn es eine Möglichkeit gäbe, int als Standardparameterwert anzugeben, wäre es vielleicht möglich, vorhandenen Code nachzurüsten.

type SignedInteger interface {
    type int, int32, int64, *big.Int
}
type ObjectIdentifier(type T SignedInteger) []T
// type ObjectIdentifier(type T SignedInteger=int) []T  // `int` would be the default instantiation type.

// New code with generic awareness would compile in go2.
var oid1 ObjectIdentifier(int) = ObjectIdentifier(int){1, 2, 3}

// But existing code would fail to compile:
var oid1 ObjectIdentifier = ObjectIdentifier{1, 2, 3}

Nur um das klarzustellen, das obige asn1.ObjectIdentifier ist nur ein Beispiel. Ich sage nicht, dass die Verwendung von Generika der einzige Weg oder der beste Weg ist, das ASN.1-Compliance-Problem zu lösen.

Gibt es außerdem Pläne, parametrisierbare endliche Schnittstellengrenzen zu ermöglichen?:

type Ordable(type T, S) interface {
    type S, type T
}

So wird die Where-Bedingung für den Typparameter unterstützt.
Können wir solchen Code schreiben:

type Vector(type T) struct {
    vec []T
}

func (v Vector(T)) Sum() T where T: Summable {
      //
}

func (v Vector(T)) First()  (T, bool) {
     //
}

Die Methode Sum funktioniert nur, wenn der Typparameter T Summable ist, andernfalls können wir Sum nicht auf Vector aufrufen.

Hallo @leaxoy

Sie können einfach etwas wie https://go2goplay.golang.org/p/pRznN30Qu8V schreiben

type Addable interface {
    type int, uint
}

type SummableVector(type T Addable) Vector(T)

func (v SummableVector(T)) Sum() T {
    var r T
    for _, i := range v.vec {
        r = r + i
    }
    return r
}

Ich denke, die Klausel where scheint nicht Go-ähnlich zu sein und wäre schwer zu analysieren, sie sollte so etwas sein

type Vector(type T) struct {
    vec []T
}

func (v Vector(T Summable)) Sum() T {
      //
}

func (v Vector(T)) First()  (T, bool) {
     //
}

aber es scheint eine Methodenspezialisierung zu sein.

@sebastien-rosset Wir haben Standardtypen für generische Typparameter nicht berücksichtigt. Die Sprache hat keine Standardwerte für Funktionsargumente, und es ist nicht offensichtlich, warum Generika anders sein sollten. Meiner Meinung nach hat die Möglichkeit, bestehenden Code mit einem Paket kompatibel zu machen, das Generika hinzufügt, keine Priorität. Wenn ein Paket umgeschrieben wird, um Generika zu verwenden, ist es in Ordnung, eine Änderung des vorhandenen Codes zu verlangen oder einfach den generischen Code unter Verwendung neuer Namen einzuführen.

@sighoya

Gibt es außerdem Pläne, parametrisierbare endliche Schnittstellengrenzen zu ermöglichen?

Tut mir leid, ich verstehe die Frage nicht.

Ich möchte die Leute daran erinnern, dass der Blogbeitrag (https://blog.golang.org/generics-next-step) vorschlägt, dass Diskussionen über Generika auf der golang-nuts-Mailingliste stattfinden, nicht auf dem Issue-Tracker. Ich werde diese Ausgabe weiter lesen, aber sie hat fast 800 Kommentare und ist völlig unhandlich, abgesehen von den anderen Schwierigkeiten des Issue-Trackers, wie z. B. dem Fehlen von Kommentar-Threading. Danke.

Feedback: Ich habe mir den neusten Go Time Podcast angehört, und ich muss sagen, dass ich die Erklärung von @griesemer zum Problem mit spitzen Klammern zum ersten Mal wirklich verstanden habe , dh was bedeutet „unbegrenzte Vorausschau auf den Parser“ eigentlich für gehen? Vielen Dank für die zusätzlichen Details dort.

Außerdem bin ich für eckige Klammern. 😄

@ianlancetaylor

Der Blogbeitrag schlägt vor, dass Diskussionen über Generika auf der Golang-Nuts-Mailingliste stattfinden, nicht auf dem Issue-Tracker

In einem kürzlich erschienenen Blogbeitrag [1] weist @ddevault darauf hin, dass Google Group (wo sich diese Mailingliste befindet) ein Google-Konto benötigt. Sie benötigen einen zum Posten, und anscheinend benötigen einige Gruppen sogar einen Account zum Lesen. Ich habe ein Google-Konto, also ist das kein Problem für mich (und ich sage auch nicht, dass ich mit allem in diesem Blogbeitrag einverstanden bin), aber ich stimme zu, dass, wenn wir eine gerechtere Golang-Community haben wollen, und Wenn wir eine Echokammer vermeiden wollen, wäre es vielleicht besser, diese Art von Anforderung nicht zu haben.

Ich wusste das nicht über Google-Gruppen, und wenn es eine Ausnahme für Golang-Nüsse gibt, dann akzeptieren Sie bitte meine Entschuldigung und ignorieren Sie dies. Für das, was es wert ist, habe ich durch das Lesen dieses Threads viel gelernt, und ich war auch ziemlich überzeugt (nachdem ich Golang weit über sechs Jahre verwendet habe), dass Generika der falsche Ansatz für die Sprache sind. Nur meine persönliche Meinung, und danke, dass Sie uns die Sprache gebracht haben, die mir so wie sie ist ziemlich viel Spaß macht!

Beifall!

[1] https://drewdevault.com/2020/08/01/pkg-go-dev-sucks.html

@purpleidea Jede Google-Gruppe kann als Mailingliste verwendet werden. Sie können teilnehmen und teilnehmen, ohne ein Google-Konto zu haben.

@ianlancetaylor

Jede Google-Gruppe kann als Mailingliste verwendet werden. Sie können teilnehmen und teilnehmen, ohne ein Google-Konto zu haben.

Wenn ich gehe:

https://groups.google.com/forum/#!forum/golang -nuts

in einem privaten Browserfenster (um mein Google-Konto, in dem ich angemeldet bin, auszublenden) und auf "Neues Thema" klicke, wird es mich zu einer Google-Anmeldeseite umleiten. Wie verwende ich es ohne ein Google-Konto?

@purpleidea Indem Sie eine E-Mail an [email protected] schreiben. Es ist eine Mailingliste. Lediglich das Webinterface benötigt ein Google-Konto. Was fair erscheint - da es sich um eine Mailingliste handelt, benötigen Sie eine E-Mail-Adresse und Gruppen können offensichtlich nur E-Mails von einem Google Mail-Konto senden.

Ich glaube, die meisten Leute verstehen nicht, was eine Mailingliste ist.

Auf jeden Fall können Sie auch jeden öffentlichen Mailinglistenspiegel verwenden, zum Beispiel https://www.mail-archive.com/[email protected]/

Das ist alles großartig, macht es aber nicht einfacher, wenn Leute darauf verlinken
Threads in Google Groups (was häufig vorkommt). Es ist unglaublich
irritierend zu versuchen, eine Nachricht von der ID in einer URL zu finden.

-Sam

Am So, 2. August 2020 um 19:24 schrieb Ahmed W.:
>
>

Ich glaube, die meisten Leute verstehen nicht, was eine Mailingliste ist.

Auf jeden Fall können Sie zum Beispiel auch jeden öffentlichen Mailinglisten-Mirror verwenden
https://www.mail-archive.com/[email protected]/

— Sie erhalten dies, weil Sie diesen Thread abonniert haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/golang/go/issues/15292#issuecomment-667738419 oder
Abmelden
https://github.com/notifications/unsubscribe-auth/AAD5EPNQTEUF5SPT6GMM4JLR6XYUBANCNFSM4CA35RXQ
.

--
Sam Whited

Hier ist nicht wirklich der Ort für diese Diskussion.

Irgendwelche Updates dazu? 🤔

@Imperatorn gab es, sie wurden hier nur nicht besprochen. Es wurde entschieden, dass eckige Klammern [ ] die gewählte Syntax sein würden und das Wort "Typ" beim Schreiben generischer Typen/Funktionen nicht erforderlich sein würde. Es gibt auch einen neuen Alias ​​"any" für die leere Schnittstelle.

Der neueste Designentwurf für Generika ist hier .
Siehe auch diesen Kommentar zu : Diskussionen zu diesem Thema. Danke.

Ich möchte die Leute daran erinnern, dass der Blogbeitrag (https://blog.golang.org/generics-next-step) vorschlägt, dass Diskussionen über Generika auf der golang-nuts-Mailingliste stattfinden, nicht auf dem Issue-Tracker. Ich werde diese Ausgabe weiter lesen, aber sie hat fast 800 Kommentare und ist völlig unhandlich, abgesehen von den anderen Schwierigkeiten des Issue-Trackers, wie z. B. dem Fehlen von Kommentar-Threading. Danke.

Obwohl ich respektiere, dass das Go-Team solche Diskussionen aus praktischen Gründen aus einem Problem entfernen möchte, scheint es, als gäbe es viele Community-Mitglieder auf GitHub, die nicht auf Golang-Nüssen stehen. Ich frage mich, ob die neue Diskussionsfunktion von GitHub gut passen würde? 🤔 Es hat anscheinend Gewinde.

@toolbox Das Argument kann auch in die andere Richtung geführt werden - es gibt Leute, die kein Github-Konto haben (und sich weigern, eines zu bekommen). Sie müssen auch nicht golang-nuts abonniert sein, um dort posten und teilnehmen zu können.

@Merovius Eine der Funktionen, die ich an GitHub-Problemen wirklich mag, ist, dass ich Benachrichtigungen nur für die Probleme abonnieren kann, an denen ich interessiert bin. Ich bin mir nicht sicher, wie ich das mit Google Groups machen soll?

Ich bin sicher, es gibt gute Gründe, das eine oder andere zu bevorzugen. Es kann sicherlich eine Diskussion darüber geben, was das bevorzugte Forum sein sollte. Aber noch einmal, ich denke nicht, dass diese Diskussion hier geführt werden sollte. Dieses Problem ist so schon laut genug.

@toolbox Das Argument kann auch in die andere Richtung geführt werden - es gibt Leute, die kein Github-Konto haben (und sich weigern, eines zu bekommen). Sie müssen auch nicht bei golang-nuts abonniert sein, um dort posten und teilnehmen zu können.

Ich verstehe, was du sagst, und es ist wahr, aber du verfehlst das Ziel. Ich sage nicht, dass Golang-Nuts-Benutzer aufgefordert werden sollten, zu GitHub zu gehen (wie es jetzt umgekehrt geschieht), ich sage, es wäre schön für die GitHub-Benutzer, ein Diskussionsforum zu haben.

Ich bin sicher, es gibt gute Gründe, das eine oder andere zu bevorzugen. Es kann sicherlich eine Diskussion darüber geben, was das bevorzugte Forum sein sollte. Aber noch einmal, ich denke nicht, dass diese Diskussion hier geführt werden sollte. Dieses Problem ist so schon laut genug.

Ich stimme zu, dass dies für dieses Thema völlig off-topic ist, und ich entschuldige mich dafür, dass ich es angesprochen habe, aber ich hoffe, Sie sehen die Ironie.

@keean @Merovius @tooolbox und Leute in der Zukunft.

FYI: Es gibt ein offenes Thema für diese Art von Diskussion, siehe #37469.

Hallo,

Erstmal vielen Dank für Go. Die Sprache ist absolut genial. Eines der erstaunlichsten Dinge an Go war für mich die Lesbarkeit. Ich bin neu in der Sprache, also bin ich noch in den frühen Stadien der Entdeckung, aber bisher ist sie unglaublich klar, klar und auf den Punkt rübergekommen.

Das einzige Feedback, das ich gerne mitteilen möchte, ist, dass ich [T Constraint] nach meinem anfänglichen Scannen des Generika-Vorschlags nicht schnell analysieren kann, zumindest nicht so einfach wie ein Zeichensatz, der für Generika vorgesehen ist . Ich verstehe, dass der C++-Stil F<T Constraint> aufgrund der Art des Multi-Return-Paradigmas von go nicht machbar ist. Alle Nicht-ASCII-Zeichen wären eine absolute Pflicht, also bin ich wirklich dankbar, dass Sie diese Idee verworfen haben.

Bitte erwägen Sie die Verwendung einer Zeichenkombination. Ich bin mir nicht sicher, ob bitweise Operationen missverstanden oder das Parsing-Gewässer verschmutzt werden könnten, aber meiner Meinung nach wäre F<<T Constraint>> nett. Jede Symbolkombination würde jedoch ausreichen. Es kann zwar eine anfängliche Eye-Scanning-Steuer verursachen, aber ich denke, dies kann leicht mit Schriftligaturen wie FireCoda und Iosevka behoben werden. Es gibt nicht viel, was getan werden kann, um den Unterschied zwischen Map[T Constraint] und map[string]T klar und einfach zu unterscheiden.

Ich habe keinen Zweifel, dass die Leute ihren Verstand trainieren werden, um zwischen den beiden Anwendungen von [] basierend auf dem Kontext zu unterscheiden. Ich vermute nur, dass es die Lernkurve steiler machen wird.

Danke für den Hinweis. Um das Offensichtliche nicht zu übersehen, aber map[T1]T2 und Map[T1 Constraint] können unterschieden werden, da ersteres keine Einschränkung hat und letzteres eine erforderliche Einschränkung hat.

Die Syntax wurde ausführlich auf golang-nuts diskutiert und ich denke, sie ist geklärt. Wir freuen uns über Kommentare, die auf tatsächlichen Daten basieren, z. B. zum Analysieren von Mehrdeutigkeiten. Bei Kommentaren, die auf Gefühlen und Vorlieben basieren, ist es meiner Meinung nach an der Zeit, anderer Meinung zu sein und sich zu verpflichten.

Danke noch einmal.

@ianlancetaylor Fair genug. Ich bin sicher, Sie haben es satt, Nitpicks darauf zu hören :) Für das, was es wert ist, meinte ich, dass man beim Scannen leicht unterscheiden kann.

Unabhängig davon freue ich mich darauf, es zu verwenden. Danke schön.

Eine generische Alternative zu reflect.MakeFunc wäre ein enormer Leistungsgewinn für die Go-Instrumentierung. Aber ich sehe keine Möglichkeit, einen Funktionstyp mit dem aktuellen Vorschlag zu zerlegen.

@Julio-Guerra Ich bin mir nicht sicher, was Sie mit "Funktionstyp zerlegen" meinen. Sie können bis zu einem gewissen Grad über Argument- und Rückgabetypen parametrisieren: https://go2goplay.golang.org/p/RwU11S4gC59

package main

import (
    "fmt"
)

func Call[In, Out any](f func(In) Out, v In) Out {
    return f(v)
}

func main() {
    triple := func(i int) int {
        return 3 * i
    }
    fmt.Println(Call(triple, 23))
}

Das funktioniert aber nur, wenn die Anzahl beider konstant ist.

@Julio-Guerra Ich bin mir nicht sicher, was Sie mit "Funktionstyp zerlegen" meinen. Sie können bis zu einem gewissen Grad über Argument- und Rückgabetypen parametrisieren: https://go2goplay.golang.org/p/RwU11S4gC59

In der Tat beziehe ich mich auf das, was Sie getan haben, aber verallgemeinert auf alle Funktionsparameter und Rückgabetypen (ähnlich wie das Array von Parametern und Rückgabetypen von reflect.MakeFunc). Dies würde es ermöglichen, verallgemeinerte Funktionswrapper zu haben (anstelle der Verwendung von Tool-Code-Generierung).

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen