Go: proposition : spec : installations de programmation génériques

CrĂ©Ă© le 14 avr. 2016  Â·  816Commentaires  Â·  Source: golang/go

Ce numéro propose que Go prenne en charge une certaine forme de programmation générique.
Il a le label Go2, puisque pour Go1.x le langage est plus ou moins fini.

Ce numéro est accompagné d'une proposition générale de génériques par @ianlancetaylor qui comprend quatre propositions spécifiques erronées de mécanismes de programmation génériques pour Go.

L'intention n'est pas d'ajouter des génériques à Go pour le moment, mais plutÎt de montrer aux gens à quoi ressemblerait une proposition complÚte. Nous espérons que cela sera utile à quiconque proposera des changements de langage similaires à l'avenir.

Go2 LanguageChange NeedsInvestigation Proposal generics

Commentaire le plus utile

Permettez-moi de rappeler prĂ©ventivement Ă  tout le monde notre politique https://golang.org/wiki/NoMeToo . La fĂȘte des emoji est au-dessus.

Tous les 816 commentaires

CL https://golang.org/cl/22057 mentionne ce problĂšme.

Permettez-moi de rappeler prĂ©ventivement Ă  tout le monde notre politique https://golang.org/wiki/NoMeToo . La fĂȘte des emoji est au-dessus.

Il y a Summary of Go Generics Discussions , qui tente de fournir un aperçu des discussions de diffĂ©rents endroits. Il fournit Ă©galement quelques exemples de rĂ©solution de problĂšmes, oĂč vous voudriez utiliser des gĂ©nĂ©riques.

Il y a deux "exigences" dans la proposition liĂ©e qui peuvent compliquer la mise en Ɠuvre et rĂ©duire la sĂ©curitĂ© de type :

  • DĂ©finissez des types gĂ©nĂ©riques basĂ©s sur des types qui ne sont pas connus tant qu'ils ne sont pas instanciĂ©s.
  • N'exige pas une relation explicite entre la dĂ©finition d'un type ou d'une fonction gĂ©nĂ©rique et son utilisation. Autrement dit, les programmes ne devraient pas avoir Ă  dire explicitement que le type T implĂ©mente le G gĂ©nĂ©rique.

Ces exigences semblent exclure par exemple un systĂšme similaire au systĂšme de traits de Rust, oĂč les types gĂ©nĂ©riques sont contraints par des limites de traits. Pourquoi sont-ils nĂ©cessaires ?

Il devient tentant de construire des génériques dans la bibliothÚque standard à un niveau trÚs bas, comme en C++ std::basic_string, std::allocateur>. Cela a ses avantages - sinon personne ne le ferait - mais cela a des effets étendus et parfois surprenants, comme dans des messages d'erreur C++ incompréhensibles.

Le problĂšme en C++ provient de la vĂ©rification de type du code gĂ©nĂ©rĂ©. Il doit y avoir une vĂ©rification de type supplĂ©mentaire avant la gĂ©nĂ©ration du code. La proposition de concepts C++ permet cela en permettant Ă  l'auteur du code gĂ©nĂ©rique de spĂ©cifier les exigences d'un type gĂ©nĂ©rique. De cette façon, la compilation peut Ă©chouer Ă  la vĂ©rification du type avant la gĂ©nĂ©ration du code et des messages d'erreur simples peuvent ĂȘtre imprimĂ©s. Le problĂšme avec les gĂ©nĂ©riques C++ (sans concepts) est que le code gĂ©nĂ©rique _est_ la spĂ©cification du type gĂ©nĂ©rique. C'est ce qui crĂ©e les messages d'erreur incomprĂ©hensibles.

Le code gĂ©nĂ©rique ne doit pas ĂȘtre la spĂ©cification d'un type gĂ©nĂ©rique.

@tamird C'est une caractĂ©ristique essentielle des types d'interface de Go que vous pouvez dĂ©finir un type non d'interface T et dĂ©finir ultĂ©rieurement un type d'interface I tel que T implĂ©mente I. Voir https://golang.org/doc/faq#implements_interface . Ce serait incohĂ©rent si Go implĂ©mentait une forme de gĂ©nĂ©riques pour lesquels un type gĂ©nĂ©rique G ne pouvait ĂȘtre utilisĂ© qu'avec un type T qui disait explicitement "Je peux ĂȘtre utilisĂ© pour implĂ©menter G".

Je ne connais pas Rust, mais je ne connais aucun langage qui oblige T Ă  dĂ©clarer explicitement qu'il peut ĂȘtre utilisĂ© pour implĂ©menter G. Les deux exigences que vous mentionnez ne signifient pas que G ne peut pas imposer d'exigences Ă  T, juste car I impose des exigences Ă  T. Les exigences signifient simplement que G et T peuvent ĂȘtre Ă©crits indĂ©pendamment. C'est une fonctionnalitĂ© hautement souhaitable pour les gĂ©nĂ©riques, et je ne peux pas imaginer l'abandonner.

@ianlancetaylor https://doc.rust-lang.org/book/traits.html explique les traits de Rust. Bien que je pense qu'ils soient un bon modÚle en général, ils conviendraient mal au Go tel qu'il existe aujourd'hui.

@sbunce J'ai aussi pensĂ© que les concepts Ă©taient la rĂ©ponse, et vous pouvez voir l'idĂ©e dispersĂ©e Ă  travers les diffĂ©rentes propositions avant la derniĂšre. Mais il est dĂ©courageant que des concepts aient Ă©tĂ© initialement prĂ©vus pour ce qui est devenu C++11, et nous sommes maintenant en 2016, et ils sont toujours controversĂ©s et pas particuliĂšrement proches d'ĂȘtre inclus dans le langage C++.

La littérature universitaire aurait-elle de la valeur pour des conseils sur l'évaluation des approches ?

Le seul article que j'ai lu sur le sujet est Les développeurs bénéficient-ils des types génériques ? (paywall désolé, vous pourriez chercher sur Google votre chemin vers un téléchargement pdf) qui avait ce qui suit à dire

Par conséquent, une interprétation conservatrice de l'expérience
est que les types gĂ©nĂ©riques peuvent ĂȘtre considĂ©rĂ©s comme un compromis
entre les caractéristiques positives de la documentation et
caractéristiques d'extensibilité négatives. La partie excitante de
l'Ă©tude est qu'elle a montrĂ© une situation oĂč l'utilisation d'un
systÚme de type statique (plus fort) a eu un impact négatif sur la
temps de dĂ©veloppement alors que dans le mĂȘme temps le bĂ©nĂ©fice attendu
fit - la réduction du temps de correction des erreurs de type - n'apparaissait pas.
Nous pensons que de telles tùches pourraient aider dans de futures expériences dans
identifier l'impact des systĂšmes de type.

Je vois également que https://github.com/golang/go/issues/15295 fait également référence à des génériques légers et flexibles orientés objet .

Si nous devions nous appuyer sur le milieu universitaire pour guider la décision, je pense qu'il serait préférable de faire une revue de la littérature en amont, et probablement de décider tÎt si nous peserions les études empiriques différemment de celles reposant sur des preuves.

Veuillez consulter : http://dl.acm.org/citation.cfm?id=2738008 par Barbara Liskov :

La prise en charge de la programmation gĂ©nĂ©rique dans les langages de programmation modernes orientĂ©s objet est maladroite et manque de puissance expressive souhaitable. Nous introduisons un mĂ©canisme de gĂ©nĂ©ricitĂ© expressive qui ajoute de la puissance expressive et renforce la vĂ©rification statique, tout en restant lĂ©ger et simple dans les cas d'utilisation courants. Comme les classes de types et les concepts, le mĂ©canisme permet aux types existants de modĂ©liser rĂ©troactivement les contraintes de type. Pour le pouvoir expressif, nous exposons les modĂšles en tant que constructions nommĂ©es qui peuvent ĂȘtre dĂ©finies et sĂ©lectionnĂ©es explicitement pour tĂ©moigner des contraintes ; dans les utilisations courantes de la gĂ©nĂ©ricitĂ©, cependant, les types sont implicitement tĂ©moins de contraintes sans effort supplĂ©mentaire du programmeur.

Je pense que ce qu'ils ont fait lĂ -bas est plutĂŽt cool - je suis dĂ©solĂ© si ce n'est pas le bon endroit pour s'arrĂȘter mais je n'ai pas trouvĂ© d'endroit pour commenter dans /proposals et je n'ai pas trouvĂ© de problĂšme appropriĂ© ici.

Il pourrait ĂȘtre intĂ©ressant d'avoir un ou plusieurs transpileurs expĂ©rimentaux - un code source gĂ©nĂ©rique Go vers un compilateur de code source Go 1.xy.
Je veux dire - trop de discussions/d'arguments pour mon opinion, et personne n'écrit de code source qui _essaye_ d'implémenter _une sorte_ de génériques pour Go.

Juste pour acquérir des connaissances et de l'expérience avec Go et les génériques - pour voir ce qui fonctionne et ce qui ne fonctionne pas.
Si toutes les solutions génériques Go ne sont pas vraiment bonnes, alors ; Pas de génériques pour Go.

La proposition peut-elle également inclure les implications sur la taille binaire et l'empreinte mémoire ? Je m'attendrais à ce qu'il y ait une duplication de code pour chaque type de valeur concrÚte afin que les optimisations du compilateur fonctionnent dessus. J'espÚre avoir la garantie qu'il n'y aura pas de duplication de code pour les types de pointeurs concrets.

Je propose une matrice de dĂ©cision de Pugh. Mes critĂšres incluent les impacts de visibilitĂ© (complexitĂ© de la source, taille). J'ai Ă©galement forcĂ© le classement des critĂšres pour dĂ©terminer les poids des critĂšres. Le vĂŽtre peut bien sĂ»r varier. J'ai utilisĂ© "interfaces" comme alternative par dĂ©faut et j'ai comparĂ© cela aux gĂ©nĂ©riques "copier/coller", aux gĂ©nĂ©riques basĂ©s sur des modĂšles (j'avais en tĂȘte quelque chose comme le fonctionnement du langage D) et quelque chose que j'ai appelĂ© les gĂ©nĂ©riques de style d'instanciation d'exĂ©cution. Je suis sĂ»r que c'est une vaste simplification. NĂ©anmoins, cela peut susciter quelques idĂ©es sur la façon d'Ă©valuer les choix... cela devrait ĂȘtre un lien public vers ma feuille de calcul Google, ici

Pinging @yizhouzhang et @andrewcmyers afin qu'ils puissent exprimer leurs opinions sur les genres comme les gĂ©nĂ©riques dans Go. Il semble que cela pourrait ĂȘtre un bon match :)

La conception des gĂ©nĂ©riques que nous avons proposĂ©e pour Genus a une vĂ©rification de type statique et modulaire, ne nĂ©cessite pas de prĂ©-dĂ©clarer que les types implĂ©mentent une interface et offre des performances raisonnables. Je l'examinerais certainement si vous songez Ă  des gĂ©nĂ©riques pour Go. Cela semble ĂȘtre un bon ajustement d'aprĂšs ma comprĂ©hension de Go.

Voici un lien vers l'article qui ne nécessite pas l'accÚs à la bibliothÚque numérique ACM :
http://www.cs.cornell.edu/andru/papers/genus/

La page d'accueil de Genus est ici : http://www.cs.cornell.edu/projects/genus/

Nous n'avons pas encore rendu public le compilateur, mais nous prévoyons de le faire assez bientÎt.

Heureux de répondre à toutes les questions des gens.

En termes de matrice de décision de @mandolyte , Genus obtient un 17, à égalité au premier rang. J'ajouterais cependant quelques critÚres supplémentaires pour marquer. Par exemple, la vérification de type modulaire est importante, comme d'autres comme @sbunce observé ci-dessus, mais les schémas basés sur des modÚles en manquent. Le rapport technique de l'article Genus contient un tableau beaucoup plus volumineux à la page 34, comparant diverses conceptions génériques.

Je viens de parcourir tout le document Summary of Go Generics , qui Ă©tait un rĂ©sumĂ© utile des discussions prĂ©cĂ©dentes. Le mĂ©canisme des gĂ©nĂ©riques dans Genus ne souffre pas, Ă  mon avis, des problĂšmes identifiĂ©s pour C++, Java ou C#. Les gĂ©nĂ©riques de genre sont rĂ©ifiĂ©s, contrairement Ă  Java, vous pouvez donc dĂ©couvrir les types au moment de l'exĂ©cution. Vous pouvez Ă©galement instancier sur des types primitifs, et vous n'obtenez pas de boxe implicite aux endroits oĂč vous ne le souhaitez vraiment pas : des tableaux de T oĂč T est une primitive. Le systĂšme de type est le plus proche de Haskell et Rust - en fait un peu plus puissant, mais je pense aussi intuitif. La spĂ©cialisation primitive comme C# n'est actuellement pas prise en charge dans Genus, mais elle pourrait l'ĂȘtre. Dans la plupart des cas, la spĂ©cialisation peut ĂȘtre dĂ©terminĂ©e au moment de la liaison, de sorte qu'une vĂ©ritable gĂ©nĂ©ration de code d'exĂ©cution ne serait pas nĂ©cessaire.

CL https://golang.org/cl/22163 mentionne ce problĂšme.

Un moyen de contraindre les types génériques qui ne nécessite pas l'ajout de nouveaux concepts de langage : https://docs.google.com/document/d/1rX4huWffJ0y1ZjqEpPrDy-kk-m9zWfatgCluGRBQveQ/edit?usp=sharing.

Genus a l'air vraiment cool et c'est clairement une avancée importante dans l'art, mais je ne vois pas comment cela s'appliquerait au Go. Quelqu'un a-t-il une esquisse de la façon dont il s'intégrerait au systÚme/à la philosophie de type Go ?

Le problĂšme est que l'Ă©quipe de go fait des tentatives d'obstruction. Le titre Ă©nonce clairement les intentions de l'Ă©quipe de go. Et si cela ne suffisait pas Ă  dissuader tous les preneurs, les caractĂ©ristiques exigĂ©es d'un domaine aussi vaste dans les propositions d'ian indiquent clairement que si vous voulez des gĂ©nĂ©riques, ils ne vous veulent pas. Il est stupide de tenter mĂȘme de dialoguer avec l'Ă©quipe de go. A ceux qui recherchent des gĂ©nĂ©riques en go, je dis fracturer le langage. Commencez un nouveau voyage - beaucoup suivront. J'ai dĂ©jĂ  vu du bon travail dans les forks. Organisez-vous, rassemblez-vous autour d'une cause

Si quelqu'un veut essayer de créer une extension générique pour Go basée sur la conception Genus, nous sommes heureux de vous aider. Nous ne connaissons pas assez Go pour produire un design qui s'harmonise avec le langage existant. Je pense que la premiÚre étape serait une proposition de conception d'homme de paille avec des exemples élaborés.

@andrewcmyers en espérant que @ianlancetaylor travaillera avec vous là-dessus. Le simple fait d'avoir quelques exemples à regarder aiderait beaucoup.

J'ai lu l'article Genus. Dans la mesure oĂč je le comprends, cela semble bien pour Java, mais ne semble pas ĂȘtre un choix naturel pour Go.

Un aspect clĂ© de Go est que lorsque vous Ă©crivez un programme Go, la plupart de ce que vous Ă©crivez est du code. Ceci est diffĂ©rent de C++ et Java, oĂč une grande partie de ce que vous Ă©crivez est constituĂ©e de types. Le genre semble concerner principalement les types : vous Ă©crivez des contraintes et des modĂšles, plutĂŽt que du code. Le systĂšme de type de Go est trĂšs trĂšs simple. Le systĂšme de types de Genus est beaucoup plus complexe.

Les idĂ©es de modĂ©lisation rĂ©troactive, bien que clairement utiles pour Java, ne semblent pas du tout convenir Ă  Go. Les utilisateurs utilisent dĂ©jĂ  des types d'adaptateurs pour faire correspondre les types existants aux interfaces ; rien de plus ne devrait ĂȘtre nĂ©cessaire lors de l'utilisation de gĂ©nĂ©riques.

Il serait intéressant de voir ces idées appliquées au Go, mais je ne suis pas optimiste quant au résultat.

Je ne suis pas un expert en Go, mais son systĂšme de type ne semble pas plus simple que Java prĂ©-gĂ©nĂ©rique. La syntaxe de type est un peu plus lĂ©gĂšre mais la complexitĂ© sous-jacente semble Ă  peu prĂšs la mĂȘme.

Dans Genus, les contraintes sont des types mais les modÚles sont du code. Les modÚles sont des adaptateurs, mais ils s'adaptent sans ajouter une couche d'emballage réel. Ceci est trÚs utile lorsque vous souhaitez, par exemple, adapter un tableau entier d'objets à une nouvelle interface. La modélisation rétroactive vous permet de traiter le tableau comme un tableau d'objets satisfaisant l'interface souhaitée.

Je ne serais pas surpris s'il Ă©tait plus compliquĂ© que Java (prĂ©-gĂ©nĂ©rique) dans un sens thĂ©orique de type, mĂȘme s'il est plus simple Ă  utiliser dans la pratique.

Mis Ă  part la complexitĂ© relative, ils sont suffisamment diffĂ©rents pour que Genus ne puisse pas cartographier 1: 1. Aucun sous-typage ne semble ĂȘtre un gros.

Si vous ĂȘtes intĂ©ressĂ©:

Le résumé le plus bref des différences philosophiques / de conception pertinentes que j'ai mentionnées est contenu dans les entrées suivantes de la FAQ :

Contrairement à la plupart des langages, la spécification Go est trÚs courte et claire sur les propriétés pertinentes du systÚme de type à partir de https://golang.org/ref/spec#Constants et continue jusqu'à la section intitulée "Blocs" (qui compte moins de 11 pages imprimées).

Contrairement aux génériques Java et C#, le mécanisme des génériques Genus n'est pas basé sur le sous-typage. Par contre, il me semble que Go a bien du sous-typage, mais du sous-typage structurel. C'est également un bon match pour l'approche Genus, qui a une saveur structurelle plutÎt que de s'appuyer sur des relations prédéclarées.

Je ne crois pas que Go ait un sous-typage structurel.

Alors que deux types dont le type sous-jacent est identique sont donc identiques
peuvent ĂȘtre substituĂ©s l'un Ă  l'autre, https://play.golang.org/p/cT15aQ-PFr

Cela ne s'Ă©tend pas Ă  deux types qui partagent un sous-ensemble commun de champs,
https://play.golang.org/p/KrC9_BDXuh.

Le jeudi 28 avril 2016 Ă  13h09, Andrew Myers [email protected]
a Ă©crit:

Contrairement aux génériques Java et C#, le mécanisme des génériques Genus ne repose pas sur
sous-typage. D'un autre cÎté, il me semble que Go a un sous-typage,
mais sous-typage structurel. C'est aussi un bon match pour l'approche genre,
qui a une saveur structurelle plutÎt que de s'appuyer sur des prédéclarés
des relations.

—
Vous recevez ceci parce que vous ĂȘtes abonnĂ© Ă  ce fil.
RĂ©pondez directement Ă  cet e-mail ou consultez-le sur GitHub
https://github.com/golang/go/issues/15292#issuecomment -215298127

Merci, j'interprĂ©tais mal une partie du langage sur le moment oĂč les types implĂ©mentent des interfaces. En fait, il me semble que les interfaces Go, avec une extension modeste, pourraient ĂȘtre utilisĂ©es comme contraintes de style Genus.

C'est exactement pourquoi je vous ai envoyĂ© un ping, le genre semble ĂȘtre une bien meilleure approche que Java/C# comme les gĂ©nĂ©riques.

Il y avait quelques idées concernant la spécialisation sur les types d'interfaces ; par exemple, les "propositions" de l'approche _package templates_ 1 2 en sont des exemples.

tl;dr; le package générique avec spécialisation d'interface ressemblerait à :

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

Version 1. avec spécialisation étendue au package :

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. la spécialisation portée par la déclaration :

package main
import set

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

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

Les gĂ©nĂ©riques Ă  portĂ©e de paquet empĂȘcheront les gens d'abuser de maniĂšre significative du systĂšme de gĂ©nĂ©riques, puisque l'utilisation est limitĂ©e aux algorithmes de base et aux structures de donnĂ©es. Cela empĂȘche essentiellement la crĂ©ation de nouvelles abstractions de langage et de code fonctionnel.

La spécialisation dans la portée de la déclaration a plus de possibilités au prix, ce qui la rend plus sujette aux abus et elle est plus détaillée. Mais, un code fonctionnel serait possible, par exemple :

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

L'approche de spécialisation d'interface a des propriétés intéressantes :

  • Les packages dĂ©jĂ  existants utilisant des interfaces seraient spĂ©cialisables. par exemple, je pourrais appeler sort.Sort[[Interface:MyItems]](...) et faire en sorte que le tri fonctionne sur le type concret au lieu de l'interface (avec des gains potentiels de l'inlining).
  • Les tests sont simplifiĂ©s, je n'ai qu'Ă  m'assurer que le code gĂ©nĂ©rique fonctionne avec les interfaces.
  • Il est facile de dire comment cela fonctionne. c'est-Ă -dire imaginez que [[E: int]] remplace toutes les dĂ©clarations de E par int .

Cependant, il existe des problÚmes de verbosité lorsque vous travaillez sur plusieurs packages :

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

_Bien sĂ»r, le tout est plus simple Ă  Ă©noncer qu'Ă  mettre en Ɠuvre. En interne, il y a probablement des tonnes de problĂšmes et de façons dont cela pourrait fonctionner._

_PS, aux grincheux sur la lenteur des progrÚs des génériques, j'applaudis l'équipe Go pour avoir passé plus de temps sur des problÚmes qui ont un plus grand avantage pour la communauté, par exemple les bogues du compilateur/d'exécution, SSA, GC, http2._

@egonelbre votre argument selon lequel les gĂ©nĂ©riques au niveau du package empĂȘcheront les "abus" est trĂšs important et je pense que la plupart des gens l'ignorent. Cela, ajoutĂ© Ă  leur relative simplicitĂ© sĂ©mantique et syntaxique (seules les constructions package et import sont affectĂ©es), les rend trĂšs attractifs pour Go.

@andrewcymyers intéressant que vous pensiez que les interfaces Go fonctionnent comme des contraintes de style Genus. J'aurais pensé qu'ils ont toujours le problÚme que vous ne pouvez pas exprimer de contraintes multi-types avec eux.

Une chose que je viens de réaliser, cependant, c'est que dans Go, vous pouvez écrire une interface en ligne. Ainsi, avec la bonne syntaxe, vous pouvez placer l'interface dans la portée de tous les paramÚtres et capturer les contraintes multi-paramÚtres :

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

Je pense que le plus gros problÚme avec les interfaces en tant que contraintes est que les méthodes ne sont pas aussi répandues en Go qu'en Java. Les types intégrés n'ont pas de méthodes. Il n'y a pas d'ensemble de méthodes universelles comme celles de java.lang.Object. Les utilisateurs ne définissent généralement pas de méthodes telles que Equals ou HashCode sur leurs types à moins qu'ils n'en aient spécifiquement besoin, car ces méthodes ne qualifient pas un type à utiliser comme clés de carte ou dans tout algorithme nécessitant une égalité.

(L'égalité dans Go est une histoire intéressante. Le langage donne votre type "==" s'il répond à certaines exigences (voir https://golang.org/ref/spec#Logical_operators, recherchez "comparable"). Tout type avec " ==" peut servir de clé de carte. Mais si votre type ne mérite pas "==", alors il n'y a rien que vous puissiez écrire qui le fera fonctionner comme clé de carte.)

Étant donnĂ© que les mĂ©thodes ne sont pas omniprĂ©sentes et qu'il n'existe aucun moyen simple d'exprimer les propriĂ©tĂ©s des types intĂ©grĂ©s (comme les opĂ©rateurs avec lesquels ils fonctionnent), j'ai suggĂ©rĂ© d'utiliser le code lui-mĂȘme comme mĂ©canisme de contrainte gĂ©nĂ©rique. Voir le lien dans mon commentaire du 18 avril, ci-dessus. Cette proposition a ses problĂšmes, mais une fonctionnalitĂ© intĂ©ressante est que le code numĂ©rique gĂ©nĂ©rique pourrait toujours utiliser les opĂ©rateurs habituels, au lieu d'appels de mĂ©thode encombrants.

L'autre façon de faire est d'ajouter des méthodes aux types qui en manquent. Vous pouvez le faire dans le langage existant de maniÚre beaucoup plus légÚre qu'en Java :

taper Entier entier
func (i Int) Less(j Int) bool { return i < j }

Le type Int « hĂ©rite » de tous les opĂ©rateurs et autres propriĂ©tĂ©s de int. Bien que vous deviez lancer entre les deux pour utiliser Int et int ensemble, ce qui peut ĂȘtre pĂ©nible.

Les modÚles de genre pourraient aider ici. Mais il faudrait les garder trÚs simples. Je pense que @ianlancetaylor était trop étroit dans sa caractérisation de Go comme écrivant plus de code, moins de types. Le principe général est que Go a horreur de la complexité. Nous regardons Java et C++ et sommes déterminés à ne jamais y aller. (Sans vouloir vous offenser.)

Donc, une idée rapide pour une fonctionnalité de type modÚle serait : demandez à l'utilisateur d'écrire des types comme Int ci-dessus, et dans les instanciations génériques, autorisez "int avec Int", ce qui signifie utiliser le type int mais le traiter comme Int. Ensuite, il n'y a pas de construction de langage ouverte appelée modÚle, avec son mot-clé, sa sémantique d'héritage, etc. Je ne comprends pas assez bien les modÚles pour savoir si c'est faisable, mais c'est plus dans l'esprit du Go.

@jba Nous sommes certainement d'accord avec le principe d'éviter la complexité. "Aussi simple que possible mais pas plus simple." Je laisserais probablement certaines fonctionnalités Genus hors de Go pour ces raisons, du moins au début.

L'un des avantages de l'approche Genus est qu'elle gĂšre les types intĂ©grĂ©s de maniĂšre fluide. Rappelez-vous que les types primitifs en Java n'ont pas de mĂ©thodes, et Genus hĂ©rite de ce comportement. Au lieu de cela, Genus traite les types primitifs _comme s'ils avaient une suite assez large de mĂ©thodes dans le but de satisfaire les contraintes. Une table de hachage nĂ©cessite que ses clĂ©s puissent ĂȘtre hachĂ©es et comparĂ©es, mais tous les types primitifs satisfont Ă  cette contrainte. Ainsi, les instanciations de type comme Map[int, boolean] sont parfaitement lĂ©gales sans autre problĂšme. Il n'est pas nĂ©cessaire de faire la distinction entre deux saveurs d'entiers (int vs Int) pour y parvenir. Cependant, si int n'Ă©tait pas Ă©quipĂ© de suffisamment d'opĂ©rations pour certaines utilisations, nous utiliserions un modĂšle presque exactement comme l'utilisation de Int ci-dessus.

Une autre chose qui mĂ©rite d'ĂȘtre mentionnĂ©e est l'idĂ©e de "modĂšles naturels" dans Genus. Normalement, vous n'avez pas besoin de dĂ©clarer un modĂšle pour utiliser un type gĂ©nĂ©rique : si l'argument de type satisfait la contrainte, un modĂšle naturel est automatiquement gĂ©nĂ©rĂ©. Notre expĂ©rience est que c'est le cas habituel; dĂ©clarer des modĂšles nommĂ©s explicites n'est normalement pas nĂ©cessaire. Mais si un modĂšle Ă©tait nĂ©cessaire - par exemple, si vous vouliez hacher des entiers de maniĂšre non standard - alors la syntaxe est similaire Ă  ce que vous avez suggĂ©rĂ© : Map[int with fancyHash, boolean] . Je dirais que Genus est syntaxiquement lĂ©ger dans les cas d'utilisation normaux, mais avec de la puissance en rĂ©serve en cas de besoin.

@egonelbre Ce que vous proposez ici ressemble à des types virtuels, qui sont pris en charge par Scala. Il existe un article ECOOP'97 de Kresten Krab Thorup, "Genericity in Java with virtual types", qui explore cette direction. Nous avons également développé des mécanismes pour les types virtuels et les classes virtuelles dans notre travail (« J& : nested intersection for scalable software composition », OOPSLA'06).

Étant donnĂ© que les initialisations littĂ©rales sont omniprĂ©sentes dans Go, je devais me demander Ă  quoi ressemblerait une fonction littĂ©rale. Je soupçonne que le code pour gĂ©rer cela existe en grande partie dans Go gĂ©nĂ©rer, corriger et renommer. Peut-ĂȘtre que cela inspirera quelqu'un :-)

// la définition (générique) du type de fonction
type Somme64 fonction (X, Y) float64 {
retour float64(X) + float64(Y)
}

// en instancie un, positionnellement
je := 42
var j uint = 86
somme := &Somme64{i, j}

// en instancie un, par des types de paramÚtres nommés
somme := &Sum64{ X : int, Y : uint}

// maintenant l'utiliser...
result := sum(i, j) // le résultat est 128

La proposition d'Ian en demande trop. Nous ne pouvons pas dĂ©velopper toutes les fonctionnalitĂ©s en mĂȘme temps, il existera dans un Ă©tat inachevĂ© pendant plusieurs mois.

En attendant, le projet inachevĂ© ne peut pas ĂȘtre qualifiĂ© de langage Go officiel tant qu'il n'est pas terminĂ©, car cela risquerait de fragmenter l'Ă©cosystĂšme.

La question est donc de savoir comment planifier cela.

Une grande partie du projet consisterait également à développer le corpus de référence.
développer les collections génériques réelles, les algorithmes et d'autres choses de telle maniÚre que nous sommes tous d'accord sur le fait qu'ils sont idiomatiques, tout en utilisant les nouvelles fonctionnalités go 2.0

Une syntaxe possible ?

// 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 , la syntaxe n'est pas la partie la plus difficile de ce problÚme. En fait, c'est de loin le plus simple. Veuillez consulter la discussion et les documents liés ci-dessus.

@ md2perpe Nous avons discutĂ© du paramĂ©trage de packages entiers ("modules") comme moyen de gĂ©nĂ©ricitĂ© en interne - cela semble ĂȘtre un moyen de rĂ©duire la surcharge syntaxique. Mais il a d'autres problĂšmes; par exemple, il n'est pas clair comment le paramĂ©trer avec des types qui ne sont pas au niveau du package. Mais l'idĂ©e peut encore valoir la peine d'ĂȘtre explorĂ©e en dĂ©tail.

J'aimerais partager une perspective : dans un univers parallÚle, toutes les signatures de fonction Go ont toujours été contraintes de ne mentionner que les types d'interface, et au lieu de la demande de génériques aujourd'hui, il y en a un pour éviter l'indirection associée aux valeurs d'interface. Pensez à la façon dont vous résoudriez ce problÚme (sans changer la langue). J'ai quelques idées.

@thwd Ainsi, l'auteur de la bibliothÚque continuerait-il à utiliser des interfaces, mais sans le changement de type et les assertions de type nécessaires aujourd'hui. Et l'utilisateur de la bibliothÚque passerait-il simplement des types concrets comme si la bibliothÚque utilisait les types tels quels... et alors le compilateur réconcilierait-il les deux ? Et s'il ne pouvait pas dire pourquoi ? (comme l'opérateur modulo a été utilisé dans la bibliothÚque, mais l'utilisateur a fourni une tranche de quelque chose.

Suis-je proche ? :-)

@mandolyte oui ! Ă©changeons des mails pour ne pas polluer ce fil. Vous pouvez me joindre Ă  « me at thwd dot me ». Toute autre personne lisant ceci qui pourrait ĂȘtre intĂ©ressĂ©e; Envoyez-moi un e-mail et je vous ajouterai au fil de discussion.

C'est une excellente fonctionnalité pour type system et collection library .
Une syntaxe potentielle :

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

Pour interface

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

super type ou type implement :

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

Le ci-dessus aka en java:

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

@leaxoy comme dit précédemment, la syntaxe n'est pas la partie la plus difficile ici. Voir la discussion ci-dessus.

Sachez simplement que le coût de l'interface est incroyablement énorme.

Veuillez expliquer pourquoi pensez-vous que le coût de l'interface est "incroyablement"
grande.
Cela ne devrait pas ĂȘtre pire que les appels virtuels non spĂ©cialisĂ©s de C++.

@minux Je ne peux pas dire sur les coĂ»ts de performance mais par rapport Ă  la qualitĂ© du code. interface{} ne peut pas ĂȘtre vĂ©rifiĂ© au moment de la compilation, mais les gĂ©nĂ©riques le peuvent. À mon avis, c'est, dans la plupart des cas, plus important que les problĂšmes de performances liĂ©s Ă  l'utilisation de interface{} .

@xoviat

Il n'y a vraiment aucun inconvénient à cela car le traitement requis pour cela ne ralentit pas le compilateur.

Il y a (au moins) deux inconvénients.

L'un est un travail accru pour l'Ă©diteur de liens : si les spĂ©cialisations pour deux types aboutissent au mĂȘme code machine sous-jacent, nous ne voulons pas compiler et lier deux copies de ce code.

Une autre est que les packages paramétrés sont moins expressifs que les méthodes paramétrées. (Voir les propositions liées au premier commentaire pour plus de détails.)

L'hypertype est-il une bonne idée ?

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

L'hypertype est-il une bonne idée ?

Ce que vous décrivez ici n'est qu'un paramétrage de type à la C++ (c'est-à-dire des modÚles). Il ne vérifie pas le type de maniÚre modulaire car il n'y a aucun moyen de savoir que le type aType a une opération + à partir des informations fournies. Le paramétrage de type contraint comme dans CLU, Haskell, Java, Genus est la solution.

@ golang101 J'ai une proposition détaillée dans ce sens. Je vais envoyer un CL pour l'ajouter à la liste, mais il est peu probable qu'il soit adopté.

CL https://golang.org/cl/38731 mentionne ce problĂšme.

@andrewcmyers

Il ne vérifie pas le type de maniÚre modulaire car il n'y a aucun moyen de savoir que le type aType a une opération + à partir des informations fournies.

Bien sĂ»r qu'il y en a. Cette contrainte est implicite dans la dĂ©finition de la fonction, et les contraintes de cette forme peuvent ĂȘtre propagĂ©es Ă  tous les appelants (transitifs) au moment de la compilation de getAddFunc .

La contrainte ne fait pas partie d'un Go _type_ — c'est-Ă -dire qu'elle ne peut pas ĂȘtre encodĂ©e dans le systĂšme de type de la partie d'exĂ©cution du langage — mais cela ne signifie pas qu'elle ne peut pas ĂȘtre Ă©valuĂ©e de maniĂšre modulaire.

Ajout de ma proposition en tant que 2016-09-compile-time-functions.md .

Je ne m'attends pas à ce qu'il soit adopté, mais il peut au moins servir de référence intéressante.

@bcmills Je pense que les fonctions de temps de compilation sont une idĂ©e puissante, en dehors de toute considĂ©ration de gĂ©nĂ©riques. Par exemple, j'ai Ă©crit un solveur de sudoku qui a besoin d'un popcount. Pour accĂ©lĂ©rer cela, j'ai prĂ©calculĂ© les popcounts pour les diffĂ©rentes valeurs possibles et je l'ai stockĂ© en tant que Go source . C'est quelque chose que l'on pourrait faire avec go:generate . Mais s'il y avait une fonction de temps de compilation, cette table de recherche pourrait tout aussi bien ĂȘtre calculĂ©e au moment de la compilation, Ă©vitant ainsi au code gĂ©nĂ©rĂ© par la machine d'avoir Ă  ĂȘtre validĂ© dans le rĂ©fĂ©rentiel. En gĂ©nĂ©ral, tout type de fonction mathĂ©matique mĂ©morisable convient parfaitement aux tables de recherche prĂ©dĂ©finies avec des fonctions de temps de compilation.

Plus spĂ©culativement, on pourrait aussi vouloir, par exemple, tĂ©lĂ©charger une dĂ©finition de protobuf Ă  partir d'une source canonique et l'utiliser pour construire des types au moment de la compilation. Mais peut-ĂȘtre que c'est trop pour ĂȘtre autorisĂ© Ă  faire au moment de la compilation ?

J'ai l'impression que les fonctions de compilation sont à la fois trop puissantes et trop faibles : elles sont trop flexibles et peuvent se tromper de maniÚre étrange / ralentir la compilation comme le font les modÚles C++, mais d'un autre cÎté, elles sont trop statiques et difficiles à s'adapter à des choses comme des fonctions de premiÚre classe.

Pour la deuxiÚme partie, je ne vois pas comment créer quelque chose comme une "tranche de fonctions qui traitent des tranches d'un type particulier et renvoient un élément", ou dans une syntaxe ad hoc []func<T>([]T) T , qui est trÚs facile à faire dans pratiquement tous les langages fonctionnels à typage statique. Ce qui est vraiment nécessaire, ce sont des valeurs capables de prendre des types paramétriques, et non une génération de code au niveau du code source.

@bunsim

Pour la deuxiÚme partie, je ne vois pas comment créer quelque chose comme une "tranche de fonctions qui traitent des tranches d'un type particulier et renvoient un élément",

Si vous parlez d'un paramĂštre de type unique, dans ma proposition, cela s'Ă©crirait:

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

Si vous parlez de mĂ©langer des paramĂštres de type et des paramĂštres de valeur, non, ma proposition ne le permet pas : une partie de l'intĂ©rĂȘt des fonctions de compilation est de pouvoir fonctionner sur des valeurs non encadrĂ©es, et le type de paramĂ©tricitĂ© d'exĂ©cution Je pense que vous dĂ©crivez Ă  peu prĂšs nĂ©cessite la boxe des valeurs.

Oui, mais Ă  mon avis, ce genre de chose qui nĂ©cessite une boxe devrait ĂȘtre autorisĂ© tout en gardant la sĂ©curitĂ© de type, peut-ĂȘtre avec une syntaxe spĂ©ciale qui indique la "boĂźte". Une grande partie de l'ajout de "gĂ©nĂ©riques" consiste vraiment Ă  Ă©viter l'insĂ©curitĂ© de type de interface{} mĂȘme lorsque la surcharge de interface{} n'est pas Ă©vitable. (Peut-ĂȘtre n'autoriser que certaines constructions de type paramĂ©trique avec des types de pointeur et d'interface qui sont "dĂ©jĂ " encadrĂ©s ? Les objets encadrĂ©s Integer etc. de Java ne sont pas complĂštement une mauvaise idĂ©e, bien que les tranches de types valeur soient dĂ©licates)

J'ai juste l'impression que les fonctions de compilation ressemblent beaucoup Ă  C++ et seraient extrĂȘmement dĂ©cevantes pour des personnes comme moi qui s'attendent Ă  ce que Go2 ait un systĂšme de type paramĂ©trique moderne fondĂ© sur une thĂ©orie de type sonore plutĂŽt qu'un hack basĂ© sur la manipulation de morceaux de code source Ă©crit dans une langue sans gĂ©nĂ©riques.

@bcmills
Ce que vous proposez ne sera pas modulaire. Si le module A utilise le module B, qui utilise le module C, qui utilise le module D, une modification de la façon dont un paramĂštre de type est utilisĂ© dans D peut devoir se propager jusqu'Ă  A, mĂȘme si l'implĂ©menteur de A n'a aucune idĂ©e que D est dans le systĂšme. Le couplage lĂąche fourni par les systĂšmes de modules sera affaibli et les logiciels seront plus fragiles. C'est l'un des problĂšmes des modĂšles C++.

Si, d'autre part, les signatures de type capturent les exigences sur les paramĂštres de type, comme dans des langages comme CLU, ML, Haskell ou Genus, un module peut ĂȘtre compilĂ© sans aucun accĂšs aux Ă©lĂ©ments internes des modules dont il dĂ©pend.

@bunsim

Une grande partie de l'ajout de "gĂ©nĂ©riques" consiste vraiment Ă  Ă©viter l'insĂ©curitĂ© de type d'interface{} mĂȘme lorsque la surcharge d'interface{} n'est pas Ă©vitable.

"non évitable" est relatif. Notez que la surcharge de la boxe est le point n ° 3 dans le post de Russ de 2009 (https://research.swtch.com/generic).

s'attendre à ce que Go2 ait un systÚme de type paramétrique moderne fondé sur une théorie de type sonore plutÎt qu'un hack basé sur la manipulation de morceaux de code source

Une bonne "thĂ©orie des types sonores" est descriptive et non prescriptive. Ma proposition s'inspire en particulier du calcul lambda du second ordre (dans le sens du systĂšme F), oĂč gotype reprĂ©sente le genre type et l'ensemble du systĂšme de type du premier ordre est hissĂ© dans le second -types d'ordre ("compilation-time").

Il est également lié aux travaux sur la théorie des types modaux de Davies, Pfenning et al à la CMU. Pour un peu de contexte, je commencerais par A Modal Analysis of Staged Computation and Modal Types as Staging Specifications for Run-time Code Generation .

Il est vrai que la théorie des types sous-jacente à ma proposition est moins formellement spécifiée que dans la littérature académique, mais cela ne veut pas dire qu'elle n'y est pas.

@andrewcmyers

Si le module A utilise le module B, qui utilise le module C, qui utilise le module D, une modification de la façon dont un paramĂštre de type est utilisĂ© dans D peut devoir se propager jusqu'Ă  A, mĂȘme si l'implĂ©menteur de A n'a aucune idĂ©e que D est dans le systĂšme.

C'est déjà vrai dans Go aujourd'hui : si vous regardez attentivement, vous remarquerez que les fichiers objets générés par le compilateur pour un package Go donné incluent des informations sur les parties des dépendances transitives qui affectent l'API exportée.

Le couplage lĂąche fourni par les systĂšmes de modules sera affaibli et les logiciels seront plus fragiles.

J'ai entendu le mĂȘme argument utilisĂ© pour prĂ©coniser l'exportation de types interface plutĂŽt que de types concrets dans les API Go, et l'inverse s'avĂšre plus courant : l'abstraction prĂ©maturĂ©e surcontraint les types et entrave l'extension des API. (Pour un tel exemple, voir # 19584.) Si vous voulez vous appuyer sur cette ligne d'argumentation, je pense que vous devez fournir des exemples concrets.

C'est l'un des problĂšmes des modĂšles C++.

Comme je le vois, les principaux problĂšmes avec les modĂšles C++ sont (sans ordre particulier):

  • AmbiguĂŻtĂ© syntaxique excessive.
    une. Ambiguïté entre les noms de type et les noms de valeur.
    b. Prise en charge excessivement large de la surcharge de l'opérateur, entraßnant une capacité affaiblie à déduire les contraintes de l'utilisation de l'opérateur.
  • DĂ©pendance excessive Ă  la rĂ©solution de surcharge pour la mĂ©taprogrammation (ou, de maniĂšre Ă©quivalente, Ă©volution ad hoc de la prise en charge de la mĂ©taprogrammation).
    une. Surtout par rapport aux rÚgles d'effondrement des références.
  • Application trop large du principe SFINAE, conduisant Ă  des contraintes trĂšs difficiles Ă  propager et beaucoup trop de conditions implicites dans les dĂ©finitions de type, conduisant Ă  un rapport d'erreur trĂšs difficile.
  • Utilisation excessive du collage de jetons et de l'inclusion textuelle (le prĂ©processeur C) au lieu de la substitution AST et des artefacts de compilation d'ordre supĂ©rieur (qui, heureusement, semblent ĂȘtre au moins en partie rĂ©solus avec les modules).
  • Manque de bons langages d'amorçage pour les compilateurs C++, entraĂźnant un mauvais rapport d'erreurs dans les lignĂ©es de compilateurs de longue durĂ©e (par exemple, la chaĂźne d'outils GCC).
  • Le doublement (et parfois la multiplication) des noms rĂ©sultant du mappage d'ensembles d'opĂ©rateurs sur des "concepts" nommĂ©s diffĂ©remment (plutĂŽt que de traiter les opĂ©rateurs eux-mĂȘmes comme les contraintes fondamentales).

Je code en C++ par intermittence depuis une décennie maintenant et je suis heureux de discuter longuement des lacunes de C++, mais le fait que les dépendances de programme soient transitives n'a jamais figuré en haut de ma liste de plaintes.

D'un autre cĂŽtĂ©, avoir besoin de mettre Ă  jour une chaĂźne de dĂ©pendances O(N) juste pour ajouter une seule mĂ©thode Ă  un type dans le module A et pouvoir l'utiliser dans le module D ? C'est le genre de problĂšme qui me ralentit rĂ©guliĂšrement. LĂ  oĂč la paramĂ©tricitĂ© et le couplage lĂąche sont en conflit, je choisirai la paramĂ©tricitĂ© n'importe quel jour.

Pourtant, je crois fermement que la mĂ©taprogrammation et le polymorphisme paramĂ©trique doivent ĂȘtre sĂ©parĂ©s, et la confusion de C++ entre eux est la cause premiĂšre de la raison pour laquelle les modĂšles C++ sont ennuyeux. En termes simples, C++ tente d'implĂ©menter une idĂ©e de thĂ©orie des types en utilisant essentiellement des macros sur des stĂ©roĂŻdes, ce qui est trĂšs problĂ©matique puisque les programmeurs aiment considĂ©rer les modĂšles comme un vĂ©ritable polymorphisme paramĂ©trique et sont frappĂ©s par un comportement inattendu. Les fonctions de compilation sont une excellente idĂ©e pour la mĂ©taprogrammation et le remplacement du hack qui est go generate , mais je ne pense pas que cela devrait ĂȘtre la maniĂšre bĂ©nie de faire de la programmation gĂ©nĂ©rique.

Le polymorphisme paramĂ©trique "rĂ©el" facilite le couplage lĂąche et ne devrait pas entrer en conflit avec lui. Il devrait Ă©galement ĂȘtre Ă©troitement intĂ©grĂ© au reste du systĂšme de type; par exemple, il devrait probablement ĂȘtre intĂ©grĂ© dans le systĂšme d'interface actuel, de sorte que de nombreuses utilisations des types d'interface pourraient ĂȘtre rĂ©Ă©crites dans des Ă©lĂ©ments tels que :

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

ce qui devrait Ă©viter la surcharge de l'interface (comme l'utilisation de Rust), bien que dans ce cas ce ne soit pas trĂšs utile.

Un meilleur exemple pourrait ĂȘtre le package sort , oĂč vous pourriez avoir quelque chose comme

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

oĂč Comparable est simplement une bonne vieille interface que les types peuvent implĂ©menter. Sort peut ensuite ĂȘtre appelĂ© sur une tranche de types valeur qui implĂ©mentent Comparable , sans les enfermer dans des types d'interface.

@bcmills Les dĂ©pendances transitives non contraintes par le systĂšme de type sont, Ă  mon avis, au cƓur de certaines de vos plaintes concernant C++. Les dĂ©pendances transitives ne sont pas vraiment un problĂšme si vous contrĂŽlez les modules A, B, C et D. En gĂ©nĂ©ral, vous dĂ©veloppez le module A et ne savez peut-ĂȘtre que faiblement que le module D est lĂ -bas, et inversement, le dĂ©veloppeur de D peut ignorer A. Si le module D maintenant, sans apporter de modification aux dĂ©clarations visibles dans D, commence Ă  utiliser un nouvel opĂ©rateur sur un paramĂštre de type - ou utilise simplement ce paramĂštre de type comme argument de type Ă  un nouveau module E avec son propre contraintes implicites - ces contraintes se rĂ©percuteront sur tous les clients, qui n'utiliseront peut-ĂȘtre pas d'arguments de type satisfaisant aux contraintes. Rien ne dit au dĂ©veloppeur D qu'ils le font exploser. En effet, vous avez une sorte d'infĂ©rence de type globale, avec toutes les difficultĂ©s de dĂ©bogage que cela implique.

Je pense que l'approche que nous avons adoptĂ©e dans Genus [ PLDI'15 ] est bien meilleure. Les paramĂštres de type ont des contraintes explicites, mais lĂ©gĂšres (je prends note de votre point de vue sur la prise en charge des contraintes d'opĂ©ration ; CLU a montrĂ© comment faire cela dĂšs 1977). La vĂ©rification de type Genre est entiĂšrement modulaire. Le code gĂ©nĂ©rique peut soit ĂȘtre compilĂ© une seule fois pour optimiser l'espace de code, soit ĂȘtre spĂ©cialisĂ© sur des arguments de type particuliers pour de bonnes performances.

@andrewcmyers

Si le module D maintenant, sans apporter de modification aux déclarations visibles dans D, commence à utiliser un nouvel opérateur sur un paramÚtre de type [
] [clients] peut ne pas utiliser d'arguments de type satisfaisant aux contraintes. Rien ne dit au développeur D qu'ils le font exploser.

Bien sûr, mais c'est déjà vrai pour de nombreuses contraintes implicites dans Go, indépendamment de tout mécanisme de programmation générique.

Par exemple, une fonction peut recevoir un paramĂštre de type interface et appeler initialement ses mĂ©thodes sĂ©quentiellement. Si cette fonction change ultĂ©rieurement pour appeler ces mĂ©thodes simultanĂ©ment (en engendrant des goroutines supplĂ©mentaires), la contrainte "doit ĂȘtre sĂ»re pour une utilisation simultanĂ©e" n'est pas reflĂ©tĂ©e dans le systĂšme de type.

De mĂȘme, le systĂšme de type Go aujourd'hui ne spĂ©cifie pas de contraintes sur les durĂ©es de vie des variables : certaines implĂ©mentations de io.Writer supposent Ă  tort qu'elles peuvent conserver une rĂ©fĂ©rence Ă  la tranche transmise et la lire plus tard (par exemple en effectuant l'Ă©criture rĂ©elle de maniĂšre asynchrone dans une goroutine d'arriĂšre-plan), mais cela provoque des courses de donnĂ©es si l'appelant de Write tente de rĂ©utiliser la mĂȘme tranche de sauvegarde pour un Write suivant.

Ou une fonction utilisant un commutateur de type peut prendre un chemin différent d'une méthode est ajoutée à l'un des types dans le commutateur.

Ou une fonction vĂ©rifiant un code d'erreur particulier peut s'arrĂȘter si la fonction gĂ©nĂ©rant l'erreur change la façon dont elle signale cette condition. (Par exemple, voir https://github.com/golang/go/issues/19647.)

Ou une fonction vérifiant un type d'erreur particulier peut se briser si des wrappers autour de l'erreur sont ajoutés ou supprimés (comme cela s'est produit dans le package standard net dans Go 1.5).

Ou la mise en mémoire tampon sur un canal exposé dans une API peut changer, introduisant des blocages et/ou des courses.

...etc.

Go n'est pas inhabituel à cet égard : les contraintes implicites sont omniprésentes dans les programmes du monde réel.


Si vous essayez de capturer toutes les contraintes pertinentes dans des annotations explicites, vous finissez par aller dans l'une des deux directions.

Dans un sens, vous construisez un systĂšme complexe et extrĂȘmement complet de types et d'annotations dĂ©pendants, et les annotations finissent par rĂ©capituler une partie substantielle du code qu'elles annotent. Comme j'espĂšre que vous pouvez le voir clairement, cette direction n'est pas du tout conforme Ă  la conception du reste du langage Go : Go privilĂ©gie la simplicitĂ© des spĂ©cifications et la concision du code par rapport au typage statique complet.

Dans l'autre sens, les annotations explicites ne couvriraient qu'un sous-ensemble des contraintes pertinentes pour une API donnée. Désormais, les annotations fournissent un faux sentiment de sécurité : le code peut toujours se casser en raison de modifications des contraintes implicites, mais la présence de contraintes explicites induit le développeur en erreur en lui faisant croire que toute modification « type-safe » maintient également la compatibilité.


Je ne comprends pas pourquoi ce type de stabilitĂ© de l'API doit ĂȘtre rĂ©alisĂ© via une annotation explicite du code source : le type de stabilitĂ© de l'API que vous dĂ©crivez peut Ă©galement ĂȘtre obtenu (avec moins de redondance dans le code) via l'analyse du code source. Par exemple, vous pouvez imaginer que l' outil api analyse le code et gĂ©nĂšre un ensemble de contraintes beaucoup plus riche que celui qui peut ĂȘtre exprimĂ© dans le systĂšme de type formel du langage, et donne Ă  l' outil guru le possibilitĂ© d'interroger l'ensemble calculĂ© de contraintes pour une fonction, une mĂ©thode ou un paramĂštre d'API donnĂ©.

@bcmills Ne faites-vous pas du parfait l'ennemi du bien ? Oui, il existe des contraintes implicites difficiles Ă  capturer dans un systĂšme de type. (Et une bonne conception modulaire Ă©vite d'introduire de telles contraintes implicites lorsque cela est possible). font des erreurs. MĂȘme avec les progrĂšs rĂ©cents en matiĂšre de diagnostic et de localisation automatiques des erreurs , je ne retiens pas mon souffle. D'une part, les outils d'analyse ne peuvent analyser que le code que vous leur fournissez. Les dĂ©veloppeurs n'ont pas toujours accĂšs Ă  tout le code qui pourrait ĂȘtre liĂ© au leur.

Alors, lĂ  oĂč il y a des contraintes faciles Ă  capturer dans un systĂšme de typage, pourquoi ne pas donner aux programmeurs la possibilitĂ© de les Ă©crire ? Nous avons 40 ans d'expĂ©rience dans la programmation avec des paramĂštres de type contraints statiquement. Il s'agit d'une annotation statique simple et intuitive qui porte ses fruits.

Une fois que vous avez commencé à créer un logiciel plus volumineux qui superpose des modules logiciels, vous commencez à vouloir écrire des commentaires expliquant ces contraintes implicites de toute façon. En supposant qu'il existe un bon moyen vérifiable de les exprimer, pourquoi ne pas laisser le compilateur participer à la blague afin qu'il puisse vous aider?

Je note que certains de vos exemples d'autres contraintes implicites impliquent la gestion des erreurs. Je pense que notre vérification statique légÚre des exceptions [ PLDI 2016 ] traiterait ces exemples.

@andrewcmyers

Alors, lĂ  oĂč il y a des contraintes faciles Ă  capturer dans un systĂšme de typage, pourquoi ne pas donner aux programmeurs la possibilitĂ© de les Ă©crire ?
[
]
Une fois que vous avez commencé à créer un logiciel plus volumineux qui superpose des modules logiciels, vous commencez à vouloir écrire des commentaires expliquant ces contraintes implicites de toute façon. En supposant qu'il existe un bon moyen vérifiable de les exprimer, pourquoi ne pas laisser le compilateur participer à la blague afin qu'il puisse vous aider?

En fait, je suis entiÚrement d'accord avec ce point, et j'utilise souvent un argument similaire en ce qui concerne la gestion de la mémoire. (Si vous devez de toute façon documenter des invariants sur le crénelage et la conservation des données, pourquoi ne pas appliquer ces invariants au moment de la compilation ?)

Mais je pousserais cet argument un peu plus loin : l'inverse est également vrai ! Si vous _n'avez pas_ besoin d'écrire un commentaire pour une contrainte (parce que c'est évident dans le contexte pour les humains qui travaillent avec le code), pourquoi devriez-vous avoir besoin d'écrire ce commentaire pour le compilateur ? Indépendamment de mes préférences personnelles, l'utilisation par Go de la récupération de place et des valeurs nulles indique clairement un parti pris pour "ne pas obliger les programmeurs à énoncer des invariants évidents". Il se peut que la modélisation de style Genus puisse exprimer bon nombre des contraintes qui seraient exprimées dans les commentaires, mais comment se comporte-t-elle en termes d'élidation des contraintes qui seraient également élidées dans les commentaires ?

Il me semble que les modĂšles de style Genus sont de toute façon plus que de simples commentaires : ils modifient en fait la sĂ©mantique du code dans certains cas, ils ne se contentent pas de le contraindre. Nous aurions maintenant deux mĂ©canismes diffĂ©rents - les interfaces et les modĂšles de type - pour paramĂ©trer les comportements. Cela reprĂ©senterait un changement majeur dans le langage Go : nous avons dĂ©couvert certaines bonnes pratiques pour les interfaces au fil du temps (telles que "dĂ©finir les interfaces cĂŽtĂ© consommateur") et il n'est pas Ă©vident que cette expĂ©rience se traduirait par un systĂšme aussi radicalement diffĂ©rent, mĂȘme en nĂ©gligeant la compatibilitĂ© Go 1.

De plus, l'une des excellentes propriĂ©tĂ©s de Go est que sa spĂ©cification peut ĂȘtre lue (et largement comprise) en un aprĂšs-midi. Il n'est pas Ă©vident pour moi qu'un systĂšme de contraintes de style Genus puisse ĂȘtre ajoutĂ© au langage Go sans le compliquer considĂ©rablement - je serais curieux de voir une proposition concrĂšte de modifications de la spĂ©cification.

Voici un point de donnĂ©es intĂ©ressant pour la "mĂ©taprogrammation". Ce serait bien pour certains types dans les packages sync et atomic — Ă  savoir, atomic.Value et sync.Map — de prendre en charge les mĂ©thodes CompareAndSwap , mais ceux-ci ne fonctionnent que pour les types qui se trouvent ĂȘtre comparables. Le reste des API atomic.Value et sync.Map restent utiles sans ces mĂ©thodes, donc pour ce cas d'utilisation, nous avons soit besoin de quelque chose comme SFINAE (ou d'autres types d'API dĂ©finies de maniĂšre conditionnelle), soit nous devons tomber retour Ă  une hiĂ©rarchie de types plus complexe.

Je veux abandonner cette idée de syntaxe créative d'utiliser l'écriture syllabique aborigÚne.

@bcmills Pouvez-vous expliquer plus en détail ces trois points ?

  1. Ambiguïté entre les noms de type et les noms de valeur.
  2. Prise en charge excessivement large de la surcharge des opérateurs
    3. Dépendance excessive à la résolution de surcharge pour la métaprogrammation

@mahdix Bien sûr.

  1. Ambiguïté entre les noms de type et les noms de valeur.

Cet article donne une bonne introduction. Pour analyser un programme C++, vous devez savoir quels noms sont des types et lesquels sont des valeurs. Lorsque vous analysez un programme C++ basé sur un modÚle, ces informations ne sont pas disponibles pour les membres des paramÚtres de modÚle.

Un problÚme similaire se pose dans Go pour les littéraux composites, mais l'ambiguïté se situe entre les valeurs et les noms de champ plutÎt qu'entre les valeurs et les types. Dans ce code Go :

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

a est-il un nom de champ littéral, ou est-ce la constante a utilisée comme clé de carte ou index de tableau ?

  1. Prise en charge excessivement large de la surcharge des opérateurs

La recherche dépendante des arguments est un bon point de départ. Les surcharges d'opérateurs en C++ peuvent se produire en tant que méthodes sur le type de récepteur ou en tant que fonctions libres dans l'un des nombreux espaces de noms, et les rÚgles de résolution de ces surcharges sont assez complexes.

Il existe de nombreuses façons d'éviter cette complexité, mais la plus simple (comme le fait actuellement Go) consiste à interdire complÚtement la surcharge de l'opérateur.

  1. Dépendance excessive à la résolution de surcharge pour la métaprogrammation

La bibliothĂšque <type_traits> est un bon point de dĂ©part. DĂ©couvrez la mise en Ɠuvre dans votre quartier convivial libc++ pour voir comment la rĂ©solution de surcharge entre en jeu.

Si Go prend en charge la mĂ©taprogrammation (et mĂȘme cela est trĂšs douteux), je ne m'attendrais pas Ă  ce qu'elle implique une rĂ©solution de surcharge comme opĂ©ration fondamentale pour protĂ©ger les dĂ©finitions conditionnelles.

@bcmills
Comme je n'ai jamais utilisé C++, pourriez-vous nous éclairer sur la surcharge d'opérateur via l'implémentation d'"interfaces" prédéfinies en termes de complexité. Python et Kotlin en sont des exemples.

Je pense qu'ADL lui-mĂȘme est un Ă©norme problĂšme avec les modĂšles C++ qui n'ont pour la plupart pas Ă©tĂ© mentionnĂ©s, car ils obligent le compilateur Ă  retarder la rĂ©solution de tous les noms jusqu'au moment de l'instanciation, et peuvent entraĂźner des bogues trĂšs subtils, en partie parce que "l'idĂ©al" et " les compilateurs paresseux" se comportent diffĂ©remment ici et la norme le permet. Le fait qu'il supporte la surcharge d'opĂ©rateurs n'est pas vraiment le pire de loin.

Cette proposition est basée sur des Templates, un systÚme d'expansion macro ne suffirait-il pas ? Je ne parle pas de go generate ou de projets comme gottemplate. Je parle plus comme ça :

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

La macro pourrait réduire le passe-partout et l'utilisation de la réflexion.

Je pense que C++ est un assez bon exemple pour que les gĂ©nĂ©riques ne soient pas basĂ©s sur des modĂšles ou des macros. Surtout si l'on considĂšre que Go a des choses comme des fonctions anonymes qui ne peuvent vraiment pas ĂȘtre "instanciĂ©es" au moment de la compilation, sauf en tant qu'optimisation.

@samadadi , vous pouvez faire passer votre message sans dire "qu'est-ce qui ne va pas chez vous". Cela dit, l'argument de la complexité a déjà été évoqué plusieurs fois.

Go n'est pas le premier langage à essayer d'atteindre la simplicité en omettant la prise en charge du polymorphisme paramétrique (génériques), bien que cette fonctionnalité soit devenue de plus en plus importante au cours des 40 derniÚres années - d'aprÚs mon expérience, c'est un incontournable des cours de programmation du deuxiÚme semestre.

Le problÚme de ne pas avoir la fonctionnalité dans le langage est que les programmeurs finissent par recourir à des solutions de contournement encore pires. Par exemple, les programmeurs Go écrivent souvent des modÚles de code qui sont macro-étendus pour produire le "vrai" code pour divers types souhaités. Mais le vrai langage de programmation est celui que vous tapez, pas celui que le compilateur voit. Donc, cette stratégie signifie effectivement que vous utilisez un langage (qui n'est plus standard) qui a toute la fragilité et le gonflement du code des modÚles C++.

Comme indiquĂ© sur https://blog.golang.org/toward-go2 , nous devons fournir des "rapports d'expĂ©rience", afin que les besoins et les objectifs de conception puissent ĂȘtre dĂ©terminĂ©s. Pourriez-vous prendre quelques minutes et documenter les macro-cas que vous avez observĂ©s ?

Veuillez garder ce bogue sur le sujet et civil. Et encore une fois, https://golang.org/wiki/NoMeToo. Veuillez ne commenter que si vous avez des informations uniques et constructives Ă  ajouter.

@mandolyte Il est trÚs facile de trouver sur le web des explications détaillées prÎnant la génération de code comme substitut (partiel) aux génériques :
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/

De toute Ă©vidence, il y a beaucoup de gens qui adoptent cette approche.

@andrewcmyers , il existe certaines limitations ainsi que des mises en garde de commodité lors de l'utilisation de la génération de code MAIS .
Généralement - si vous pensez que cette approche est la meilleure/suffisante, je pense que l'effort de permettre une génération quelque peu similaire à partir de la chaßne d'outils go serait une bénédiction.

  • L'optimisation du compilateur peut ĂȘtre un dĂ©fi dans ce cas, mais l'exĂ©cution sera cohĂ©rente, ET la maintenance du code, l'expĂ©rience utilisateur (simplicitĂ©...) , les meilleures pratiques standard et les normes de code unifiĂ©es peuvent ĂȘtre conservĂ©es .
    De plus - toute la chaĂźne d'outils restera la mĂȘme, Ă  l'exception des outils de dĂ©bogage (profileurs, dĂ©bogueurs d'Ă©tape, etc.) qui verront des lignes de code qui n'ont pas Ă©tĂ© Ă©crites par le dĂ©veloppeur, mais c'est un peu comme entrer dans le code ASM pendant le dĂ©bogage - seulement c'est un code lisible :) .

Inconvénient - aucun précédent (à ma connaissance) à cette approche dans la chaßne d'outils go.

Pour rĂ©sumer, considĂ©rez la gĂ©nĂ©ration de code comme faisant partie du processus de construction, elle ne devrait pas ĂȘtre trop compliquĂ©e, assez sĂ»re, optimisĂ©e pour l'exĂ©cution, peut conserver la simplicitĂ© et de trĂšs petits changements dans le langage.

IMHO : C'est un compromis facilement atteint, avec un prix bas.

Pour ĂȘtre clair, je ne considĂšre pas que la gĂ©nĂ©ration de code de style macro, qu'elle soit effectuĂ©e avec gen, cpp, gofmt -r ou d'autres outils de macro/modĂšle, soit une bonne solution au problĂšme des gĂ©nĂ©riques, mĂȘme si elle est standardisĂ©e. Il a les mĂȘmes problĂšmes que les templates C++ : gonflement du code, manque de vĂ©rification de type modulaire et difficultĂ© de dĂ©bogage. Cela s'aggrave au fur et Ă  mesure que vous commencez, comme c'est naturel, en construisant du code gĂ©nĂ©rique en termes d'autre code gĂ©nĂ©rique. À mon avis, les avantages sont limitĂ©s : cela rendrait la vie relativement simple aux auteurs du compilateur Go et cela produirait un code efficace - Ă  moins qu'il n'y ait une pression sur le cache d'instructions, une situation frĂ©quente dans les logiciels modernes !

Je pense que le point était plutÎt que la génération de code est utilisée pour remplacer
génériques, les génériques devraient donc chercher à résoudre la plupart de ces cas d'utilisation.

Le mercredi 26 juillet 2017, 22h41, Andrew Myers, [email protected] a Ă©crit :

Pour ĂȘtre clair, je ne considĂšre pas la gĂ©nĂ©ration de code de style macro, qu'elle soit effectuĂ©e
avec gen, cpp, gofmt -r ou d'autres outils de macro/modĂšle, pour ĂȘtre un bon
solution au problĂšme des gĂ©nĂ©riques mĂȘme s'il est standardisĂ©. Il a le mĂȘme
problÚmes en tant que modÚles C++ : gonflement du code, manque de vérification de type modulaire et
difficulté de débogage. Cela s'aggrave au fur et à mesure que vous commencez, comme c'est naturel, à construire
code générique en termes d'autre code générique. Selon moi, les avantages sont
limité : cela garderait la vie relativement simple pour les auteurs du compilateur Go
et il produit un code efficace - Ă  moins qu'il n'y ait un cache d'instructions
la pression, une situation fréquente dans les logiciels modernes !

—
Vous recevez ceci parce que vous avez commenté.
RĂ©pondez directement Ă  cet e-mail, consultez-le sur GitHub
https://github.com/golang/go/issues/15292#issuecomment-318242016 , ou muet
le fil
https://github.com/notifications/unsubscribe-auth/AT4HVb2SPMpe5dlEDUQeadIRKPaB74zoks5sR_jSgaJpZM4IG-xv
.

Il ne fait aucun doute que la gĂ©nĂ©ration de code n'est pas une VRAIE solution, mĂȘme si elle est accompagnĂ©e d'un support linguistique pour donner l'apparence et la convivialitĂ© comme une "partie du langage"

Mon point était que c'était TRÈS rentable.

Au fait, si vous regardez certains des substituts de gĂ©nĂ©ration de code, vous pouvez facilement voir comment ils auraient pu ĂȘtre beaucoup plus lisibles, plus rapides et manquer de concepts erronĂ©s (par exemple, itĂ©ration sur des tableaux de pointeurs vs valeurs) si le langage leur avait donnĂ© de meilleurs outils pour ça.

Et c'est peut-ĂȘtre une meilleure solution Ă  court terme, qui ne ressemblerait pas Ă  un patch :
avant de penser au "meilleur support générique qui sera également idiomatique" (je pense que certaines implémentations ci-dessus prendraient des années pour accomplir une intégration complÚte), implémentez des ensembles de fonctions "en langage" qui sont nécessaires de toute façon (comme un build in structure copie profonde) rendrait ces solutions de génération de code beaucoup plus utilisables.

AprÚs avoir lu les propositions génériques de @bcmills et @ianlancetaylor , j'ai fait les observations suivantes :

Fonctions de compilation et types de premiĂšre classe

J'aime l'idée de l'évaluation au moment de la compilation, mais je ne vois pas l'avantage de la limiter aux fonctions pures. Cette proposition introduit la fonction intégrée gotype , mais limite son utilisation aux fonctions const et à tous les types de données définis dans la portée de la fonction. Du point de vue d'un utilisateur de bibliothÚque, l'instanciation est limitée aux fonctions constructeur comme "New", et conduit à des signatures de fonction comme celle-ci :

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

Le type de retour ici ne peut pas ĂȘtre sĂ©parĂ© en un type de fonction car nous sommes limitĂ©s aux fonctions pures. De plus, la signature dĂ©finit deux nouveaux "types" dans la signature elle-mĂȘme (K et V), ce qui signifie que pour analyser un seul paramĂštre, nous devons analyser toute la liste des paramĂštres. C'est bien pour un compilateur, mais je me demande si cela ajoute de la complexitĂ© Ă  l'API publique d'un package.

ParamĂštres de type dans Go

Les types paramétrés permettent la plupart des cas d'utilisation de la programmation générique, par exemple la possibilité de définir des structures de données génériques et des opérations sur différents types de données. La proposition répertorie de maniÚre exhaustive les améliorations du vérificateur de type qui seraient nécessaires pour produire de meilleures erreurs de compilation, des temps de compilation plus rapides et des fichiers binaires plus petits.

Sous la section "Type Checker", la proposition répertorie également certaines restrictions de type utiles pour accélérer le processus, comme "Indexable", "Comparable", "Callable", "Composite", etc. Ce que je ne comprends pas, c'est pourquoi ne pas permettre à l'utilisateur de spécifier ses propres restrictions de type ? La proposition stipule que

Il n'y a aucune restriction sur la façon dont les types paramĂ©trĂ©s peuvent ĂȘtre utilisĂ©s dans une fonction paramĂ©trĂ©e.

Cependant, si les identifiants avaient plus de contraintes liées à eux, cela n'aurait-il pas pour effet d'aider le compilateur ? Envisager:

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.

SĂ©parer les contraintes de type des paramĂštres de type et autoriser les contraintes dĂ©finies par l'utilisateur pourrait Ă©galement amĂ©liorer la lisibilitĂ©, rendant les packages gĂ©nĂ©riques plus faciles Ă  comprendre. Fait intĂ©ressant, les dĂ©fauts Ă©numĂ©rĂ©s Ă  la fin de la proposition concernant la complexitĂ© des rĂšgles de dĂ©duction de type pourraient en fait ĂȘtre attĂ©nuĂ©s si ces rĂšgles sont explicitement dĂ©finies par l'utilisateur.

@ smasher164

J'aime l'idée de l'évaluation au moment de la compilation, mais je ne vois pas l'avantage de la limiter aux fonctions pures.

L'avantage est qu'il permet une compilation séparée. Si une fonction au moment de la compilation peut modifier l'état global, le compilateur doit soit disposer de cet état, soit journaliser les modifications de maniÚre à ce que l'éditeur de liens puisse les séquencer au moment de la liaison. Si une fonction de compilation peut modifier l'état local, nous aurions besoin d'un moyen de savoir quel état est local ou global. Les deux ajoutent de la complexité, et il n'est pas évident que l'un ou l'autre fournirait suffisamment d'avantages pour le compenser.

@ smasher164

Ce que je ne comprends pas, c'est pourquoi ne pas autoriser l'utilisateur à spécifier ses propres restrictions de type ?

Les restrictions de type dans cette proposition correspondent Ă  des opĂ©rations dans la syntaxe du langage. Cela rĂ©duit la surface des nouvelles fonctionnalitĂ©s : il n'est pas nĂ©cessaire de spĂ©cifier une syntaxe supplĂ©mentaire pour les types contraignants, car toutes les contraintes syntaxiques peuvent ĂȘtre dĂ©duites de l'utilisation.

si les identifiants avaient plus de contraintes liées à eux, cela n'aurait-il pas pour effet d'aider le compilateur ?

Le langage doit ĂȘtre conçu pour ses utilisateurs, pas pour les compilateurs-Ă©crivains.

il n'est pas nĂ©cessaire de spĂ©cifier une syntaxe supplĂ©mentaire pour les types contraignants car toutes les contraintes syntaxiques peuvent ĂȘtre dĂ©duites de l'utilisation.

C'est la voie empruntĂ©e par C++. Elle nĂ©cessite une analyse globale du programme pour identifier les usages pertinents. Le code ne peut pas ĂȘtre raisonnĂ© par les programmeurs de maniĂšre modulaire, et les messages d'erreur sont verbeux et incomprĂ©hensibles.

Il peut ĂȘtre si facile et lĂ©ger de spĂ©cifier les opĂ©rations nĂ©cessaires. Voir CLU (1977) pour un exemple.

@andrewcmyers

Elle nĂ©cessite une analyse globale du programme pour identifier les usages pertinents. Le code ne peut pas ĂȘtre raisonnĂ© par les programmeurs de maniĂšre modulaire,

Cela utilise une dĂ©finition particuliĂšre de "modulaire", qui, Ă  mon avis, n'est pas aussi universelle que vous semblez le supposer. Selon la proposition de 2013, chaque fonction ou type aurait un ensemble non ambigu de contraintes dĂ©duites de bas en haut Ă  partir de packages importĂ©s, exactement de la mĂȘme maniĂšre que le temps d'exĂ©cution (et les contraintes d'exĂ©cution) des fonctions non paramĂ©triques sont dĂ©rivĂ©s de bas en haut. des chaĂźnes d'appel aujourd'hui.

Vous pourriez probablement interroger les contraintes dĂ©duites Ă  l'aide guru ou d'un outil similaire, et il pourrait rĂ©pondre Ă  ces requĂȘtes en utilisant des informations locales Ă  partir des mĂ©tadonnĂ©es du package exportĂ©.

et les messages d'erreur sont verbeux et incompréhensibles.

Nous avons quelques exemples (GCC et MSVC) démontrant que les messages d'erreur générés naïvement sont incompréhensibles. Je pense qu'il est exagéré de supposer que les messages d'erreur pour les contraintes implicites sont intrinsÚquement mauvais.

Je pense que le plus gros inconvénient des contraintes inférées est qu'elles facilitent l'utilisation d'un type d'une maniÚre qui introduit une contrainte sans la comprendre pleinement. Dans le meilleur des cas, cela signifie simplement que vos utilisateurs peuvent rencontrer des échecs de compilation inattendus, mais dans le pire des cas, cela signifie que vous pouvez casser le package pour les consommateurs en introduisant une nouvelle contrainte par inadvertance. Des contraintes explicitement spécifiées éviteraient cela.

Personnellement, je ne pense pas non plus que les contraintes explicites soient en décalage avec l'approche Go existante, car les interfaces sont des contraintes de type d'exécution explicites, bien qu'elles aient une expressivité limitée.

Nous avons quelques exemples (GCC et MSVC) démontrant que les messages d'erreur générés naïvement sont incompréhensibles. Je pense qu'il est exagéré de supposer que les messages d'erreur pour les contraintes implicites sont intrinsÚquement mauvais.

La liste des compilateurs sur lesquels l'infĂ©rence de type non locale - ce que vous proposez - entraĂźne de mauvais messages d'erreur est un peu plus longue que cela. Cela inclut SML, OCaml et GHC, oĂč beaucoup d'efforts ont dĂ©jĂ  Ă©tĂ© consacrĂ©s Ă  l'amĂ©lioration de leurs messages d'erreur et oĂč il existe au moins une structure de module explicite aidant. Vous pourrez peut-ĂȘtre faire mieux, et si vous trouvez un algorithme pour de bons messages d'erreur avec le schĂ©ma que vous proposez, vous aurez une belle publication. Comme point de dĂ©part vers cet algorithme, vous trouverez peut-ĂȘtre utiles nos articles POPL 2014 et PLDI 2015 sur la localisation des erreurs. Ils sont plus ou moins Ă  la pointe de la technologie.

car toutes les contraintes syntaxiques peuvent ĂȘtre dĂ©duites de l'usage.

Cela ne limite-t-il pas l'étendue des programmes génériques vérifiables par type ? Par exemple, notez que la proposition type-params ne spécifie pas de contrainte "Iterable". Dans le langage actuel, cela correspondrait soit à une tranche, soit à un canal, mais un type composite (disons une liste chaßnée) ne satisferait pas nécessairement à ces exigences. Définir une interface comme

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

aide le cas de la liste chaĂźnĂ©e, mais maintenant les types de tranche et de canal intĂ©grĂ©s doivent ĂȘtre Ă©tendus pour satisfaire cette interface.

Une contrainte qui dit "J'accepte l'ensemble de tous les types qui sont des itĂ©rables, des tranches ou des canaux" semble ĂȘtre une situation gagnant-gagnant-gagnant pour l'utilisateur, l'auteur du package et l'implĂ©menteur du compilateur. Le point que j'essaie de faire valoir est que les contraintes sont un sur-ensemble de programmes syntaxiquement valides, et certains peuvent ne pas avoir de sens du point de vue du langage, mais uniquement du point de vue de l'API.

Le langage doit ĂȘtre conçu pour ses utilisateurs, pas pour les compilateurs-Ă©crivains.

Je suis d'accord, mais j'aurais peut-ĂȘtre dĂ» le formuler diffĂ©remment. L'amĂ©lioration de l'efficacitĂ© du compilateur pourrait ĂȘtre un effet secondaire des contraintes dĂ©finies par l'utilisateur. Le principal avantage serait la lisibilitĂ©, car l'utilisateur a de toute façon une meilleure idĂ©e du comportement de son API que le compilateur. Le compromis ici est que les programmes gĂ©nĂ©riques devraient ĂȘtre lĂ©gĂšrement plus explicites sur ce qu'ils acceptent.

Et si au lieu de

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

nous avons séparé la notion d'"interfaces" de celle de "contraintes". Ensuite, nous pourrions avoir

type T generic

type Iterable class {
    Next() T
}

oĂč "classe" signifie une classe de type de style Haskell, pas une classe de style Java.

Avoir des "classes de types" séparées des "interfaces" pourrait aider à clarifier une partie de la non-orthogonalité des deux idées. Alors Sortable (en ignorant sort.Interface) pourrait ressembler à :

type T generic

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

type Sortable class {
    Next() Comparable
}

Voici quelques commentaires sur la section "Classes et concepts de types" dans Genus par @andrewcmyers et son applicabilité à Go.

Cette section aborde les limitations des classes de types et des concepts, indiquant

premiĂšrement, la satisfaction des contraintes doit ĂȘtre observĂ©e de maniĂšre unique

Je ne suis pas sĂ»r de comprendre cette limitation. Lier une contrainte Ă  des identifiants sĂ©parĂ©s ne l'empĂȘcherait-il pas d'ĂȘtre unique pour un type donné ? Il me semble que la clause "where" dans Genus construit essentiellement un type/contrainte Ă  partir d'une contrainte donnĂ©e, mais cela semble analogue Ă  l'instanciation d'une variable Ă  partir d'un type donnĂ©. Une contrainte ressemble ainsi Ă  un kind .

Voici une simplification spectaculaire des définitions de contraintes, adaptée à 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.

Ainsi, une déclaration de carte apparaßtrait comme :

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

oĂč dans Genus, cela pourrait ressembler Ă :

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

et dans la proposition Type-Params existante, cela ressemblerait à :

type Map[K,V] struct {
}

Je pense que nous pouvons tous convenir que permettre aux contraintes de tirer parti du systÚme de type existant peut à la fois supprimer le chevauchement entre les fonctionnalités du langage et faciliter la compréhension des nouvelles.

et deuxiÚmement, leurs modÚles définissent comment adapter un seul type, alors que dans un langage avec sous-typage, chaque type adapté représente en général tous ses sous-types.

Cette limitation semble moins pertinente pour Go puisque le langage a déjà de bonnes rÚgles de conversion entre les types nommés/non nommés et les interfaces qui se chevauchent.

Les exemples donnĂ©s proposent des modĂšles comme solution, ce qui semble ĂȘtre une fonctionnalitĂ© utile mais pas nĂ©cessaire pour Go. Si une bibliothĂšque s'attend Ă  ce qu'un type implĂ©mente http.Handler par exemple, et que l'utilisateur souhaite des comportements diffĂ©rents selon le contexte, Ă©crire des adaptateurs est simple :

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

En fait, c'est ce que fait la bibliothĂšque standard .

@ smasher164

premiĂšrement, la satisfaction des contraintes doit ĂȘtre observĂ©e de maniĂšre unique
Je ne suis pas sĂ»r de comprendre cette limitation. Est-ce que lier une contrainte Ă  des identifiants sĂ©parĂ©s ne l'empĂȘcherait pas d'ĂȘtre unique pour un type donné ?

L'idĂ©e est que dans Genus vous pouvez satisfaire la mĂȘme contrainte avec le mĂȘme type de plusieurs maniĂšres, contrairement Ă  Haskell. Par exemple, si vous avez un HashSet[T] , vous pouvez Ă©crire HashSet[String] pour hacher les chaĂźnes de la maniĂšre habituelle mais HashSet[String with CaseInsens] pour hacher et comparer les chaĂźnes avec le CaseInsens modĂšle, qui traite vraisemblablement les chaĂźnes de maniĂšre insensible Ă  la casse. Le genre distingue en fait ces deux types ; cela pourrait ĂȘtre exagĂ©rĂ© pour Go. MĂȘme si le systĂšme de type n'en garde pas trace, il semble toujours important de pouvoir remplacer les opĂ©rations par dĂ©faut fournies par un type.

kind Any interface{} // accepte tout type qui satisfait interface{}.
type T Any // DĂ©clare un type Any kind. Le lie Ă©galement Ă  un identifiant.
kind Eq T == T // accepte tout type pour lequel l'égalité est définie.
type Map[K Eq, V Any] struct { ...
}

L'Ă©quivalent moral de ceci dans Genus serait :

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

Dans Familia nous Ă©crirons simplement :

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

Edit : rétracter ceci en faveur d'une solution basée sur la réflexion comme décrit dans # 4146 Une solution basée sur les génériques comme je l'ai décrite ci-dessous augmente de maniÚre linéaire le nombre de compositions. Alors qu'une solution basée sur la réflexion aura toujours un handicap de performance, elle peut s'optimiser au moment de l'exécution afin que le handicap soit constant quel que soit le nombre de compositions.

Il ne s'agit pas d'une proposition, mais d'un cas d'utilisation potentiel Ă  prendre en compte lors de la conception d'une proposition.

Deux choses sont communes dans le code Go aujourd'hui

  • envelopper une valeur d'interface pour fournir des fonctionnalitĂ©s supplĂ©mentaires (envelopper un http.ResponseWriter pour un framework)
  • avoir des mĂ©thodes optionnelles qui ont parfois des valeurs d'interface (comme Temporary() bool sur net.Error )

Ce sont à la fois bons et utiles, mais ils ne se mélangent pas. Une fois que vous avez encapsulé une interface, vous avez perdu la possibilité d'accéder à toutes les méthodes non définies sur le type d'encapsulation. c'est-à-dire étant donné

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

Si vous encapsulez une erreur dans cette structure, vous masquez toutes les méthodes supplémentaires sur l'erreur d'origine.

Si vous n'encapsulez pas l'erreur dans la structure, vous ne pouvez pas fournir le contexte supplémentaire.

Disons que la proposition générique acceptée vous permet de définir quelque chose comme ce qui suit (syntaxe arbitraire que j'ai essayé de rendre intentionnellement laide pour que personne ne se concentre dessus)

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

En tirant parti de l'intégration, nous pourrions intégrer n'importe quel type concret satisfaisant l'interface d'erreur et à la fois l'envelopper et avoir accÚs à ses autres méthodes. Malheureusement, cela ne nous amÚne qu'à mi-chemin.

Ce dont nous avons vraiment besoin ici, c'est de prendre une valeur arbitraire de l'interface d'erreur et d'intégrer son type dynamique.

Cela soulÚve immédiatement deux préoccupations

  • le type devrait ĂȘtre crĂ©Ă© au moment de l'exĂ©cution (probablement nĂ©cessaire par reflect de toute façon)
  • la crĂ©ation de type devrait paniquer si la valeur d'erreur est nulle

Si ceux-ci ne vous ont pas aigri, vous avez Ă©galement besoin d'un mĂ©canisme pour "sauter" l'interface vers son type dynamique, soit par une annotation dans la liste des paramĂštres gĂ©nĂ©riques pour dire "toujours instancier sur le type dynamique des valeurs d'interface " ou par une fonction magique qui ne peut ĂȘtre appelĂ©e que lors de l'instanciation de type pour dĂ©baller l'interface afin que son type et sa valeur puissent ĂȘtre correctement Ă©pissĂ©s.

Sans cela, vous instanciez simplement MyError sur le type d'erreur lui-mĂȘme et non sur le type dynamique de l'interface.

Disons que nous avons une fonction magique unbox à extraire et (d'une maniÚre ou d'une autre) appliquer les informations :

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

Supposons maintenant que nous ayons une erreur non nulle, err , dont le type dynamique est *net.DNSError . Ensuite ceci

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

imprimerait true . Mais si le type dynamique de err avait été *os.PathError , il aurait imprimé false.

J'espÚre que la sémantique proposée est claire compte tenu de la syntaxe obtuse utilisée dans la démonstration.

J'espÚre également qu'il existe un meilleur moyen de résoudre ce problÚme avec moins de mécanisme et de cérémonie, mais je pense que ce qui précÚde pourrait fonctionner.

@jimmyfrasche Si je comprends ce que vous voulez, c'est un mécanisme d'adaptation sans wrapper. Vous voulez pouvoir étendre l'ensemble des opérations qu'un type offre sans l'envelopper dans un autre objet qui masque l'original. C'est une fonctionnalité offerte par Genus.

@andrewcmyers non.

Struct's in Go permet l'intĂ©gration. Si vous ajoutez un champ sans nom mais avec un type Ă  une structure, il fait deux choses : il crĂ©e un champ avec le mĂȘme nom que le type et il permet une distribution transparente Ă  toutes les mĂ©thodes de ce type. Cela ressemble terriblement Ă  un hĂ©ritage, mais ce n'en est pas un. Si vous aviez un type T qui avait une mĂ©thode Foo() alors les Ă©lĂ©ments suivants sont Ă©quivalents

type S struct {
  T
}

et

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

(lorsque Foo est appelé, son "ceci" est toujours de type T).

Vous pouvez également intégrer des interfaces dans des structures. Cela donne à la structure toutes les méthodes du contrat de l'interface (bien que vous deviez attribuer une valeur dynamique au champ implicite ou cela provoquera une panique avec l'équivalent d'une exception de pointeur nul)

Go a des interfaces qui dĂ©finissent un contrat en termes de mĂ©thodes d'un type. Une valeur de n'importe quel type qui satisfait le contrat peut ĂȘtre encadrĂ©e dans une valeur de cette interface. Une valeur d'une interface est un pointeur vers le manifeste de type interne (type dynamique) et un pointeur vers une valeur de ce type dynamique (valeur dynamique). Vous pouvez faire des assertions de type sur une valeur d'interface pour (a) obtenir la valeur dynamique si vous affirmez Ă  son type non-interface ou (b) obtenir une nouvelle valeur d'interface si vous affirmez Ă  une interface diffĂ©rente que la valeur dynamique satisfait Ă©galement. Il est courant d'utiliser ce dernier pour "tester les fonctionnalitĂ©s" d'un objet pour voir s'il prend en charge les mĂ©thodes facultatives. Pour rĂ©utiliser un exemple prĂ©cĂ©dent, certaines erreurs ont une mĂ©thode "Temporary() bool" afin que vous puissiez voir si une erreur est temporaire avec :

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

Il est Ă©galement courant d'envelopper un type dans un autre type pour fournir des fonctionnalitĂ©s supplĂ©mentaires. Cela fonctionne bien avec les types non-interface. Lorsque vous encapsulez une interface, vous masquez Ă©galement les mĂ©thodes que vous ne connaissez pas et que vous ne pouvez pas les rĂ©cupĂ©rer avec des assertions de type "test de fonctionnalitĂ©": le type encapsulĂ© n'expose que les mĂ©thodes requises de l'interface mĂȘme si elle a des mĂ©thodes optionnelles . Envisager:

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

type I interface {
  Foo()
}

type B struct {
  I
}

var i I = B{A{}}

Vous ne pouvez pas appeler Bar sur i ou mĂȘme savoir qu'il existe Ă  moins que vous ne sachiez que le type dynamique de i est un B afin que vous puissiez le dĂ©baller et accĂ©der au champ I pour taper assert dessus .

Cela pose de réels problÚmes, en particulier avec les interfaces courantes telles que error ou Reader.

S'il existait un moyen d'extraire le type et la valeur dynamiques d'une interface (d'une maniĂšre sĂ»re et contrĂŽlĂ©e), vous pourriez paramĂ©trer un nouveau type avec cela, dĂ©finir le champ intĂ©grĂ© sur la valeur et renvoyer une nouvelle interface. Ensuite, vous obtenez une valeur qui satisfait l'interface d'origine, possĂšde toutes les fonctionnalitĂ©s amĂ©liorĂ©es que vous souhaitez ajouter, mais le reste des mĂ©thodes du type dynamique d'origine sont toujours lĂ  pour ĂȘtre testĂ©es.

@jimmyfrasche En effet. Ce que Genus vous permet de faire, c'est d'utiliser un type pour satisfaire un contrat "d'interface" sans le boxer. La valeur a toujours son type d'origine et ses opérations d'origine. De plus, le programme peut spécifier les opérations que le type doit utiliser pour satisfaire le contrat -- par défaut, ce sont les opérations que le type fournit, mais le programme peut en fournir de nouvelles si le type n'a pas les opérations nécessaires. Il peut également remplacer les opérations que le type utiliserait.

@jimmyfrasche @andrewcmyers Pour ce cas d'utilisation, voir aussi https://github.com/golang/go/issues/4146#issuecomment -318200547.

@jimmyfrasche Pour moi, il semble que le problÚme clé ici soit d'obtenir le type/valeur dynamique d'une variable. Mis à part l'intégration, un exemple simplifié serait

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

La valeur affectée à e doit avoir un type dynamique (ou concret) de quelque chose comme *net.DNSError , qui implémente error . Voici quelques façons possibles dont un futur changement de langue pourrait résoudre ce problÚme :

  1. Avoir une fonction magique semblable unbox qui découvre la valeur dynamique d'une variable. Cela s'applique à tout type qui n'est pas concret, par exemple les syndicats.
  2. Si le changement de langage prend en charge les variables de type, fournissez un moyen d'obtenir le type dynamique de la variable. Avec les informations de type, nous pouvons Ă©crire nous-mĂȘmes la fonction unbox . Par example,
func unbox(v T1) T2 {
    t := dynTypeOf(v)
    return v.(t)
}

wrap peut ĂȘtre Ă©crit de la mĂȘme maniĂšre qu'avant, ou comme

func wrap(ec extraContext, err error) error {
  if err == nil {
    return nil
  }
  t := dynTypeOf(err)
  return MyError{
    e: v.(t),
    extraContext: ec,
  }
}
  1. Si le changement de langue prend en charge les contraintes de type, voici une idée alternative :
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,
  }
}

Dans cet exemple, nous acceptons une valeur de n'importe quel type qui implĂ©mente error. Tout utilisateur de wrap qui attend un error en recevra un. Cependant, le type du e Ă  l'intĂ©rieur MyError est le mĂȘme que celui du err qui est passĂ©, qui n'est pas limitĂ© Ă  un type d'interface. Si on voulait le mĂȘme comportement que 2,

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

Puisque personne d'autre ne semble l'avoir fait, je voudrais souligner les "rapports d'expérience" trÚs évidents pour les génériques, comme demandé par https://blog.golang.org/toward-go2.

Le premier est le type intégré map :

m := make(map[string]string)

Le suivant est le type intégré chan :

c := make(chan bool)

Enfin, la bibliothĂšque standard est truffĂ©e d'alternatives interface{} oĂč les gĂ©nĂ©riques fonctionneraient de maniĂšre plus sĂ»re :

  • 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)
  • Ă  venir sync.Map (https://tip.golang.org/pkg/sync/#Map)
  • atomic.Value (https://golang.org/pkg/sync/atomic/#Value)

Il y en a peut-ĂȘtre d'autres qui me manquent. Le fait est que chacun des Ă©lĂ©ments ci-dessus est lĂ  oĂč je m'attendrais Ă  ce que les gĂ©nĂ©riques soient utiles.

(Remarque : je n'inclus pas sort.Sort ici car c'est un excellent exemple de la façon dont les interfaces peuvent ĂȘtre utilisĂ©es Ă  la place des gĂ©nĂ©riques.)

http://www.yinwang.org/blog-cn/2014/04/18/golang
Je pense que le générique est important. Sinon, ne peut pas gérer des types similaires. Parfois, l'interface ne peut pas résoudre le problÚme.

La syntaxe simple et le systĂšme de type sont les avantages importants de Go. Si vous ajoutez des gĂ©nĂ©riques, le langage deviendra un vilain gĂąchis comme Scala ou Haskell. De plus, cette fonctionnalitĂ© attirera des fanboys pseudo-acadĂ©miques, qui finiront par transformer les valeurs communautaires de "Faisons-le" Ă  "Parlons de la thĂ©orie et des mathĂ©matiques de CS". Évitez les gĂ©nĂ©riques, c'est un chemin vers l'abĂźme.

@bxqgit s'il vous plaĂźt gardez cela civil. Inutile d'insulter qui que ce soit.

Quant à ce que l'avenir apportera, nous verrons, mais je sais que pendant 98% de mon temps, je n'ai pas besoin de génériques, chaque fois que j'en ai besoin, j'aimerais pouvoir les utiliser. Comment sont-ils utilisés et comment sont-ils utilisés à tort est une discussion différente. L'éducation des utilisateurs devrait faire partie du processus.

@bxqgit
Il y a des situations dans lesquelles des gĂ©nĂ©riques sont nĂ©cessaires, comme des structures de donnĂ©es gĂ©nĂ©riques (Trees, Stacks, Queues , ...) ou des fonctions gĂ©nĂ©riques (Map, Filter, Reduce, ...) et ces choses sont inĂ©vitables, en utilisant des interfaces au lieu de gĂ©nĂ©riques dans ces situations ajoutent simplement une Ă©norme complexitĂ© Ă  la fois pour l'Ă©crivain de code et le lecteur de code et cela a Ă©galement un effet nĂ©faste sur l'efficacitĂ© du code au moment de l'exĂ©cution, il devrait donc ĂȘtre beaucoup plus rationnel d'ajouter aux gĂ©nĂ©riques du langage que d'essayer d'utiliser des interfaces et de rĂ©flĂ©chir pour Ă©crire complexe et code inefficace.

@bxqgit L'ajout de gĂ©nĂ©riques n'ajoute pas nĂ©cessairement de la complexitĂ© au langage, cela peut Ă©galement ĂȘtre rĂ©alisĂ© avec une syntaxe simple. Avec les gĂ©nĂ©riques, vous ajoutez une contrainte de type de temps de compilation variable qui est trĂšs utile avec les structures de donnĂ©es, comme l'a dit @riwogo .

Le systĂšme d'interface actuel dans go est trĂšs utile, nĂ©anmoins trĂšs mauvais lorsque vous avez besoin, par exemple, d'une implĂ©mentation gĂ©nĂ©rale de list, qui avec les interfaces nĂ©cessite une contrainte de type de temps d'exĂ©cution, nĂ©anmoins si vous ajoutez des gĂ©nĂ©riques, le type gĂ©nĂ©rique peut ĂȘtre substituĂ© dans temps de compilation avec le type rĂ©el, rendant la contrainte inutile.

De plus, rappelez-vous, les personnes derriÚre vont, développent le langage en utilisant ce que vous appelez "la théorie et les mathématiques de l'informatique", et sont également les personnes qui "font cela".

De plus, rappelez-vous, les personnes derriÚre vont, développent le langage en utilisant ce que vous appelez "la théorie et les mathématiques de l'informatique", et sont également les personnes qui "font cela".

Personnellement, je ne vois pas beaucoup de théorie CS et de mathématiques dans la conception du langage Go. C'est une langue assez primitive, ce qui est bien à mon avis. De plus, les personnes dont vous parlez ont décidé d'éviter les génériques et ont fait avancer les choses. Si ça marche bien, pourquoi changer quoi que ce soit ? En rÚgle générale, je pense que l'évolution et l'extension constantes de la syntaxe du langage sont une mauvaise pratique. Cela ne fait qu'ajouter de la complexité qui conduit au chaos de Haskell et Scala.

Le modÚle est compliqué mais les génériques sont simples

Regardez les fonctions SortInts, SortFloats, SortStrings dans le package de tri. Ou SearchInts, SearchFloats, SearchStrings. Ou les méthodes Len, Less et Swap de byName dans le package io/ioutil. Copie passe-partout pure.

Les fonctions de copie et d'ajout existent car elles rendent les tranches beaucoup plus utiles. Les génériques signifieraient que ces fonctions sont inutiles. Les génériques permettraient d'écrire des fonctions similaires pour les cartes et les canaux, sans parler des types de données créés par l'utilisateur. Certes, les tranches sont le type de données composite le plus important, et c'est pourquoi ces fonctions étaient nécessaires, mais d'autres types de données sont toujours utiles.

Mon vote est non aux applications gĂ©nĂ©riques gĂ©nĂ©ralisĂ©es, oui Ă  des fonctions gĂ©nĂ©riques plus intĂ©grĂ©es telles que append et copy qui fonctionnent sur plusieurs types de base. Peut-ĂȘtre que sort et search pourraient ĂȘtre ajoutĂ©s pour les types de collection ?

Pour mes applications, le seul type manquant est un ensemble non ordonné (https://github.com/golang/go/issues/7088), j'aimerais cela comme un type intégré afin qu'il obtienne le typage générique comme slice et map . Mettez le travail dans le compilateur (analyse comparative pour chaque type de base et un ensemble sélectionné de types struct puis ajustement pour de meilleures performances) et gardez les annotations supplémentaires hors du code de l'application.

smap intégré au lieu de sync.Map aussi s'il vous plaßt. D'aprÚs mon expérience, l'utilisation interface{} pour la sécurité du type d'exécution est un défaut de conception. La vérification de type au moment de la compilation est une raison majeure d'utiliser Go.

@pciet

D'aprÚs mon expérience, l'utilisation de l'interface{} pour la sécurité du type d'exécution est un défaut de conception.

Pouvez-vous simplement écrire un petit emballage (de type sûr) ?
https://play.golang.org/p/tG6hd-j5yx

@pierrre Ce wrapper vaut mieux qu'un reflect.TypeOf(item).AssignableTo(type) . Mais Ă©crire votre propre type avec map + sync.Mutex ou sync.RWMutex est la mĂȘme complexitĂ© sans l'assertion de type que requiert sync.Map .

Mon utilisation de la carte synchronisĂ©e a Ă©tĂ© pour les cartes globales de mutex avec un var myMapLock = sync.RWMutex{} Ă  cĂŽtĂ© au lieu de crĂ©er un type. Cela pourrait ĂȘtre plus propre. Un type intĂ©grĂ© gĂ©nĂ©rique me semble juste mais demande un travail que je ne peux pas faire, et je prĂ©fĂšre mon approche Ă  l'assertion de type.

Je soupçonne que la rĂ©action viscĂ©rale nĂ©gative aux gĂ©nĂ©riques que de nombreux programmeurs Go semblent avoir survient parce que leur principale exposition aux gĂ©nĂ©riques s'est faite via des modĂšles C++. C'est malheureux car C++ s'est tragiquement trompĂ© sur les gĂ©nĂ©riques dĂšs le premier jour et a aggravĂ© l'erreur depuis. Les gĂ©nĂ©riques pour Go pourraient ĂȘtre beaucoup plus simples et moins sujets aux erreurs.

Il serait dĂ©cevant de voir Go devenir de plus en plus complexe en ajoutant des types paramĂ©trĂ©s intĂ©grĂ©s. Il serait prĂ©fĂ©rable d'ajouter simplement le support du langage pour que les programmeurs Ă©crivent leurs propres types paramĂ©trĂ©s. Ensuite, les types spĂ©ciaux pourraient simplement ĂȘtre fournis en tant que bibliothĂšques plutĂŽt que d'encombrer le langage de base.

@andrewcmyers "Les gĂ©nĂ©riques pour Go pourraient ĂȘtre beaucoup plus simples et moins sujets aux erreurs." --- comme les gĂ©nĂ©riques en C#.

Il est décevant de voir Go devenir de plus en plus complexe en ajoutant des types paramétrés intégrés.

MalgrĂ© les spĂ©culations dans ce numĂ©ro, je pense que cela est extrĂȘmement peu probable.

L'exposant de la mesure de complexité des types paramétrés est la variance.
Les types de Go (Ă  l'exception des interfaces) sont invariants et cela peut et doit ĂȘtre
gardé la rÚgle.

Une implémentation mécanique des génériques "copier-coller" assistée par un compilateur
résoudrait 99 % du problÚme d'une maniÚre fidÚle à la stratégie sous-jacente de Go
principes de superficialité et de non-surprise.

Soit dit en passant, cela et des dizaines d'autres idées viables ont été discutées
avant et certains ont mĂȘme abouti Ă  de bonnes approches rĂ©alisables. À ceci
point, je suis à la limite du papier d'aluminium sur la façon dont ils ont tous disparu
silencieusement dans le vide.

Le 28 novembre 2017 Ă  23h54, "Andrew Myers" [email protected] a Ă©crit :

Je soupçonne que la réaction viscérale négative aux génériques que beaucoup de Go
les programmeurs semblent avoir surgi parce que leur exposition principale aux génériques était
via des modÚles C++. C'est malheureux car C++ a tragiquement obtenu des génériques
tort dÚs le premier jour et a aggravé l'erreur depuis. Génériques pour
Go pourrait ĂȘtre beaucoup plus simple et moins sujet aux erreurs.

Il est décevant de voir le Go devenir de plus en plus complexe en ajoutant
types paramétrés intégrés. Il serait préférable d'ajouter simplement la langue
prise en charge pour les programmeurs d'écrire leurs propres types paramétrés. Puis le
les types spĂ©ciaux pourraient simplement ĂȘtre fournis sous forme de bibliothĂšques plutĂŽt que d'encombrer
la langue de base.

—
Vous recevez ceci parce que vous avez été mentionné.
RĂ©pondez directement Ă  cet e-mail, consultez-le sur GitHub
https://github.com/golang/go/issues/15292#issuecomment-347691444 , ou muet
le fil
https://github.com/notifications/unsubscribe-auth/AJZ_jPsQd2qBbn9NI1wZeT-O2JpyraTMks5s7I81gaJpZM4IG-xv
.

Oui, vous pouvez avoir des génériques sans modÚles. Les modÚles sont une forme de polymorphisme paramétrique avancé principalement pour les installations de métaprogrammation.

@ianlancetaylor Rust permet à un programme d'implémenter un trait T sur un type existant Q , à condition que leur caisse définisse T ou Q .

Juste une pensĂ©e : je me demande si Simon Peyton Jones (oui, de la renommĂ©e de Haskell) et/ou les dĂ©veloppeurs de Rust pourraient ĂȘtre en mesure d'aider. Rust et Haskell ont probablement les deux systĂšmes de type les plus avancĂ©s de tous les langages de production, et Go devrait apprendre d'eux.

Il y a aussi Phillip Wadler , qui a travaillé sur Generic Java , qui a finalement conduit à l'implémentation des génériques dont Java dispose aujourd'hui.

@tarcieri Je ne pense pas que les génériques de Java soient trÚs bons, mais ils ont fait leurs preuves.

@DemiMarie Nous avons eu Andrew Myers ici, heureusement.

Sur la base de mon expĂ©rience personnelle, je pense que les personnes qui connaissent bien les diffĂ©rentes langues et les diffĂ©rents systĂšmes de types peuvent ĂȘtre trĂšs utiles pour examiner les idĂ©es. Mais pour produire les idĂ©es en premier lieu, ce dont nous avons besoin, ce sont des personnes qui connaissent trĂšs bien Go, comment cela fonctionne aujourd'hui et comment cela peut raisonnablement fonctionner Ă  l'avenir. Go est conçu pour ĂȘtre, entre autres, un langage simple. Il est peu probable que l'importation d'idĂ©es Ă  partir de langages tels que Haskell ou Rust, qui sont nettement plus compliquĂ©s que Go, soit un bon choix. Et en gĂ©nĂ©ral, les idĂ©es de personnes qui n'ont pas encore Ă©crit une quantitĂ© raisonnable de code Go sont peu susceptibles de convenir ; non pas que les idĂ©es soient mauvaises en tant que telles, mais simplement qu'elles ne cadrent pas bien avec le reste du langage.

Par exemple, il est important de comprendre que Go a déjà une prise en charge partielle de la programmation générique à l'aide de types d'interface et a déjà une prise en charge (presque) complÚte à l'aide du package reflect. Bien que ces deux approches de la programmation générique ne soient pas satisfaisantes pour diverses raisons, toute proposition de génériques en Go doit bien interagir avec elles tout en remédiant à leurs lacunes.

En fait, pendant que je suis ici, il y a quelque temps, j'ai pensé à la programmation générique avec des interfaces pendant un certain temps et j'ai trouvé trois raisons pour lesquelles cela n'était pas satisfaisant.

  1. Les interfaces exigent que toutes les opĂ©rations soient exprimĂ©es sous forme de mĂ©thodes. Cela rend difficile l'Ă©criture d'une interface pour les types intĂ©grĂ©s, tels que les types de canal. Tous les types de canaux prennent en charge l'opĂ©rateur <- pour les opĂ©rations d'envoi et de rĂ©ception, et il est assez facile d'Ă©crire une interface avec les mĂ©thodes Send et Receive , mais pour attribuer une valeur de canal pour ce type d'interface, vous devez Ă©crire les mĂ©thodes passe-partout Send et Receive . Ces mĂ©thodes passe-partout auront exactement la mĂȘme apparence pour chaque type de canal diffĂ©rent, ce qui est fastidieux.

  2. Les interfaces sont typĂ©es dynamiquement, et ainsi les erreurs combinant diffĂ©rentes valeurs typĂ©es statiquement ne sont dĂ©tectĂ©es qu'au moment de l'exĂ©cution, pas au moment de la compilation. Par exemple, une fonction Merge qui fusionne deux canaux en un seul canal en utilisant leurs mĂ©thodes Send et Receive exigera que les deux canaux aient des Ă©lĂ©ments du mĂȘme type, mais que la vĂ©rification ne peut ĂȘtre effectuĂ©e qu'au moment de l'exĂ©cution.

  3. Les interfaces sont toujours encadrées. Par exemple, il n'y a aucun moyen d'utiliser des interfaces pour agréger une paire d'autres types sans mettre ces autres types dans des valeurs d'interface, ce qui nécessite des allocations de mémoire supplémentaires et la recherche de pointeurs.

Je suis heureux de kibitz sur les propositions de gĂ©nĂ©riques pour Go. La quantitĂ© croissante de recherches sur les gĂ©nĂ©riques Ă  Cornell ces derniers temps, apparemment pertinente pour ce qui pourrait ĂȘtre fait avec Go, est peut-ĂȘtre Ă©galement intĂ©ressante :

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)

Dans l'analyse comparative de la carte par rapport à la tranche pour un type d'ensemble non ordonné, j'ai écrit des tests unitaires séparés pour chacun, mais avec les types d'interface, je peux combiner ces deux listes de tests en une seule :

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
}

Test de suppression d'un élément :

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

De cette façon, je peux regrouper mes cas précédemment séparés en une seule tranche de cas sans aucun problÚme :

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

Pour chaque type de béton, j'ai dû définir les méthodes d'interface. Par example:

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

Ces tests génériques pourraient utiliser une vérification de type proposée au moment de la compilation :

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

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

En y réfléchissant davantage, il ne semble pas qu'une vérification de type au moment de la compilation soit possible pour un tel test, car vous devrez exécuter le programme pour savoir si un type est passé à la méthode d'interface correspondante.

Alors qu'en est-il d'un type "générique" qui est une interface et qui a une assertion de type invisible ajoutée par le compilateur lorsqu'il est utilisé concrÚtement ?

@andrewcmyers L'article "Familia" Ă©tait intĂ©ressant (et bien au-dessus de ma tĂȘte). Une notion clĂ© Ă©tait l'hĂ©ritage. Comment les concepts changeraient-ils pour un langage comme Go qui repose sur la composition au lieu de l'hĂ©ritage ?

Merci. La partie hĂ©ritage ne s'applique pas Ă  Go - si vous n'ĂȘtes intĂ©ressĂ© que par les gĂ©nĂ©riques pour Go, vous pouvez arrĂȘter de lire aprĂšs la section 4 de l'article. La principale chose Ă  propos de cet article qui est pertinente pour Go est qu'il montre comment utiliser les interfaces Ă  la fois dans la façon dont elles sont utilisĂ©es pour Go maintenant et en tant que contraintes sur les types pour les abstractions gĂ©nĂ©riques. Ce qui signifie que vous bĂ©nĂ©ficiez de la puissance des classes de type Haskell sans ajouter une toute nouvelle construction au langage.

@andrewcmyers Pouvez-vous donner un exemple de ce à quoi cela ressemblerait dans Go ?

La principale chose à propos de cet article qui est pertinente pour Go est qu'il montre comment utiliser les interfaces à la fois dans la façon dont elles sont utilisées pour Go maintenant et en tant que contraintes sur les types pour les abstractions génériques.

Ma comprĂ©hension est que l'interface Go dĂ©finit une contrainte sur un type (par exemple "ce type peut ĂȘtre comparĂ© pour l'Ă©galitĂ© en utilisant 'l'interface de type Comparable' car il satisfait d'avoir une mĂ©thode Eq"). Je ne suis pas sĂ»r de comprendre ce que vous entendez par une contrainte de type.

Je ne connais pas Haskell, mais la lecture d'un aperçu rapide me fait deviner que les types qui correspondent à une interface Go s'intégreraient dans cette classe de types. Pouvez-vous expliquer ce qui est différent dans les classes de type Haskell ?

Une comparaison concrÚte entre Familia et Go serait intéressante. Merci d'avoir partagé votre papier.

Les interfaces Go peuvent ĂȘtre considĂ©rĂ©es comme dĂ©crivant une contrainte sur les types, via le sous-typage structurel. Cependant, cette contrainte de type, telle quelle, n'est pas suffisamment expressive pour capturer les contraintes souhaitĂ©es pour la programmation gĂ©nĂ©rique. Par exemple, vous ne pouvez pas exprimer la contrainte de type nommĂ©e Eq dans l'article Familia.

Quelques réflexions sur la motivation pour des installations de programmation plus génériques en Go :

Il y a donc ma liste de tests gĂ©nĂ©riques qui n'a pas vraiment besoin d'ĂȘtre ajoutĂ© Ă  la langue. À mon avis, le type gĂ©nĂ©rique que j'ai proposĂ© ne satisfait pas l'objectif Go de comprĂ©hension directe, il n'a pas grand-chose Ă  voir avec le terme de programmation gĂ©nĂ©ralement acceptĂ©, et faire l'affirmation de type n'Ă©tait pas moche car une panique sur l'Ă©chec est amende. Je suis dĂ©jĂ  satisfait des fonctionnalitĂ©s de programmation gĂ©nĂ©riques de Go pour mon besoin.

Mais sync.Map est un cas d'utilisation diffĂ©rent. Il y a un besoin dans la bibliothĂšque standard pour une implĂ©mentation de carte synchronisĂ©e gĂ©nĂ©rique mature au-delĂ  d'une simple structure avec une carte et un mutex. Pour la gestion des types, nous pouvons l'envelopper avec un autre type qui dĂ©finit un type non-interface{} et effectue une assertion de type, ou nous pouvons ajouter une vĂ©rification de rĂ©flexion en interne afin que les Ă©lĂ©ments qui suivent le premier doivent correspondre au mĂȘme type. Les deux ont des vĂ©rifications d'exĂ©cution, l'emballage nĂ©cessite de rĂ©Ă©crire chaque mĂ©thode pour chaque type d'utilisation, mais il ajoute une vĂ©rification de type au moment de la compilation pour l'entrĂ©e et masque l'assertion de type de sortie, et avec la vĂ©rification interne, nous devons quand mĂȘme faire une assertion de type de sortie. Dans tous les cas, nous effectuons des conversions d'interface sans aucune utilisation rĂ©elle des interfaces ; interface{} est un hack du langage et ne sera pas clair pour les nouveaux programmeurs Go. Bien que json.Marshal soit une bonne conception Ă  mon avis (y compris les balises de structure laides mais sensibles).

J'ajouterai que puisque sync.Map est dans la bibliothĂšque standard, idĂ©alement, il devrait Ă©changer l'implĂ©mentation pour les cas d'utilisation mesurĂ©s oĂč la structure simple est plus performante. La carte non synchronisĂ©e est un premier piĂšge courant dans la programmation simultanĂ©e Go et un correctif de bibliothĂšque standard devrait fonctionner.

La carte rĂ©guliĂšre n'a qu'une vĂ©rification de type au moment de la compilation et ne nĂ©cessite aucun de ces Ă©chafaudages. Je soutiens que sync.Map devrait ĂȘtre le mĂȘme ou ne devrait pas ĂȘtre dans la bibliothĂšque standard pour Go 2.

J'ai proposĂ© d'ajouter sync.Map Ă  la liste des types intĂ©grĂ©s et de faire de mĂȘme pour les futurs besoins similaires. Mais ma comprĂ©hension est de donner aux programmeurs Go un moyen de le faire sans avoir Ă  travailler sur le compilateur et passer par le gant d'acceptation open source est l'idĂ©e derriĂšre cette discussion. À mon avis, la fixation de sync.Map est un cas rĂ©el qui dĂ©finit en partie ce que devrait ĂȘtre cette proposition de gĂ©nĂ©riques.

Si vous ajoutez sync.Map en tant que module intĂ©grĂ©, jusqu'oĂč allez-vous ? Faites-vous un cas particulier pour chaque conteneur ?
sync.Map n'est pas le seul conteneur et certains sont meilleurs dans certains cas que dans d'autres.

@Azareal : @chowey les a répertoriés en août :

Enfin, la bibliothĂšque standard est truffĂ©e d'alternatives d'interface{} oĂč les gĂ©nĂ©riques fonctionneraient de maniĂšre plus sĂ»re :

‱ 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)
‱ sync.Map à venir (https://tip.golang.org/pkg/sync/#Map)
‱ atomic.Value (https://golang.org/pkg/sync/atomic/#Value)

Il y en a peut-ĂȘtre d'autres qui me manquent. Le fait est que chacun des Ă©lĂ©ments ci-dessus est lĂ  oĂč je m'attendrais Ă  ce que les gĂ©nĂ©riques soient utiles.

Et j'aimerais l'ensemble non ordonnĂ© pour les types qui peuvent ĂȘtre comparĂ©s pour l'Ă©galitĂ©.

J'aimerais que beaucoup de travail soit consacré à une implémentation variable dans le runtime pour chaque type basé sur l'analyse comparative afin que la meilleure implémentation possible soit généralement celle qui est utilisée.

Je me demande s'il existe des implĂ©mentations alternatives raisonnables avec Go 1 qui atteignent le mĂȘme objectif pour ces types de bibliothĂšques standard sans interface {} et sans gĂ©nĂ©riques.

Les interfaces golang et les classes de type haskell surmontent deux choses (qui sont trÚs bien !) :

1.) (Contrainte de type) Ils regroupent différents types avec une seule balise, le nom de l'interface
2.) (Dispatch) Ils proposent de dispatcher diffĂ©remment sur chaque type pour un ensemble de fonctions donnĂ© via la mise en Ɠuvre de l'interface

Mais,

1.) Parfois, vous ne voulez que des groupes anonymes comme un groupe int, float64 et string. Comment nommer une telle interface, NumericandString ?

2.) TrĂšs souvent, vous ne souhaitez pas rĂ©partir diffĂ©remment chaque type d'interface, mais fournir une seule mĂ©thode pour tous les types d'interface rĂ©pertoriĂ©s (peut-ĂȘtre possible avec les mĂ©thodes d'interface par dĂ©faut )

3.) TrÚs souvent, vous ne voulez pas énumérer tous les types possibles pour un groupe. Au lieu de cela, vous allez paresseux et dites que je veux que tous les types T implémentent une interface A et le compilateur recherche ensuite tous les types dans tous les fichiers source que vous modifiez et dans toutes les bibliothÚques que vous utilisez pour générer les fonctions appropriées au moment de la compilation.

Bien que le dernier point soit possible via le polymorphisme d'interface, il prĂ©sente l'inconvĂ©nient d'ĂȘtre un polymorphisme d'exĂ©cution impliquant des transtypages et comment restreindre l'entrĂ©e de paramĂštre d'une fonction pour contenir des types implĂ©mentant plus d'une interface ou l'une des nombreuses interfaces. La solution consiste Ă  introduire de nouvelles interfaces Ă©tendant d'autres interfaces (par imbrication d'interfaces) pour obtenir quelque chose de similaire, mais pas avec les meilleures pratiques.

D'ailleurs.
J'admets à ceux qui disent que go a déjà du polymorphisme et donc exactement go n'est plus un langage simple comme C. C'est un langage de programmation systÚme de haut niveau. Alors pourquoi ne pas étendre les offres de polymorphisme go.

Voici une bibliothÚque que j'ai commencée aujourd'hui pour les types d'ensembles génériques non ordonnés : https://github.com/pciet/unordered

Cela donne dans la documentation et les exemples de test qui type wrapper pattern (merci @pierrre) pour la sécurité du type au moment de la compilation et a également la vérification de réflexion pour la sécurité du type au moment de l'exécution.

Quels sont les besoins en gĂ©nĂ©riques ? Mon attitude nĂ©gative vis-Ă -vis des types gĂ©nĂ©riques de bibliothĂšque standard Ă©tait auparavant centrĂ©e sur l'utilisation d'interface{} ; ma plainte pourrait ĂȘtre rĂ©solue avec un type spĂ©cifique au package pour l'interface {} (comme type Item interface{} dans pciet/unordered) qui documente les contraintes non exprimables prĂ©vues.

Je ne vois pas la nécessité d'une fonctionnalité de langue supplémentaire alors que la simple documentation pourrait nous y amener maintenant. Il existe déjà de grandes quantités de code testé au combat dans la bibliothÚque standard qui fournit des fonctionnalités génériques (voir https://github.com/golang/go/issues/23077).

Votre type de code vérifie lors de l'exécution (et de ce point de vue, ce n'est en aucun cas mieux que interface{} sinon pire). Avec les génériques, vous auriez pu avoir les types de collection avec des vérifications de type au moment de la compilation.

Les vĂ©rifications au moment de l'exĂ©cution @zerkms peuvent ĂȘtre dĂ©sactivĂ©es en dĂ©finissant asserting = false (cela n'irait pas dans la bibliothĂšque standard), il existe un modĂšle d'utilisation pour les vĂ©rifications au moment de la compilation, et de toute façon une vĂ©rification de type regarde simplement la structure d'interface (en utilisant l'interface ajoute plus de dĂ©penses que la vĂ©rification de type). Si l'interface ne fonctionne pas, vous devrez Ă©crire votre propre type.

Vous dites que le code gĂ©nĂ©rique aux performances maximisĂ©es est un besoin clĂ©. Cela n'a pas Ă©tĂ© pour mes cas d'utilisation, mais peut-ĂȘtre que la bibliothĂšque standard pourrait devenir plus rapide, et peut-ĂȘtre que d'autres ont besoin d'une telle chose.

les contrĂŽles d'exĂ©cution peuvent ĂȘtre dĂ©sactivĂ©s en dĂ©finissant asserting = false

alors rien ne garantit l'exactitude

Vous dites que le code générique aux performances maximisées est un besoin clé.

Je n'ai pas dit ça. La sécurité de type serait une bonne affaire. Votre solution est toujours interface{} -infectée.

mais peut-ĂȘtre que la bibliothĂšque standard pourrait devenir plus rapide, et peut-ĂȘtre que d'autres en ont besoin.

peut-ĂȘtre, si l'Ă©quipe de dĂ©veloppement principale est heureuse de mettre en Ɠuvre tout ce dont j'ai besoin Ă  la demande et rapidement.

@pciet

Je ne vois pas la nécessité d'une fonctionnalité de langue supplémentaire alors que la simple documentation pourrait nous y amener maintenant.

Vous dites cela, mais vous n'avez aucun problÚme à utiliser les fonctionnalités génériques du langage sous la forme de tranches et la fonction make.

Je ne vois pas la nécessité d'une fonctionnalité de langue supplémentaire alors que la simple documentation pourrait nous y amener maintenant.

Alors pourquoi s'embĂȘter Ă  utiliser un langage typĂ© statiquement ? Vous pouvez utiliser un langage typĂ© dynamiquement comme Python et vous fier Ă  la documentation pour vous assurer que les types de donnĂ©es corrects sont envoyĂ©s Ă  votre API.

Je pense que l'un des avantages de Go est la possibilitĂ© d'imposer certaines contraintes au compilateur pour Ă©viter de futurs bogues. Ces fonctionnalitĂ©s peuvent ĂȘtre Ă©tendues (avec le support des gĂ©nĂ©riques) pour appliquer d'autres contraintes afin d'Ă©viter d'autres bogues Ă  l'avenir.

Vous dites cela, mais vous n'avez aucun problÚme à utiliser les fonctionnalités génériques du langage sous la forme de tranches et la fonction make.

Je dis que les fonctionnalités existantes nous amÚnent à un bon point équilibré qui a des solutions de programmation génériques et qu'il devrait y avoir de bonnes raisons réelles de changer du systÚme de type Go 1. Non pas comment un changement améliorerait le langage, mais quels problÚmes les gens rencontrent actuellement, tels que le maintien d'un grand nombre de changements de type d'exécution pour l'interface {} dans les packages de bibliothÚques standard fmt et base de données, qui seraient résolus.

Alors pourquoi s'embĂȘter Ă  utiliser un langage typĂ© statiquement ? Vous pouvez utiliser un langage typĂ© dynamiquement comme Python et vous fier Ă  la documentation pour vous assurer que les types de donnĂ©es corrects sont envoyĂ©s Ă  votre API.

J'ai entendu des suggestions pour Ă©crire des systĂšmes en Python au lieu de langages et d'organisations Ă  typage statique.

La plupart des programmeurs Go utilisant la bibliothĂšque standard utilisent des types qui ne peuvent pas ĂȘtre complĂštement dĂ©crits sans documentation ou sans regarder l'implĂ©mentation. Les types avec des sous-types paramĂ©triques ou des types gĂ©nĂ©raux avec des contraintes appliquĂ©es ne corrigent qu'un sous-ensemble de ces cas par programmation et gĂ©nĂ©reraient beaucoup de travail dĂ©jĂ  effectuĂ© dans la bibliothĂšque standard.

Dans la proposition pour les types de somme, j'ai suggĂ©rĂ© une fonctionnalitĂ© de construction pour le commutateur de type d'interface oĂč une interface utilisĂ©e dans une fonction ou une mĂ©thode a une erreur de construction Ă©mise lorsqu'une valeur possible attribuĂ©e Ă  l'interface ne correspond Ă  aucun cas de commutateur de type d'interface contenu.

Une fonction/mĂ©thode prenant une interface pourrait rejeter certains types lors de la construction en n'ayant aucun cas par dĂ©faut et aucun cas pour le type. Cela semble ĂȘtre un ajout de programmation gĂ©nĂ©rique raisonnable si la fonctionnalitĂ© est rĂ©alisable.

Si les interfaces Go pouvaient capturer le type de l'implémenteur, il pourrait y avoir une forme de génériques complÚtement compatible avec la syntaxe Go actuelle - une forme de générique à paramÚtre unique ( démonstration ).

@dc0d pour les types de conteneurs génériques, je pense que cette fonctionnalité ajoute une vérification de type au moment de la compilation sans nécessiter de type wrapper : https://gist.github.com/pciet/36a9dcbe99f6fb71f5fc2d3c455971e5

@pciet Vous avez raison. Dans le code fourni, n° 4, l'exemple indique que le type est capturé pour les tranches et les canaux (et les tableaux). Mais pas pour les cartes, car il n'y a qu'un seul et unique paramÚtre de type : l'implémenteur. Et comme une carte a besoin de deux paramÚtres de type, des interfaces wrapper sont nécessaires.

BTW, je dois mettre l'accent sur le but démonstratif de ce code, en tant que ligne de pensée. Je ne suis pas un concepteur de langage. Ceci n'est qu'une façon hypothétique de penser à l'implémentation des génériques dans Go :

  • Compatible avec les Go actuels
  • Simple (paramĂštre de type gĂ©nĂ©rique unique, qui _ressemble_ Ă  _this_ dans d'autres OO, se rĂ©fĂ©rant Ă  l'implĂ©menteur actuel)

Discuter de la généricité et de tous les cas d'utilisation possibles dans le cadre d'une volonté de minimiser les impacts tout en maximisant les cas d'utilisation importants et la flexibilité d'expression est une analyse trÚs complexe. Je ne sais pas si l'un d'entre nous sera en mesure de le résumer en un petit ensemble de principes, c'est-à-dire l'essence générative. J'essaie. Quoi qu'il en soit, voici quelques-unes de mes premiÚres réflexions issues de ma lecture _cursory_ de ce fil


@adg a Ă©crit :

Ce numéro est accompagné d'une proposition générale de génériques par @ianlancetaylor qui comprend quatre propositions spécifiques erronées de mécanismes de programmation génériques pour Go.

Afaics, la section liĂ©e extraite comme suit ne parvient pas Ă  indiquer un cas de gĂ©nĂ©ricitĂ© manquant avec les interfaces actuelles, _ "Il n'y a aucun moyen d'Ă©crire une mĂ©thode qui prend une interface pour le type T fourni par l'appelant, pour tout T, et renvoie une valeur de le mĂȘme type T.”_.

Il n'y a aucun moyen d'Ă©crire une interface avec une mĂ©thode qui prend un argument de type T, pour tout T, et renvoie une valeur du mĂȘme type.

Alors, comment le code du type de site d'appel pourrait-il vérifier qu'il a un type T comme valeur de résultat ? Par exemple, ladite interface peut avoir une méthode de fabrique pour construire le type T. C'est pourquoi nous devons paramétrer les interfaces sur le type T.

Les interfaces ne sont pas simplement des types ; ce sont aussi des valeurs. Il n'y a aucun moyen d'utiliser les types d'interface sans utiliser les valeurs d'interface, et les valeurs d'interface ne sont pas toujours efficaces.

Convenu que puisque les interfaces ne peuvent actuellement pas ĂȘtre explicitement paramĂ©trĂ©es sur le type T sur lequel elles fonctionnent, le type T n'est pas accessible au programmeur.

Voici donc ce que font les limites de la classe de types sur le site de dĂ©finition de la fonction en prenant comme entrĂ©e un type T et en ayant une clause where ou requires indiquant la ou les interfaces requises pour le type T. Dans de nombreuses circonstances ces dictionnaires d'interface peuvent ĂȘtre automatiquement monomorphisĂ©s au moment de la compilation afin qu'aucun pointeur de dictionnaire (pour les interfaces) ne soit transmis Ă  la fonction au moment de l'exĂ©cution (monomorphisation que je suppose que le compilateur Go applique actuellement aux interfaces ?). Par "valeurs" dans la citation ci-dessus, je suppose qu'il entend le type d'entrĂ©e T et non le dictionnaire des mĂ©thodes pour le type d'interface implĂ©mentĂ© par le type T.

Si nous autorisons ensuite les paramĂštres de type sur les types de donnĂ©es (par exemple struct ), alors ledit type T ci-dessus peut ĂȘtre lui-mĂȘme paramĂ©trĂ©, nous avons donc vraiment un type T<U> . Les usines pour de tels types qui doivent conserver la connaissance de U sont appelĂ©es types de type supĂ©rieur (HKT) .

Les génériques autorisent les conteneurs polymorphes de type sécurisé.

Cf également la question des conteneurs _hétérogÚnes_ discutée ci-dessous. Donc, par polymorphe, nous entendons la généricité du type de valeur du conteneur (par exemple, le type d'élément de la collection), mais il y a aussi la question de savoir si nous pouvons mettre plus d'un type de valeur dans le conteneur simultanément, ce qui les rend hétérogÚnes.


@tamird a Ă©crit :

Ces exigences semblent exclure par exemple un systĂšme similaire au systĂšme de traits de Rust, oĂč les types gĂ©nĂ©riques sont contraints par des limites de traits.

Les limites de trait de Rust sont essentiellement des limites de classe de types.

@alex a Ă©crit :

Les traits de Rust. Bien que je pense qu'ils soient un bon modÚle en général, ils conviendraient mal au Go tel qu'il existe aujourd'hui.

Pourquoi pensez-vous qu'ils sont mal adaptĂ©s ? Peut-ĂȘtre pensez-vous aux objets de trait qui utilisent la rĂ©partition d'exĂ©cution et sont donc moins performants que le monomorphisme ? Mais ceux-ci peuvent ĂȘtre considĂ©rĂ©s sĂ©parĂ©ment du principe de gĂ©nĂ©ricitĂ© des limites de classe de types (cf ma discussion sur les conteneurs/collections hĂ©tĂ©rogĂšnes ci-dessous). Afaics, les interfaces de Go sont dĂ©jĂ  des limites de type trait et accomplissent l'objectif des classes de types qui est de lier tardivement les dictionnaires aux types de donnĂ©es sur le site d'appel, plutĂŽt que l'anti-modĂšle de la POO qui se lie tĂŽt (mĂȘme si encore Ă  la compilation- temps) dictionnaires aux types de donnĂ©es (Ă  l'instanciation/construction). Les classes de types peuvent (au moins une amĂ©lioration partielle des degrĂ©s de libertĂ©) rĂ©soudre le problĂšme d'expression que la POO ne peut pas.

@jimmyfrasche a Ă©crit :

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

Je suis d'accord avec le lien ci-dessus sur le fait que les classes de types ne sous-typent pas et n'expriment aucune relation d'héritage. Et acceptez de ne pas confondre inutilement la «généricité» (en tant que concept plus général de réutilisation ou de modularité que le polymorphisme paramétrique) avec l'héritage comme le fait le sous-classement.

Cependant, je tiens Ă©galement Ă  souligner que les hiĂ©rarchies d'hĂ©ritage (alias sous-typage) sont inĂ©vitables 1 sur l'affectation Ă  (entrĂ©es de fonction) et Ă  partir de (sorties de fonction) si le langage prend en charge les unions et les intersections, car par exemple un int Îœ string peut accepter une affectation d'un int ou d'un string mais aucun ne peut accepter une affectation d'un int Îœ string . Sans union autant que je sache, les seuls moyens alternatifs de fournir des conteneurs / collections hĂ©tĂ©rogĂšnes typĂ©s statiquement sont le polymorphisme de sous-classement ou existentiellement limitĂ© (alias objets de trait dans Rust et quantification existentielle dans Haskell). Les liens ci-dessus contiennent une discussion sur les compromis entre les existentiels et les syndicats. Afaik, la seule façon de faire des conteneurs/collections hĂ©tĂ©rogĂšnes dans Go maintenant est de subsumer tous les types dans un interface{} vide qui jette les informations de typage et devrais-je prĂ©sumer qu'il faudrait des casts et une inspection de type d'exĂ©cution, ce qui en quelque sorte 2 dĂ©fait le point de typage statique.

L '"anti-pattern" à éviter est le sous-classement , c'est-à-dire l'héritage virtuel (cf aussi "EDIT#2" sur les problÚmes de subsomption et d'égalité implicites, etc.).

1 Indépendamment du fait qu'ils correspondent structurellement ou nominalement, car le sous-typage est dû au principe de substitution de Liskov basé sur des ensembles comparatifs et la direction d'affectation avec des entrées de fonction opposées aux valeurs de retour, par exemple un paramÚtre de type d'un struct ou interface ne peut pas résider à la fois dans les entrées de la fonction et dans les valeurs de retour à moins qu'il soit invariant au lieu de co- ou contra-variant.

2 L'absolutisme ne s'appliquera pas parce que nous ne pouvons pas vérifier le type de l'univers du non-déterminisme illimité. Donc, si je comprends bien, ce fil concerne le choix d'une limite optimale («sweet spot») au niveau de la déclaration de frappe par rapport aux problÚmes de généricité.

@andrewcmyers a Ă©crit :

Contrairement aux génériques Java et C#, le mécanisme des génériques Genus n'est pas basé sur le sous-typage.

C'est l'héritage et le sous-classement ( pas le sous-typage structurel ) qui est le pire anti-modÚle que vous ne voulez pas copier depuis Java, Scala, Ceylan et C++ (sans rapport avec les problÚmes avec les modÚles C++ ).

@thwd a Ă©crit :

L'exposant de la mesure de complexité des types paramétrés est la variance. Les types de Go (à l'exception des interfaces) sont invariants et cela peut et doit rester la rÚgle.

Le sous-typage avec immuabilité évite la complexité de la covariance. L'immuabilité améliore également certains des problÚmes de sous-classement (par exemple, Rectangle vs. Square ) mais pas d'autres (par exemple, subsomption implicite, égalité, etc.).

@bxqgit a Ă©crit :

La syntaxe simple et le systÚme de type sont les avantages importants de Go. Si vous ajoutez des génériques, le langage deviendra un vilain gùchis comme Scala ou Haskell.

Notez que Scala tente de fusionner OOP, sous-classification, FP, modules gĂ©nĂ©riques, HKT et classes de types (via implicit ) en un seul PL. Peut-ĂȘtre que les classes de types seules pourraient suffire.

Haskell n'est pas nécessairement obtus à cause des génériques de classes de types, mais plus probablement parce qu'il applique des fonctions pures partout et utilise la théorie monadique des catégories pour modéliser des effets impératifs contrÎlés.

Ainsi, je pense qu'il n'est pas correct d'associer l'obtusité et la complexité de ces PL avec des classes de type, par exemple dans Rust. Et ne blùmons pas les classes de types pour les durées de vie de Rust + l'abstraction exclusive d'emprunt de mutabilité.

Afaics, dans la section Semantics des _Type Parameters in Go_, le problÚme rencontré par @ianlancetaylor est un problÚme de conceptualisation car il (afaics) réinvente apparemment sans le vouloir les classes de types :

Pouvons-nous fusionner SortableSlice et PSortableSlice pour avoir le meilleur des deux mondes ? Pas assez; il n'y a aucun moyen d'Ă©crire une fonction paramĂ©trĂ©e qui prend en charge un type avec une mĂ©thode Less ou un type intĂ©grĂ©. Le problĂšme est que SortableSlice.Less ne peut pas ĂȘtre instanciĂ© pour un type sans une mĂ©thode Less , et il n'y a aucun moyen d'instancier une mĂ©thode uniquement pour certains types mais pas pour d'autres.

La clause requires Less[T] pour la classe de type liĂ©e (mĂȘme si implicitement dĂ©duite par le compilateur) sur la mĂ©thode Less pour []T est sur T non []T . L'implĂ©mentation de la classe de types Less[T] (qui contient une mĂ©thode Less ) pour chaque T fournira soit une implĂ©mentation dans le corps de la fonction de la mĂ©thode, soit attribuera la < Fonction intĂ©grĂ©e U[T] si les mĂ©thodes de Sortable[U] ont besoin d'un paramĂštre de type U reprĂ©sentant le type d'implĂ©mentation, par exemple []T . Afair @keean a une autre façon de structurer un tri en utilisant une classe de types distincte pour le type de valeur T qui ne nĂ©cessite pas de HKT.

Notez que ces mĂ©thodes pour []T peuvent implĂ©menter une classe de type Sortable[U] , oĂč U est []T .

(Aparté technique : il peut sembler que nous pourrions fusionner SortableSlice et PSortableSlice en ayant un mécanisme pour instancier une méthode uniquement pour certains arguments de type mais pas pour d'autres. Cependant, le résultat serait de sacrifier la compilation -time type safety, car l'utilisation du mauvais type conduirait à une panique d'exécution. Dans Go, on peut déjà utiliser des types et des méthodes d'interface et des assertions de type pour sélectionner le comportement au moment de l'exécution. Il n'est pas nécessaire de fournir un autre moyen de le faire en utilisant des paramÚtres de type .)

La sélection de la classe de types liée au site d'appel est résolue au moment de la compilation pour un T statiquement connu. Si une répartition dynamique hétérogÚne est nécessaire, consultez les options que j'ai expliquées dans mon message précédent.

J'espĂšre que @keean pourra trouver le temps de venir ici et d'aider Ă  expliquer les classes de types car il est plus expert et m'a aidĂ© Ă  apprendre ces concepts. J'ai peut-ĂȘtre des erreurs dans mon explication.

Remarque PS pour ceux qui ont déjà lu mon post précédent, notez que je l'ai édité en profondeur environ 10 heures aprÚs l'avoir posté (aprÚs un peu de sommeil) pour, espérons-le, rendre les points sur les conteneurs hétérogÚnes plus cohérents.


La section Cycles semble incorrecte. La construction à l'exécution de l'instance S[T]{e} d'un struct n'a rien à voir avec la sélection de l'implémentation de la fonction générique appelée. Il pense vraisemblablement que le compilateur ne sait pas s'il spécialise l'implémentation de la fonction générique pour le type des arguments, mais tous ces types sont connus au moment de la compilation.

Peut-ĂȘtre que la spĂ©cification de la section Type Checking pourrait ĂȘtre simplifiĂ©e en Ă©tudiant le concept de @keean d'un graphe connexe de types distincts en tant que nƓuds pour un algorithme d'unification. Tous les types distincts connectĂ©s par une arĂȘte doivent avoir des types congruents, avec des arĂȘtes crĂ©Ă©es pour tous les types qui se connectent via une affectation ou autrement dans le code source. S'il y a union et intersection (de mon post prĂ©cĂ©dent), alors la direction de l'affectation doit ĂȘtre prise en compte (d'une maniĂšre ou d'une autre? ). Chaque type inconnu distinct commence par une borne supĂ©rieure (LUB) de Top et une borne infĂ©rieure (GLB) de Bottom , puis des contraintes peuvent modifier ces bornes. Les types connectĂ©s doivent avoir des bornes compatibles. Les contraintes doivent toutes ĂȘtre des limites de classe de types.

En implémentation :

Par exemple, il est toujours possible d'implĂ©menter des fonctions paramĂ©trĂ©es en gĂ©nĂ©rant une nouvelle copie de la fonction pour chaque instanciation, oĂč la nouvelle fonction est crĂ©Ă©e en remplaçant les paramĂštres de type par les arguments de type.

Je crois que le terme technique correct est monomorphisation .

Cette approche donnerait le temps d'exĂ©cution le plus efficace au prix d'un temps de compilation supplĂ©mentaire considĂ©rable et d'une taille de code accrue. C'est probablement un bon choix pour les fonctions paramĂ©trĂ©es qui sont suffisamment petites pour ĂȘtre intĂ©grĂ©es, mais ce serait un mauvais compromis dans la plupart des autres cas.

Le profilage indiquerait au programmeur quelles fonctions peuvent le plus bĂ©nĂ©ficier de la monomorphisation. Peut-ĂȘtre que l'optimiseur Java Hotspot effectue l'optimisation de la monomorphisation au moment de l'exĂ©cution ?

@egonelbre a Ă©crit :

Il y a Summary of Go Generics Discussions , qui tente de fournir un aperçu des discussions de différents endroits.

La section Vue d'ensemble semble impliquer que l'utilisation universelle par Java des rĂ©fĂ©rences de boxe pour les instances dans un conteneur est le seul axe de conception diamĂ©tralement opposĂ© Ă  la monomorphisation des modĂšles de C++. Mais les limites de classe de types (qui peuvent Ă©galement ĂȘtre implĂ©mentĂ©es avec des modĂšles C++ mais toujours monomorphisĂ©s) sont appliquĂ©es aux fonctions et non aux paramĂštres de type conteneur. Ainsi, afaics, la vue d'ensemble manque l'axe de conception pour les classes de types dans lequel nous pouvons choisir de monomorphiser chaque fonction dĂ©limitĂ©e de classe de types. Avec les classes de types, nous rendons toujours les programmeurs plus rapides (moins passe-partout) et pouvons obtenir un Ă©quilibre plus raffinĂ© entre rendre les compilateurs/l'exĂ©cution plus rapides/plus lents et le gonflement du code plus/moins. Selon mon post prĂ©cĂ©dent, peut-ĂȘtre que l'optimum serait si le choix des fonctions Ă  monomorphiser Ă©tait pilotĂ© par le profileur (automatiquement ou plus probablement par annotation).

Dans la section ProblÚmes : Structures de données génériques :

Les inconvénients

  • Les structures gĂ©nĂ©riques ont tendance Ă  accumuler des fonctionnalitĂ©s de toutes les utilisations, ce qui entraĂźne une augmentation des temps de compilation ou un gonflement du code ou nĂ©cessite un Ă©diteur de liens plus intelligent.

Pour les classes de types, ce n'est pas vrai ou cela pose moins de problĂšme, car les interfaces ne doivent ĂȘtre implĂ©mentĂ©es que pour les types de donnĂ©es qui sont fournis aux fonctions qui utilisent ces interfaces. Les classes de types concernent la liaison tardive de l'implĂ©mentation Ă  l'interface, contrairement Ă  la POO qui lie chaque type de donnĂ©es Ă  ses mĂ©thodes pour l'implĂ©mentation class .

De plus, toutes les mĂ©thodes n'ont pas besoin d'ĂȘtre placĂ©es dans une seule interface. La clause requires (mĂȘme si elle est implicitement dĂ©duite par le compilateur) sur une classe de type liĂ©e Ă  une dĂ©claration de fonction peut mĂ©langer et assortir les interfaces requises.

  • Les structures gĂ©nĂ©riques et les API qui fonctionnent sur elles ont tendance Ă  ĂȘtre plus abstraites que les API spĂ©cialement conçues, ce qui peut imposer une charge cognitive aux appelants

Un contre-argument qui, Ă  mon avis, attĂ©nue considĂ©rablement cette prĂ©occupation est que le fardeau cognitif de l'apprentissage d'un nombre illimitĂ© de rĂ©implĂ©mentations de cas particuliers des mĂȘmes algorithmes gĂ©nĂ©riques est illimitĂ©. Alors que l'apprentissage des API gĂ©nĂ©riques abstraites est limitĂ©.

  • Les optimisations approfondies sont trĂšs non gĂ©nĂ©riques et spĂ©cifiques au contexte, il est donc plus difficile de les optimiser dans un algorithme gĂ©nĂ©rique.

Ce n'est pas une escroquerie valide. La rÚgle 80/20 dit de ne pas ajouter de complexité illimitée (par exemple, une optimisation prématurée) pour le code qui, une fois profilé, n'en a pas besoin. Le programmeur est libre d'optimiser dans 20 % des cas tandis que les 80 % restants sont gérés par la complexité limitée et la charge cognitive des API génériques.

Ce que nous voulons vraiment dire ici, c'est la régularité d'un langage et l'aide des API génériques, pas de mal à cela. Ces inconvénients ne sont vraiment pas correctement conceptualisés.

Solutions alternatives:

  • utiliser des structures plus simples au lieu de structures compliquĂ©es

    • par exemple, utilisez map[int]struct{} au lieu de Set

Rob Pike (et je l'ai Ă©galement vu faire valoir ce point dans la vidĂ©o) semble ne pas comprendre que les conteneurs gĂ©nĂ©riques ne suffisent pas pour crĂ©er des fonctions gĂ©nĂ©riques. Nous avons besoin de ce T dans map[T] pour pouvoir passer le type de donnĂ©es gĂ©nĂ©rique dans les fonctions pour les entrĂ©es, les sorties et pour notre propre struct . Les gĂ©nĂ©riques uniquement sur les paramĂštres de type de conteneur sont totalement insuffisants pour exprimer des API gĂ©nĂ©riques et des API gĂ©nĂ©riques sont nĂ©cessaires pour une complexitĂ© et une charge cognitive limitĂ©es et pour obtenir la rĂ©gularitĂ© dans un Ă©cosystĂšme de langage. De plus, je n'ai pas vu le niveau accru de refactorisation (donc la composabilitĂ© rĂ©duite des modules qui ne peuvent pas ĂȘtre facilement refactorisĂ©s) que le code non gĂ©nĂ©rique nĂ©cessite, ce qui est le problĂšme d'expression que j'ai mentionnĂ© dans mon premier message.

Dans la section Approches génériques :

ModĂšles de package
Il s'agit d'une approche utilisée par Modula-3, OCaml, SML (appelés « foncteurs ») et Ada. Au lieu de spécifier un type individuel de spécialisation, l'ensemble du package est générique. Vous spécialisez le package en fixant les paramÚtres de type lors de l'importation.

Je peux me tromper mais cela ne semble pas tout Ă  fait correct. Les foncteurs ML (Ă  ne pas confondre avec les foncteurs FP) peuvent Ă©galement renvoyer une sortie qui reste paramĂ©trĂ©e par type. Sinon, il n'y aurait aucun moyen d'utiliser les algorithmes dans d'autres fonctions gĂ©nĂ©riques, donc les modules gĂ©nĂ©riques ne pourraient donc pas rĂ©utiliser (en important avec des types concrets dans) d'autres modules gĂ©nĂ©riques. Cela semble ĂȘtre une tentative de simplifier Ă  l'excĂšs, puis de passer complĂštement Ă  cĂŽtĂ© de l'intĂ©rĂȘt des gĂ©nĂ©riques, de la rĂ©utilisation des modules, etc.

Je crois plutÎt que cette paramétrisation de type de package (alias module) permet d'appliquer des paramÚtres de type à un groupement de struct , interface et func .

SystÚme de type plus compliqué
C'est l'approche adoptée par Haskell et Rust.
[
]
Les inconvénients:

  • difficile Ă  intĂ©grer dans un langage plus simple (https://groups.google.com/d/msg/golang-nuts/smT_0BhHfBs/MWwGlB-n40kJ)

Citant @ianlancetaylor dans le document lié :

Si vous croyez cela, alors il vaut la peine de souligner que le cƓur du
le code de carte et de tranche dans le runtime Go n'est pas générique dans le sens de
en utilisant le polymorphisme de type. Il est gĂ©nĂ©rique dans le sens oĂč il regarde
tapez des informations de réflexion pour voir comment déplacer et comparer le type
valeurs. Nous avons donc la preuve par l'existence qu'il est acceptable d'Ă©crire
code "générique" dans Go en écrivant du code non polymorphe qui utilise le type
informations de réflexion de maniÚre efficace, puis pour envelopper ce code dans
passe-partout de type sécurisé au moment de la compilation (dans le cas de cartes et de tranches
cette plaque de chaudiÚre est, bien sûr, fournie par le compilateur).

Et c'est ce qu'un compilateur transpilant à partir d'un sur-ensemble de Go avec des génériques ajoutés, afficherait sous forme de code Go. Mais l'emballage ne serait pas basé sur une délimitation telle que package, car cela n'aurait pas la composabilité que j'ai déjà mentionnée. Le fait est qu'il n'y a pas de raccourci vers un bon systÚme de types de génériques composables. Soit nous le faisons correctement, soit nous ne faisons rien, car l'ajout d'un hack non composable qui n'est pas vraiment générique finira par créer une inertie de clusterfuck de patchwork à moitié générique et d'irrégularité des cas d'angle et des solutions de contournement faisant du code de l'écosystÚme Go inintelligible.

Il est Ă©galement vrai que la plupart des gens qui Ă©crivent de grands programmes Go complexes ont
pas constaté de besoin significatif de génériques. Jusqu'à présent, c'était plus comme
une verrue irritante - la nécessité d'écrire trois lignes passe-partout pour
chaque type Ă  trier - plutĂŽt qu'un obstacle majeur Ă  l'Ă©criture utile
code.

Oui, cela a Ă©tĂ© l'une des pensĂ©es dans mon esprit, Ă  savoir si le passage Ă  un systĂšme de classe de types complet est justifiable. Si toutes vos bibliothĂšques sont basĂ©es autour de cela, alors apparemment cela pourrait ĂȘtre une belle harmonie, mais si nous envisageons l'inertie des hacks Go existants pour la gĂ©nĂ©ricitĂ©, alors peut-ĂȘtre que la synergie supplĂ©mentaire obtenue sera faible pour beaucoup de projets ?

Mais si un transpileur Ă  partir d'une syntaxe de classe de types Ă©mulait la maniĂšre manuelle existante dont Go peut modĂ©liser les gĂ©nĂ©riques (Modifier : ce que je viens de lire que @andrewcmyers dĂ©clare plausible ), cela pourrait ĂȘtre moins onĂ©reux et trouver des synergies utiles. Par exemple, j'ai rĂ©alisĂ© que deux classes de types de paramĂštres peuvent ĂȘtre Ă©mulĂ©es avec interface implĂ©mentĂ© sur un struct qui Ă©mule un tuple, ou @jba a mentionnĂ© une idĂ©e pour employer en ligne interface en contexte . Apparemment, struct sont structurellement plutĂŽt que nominalement typĂ©s Ă  moins qu'on leur donne un nom avec type ? De plus, j'ai confirmĂ© qu'une mĂ©thode d'un interface peut entrer un autre interface donc afaics il peut ĂȘtre possible de transpiler Ă  partir de HKT dans votre exemple de tri dont j'ai parlĂ© dans mon prĂ©cĂ©dent post ici. Mais j'ai besoin d'y rĂ©flĂ©chir davantage quand je n'ai pas si sommeil.

Je pense qu'il est juste de dire que la plupart des membres de l'Ă©quipe Go n'aiment pas C++
modÚles, dans lesquels un langage complet de Turing a été superposé
un autre langage complet de Turing tel que les deux langages aient
syntaxes complÚtement différentes, et les programmes dans les deux langages sont
écrites de maniÚre trÚs différente. Les modÚles C++ servent de mise en garde
conte parce que la mise en Ɠuvre complexe a imprĂ©gnĂ© l'ensemble
bibliothĂšque standard, faisant des messages d'erreur C++ une source de
Ă©merveillement et Ă©tonnement. Ce n'est pas un chemin que Go suivra jamais.

Je doute que quelqu'un soit en désaccord ! L'avantage de la monomorphisation est orthogonal aux inconvénients d'un moteur de métaprogrammation générique complet de Turing.

Au fait, l' erreur de conception des modĂšles C++ me semble ĂȘtre la mĂȘme essence gĂ©nĂ©rative que la faille des foncteurs ML gĂ©nĂ©ratifs (par opposition aux foncteurs applicatifs). Le principe de moindre puissance s'applique.


@ianlancetaylor a Ă©crit :

Il est décevant de voir Go devenir de plus en plus complexe en ajoutant des types paramétrés intégrés.

MalgrĂ© les spĂ©culations dans ce numĂ©ro, je pense que cela est extrĂȘmement peu probable.

J'espere. Je crois fermement que Go devrait soit ajouter un systÚme de génériques cohérent, soit simplement accepter qu'il n'y aura jamais de génériques.

Je pense qu'un fork vers un transpiler est plus susceptible de se produire, en partie parce que j'ai des fonds pour le mettre en Ɠuvre et que je suis intĂ©ressĂ© Ă  le faire. Pourtant, j'analyse toujours la situation.

Cela fracturerait l'Ă©cosystĂšme, mais au moins alors Go peut rester pur Ă  ses principes minimalistes. Ainsi, pour Ă©viter de fracturer l'Ă©cosystĂšme et permettre d' autres innovations que j'aimerais, je n'en ferais probablement pas un sur-ensemble et je l'appellerais plutĂŽt ZĂ©ro .

@pciet a Ă©crit :

Mon vote est non aux applications gĂ©nĂ©riques gĂ©nĂ©ralisĂ©es, oui Ă  des fonctions gĂ©nĂ©riques plus intĂ©grĂ©es telles que append et copy qui fonctionnent sur plusieurs types de base. Peut-ĂȘtre que sort et search pourraient ĂȘtre ajoutĂ©s pour les types de collection ?

L'expansion de cette inertie empĂȘchera peut-ĂȘtre une fonctionnalitĂ© gĂ©nĂ©rique complĂšte d'ĂȘtre intĂ©grĂ©e Ă  Go. Ceux qui voulaient des gĂ©nĂ©riques sont susceptibles de partir vers des pĂąturages plus verts. @andrewcmyers a rĂ©itĂ©rĂ© ceci :

Il serait ~is~ décevant de voir Go devenir de plus en plus complexe en ajoutant des types paramétrés intégrés. Il serait préférable d'ajouter simplement le support du langage pour que les programmeurs écrivent leurs propres types paramétrés.

@shelby3

Afaik, la seule façon de faire des conteneurs/collections hétérogÚnes dans Go maintenant est de subsumer tous les types dans une interface vide{} qui jette les informations de typage et devrais-je supposer nécessiter des casts et une inspection de type d'exécution, ce qui en quelque sorte2 va à l'encontre du point de typage statique.

Voir le modÚle de wrapper dans les commentaires ci-dessus pour la vérification de type statique des collections interface{} dans Go.

Le fait est qu'il n'y a pas de raccourci vers un bon systÚme de types de génériques composables. Soit on le fait correctement, soit on ne fait rien, car ajouter un hack non composable qui n'est pas vraiment générique


Pouvez-vous expliquer cela davantage? Pour le cas des types de collection, avoir une interface définissant le comportement générique nécessaire des éléments contenus semble raisonnable pour écrire des fonctions.

@pciet ce code fait littéralement exactement ce que @ shelby3 décrivait et envisageait un antipattern. Je te cite plus tÎt :

Cela donne dans la documentation et les exemples de test qui type wrapper pattern (merci @pierrre) pour la sécurité du type au moment de la compilation et a également la vérification de réflexion pour la sécurité du type au moment de l'exécution.

Vous prenez du code qui manque d'informations de type et, type par type, vous ajoutez des conversions et une inspection de type à l'exécution à l'aide de reflect. C'est exactement ce dont @ shelby3 se plaignait. J'ai tendance à appeler cette approche "monomorphisation à la main" et c'est exactement le genre de corvée fastidieuse que je pense qu'il vaut mieux confier à un compilateur.

Cette approche présente plusieurs inconvénients :

  • NĂ©cessite des wrappers type par type, maintenus soit Ă  la main, soit avec un outil de type go generate
  • (Si fait Ă  la main au lieu d'un outil) possibilitĂ© de faire des erreurs dans le passe-partout qui ne seront dĂ©tectĂ©es qu'au moment de l'exĂ©cution
  • NĂ©cessite une rĂ©partition dynamique au lieu d'une rĂ©partition statique, qui est Ă  la fois plus lente et utilise plus de mĂ©moire
  • Utilise la rĂ©flexion d'exĂ©cution plutĂŽt que les assertions de type au moment de la compilation, ce qui est Ă©galement lent
  • Non composable : agit entiĂšrement sur des types concrets sans possibilitĂ© d'utiliser des limites de type typeclass (ou mĂȘme de type interface) sur les types, Ă  moins que vous ne manipuliez une autre couche d'indirection pour chaque interface non vide sur laquelle vous souhaitez Ă©galement faire abstraction

Pouvez-vous expliquer cela davantage? Pour le cas des types de collection, avoir une interface définissant le comportement générique nécessaire des éléments contenus semble raisonnable pour écrire des fonctions.

DĂ©sormais, partout oĂč vous souhaitez utiliser une borne Ă  la place ou en plus d'un type concret, vous devez Ă©galement Ă©crire le mĂȘme passe-partout de vĂ©rification de type pour chaque type d'interface. Cela ne fait qu'aggraver l'explosion (peut-ĂȘtre combinatoire) des wrappers de type statique que vous devez Ă©crire.

Il y a aussi des idĂ©es qui, pour autant que je sache, ne peuvent tout simplement pas ĂȘtre exprimĂ©es dans le systĂšme de types de Go aujourd'hui, comme une limite sur une combinaison d'interfaces. Imaginons que nous ayons :

type Foo interface {
    ...
}

type Bar interface {
    ...
}

Comment exprimons-nous, en utilisant une vérification de type purement statique, que nous voulons un type qui implémente à la fois Foo et Bar ? Autant que je sache, cela n'est pas possible dans Go (à moins de recourir à des vérifications d'exécution qui peuvent échouer, en évitant la sécurité de type statique).

Avec un systÚme de génériques basé sur les classes de types, nous pourrions exprimer cela comme suit :

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

@tarcieri

Comment exprimons-nous, en utilisant une vérification de type purement statique, que nous voulons un type qui implémente à la fois Foo et Bar ?

simplement comme ceci :

type T interface {
    Foo
    Bar
}

func baz(t T) { ... }

@sbinet soigné, TIL

Personnellement, je considÚre la réflexion d'exécution comme une mauvaise fonctionnalité, mais ce n'est que moi ... Je peux expliquer pourquoi si quelqu'un est intéressé.

Je pense que toute personne implĂ©mentant des gĂ©nĂ©riques de quelque nature que ce soit devrait d'abord lire plusieurs fois "Elements of Programming" de Stepanov. Cela Ă©viterait beaucoup de problĂšmes non inventĂ©s ici et rĂ©inventerait la roue. AprĂšs avoir lu cela, il devrait ĂȘtre clair pourquoi "C++ Concepts" et "Haskell Typeclasses" sont la bonne façon de faire des gĂ©nĂ©riques.

Je vois que ce problĂšme semble Ă  nouveau actif
Voici un terrain de jeu de proposition d'homme de paille
https://go-li.github.io/test.html
collez simplement les programmes de démonstration d'ici
https://github.com/go-li/demo

Merci beaucoup pour votre Ă©valuation de ce paramĂštre unique
fonctions génériques.

Nous maintenons le gccgo piraté et
ce projet aurait été impossible sans vous, alors nous
voulait contribuer en retour.

Nous attendons également avec impatience les génériques que vous adopterez, continuez votre excellent travail !

@anlhord oĂč sont les dĂ©tails de mise en Ɠuvre Ă  ce sujet ? OĂč peut-on lire la syntaxe ? Qu'est-ce qui est implĂ©mentĂ© ? Qu'est-ce qui n'est pas implĂ©mentĂ© ? Quelles sont les spĂ©cifications de ces implĂ©mentations ? Quels en sont les avantages et les inconvĂ©nients ?

Le lien du terrain de jeu en contient le pire exemple possible :

package main

import "fmt"

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

Ce code ne me dit rien sur la façon de l'utiliser et que puis-je tester.

Si vous pouviez améliorer ces choses, cela aiderait à mieux comprendre quelle est votre proposition et comment elle se compare aux précédentes / voir comment les autres points soulevés ici s'y appliquent ou non.

J'espĂšre que cela vous aide Ă  comprendre les problĂšmes avec votre commentaire.

@joho a Ă©crit :

La littérature universitaire aurait-elle de la valeur pour des conseils sur l'évaluation des approches ?

Le seul article que j'ai lu sur le sujet est Les développeurs bénéficient-ils des types génériques ? (paywall désolé, vous pourriez chercher sur Google votre chemin vers un téléchargement pdf) qui avait ce qui suit à dire

Par conséquent, une interprétation conservatrice de l'expérience
est que les types gĂ©nĂ©riques peuvent ĂȘtre considĂ©rĂ©s comme un compromis
entre les caractéristiques positives de la documentation et
caractéristiques d'extensibilité négatives.

Je présume que la POO et les sous-classes (par exemple, les classes en Java et C++) ne seront pas prises au sérieux car Go a déjà une classe de type interface (sans le paramÚtre de type générique explicite T ), Java est cité comme ce qu'il ne faut pas copier, et parce que beaucoup ont fait valoir qu'ils sont un anti-modÚle. Upthread J'ai lié à une partie de cet argument. Nous pourrions approfondir cette analyse si quelqu'un est intéressé.

Je n'ai pas encore Ă©tudiĂ© de recherches plus rĂ©centes telles que le systĂšme Genus mentionnĂ© upthread . Je me mĂ©fie des systĂšmes "d'Ă©vier de cuisine" qui tentent de mĂ©langer tant de paradigmes (par exemple sous-classement, hĂ©ritage multiple, OOP, linĂ©arisation des traits, implicit , classes de types, types abstraits, etc.), en raison des plaintes concernant Scala avoir autant de cas d'angle dans la pratique, bien que cela s'amĂ©liorera peut-ĂȘtre avec Scala 3 (alias Dotty et le calcul DOT). Je suis curieux de savoir si leur tableau de comparaison se compare Ă  Scala 3 expĂ©rimental ou Ă  la version actuelle de Scala ?

Donc afaics, ce qui reste sont les foncteurs ML et les classes de types Haskell en termes de systÚmes de généricité éprouvés, qui améliorent considérablement l'extensibilité et la flexibilité par rapport à la sous-classification OOP +.

J'ai Ă©crit une partie de la discussion privĂ©e @keean et j'ai eu Ă  propos des modules de foncteurs ML par rapport aux classes de types. Les points forts semblent ĂȘtre :

  • les classes de type _modĂ©lisent une algĂšbre_ (mais sans les axiomes vĂ©rifiĂ©s ) et implĂ©mentent chaque type de donnĂ©es pour chaque interface d'une seule maniĂšre. Permettant ainsi une sĂ©lection implicite des implĂ©mentations par le compilateur sans annotation sur le site d'appel.

  • Les foncteurs applicatifs ont une transparence rĂ©fĂ©rentielle alors que les foncteurs gĂ©nĂ©ratifs crĂ©ent une nouvelle instance Ă  chaque instanciation, ce qui signifie qu'ils ne sont pas invariants Ă  l'ordre d'initialisation.

  • Les foncteurs ML sont plus puissants/flexibles que les classes de types, mais cela se fait au prix de plus d'annotations et potentiellement plus d'interactions de cas extrĂȘmes. Et selon @keean, ils nĂ©cessitent des types dĂ©pendants (pour les types associĂ©s ) qui sont un systĂšme de type plus complexe. @keean pense que l'_expression de la gĂ©nĂ©ricitĂ© de Stepanov en tant qu'algĂšbre_ plus les classes de types est suffisamment puissante et flexible , ce qui semble ĂȘtre le point idĂ©al pour une gĂ©nĂ©ricitĂ© de pointe et bien Ă©prouvĂ©e (dans Haskell et maintenant dans Rust). Cependant, les axiomes ne sont pas appliquĂ©s par les classes de types.

  • J'ai suggĂ©rĂ© d'ajouter des unions pour les conteneurs hĂ©tĂ©rogĂšnes avec des classes de types Ă  Ă©tendre le long d'un autre axe du problĂšme d'expression, bien que cela nĂ©cessite l'immuabilitĂ© ou la copie (uniquement pour les cas oĂč l'extensibilitĂ© hĂ©tĂ©rogĂšne est utilisĂ©e) qui est connue pour avoir un O(log n) ralentissement par rapport Ă  l'impĂ©rativitĂ© mutable effrĂ©nĂ©e.

@larsth a Ă©crit :

Il pourrait ĂȘtre intĂ©ressant d'avoir un ou plusieurs transpileurs expĂ©rimentaux - un code source gĂ©nĂ©rique Go vers un compilateur de code source Go 1.xy.

PS Je doute que Go adopte un systĂšme de typage aussi sophistiquĂ©, mais j'envisage un transpiler Ă  la syntaxe Go existante comme je l'ai mentionnĂ© dans mon post prĂ©cĂ©dent (voir la modification en bas). Et je veux un systĂšme gĂ©nĂ©rique robuste avec ces fonctionnalitĂ©s Go trĂšs souhaitables. Les gĂ©nĂ©riques Typeclass sur Go semblent ĂȘtre ce que je veux.

@bcmills a écrit à propos de sa proposition sur les fonctions de compilation pour la généricité :

J'ai entendu le mĂȘme argument utilisĂ© pour prĂ©coniser l'exportation de types interface plutĂŽt que de types concrets dans les API Go, et l'inverse s'avĂšre plus courant : l'abstraction prĂ©maturĂ©e surcontraint les types et entrave l'extension des API. (Pour un tel exemple, voir # 19584.) Si vous voulez vous appuyer sur cette ligne d'argumentation, je pense que vous devez fournir des exemples concrets.

Il est certainement vrai que les abstractions du systĂšme de types abandonnent nĂ©cessairement certains degrĂ©s de libertĂ© et parfois nous avons Ă©liminĂ© ces contraintes avec "unsafe" (c'est-Ă -dire en violation de l'abstraction vĂ©rifiĂ©e statiquement), mais cela doit ĂȘtre Ă©changĂ© contre les avantages de dĂ©couplage modulaire avec des invariants succinctement annotĂ©s.

Lors de la conception d'un systÚme pour la généricité, nous souhaitons probablement augmenter la régularité et la prévisibilité de l'écosystÚme comme l'un des principaux objectifs, en particulier si la philosophie de base de Go est prise en compte (par exemple, les programmeurs moyens sont une priorité).

Le principe de moindre puissance s'applique. La puissance/flexibilitĂ© des invariants "cachĂ©s dans" les fonctions de compilation pour la gĂ©nĂ©ricitĂ© doit ĂȘtre mise en balance avec leur capacitĂ© Ă  nuire, par exemple, Ă  la lisibilitĂ© du code source dans l'Ă©cosystĂšme (oĂč le dĂ©couplage modulaire est extrĂȘmement important car le lecteur ne pas besoin de lire une quantitĂ© de code potentiellement illimitĂ©e en raison de dĂ©pendances transitives implicites, afin de comprendre un module/package donné !). La rĂ©solution implicite des instances d'implĂ©mentation de classe de types prĂ©sente ce problĂšme si leur algĂšbre n'est pas respectĂ©e .

Bien sûr, mais c'est déjà vrai pour de nombreuses contraintes implicites dans Go, indépendamment de tout mécanisme de programmation générique.

Par exemple, une fonction peut recevoir un paramĂštre de type interface et appeler initialement ses mĂ©thodes sĂ©quentiellement. Si cette fonction change ultĂ©rieurement pour appeler ces mĂ©thodes simultanĂ©ment (en engendrant des goroutines supplĂ©mentaires), la contrainte "doit ĂȘtre sĂ»re pour une utilisation simultanĂ©e" n'est pas reflĂ©tĂ©e dans le systĂšme de type.

Mais autant que je sache, Go n'a pas tentĂ© de concevoir une abstraction pour modulariser ces effets. Rust a une telle abstraction (ce qui, je pense, est exagĂ©rĂ© pita/tsuris/limitant pour certains/la plupart des cas d'utilisation et je plaide pour une abstraction de modĂšle Ă  un seul thread plus facile, mais malheureusement, Go ne prend pas en charge la restriction de toutes les goroutines gĂ©nĂ©rĂ©es au mĂȘme thread ) . Et Haskell nĂ©cessite un contrĂŽle monadique sur les effets en raison de l'application de fonctions pures pour la transparence rĂ©fĂ©rentielle .


@alerca a Ă©crit :

Je pense que le plus gros inconvénient des contraintes inférées est qu'elles facilitent l'utilisation d'un type d'une maniÚre qui introduit une contrainte sans la comprendre pleinement. Dans le meilleur des cas, cela signifie simplement que vos utilisateurs peuvent rencontrer des échecs de compilation inattendus, mais dans le pire des cas, cela signifie que vous pouvez casser le package pour les consommateurs en introduisant une nouvelle contrainte par inadvertance. Des contraintes explicitement spécifiées éviteraient cela.

D'accord. Être capable de casser subrepticement du code dans d'autres modules parce que les invariants des types ne sont pas explicitement annotĂ©s est extrĂȘmement insidieux.


@andrewcmyers a Ă©crit :

Pour ĂȘtre clair, je ne considĂšre pas que la gĂ©nĂ©ration de code de style macro, qu'elle soit effectuĂ©e avec gen, cpp, gofmt -r ou d'autres outils de macro/modĂšle, soit une bonne solution au problĂšme des gĂ©nĂ©riques, mĂȘme si elle est standardisĂ©e. Il a les mĂȘmes problĂšmes que les templates C++ : gonflement du code, manque de vĂ©rification de type modulaire et difficultĂ© de dĂ©bogage. Cela s'aggrave au fur et Ă  mesure que vous commencez, comme c'est naturel, en construisant du code gĂ©nĂ©rique en termes d'autre code gĂ©nĂ©rique. À mon avis, les avantages sont limitĂ©s : cela rendrait la vie relativement simple aux auteurs du compilateur Go et cela produirait un code efficace - Ă  moins qu'il n'y ait une pression sur le cache d'instructions, une situation frĂ©quente dans les logiciels modernes !

@keean semble ĂȘtre d'accord avec vous.

@ shelby3 merci pour les commentaires. Pouvez-vous la prochaine fois faire les commentaires/modifications directement dans le document lui-mĂȘme. Il est plus facile de suivre oĂč les choses doivent ĂȘtre corrigĂ©es et plus facile de s'assurer que toutes les notes obtiennent une rĂ©ponse appropriĂ©e.

La section Présentation semble impliquer que l'utilisation universelle par Java des références de boxe pour les instances ...

Ajout d'un commentaire pour préciser qu'il ne s'agit pas d'une liste exhaustive. C'est principalement là pour que les gens comprennent l'essentiel des différents compromis. La liste complÚte des différentes approches est présentée ci-dessous.

Les structures génériques ont tendance à accumuler des fonctionnalités de toutes les utilisations, ce qui entraßne une augmentation des temps de compilation ou un gonflement du code ou nécessite un éditeur de liens plus intelligent.
Pour les classes de types, ce n'est pas vrai ou cela pose moins de problĂšme, car les interfaces ne doivent ĂȘtre implĂ©mentĂ©es que pour les types de donnĂ©es qui sont fournis aux fonctions qui utilisent ces interfaces. Les classes de type concernent la liaison tardive de l'implĂ©mentation Ă  l'interface, contrairement Ă  la POO qui lie chaque type de donnĂ©es Ă  ses mĂ©thodes pour l'implĂ©mentation de la classe.

Cette déclaration concerne ce qui arrive aux structures de données génériques à long terme. En d'autres termes, une structure de données générique finit souvent par collecter toutes les utilisations différentes - plutÎt que d'avoir plusieurs implémentations plus petites à des fins différentes. Juste à titre d'exemple, regardez https://www.scala-lang.org/api/2.12.3/scala/collection/immutable/List.html.

Il est important de noter que la "conception mĂ©canique" et "autant de flexibilitĂ©" ne suffisent pas pour crĂ©er une bonne "solution gĂ©nĂ©rique". Il a Ă©galement besoin de bonnes instructions, de la maniĂšre dont les choses doivent ĂȘtre utilisĂ©es et de ce qu'il faut Ă©viter, et de la maniĂšre dont les gens finissent par l'utiliser.

Les structures gĂ©nĂ©riques et les API qui fonctionnent dessus ont tendance Ă  ĂȘtre plus abstraites que les API spĂ©cialement conçues...

Un contre-argument qui, Ă  mon avis, attĂ©nue considĂ©rablement cette prĂ©occupation est que le fardeau cognitif de l'apprentissage d'un nombre illimitĂ© de rĂ©implĂ©mentations de cas particuliers des mĂȘmes algorithmes gĂ©nĂ©riques, est illimitĂ© ...

Ajout d'une note sur la charge cognitive de nombreuses API similaires.

Les réimplémentations de cas particuliers ne sont pas illimitées dans la pratique. Vous ne verrez qu'un nombre fixe de spécialisations.

Ce n'est pas une escroquerie valide.

Vous pouvez ĂȘtre en dĂ©saccord avec certains points, je suis en dĂ©saccord avec un certain nombre d'entre eux dans une certaine mesure, mais je comprends leur point de vue et j'essaie de comprendre les problĂšmes auxquels les gens sont confrontĂ©s au quotidien. Le but du document est de recueillir des opinions diffĂ©rentes, pas de juger "Ă  quel point quelque chose est ennuyeux pour quelqu'un".

Cependant, le document prend position sur les "problÚmes liés aux problÚmes du monde réel", car les problÚmes abstraits et facilités dans les forums ont tendance à se transformer en bavardage sans signification sans qu'aucune compréhension ne soit construite.

Ce que nous voulons vraiment dire ici, c'est la régularité d'un langage et l'aide des API génériques, pas de mal à cela.

Bien sûr, dans la pratique, vous n'aurez besoin de ce style d'optimisation que pour moins de 1% des cas.

Solutions alternatives:

Les solutions alternatives ne sont pas censées se substituer aux génériques. Mais plutÎt une liste de solutions potentielles pour différents types de problÚmes.

ModĂšles de package

Je peux me tromper mais cela ne semble pas tout à fait correct. Les foncteurs ML (à ne pas confondre avec les foncteurs FP) peuvent également renvoyer une sortie qui reste paramétrée par type.

Pouvez-vous fournir une formulation plus claire et, si nécessaire, scinder en deux approches différentes ?

@egonelbre merci également d'avoir répondu afin que je puisse savoir sur quels points je dois clarifier davantage ma pensée.

Pouvez-vous la prochaine fois faire les commentaires/modifications directement dans le document lui-mĂȘme.

Mes excuses, j'aimerais pouvoir me conformer, mais je n'ai jamais utilisé les fonctionnalités de discussion de Google Doc, je n'ai pas le temps de l'apprendre, et je préfÚre également pouvoir créer un lien vers mes discussions sur Github pour référence future.

Juste Ă  titre d'exemple, regardez https://www.scala-lang.org/api/2.12.3/scala/collection/immutable/List.html.

La conception de la bibliothÚque des collections Scala a été critiquée par de nombreuses personnes, dont l'un des anciens membres clés de l'équipe . Un commentaire posté sur LtU est représentatif. Notez que j'ai ajouté ce qui suit à l'un de mes messages précédents dans ce fil pour résoudre ce problÚme :

Je me mĂ©fie des systĂšmes "d'Ă©vier de cuisine" qui tentent de mĂ©langer tant de paradigmes (par exemple sous-classement, hĂ©ritage multiple, POO, linĂ©arisation des traits, implicit , classes de types, types abstraits, etc.), en raison des plaintes concernant Scala avoir autant de cas d'angle dans la pratique, bien que cela s'amĂ©liorera peut-ĂȘtre avec Scala 3 (alias Dotty et le calcul DOT).

Je ne pense pas que la bibliothĂšque de collections de Scala soit reprĂ©sentative des bibliothĂšques crĂ©Ă©es pour un PL avec uniquement des classes de types pour le polymorphisme. Afair, les collections Scala utilisent l' anti-modĂšle d'hĂ©ritage , qui a causĂ© les hiĂ©rarchies complexes, combinĂ© avec des aides implicit telles que CanBuildFrom qui ont fait exploser le budget de complexitĂ©. Et je pense que si le point de @keean est respectĂ© sur le fait que _Elements of Programming_ de Stepanov est une algĂšbre , une Ă©lĂ©gante bibliothĂšque de collections pourrait ĂȘtre crĂ©Ă©e. C'Ă©tait la premiĂšre alternative que j'avais vue Ă  une bibliothĂšque de collections basĂ©e sur un foncteur (FP) (c'est-Ă -dire ne copiant pas Haskell ) Ă©galement basĂ©e sur les mathĂ©matiques. Je veux voir cela dans la pratique, ce qui est l'une des raisons pour lesquelles je collabore/discute avec lui sur la conception d'un nouveau PL. Et Ă  partir de ce moment, je prĂ©vois de faire d'abord transpiler cette langue en Go (bien que j'essaie depuis des annĂ©es de trouver un moyen d'Ă©viter de le faire). J'espĂšre donc que nous pourrons bientĂŽt expĂ©rimenter pour voir comment cela fonctionne.

Ma perception est que la communautĂ©/philosophie Go prĂ©fĂšre attendre de voir ce qui fonctionne dans la pratique et l'adopter plus tard une fois prouvĂ©, que de se prĂ©cipiter et de polluer le langage avec des expĂ©riences ratĂ©es. Parce que, comme vous l'avez rĂ©pĂ©tĂ©, toutes ces affirmations abstraites ne sont pas si constructives (sauf peut-ĂȘtre pour les thĂ©oriciens de la conception PL). De plus, il est probablement invraisemblable de concevoir un systĂšme de gĂ©nĂ©riques cohĂ©rent par comitĂ©.

Il a Ă©galement besoin de bonnes instructions, de la maniĂšre dont les choses doivent ĂȘtre utilisĂ©es et de ce qu'il faut Ă©viter, et de la maniĂšre dont les gens finissent par l'utiliser.

Et je pense que cela aidera Ă  ne pas mĂ©langer autant de paradigmes diffĂ©rents disponibles pour le programmeur dans le mĂȘme langage. Ce n'est apparemment pas nĂ©cessaire ( @keean et moi devons prouver cette affirmation). Je pense que nous adhĂ©rons tous les deux Ă  la philosophie selon laquelle le budget de complexitĂ© est fini et c'est ce que vous laissez en dehors du PL qui est aussi important que les fonctionnalitĂ©s incluses.

Cependant, le document prend position sur les "problÚmes liés aux problÚmes du monde réel", car les problÚmes abstraits et facilités dans les forums ont tendance à se transformer en bavardage sans signification sans qu'aucune compréhension ne soit construite.

D'accord. Et il est également difficile pour tout le monde de suivre les points abstraits. Le diable est dans les détails et les résultats réels dans la nature.

Bien sûr, dans la pratique, vous n'aurez besoin de ce style d'optimisation que pour moins de 1% des cas.

Go a dĂ©jĂ  interface pour la gĂ©nĂ©ricitĂ©, ce qui permet de gĂ©rer les cas oĂč le polymorphisme paramĂ©trique n'est pas nĂ©cessaire sur le type T pour l'instance de l'interface fournie par le site d'appel.

Je pense avoir lu quelque part, peut-ĂȘtre Ă©tait-il en amont, l'argument selon lequel la bibliothĂšque standard de Go souffre en fait d'une incohĂ©rence dans l'utilisation optimale des idiomes les plus mis Ă  jour. Je ne sais pas si c'est vrai, car je n'ai pas encore d'expĂ©rience avec Go. Ce que je veux dire, c'est que le paradigme des gĂ©nĂ©riques choisi infecte toutes les bibliothĂšques. Alors oui, Ă  partir de maintenant, vous pouvez prĂ©tendre que seulement 1% du code en aurait besoin, car il y a dĂ©jĂ  une inertie dans les idiomes qui Ă©vitent le besoin de gĂ©nĂ©riques.

Vous avez peut-ĂȘtre raison. J'ai aussi mon scepticisme quant Ă  la mesure dans laquelle j'utiliserai une fonctionnalitĂ© linguistique particuliĂšre. Je pense que l'expĂ©rimentation pour dĂ©couvrir est la voie que je vais procĂ©der. La conception PL est un processus itĂ©ratif, alors le problĂšme est de lutter contre l'inertie qui se dĂ©veloppe rend difficile l'itĂ©ration du processus. Je suppose donc que Rob Pike a raison dans la vidĂ©o oĂč il suggĂšre d'Ă©crire des programmes qui Ă©crivent du code pour les programmes (c'est-Ă -dire des outils de gĂ©nĂ©ration d'Ă©criture et des transpilers) pour expĂ©rimenter et tester des idĂ©es.

Lorsque nous pouvons montrer qu'un ensemble particulier de fonctionnalitĂ©s est supĂ©rieur dans la pratique (et, espĂ©rons-le, Ă©galement dans la popularitĂ© d'utilisation) Ă  ceux actuellement en Go, alors nous pouvons peut-ĂȘtre voir une forme de consensus autour de leur ajout Ă  Go. J'encourage les autres Ă  crĂ©er Ă©galement des systĂšmes expĂ©rimentaux qui se transpilent au Go.

Pouvez-vous fournir une formulation plus claire et, si nécessaire, scinder en deux approches différentes ?

J'ajoute ma voix Ă  ceux qui voudraient dĂ©courager la tentative de mettre une fonctionnalitĂ© de modĂ©lisation trop simpliste dans Go et prĂ©tendre qu'il s'agit de gĂ©nĂ©riques. IOW, je pense qu'un systĂšme de gĂ©nĂ©riques fonctionnant correctement et qui ne finira pas par ĂȘtre une mauvaise inertie est fondamentalement incompatible avec le dĂ©sir d'avoir une conception trop simpliste pour les gĂ©nĂ©riques. Afaik, un systĂšme de gĂ©nĂ©riques a besoin d'une conception holistique bien pensĂ©e et Ă©prouvĂ©e. Faisant Ă©cho Ă  ce que @larsth a Ă©crit , j'encourage ceux qui ont des propositions sĂ©rieuses Ă  d'abord construire un transpiler (ou Ă  implĂ©menter dans un fork de l'interface gccgo) puis Ă  expĂ©rimenter la proposition afin que nous puissions tous mieux comprendre ses limites. J'ai Ă©tĂ© encouragĂ© Ă  lire en amont que @ianlancetaylor ne pensait pas qu'une pollution de mauvaise inertie serait ajoutĂ©e au Go. En ce qui concerne ma plainte spĂ©cifique concernant la proposition de paramĂ©trage au niveau du package, ma suggestion pour celui qui la propose, veuillez envisager de crĂ©er un compilateur que nous pouvons tous utiliser pour jouer avec, puis nous pouvons tous parler d'exemples de ce que nous aimons et ne faisons pas ' Je n'aime pas ça. Sinon, nous nous parlons parce que je ne comprends peut-ĂȘtre mĂȘme pas correctement la proposition telle qu'elle est dĂ©crite de maniĂšre abstraite. Je ne dois pas comprendre la proposition, car je ne comprends pas comment le package paramĂ©trĂ© peut ĂȘtre rĂ©utilisĂ© dans un autre package Ă©galement paramĂ©trĂ©. IOW, si un package prend des paramĂštres, il doit Ă©galement instancier d'autres packages avec des paramĂštres. Mais il semblait que la proposition indiquait que la seule façon d'instancier un package paramĂ©trĂ© Ă©tait avec un type concret, pas avec des paramĂštres de type.

Des excuses si interminables. Je veux m'assurer que je ne suis pas mal compris.

@ shelby3 ah, j'ai alors mal compris la plainte initiale. Tout d'abord, je dois préciser que les sections des "Approches génériques" ne sont pas des propositions concrÚtes. Ce sont des approches ou, en d'autres termes, des décisions de conception plus importantes que l'on pourrait prendre dans une approche générique concrÚte. Cependant, les regroupements sont fortement motivés par des implémentations existantes ou des propositions concrÚtes/informelles. De plus, je soupçonne qu'il manque encore au moins 5 grandes idées dans cette liste.

Pour l'approche "modÚles de packages", il existe deux variantes (voir les discussions liées dans le document):

  1. packages génériques basés sur "l'interface",
  2. paquets explicitement génériques.

Pour 1. il n'est pas nĂ©cessaire que le package gĂ©nĂ©rique fasse quoi que ce soit de spĂ©cial - par exemple, le container/ring actuel deviendrait utilisable pour la spĂ©cialisation. Imaginez la "spĂ©cialisation" ici comme le remplacement de toutes les instances de l'interface dans le package par le type concret (et en ignorant les importations circulaires). Lorsque ce paquet lui-mĂȘme spĂ©cialise un autre paquet, il peut utiliser "l'interface" comme spĂ©cialisation - il s'ensuit que cette utilisation sera Ă©galement spĂ©cialisĂ©e.

Pour 2. vous pouvez les regarder de deux maniÚres. L'un est la spécialisation concrÚte récursive à chaque importation - similaire à la modélisation/macroisation, à aucun moment il n'y aurait de "paquet partiellement appliqué". Bien sûr, on peut également voir, du cÎté fonctionnel, que le package générique est un partiel avec des paramÚtres, puis vous le spécialisez.

Donc, oui, vous pouvez utiliser un package paramétré dans un autre.

Faisant écho à ce que @larsth a écrit, j'encourage ceux qui ont des propositions sérieuses à d'abord construire un transpiler (ou à implémenter dans un fork de l'interface gccgo) puis à expérimenter la proposition afin que nous puissions tous mieux comprendre ses limites.

Je sais que ce n'était pas explicitement dirigé vers cette approche, mais il y a 4 prototypes différents pour tester l'idée. Bien sûr, ce ne sont pas des transpilateurs complets, mais ils sont suffisants pour tester quelques idées. c'est-à-dire que je ne suis pas sûr que quelqu'un ait implémenté le cas "utiliser un package paramétré d'un autre".

Les packages paramĂ©trĂ©s ressemblent beaucoup Ă  des modules ML (et les foncteurs ML sont les paramĂštres peuvent ĂȘtre d'autres packages). Il y a deux maniĂšres dont ceux-ci peuvent fonctionner "applicatif" ou "gĂ©nĂ©ratif". Un foncteur applicatif est comme une valeur ou un alias de type. Un foncteur gĂ©nĂ©ratif doit ĂȘtre construit et chaque instance est diffĂ©rente. Une autre façon de penser Ă  cela est que pour qu'un package soit applicatif, il doit ĂȘtre pur (c'est-Ă -dire qu'il n'y a pas de variables mutables au niveau du package). S'il y a un Ă©tat au niveau du package, il doit ĂȘtre gĂ©nĂ©ratif car cet Ă©tat doit ĂȘtre initialisĂ©, et il importe quelle "instance" d'un package gĂ©nĂ©ratif vous transmettez rĂ©ellement en tant que paramĂštre Ă  d'autres packages qui Ă  leur tour doivent ĂȘtre gĂ©nĂ©ratifs. Par exemple, les packages Ada sont gĂ©nĂ©ratifs.

Le problĂšme avec l'approche de package gĂ©nĂ©ratif est qu'elle crĂ©e beaucoup de passe-partout, oĂč vous instanciez des packages avec des paramĂštres. Vous pouvez regarder les gĂ©nĂ©riques d'Ada pour voir Ă  quoi cela ressemble.

Les classes de type Ă©vitent ce passe-partout en sĂ©lectionnant implicitement la classe de type en fonction des types utilisĂ©s dans la fonction uniquement. Vous pouvez Ă©galement voir les classes de types comme une surcharge contrainte avec une distribution multiple, oĂč la rĂ©solution de surcharge se produit presque toujours de maniĂšre statique au moment de la compilation, avec des exceptions pour la rĂ©cursivitĂ© polymorphe et les types existentiels (qui sont essentiellement des variantes que vous ne pouvez pas chasser, vous ne pouvez utiliser les interfaces avec lesquelles la variante confirme).

Un foncteur applicatif est comme une valeur ou un alias de type. Un foncteur gĂ©nĂ©ratif doit ĂȘtre construit et chaque instance est diffĂ©rente. Une autre façon de penser Ă  cela est que pour qu'un package soit applicatif, il doit ĂȘtre pur (c'est-Ă -dire qu'il n'y a pas de variables mutables au niveau du package). S'il y a un Ă©tat au niveau du package, il doit ĂȘtre gĂ©nĂ©ratif car cet Ă©tat doit ĂȘtre initialisĂ©, et il importe quelle "instance" d'un package gĂ©nĂ©ratif vous transmettez rĂ©ellement en tant que paramĂštre Ă  d'autres packages qui Ă  leur tour doivent ĂȘtre gĂ©nĂ©ratifs. Par exemple, les packages Ada sont gĂ©nĂ©ratifs.

Merci pour la terminologie exacte, je dois réfléchir à la façon d'intégrer ces idées dans le document.

De plus, je ne vois pas pourquoi vous ne pourriez pas avoir un "alias de type automatique vers un package généré" - en quelque sorte quelque chose entre l'approche "foncteur applicatif" et "foncteur génératif". De toute évidence, lorsque le package contient une forme d'état, il peut devenir compliqué à déboguer et à comprendre.

Le problĂšme avec l'approche de package gĂ©nĂ©ratif est qu'elle crĂ©e beaucoup de passe-partout, oĂč vous instanciez des packages avec des paramĂštres. Vous pouvez regarder les gĂ©nĂ©riques d'Ada pour voir Ă  quoi cela ressemble.

Autant que je sache, cela créerait moins de passe-partout que les modÚles C++ mais plus que les classes de type. Avez-vous un bon programme du monde réel pour Ada qui illustre le problÚme ? _(Par monde réel, je veux dire le code que quelqu'un utilise/utilisait en production.)_

Bien sĂ»r, jetez un Ɠil Ă  mon go-board Ada : https://github.com/keean/Go-Board-Ada/blob/master/go.adb

Bien qu'il s'agisse d'une définition assez vague de la production, le code est optimisé, fonctionne aussi bien que la version C++ et son open-source, et l'algorithme a été affiné sur plusieurs années. Vous pouvez également consulter la version C++ : https://github.com/keean/Go-Board/blob/master/go.cpp

Cela montre (je pense) que les génériques Ada sont une solution plus soignée que les modÚles C++ (mais ce n'est pas difficile), d'autre part, il est difficile de faire l'accÚs rapide aux structures de données dans Ada en raison des restrictions sur le retour d'une référence .

Si vous voulez regarder un systĂšme de gĂ©nĂ©riques de paquets pour un langage impĂ©ratif, je pense qu'Ada est l'un des meilleurs Ă  regarder. C'est dommage qu'ils aient dĂ©cidĂ© d'aller multi-paradigmes et d'ajouter tous les trucs OO Ă  Ada. Ada est un Pascal amĂ©liorĂ©, et Pascal Ă©tait un langage petit et Ă©lĂ©gant. Les gĂ©nĂ©riques Pascal plus Ada auraient Ă©tĂ© encore un tout petit langage, mais auraient Ă©tĂ© bien meilleurs Ă  mon avis. Parce que l'accent d'Ada s'est dĂ©placĂ© vers une approche OO, trouver une bonne documentation et des exemples sur la façon de faire les mĂȘmes choses avec des gĂ©nĂ©riques semble difficile Ă  trouver.

Bien que je pense que les classes de types ont certains avantages, je pourrais vivre avec les gĂ©nĂ©riques de style Ada, il y a quelques problĂšmes qui m'empĂȘchent d'utiliser Ada plus largement, je pense que les valeurs/objets sont erronĂ©s (je pense que trĂšs peu de langages obtiennent ce droit, 'C' Ă©tant l'un des seuls), il est difficile de travailler avec des pointeurs (variables d'accĂšs) et de crĂ©er des abstractions de pointeur sĂ»r, et il ne fournit pas un moyen d'utiliser des packages avec un polymorphisme d'exĂ©cution (il fournit un modĂšle d'objet pour cela, mais cela ajoute un tout nouveau paradigme au lieu d'essayer de trouver un moyen d'avoir un polymorphisme d'exĂ©cution Ă  l'aide de packages).

La solution au polymorphisme d'exĂ©cution est de crĂ©er des packages de premiĂšre classe afin que les instances de signatures de packages puissent ĂȘtre transmises en tant qu'arguments de fonction, cela nĂ©cessite malheureusement des types dĂ©pendants (voir le travail effectuĂ© sur les types d'objets dĂ©pendants pour Scala pour Ă©liminer le gĂąchis qu'ils ont crĂ©Ă© avec leur systĂšme de type d'origine).

Je pense donc que les gĂ©nĂ©riques de packages peuvent fonctionner, mais il a fallu des dĂ©cennies Ă  Ada pour traiter tous les cas extrĂȘmes, donc je regarderais un systĂšme de gĂ©nĂ©riques de production pour voir quels raffinements utilisent dans la production produite. Cependant, Ada est toujours en deçà car les packages ne sont pas de premiĂšre classe et ne peuvent pas ĂȘtre utilisĂ©s dans le polymorphisme d'exĂ©cution, et cela devrait ĂȘtre rĂ©solu.

@keean a Ă©crit :

Personnellement, je considÚre la réflexion d'exécution comme une mauvaise fonctionnalité, mais ce n'est que moi ... Je peux expliquer pourquoi si quelqu'un est intéressé.

L'effacement de type active "Theorems for free", ce qui a des implications pratiques . La rĂ©flexion d'exĂ©cution inscriptible (et peut-ĂȘtre mĂȘme lisible en raison de relations transitives avec le code impĂ©ratif ?) rend impossible de garantir la transparence rĂ©fĂ©rentielle dans n'importe quel code et donc certaines optimisations du compilateur ne sont pas possibles et les monades sĂ©curisĂ©es de type ne sont pas possibles. Je me rends compte que Rust n'a mĂȘme pas encore de fonctionnalitĂ© d'immuabilitĂ©. OTOH, la rĂ©flexion permet d'autres optimisations qui ne seraient autrement pas possibles si elles ne pouvaient pas ĂȘtre typĂ©es statiquement.

J'avais également indiqué upthread:

Et c'est ce qu'un compilateur transpilant à partir d'un sur-ensemble de Go avec des génériques ajoutés, afficherait sous forme de code Go. Mais l'emballage ne serait pas basé sur une délimitation telle que package, car cela n'aurait pas la composabilité que j'ai déjà mentionnée. Le fait est qu'il n'y a pas de raccourci vers un bon systÚme de types de génériques composables. Soit nous le faisons correctement, soit nous ne faisons rien, car l'ajout d'un hack non composable qui n'est pas vraiment générique finira par créer une inertie de clusterfuck de patchwork à moitié générique et d'irrégularité des cas d'angle et des solutions de contournement faisant du code de l'écosystÚme Go inintelligible.


@keean a Ă©crit :

[
] pour qu'un package soit applicatif, il doit ĂȘtre pur (c'est-Ă -dire qu'il n'y a pas de variables mutables au niveau du package)

Et aucune fonction impure ne peut ĂȘtre employĂ©e pour initialiser des variables immuables.

@egonelbre a Ă©crit :

Donc, oui, vous pouvez utiliser un package paramétré dans un autre.

Ce que j'avais apparemment à l'esprit, c'était des "paquets paramétrés de premiÚre classe" et le polymorphisme d'exécution proportionnel (alias dynamique) que @keean a mentionné par la suite, car je supposais que les packages paramétrés étaient proposés à la place des classes de type ou de la POO.

EDIT: mais il y a deux significations possibles pour les modules "de premiÚre classe": les modules en tant que valeurs de premiÚre classe comme dans Successor ML et MixML distingués des modules en tant que valeurs de premiÚre classe avec des types de premiÚre classe comme dans 1ML, et le compromis nécessaire dans la récursivité du module (c'est-à-dire le mélange ) entre eux.

@keean a Ă©crit :

La solution au polymorphisme d'exĂ©cution est de crĂ©er des packages de premiĂšre classe afin que les instances de signatures de packages puissent ĂȘtre transmises en tant qu'arguments de fonction, cela nĂ©cessite malheureusement des types dĂ©pendants (voir le travail effectuĂ© sur les types d'objets dĂ©pendants pour Scala pour Ă©liminer le gĂąchis qu'ils ont crĂ©Ă© avec leur systĂšme de type d'origine).

Qu'entendez-vous par types dĂ©pendants ? (EDIT: je suppose maintenant qu'il voulait dire typage "non dĂ©pendant de la valeur", c'est-Ă -dire " fonctions dont le type de rĂ©sultat dĂ©pend de l'argument [d'exĂ©cution?] ['s type]") Certainement pas dĂ©pendant des valeurs de par exemple int donnĂ©es, comme dans Idris. Je pense que vous faites rĂ©fĂ©rence au typage dĂ©pendant (c'est-Ă -dire au suivi) du type des valeurs reprĂ©sentant les instances de module instanciĂ©es dans la hiĂ©rarchie des appels afin que de telles fonctions polymorphes puissent ĂȘtre monomorphisĂ©es au moment de la compilation? Le polymorphisme d'exĂ©cution entre-t-il en raison du fait que ces types monomorphisĂ©s sont le type existentiel liĂ© aux types dynamiques ? Les modules F-ing ont dĂ©montrĂ© que les types « dĂ©pendants » ne sont pas absolument nĂ©cessaires pour modĂ©liser les modules ML dans le systĂšme F ω . Ai-je trop simplifiĂ© si je prĂ©sume que @rossberg a reformulĂ© le modĂšle de typage pour supprimer toutes les exigences de monomorphisation?

Le problÚme avec l'approche de package génératif est qu'il crée beaucoup de passe-partout [
]
Les classes de type évitent ce passe-partout en sélectionnant implicitement la classe de type en fonction des types utilisés dans la fonction uniquement.

N'y a-t-il pas aussi un passe-partout avec des foncteurs applicatifs ML ? Il n'y a pas d'unification connue des classes de types et des foncteurs ML (modules) qui conserve la briĂšvetĂ© sans introduire de restrictions nĂ©cessaires pour empĂȘcher (cf aussi ) l'anti-modularitĂ© inhĂ©rente au critĂšre d'unicitĂ© globale des instances d'implĂ©mentation de classes de types.

Les classes de types ne peuvent implĂ©menter chaque type que d'une seule maniĂšre et nĂ©cessitent sinon un passe-partout wrapper newtype pour surmonter la limitation. Voici un autre exemple de plusieurs façons d'implĂ©menter un algorithme. Afaics, @keean a contournĂ© cette limitation dans son exemple de tri de classe de types en remplaçant la sĂ©lection implicite par un Relation explicitement sĂ©lectionnĂ© en utilisant des types d'emballage data pour nommer diffĂ©rentes relations de maniĂšre gĂ©nĂ©rique sur le type de valeur, mais je doute si ces tactiques sont gĂ©nĂ©rales Ă  toutes les variantes de modularitĂ©. Cependant, une solution plus gĂ©nĂ©ralisĂ©e (qui peut aider Ă  amĂ©liorer le problĂšme de modularitĂ© de l'unicitĂ© globale Ă©ventuellement combinĂ©e avec une restriction orpheline comme amĂ©lioration de la version proposĂ©e pour la rĂ©solution orpheline en employant une valeur non par dĂ©faut pour les implĂ©mentations qui pourraient ĂȘtre orphelines) peut ĂȘtre d'avoir un paramĂštre de type supplĂ©mentaire implicitement sur toutes les classes de type interface , qui, lorsqu'il n'est pas spĂ©cifiĂ©, utilise par dĂ©faut la correspondance implicite normale, mais lorsqu'il est spĂ©cifiĂ© (ou lorsqu'il n'est pas spĂ©cifiĂ©, il ne correspond Ă  aucun autre 2 ) sĂ©lectionne alors l'implĂ©mentation qui a la mĂȘme valeur dans sa liste de valeurs personnalisĂ©es dĂ©limitĂ©es par des virgules (il s'agit donc d'une correspondance modulaire plus gĂ©nĂ©ralisĂ©e que de nommer une instance implement spĂ©cifique). La liste dĂ©limitĂ©e par des virgules est telle qu'une implĂ©mentation peut ĂȘtre diffĂ©renciĂ©e dans plus d'un degrĂ© de libertĂ©, comme si elle avait deux spĂ©cialisations orthogonales. Le spĂ©cialisĂ© non par dĂ©faut souhaitĂ© peut ĂȘtre spĂ©cifiĂ© soit au niveau de la dĂ©claration de la fonction, soit au niveau du site d'appel. Au site d'appel, par exemple f<non-default>(
) .

Alors pourquoi aurions-nous besoin de modules paramĂ©trĂ©s si nous avons des classes de types ? Afaics uniquement pour la substitution (← lien important Ă  cliquer) car la rĂ©utilisation des classes de types Ă  cette fin ne convient pas dans la mesure oĂč, par exemple, nous voulons qu'un module de package puisse s'Ă©tendre sur plusieurs fichiers et nous voulons pouvoir ouvrir implicitement le contenu de le module dans la portĂ©e sans passe-partout supplĂ©mentaire . Donc peut-ĂȘtre aller de l'avant avec une paramĂ©trisation de package _syntaxique uniquement_ substitution uniquement (pas de premiĂšre classe) est une premiĂšre Ă©tape raisonnable qui peut traiter la gĂ©nĂ©ricitĂ© au niveau du module tout en restant ouvert Ă  la compatibilitĂ© et au non-chevauchement des fonctionnalitĂ©s si vous ajoutez des classes de type plus tard pour le niveau de la fonction gĂ©nĂ©ricitĂ©. macros sont par exemple typĂ©es ou simplement une substitution syntaxique (alias "prĂ©processeur"). S'ils sont typĂ©s, les modules dupliquent la fonctionnalitĂ© des classes de types, ce qui n'est pas souhaitable Ă  la fois du point de vue de la minimisation des paradigmes/concepts qui se chevauchent du PL et des cas potentiels d'angle dus aux interactions du chevauchement ( comme ceux lorsque l'on tente d'offrir Ă  la fois des foncteurs ML et des classes de types ). Les modules typĂ©s sont plus modulaires car les modifications apportĂ©es Ă  toute implĂ©mentation encapsulĂ©e dans le module qui ne modifie pas les signatures exportĂ©es ne peuvent pas rendre les consommateurs du module incompatibles (autre que le problĂšme d'anti-modularitĂ© susmentionnĂ© des instances d'implĂ©mentation qui se chevauchent). Je suis intĂ©ressĂ© de lire les rĂ©flexions de @keean Ă  ce sujet.

[
] avec des exceptions pour la récursivité polymorphe et les types existentiels (qui sont essentiellement des variantes dont vous ne pouvez pas sortir, vous ne pouvez utiliser que les interfaces que la variante confirme).

Pour aider les autres lecteurs. Par "rĂ©cursivitĂ© polymorphe", je pense qu'il fait rĂ©fĂ©rence Ă  des types de rang supĂ©rieur, par exemple des rappels paramĂ©trĂ©s dĂ©finis au moment de l'exĂ©cution oĂč le compilateur ne peut pas monomorphiser le corps de la fonction de rappel car il n'est pas connu au moment de la compilation. Les types existentiels sont, comme je l'ai mentionnĂ© prĂ©cĂ©demment, Ă©quivalents aux objets de trait de Rust, qui sont un moyen d'atteindre des conteneurs hĂ©tĂ©rogĂšnes avec une liaison ultĂ©rieure dans le problĂšme d'expression que class sous-classant l'hĂ©ritage virtuel, mais pas aussi ouvert Ă  l'extension dans l'expression ProblĂšme car les unions avec des structures de donnĂ©es immuables ou la copie 3 qui ont un coĂ»t de performance O(log n) .

1 Qui ne nécessite pas HKT dans l'exemple ci-dessus, car SET ne nécessite pas le type elem est un paramÚtre de type du type générique de set , c'est-à-dire que ce n'est pas set<elem> .

2 Pourtant, s'il existait plus d'une implémentation non par défaut et aucune implémentation par défaut, alors la sélection serait ambiguë et le compilateur devrait générer une erreur.

3 Notez que la mutation avec des structures de données immuables ne nécessite pas nécessairement de copier la totalité de la structure de données, si la structure de données est suffisamment intelligente pour isoler l'historique tel qu'une liste à liaison simple.

L'implémentation func pick(a CollectionOfT, count uint) []T serait un bon exemple d'application de génériques (à partir de 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) {

L'approche de l'interface{} ici est compliquée.

J'ai commenté à plusieurs reprises sur ce problÚme que l'un des principaux problÚmes de l'approche de modÚle C++ est sa dépendance à la résolution de surcharge en tant que mécanisme de métaprogrammation au moment de la compilation.

Il semble que Herb Sutter soit arrivĂ© Ă  la mĂȘme conclusion : il existe maintenant une proposition intĂ©ressante pour la programmation Ă  la compilation en C++ .

Il a certains éléments en commun avec le package Go reflect et ma précédente proposition de fonctions de compilation dans Go .

Salut.
J'ai Ă©crit une proposition de gĂ©nĂ©riques avec des contraintes pour Go. Vous pouvez le lire ici . Peut-ĂȘtre peut-il ĂȘtre ajoutĂ© en tant que document de 15292. Il s'agit principalement de contraintes et se lit comme un amendement aux paramĂštres de type Taylors dans Go .
Il est conçu comme un exemple d'une maniÚre pratique (je crois) de faire des génériques "de type sûr" dans Go, - j'espÚre que cela peut ajouter quelque chose à cette discussion.
Veuillez noter que mĂȘme si j'ai lu (la plupart de) ce trĂšs long fil, je n'ai pas suivi tous les liens qu'il contient, donc d'autres peuvent avoir fait des suggestions similaires. Si c'est le cas, je m'excuse.

Br. Chr.

Syntaxe bikeshedding :

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

pourrait ĂȘtre

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

qui ressemble plus Ă  Allez vers moi. :-)

Plusieurs éléments ici.

Une chose est de remplacer : par _ et de remplacer # par ... .
Je suppose que vous pourriez le faire si vous préférez.

Une autre chose est de remplacer constraint[T] Array par type[T] Array constraint .
Cela semblerait indiquer que les contraintes sont des types, ce qui, à mon avis, n'est pas correct. Formellement, une contrainte est un _prédicat_ sur l'ensemble de tous les types, c'est-à-dire. un mappage de l'ensemble de types à l'ensemble { true , false }.
Ou si vous préférez, vous pouvez considérer une contrainte comme simplement _un ensemble de_ types.
Ce n'est pas un type.

Br. Chr.

Pourquoi ce constraint n'est-il pas juste un interface ?

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

Une interface serait un peu plus utile comme contrainte avec la proposition suivante : #23796 qui, Ă  son tour, donnerait Ă©galement du mĂ©rite Ă  la proposition elle-mĂȘme.

De plus, si la proposition de types de somme est acceptĂ©e sous une forme quelconque (#19412), alors ceux-ci doivent ĂȘtre utilisĂ©s pour contraindre le type.

Bien que je croie le mot-clĂ© de contrainte, quelque chose comme ça devrait ĂȘtre ajoutĂ©, afin de ne pas rĂ©pĂ©ter de grandes contraintes et d'Ă©viter les erreurs dues Ă  la distraction.

Enfin, pour la partie bikeshedding, je pense que les contraintes devraient ĂȘtre listĂ©es Ă  la fin d'une dĂ©finition, pour Ă©viter la surpopulation (la rouille semble avoir une bonne idĂ©e ici):

// 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 : Je pense que c'est un gros avantage pour go d'avoir des interfaces implémentées implicitement plutÎt qu'explicitement. La proposition @surlykke dans ce commentaire est, je pense, beaucoup plus proche de l'esprit des autres syntaxes Go.

@surlykke Je m'excuse si la proposition a la réponse à l'une de ces questions.

Une utilisation des génériques consiste à autoriser les fonctions de style intégrées. Comment implémenteriez-vous le len au niveau de l'application avec cela? La disposition de la mémoire est différente pour chaque entrée autorisée, alors en quoi est-ce mieux qu'une interface ?

Le "pick" dĂ©crit prĂ©cĂ©demment a un problĂšme similaire oĂč l'indexation dans une carte et l'indexation dans une tranche sont diffĂ©rentes. Dans le cas de la carte, s'il y a eu une conversion en tranche en premier, le mĂȘme code de sĂ©lection peut ĂȘtre utilisĂ©, mais comment cela se fait-il ?

Les collections sont une autre utilisation :

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

Pour le cas de type collection, je ne suis pas convaincu que ce soit une grande victoire sur les génériques Go 1 car il y a des compromis de lisibilité.

Je suis d'accord que les paramĂštres de type doivent avoir une certaine forme de contraintes. Sinon, nous rĂ©pĂ©terons les erreurs des modĂšles C++. La question est de savoir jusqu'Ă  quel point les contraintes doivent-elles ĂȘtre expressives ?

À une extrĂ©mitĂ©, nous pourrions simplement utiliser des interfaces. Mais comme vous l'avez soulignĂ©, de nombreux modĂšles utiles ne peuvent pas ĂȘtre capturĂ©s de cette façon.

Ensuite, il y a votre idĂ©e, et d'autres similaires, qui tentent de dĂ©finir un ensemble de contraintes utiles et de fournir une nouvelle syntaxe pour les exprimer. Mis Ă  part le problĂšme d'ajouter encore plus de syntaxe, il n'est pas clair oĂč s'arrĂȘter. Comme vous le soulignez, votre proposition capture de nombreux modĂšles, mais en aucun cas tous.

A l'autre extrĂȘme se trouve l'idĂ©e que je propose dans cette doc . Il utilise le code Go lui-mĂȘme comme langage de contrainte. Vous pouvez capturer pratiquement n'importe quelle contrainte de cette façon, et cela ne nĂ©cessite aucune nouvelle syntaxe.

@jba
C'est un peu verbeux. Peut-ĂȘtre que si Go avait une syntaxe lambda, ce serait un peu plus acceptable. D'un autre cĂŽtĂ©, il semble que le plus gros problĂšme qu'il essaie de rĂ©soudre est de vĂ©rifier si un type prend en charge un opĂ©rateur quelconque. Ce serait peut-ĂȘtre plus facile si Go n'avait que des interfaces prĂ©dĂ©finies pour diffĂ©rents opĂ©rateurs :

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

ou quelque chose dans ce sens.

Si le problĂšme concerne l'extension des fonctions intĂ©grĂ©es, le problĂšme rĂ©side peut-ĂȘtre dans la maniĂšre dont le langage crĂ©e les types d'adaptateur. Par exemple, le ballonnement associĂ© Ă  sort.Interface n'est-il pas la raison derriĂšre https://github.com/golang/go/issues/16721 et sort.Slice ?
En regardant https://github.com/golang/go/issues/21670#issuecomment -325739411, l'idĂ©e de @Sajmani d'avoir des littĂ©raux d'interface pourrait ĂȘtre l'ingrĂ©dient nĂ©cessaire pour que les paramĂštres de type fonctionnent facilement avec les builtins.
Regardez la définition suivante d'Iterator :

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

Si print est une fonction qui itÚre simplement sur une liste et imprime son contenu, l'exemple suivant utilise des littéraux d'interface pour construire une interface satisfaisante pour print .

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

On peut déjà le faire s'ils déclarent globalement des types dont la seule responsabilité est de satisfaire une interface. Cependant, cette conversion d'une fonction en une méthode rend les interfaces (et donc les "contraintes") plus faciles à satisfaire. Nous ne polluons pas les déclarations de niveau supérieur avec des adaptateurs simples (comme "widgetsByName" dans le tri).
Les types définis par l'utilisateur peuvent évidemment également tirer parti de cette fonctionnalité, comme le montre cet exemple LinkedList :

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 : Les contraintes telles que je les ai décrites sont plus expressives que les interfaces (champs, opérateurs). J'ai briÚvement envisagé d'étendre les interfaces au lieu d'introduire des contraintes, mais je pense que ce serait un changement beaucoup trop intrusif pour un élément existant de Go.

@pciet Je ne suis pas tout Ă  fait sĂ»r de ce que vous entendez par "niveau d'application". Go a une fonction len intĂ©grĂ©e qui peut ĂȘtre appliquĂ©e Ă  un tableau, un pointeur vers un tableau, une tranche, une chaĂźne et un canal, donc, dans ma proposition, si un paramĂštre de type est contraint d'avoir l'un de ceux-ci comme type sous-jacent , len peut lui ĂȘtre appliquĂ©.

@pciet À propos de votre exemple avec contrainte/interface Comparable . Notez que si vous dĂ©finissez (la variante d'interface) :

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

Ensuite, vous pouvez mettre tout ce qui implémente Comparable dans Set . Comparez cela à :

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

oĂč vous ne pouvez mettre que des valeurs de type Foo dans FooSet . C'est une sĂ©curitĂ© de type plus forte.

@urandom Encore une fois, je ne suis pas fan de :

type MyConstraint constraint {....}

car je ne crois pas qu'une contrainte soit un type. De plus, je n'autoriserais certainement pas :

var myVar MyConstraint

qui n'a aucun sens pour moi. Une autre indication que les contraintes ne sont pas des types.

@urandom On bikeshedding: Je pense que les contraintes doivent ĂȘtre dĂ©clarĂ©es juste Ă  cĂŽtĂ© des paramĂštres de type. ConsidĂ©rons une fonction ordinaire, dĂ©finie comme ceci :

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

Vous ne pouviez pas lire ceci de gauche Ă  droite. Au lieu de cela, vous devez d'abord lire func MyFunc(i) pour dĂ©terminer qu'il s'agit d'une dĂ©finition de fonction. Ensuite, vous devrez sauter Ă  la fin pour comprendre ce qu'est i , puis revenir au corps de la fonction. Pas idĂ©al, OMI. Et je ne vois pas en quoi les dĂ©finitions gĂ©nĂ©riques devraient ĂȘtre diffĂ©rentes.
Mais évidemment, cette discussion est orthogonale à celle de savoir si Go devrait avoir des contraintes ou des génériques.

@surlykke
Je suis d'accord pour que ce ne soit pas un type. La chose la plus importante est qu'ils aient un nom afin qu'ils puissent ĂȘtre rĂ©fĂ©rencĂ©s par plusieurs types.

Pour les fonctions, si nous suivons la syntaxe de rust, ce serait :

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

Ainsi, il ne cachera pas des choses comme le nom de la fonction ou ses paramÚtres, et vous n'aurez pas besoin d'aller à la fin du corps de la fonction pour voir quelle est la contrainte sur les types génériques.

@surlykke pour la postĂ©ritĂ©, pourriez-vous localiser oĂč votre proposition pourrait ĂȘtre ajoutĂ©e :
https://docs.google.com/document/d/1vrAy9gMpMoS3uaVphB32uVXX4pi-HnNjkMEgyAHX4N4

C'est un endroit idéal pour "compiler" toutes les propositions.

Une autre question que je vous pose Ă  tous est de savoir comment traiter la spĂ©cialisation de diffĂ©rentes instanciations d'un type gĂ©nĂ©rique. Dans la proposition type-params , la façon de le faire est de gĂ©nĂ©rer la mĂȘme fonction basĂ©e sur un modĂšle pour chaque type instanciĂ©, en remplaçant le paramĂštre de type par le nom du type. Afin d'avoir des fonctionnalitĂ©s distinctes pour diffĂ©rents types, effectuez un changement de type sur le paramĂštre de type.

Est-il prudent de supposer que lorsque le compilateur voit un changement de type sur un paramÚtre de type, il est autorisé à générer une implémentation distincte pour chaque assertion ? Ou est-ce trop impliqué dans une optimisation, puisque les paramÚtres de type imbriqués dans les structures affirmées peuvent créer un aspect paramétrique à la génération de code ?

Dans la proposition de fonctions au moment de la compilation , comme nous savons que ces déclarations sont générées au moment de la compilation, un changement de type ne pose aucun coût d'exécution.

Un scénario pratique : si nous considérons un cas du package math/bits , effectuer une assertion de type pour appeler OnesCount pour chaque uintXX dépasserait le point d'avoir une bibliothÚque de manipulation de bits efficace. Si toutefois, les assertions de type étaient transformées en ce qui suit

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

Un appel Ă 

var x uint8 = 255
bits.OnesCount(x)

appellerait alors la fonction générée suivante (le nom n'est pas important ici):

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

@jba C'est une proposition intĂ©ressante, mais pour moi, cela souligne surtout le fait que la dĂ©finition de la fonction paramĂ©trique elle-mĂȘme suffit gĂ©nĂ©ralement Ă  dĂ©finir ses contraintes.

Si vous allez utiliser des « opérateurs utilisés dans une fonction » comme contraintes, alors quel avantage cela vous rapporte-t-il d'écrire une seconde fonction contenant un sous-ensemble des opérateurs utilisés dans la premiÚre ?

@bcmills L'un d'eux est une spĂ©cification et l'autre est l'implĂ©mentation. C'est le mĂȘme avantage que le typage statique : vous pouvez dĂ©tecter les erreurs plus tĂŽt.

Si l'implĂ©mentation est la spĂ©cification, Ă  la maniĂšre des modĂšles C++, alors toute modification de l'implĂ©mentation casse potentiellement les dĂ©pendances. Cela ne peut ĂȘtre dĂ©couvert que bien plus tard, lorsque les dĂ©pendants se recompilent et que les dĂ©couvreurs n'ont aucun contexte pour comprendre le message d'erreur. Avec la spĂ©cification dans le mĂȘme package, vous pouvez dĂ©tecter la casse localement.

@mandolyte Je ne sais pas trop oĂč l'ajouter - peut-ĂȘtre un paragraphe sous "Approches gĂ©nĂ©riques" nommĂ© "GĂ©nĂ©riques avec contraintes" ?
Le document ne semble pas contenir grand-chose sur la contrainte des paramĂštres de type, donc si vous ajoutiez un paragraphe oĂč ma proposition serait mentionnĂ©e, d'autres approches des contraintes pourraient Ă©galement y ĂȘtre rĂ©pertoriĂ©es.

@surlykke l'approche générale sur le document est de faire un changement ce qui semble juste et j'essaierai de l'accepter, de l'incorporer et de l'organiser avec le reste du document. J'ai ajouté une section ici . N'hésitez pas à ajouter des choses que j'ai ratées.

@egonelbre C'est trĂšs gentil. Merci!

@jba
J'aime votre proposition, mais je pense que c'est beaucoup trop lourd pour golang. Cela me rappelle beaucoup de modĂšles en c++. Je pense que le principal problĂšme est que vous pouvez Ă©crire du code trĂšs complexe avec.
Décider si deux instances d'interface génériques se chevauchent parce que l'ensemble contraint de types se chevauchent serait une tùche difficile entraßnant des temps de compilation plus lents. Idem pour la génération de code.

Je pense que les contraintes proposĂ©es sont plus lĂ©gĂšres pour aller. D'aprĂšs ce que j'ai entendu, les contraintes, c'est-Ă -dire les classes de types, pourraient ĂȘtre implĂ©mentĂ©es orthogonalement au systĂšme de types d'un langage.

Je suis tout à fait d'accord sur le fait que nous ne devrions pas utiliser de contraintes implicites du corps de la fonction. Ils sont largement considérés comme l'un des défauts les plus importants des modÚles C++ :

  • Les contraintes ne sont pas facilement visibles. Bien que godoc puisse thĂ©oriquement Ă©numĂ©rer toutes les contraintes dans la documentation, elles ne sont pas visibles dans le code source, sauf implicitement.
  • Pour cette raison, il est possible d'inclure accidentellement une contrainte supplĂ©mentaire qui n'est visible que lorsque vous essayez d'utiliser la fonction d'une maniĂšre inattendue. En exigeant une spĂ©cification explicite des contraintes, le programmeur doit savoir exactement quelles contraintes il introduit.
  • Il rend la dĂ©cision sur les types de contraintes autorisĂ©es beaucoup plus ad hoc. Par exemple, suis-je autorisĂ© Ă  dĂ©finir la fonction suivante ? Quelles sont les contraintes rĂ©elles sur T, U et V ici ? Si nous demandons au programmeur de spĂ©cifier explicitement les contraintes, alors nous sommes conservateurs dans le type de contraintes que nous autorisons (nous permettant de dĂ©velopper cela lentement et dĂ©libĂ©rĂ©ment). Si nous essayons quand mĂȘme d'ĂȘtre conservateurs, comment pouvons-nous donner un message d'erreur pour une fonction comme celle-ci ? "Erreur : impossible d'affecter uv() Ă  T car il impose une contrainte illĂ©gale" ?
func[T, U, V] Foo(u U, v V) {
  var t T = u.v(V) + 1;
}
  • L'appel de fonctions gĂ©nĂ©riques dans d'autres fonctions gĂ©nĂ©riques aggrave les situations ci-dessus, car vous devez maintenant examiner toutes les contraintes des appelĂ©s afin de comprendre les contraintes de la fonction que vous Ă©crivez ou lisez.
  • Le dĂ©bogage peut ĂȘtre trĂšs difficile, car les messages d'erreur doivent soit ne pas fournir suffisamment d'informations pour trouver la source de la contrainte, soit ils doivent divulguer des dĂ©tails internes de la fonction. Par exemple, si F a une exigence sur un type T et que l'auteur de F essaie de comprendre d'oĂč vient cette exigence, il aimerait que le compilateur avertissez-les de la dĂ©claration exacte qui donne lieu Ă  la contrainte (surtout si elle provient d'un appelĂ© gĂ©nĂ©rique). Mais un utilisateur de F ne veut pas cette information et, en effet, si elle est incluse dans les messages d'erreur, alors nous divulguons des dĂ©tails d'implĂ©mentation de F dans les messages d'erreur de ses utilisateurs, ce qui sont une expĂ©rience utilisateur terrible.

@alercah

Par exemple, suis-je autorisé à définir la fonction suivante ?

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

Non u.v(V) est une erreur de syntaxe car V est un type et la variable t n'est pas utilisée.

Cependant, vous pourriez dĂ©finir cette fonction, qui peut ĂȘtre celle que vous vouliez :

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

Quelles sont les contraintes réelles sur T, U et V ici ?

  • Le type V n'est pas contraint.
  • Le type U doit avoir une mĂ©thode v qui accepte un seul paramĂštre ou varargs d'un certain type assignable Ă  partir de V , car u.v est invoquĂ© avec un seul argument de type V .

    • U.v pourrait ĂȘtre un champ de type fonction, mais cela devrait sans doute impliquer une mĂ©thode ; voir #23796.

  • Le type renvoyĂ© par U.v doit ĂȘtre numĂ©rique, car la constante 1 lui est ajoutĂ©e.
  • Le type de retour de U.v doit ĂȘtre assignable Ă  T , car u.v(
) + 1 est assignĂ© Ă  une variable de type T .
  • Le type T doit ĂȘtre numĂ©rique, car le type de retour de U.v est numĂ©rique et attribuable Ă  T .

(Un aparté : vous pourriez dire que U et V devraient avoir la contrainte "copiable" parce que les arguments de ces types sont passés par valeur, mais le systÚme de type non générique existant n'applique pas cette contrainte non plus. Cela fait l'objet d'une proposition distincte.)

Si nous demandons au programmeur de spécifier explicitement les contraintes, alors nous sommes conservateurs dans le type de contraintes que nous autorisons (nous permettant de développer cela lentement et délibérément).

Oui, c'est vrai : mais omettre une contrainte serait un grave dĂ©faut, que ces contraintes soient implicites ou non. Selon l'OMI, le rĂŽle le plus important des contraintes est de rĂ©soudre l'ambiguĂŻtĂ©. Par exemple, dans les contraintes ci-dessus, le compilateur doit ĂȘtre prĂȘt Ă  instancier u.v en tant que mĂ©thode Ă  argument unique ou variadique.

L'ambiguĂŻtĂ© la plus intĂ©ressante se produit pour les littĂ©raux, oĂč nous devons lever l'ambiguĂŻtĂ© entre les types struct et les types composites :

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

Si nous essayons quand mĂȘme d'ĂȘtre conservateurs, comment pouvons-nous donner un message d'erreur pour une fonction comme celle-ci ? "Erreur : impossible d'affecter uv() Ă  T car il impose une contrainte illĂ©gale" ?

Je ne suis pas tout à fait sûr de ce que vous demandez, car je ne vois pas de contraintes conflictuelles pour cet exemple. Qu'entendez-vous par « contrainte illégale » ?

Le dĂ©bogage peut ĂȘtre trĂšs difficile, car les messages d'erreur doivent soit ne pas fournir suffisamment d'informations pour trouver la source de la contrainte, soit ils doivent divulguer des dĂ©tails internes de la fonction.

Toutes les contraintes pertinentes ne peuvent pas ĂȘtre exprimĂ©es par le systĂšme de type (voir aussi https://github.com/golang/go/issues/22876#issuecomment-347035323). Certaines contraintes sont appliquĂ©es par des paniques d'exĂ©cution ; certains sont appliquĂ©s par le dĂ©tecteur de course ; les contraintes les plus dangereuses sont simplement documentĂ©es et pas du tout dĂ©tectĂ©es.

Tous ces "détails internes qui fuient" dans une certaine mesure. (Voir aussi https://xkcd.com/1172/.)

Par exemple, si [
] l'auteur de F essaie de comprendre d'oĂč vient cette exigence, il aimerait que le compilateur l'alerte sur la dĂ©claration exacte qui donne lieu Ă  la contrainte (surtout si elle provient d'un appelĂ© gĂ©nĂ©rique). Mais un utilisateur de F ne veut pas cette information[.]

Peut-ĂȘtre? C'est ainsi que les auteurs d'API utilisent les annotations de type dans les langages infĂ©rĂ©s par type tels que Haskell et ML, mais cela conduit Ă©galement Ă  un terrier de lapin de types profondĂ©ment paramĂ©triques ("d'ordre supĂ©rieur") en gĂ©nĂ©ral.

Par exemple, supposons que vous ayez cette fonction :

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

Comment exprimez-vous les contraintes explicites sur le type Arg ? Ils dépendent de l'instanciation spécifique de F . Ce type de dépendance semble manquer dans de nombreuses propositions récentes de contraintes.

Non. uv(V) est une erreur de syntaxe car V est un type et la variable t est inutilisée.

Cependant, vous pourriez dĂ©finir cette fonction, qui peut ĂȘtre celle que vous vouliez :

Oui, c'Ă©tait l'intention, mes excuses.

Le type T doit ĂȘtre numĂ©rique, car le type de retour de U.v est numĂ©rique et attribuable Ă  T .

Doit-on vraiment considérer cela comme une contrainte ? C'est déductible des autres contraintes, mais est-il plus ou moins utile d'appeler cela une contrainte distincte ? Les contraintes implicites posent cette question d'une maniÚre que les contraintes explicites ne posent pas.

Oui, c'est vrai : mais omettre une contrainte serait un grave dĂ©faut, que ces contraintes soient implicites ou non. Selon l'OMI, le rĂŽle le plus important des contraintes est de rĂ©soudre l'ambiguĂŻtĂ©. Par exemple, dans les contraintes ci-dessus, le compilateur doit ĂȘtre prĂȘt Ă  instancier uv en tant que mĂ©thode Ă  argument unique ou variadique.

Je voulais dire "contraintes que nous autorisons" comme dans le langage. Avec des contraintes explicites, il est beaucoup plus facile pour nous de dĂ©cider quel type de contraintes nous sommes prĂȘts Ă  autoriser les utilisateurs Ă  Ă©crire, plutĂŽt que de simplement dire que la contrainte est "tout ce qui fait compiler les choses". Par exemple, mon exemple Foo ci-dessus implique en fait un type supplĂ©mentaire implicite distinct de T , U ou V , puisque nous devons considĂ©rer le type de retour de u.v . Ce type n'est pas explicitement mentionnĂ© de quelque maniĂšre que ce soit dans la dĂ©claration de f ; les propriĂ©tĂ©s qu'il doit avoir sont complĂštement implicites. De mĂȘme, sommes-nous disposĂ©s Ă  autoriser les types de rang supĂ©rieur ( forall ) ? Je ne peux pas trouver d'exemple du haut de ma tĂȘte, mais je ne peux pas non plus me convaincre que vous ne pouvez pas implicitement Ă©crire une liaison de type de rang supĂ©rieur.

Un autre exemple est de savoir si nous devons autoriser une fonction Ă  tirer parti d'une syntaxe surchargĂ©e. Si une fonction implicitement contrainte fait for i := range t pour certains t de type gĂ©nĂ©rique T , la syntaxe fonctionne si T est un tableau, une tranche, un canal, ou carte. Mais la sĂ©mantique est assez diffĂ©rente, surtout si T est un type de canal. Par exemple, si t == nil (ce qui peut arriver tant que T est un tableau), alors l'itĂ©ration ne fait rien, car il n'y a pas d'Ă©lĂ©ments dans une tranche ou une carte nulle, ou bloque pour toujours puisque c'est ce que font les chaĂźnes nil . C'est un gros footgun qui attend d'arriver. De mĂȘme fait m[i] = ... ; si j'ai l'intention que m soit une carte, je devrai me prĂ©munir contre le fait qu'il s'agisse d'une tranche car le code pourrait paniquer sur une affectation hors plage sinon.

En fait, je pense que cela se prĂȘte Ă  un autre argument contre les contraintes implicites : les auteurs d'API peuvent Ă©crire des dĂ©clarations artificielles juste pour ajouter des contraintes. Par exemple, for _, _ := range t { break } empĂȘche un canal tout en autorisant les cartes, les tranches et les tableaux ; x = append(x) force x Ă  avoir le type tranche. var _ = make(T, 0) autorise les tranches, les cartes et les canaux, mais pas les tableaux. Il y aura un livre de recettes expliquant comment ajouter implicitement des contraintes afin que quelqu'un ne puisse pas appeler votre fonction avec un type pour lequel vous n'avez pas Ă©crit de code correct. Je ne peux mĂȘme pas penser Ă  un moyen d'Ă©crire du code qui ne compile que pour les types de carte, Ă  moins que je ne connaisse Ă©galement le type de clĂ©. Et je ne pense pas que ce soit hypothĂ©tique du tout; les cartes et les tranches se comportent assez diffĂ©remment pour la plupart des applications

Je ne suis pas tout à fait sûr de ce que vous demandez, car je ne vois pas de contraintes conflictuelles pour cet exemple. Qu'entendez-vous par « contrainte illégale » ?

Je veux dire une contrainte qui n'est pas autorisée par le langage, comme si le langage décide d'interdire les contraintes de rang supérieur.

Toutes les contraintes pertinentes ne peuvent pas ĂȘtre exprimĂ©es par le systĂšme de type (voir aussi #22876 (commentaire)). Certaines contraintes sont appliquĂ©es par des paniques d'exĂ©cution ; certains sont appliquĂ©s par le dĂ©tecteur de course ; les contraintes les plus dangereuses sont simplement documentĂ©es et pas du tout dĂ©tectĂ©es.

Tous ces "détails internes qui fuient" dans une certaine mesure. (Voir aussi https://xkcd.com/1172/.)

Je ne vois pas vraiment en quoi #22876 entre en jeu ; qui essaie d'utiliser le systĂšme de type pour exprimer un autre type de contrainte. Il sera toujours vrai que nous ne pouvons pas exprimer certaines contraintes sur des valeurs, ou sur des programmes, mĂȘme avec un systĂšme de types de complexitĂ© arbitraire. Mais nous ne parlons ici que des contraintes sur les types . Le compilateur doit pouvoir rĂ©pondre Ă  la question "Puis-je instancier ce gĂ©nĂ©rique avec le type T ?" ce qui signifie qu'il doit comprendre les contraintes, qu'elles soient implicites ou explicites. (Notez que certains langages, comme C++ et Rust, ne peuvent pas trancher cette question en gĂ©nĂ©ral parce qu'elle peut dĂ©pendre d'un calcul arbitraire et revient donc au problĂšme d'arrĂȘt, mais ils expriment toujours les contraintes qui doivent ĂȘtre satisfaites.)

Ce que je veux dire ressemble plus Ă  "quel message d'erreur l'exemple suivant devrait-il donner?"

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

Nous pouvons dire Error: cannot call IndirectlyConstrained with [T = int]; T must have a method with signature func (T t) DoSomething() . Ce message d'erreur est utile pour un utilisateur de IndirectlyConstrained , car il définit clairement les contraintes manquantes. Mais il ne fournit aucune information à quelqu'un essayant de déboguer pourquoi IndirectlyConstrained a cette contrainte, ce qui est un gros problÚme d'utilisabilité s'il s'agit d'une fonction volumineuse. Nous pourrions ajouter Note: this constraint exists on T because IndirectlyConstrained calls DirectlyConstrained with [U = T] on line N , mais maintenant nous divulguons des détails sur l'implémentation de IndirectlyConstrained . De plus, nous n'avons pas expliqué pourquoi IndirectlyConstrained a la contrainte, alors ajoutons-nous un autre Note: this constraint exists on U because DirectlyConstrained calls t.DoSomething() on line M ? Que se passe-t-il si la contrainte implicite provient d'un appelé quatre niveaux plus bas dans la pile des appels ?

De plus, comment formatons-nous ces messages d'erreur pour les types qui ne sont pas explicitement rĂ©pertoriĂ©s en tant que paramĂštres ? Par exemple, si dans l'exemple ci-dessus, IndirectlyConstrained appelle DirectlyConstrained(t.U()) . Comment se rĂ©fĂšre-t-on mĂȘme au type? Dans ce cas, nous pourrions dire the type of t.U() , mais la valeur ne sera pas nĂ©cessairement le rĂ©sultat d'une seule expression ; il pourrait ĂȘtre construit sur plusieurs dĂ©clarations. Ensuite, nous aurions soit besoin de synthĂ©tiser une expression avec les types corrects Ă  mettre dans le message d'erreur, une expression qui n'apparaĂźt jamais dans le code, soit nous aurions besoin de trouver un autre moyen de s'y rĂ©fĂ©rer qui serait moins clair pour le pauvre appelant qui a violĂ© la contrainte.

Comment exprimez-vous les contraintes explicites sur le type Arg ? Elles dépendent de l'instanciation spécifique de F. Ce type de dépendance semble manquer dans de nombreuses propositions récentes de contraintes.

DĂ©posez F et faites en sorte que le type de f soit func (Arg) Result . Oui, il ignore les fonctions variadiques, mais le reste de Go le fait aussi. Une proposition visant Ă  rendre les varargs funcs attribuables Ă  des signatures compatibles pourrait ĂȘtre faite sĂ©parĂ©ment.

Pour les cas oĂč nous avons rĂ©ellement besoin de limites de type d'ordre supĂ©rieur, il peut ĂȘtre logique ou non de les inclure dans les gĂ©nĂ©riques v1. Les contraintes explicites nous obligent Ă  dĂ©cider explicitement si nous voulons prendre en charge les types d'ordre supĂ©rieur, et comment. Le manque de considĂ©ration jusqu'Ă  prĂ©sent est un symptĂŽme, je pense, du fait que Go n'a actuellement aucun moyen de se rĂ©fĂ©rer aux propriĂ©tĂ©s des types intĂ©grĂ©s. C'est une question ouverte gĂ©nĂ©rale de savoir comment tout systĂšme gĂ©nĂ©rique autorisera des fonctions gĂ©nĂ©riques sur tous les types numĂ©riques, ou tous les types entiers, et la plupart des propositions ne se sont pas beaucoup concentrĂ©es sur cela.

Veuillez évaluer l'implémentation de mes génériques dans votre prochain projet
http://go-li.github.io/

Nous pouvons dire Error: cannot call IndirectlyConstrained with [T = int]; T must have a method with signature func (T t) DoSomething() . Ce message d'erreur [
] ne fournit aucune information à quelqu'un essayant de déboguer pourquoi IndirectlyConstrained a cette contrainte, ce qui est un gros problÚme d'utilisabilité s'il s'agit d'une fonction volumineuse.

Je tiens à souligner une grande hypothÚse que vous faites ici : que le message d'erreur de go build est le _seul_ outil dont dispose le programmeur pour diagnostiquer le problÚme.

Pour utiliser une analogie : si vous rencontrez un error au moment de l'exĂ©cution, vous disposez de plusieurs options pour le dĂ©bogage. L'erreur elle-mĂȘme ne contient qu'un simple message, qui peut ou non suffire Ă  dĂ©crire l'erreur. Mais ce ne sont pas les seules informations dont vous disposez : par exemple, vous disposez Ă©galement de toutes les instructions de journal Ă©mises par le programme, et s'il s'agit d'un bogue vraiment Ă©pineux, vous pouvez le charger dans un dĂ©bogueur interactif.

Autrement dit, le débogage à l'exécution est un processus interactif. Alors, pourquoi devrions-nous supposer un débogage non interactif pour les erreurs de compilation➟ Comme alternative, nous pourrions enseigner à l'outil guru les contraintes de type. Ensuite, la sortie du compilateur serait quelque chose comme :

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

Cela donne à l'utilisateur du paquet générique les informations dont il a besoin pour déboguer le site d'appel immédiat, mais donne _également_ un fil d'Ariane au responsable du paquet (et, surtout, à son environnement d'édition !) pour approfondir ses recherches.

Nous pourrions ajouter Note: this constraint exists on T because IndirectlyConstrained calls DirectlyConstrained with [U = T] on line N , mais maintenant nous divulguons des détails sur l'implémentation de IndirectlyConstrained .

Oui, c'est ce que je veux dire Ă  propos des fuites d'informations de toute façon. Vous pouvez dĂ©jĂ  utiliser guru describe pour jeter un coup d'Ɠil Ă  l'intĂ©rieur d'une implĂ©mentation. Vous pouvez jeter un coup d'Ɠil Ă  l'intĂ©rieur d'un programme en cours d'exĂ©cution Ă  l'aide d'un dĂ©bogueur, et non seulement rechercher la pile, mais Ă©galement descendre dans des fonctions arbitrairement de bas niveau.

Je suis tout à fait d'accord que nous devrions cacher les informations probablement non pertinentes _par défaut_, mais cela ne signifie pas que nous devons les cacher dans l'absolu.

Si une fonction implicitement contrainte fait pour i := range t pour certains t de type générique T , la syntaxe fonctionne si T est n'importe quel tableau, tranche, canal , ou carte. Mais la sémantique est assez différente, surtout si T est un type de canal.

Je pense que c'est l'argument le plus convaincant pour les contraintes de type, mais cela ne nécessite pas que les contraintes explicites soient aussi détaillées que ce que certains proposent. Pour lever l'ambiguïté des sites d'appel, il semble suffisant de contraindre les paramÚtres de type par quelque chose de plus proche de reflect.Kind . Nous n'avons pas besoin de décrire des opérations qui sont déjà claires dans le code ; à la place, nous n'avons qu'à dire des choses comme « T est un type de tranche ». Cela conduit à un ensemble de contraintes beaucoup plus simple :

  • un type soumis Ă  des opĂ©rations d'index doit ĂȘtre Ă©tiquetĂ© comme linĂ©aire ou associatif,
  • un type soumis Ă  des opĂ©rations range doit ĂȘtre Ă©tiquetĂ© comme nil-empty ou nil-blocking,
  • un type avec des littĂ©raux doit ĂȘtre Ă©tiquetĂ© comme ayant des champs ou des indices, et
  • (peut-ĂȘtre) un type avec des opĂ©rations numĂ©riques doit ĂȘtre Ă©tiquetĂ© comme virgule fixe ou flottante.

Cela conduit Ă  un langage de contraintes beaucoup plus Ă©troit, peut-ĂȘtre quelque chose comme :

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

avec des exemples comme :

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

Je pense que nous avons fait un grand pas vers le générique personnalisé en introduisant un alias de type.
L'alias de type rend les super types (type de types) possibles.
Nous pouvons traiter les types comme des valeurs en utilisant.

Pour simplifier les explications, nous pouvons ajouter un nouvel élément de code, genre .
La relation entre les genres et les types est comme la relation entre les types et les valeurs.
En d'autres termes, un genre signifie un type de types.

Chaque genre de type, à l'exception des genres struct et interface et function, correspond à un genre prédéclaré.

  • Bool
  • ChaĂźne de caractĂšres
  • Int8, Uint8, Int16, Uint16, Int32, Uint32, Int64, Uint64, Int, Uint, Uintptr
  • Float32, Float64
  • Complexe64, Complexe128
  • Tableau, Tranche, Carte, Canal, Pointeur, UnsafePointer

Il existe d'autres genres prédéclarés, tels que Comaprable, Numeric, Inger, Float, Complex, Container, etc. Nous pouvons utiliser Type ou * désigner le genre de tous les types.

Les noms de tous les genres intégrés commencent tous par une lettre majuscule.

Chaque type de structure et d'interface et de fonction correspond Ă  un genre.

Nous pouvons également déclarer des genres personnalisés :

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

Pour rendre l'explication suivante cohérente, un modificateur de genre est nécessaire.
Le modificateur de genre est noté Const . Par example:

  • Const Integer est un genre (diffĂ©rent de Integer ) et son instance doit ĂȘtre une valeur constante dont le type doit ĂȘtre un entier. Cependant, la valeur constante peut ĂȘtre considĂ©rĂ©e comme un type spĂ©cial.
  • Const func(int) bool est un genre (diffĂ©rent de func(int) bool ) et son instance doit ĂȘtre une valeur de fonction dĂ©clarĂ©e. Cependant, la dĂ©claration de fonction peut ĂȘtre considĂ©rĂ©e comme un type spĂ©cial.

(La solution du modificateur est dĂ©licate, il existe peut-ĂȘtre d'autres meilleures solutions de conception.)

Bon, continuons.
Nous avons besoin d'un autre concept. Trouver un bon nom pour ce n'est pas facile,
Appelons-le simplement crate .
Généralement, la relation entre les caisses et les genres est comme la relation entre les fonctions et les types.
Un crate peut prendre des types comme paramĂštres et des types de retour.

Une déclaration de caisse (en supposant que le code suivant est déclaré dans le package lib ) :

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
}

Utilisation de la caisse ci-dessus.

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

Mes idées absorbent de nombreuses idées d'autres personnes présentées ci-dessus.
Ils ne sont pas vraiment mûrs maintenant.
Je les poste ici juste parce que je sens qu'ils sont intéressants,
et je ne veux plus l'améliorer.
Tant de cellules cérébrales ont été tuées en réparant les trous dans les idées.
J'espÚre que ces idées pourront apporter des inspirations à d'autres gophers.

Ce que vous appelez "genre" s'appelle en fait "genre", et est bien connu dans le
communauté de programmation fonctionnelle. Ce que vous appelez une caisse est un objet restreint
sorte de foncteur ML.

Le mercredi 4 avril 2018 Ă  12 h 41, dotaheor [email protected] a Ă©crit :

Je pense que nous avons fait un grand pas vers le générique personnalisé en introduisant
alias de type.
L'alias de type rend les super types (type de types) possibles.
Nous pouvons traiter les types comme des valeurs en utilisant.

Pour simplifier les explications, nous pouvons ajouter un nouvel élément de code, genre.
La relation entre les genres et les types est comme la relation entre les types
et valeurs.
En d'autres termes, un genre signifie un type de types.

Chaque type de type, Ă  l'exception des types de structure et d'interface et de fonction,
correspond à un genre pré-déclaré.

  • Bool
  • ChaĂźne de caractĂšres
  • Int8, Uint8, Int16, Uint16, Int32, Uint32, Int64, Uint64, Int, Uint,
    Uintptr
    & Float32, Float64
  • Complexe64, Complexe128
  • Tableau, Tranche, Carte, Canal, Pointeur, UnsafePointer

Il existe d'autres genres prédéclarés, tels que Comaprable, Numeric,
Entier, Flottant, Complexe, Conteneur, etc. Nous pouvons utiliser Type ou * indique
le genre de tous les types.

Les noms de tous les genres intégrés commencent tous par une lettre majuscule.

Chaque type de structure et d'interface et de fonction correspond Ă  un genre.

Nous pouvons également déclarer des genres personnalisés :

genre Addable = Numérique | Chaßne de caractÚres
genre Ordonnable = Entier | Flotteur | ChaĂźne de caractĂšres
genre Validator = func(int) bool // chaque paramĂštre et type de rĂ©sultat doit ĂȘtre un type spĂ©cifiĂ©.
genre HaveFieldsAndMethods = {
width int // nous devons utiliser un type spécifique pour définir les champs.
height int // nous ne pouvons pas utiliser de genre pour définir les champs.
Load(v []byte) error // chaque paramĂštre et type de rĂ©sultat doit ĂȘtre un type spĂ©cifiĂ©.
FaireQuelqueChose()
}
genre GenreFromStruct = aStructType // déclare un genre à partir d'un type struct
genre GenreFromInterface = anInterfaceType // déclare un genre à partir d'un type d'interface
genre GenreFromStructInterface = aStructType | unTypeInterface

Pour rendre l'explication suivante cohérente, un modificateur de genre est nécessaire.
Le modificateur de genre est noté Const. Par example:

  • Const Integer est un genre et son instance doit ĂȘtre une valeur constante
    dont le type doit ĂȘtre un entier.
    Cependant, la valeur constante peut ĂȘtre considĂ©rĂ©e comme un type spĂ©cial.
  • Const func(int) bool est un genre et son instance doit ĂȘtre un delcared
    valeur de la fonction.
    Cependant, la dĂ©claration de fonction peut ĂȘtre considĂ©rĂ©e comme un type spĂ©cial.

(La solution du modificateur est dĂ©licate, il existe peut-ĂȘtre une autre meilleure conception
solutions.)

Bon, continuons.
Nous avons besoin d'un autre concept. Trouver un bon nom pour ce n'est pas facile,
Appelons ça caisse.
Généralement, la relation entre les caisses et les genres est comme la relation
entre les fonctions et les types.
Un crate peut prendre des types comme paramĂštres et des types de retour.

Une déclaration de caisse (en supposant que le code suivant est déclaré dans lib
paquet):

caisse Exemple [T Float, S {largeur, hauteur T}, N Const Entier] [*, *, *] {
tapez MonTableau [N]T

fonction Ajouter(a, b T) T {
retour a+b
}

// Un genre de portĂ©e de caisse. Ne peut ĂȘtre utilisĂ© que dans la caisse.

// M est un type de genre G
structure de type M {
x T
y S
}

func (m *M) Aire() T {
m.FaireQuelqueChose()
retourner malargeur * mahauteur
}

func (m *M) PĂ©rimĂštre() T {
return 2 * Ajouter(malargeur, mahauteur)
}

exporter M, ajouter, MyArray
}

Utilisation de la caisse ci-dessus.

importer "lib"

// Nous pouvons utiliser AddFunc comme une fonction delcared normale.
type Rect, AddFunc, Array = lib.Example(float32, struct{x, y float32})

func demo() {
var r Rect
a, p = r.Aire(), r.PĂ©rimĂštre()
_ = AddFunc(a, p)
}

Mes idées absorbent de nombreuses idées d'autres personnes présentées ci-dessus.
Ils ne sont pas vraiment mûrs maintenant.
Je les poste ici juste parce que je sens qu'ils sont intéressants,
et je ne veux plus l'améliorer.
Tant de cellules cérébrales ont été tuées en réparant les trous dans les idées.

—
Vous recevez ceci parce que vous avez été mentionné.
RĂ©pondez directement Ă  cet e-mail, consultez-le sur GitHub
https://github.com/golang/go/issues/15292#issuecomment-378665695 , ou muet
le fil
https://github.com/notifications/unsubscribe-auth/AGGWB78BrjN0BxRfroH-jRNy4mCXgSwCks5tlPfMgaJpZM4IG-xv
.

J'ai l'impression qu'il y a des différences entre Kind et Genre.

Au fait, si un crate ne renvoie qu'un seul type, nous pouvons utiliser son appel comme type directement.

package lib

// export a type
crate List [T *] * {
    type List struct {
        ...
    }

    export List
}

utilise le:

import "lib"

var l lib.List[int]

Il y aurait des rÚgles de "déduction de genre", tout comme la "déduction de type" dans le systÚme actuel.

@dotaheor , @DemiMarie a raison. Votre concept de "genre" ressemble exactement au "genre" de la théorie des types. (Votre proposition nécessite une rÚgle de sous-type, mais ce n'est pas rare.)

Le mot-clé genre de votre proposition définit les nouveaux genres comme des super-genres de genres existants. Le mot-clé crate définit les objets avec des "signatures de caisse", qui sont un genre qui n'est pas un sous-genre de Type .

En tant que systÚme formel, votre proposition ressemble à quelque chose comme :

Caisse ::= χ | ⋯
Tapez ::= τ | χ | int | bool | ⋯ | func(τ) | func(τ) τ | []τ | χ[τ₁, 
]

CaisseSig ::= [Îș₁, 
] ⇒ [Îșₙ, 
]
Genre ::= Îș | exactly τ | kindOf Îș | Map | Chan | ⋯ | Const Îș | Type | CaisseSig

Pour abuser de la notation de la théorie des types :

  • Lire "⊱" comme "implique".
  • Lisez « k1 ⊑ k2 » comme « k1 est un sous-type de k2 ».
  • Lisez ":" comme "est de la sorte".

Ensuite, les rÚgles ressemblent à quelque chose comme :

⊱ τ : exactly τ
⊱ exactly τ ⊑ kindOf exactly τ
⊱ kindOf exactly τ ⊑ Type

τ : Îș₁ ∧ Îș₁ ⊑ Îș₂ ⊱ τ : Îș₂

τ₁ : Type ∧ τ₂ : Type ⊱ kindOf exactly map[τ₁]τ₂ ⊑ Map
⊱ Map ⊑ Type

Îș₁ ⊑ Îș₂ ⊱ Const Îș₁ ⊑ Const Îș₂

[
]
(Et ainsi de suite, pour tous les types intégrés)


Les définitions de type confÚrent des genres, et les genres sous-jacents se réduisent aux genres des types intégrés :

type τ₁ τ₂ ∧ τ₂ : Îș ⊱ τ₁ : kindOf Îș

⊱ kindOf kindOf Îș ⊑ kindOf Îș
⊱ kindOf Map ⊑ Map
[
]


genre définit de nouvelles relations de sous-type :
genre Îș = Îș₁ | Îș₂ ⊱ Îș₁ ⊑ Îș
genre Îș = Îș₁ | Îș₂ ⊱ Îș₂ ⊑ Îș

(Vous pouvez définir Numeric et autres en termes de | .)

genre Îș = Îș₁ & Îș₂ ∧ ( Îș₃ ⊑ Îș₁ ) ∧ ( Îș₃ ⊑ Îș₂ ) ⊱ Îș₃ ⊑ Îș


La rÚgle d'expansion de caisse est similaire :
type τₙ, 
 = χ[τ₁, 
] ∧ ( χ : [Îș₁, 
] ⇒ [Îșₙ, 
] ) ∧ ( τ₁ : Îș₁ ) ∧ ⋯ ⊱ τₙ : Îșₙ

Tout cela ne parle que des types, bien sĂ»r. Si vous voulez en faire un systĂšme de type, vous avez Ă©galement besoin de rĂšgles de type. 🙂


Donc, ce que vous dĂ©crivez est une forme assez bien comprise de paramĂ©tricitĂ©. C'est bien, dans la mesure oĂč c'est bien compris, mais dĂ©cevant dans la mesure oĂč cela n'aide pas Ă  rĂ©soudre les problĂšmes uniques que Go introduit.

Les problÚmes vraiment intéressants et noueux que Go introduit concernent principalement l'inspection de type dynamique. Comment les paramÚtres de type doivent-ils interagir avec les assertions de type et la réflexion ?

(Par exemple, devrait-il ĂȘtre possible de dĂ©finir des interfaces avec des mĂ©thodes de types paramĂ©triques ? Si oui, que se passe-t-il si vous tapez une valeur de cette interface avec un nouveau paramĂštre au moment de l'exĂ©cution ?)

Sur une note connexe, y a-t-il eu une discussion sur la façon de rendre le code générique plutÎt que les types intégrés et définis par l'utilisateur ? Comme faire du code qui peut gérer les bigints et les entiers primitifs ?

Sur une note connexe, y a-t-il eu une discussion sur la façon de rendre le code générique plutÎt que les types intégrés et définis par l'utilisateur ? Comme faire du code qui peut gérer les bigints et les entiers primitifs ?

Les mécanismes basés sur les classes de type, comme dans Genus et Familia, peuvent le faire efficacement. Consultez notre article PLDI 2015 pour plus de détails.

@DemiMarie
Je pense que "genre" == "ensemble de traits".

[Éditer]
Peut-ĂȘtre que traits est un meilleur mot clĂ©.
Nous pouvons voir que chaque type est Ă©galement un ensemble de traits.

La plupart des traits sont définis pour un seul type.
Mais un trait plus complexe peut définir une relation entre deux types.

[modifier 2]
supposons qu'il existe deux ensembles de traits A et B, nous pouvons effectuer les opérations suivantes :

A + B: union set
A - B: difference set
A & B: intersection set

L'ensemble de traits d'un type d'argument doit ĂȘtre un super ensemble du genre de paramĂštre correspondant (un ensemble de traits).
L'ensemble de caractĂ©ristiques d'un type de rĂ©sultat doit ĂȘtre un sous-ensemble du genre de rĂ©sultat correspondant (un ensemble de caractĂ©ristiques).

(A MON HUMBLE AVIS)

Pourtant, je pense que la reliure des alias de type est la voie Ă  suivre pour ajouter des gĂ©nĂ©riques Ă  Go. Il n'a pas besoin d'un Ă©norme changement dans la langue. Les packages gĂ©nĂ©ralisĂ©s de cette maniĂšre peuvent toujours ĂȘtre utilisĂ©s dans Go 1.x. Et il n'est pas nĂ©cessaire d'ajouter des contraintes car il est possible de le faire en dĂ©finissant le type par dĂ©faut pour l'alias de type, sur quelque chose qui remplit dĂ©jĂ  ces contraintes. Et l'aspect le plus important des alias de type de reliaison est que les types composites intĂ©grĂ©s (tranches, cartes et canaux) n'ont pas besoin d'ĂȘtre modifiĂ©s et gĂ©nĂ©ralisĂ©s.

@dc0d

Comment les alias de type devraient-ils remplacer les génériques ?

@sighoya Rebinding Type Aliases peut remplacer les génériques (pas seulement les alias de type). Supposons qu'un package introduit des alias de type au niveau du package comme :

package likedlist

type T = interface{}

type LinkedList struct {
    // ...
}

Si Type Alias ​​Rebinding (et les fonctionnalitĂ©s du compilateur) sont fournis, il est alors possible d'utiliser ce package pour crĂ©er des listes chaĂźnĂ©es pour diffĂ©rents types concrets, au lieu d'une interface vide :

package main

import (
    "likedlist"
)

type intLL = likedlist.LinkedList(likedlist.T = int)
type stringLL = likedlist.LinkedList(likedlist.T = string)

func main() {}

Si nous utilisons l'alias en tant que tel, la méthode suivante est plus propre.

// 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 et comment cela serait-il mis en Ɠuvre ? Le code est sympa mais il ne dit rien sur la façon dont il fonctionne rĂ©ellement Ă  l'intĂ©rieur. Et, en regardant l'histoire des propositions de gĂ©nĂ©riques, pour Go c'est trĂšs important, pas seulement son apparence et sa sensation.

@dotaheor C'est incompatible avec Go 1.x.

@creker J'ai implĂ©mentĂ© un outil (nommĂ© goreuse ) qui utilise cette technique pour gĂ©nĂ©rer du code et nĂ© comme un concept pour Type Alias ​​Rebinding.

Il peut ĂȘtre trouvĂ© ici . Il y a une vidĂ©o de 15 minutes qui explique l'outil.

@dc0d donc cela fonctionne un peu comme des modĂšles C++ gĂ©nĂ©rant des implĂ©mentations spĂ©cialisĂ©es. Je ne pense pas que cela serait acceptĂ© car l'Ă©quipe Go (et, franchement, moi et beaucoup d'autres personnes ici) semble ĂȘtre contre tout ce qui ressemble aux modĂšles C++. Il augmente les binaires, ralentit la compilation, ne serait peut-ĂȘtre pas en mesure de produire des erreurs significatives. Et, en plus de cela, n'est pas compatible avec les packages binaires uniquement que Go prend en charge. C'est pourquoi C++ a choisi d'Ă©crire des modĂšles dans les fichiers d'en-tĂȘte.

@creker

cela fonctionne donc un peu comme les modÚles C++ générant des implémentations spécialisées pour chaque type utilisé.

Je ne sais pas (Cela fait environ 16 ans que je n'ai pas Ă©crit de C++). Mais d'aprĂšs votre explication, cela semble ĂȘtre le cas. Pourtant, je ne sais pas si ou comment ils sont les mĂȘmes.

Je ne pense pas que cela serait acceptĂ© car l'Ă©quipe Go (et, franchement, moi et beaucoup d'autres personnes ici) semble ĂȘtre contre tout ce qui ressemble aux modĂšles C++.

Bien sûr, tout le monde ici a de bonnes raisons pour ses préférences en fonction de ses priorités. Le premier sur ma liste est la compatibilité avec Go 1.x.

Il augmente les binaires,

Ça pourrait.

ralentit la compilation,

J'en doute fortement (comme cela peut ĂȘtre vĂ©cu avec goreuse ).

Et, en plus de cela, n'est pas compatible avec les packages binaires uniquement que Go prend en charge.

Je ne suis pas sûr. Est-ce que d'autres façons d'implémenter des génériques supportent cela ?

ne serait peut-ĂȘtre pas en mesure de produire des erreurs significatives.

Cela pourrait ĂȘtre un peu gĂȘnant. Pourtant, cela se produit au moment de la compilation et peut ĂȘtre compensĂ©, en utilisant certains outils, dans une large mesure. De plus, si l'alias de type agissant comme paramĂštre de type pour le package est une interface, il peut simplement ĂȘtre vĂ©rifiĂ© qu'il est assignable Ă  partir du type concret fourni. Bien que le problĂšme pour les types primitifs comme int et string et les structures demeure.

@dc0d

J'y pense un peu.
A cÎté de cela, il est établi en interne sur les interfaces, le 'T' dans votre exemple

type T=interface{}

est traitĂ© comme une variable de type mutable, mais il doit ĂȘtre un alias vers un type spĂ©cifique, c'est-Ă -dire une rĂ©fĂ©rence const Ă  un type.
Ce que vous voulez, c'est le type T, mais cela impliquerait l'introduction de génériques.

@sighoya Je ne suis pas sûr de comprendre ce que vous avez dit.

Il est Ă©tabli en interne sur les interfaces

Pas vrai. Comme dĂ©crit dans mon commentaire d'origine, il est possible d'utiliser des types spĂ©cifiques qui remplissent une contrainte. Par exemple, l'alias de type de paramĂštre de type peut ĂȘtre dĂ©clarĂ© comme :

type T = int

Et seuls les types qui ont l'opĂ©rateur + (ou - ou * ; cela dĂ©pend si cet opĂ©rateur est utilisĂ© dans le corps du package) peuvent ĂȘtre utilisĂ©s comme valeur de type qui se trouve dans ce paramĂštre de type.

Il n'y a donc pas que les interfaces qui peuvent ĂȘtre utilisĂ©es comme espace rĂ©servĂ© pour les paramĂštres de type.

mais cela impliquerait l'introduction de génériques.

Ceci _est_ un moyen d'introduire/implĂ©menter des gĂ©nĂ©riques dans le langage Go lui-mĂȘme.

@dc0d

Pour fournir le polymorphisme, vous utiliserez interface{} car cela permet de définir T sur n'importe quel type ultérieurement.

DĂ©finir 'type T=Int' ne gagnerait pas grand-chose.

Si vous diriez que 'type T' est d'abord non dĂ©clarĂ©/non dĂ©fini, ce qui peut ĂȘtre dĂ©fini plus tard, eh bien, vous avez quelque chose comme des gĂ©nĂ©riques.

Le problĂšme est que 'T' contient le module/paquet Ă  l'Ă©chelle et n'est local Ă  aucune fonction ou structure (d'accord, peut-ĂȘtre une dĂ©claration de type imbriquĂ©e dans une structure accessible de l'extĂ©rieur).

Pourquoi ne pas écrire à la place ? :

fun<type T>(t T)

ou

fun[type T](t T)

De plus, nous avons besoin d'une machinerie d'inférence de type pour déduire les bons types lors de l'appel d'une fonction ou d'un struct générique sans spécialisation de paramÚtre de type au début.

@dc0d a Ă©crit

Et seuls les types qui ont l'opĂ©rateur + (ou - ou * ; cela dĂ©pend si cet opĂ©rateur est utilisĂ© dans le corps du package) peuvent ĂȘtre utilisĂ©s comme valeur de type qui se trouve dans ce paramĂštre de type.

Pouvez-vous en dire plus ?

@sighoya

Pour fournir le polymorphisme, vous utiliserez interface{} car cela permet de définir T sur n'importe quel type ultérieurement.

Le polymorphisme n'est pas atteint en ayant des types compatibles, lors de la reliaison des alias de type. La seule contrainte rĂ©elle est le corps du package gĂ©nĂ©rique. Ils doivent ĂȘtre compatibles mĂ©caniquement.

Pouvez-vous en dire plus ?

Par exemple, si un alias de type de paramÚtre de niveau de package est défini comme :

package genericadd

type T = int

func Add(a, b T) T { return a + b }

Ensuite, pratiquement tous les types numĂ©riques peuvent ĂȘtre affectĂ©s Ă  T , comme :

package main

import (
    "genericadd"
)

var add = genericadd.Add(
    T = float64
)

func main() {
    var (
        a, b float64
    )

    println(add(a, b))
}

@dc0d

Pourtant, je ne sais pas si ou comment ils sont les mĂȘmes.

Ils sont les mĂȘmes dans un sens oĂč ils fonctionnent Ă  peu prĂšs de la mĂȘme maniĂšre d'aprĂšs ce que je vois. Pour chaque modĂšle de classe, le compilateur d'instanciation gĂ©nĂ©rerait une implĂ©mentation unique si c'est la premiĂšre fois qu'il voit l'utilisation de la combinaison particuliĂšre du modĂšle de classe et de sa liste de paramĂštres. Cela augmente la taille binaire car vous avez maintenant plusieurs implĂ©mentations du mĂȘme modĂšle de classe. Ralentit la compilation car le compilateur devrait maintenant gĂ©nĂ©rer ces implĂ©mentations et effectuer toutes sortes de vĂ©rifications. Dans le cas de C++, l'augmentation du temps de compilation peut ĂȘtre Ă©norme. Vos exemples de jouets sont rapides, mais ceux de C++ le sont aussi.

Je ne suis pas sûr. Est-ce que d'autres façons d'implémenter des génériques supportent cela ?

Les autres langues n'ont aucun problÚme avec cela. En particulier, C # comme le plus familier pour moi. Mais il utilise la génération de code d'exécution que l'équipe Go exclut complÚtement. Java fonctionne également mais leur implémentation n'est pas la meilleure, c'est le moins qu'on puisse dire. D'aprÚs ce que j'ai compris, certaines des propositions d'ianlancetaylor pourraient gérer uniquement des packages binaires.

La seule chose que je ne comprends pas, c'est si les packages uniquement binaires doivent ĂȘtre pris en charge. Je ne les vois pas explicitement mentionnĂ©s dans les propositions. Je ne m'en soucie pas vraiment, mais c'est quand mĂȘme une caractĂ©ristique de la langue.

Juste pour tester ma comprĂ©hension... considĂ©rez ce dĂ©pĂŽt d'algorithmes de copier/coller [ ici ]. Sauf si vous souhaitez utiliser "int", le code ne peut pas ĂȘtre utilisĂ© directement. Il doit ĂȘtre copiĂ© et collĂ© et modifiĂ© pour fonctionner. Et par modifications, je veux dire que chaque instance de "int" doit ĂȘtre remplacĂ©e par le type dont vous avez vraiment besoin.

L'approche d'alias de type apporterait les modifications une fois à, disons T, et insérerait une ligne "type T int". Ensuite, le compilateur aurait besoin de relier T à autre chose, disons float64.

Donc:
a) Je dirais qu'il n'y aurait pas de ralentissement du compilateur à moins que vous n'utilisiez réellement cette technique. C'est donc votre choix.
b) Étant donnĂ© le nouveau truc vgo, oĂč plusieurs versions du mĂȘme code peuvent ĂȘtre utilisĂ©es ... ce qui signifie qu'il doit y avoir une mĂ©thode pour cacher les sources utilisĂ©es hors de vue, alors le compilateur peut sĂ»rement garder une trace de si deux des utilisations de la mĂȘme reliure sont utilisĂ©es et Ă©vitent les doublons. Je pense donc que le gonflement du code serait le mĂȘme que les techniques actuelles de copier/coller.

Il me semble qu'entre les alias de type et le vgo à venir, les bases de cette approche des génériques sont quasiment achevées...

Il y a quelques "inconnues" listées dans la proposition [ ici ]. Ce serait donc bien de le détailler un peu plus.

@mandolyte , vous pouvez ajouter un autre niveau d'indirection en enveloppant des types spĂ©cialisĂ©s dans un conteneur gĂ©nĂ©ral. De cette façon, votre implĂ©mentation peut rester la mĂȘme. Le compilateur fera alors toute la magie. Je pense que la proposition de paramĂštres de type de Ian fonctionne de cette façon.

Je pense que l'utilisateur a besoin de choisir entre l'effacement de type et la monomorphisation.
C'est pourquoi Rust fournit des abstractions à coût nul. Allez devrait aussi.

Le lun. 9 avril 2018, 08:32 Antonenko Artem [email protected]
a Ă©crit:

@mandolyte https://github.com/mandolyte vous pouvez ajouter un autre niveau de
l'indirection en enveloppant des types spécialisés dans un conteneur général. Cette
façon dont votre implĂ©mentation peut rester la mĂȘme. Le compilateur fera alors tout
la magie. Je pense que la proposition de paramÚtres de type de Ian fonctionne de cette façon.

—
Vous recevez ceci parce que vous avez été mentionné.
RĂ©pondez directement Ă  cet e-mail, consultez-le sur GitHub
https://github.com/golang/go/issues/15292#issuecomment-379735199 , ou muet
le fil
https://github.com/notifications/unsubscribe-auth/AGGWB1v9h5kWmuHCBuoewTTSX751OHgrks5tm1TsgaJpZM4IG-xv
.

Il me semble qu'il y a une confusion compréhensible dans cette discussion sur le compromis entre modularité et performance. La technique C++ de re-vérification de type et d'instanciation du code générique à chaque type pour lequel il est utilisé est mauvaise pour la modularité, mauvaise pour les distributions binaires et, à cause du gonflement du code, mauvaise pour les performances. La bonne partie de cette approche est qu'elle spécialise automatiquement le code généré aux types utilisés, ce qui est particuliÚrement utile lorsque les types utilisés sont des types primitifs comme int . Java traduit de maniÚre homogÚne le code générique, mais paie un prix en termes de performances, en particulier lorsque le code utilise le type T[] .

Heureusement, il existe plusieurs façons de résoudre ce problÚme sans la non-modularité de C++ et sans génération de code d'exécution complÚte :

  1. GĂ©nĂ©rez des instanciations spĂ©cialisĂ©es pour les types primitifs. Cela peut ĂȘtre fait automatiquement ou par une directive du programmeur. Un certain dispatching est nĂ©cessaire pour accĂ©der Ă  l'instanciation correcte, mais peut ĂȘtre intĂ©grĂ© au dispatching dĂ©jĂ  nĂ©cessaire par une traduction homogĂšne. Cela fonctionnerait de la mĂȘme maniĂšre que C #, mais ne nĂ©cessite pas de gĂ©nĂ©ration de code d'exĂ©cution complĂšte ; une petite prise en charge supplĂ©mentaire peut ĂȘtre souhaitable dans le runtime pour configurer les tables de rĂ©partition lors des chargements de code.
  2. Utilisez une implémentation générique unique dans laquelle un tableau de T est en fait représenté comme un tableau d'un type primitif lorsque T est instancié comme un type primitif. Cette approche, que nous avons utilisée dans PolyJ, Genus et Familia, améliore considérablement les performances par rapport à l'approche Java, bien qu'elle ne soit pas aussi rapide qu'une implémentation entiÚrement spécialisée.

@dc0d

Le polymorphisme n'est pas atteint en ayant des types compatibles, lors de la reliaison des alias de type. La seule contrainte rĂ©elle est le corps du package gĂ©nĂ©rique. Ils doivent ĂȘtre compatibles mĂ©caniquement.

Les alias de type ne sont pas le bon moyen, car il devrait s'agir d'une référence constante.
Il est préférable d'écrire 'T Type' directement et vous voyez alors que vous utilisez effectivement des génériques.

Pourquoi voulez-vous utiliser une variable de type globale 'T' pour l'ensemble du package/module, les variables de type locales dans <> ou [] sont plus modulaires.

@creker

En particulier, C # comme le plus familier pour moi. Mais il utilise la génération de code d'exécution que l'équipe Go exclut complÚtement.

Pour les types référence, mais pas pour les types valeur.

@DemiMarie

Je pense que l'utilisateur a besoin de choisir entre l'effacement de type et la monomorphisation.
C'est pourquoi Rust fournit des abstractions à coût nul. Allez devrait aussi.

"Type Erasure" est ambigu, je suppose que vous voulez dire Type Parameter Erasure, la chose que Java fournit qui n'est pas non plus tout Ă  fait vraie.
Java a une monomorphisation, mais il monomorphise (semi) constamment jusqu'à la limite supérieure de la contrainte générique qui est principalement Object.
Pour fournir des méthodes et des champs d'autres types, la limite supérieure est convertie en interne dans votre type approprié, ce qui est assez moche.
Si le projet Valhalla est accepté, les choses changeront pour les types valeur mais malheureusement pas pour les types référence.

Go n'est pas obligé de suivre Java Way car :

"La compatibilité binaire des packages compilés n'est pas garantie entre les versions"

alors que ce n'est pas possible en Java.

Il me semble qu'il y a une confusion compréhensible dans cette discussion sur le compromis entre modularité et performance. La technique C++ de re-vérification de type et d'instanciation du code générique à chaque type pour lequel il est utilisé est mauvaise pour la modularité, mauvaise pour les distributions binaires et, à cause du gonflement du code, mauvaise pour les performances.

De quel type de performance parlez-vous ici ?

Si par "gonflement du code" et "performances", vous entendez "taille binaire" et "pression du cache d'instructions", alors le problĂšme est assez simple Ă  rĂ©soudre : tant que vous ne conservez pas trop d'informations de dĂ©bogage pour chaque spĂ©cialisation, vous pouvez regrouper les fonctions avec les mĂȘmes corps dans la mĂȘme fonction au moment de la liaison (ce que l'on appelle le "modĂšle de Borland" ). Cela gĂšre trivialement les spĂ©cialisations pour les types primitifs et les types sans appels Ă  des mĂ©thodes non triviales.

Si par "gonflement du code" et "performances", vous entendez "taille d'entrée de l'éditeur de liens" et "temps de liaison", alors le problÚme est également assez simple, si vous pouvez faire certaines hypothÚses (raisonnables) sur votre systÚme de construction. Au lieu d'émettre chaque spécialisation dans chaque unité de compilation, vous pouvez à la place émettre une liste des spécialisations nécessaires et faire en sorte que le systÚme de construction instancie chaque spécialisation unique exactement une fois avant la liaison (le "modÚle Cfront"). IIRC, c'est l'un des problÚmes que les modules C++ tentent de résoudre.

Donc, à moins que vous ne parliez d'un troisiÚme type de "gonflement du code" et de "performances" que j'ai manqué, il semble que vous parliez d'un problÚme avec l'implémentation, pas la spécification : _tant que l'implémentation ne conserve pas trop le débogage informations,_ les problÚmes de performances sont assez simples à résoudre.


Le plus gros problĂšme pour Go est que, si nous ne faisons pas attention, il devient possible d'utiliser des assertions de type ou une rĂ©flexion pour produire une nouvelle instance d'un type paramĂ©trĂ© au moment de l'exĂ©cution, ce qui ne nĂ©cessite aucune intelligence d'implĂ©mentation - Ă  moins d'un ensemble coĂ»teux. ‐analyse de programme — peut rĂ©parer.

C'est en effet un échec de la modularité, mais cela n'a ~rien à voir avec le gonflement du code : au lieu de cela, cela vient du fait que les types de fonctions Go (et de méthodes) ne capturent pas un ensemble suffisamment complet de contraintes sur leurs arguments.

@sighoya

Pour les types référence, mais pas pour les types valeur.

D'aprĂšs ce que j'ai lu, C # JIT effectue une spĂ©cialisation au moment de l'exĂ©cution pour chaque type de valeur et une fois pour tous les types de rĂ©fĂ©rence. Il n'y a pas de spĂ©cialisation au moment de la compilation (IL-time). C'est pourquoi l'approche C # est complĂštement ignorĂ©e - l'Ă©quipe Go ne veut pas dĂ©pendre de la gĂ©nĂ©ration de code d'exĂ©cution car elle limite les plates-formes sur lesquelles Go peut s'exĂ©cuter. En particulier, sur iOS, vous n'ĂȘtes pas autorisĂ© Ă  gĂ©nĂ©rer du code lors de l'exĂ©cution. Cela fonctionne et j'en ai fait une partie, mais Apple ne l'autorise pas dans l'AppStore.

Comment avez-vous fait?

Le lun. 9 avril 2018, 15:41 Antonenko Artem [email protected]
a Ă©crit:

@sighoya https://github.com/sighoya

Pour les types référence, mais pas pour les types valeur.

D'aprÚs ce que j'ai lu, C # JIT effectue une spécialisation au moment de l'exécution pour chaque valeur
type et une fois pour tous les types de référence. Il n'y a pas de temps de compilation
spécialisation. C'est pourquoi l'approche C # est complÚtement ignorée - Go team
ne veut pas dépendre de la génération de code d'exécution car cela limite les plates-formes Go
peut fonctionner. En particulier, sur iOS, vous n'ĂȘtes pas autorisĂ© Ă  gĂ©nĂ©rer du code
à l'exécution. Cela fonctionne et j'en ai fait une partie mais Apple ne le permet pas
dans l'AppStore.

—
Vous recevez ceci parce que vous avez été mentionné.
RĂ©pondez directement Ă  cet e-mail, consultez-le sur GitHub
https://github.com/golang/go/issues/15292#issuecomment-379870005 , ou muet
le fil
https://github.com/notifications/unsubscribe-auth/AGGWB-tslGeUSGXl2ZlEDLf0dCATUaYvks5tm7lvgaJpZM4IG-xv
.

@DemiMarie a lancĂ© mon ancien code de recherche juste pour ĂȘtre sĂ»r (que la recherche a Ă©tĂ© abandonnĂ©e pour d'autres raisons). Encore une fois, le dĂ©bogueur m'a induit en erreur. J'alloue une page, j'y Ă©cris des instructions, je la protĂšge avec PROT_EXEC et j'y saute. Sous dĂ©bogueur, cela fonctionne. Sans le dĂ©bogueur, l'application est SIGKILLed avec le message CODESIGN dans le journal des plantages, comme prĂ©vu. Donc, cela ne fonctionne pas mĂȘme sans AppStore. Argument encore plus fort contre la gĂ©nĂ©ration de code d'exĂ©cution si iOS est important pour Go.

Tout d'abord, il serait utile de réfléchir une fois de plus aux 5 rÚgles de programmation de Rob Pike .

DeuxiĂšmement (Ă  mon humble avis):

À propos de la lenteur de la compilation et de la taille binaire, combien de types gĂ©nĂ©riques sont utilisĂ©s dans les types courants d'applications dĂ©veloppĂ©es Ă  l'aide de Go (_n est gĂ©nĂ©ralement petit_ d'aprĂšs la rĂšgle 3) ? À moins que le problĂšme n'ait besoin d'un niveau Ă©levĂ© de cardinalitĂ© dans des concepts concrets (nombre Ă©levĂ© de types), cette surcharge peut ĂȘtre nĂ©gligĂ©e. MĂȘme alors, je dirais que quelque chose ne va pas avec cette approche. Lors de la mise en Ɠuvre d'un systĂšme de commerce Ă©lectronique, personne ne dĂ©finit un type distinct pour chaque type de produit et ses variations et peut-ĂȘtre les personnalisations possibles.

La verbositĂ© est une bonne forme de simplicitĂ© et de familiaritĂ© (par exemple dans la syntaxe) qui rend les choses plus Ă©videntes et plus propres. Bien que je doute que le gonflement du code soit plus Ă©levĂ© en utilisant Type Alias ​​Rebinding, j'aime la syntaxe Go-ish familiĂšre et la verbositĂ© Ă©vidente qui l'accompagne. L'un des objectifs de Go est d'ĂȘtre facile Ă  lire (alors que je trouve personnellement qu'il est relativement facile et agrĂ©able d'Ă©crire aussi).

Je ne comprends pas comment cela peut nuire aux performances car au moment de l'exécution, seuls des types bornés concrets sont utilisés qui ont été générés au moment de la compilation. Il n'y a pas de surcharge d'exécution.

Le seul problĂšme avec Type Alias ​​Rebinding que je vois, pourrait ĂȘtre la distribution binaire.

@ dc0d les dommages aux performances signifient généralement le remplissage du cache d'instructions en raison de différentes implémentations de modÚles de classe. Comment cela se rapporte-t-il exactement à la performance réelle est une question ouverte, je ne connais aucun point de repÚre, mais théoriquement, c'est un problÚme.

Quant Ă  la taille binaire. C'est un autre problĂšme thĂ©orique que les gens soulĂšvent gĂ©nĂ©ralement (comme je l'ai fait plus tĂŽt), mais comment le vrai code en souffrira est, encore une fois, une question ouverte. Par exemple, la spĂ©cialisation pour tous les types de pointeurs et d'interfaces pourrait ĂȘtre la mĂȘme, je pense. Mais la spĂ©cialisation pour tous les types de valeur serait unique. Et cela inclut Ă©galement les structures. L'utilisation de conteneurs gĂ©nĂ©riques pour les stocker est courante et entraĂźnerait un gonflement important du code, car les implĂ©mentations de conteneurs gĂ©nĂ©riques ne sont pas petites.

Le seul problĂšme avec Type Alias ​​Rebinding que je vois, pourrait ĂȘtre la distribution binaire.

Ici, je ne suis toujours pas sûr. La proposition de génériques doit-elle prendre en charge les packages uniquement binaires ou nous pourrions simplement mentionner que les packages uniquement binaires ne prennent pas en charge les génériques. Ce serait beaucoup plus facile, c'est certain.

Comme mentionné précédemment, si l'on n'a pas besoin de supporter le débogage, on
peut combiner des instanciations de modĂšles identiques.

Le mar. 10 avril 2018, 05:46 Kaveh Shahbazian [email protected]
a Ă©crit:

Tout d'abord, il serait utile de réfléchir aux 5 rÚgles de programmation de Rob Pike
https://users.ece.utexas.edu/%7Eadnan/pike.html une fois de plus.

DeuxiĂšmement (Ă  mon humble avis):

À propos de la compilation lente et de la taille binaire, combien de types gĂ©nĂ©riques sont utilisĂ©s dans
types courants d'applications dĂ©veloppĂ©es Ă  l'aide de Go ( n estgĂ©nĂ©ralement petit de la rĂšgle 3) ? À moins que le problĂšme nĂ©cessite un niveau Ă©levĂ© de
cardinalité dans les concepts concrets (nombre élevé de types) que les frais généraux peuvent
ĂȘtre nĂ©gligĂ©. MĂȘme alors, je dirais que quelque chose ne va pas avec ça
approcher. Lors de la mise en Ɠuvre d'un systĂšme de commerce Ă©lectronique, personne ne dĂ©finit un
type pour chaque type de produit et ses variations et peut-ĂȘtre le possible
personnalisations.

La verbosité est une bonne forme de simplicité et de familiarité (par exemple dans
syntaxe) qui rend les choses plus Ă©videntes et plus propres. Alors que j'en doute
le gonflement du code serait plus Ă©levĂ© en utilisant Type Alias ​​Rebinding, j'aime bien le
syntaxe Go-ish familiÚre et la verbosité évidente qui l'accompagne. Un des
les objectifs de Go sont faciles Ă  lire (alors que je le trouve personnellement
relativement facile et agréable à écrire aussi).

Je ne comprends pas comment cela peut nuire aux performances car lors de l'exécution, seul
des types bornés concrets sont utilisés qui ont été générés à
au moment de la compilation. Il n'y a pas de surcharge d'exécution.

Le seul problĂšme avec Type Alias ​​Rebinding que je vois, pourrait ĂȘtre le binaire
Distribution.

—
Vous recevez ceci parce que vous avez été mentionné.
RĂ©pondez directement Ă  cet e-mail, consultez-le sur GitHub
https://github.com/golang/go/issues/15292#issuecomment-380040032 , ou muet
le fil
https://github.com/notifications/unsubscribe-auth/AGGWB6aDfoHz2wbsmu8mCGEt652G_VE9ks5tnH9xgaJpZM4IG-xv
.

Les instanciations n'ont mĂȘme pas besoin d'ĂȘtre "identiques" dans le sens "d'utiliser les mĂȘmes arguments", ou mĂȘme "d'utiliser des arguments avec le mĂȘme type sous-jacent". Ils doivent juste ĂȘtre suffisamment proches pour aboutir au mĂȘme code gĂ©nĂ©rĂ©. (Pour Go, cela implique Ă©galement "les mĂȘmes masques de pointeur".)

@creker

D'aprÚs ce que j'ai lu, C # JIT effectue une spécialisation au moment de l'exécution pour chaque type de valeur et une fois pour tous les types de référence. Il n'y a pas de spécialisation au moment de la compilation (IL-time).

Eh bien, c'est parfois un peu compliqué car leur byte code est interprété juste à temps avant l'exécution du code, donc la génération de code se fait avant l'exécution du programme mais aprÚs la compilation, donc vous avez raison dans le sens de la vm qui s'exécute pendant que le code est généré.

Je pense que le systÚme générique de c # serait parfait si nous générions plutÎt du code au moment de la compilation.
La génération de code d'exécution au sens de c# n'est pas possible avec go, car go n'est pas une machine virtuelle.

@dc0d

Le seul problĂšme avec Type Alias ​​Rebinding que je vois, pourrait ĂȘtre la distribution binaire.

Pouvez-vous Ă©laborer un peu.

@sighoya Mon erreur ; Je ne parlais pas de distribution binaire mais de packages binaires - dont personnellement je n'ai aucune idée de l'importance.

@creker Joli rĂ©sumé ! (MO) À moins qu'une raison valable ne soit trouvĂ©e, toute forme de surcharge des constructions du langage Go doit ĂȘtre Ă©vitĂ©e. L'une des raisons d'utiliser Type Alias ​​Rebinding est d'Ă©viter de surcharger les types composites intĂ©grĂ©s tels que les tranches ou les cartes.

La verbositĂ© est une bonne forme de simplicitĂ© et de familiaritĂ© (par exemple dans la syntaxe) qui rend les choses plus Ă©videntes et plus propres. Bien que je doute que le gonflement du code soit plus Ă©levĂ© en utilisant Type Alias ​​Rebinding, j'aime la syntaxe Go-ish familiĂšre et la verbositĂ© Ă©vidente qui l'accompagne. L'un des objectifs de Go est d'ĂȘtre facile Ă  lire (alors que je trouve personnellement qu'il est relativement facile et agrĂ©able d'Ă©crire aussi).

Je ne suis pas d'accord avec cette notion. Votre proposition obligera les utilisateurs Ă  faire la chose la plus difficile connue de tout programmeur - nommer les choses. Nous nous retrouverons donc avec un code criblĂ© de notation hongroise, qui non seulement a l'air mauvais, mais qui est inutilement verbeux et provoque des bĂ©gaiements. De plus, d'autres propositions apportent Ă©galement une syntaxe go-ish, et en mĂȘme temps n'ont pas ces problĂšmes.

Il existe trois catégories de noms que nous devons inventer au quotidien :

  • Pour les entitĂ©s de domaine/la logique
  • Types de donnĂ©es/logique du flux de travail du programme
  • Services/Types de donnĂ©es d'interfaçage/Logique

Combien de fois un programmeur avait-il réussi à éviter de nommer quoi que ce soit dans son code, jamais ?

Difficile ou pas, cela doit ĂȘtre fait quotidiennement. Et la plupart de ses obstacles proviennent de l'incompĂ©tence Ă  structurer une base de code - et non des difficultĂ©s du processus de nommage lui-mĂȘme. Cette citation - du moins dans sa forme actuelle - a rendu un trĂšs mauvais service au monde de la programmation jusqu'Ă  prĂ©sent. Il essaie simplement de souligner l'importance de nommer. Parce que nous communiquons via des noms dans notre code.

Et les noms deviennent d'autant plus puissants lorsqu'ils accompagnent une pratique de structuration de code ; à la fois en termes de disposition du code (un fichier, une structure de répertoires, des packages/modules) et des pratiques (modÚles de conception, abstractions de service - telles que REST, gestion des ressources - programmation simultanée, accÚs au disque dur, débit/latence).

En ce qui concerne la syntaxe et la verbositĂ©, je privilĂ©gie la verbositĂ© Ă  une concision intelligente (au moins dans le contexte de Go) - encore une fois, Go est censĂ© ĂȘtre facile Ă  lire, pas nĂ©cessairement facile Ă  Ă©crire (ce qui Ă©trangement, je le trouve bien aussi) .

J'ai lu beaucoup de rapports d'expérience et de propositions sur pourquoi et comment implémenter des génériques dans Go.

Cela vous dérange-t-il si j'essaie de les implémenter dans ma gomacro d' interpréteur Go ?

J'ai une certaine expérience sur le sujet, ayant ajouté des génériques à deux langues dans le passé

  1. un langage maintenant abandonné que j'ai créé quand j'étais naïf :) Il s'est transpilé en code source C
  2. Common Lisp avec ma bibliothÚque cl-parametric-types - il prend également en charge les spécialisations partielles et complÚtes des types et fonctions génériques

@cosmos72 ça ferait un beau reportage d'expérience de voir un prototype d'une technique qui a préservé la sécurité de type.

Je viens de commencer Ă  travailler dessus. Vous pouvez suivre l'avancement sur https://github.com/cosmos72/gomacro/tree/generics-v1

Pour le moment, je commence par un mélange (légÚrement modifié) des troisiÚme et quatriÚme propositions d'Ian répertoriées sur https://github.com/golang/proposal/blob/master/design/15292-generics.md#Proposal

@cosmos72 Il y a un résumé des propositions sur le lien ci-dessous. Votre mélange en fait-il partie ?
https://docs.google.com/document/d/1vrAy9gMpMoS3uaVphB32uVXX4pi-HnNjkMEgyAHX4N4

J'ai lu ce document, il résume de nombreuses approches différentes des génériques par divers langages de programmation.

Pour le moment, je me dirige vers la technique de "spécialisation de type" utilisée par C++, Rust et d'autres, éventuellement avec un peu de "portées de modÚle paramétrées" car Go la syntaxe la plus générale pour les nouveaux types est type ( Foo ...; Bar ...) et j'étends à template[T1,T2...] type ( Foo ...; Bar ...) .
Aussi, je garde la porte ouverte à la "spécialisation contrainte".

Je voudrais Ă©galement implĂ©menter la "spĂ©cialisation de la fonction polymorphe", c'est-Ă -dire faire en sorte que la spĂ©cialisation soit automatiquement dĂ©duite par la langue sur le site d'appel si elle n'est pas spĂ©cifiĂ©e par le programmeur, mais je suppose que cela peut ĂȘtre quelque peu complexe Ă  implĂ©menter. Nous verrons.

Le mélange auquel je faisais référence est entre https://github.com/golang/proposal/blob/master/design/15292/2013-10-gen.md et https://github.com/golang/proposal/blob/ master/design/15292/2013-12-type-params.md

Mise à jour : pour éviter de spammer ce problÚme officiel de Go au-delà de l'annonce initiale, il est probablement préférable de poursuivre la discussion spécifique à gomacro sur le problÚme gomacro 24 : ajouter des génériques

Mise à jour 2 : premiÚres fonctions de modÚle compilées et exécutées avec succÚs. Voir https://github.com/cosmos72/gomacro/tree/generics-v1

Pour mĂ©moire, il est possible de reformuler mon avis (sur les gĂ©nĂ©riques et Type Alias ​​Rebinding) :

Les gĂ©nĂ©riques doivent ĂȘtre ajoutĂ©s en tant que fonctionnalitĂ© du compilateur (gĂ©nĂ©ration de code, modĂšles, etc.), et non en tant que fonctionnalitĂ© du langage (ingĂ©rence avec le systĂšme de type de Go Ă  tous les niveaux).

@dc0d
Mais les modÚles C++ ne sont-ils pas un compilateur et une fonctionnalité de langage ?

@sighoya La derniĂšre fois que j'ai Ă©crit C++ professionnellement, c'Ă©tait vers 2001. Je me trompe donc peut-ĂȘtre. Mais en supposant que les implications de la dĂ©nomination sont exactes - la partie "modĂšle" - oui (ou plutĂŽt non); il peut s'agir d'une fonctionnalitĂ© de compilateur (et non d'une fonctionnalitĂ© de langage), accompagnĂ©e de certaines constructions de langage, qui ne surchargent probablement pas les constructions de langage impliquĂ©es dans le systĂšme de type.

Je soutiens @dc0d. Si vous le considérez, cette fonctionnalité ne serait rien de plus qu'un générateur de code intégré.

Oui : la taille binaire peut augmenter et VA augmenter, mais pour le moment, nous utilisons des gĂ©nĂ©rateurs de code, qui sont Ă  peu prĂšs les mĂȘmes mais en tant que fonctionnalitĂ© externe. Si je dois crĂ©er mon modĂšle en tant que :

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

... J'aimerais sĂ©rieusement que, plutĂŽt que de copier-coller ou d'utiliser un outil externe, cette fonctionnalitĂ© fasse partie du compilateur lui-mĂȘme.

Veuillez noter:

  • Oui, le code de fin serait dupliquĂ©. Exactement comme si nous utilisions un gĂ©nĂ©rateur. Et le binaire serait plus grand.
  • Oui, l'idĂ©e n'est pas originale, mais empruntĂ©e au C++.
  • Oui, fonctions de MyTypen'impliquant rien avec le type T (directement ou indirectement) serait Ă©galement rĂ©pĂ©tĂ©. Cela pourrait ĂȘtre optimisĂ© (par exemple, les mĂ©thodes qui font rĂ©fĂ©rence Ă  quelque chose de type T -autre que le pointeur vers l'objet de rĂ©ception de message- seront gĂ©nĂ©rĂ©es pour chaque T ; les mĂ©thodes qui contiennent des invocations Ă  des mĂ©thodes qui ĂȘtre gĂ©nĂ©rĂ© pour chaque T , sera Ă©galement gĂ©nĂ©rĂ© pour chaque T , de maniĂšre rĂ©cursive - tandis que les mĂ©thodes oĂč leur seule rĂ©fĂ©rence Ă  T est *T dans le rĂ©cepteur, et d'autres mĂ©thodes n'appelant que ces mĂ©thodes sĂ»res et satisfaisant aux mĂȘmes critĂšres, ne pourront ĂȘtre faites qu'une seule fois). Quoi qu'il en soit, IMO ce point est important et moins pertinent : je serais plutĂŽt content mĂȘme si cette optimisation n'existe pas.
  • Les arguments de type doivent ĂȘtre explicites Ă  mon avis. Surtout quand un objet satisfait des interfaces potentiellement infinies. Encore une fois : un gĂ©nĂ©rateur de code.

Jusqu'à présent dans mon commentaire, ma proposition est de l'implémenter tel quel : en tant que générateur de code pris en charge par le compilateur , au lieu d'un outil externe.

Il serait dommage que Go suive la route C++. Beaucoup de gens considĂšrent l'approche C++ comme un gĂąchis qui a retournĂ© les programmeurs contre toute l'idĂ©e des gĂ©nĂ©riques : difficultĂ© de dĂ©bogage, manque de modularitĂ©, gonflement du code. Toutes les solutions de "gĂ©nĂ©rateur de code" ne sont en rĂ©alitĂ© que des substitutions de macros - si c'est ainsi que vous voulez Ă©crire du code, pourquoi avons-nous mĂȘme besoin du support du compilateur ?

@andrewcmyers J'ai eu cette proposition Type Alias ​​Rebinding dans laquelle nous Ă©crivons uniquement des packages normaux et au lieu d'utiliser interface{} explicitement, nous l'utilisons simplement comme type T = interface{} comme paramĂštre gĂ©nĂ©rique au niveau du package. Et c'est tout.

  • Nous le dĂ©boguons comme un paquet normal - c'est du code rĂ©el, pas une crĂ©ature Ă  demi-vie intermĂ©diaire.
  • Il n'est pas nĂ©cessaire de se mĂȘler du systĂšme de type Go Ă  tous les niveaux - pensez uniquement Ă  l'assignabilitĂ©.
  • C'est explicite. Pas de mojo cachĂ©. Bien sĂ»r, on pourrait trouver que le fait de ne pas pouvoir enchaĂźner les appels gĂ©nĂ©riques de maniĂšre transparente est un inconvĂ©nient. Je le vois comme un tirage au sort ! Changer de type dans deux appels consĂ©cutifs, dans une seule dĂ©claration n'est pas Goish (IMO).
  • Et surtout, il est rĂ©trocompatible avec la sĂ©rie Go 1.x (x >= 8).

Si l'idĂ©e n'est pas nouvelle, la maniĂšre dont Go permet de la mettre en Ɠuvre est pragmatique et claire.

Autre bonus : il n'y a pas de surcharge d'opĂ©rateurs en Go. Mais en dĂ©finissant la valeur par dĂ©faut de l'alias de type comme (par exemple) type T = int , ce ne sont pas les seuls types valides qui peuvent ĂȘtre utilisĂ©s pour personnaliser ce package gĂ©nĂ©rique, ce sont les types numĂ©riques qui ont une implĂ©mentation interne pour + OpĂ©rateur

De plus, le paramĂštre de type d'alias peut ĂȘtre forcĂ© Ă  remplir plus d'une interface simplement en ajoutant des types et des instructions de validateur.

Maintenant, ce serait super moche d'utiliser n'importe quelle notation explicite pour un type générique qui a un paramÚtre qui implémente les interfaces Error et Stringer et est également un type numérique qui prend en charge l'opérateur + !

en ce moment, nous utilisons des gĂ©nĂ©rateurs de code, qui sont Ă  peu prĂšs les mĂȘmes mais en tant que fonctionnalitĂ© externe.

La diffĂ©rence Ă©tant que la maniĂšre largement acceptĂ©e de gĂ©nĂ©rer du code (via go generate ) se produit au moment de la validation/du dĂ©veloppement, et non au moment de la compilation. Le faire au moment de la compilation implique que vous devez autoriser l'exĂ©cution de code arbitraire dans le compilateur, les bibliothĂšques peuvent exploser les temps de compilation par ordre de grandeur et/ou vous aurez des dĂ©pendances de construction distinctes (c'est-Ă -dire que le code ne peut plus ĂȘtre construit uniquement avec le Go outil). J'aime Go pour pousser l'invocation de la mĂ©ta-programmation au dĂ©veloppeur en amont.

Autrement dit, comme toutes les approches pour résoudre ces problÚmes, cette approche a également des inconvénients et implique des compromis. Personnellement, je dirais que les génériques réels avec prise en charge dans le systÚme de type sont non seulement meilleurs (c'est-à-dire qu'ils ont un ensemble de fonctionnalités plus puissant), mais peuvent également conserver l'avantage d'une compilation prévisible et sûre.

Je vais lire tout ce qui précÚde, je le promets, et pourtant j'ajouterai un peu aussi - GoLang SDK pour Apache Beam semble un exemple/une vitrine plutÎt brillant des problÚmes que le concepteur de bibliothÚque doit endurer pour réussir quoi que ce soit de _correctement_ de haut niveau.

Il existe au moins deux implĂ©mentations expĂ©rimentales pour les gĂ©nĂ©riques Go. Plus tĂŽt cette semaine, j'ai passĂ© du temps avec (1). J'ai Ă©tĂ© ravi de constater que l'impact du code sur la lisibilitĂ© Ă©tait minime. Et j'ai trouvĂ© que l'utilisation de fonctions anonymes pour fournir des tests d'Ă©galitĂ© fonctionnait bien; donc je suis convaincu que la surcharge de l'opĂ©rateur n'est pas nĂ©cessaire. Le seul problĂšme que j'ai trouvĂ© Ă©tait dans la gestion des erreurs. L'idiome commun de "return nil,err" ne fonctionnera pas si le type est, par exemple, un entier ou une chaĂźne. Il existe plusieurs façons de contourner ce problĂšme, toutes avec un coĂ»t de complexitĂ©. Je suis peut-ĂȘtre un peu bizarre, mais j'aime la gestion des erreurs de Go. Cela m'amĂšne donc Ă  observer qu'une solution gĂ©nĂ©rique Go devrait avoir un mot-clĂ© universel pour la valeur zĂ©ro d'un type. Le compilateur le remplacerait simplement par zĂ©ro pour les types numĂ©riques, une chaĂźne vide pour les types de chaĂźne et nil pour les structures.

Bien que cette implémentation n'impose pas une approche au niveau du package, il serait certainement naturel de le faire. Et, bien sûr, cette implémentation n'abordait pas tous les détails techniques sur l'emplacement du code instancié du compilateur (le cas échéant), le fonctionnement des débogueurs de code, etc.

C'Ă©tait plutĂŽt agrĂ©able d'utiliser le mĂȘme code d'algorithme pour les entiers et quelque chose comme un point :

type Point struct {
    x,y int
}

Voir (2) pour mes tests et mes observations.

(1) https://github.com/albrow/fo ; l'autre est le https://github.com/cosmos72/gomacro#generics susmentionné
(2) https://github.com/mandolyte/fo-experiments

@mandolyte Vous pouvez utiliser *new(T) pour obtenir la valeur zéro de n'importe quel type.

Une construction de langage comme default(T) ou zero(T) (la premiĂšre est celle
en C# IIRC) serait clair, mais OTOH plus long que *new(T) (bien que plus
performantes).

2018-07-06 9:15 GMT-05:00 Tom Thorogood [email protected] :

@mandolyte https://github.com/mandolyte Vous pouvez utiliser *new(T) pour obtenir le
valeur zéro de tout type.

—
Vous recevez ceci parce que vous avez commenté.
RĂ©pondez directement Ă  cet e-mail, consultez-le sur GitHub
https://github.com/golang/go/issues/15292#issuecomment-403046735 , ou muet
le fil
https://github.com/notifications/unsubscribe-auth/AlhWhQ5cQwnc3x_XUldyJXCHYzmr6aN3ks5uD3ETgaJpZM4IG-xv
.

--
Ceci est un test pour les signatures de courrier Ă  utiliser dans TripleMint

19642 est pour discuter d'une valeur zéro générique

@tmthrgd D'une maniÚre ou d'une autre, j'ai raté cette petite friandise. Merci!

prélude

Les génériques consistent à spécialiser les constructions personnalisables. Trois catégories de spécialisation sont :

  • Types spĂ©cialisĂ©s, Type<T> - un _tableau_ ;
  • Calculs spĂ©cialisĂ©s, F<T>(T) ou F<T>(Type<T>) - un _tableau triable_ ;
  • Notation spĂ©cialisĂ©e, _LINQ_ par exemple - instructions select ou for en Go ;

Bien sûr, il existe des langages de programmation qui présentent des constructions encore plus génériques. Mais les langages de programmation conventionnels comme _C++_, _C#_ ou _Java_ fournissent plus ou moins de constructions de langage limitées à cette liste.

les pensées

La premiĂšre catĂ©gorie de types/constructions gĂ©nĂ©riques doit ĂȘtre indĂ©pendante du type.

La deuxiĂšme catĂ©gorie de types/constructions gĂ©nĂ©riques doit _agir_ sur une _propriĂ©tĂ©_ du paramĂštre de type. Par exemple, un _tableau triable_ doit pouvoir _comparer_ la _propriĂ©tĂ© comparable_ de ses Ă©lĂ©ments. En supposant T.(P) est une propriĂ©tĂ© de T et que A(T.(P)) est un calcul/action qui agit sur cette propriĂ©tĂ©, le (A, .(P)) peut ĂȘtre appliquĂ© soit Ă  chaque Ă©lĂ©ment individuel ou ĂȘtre dĂ©clarĂ© comme calcul spĂ©cialisĂ©, passĂ© au calcul personnalisable d'origine. Un exemple de ce dernier cas dans Go est l'interface sort.Interface qui a Ă©galement la fonction distincte correspondante sort.Reverse .

La troisiĂšme catĂ©gorie de types/constructions gĂ©nĂ©riques sont les notations de langage _spĂ©cialisĂ©es_ par type - ne semble pas ĂȘtre une chose Go _en gĂ©nĂ©ral_.

des questions

Ă  suivre ...

Tout commentaire plus descriptif qu'un emoji est le bienvenu !

@ dc0d Je recommanderais d'Ă©tudier les "Ă©lĂ©ments de programmation" de Sepanov avant d'essayer de dĂ©finir les gĂ©nĂ©riques. Le TL; DR est que nous Ă©crivons du code concret pour commencer, disons un algorithme qui trie un tableau. Plus tard, nous ajoutons d'autres types de collection comme un Btree, etc. Nous remarquons que nous Ă©crivons de nombreuses copies de l'algorithme de tri qui sont essentiellement les mĂȘmes, nous dĂ©finissons donc un concept, disons "triable". Maintenant, nous voulons catĂ©goriser les algorithmes de tri, peut-ĂȘtre par modĂšle d'accĂšs dont ils ont besoin, disons vers l'avant uniquement, un seul passage (un flux), vers l'avant uniquement plusieurs passages (une liste liĂ©e individuellement), bidirectionnel (une liste doublement liĂ©e), accĂšs alĂ©atoire (un dĂ©ployer). Lorsque nous ajoutons un nouveau type de collection, nous n'avons qu'Ă  indiquer Ă  quelle catĂ©gorie de "coordonnĂ©es" il appartient pour avoir accĂšs Ă  tous les algorithmes de tri pertinents. Ces catĂ©gories d'algorithmes ressemblent beaucoup aux interfaces "Go". Je chercherais Ă  Ă©tendre les interfaces dans Go pour prendre en charge plusieurs paramĂštres de type et les types abstraits/associĂ©s. Je ne pense pas que les fonctions aient besoin d'un paramĂ©trage de type ad hoc.

@ dc0d Pour tenter de diviser les gĂ©nĂ©riques en composants, je n'avais pas considĂ©rĂ© 3, "notation spĂ©cialisĂ©e", comme sa propre partie distincte auparavant. Peut-ĂȘtre pourrait-il ĂȘtre caractĂ©risĂ© comme dĂ©finissant les DSL en utilisant des contraintes de type.

Je pourrais dire que vos 1 et 2 sont respectivement des "structures de donnĂ©es" et des "algorithmes". Avec cette terminologie, il est un peu plus clair pourquoi il peut ĂȘtre difficile de les sĂ©parer proprement, car ils sont souvent trĂšs dĂ©pendants les uns des autres. Mais sort.Interface est un assez bon exemple de l'endroit oĂč vous pouvez tracer une ligne entre le stockage et le comportement (avec un peu de sucre rĂ©cent pour le rendre plus agrĂ©able), car il encode les exigences Indexable et Comparable dans le comportement minimum nĂ©cessaire pour implĂ©menter l'algorithme de tri avec "swap" et "moins" (et len). Mais cela semble s'effondrer sur des structures de donnĂ©es plus compliquĂ©es comme les arbres ou les tas, qui nĂ©cessitent actuellement quelques contorsions pour ĂȘtre mappĂ©s en comportement pur en tant qu'interfaces Go.

Je pourrais imaginer un ajout relativement petit de gĂ©nĂ©riques aux interfaces (ou autrement) qui pourrait permettre Ă  la plupart des structures de donnĂ©es et des algorithmes des manuels d'ĂȘtre implĂ©mentĂ©s relativement proprement sans contorsions (comme sort.Interface l'est aujourd'hui), mais pas assez puissant pour concevoir des DSL. La question de savoir si nous voulons nous limiter Ă  une implĂ©mentation de gĂ©nĂ©riques aussi restreinte alors que nous nous donnons la peine d'ajouter des gĂ©nĂ©riques est une autre question.

Les structures de coordonnĂ©es @infogulch pour les arbres binaires sont des "coordonnĂ©es bifurquantes", et des Ă©quivalents existent pour d'autres arbres. Cependant, vous pouvez Ă©galement projeter la commande d'un arbre Ă  travers l'une des trois commandes, prĂ©-commande, en commande et post-commande. AprĂšs avoir choisi l'un d'entre eux, l'arbre peut ĂȘtre adressĂ© comme une coordonnĂ©e bidirectionnelle, et la famille d'algorithmes de tri dĂ©finis sur des coordonnĂ©es bidirectionnelles serait d'une efficacitĂ© optimale.

Le fait est que vous catégorisez les algorithmes de tri en fonction de leurs modÚles d'accÚs. Il n'y a qu'un nombre fini d'algorithmes de tri optimaux pour chaque modÚle d'accÚs. Vous ne vous souciez pas des structures de données à ce stade. Parler de structures plus complexes passe à cÎté de l'essentiel, nous voulons catégoriser la famille des algorithmes de tri et non les structures de données. Quelles que soient les données dont vous disposez, vous devrez utiliser l'un des algorithmes existants pour les trier. La question est donc de savoir laquelle des catégorisations de modÚles d'accÚs aux données disponibles des algorithmes de tri est optimale pour les structures de données dont vous disposez.

(A MON HUMBLE AVIS)

@infogulch

Peut-ĂȘtre pourrait-il ĂȘtre caractĂ©risĂ© comme dĂ©finissant les DSL en utilisant des contraintes de type

Vous avez raison. Mais comme ils font partie de l'ensemble des constructions de langage, l'OMI les appelant DSL serait un peu inexact.

1 et 2 ... sont souvent trÚs dépendants

Encore vrai. Mais il existe de nombreux cas oĂč il est nĂ©cessaire de transmettre un type de conteneur, alors que l'utilisation rĂ©elle n'est pas encore dĂ©cidĂ©e - Ă  ce stade d'un programme. C'est pourquoi 1 doit ĂȘtre Ă©tudiĂ© seul.

sort.Interface est un assez bon exemple oĂč vous pouvez tracer une ligne entre _storage_ et _behavior_

Bien dit;

cela semble s'effondrer sur des structures de données plus compliquées

C'est une de mes questions : généraliser le paramÚtre de type et le décrire en termes de restrictions (comme List<T> where T:new, IDisposable ) ou fournir un _protocole_ généralisé applicable à tous les éléments (d'un ensemble ; d'un certain type) ?

@keean

la question devient laquelle des catégorisations de modÚles d'accÚs aux données disponibles des algorithmes de tri est optimale pour les structures de données que vous avez

Vrai. L'accĂšs par index est une _propriĂ©tĂ©_ d'une tranche (ou d'un tableau). Ainsi, la premiĂšre exigence pour un conteneur triable (ou un conteneur _tree_ -able quel que soit l'algorithme _tree_) est de fournir un utilitaire _access & mutate (swap)_. La deuxiĂšme exigence est que les Ă©lĂ©ments doivent ĂȘtre comparables. C'est la partie dĂ©routante (pour moi) de ce que vous appelez les algorithmes : les exigences doivent ĂȘtre remplies des deux cĂŽtĂ©s (sur le conteneur et sur le paramĂštre de type). C'est lĂ  que je ne peux pas imaginer une implĂ©mentation pragmatique des gĂ©nĂ©riques en Go. Chaque cĂŽtĂ© du problĂšme peut ĂȘtre parfaitement dĂ©crit en termes d'interfaces. Mais comment combiner ces deux dans une notation efficace ?

Les algorithmes @dc0d nécessitent des interfaces, les structures de données les fournissent. Cela suffit pour une généralité complÚte, à condition que les interfaces soient suffisamment puissantes. Les interfaces sont paramétrées par types, mais vous avez besoin de variables de type.

Prenant l'exemple 'sort', 'Ord' est une propriĂ©tĂ© du type stockĂ© dans le conteneur, pas le conteneur lui-mĂȘme. Le modĂšle d'accĂšs est une propriĂ©tĂ© du conteneur. Les modĂšles d'accĂšs simples sont des « itĂ©rateurs », mais ce nom vient du C++, Stepanov a prĂ©fĂ©rĂ© les « coordonnĂ©es » car il peut ĂȘtre appliquĂ© Ă  des conteneurs multidimensionnels plus complexes.

En essayant de définir le tri, nous voulons quelque chose comme ceci :

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

Remarque : je ne suggÚre pas cette notation, j'essaie simplement d'intégrer d'autres travaux connexes, la clause requirements est dans la syntaxe préférée de Stepanov, le type de fonction provient de Haskell, dont les classes de types représentent probablement une bonne implémentation de ces concepts.

@keean
Je vous ai peut-ĂȘtre mal compris, mais je ne pense pas que vous puissiez simplement restreindre les algorithmes aux interfaces uniquement, du moins dans la maniĂšre dont les interfaces sont dĂ©finies actuellement.
Considérez sort.Slice par exemple, nous sommes intéressés par le tri des tranches, et je ne vois pas comment on construirait une interface qui représenterait toutes les tranches.

@urandom vous faites abstraction des algorithmes et non des collections. Vous demandez donc quels modÚles d'accÚs aux données existent dans les algorithmes de "tri", puis classez-les. Ainsi, peu importe que le conteneur soit une "tranche", nous n'essayons pas de définir toutes les opérations que vous souhaitez effectuer sur une tranche, nous essayons de déterminer les exigences d'un algorithme et de l'utiliser pour définir une interface. Une tranche n'est pas spéciale, c'est juste un type T sur lequel nous pouvons définir un ensemble d'opérations.

Les interfaces sont donc liĂ©es Ă  des bibliothĂšques d'algorithmes, et vous pouvez dĂ©finir vos propres interfaces pour vos propres structures de donnĂ©es afin de pouvoir utiliser ces algorithmes. Les bibliothĂšques peuvent ĂȘtre livrĂ©es avec des interfaces prĂ©dĂ©finies pour les types intĂ©grĂ©s.

@keean
Je pensais que c'Ă©tait ce que tu voulais dire. Mais dans le contexte de Go, cela signifierait probablement qu'il faudrait une refonte importante de ce que les interfaces peuvent dĂ©finir. J'imagine que diverses opĂ©rations intĂ©grĂ©es, telles que des itĂ©rations ou des opĂ©rateurs, devraient ĂȘtre exposĂ©es via des mĂ©thodes pour que des choses comme sort.Slice ou math.Max soient rendues gĂ©nĂ©riques sur les interfaces.

Vous devez donc prendre en charge l'interface suivante (pseudo-code):

type [T] OrderedIterator interface {
   Len() int
   ValueAt(i int) *T
}

...
package sort

func [T] Slice(s [T]OrderedIterator, func(i, j int) bool) {
   ...
}

et toutes les tranches auraient alors ces méthodes ?

@urandom Un itérateur n'est pas une abstraction d'une collection, mais une abstraction de la référence/pointeur dans une collection. Par exemple, l'itérateur avant pourrait avoir une seule méthode 'successeur' (parfois 'suivant'). Pouvoir accéder aux données à l'emplacement d'un itérateur n'est pas une propriété de l'itérateur (sinon vous vous retrouverez avec des versions d'itérateur en lecture/écriture/mutable). Il est préférable de définir les "références" séparément en tant qu'interfaces lisibles, inscriptibles et modifiables :

type T ForwardIterator interface {
   type DistanceType D
   successor(x T) T
}

type T Readable interface {
   type ValueType U 
   source(x T) U
}

Remarque : Le type 'T' n'est pas la tranche, mais le type de l'itĂ©rateur sur la tranche. Cela pourrait simplement ĂȘtre un simple pointeur, si nous adoptons le style C++ consistant Ă  transmettre un itĂ©rateur de dĂ©but et de fin Ă  des fonctions telles que sort.

Pour un itérateur à accÚs aléatoire, nous nous retrouverions avec quelque chose comme :

type T RandomIterator interface {
   type DistanceType D
   setPosition(x DistanceType)
}

Ainsi, un itĂ©rateur/coordonnĂ©e est une abstraction de la rĂ©fĂ©rence Ă  une collection, pas la collection elle-mĂȘme. Le nom 'coordinate' exprime cela assez bien, si vous considĂ©rez l'itĂ©rateur comme la coordonnĂ©e et la collection comme la carte.

Ne vendons-nous pas Go short en ne tirant pas parti des fermetures de fonctions et des fonctions anonymes ? Avoir des fonctions/méthodes comme type de premiÚre classe dans Go peut aider. Par exemple, en utilisant la syntaxe de albrow/fo , un tri à bulles pourrait ressembler à ceci :

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

Veuillez ignorer toutes les erreurs... complÚtement non testées !

@mandolyte Je ne sais pas si cela m'a Ă©tĂ© adressĂ© ? Je ne vois pas vraiment de diffĂ©rence entre ce que je suggĂ©rais et votre exemple, sauf que vous utilisez des interfaces multi-paramĂštres, et je donnais des exemples utilisant des types abstraits/associĂ©s. Pour ĂȘtre clair, je pense que vous avez besoin Ă  la fois d'interfaces multi-paramĂštres et de types abstraits/associĂ©s pour une gĂ©nĂ©ralitĂ© totale, dont aucun n'est actuellement pris en charge par Go.

Je dirais que vos interfaces sont moins gĂ©nĂ©rales que celles que j'ai proposĂ©es, car elles lient l'ordre de tri, le modĂšle d'accĂšs et l'accessibilitĂ© dans la mĂȘme interface, ce qui entraĂźnera bien sĂ»r la prolifĂ©ration d'interfaces, par exemple deux ordres (moins , supĂ©rieur), trois types d'accĂšs (lecture seule, Ă©criture seule, mutable) et cinq modĂšles d'accĂšs (passage unique direct, passage multiple direct, bidirectionnel, indexĂ©, alĂ©atoire) conduiraient Ă  36 interfaces contre seulement 11 si les prĂ©occupations sont sĂ©parĂ©es.

Vous pourriez définir les interfaces que je propose avec des interfaces multi-paramÚtres au lieu de types abstraits comme ceci :

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
}

Notez que le seul qui nĂ©cessite deux paramĂštres de type est l'interface Readable. Cependant, nous perdons cette capacitĂ© pour un objet itĂ©rateur de 'contenir' le type des objets itĂ©rĂ©s, ce qui est un gros problĂšme car maintenant nous devons dĂ©placer le type 'valeur' ​​dans le systĂšme de type, et nous devons le faire correctement . Cela conduit Ă  une prolifĂ©ration de paramĂštres de type qui n'est pas bonne, et cela augmente la possibilitĂ© d'erreurs de codage. Nous perdons Ă©galement la possibilitĂ© de dĂ©finir le 'DistanceType' sur l'itĂ©rateur, qui est le plus petit type de nombre nĂ©cessaire pour compter les Ă©lĂ©ments de la collection, ce qui est utile pour mapper sur int8, int16, int32, etc., pour donner le type dont vous avez besoin pour compter les Ă©lĂ©ments sans dĂ©bordement.

Ceci est Ă©troitement liĂ© au concept de « dĂ©pendance fonctionnelle ». Si un type est fonctionnellement dĂ©pendant d'un autre type, il doit s'agir d'un type abstrait/associĂ©. Ce n'est que si les deux types sont indĂ©pendants qu'ils doivent ĂȘtre des paramĂštres de type distincts.

Quelques problĂšmes:

  1. Impossible d'utiliser la syntaxe f(x I) actuelle pour les interfaces multiparamÚtres. Je n'aime pas que cette syntaxe confond les interfaces (qui sont des contraintes sur les types) avec les types de toute façon.
  2. Il faudrait un moyen de déclarer des types paramétrés.
  3. Il devrait y avoir un moyen de déclarer les types associés pour les interfaces avec un ensemble donné de paramÚtres de type.

@keean Je ne suis pas sûr de comprendre comment ou pourquoi le nombre d'interfaces devient si élevé. Voici un exemple de travail complet : https://play.folang.org/p/BZa6BdsfBgZ (basé sur les tranches, pas sur un conteneur général, donc pas de méthode Next() nécessaire).

Il utilise une seule structure de type, aucune interface du tout. Je dois fournir toutes les fonctions et fermetures anonymes (c'est probablement lĂ  que se trouve le compromis ?). L'exemple utilise le mĂȘme algorithme de tri Ă  bulles pour trier Ă  la fois une tranche d'entiers et une tranche de points "(x,y)", oĂč la distance Ă  l'origine est la base de la fonction Less().

En tout cas, j'espérais montrer comment avoir des fonctions dans le systÚme de type peut aider.

@mandolyte Je pense que j'ai mal compris ce que vous proposiez. Je vois que ce dont vous parlez est "folang" qui a déjà quelques fonctionnalités de programmation fonctionnelles intéressantes ajoutées à Go. Ce que vous avez implémenté consiste essentiellement à créer manuellement une classe de types multiparamÚtres. Vous passez ce qu'on appelle un dictionnaire de fonctions à la fonction de tri. Cela fait explicitement ce qu'une interface ferait implicitement. Ce type de fonctionnalités est probablement nécessaire avant les interfaces multi-paramÚtres et les types associés, mais vous rencontrez éventuellement des problÚmes pour transmettre tous ces dictionnaires. Je pense que les interfaces fournissent un code plus propre et plus lisible.

Trier une tranche est un problÚme résolu. Voici le code d'un slice quicksort.go implémenté à l'aide du langage go-li (golang amélioré) .

func main(){
    var data = []int{5,3,1,8,9}

    Sort(data, func(a *int, b *int) int {
        return *a - *b
    })

    fmt.Println(data)
}

Vous pouvez expérimenter cela sur le terrain de jeu

Exemple complet que vous pouvez coller dans le playground , car l'importation du package quicksort ne fonctionne pas sur le playground.

@go-li Je suis sĂ»r que vous pouvez trier une tranche, ce serait un peu pauvre si vous ne le pouviez pas. Le fait est que, de maniĂšre gĂ©nĂ©rique, vous aimeriez pouvoir trier n'importe quel conteneur linĂ©aire avec le mĂȘme code, de sorte que vous n'ayez Ă  Ă©crire un algorithme de tri qu'une seule fois, quel que soit le conteneur (structure de donnĂ©es) que vous triez et quel que soit le le contenu est.

Lorsque vous pouvez le faire, la bibliothĂšque standard peut fournir des fonctions de tri universelles, et personne n'a plus jamais besoin d'en Ă©crire une. Il y a deux avantages Ă  cela, moins d'erreurs car il est plus difficile que vous ne le pensez d'Ă©crire un algorithme de tri correct, Stepanov utilise l'exemple que la plupart des programmeurs ne peuvent pas dĂ©finir correctement la paire 'min' et 'max', alors quel espoir avons-nous d'ĂȘtre correct pour les algorithmes plus complexes. L'autre avantage est que lorsqu'il n'y a qu'une seule dĂ©finition de chaque algorithme de tri, toute amĂ©lioration de la clartĂ© ou des performances qui peut ĂȘtre apportĂ©e profite Ă  tous les programmes qui l'utilisent. Les gens peuvent passer leur temps Ă  essayer d'amĂ©liorer l'algorithme commun au lieu d'avoir Ă  Ă©crire le leur pour chaque type de donnĂ©es diffĂ©rent.

@keean
Une autre question liĂ©e Ă  notre discussion prĂ©cĂ©dente. Je n'arrive pas Ă  comprendre comment on pourrait dĂ©finir une fonction de mappage qui change les Ă©lĂ©ments d'un itĂ©rable, renvoyant un nouveau type itĂ©rable concret dont les Ă©lĂ©ments pourraient ĂȘtre d'un type diffĂ©rent de celui d'origine.

Et j'imagine qu'un utilisateur d'une telle fonction voudrait qu'un type concret soit renvoyé, pas une autre interface.

@urandom En supposant que nous ne voulons pas le faire "sur place", ce qui serait dangereux, ce que vous voulez, c'est une fonction de carte qui a un "itĂ©rateur de lecture" d'un type et un "itĂ©rateur d'Ă©criture" d'un autre type, qui peut ĂȘtre dĂ©fini quelque chose comme :

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)

Pour plus de clarté, "ValueType" est un type associé des interfaces "Readable" et "Writable", "Domain" et "Codomain" sont des types associés de l'interface "UnaryFunction". Cela aide évidemment beaucoup si le compilateur peut dériver automatiquement les interfaces pour les types de données comme "UnaryFunction". Bien que cela ressemble à de la réflexion, ce n'est pas le cas, et tout se passe au moment de la compilation en utilisant des types statiques.

@keean Comment modéliser ces contraintes Readable et Writable dans le contexte des interfaces Go actuelles ?

Je veux dire, quand nous avons un type A et que nous voulons convertir en type B , la signature de cette UnaryFunction serait func (input A) B (n'est-ce pas ?), mais comment cela peut-il ĂȘtre modĂ©lisĂ© en utilisant uniquement des interfaces et comment ce gĂ©nĂ©rique map (ou filter , reduce , etc.) serait modĂ©lisĂ© pour conserver le pipeline de types ?

@ geovanisouza92 Je pense que "Type Families" fonctionnerait bien car elles peuvent ĂȘtre implĂ©mentĂ©es comme un mĂ©canisme orthogonal dans le systĂšme de type, puis intĂ©grĂ©es dans la syntaxe des interfaces comme cela se fait dans Haskell.

Une famille de types est comme une fonction restreinte sur les types (un mappage). Comme les implémentations d'interface sont sélectionnées par type, nous pouvons fournir un mappage de type pour chaque implémentation.

Donc si on définit :

ValueType MyIntArrayIterator -> Int

Les fonctions sont un peu plus compliquées mais une fonction a un type, par exemple :

fn(x : Int) Float

On Ă©crirait ce type :

Int -> Float

Il est important de réaliser que -> n'est qu'un constructeur de type infixe, comme '[]' pour un Array est un constructeur de type, nous pourrions tout aussi bien écrire ceci ;

Fn Int Float
Or
Fn<Int, Float>

Selon notre préférence pour la syntaxe de type. Maintenant, nous pouvons voir clairement comment nous pouvons définir :

Domain  Fn<Int, Float> -> Int
Codomain Fn<Int, Float> -> Float

Bien que nous puissions fournir toutes ces dĂ©finitions Ă  la main, elles peuvent facilement ĂȘtre dĂ©rivĂ©es par le compilateur.

Compte tenu de ces familles de types, nous pouvons voir que la définition de map que j'ai donnée ci-dessus ne nécessite que les types IO et U pour instancier le générique, car tous les autres types en dépendent fonctionnellement. Nous pouvons voir que ces types sont directement fournis par les arguments.

Merci, @keean.

Cela fonctionnerait bien pour les fonctions intĂ©grĂ©es/prĂ©dĂ©finies. Êtes-vous en train de dire que le mĂȘme concept serait appliquĂ© aux fonctions dĂ©finies par l'utilisateur ou aux bibliothĂšques utilisateur ?

Ces "familles de types" seront transportées pour l'exécution, dans le cas d'un contexte d'erreur ?

Qu'en est-il des interfaces vides, des commutateurs de type et de la réflexion ?


EDIT : Je suis juste curieux, je ne me plains pas.

@ giovanisouza92 eh bien personne ne s'est engagé à avoir des génériques donc je m'attends à du scepticisme. Mon approche est que si vous allez faire des génériques, vous devez les faire correctement.

Dans mon exemple, 'map' est défini par l'utilisateur. Il n'y a rien de spécial à ce sujet, et dans la fonction, vous utilisez simplement les méthodes des interfaces dont vous avez besoin sur ces types exactement comme vous le faites actuellement dans Go. La seule différence est que nous pouvons exiger qu'un type satisfasse plusieurs interfaces, les interfaces peuvent avoir plusieurs paramÚtres de type (bien que l'exemple de carte ne l'utilise pas) et il existe également des types associés (et des contraintes sur les types comme l'égalité de type '==' mais c'est comme une égalité Prolog et unifie les types). C'est pourquoi il existe une syntaxe différente pour spécifier les interfaces requises par une fonction. Notez qu'il existe une autre différence importante :

f(x I, y I) requires ForwardIterator<I>

Vs

f(x ForwardIterator, y ForwardIterator)

Notez qu'il y a une diffĂ©rence dans ce dernier 'x' et 'y' peuvent ĂȘtre des types diffĂ©rents qui satisfont l'interface ForwardIterator, alors que dans l'ancienne syntaxe 'x' et 'y' doivent tous deux ĂȘtre du mĂȘme type (ce qui satisfait l'itĂ©rateur avant). Ceci est important pour que les fonctions ne soient pas sous-contraintes et permettent aux types concrets d'ĂȘtre propagĂ©s beaucoup plus loin lors de la compilation.

Je ne pense pas que quoi que ce soit change concernant les changements de type et la rĂ©flexion, car nous ne faisons qu'Ă©tendre le concept d'interfaces. Comme go contient des informations sur le type d'exĂ©cution, vous ne rencontrez pas le mĂȘme problĂšme que Haskell et vous avez besoin de types existentiels.

En pensant Ă  Go, au polymorphisme d'exĂ©cution et aux familles de types, nous voudrions probablement contraindre la famille de types elle-mĂȘme Ă  une interface pour Ă©viter d'avoir Ă  traiter chaque type associĂ© comme une interface vide Ă  l'exĂ©cution, ce qui serait lent.

Donc, à la lumiÚre de ces réflexions, je modifierais ma proposition ci-dessus afin que lors de la déclaration d'une interface, vous déclariez une interface/un type pour chaque type associé que toutes les implémentations de cette interface devraient fournir un type associé qui satisfait cette interface. De cette façon, nous pouvons savoir qu'il est sûr d'appeler toutes les méthodes de cette interface sur les types associés au moment de l'exécution sans avoir à changer de type à partir d'une interface vide.

@keean
Dans l'intĂ©rĂȘt de faire avancer le dĂ©bat, permettez-moi de dissiper l'idĂ©e fausse qui me semble similaire au syndrome non inventĂ© ici.

L'itĂ©rateur bidirectionnel (en syntaxe T func (*T) *[2]*T ) a le type func (*) *[2]* en syntaxe go-li. En mots, il prend un pointeur vers un certain type et renvoie un pointeur vers deux pointeurs vers l'Ă©lĂ©ment suivant et prĂ©cĂ©dent du mĂȘme type. C'est le type fondamental fondamental concret utilisĂ© par une liste doublement chaĂźnĂ©e .

Maintenant vous pouvez écrire ce que vous appelez map, ce que j'appelle la fonction générique foreach. Ne vous méprenez pas, cela fonctionne non seulement sur une liste chaßnée, mais sur tout ce qui expose un itérateur bidirectionnel !

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
}

Le Foreach peut ĂȘtre utilisĂ© de deux maniĂšres, vous l'utilisez avec un lambda dans une itĂ©ration de type boucle for sur des Ă©lĂ©ments de liste ou de collection.

const forward = 1
const backwards = 0
Foreach(iterator, collection, forward, func(element *element_type){
    // do something with every element
})

Ou vous pouvez l'utiliser pour mapper fonctionnellement une fonction à chaque élément de collection.

Foreach(iterator, collection, backwards, function_to_be_mapped_on_elements)

L'itĂ©rateur bidirectionnel peut bien sĂ»r Ă©galement ĂȘtre modĂ©lisĂ© Ă  l'aide d'interfaces dans go 1.
interface Iterator { Iter() [2]Iterator } Vous devez le modéliser à l'aide d'interfaces afin d'envelopper ("boßte") le type sous-jacent. L'utilisateur itérateur puis le type affirme le type connu une fois qu'il a localisé et souhaite visiter un élément de collection spécifique. Ceci est potentiellement dangereux au moment de la compilation.

Ce que vous décrivez ensuite, ce sont les différences entre l'approche héritée et l'approche basée sur les génériques.

func modern(x func  (*) *[2]*, y func  (*) *[2]*){}

cette approche compile time type vĂ©rifie que les deux collections ont le mĂȘme type sous-jacent, c'est-Ă -dire si les itĂ©rateurs renvoient effectivement les mĂȘmes types concrets

func modern_T_syntax<T>(x func  (*T) *[2]*T, y func  (*T) *[2]*T){}

Identique à ci-dessus mais en utilisant la syntaxe d'espace réservé de type T signifie familiÚre

func legacy(x Iterator, y Iterator){}

Dans ce cas, l'utilisateur peut passer par exemple une liste liée entiÚre comme x et une liste liée flottante comme y. Cela pourrait entraßner des erreurs d'exécution potentielles, des paniques ou d'autres décohérences internes, mais tout dépend de ce que l'héritage ferait avec les deux itérateurs.

Maintenant, l'idée fausse. Vous prétendez que faire des itérateurs et faire des tris génériques pour trier ces itérateurs serait la voie à suivre. Ce serait vraiment une mauvaise chose à faire, voici pourquoi

ItĂ©rateur et liste chaĂźnĂ©e sont les deux faces d'une mĂȘme mĂ©daille. Preuve : toute collection qui expose un itĂ©rateur s'annonce simplement comme une liste chaĂźnĂ©e. Disons que vous devez trier cela. Qu'est-ce que?

Évidemment, vous supprimez la liste chaĂźnĂ©e de votre base de code et la remplacez par un arbre binaire. Ou si vous voulez ĂȘtre fantaisiste, utilisez un arbre de recherche Ă©quilibrĂ© comme avl, rouge-noir, comme proposĂ© il y a je ne sais combien d'annĂ©es par Ian et tous. Cela n'a toujours pas Ă©tĂ© fait de maniĂšre gĂ©nĂ©rique dans golang. Maintenant, ce serait la voie Ă  suivre.

Une autre solution consiste à effectuer rapidement une boucle temporelle O(N) sur l'itérateur, à collecter les pointeurs vers des éléments dans une tranche de pointeurs génériques, notée []*T et à trier ces pointeurs génériques à l'aide du mauvais tri par tranche

Merci de donner une chance aux idées des autres

@go-li Si nous voulons éviter le syndrome non inventé ici, nous devrions nous tourner vers Alex Stepanov pour une définition, car il a pratiquement inventé la programmation générique. Voici comment je le définirais, tiré de la page 111 "Elements of Programming" de Stepanov :

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

Cela dépend de la définition de ForwardIterator :

ForwardIterator<T> =
    Iterator<T>
/\ regular_unary_function(successor)

Nous avons donc essentiellement une interface qui dĂ©clare une fonction successor et une fonction predecessor , ainsi que certains axiomes auxquels elles doivent se conformer pour ĂȘtre valides.

En ce qui concerne legacy , ce n'est pas que l'héritage ira mal, il ne va évidemment pas mal dans Go actuellement, mais le compilateur manque des opportunités d'optimisation, et le systÚme de type manque l'opportunité de propager davantage les types concrets. Cela limite également la capacité des programmeurs à spécifier précisément leur intention. Un exemple serait une fonction d'identité, ce que je veux dire pour renvoyer exactement le type qui lui est passé :

id(x T) T

Peut-ĂȘtre vaut-il Ă©galement la peine de mentionner la diffĂ©rence entre un type paramĂ©trique et un type universellement quantifiĂ©. Un type paramĂ©trique serait id<T>(x T) T alors que celui universellement quantifiĂ© est id(x T) T (nous omettons normalement le quantificateur universel le plus externe dans ce cas forall T ). Avec les types paramĂ©triques, le systĂšme de types doit avoir un type pour T fourni sur le site d'appel pour id , avec une quantification universelle qui n'est pas nĂ©cessaire tant que T est unifiĂ© avec un type concret avant la fin de la compilation. Une autre façon de comprendre cela est que la fonction paramĂ©trique n'est pas un type mais un modĂšle pour un type, et ce n'est un type valide qu'aprĂšs que T a Ă©tĂ© remplacĂ© par un type concret. Avec la fonction universellement quantifiĂ©e, id a en fait un type forall T . T -> T qui peut ĂȘtre passĂ© par le compilateur comme Int .

@go-li

Évidemment, vous supprimez la liste chaĂźnĂ©e de votre base de code et la remplacez par un arbre binaire. Ou si vous voulez ĂȘtre fantaisiste, utilisez un arbre de recherche Ă©quilibrĂ© comme avl, rouge-noir, comme proposĂ© il y a je ne sais combien d'annĂ©es par Ian et tous. Cela n'a toujours pas Ă©tĂ© fait de maniĂšre gĂ©nĂ©rique dans golang. Maintenant, ce serait la voie Ă  suivre.

Avoir des structures de données ordonnées ne signifie pas que vous n'avez jamais besoin de trier les données.

Si nous voulons éviter le syndrome du non-inventé ici, nous devrions nous tourner vers Alex Stepanov pour une définition, car il a pratiquement inventé la programmation générique.

Je contesterais toute affirmation selon laquelle la programmation générique aurait été inventée par C++. Lire Liskov et al. Article CACM de 1977 si vous voulez voir un premier modÚle de programmation générique qui fonctionne réellement (type-safe, modulaire, pas de gonflement du code): https://dl.acm.org/citation.cfm?id=359789 (voir Section 4 )

Je pense que nous devrions arrĂȘter cette discussion et attendre que l'Ă©quipe golang (russ) vienne avec quelques articles de blog, puis implĂ©mente une solution 👍 (voir vgo) Ils le feront 🎉

https://peter.bourgon.org/blog/2018/07/27/a-response-about-dep-and-vgo.html

J'espĂšre que cette histoire servira d'avertissement aux autres : si vous ĂȘtes intĂ©ressĂ© Ă  apporter des contributions substantielles au projet Go, aucune diligence raisonnable indĂ©pendante ne peut compenser une conception qui ne provient pas de l'Ă©quipe principale.

Ce fil montre comment l'équipe principale n'est pas intéressée à participer activement à la recherche d'une solution avec la communauté.

Mais Ă  la fin, s'ils peuvent Ă  nouveau trouver une solution par eux-mĂȘmes, ça me va, faites-le 👍

@andrewcmyers Eh bien peut-ĂȘtre que "inventĂ©" Ă©tait un peu exagĂ©rĂ©, c'est probablement plus comme David Musser en 1971, qui a ensuite travaillĂ© avec Stepanov sur certaines bibliothĂšques gĂ©nĂ©riques pour Ada.

Elements of Programming n'est pas un livre sur C++, les exemples peuvent ĂȘtre en C++ mais c'est une chose trĂšs diffĂ©rente. Je pense que ce livre est une lecture essentielle pour quiconque souhaite implĂ©menter des gĂ©nĂ©riques dans n'importe quelle langue. Avant de renvoyer Stepanov, vous devriez vraiment lire le livre pour voir de quoi il s'agit rĂ©ellement.

Ce problÚme dépasse déjà les limites de l'évolutivité de GitHub. Veuillez garder la discussion ici concentrée sur des questions concrÚtes pour les propositions Go.

Il serait dommage que Go suive la route C++.

@andrewcmyers Oui, je suis entiĂšrement d'accord, veuillez ne pas utiliser C++ pour des suggestions de syntaxe ou comme rĂ©fĂ©rence pour faire les choses correctement. Au lieu de cela, veuillez jeter un Ɠil Ă  D pour vous inspirer .

@nomad-software

J'aime beaucoup D, mais a-t-il besoin des puissantes fonctionnalités de méta-programmation au moment de la compilation offertes par D ?

Je n'aime pas non plus la syntaxe des modĂšles en C++, issue de l'Ăąge de pierre.

Mais qu'en est-il du ParametricType normalstandard trouvé en Java ou C #, si nécessaire, on peut également le surcharger avec ParametricType

Et plus loin, je n'aime pas la syntaxe d'appel de modÚle en D avec son symbole bang, le symbole bang est plutÎt utilisé de nos jours pour désigner un accÚs mutable ou immuable pour les paramÚtres d'une fonction.

@nomad-software Je ne suggérais pas que la syntaxe C++ ou le mécanisme de modÚle est la bonne façon de faire des génériques. De plus, les "concepts" tels que définis par Stepanov traitent les types comme une algÚbre, ce qui est tout à fait la bonne façon de faire des génériques. Regardez les classes de types Haskell pour voir à quoi cela pourrait ressembler. Les classes de type Haskell sont sémantiquement trÚs proches des modÚles et des concepts c++, si vous comprenez ce qui se passe.

Donc +1 pour ne pas suivre la syntaxe c++ et +1 pour ne pas implémenter un systÚme de template non sécurisé :-)

@keean La raison de la syntaxe D est d'éviter complÚtement <,> et d'adhérer à une grammaire sans contexte. Cela fait partie de mon point d'utiliser D comme source d'inspiration. <,> est un trÚs mauvais choix pour la syntaxe des paramÚtres génériques.

@nomad-software Comme je l'ai soulignĂ© ci-dessus (dans un commentaire dĂ©sormais masquĂ©), vous devez spĂ©cifier les paramĂštres de type pour les types paramĂ©triques, mais pas pour les types universellement quantifiĂ©s (d'oĂč la diffĂ©rence entre Rust et Haskell, la maniĂšre dont les types sont gĂ©rĂ©s est en fait diffĂ©rente dans le systĂšme de types). Concepts C++ Ă©galement == classes de types Haskell == interfaces Go, au moins au niveau conceptuel.

La syntaxe D est-elle vraiment préférable :

auto add(T)(T lhs, T rhs) {
    return lhs + rhs;

Pourquoi est-ce mieux que le style C++/Java/Rust :

T add<T>(T lhs, T rhs) {
    return lhs + rhs;
}

Ou style Scala :

T add[T](T lhs, T rhs) {
    return lhs + rhs;
}

J'ai réfléchi à la syntaxe des paramÚtres de type. Je n'ai jamais été fan des "crochets angulaires" en C++ et Java car ils rendent l'analyse assez délicate et entravent ainsi le développement d'outils. Les crochets sont en fait un choix classique (de CLU, System F et d'autres langages anciens avec polymorphisme paramétrique).

Cependant, la syntaxe de Go est assez dĂ©licate, peut-ĂȘtre parce qu'elle est dĂ©jĂ  si laconique. Les syntaxes possibles basĂ©es sur des crochets ou des parenthĂšses crĂ©ent des ambiguĂŻtĂ©s grammaticales encore pires que celles introduites par des crochets angulaires. Donc malgrĂ© mes prĂ©dispositions, les Ă©querres semblent en fait ĂȘtre le meilleur choix pour Go. (Bien sĂ»r, il existe aussi de vrais chevrons qui ne crĂ©eraient aucune ambiguĂŻtĂ© — ⟚⟩ — mais ils nĂ©cessiteraient l'utilisation de caractĂšres Unicode).

Bien sûr, la syntaxe précise utilisée pour les paramÚtres de type est moins importante que la bonne sémantique . Sur ce point, le langage C++ est un mauvais modÚle. Les travaux de mon groupe de recherche sur les génériques dans Genus (PLDI 2015) et Familia (OOPSLA 2017) proposent une autre approche qui étend les classes de types et les unifie avec des interfaces.

@andrewcmyers Je pense que ces deux articles sont intéressants, mais je dirais que ce n'est pas une bonne direction pour Go, car Genus est orienté objet, et Go ne l'est pas, et Familia unifie le sous-typage et le polymorphisme paramétrique, et Go n'a ni l'un ni l'autre. Je pense que Go devrait simplement adopter soit le polymorphisme paramétrique, soit la quantification universelle, il n'a pas besoin de sous-typage, et à mon avis, c'est un meilleur langage pour ne pas l'avoir.

Je pense que Go devrait rechercher des gĂ©nĂ©riques qui ne nĂ©cessitent pas d'orientation objet et ne nĂ©cessitent pas de sous-typage. Go a dĂ©jĂ  des interfaces, qui je pense sont un bon mĂ©canisme pour les gĂ©nĂ©riques. Si vous pouvez voir que Go interfaces == c++ concepts == Haskell type-classes, il me semblerait que la façon d'ajouter des gĂ©nĂ©riques tout en gardant la saveur de 'Go' serait d'Ă©tendre les interfaces pour prendre plusieurs paramĂštres de type (je dirais comme les types associĂ©s sur les interfaces aussi, mais cela pourrait ĂȘtre une extension distincte de celui-ci aide Ă  faire accepter plusieurs paramĂštres de type). Ce serait le changement clĂ©, mais pour activer cela, il faudrait une syntaxe `` alternative '' pour les interfaces dans les signatures de fonction, afin que vous puissiez obtenir les multiples paramĂštres de type vers les interfaces, c'est lĂ  que toute la syntaxe des crochets angulaires entre en jeu .

Les interfaces Go ne sont pas des classes de types - ce sont simplement des types - mais l'unification des interfaces avec des classes de types est ce que Familia montre comment faire. Les mĂ©canismes de Genus et Familia ne sont pas liĂ©s au fait que les langages soient entiĂšrement orientĂ©s objet. Les interfaces Go rendent dĂ©jĂ  Go "orientĂ© objet" de la maniĂšre qui compte, donc je pense que les idĂ©es pourraient ĂȘtre adaptĂ©es sous une forme lĂ©gĂšrement simplifiĂ©e.

@andrewcmyers

Les interfaces Go ne sont pas des classes de types - ce sont simplement des types

Ils ne se comportent pas comme des types pour moi, car ils permettent le polymorphisme. L'objet dans un tableau polymorphe comme Addable[] a toujours son type réel (visible par réflexion à l'exécution), ils se comportent donc exactement comme des classes de type à paramÚtre unique. Le fait qu'ils soient mis à la place d'un type dans les signatures de type est simplement une notation abrégée omettant la variable de type. Ne confondez pas la notation avec la sémantique.

f(x : Addable) == f<T>(x : T) requires Addable<T>

Cette identité n'est bien sûr valable que pour les interfaces à paramÚtre unique.

La seule diffĂ©rence significative entre les interfaces et les classes de type Ă  paramĂštre unique est que les interfaces sont dĂ©finies localement, mais cela est utile car cela Ă©vite le problĂšme de cohĂ©rence globale que Haskell a avec ses classes de type. Je pense que c'est un point intĂ©ressant dans l'espace de conception. Les interfaces multiparamĂštres vous donneraient toute la puissance des classes de types multiparamĂštres avec l'avantage d'ĂȘtre locales. Il n'est pas nĂ©cessaire d'ajouter d'hĂ©ritage ou de sous-typage au langage Go (qui sont les deux caractĂ©ristiques clĂ©s qui dĂ©finissent OO je pense).

A MON HUMBLE AVIS:

Avoir toujours un type par défaut serait préférable à un DSL dédié à l'expression des restrictions de type. Comme avoir une fonction f(s T fmt.Stringer) qui est une fonction générique qui accepte tout type qui est aussi/satisfait l'interface fmt.Stringer .

De cette façon, il est possible d'avoir une fonction générique comme :

func add(a, b T int) T int {
    return a + b
}

Désormais, la fonction add() fonctionne avec n'importe quel type T qui, comme int s, prend en charge l'opérateur + .

@dc0d Je suis d'accord que cela semble attrayant en regardant la syntaxe actuelle de Go. Cependant, il n'est pas « complet » dans la mesure oĂč il ne peut pas reprĂ©senter toutes les contraintes nĂ©cessaires aux gĂ©nĂ©riques, et il y aura toujours une poussĂ©e pour l'Ă©tendre davantage. Il en rĂ©sultera une prolifĂ©ration de syntaxes diffĂ©rentes que je considĂšre comme en conflit avec l'objectif de simplicitĂ©. Mon point de vue est que la simplicitĂ© n'est pas simple, elle doit ĂȘtre la plus simple tout en offrant la puissance expressive requise. Actuellement, je vois que la principale limitation de Go dans la puissance expressive gĂ©nĂ©rique est le manque d'interfaces multi-paramĂštres. Par exemple, une interface Collection pourrait ĂȘtre dĂ©finie comme :

type T U Collection interface {
   member(c T, v U) Bool
   insert(c T, v U) T
}

Cela a donc du sens, n'est-ce pas ? Nous aimerions écrire des interfaces sur des choses comme des collections. La question est donc de savoir comment utiliser cette interface dans une fonction. Ma suggestion serait quelque chose comme:

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
}

La syntaxe n'est qu'une suggestion cependant, peu m'importe quelle est la syntaxe, tant que vous pouvez exprimer ces concepts dans le langage.

@keean Ce ne serait pas exact si je disais que la syntaxe ne me dérange pas du tout. Mais le but était de mettre l'accent sur le fait d'avoir un type par défaut pour chaque paramÚtre générique. En ce sens, l'exemple d'interface fourni deviendra :

type Collection interface (T interface{}, U interface{}) {
   member(c T, v U) Bool
   insert(c T, v U) T
}

Maintenant, la partie (T interface{}, U interface{}) aide à définir les contraintes. Par exemple, si les membres sont censés satisfaire fmt.Stringer , alors la définition serait :

type Collection interface (T fmt.Stringer, U fmt.Stringer) {
   member(c T, v U) Bool
   insert(c T, v U) T
}

@dc0d Ce serait Ă  nouveau restrictif dans le sens oĂč vous souhaitez contraindre par plus d'un paramĂštre de type, considĂ©rez:

type OrderedCollection[T, U] interface
   requires Collection[T, U], Ord[U] {...}

Je pense voir oĂč vous voulez en venir avec le placement des paramĂštres, vous pourriez avoir :

type OrderedCollection interface(T, U)
   requires Collection(T, U), Ord(U) {...}

Comme je l'ai dit, je ne suis pas trop préoccupé par la syntaxe, car je peux m'habituer à la plupart des syntaxes. D'aprÚs ce qui précÚde, je suppose que vous préférez les parenthÚses '()' pour les interfaces multi-paramÚtres.

@keean Considérons l'interface heap.Interface . La définition actuelle dans la bibliothÚque standard est :

type Interface interface {
    sort.Interface
    Push(x interface{}) // add x as element Len()
    Pop() interface{}   // remove and return element Len() - 1.
}

Réécrivons-la maintenant comme une interface générique, en utilisant le type par défaut :

type Interface interface (T interface{}) {
    sort.Interface
    Push(x T) // add x as element Len()
    Pop() T   // remove and return element Len() - 1.
}

Cela ne casse aucune des sĂ©ries de codes Go 1.x lĂ -bas. Une implĂ©mentation serait ma proposition pour Type Alias ​​Rebinding. Mais je suis sĂ»r qu'il peut y avoir de meilleures implĂ©mentations.

Avoir des types par dĂ©faut nous permet d'Ă©crire du code gĂ©nĂ©rique qui peut ĂȘtre utilisĂ© avec le code de style Go 1.x. Et la bibliothĂšque standard peut devenir gĂ©nĂ©rique, sans rien casser. C'est une grande victoire IMO.

@dc0d donc vous suggérez une amélioration progressive ? Ce que vous suggérez me semble bien comme une amélioration progressive, mais il a encore un pouvoir expressif générique limité. Comment implémenteriez-vous les interfaces "Collection" et "OrderedCollection" ?

ConsidĂ©rez que plusieurs extensions de langage partielles peuvent conduire Ă  un produit final plus complexe (avec plusieurs syntaxes alternatives) que la mise en Ɠuvre de la solution complĂšte de la maniĂšre la plus simple possible.

@keean Je ne comprends pas la partie requires Collection[T, U], Ord[U] . Comment restreignent-ils les paramĂštres de type T et U ?

@dc0d Ils fonctionnent de la mĂȘme maniĂšre que dans une fonction, mais s'appliquent Ă  tout. Ainsi, pour toute paire de types TU qui est une OrderedCollection, nous exigeons que TU soit Ă©galement une instance de Collection et que U soit Ord. Ainsi, partout oĂč nous utilisons OrderedCollection, nous pouvons utiliser les mĂ©thodes de Collection et Ord, le cas Ă©chĂ©ant.

Si nous sommes minimalistes, elles ne sont pas nĂ©cessaires, car nous pouvons inclure les interfaces supplĂ©mentaires dans les types de fonctions oĂč nous en avons besoin, par exemple :

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

Mais c'est peut-ĂȘtre plus lisible :

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) Tant qu'il y a une valeur par défaut obligatoire pour les paramÚtres de type, je me sens heureux. De cette façon, il est possible de maintenir la compatibilité descendante avec la série de codes Go 1.x. C'est le point principal que j'ai essayé de faire valoir.

@keean

Les interfaces Go ne sont pas des classes de types - ce sont simplement des types

Ils ne se comportent pas comme des types pour moi, car ils permettent le polymorphisme.

Oui, ils autorisent le polymorphisme de sous-type . Go a un sous-typage via les types d'interface. Il n'a pas de hiérarchies de sous-types explicitement déclarées, mais c'est largement orthogonal. Ce qui fait que Go n'est pas entiÚrement orienté objet, c'est le manque d'héritage.

Vous pouvez Ă©galement afficher les interfaces comme des applications existentiellement quantifiĂ©es de classes de type. Je crois que c'est ce que vous avez en tĂȘte. C'est ce que nous avons fait dans Genus et Familia.

@andrewcmyers

Oui, ils permettent le polymorphisme de sous-type.

Aller aussi loin que je sache est invariant, il n'y a pas de covariance ou de contravariance, cela indique fortement qu'il ne s'agit pas de sous-typage. Les systÚmes de types polymorphes sont invariants, il me semble donc que Go est plus proche de ce modÚle, et traiter les interfaces comme des classes de types à paramÚtre unique semble plus conforme à la simplicité de Go. L'absence de covariance et de contravariance est un grand avantage pour les génériques, il suffit de regarder la confusion que de telles choses créent dans des langages comme C# :

https://docs.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance

Je pense que Go devrait totalement Ă©viter ce genre de complexitĂ©. Pour moi, cela signifie que nous ne voulons pas de gĂ©nĂ©riques et de sous-typages dans le mĂȘme systĂšme de typage.

Vous pouvez Ă©galement afficher les interfaces comme des applications existentiellement quantifiĂ©es de classes de type. Je crois que c'est ce que vous avez en tĂȘte. C'est ce que nous avons fait dans Genus et Familia.

Parce que Go a des informations de type au moment de l'exĂ©cution, il n'y a pas besoin de quantification existentielle. Dans Haskell, les types sont dĂ©ballĂ©s (comme les types 'C' natifs) et cela signifie qu'une fois que nous avons mis quelque chose dans une collection existentielle, nous ne pouvons pas (facilement) rĂ©cupĂ©rer le type du contenu, tout ce que nous pouvons faire est d'utiliser les interfaces fournies (type-classes ). Ceci est mis en Ɠuvre en stockant un pointeur vers les interfaces Ă  cĂŽtĂ© des donnĂ©es brutes. Dans Go, le type de donnĂ©es est stockĂ© Ă  la place, les donnĂ©es sont 'Boxed' (comme dans C# boxed et unboxed data). En tant que tel, Go n'est pas limitĂ© aux seules interfaces stockĂ©es avec les donnĂ©es car il est possible (par l'utilisation d'un type-case) de rĂ©cupĂ©rer le type des donnĂ©es dans la collection, ce qui n'est possible que dans Haskell en implĂ©mentant un 'Reflection' typeclass (bien que difficile d'extraire les donnĂ©es, il est possible de sĂ©rialiser le type et les donnĂ©es, pour dire des chaĂźnes, puis de dĂ©sĂ©rialiser en dehors de la boĂźte existentielle). Donc, la conclusion que j'ai est que les interfaces Go se comportent exactement comme les classes de type le feraient, si Haskell fournissait la classe de type 'Reflection' en tant que fonction intĂ©grĂ©e. En tant que tel, il n'y a pas de boĂźte existentielle et nous pouvons toujours saisir la casse sur le contenu des collections, mais les interfaces se comportent exactement comme des classes de types. La diffĂ©rence entre Haskell et Go rĂ©side dans la sĂ©mantique des donnĂ©es en boĂźte et non en boĂźte, et les interfaces sont des classes de type Ă  paramĂštre unique. En effet, lorsque 'Go' traite une interface comme un type, ce qu'il fait rĂ©ellement est :

Addable[] == exists T . T[] requires Addable[T], Reflection[T]

Il est probablement intĂ©ressant de noter que c'est de la mĂȘme maniĂšre que "Trait Objects" fonctionne dans Rust.

Go peut totalement éviter les existentiels (étant visibles pour le programmeur), la covariance et la contravariance ce qui est une bonne chose, et cela rendra les génériques beaucoup plus simples et plus puissants à mon avis.

Aller aussi loin que je sache est invariant, il n'y a pas de covariance ou de contravariance, cela indique fortement qu'il ne s'agit pas de sous-typage.

Les systÚmes de types polymorphes sont invariants, donc cela me semble plus proche de ce modÚle, et traiter les interfaces comme des classes de types à paramÚtre unique semble plus conforme à la simplicité de Go.

Puis-je suggĂ©rer que vous avez tous les deux raison ? En cela, les interfaces sont Ă©quivalentes aux classes de types, mais les classes de types sont une forme de sous-typage. Les dĂ©finitions de sous-typage que j'ai trouvĂ©es jusqu'Ă  prĂ©sent sont toutes assez vagues et imprĂ©cises et se rĂ©sument Ă  "A est un sous-type de B, si l'un peut ĂȘtre remplacĂ© par l'autre". Ce qui, IMO, peut ĂȘtre assez facilement soutenu pour ĂȘtre satisfait par les classes de type .

Notez que l'argument variance en lui-mĂȘme ne fonctionne pas vraiment Ă  l'OMI. La variance est une propriĂ©tĂ© des constructeurs de types, pas un langage. Et il est assez normal que tous les constructeurs de type d'un langage ne soient pas des variants (par exemple, de nombreux langages avec sous-typage ont des tableaux modifiables, qui doivent ĂȘtre invariants pour ĂȘtre de type sĂ»r). Je ne vois donc pas pourquoi vous ne pourriez pas avoir de sous-typage sans constructeurs de types variants.

De plus, je pense que cette discussion est un peu trop large pour un problÚme sur le référentiel Go. Il ne devrait pas s'agir de discuter des subtilités des théories des types, mais de savoir si et comment ajouter des génériques à Go.

@Merovius Variance est une propriété associée au sous-typage. Dans les langues sans sous-typage, il n'y a pas de variance. Pour qu'il y ait variance en premier lieu, vous devez avoir un sous-typage, qui introduit le problÚme de covariance/contravariance aux constructeurs de type. Vous avez raison cependant que dans un langage avec sous-typage, il est possible d'avoir tous les constructeurs de types invariants.

Les classes de types ne sont certainement pas des sous-typages, car une classe de types n'est pas un type. Cependant, nous pouvons voir les «types d'interface» dans Go comme ce que Rust appelle un «objet de trait», en fait un type dérivé de la classe de types.

La sĂ©mantique de Go semble correspondre Ă  l'un ou l'autre modĂšle pour le moment, car elle n'a pas de variance et elle a des «objets de trait» implicites. Alors peut-ĂȘtre que Go est Ă  un point de basculement, les gĂ©nĂ©riques et le systĂšme de type pourraient ĂȘtre dĂ©veloppĂ©s dans le sens du sous-typage, introduisant de la variance et aboutissant Ă  quelque chose comme les gĂ©nĂ©riques en C#. Alternativement, Go pourrait introduire des interfaces multi-paramĂštres, permettant des interfaces pour les collections, ce qui romprait le lien immĂ©diat entre les interfaces et les "types d'interface". Par exemple si vous avez :

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[] {...}

Il n'y a plus de relation de sous-type Ă©vidente entre les types T, U et l'interface Collection. Ainsi, vous ne pouvez voir la relation entre le type d'instance et les types d'interface que comme sous-typage pour le cas particulier des interfaces Ă  paramĂštre unique, et nous ne pouvons pas exprimer des abstractions de choses comme des collections avec des interfaces Ă  paramĂštre unique.

Je pense que pour les gĂ©nĂ©riques, vous devez clairement ĂȘtre capable de modĂ©liser des choses comme des collections, donc les interfaces multi-paramĂštres sont un must pour moi. Cependant, je pense que l'interaction entre la covariance et la contravariance dans les gĂ©nĂ©riques crĂ©e un systĂšme de type trop complexe, donc je voudrais Ă©viter le sous-typage.

@keean Étant donnĂ© que les interfaces peuvent ĂȘtre utilisĂ©es comme types et que les classes de types ne sont pas des types, l'explication la plus naturelle de la sĂ©mantique Go est que les interfaces ne sont pas des classes de types. Je comprends que vous prĂ©conisez de gĂ©nĂ©raliser les interfaces en tant que classes de types ; Je pense que c'est une direction raisonnable de prendre la langue, et en fait nous avons dĂ©jĂ  largement explorĂ© cette approche dans nos travaux publiĂ©s.

Pour savoir si Go a un sous-typage, veuillez considérer le code suivant :

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

L'affectation de x Ă  y dĂ©montre que le type de y peut ĂȘtre utilisĂ© lĂ  oĂč le type de x est attendu. Il s'agit d'une relation de sous-typage, Ă  savoir : CloneableZ <: Cloneable , et aussi S <: CloneableZ . MĂȘme si vous expliquiez les interfaces en termes de classes de types, il y aurait toujours une relation de sous-typage en jeu ici, quelque chose comme S <: ∃T.CloneableZ[T] <: ∃T.Cloneable[T] .

Notez qu'il serait parfaitement sĂ»r pour Go d'autoriser la fonction Clone Ă  renvoyer un S , mais il se trouve que Go applique des rĂšgles inutilement restrictives pour la conformitĂ© aux interfaces : en fait, les mĂȘmes rĂšgles que Java appliquĂ© Ă  l'origine. Le sous-typage ne nĂ©cessite pas de constructeurs de type non invariants, comme l'a observĂ© @Merovius .

@andrewcmyers Que se passe-t-il avec les interfaces multi-paramÚtres, comme celles nécessaires aux collections abstraites ?

De plus, l'affectation de x Ă  y peut ĂȘtre considĂ©rĂ©e comme dĂ©montrant l'hĂ©ritage d'interface sans aucun sous-typage. En Haskell (qui n'a clairement pas de sous-typage), vous Ă©cririez :

class Cloneable t => CloneableZ t where...

OĂč nous avons x est un type qui implĂ©mente CloneableZ qui, par dĂ©finition, implĂ©mente Ă©galement Cloneable , donc peut Ă©videmment ĂȘtre assignĂ© Ă  y .

Pour essayer de rĂ©sumer, vous pouvez soit voir une interface comme un type et Go pour avoir un sous-typage limitĂ© sans constructeurs de type covariant ou contravariant, soit vous pouvez la voir comme un "objet trait", ou peut-ĂȘtre que dans Go nous l'appellerions un " interface object", qui est en fait un conteneur polymorphe contraint par une interface "typeclass". Dans le modĂšle de classe de types, il n'y a pas de sous-typage, et donc aucune raison d'avoir Ă  penser Ă  la covariance et Ă  la contravariance.

Si nous nous en tenons au modÚle de sous-typage, nous ne pouvons pas avoir de types de collection, c'est pourquoi C++ a dû introduire des modÚles, car le sous-typage orienté objet n'est pas suffisant pour définir de maniÚre générique des concepts comme les conteneurs. Nous nous retrouvons avec deux mécanismes d'abstraction, objets et sous-typage, et modÚles/traits et génériques, et les interactions entre les deux deviennent complexes, regardez C++, C# et Scala par exemple. Il y aura des appels continus pour introduire des constructeurs covariants et contravariants pour augmenter la puissance des génériques, conformément à ces autres langages.

Si nous voulons des collections génériques sans introduire un systÚme de génériques séparé, alors nous devrions penser à des interfaces comme des classes de types. Les interfaces multi-paramÚtres signifieraient ne plus penser au sous-typage, mais plutÎt penser à l'héritage d'interface. Si nous voulons améliorer les génériques dans Go et autoriser les abstractions de choses comme les collections, et que nous ne voulons pas la complexité des systÚmes de types de langages comme C++, C#, Scala, etc., alors les interfaces multiparamÚtres et l'héritage d'interface sont le moyen aller.

@keean

Que se passe-t-il avec les interfaces multi-paramÚtres, comme celles nécessaires pour abstraire les collections ?

Veuillez consulter nos articles sur Genus et Familia, qui prennent en charge les contraintes de type multiparamĂštres. Familia unifie ces contraintes avec des interfaces et permet aux interfaces de contraindre plusieurs types.

Si nous nous en tenons au modĂšle de sous-typage, nous ne pouvons pas avoir de types de collection

Je ne suis pas tout à fait sûr de ce que vous entendez par "le modÚle de sous-typage", mais il est assez clair que Java et C # ont des types de collection, donc cette affirmation n'a pas beaucoup de sens pour moi.

OĂč nous avons x est un type qui implĂ©mente CloneableZ qui, par dĂ©finition, implĂ©mente Ă©galement Cloneable, donc peut Ă©videmment ĂȘtre affectĂ© Ă  y.

Non, dans mon exemple, x est une variable et y est une autre variable. Si je sais que y est un type CloneableZ et que x est un type Cloneable , cela ne signifie pas que je peux attribuer de y Ă  x. C'est ce que fait mon exemple.

Pour clarifier que le sous-typage est nécessaire pour modéliser Go, vous trouverez ci-dessous une version affinée de l'exemple dont l'équivalent moral ne vérifie pas le type dans Haskell. L'exemple montre que le sous-typage permet la création de collections hétérogÚnes dans lesquelles différents éléments ont des implémentations différentes. De plus, l'ensemble des implémentations possibles est ouvert.

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

Je ne suis pas tout à fait sûr de ce que vous entendez par "le modÚle de sous-typage", mais il est assez clair que Java et C # ont des types de collection, donc cette affirmation n'a pas beaucoup de sens pour moi.

Regardez pourquoi C++ a dĂ©veloppĂ© des modĂšles, le modĂšle de sous-typage OO n'Ă©tait pas capable d'exprimer les concepts gĂ©nĂ©riques nĂ©cessaires pour gĂ©nĂ©raliser des choses comme les collections. C # et Java ont Ă©galement dĂ» introduire un systĂšme gĂ©nĂ©rique complet sĂ©parĂ© des objets, du sous-typage et de l'hĂ©ritage, puis ont dĂ» nettoyer le gĂąchis des interactions complexes des deux systĂšmes avec des choses comme les constructeurs de types covariants et contravariants. Avec le recul, nous pouvons Ă©viter le sous-typage OO et regarder Ă  la place ce qui se passe si nous ajoutons des interfaces (classes de types) Ă  un langage simplement typĂ©. C'est ce que Rust a fait, il vaut donc la peine d'y jeter un coup d'Ɠil, mais bien sĂ»r, c'est compliquĂ© par toute la durĂ©e de vie. Go a GC donc il n'aurait pas cette complexitĂ©. Ma suggestion est que Go peut ĂȘtre Ă©tendu pour permettre des interfaces multi-paramĂštres et Ă©viter cette complexitĂ©.

Concernant votre affirmation selon laquelle vous ne pouvez pas faire cet exemple dans Haskell, voici le 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)

Quelques diffĂ©rences intĂ©ressantes, Go dĂ©rive automatiquement ce type data Cloneable = forall a . (ICloneable a, Show a) => ToCloneable a car c'est ainsi que vous transformez une interface (qui n'a pas de stockage) en un type (qui a du stockage), Rust dĂ©rive Ă©galement ces types et les appelle "objets de trait" . Dans d'autres langages comme Java, C# et Scala, nous constatons que vous ne pouvez pas instancier les interfaces, ce qui est en fait "correct", les interfaces ne sont pas des types, elles n'ont pas de stockage, Go dĂ©rive automatiquement le type d'un conteneur existentiel pour vous afin que vous puissiez traiter l'interface comme un type, et Go vous le cache en donnant au conteneur existentiel le mĂȘme nom que l'interface dont il est dĂ©rivĂ©. L'autre chose Ă  noter est que ce [2]Cloneable{x, y} contraint tous les membres Ă  Cloneable , alors que Haskell n'a pas de telles coercitions implicites, et nous devons explicitement contraindre les membres avec ToCloneable .

On m'a également fait remarquer que nous ne devrions pas considérer les sous-types S et T de Cloneable parce que S et T ne sont pas structurellement compatibles. Nous pouvons littéralement déclarer n'importe quel type une instance de Cloneable (juste en déclarant la définition pertinente de la fonction clone dans Go) et ces types n'ont aucune relation entre eux.

La plupart des propositions de gĂ©nĂ©riques semblent inclure des jetons supplĂ©mentaires qui, Ă  mon avis, nuisent Ă  la lisibilitĂ© et Ă  la simple sensation de Go. Je voudrais proposer une syntaxe diffĂ©rente qui, je pense, pourrait bien fonctionner avec la grammaire existante de Go (il arrive mĂȘme que la syntaxe soit assez bien mise en Ă©vidence dans Github Markdown).

Les points principaux de la proposition :

  • La grammaire de Go semble toujours avoir un moyen simple de dĂ©terminer quand une dĂ©claration de type s'est terminĂ©e car nous recherchons un jeton ou un mot-clĂ© spĂ©cifique. Si cela est vrai dans tous les cas, les arguments de type peuvent simplement ĂȘtre ajoutĂ©s Ă  la suite des noms de type eux-mĂȘmes.
  • Comme la plupart des propositions, le mĂȘme identifiant signifie le mĂȘme type dans toute dĂ©claration de fonction. Ces identifiants n'Ă©chappent jamais Ă  la dĂ©claration.
  • Dans la plupart des propositions, vous devez dĂ©clarer des arguments de type gĂ©nĂ©rique, mais dans cette proposition, c'est implicite. Certaines personnes prĂ©tendront que cela nuit Ă  la lisibilitĂ© ou Ă  la clartĂ© (l'implicite est mauvaise), ou limite la capacitĂ© de nommer un type, les rĂ©futations suivent :

    • Quand il s'agit de nuire Ă  la lisibilitĂ©, je pense que vous pouvez le discuter dans les deux sens, le plusou [T] nuit tout autant Ă  la lisibilitĂ© en faisant beaucoup de bruit syntaxique.

    • L'implicite, lorsqu'elle est utilisĂ©e correctement, peut aider une langue Ă  ĂȘtre moins verbeuse. Nous Ă©lidons les dĂ©clarations de type avec := tout le temps parce que les informations cachĂ©es par cela ne sont tout simplement pas assez importantes pour ĂȘtre Ă©pelĂ©es Ă  chaque fois.

    • Nommer un type concret (non gĂ©nĂ©rique) a ou t est probablement une mauvaise pratique, donc cette proposition suppose qu'il est sĂ»r de rĂ©server ces identifiants pour agir comme arguments de type gĂ©nĂ©rique. Bien que cela nĂ©cessiterait peut-ĂȘtre une migration go fix?

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
}

Ceci est tiré d'un Gist qui contient un peu plus de détails ainsi que des types de somme proposés ici : https://gist.github.com/aarondl/9b950373642fcf5072942cf0fca2c3a2

Ce n'est pas une proposition de gĂ©nĂ©riques complĂštement dĂ©busquĂ©e et ce n'est pas censĂ© l'ĂȘtre, il y a beaucoup de problĂšmes Ă  rĂ©soudre pour pouvoir ajouter des gĂ©nĂ©riques Ă  Go. Celui-ci ne traite que de la syntaxe, et j'espĂšre que nous pourrons avoir une conversation pour savoir si ce qui est proposĂ© est faisable/souhaitable.

@aarondl
Cela me semble bien, en utilisant cette syntaxe, nous aurions:

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 Pourriez-vous s'il vous plaĂźt expliquer un peu le type Collection . Je n'arrive pas Ă  comprendre :

type Collection a b interface {
   member(c a, e b) Bool
   insert(c a, e b) a
}

@dc0d Collection est une interface qui résume _toutes_ les collections, donc les arbres, les listes, les tranches, etc., nous pouvons donc avoir des opérations génériques comme member et insert qui fonctionneront sur n'importe quelle collection contenant n'importe quel type de données. Dans ce qui précÚde, j'ai donné l'exemple de la définition de 'insert' pour le type LinkedList dans l'exemple précédent :

func insert(c *LinkedList a, e a) *LinkedList a {
   c.Append(e)
   return c
}

On pourrait aussi le définir pour une tranche

func insert(c []a, e a) []a {
   return append(c, e)
}

Cependant, nous n'avons mĂȘme pas besoin du type de fonctions paramĂ©triques avec des variables de type comme illustrĂ© par @aarondl avec le type polymorphe a pour que cela fonctionne, car vous pouvez simplement dĂ©finir pour les types concrets :

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

Donc Collection est une interface pour généraliser à la fois le type d'un conteneur et le type de son contenu, permettant d'écrire des fonctions génériques qui fonctionnent sur toutes les combinaisons de conteneur et de contenu.

Il n'y a aucune raison pour que vous ne puissiez pas Ă©galement avoir une tranche de collections []Collection oĂč le contenu serait tous diffĂ©rents types de collection avec diffĂ©rents types de valeurs, Ă  condition member et insert aient Ă©tĂ© dĂ©finis pour chaque combinaison .

@aarondl Étant donnĂ© que type LinkedList a est dĂ©jĂ  une dĂ©claration de type valide, je ne vois que deux façons de rendre cette analyse analysable sans ambiguĂŻtĂ©: Rendre la grammaire sensible au contexte (entrer dans les problĂšmes d'analyse C, ugh) ou utiliser une anticipation illimitĂ©e ( ce que la grammaire go a tendance Ă  Ă©viter, Ă  cause des mauvais messages d'erreur en cas d'Ă©chec). Je comprends peut-ĂȘtre mal quelque chose, mais l'OMI qui s'oppose Ă  une approche sans jeton.

@keean Les interfaces dans Go utilisent des méthodes, pas des fonctions. Dans la syntaxe spécifique que vous avez suggérée, rien n'attache insert à *LinkedList pour le compilateur (dans Haskell, cela se fait via des déclarations instance ). Il est également normal que les méthodes modifient la valeur sur laquelle elles opÚrent. Rien de tout cela n'est un Show-Stopper, soulignant simplement que la syntaxe que vous suggérez ne fonctionne pas bien avec Go. Probablement plus quelque chose comme

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

Ce qui dĂ©montre Ă©galement quelques questions supplĂ©mentaires sur la façon dont les paramĂštres de type sont dĂ©finis et comment cela doit ĂȘtre analysĂ©.

@aarondl il y a aussi plus de questions que j'aurais sur votre proposition. Par exemple, il n'autorise pas les contraintes, vous n'obtenez donc qu'un polymorphisme sans contrainte. Ce qui, en gĂ©nĂ©ral, n'est pas vraiment utile, car vous n'ĂȘtes pas autorisĂ© Ă  faire quoi que ce soit avec les valeurs que vous obtenez (par exemple, vous ne pouvez pas implĂ©menter Collection avec une carte, car tous les types ne sont pas des clĂ©s de carte valides). Que devrait-il se passer lorsque quelqu'un essaie de faire quelque chose comme ça ? S'il s'agit d'une erreur de compilation, se plaint-il de l'instanciation (messages d'erreur C++ Ă  venir) ou de la dĂ©finition (vous ne pouvez pratiquement rien faire, car rien ne fonctionne avec tous les types) ?

@keean Je n'arrive toujours pas à comprendre comment a est limité à une liste (ou une tranche ou toute autre collection). S'agit-il d'une grammaire spéciale dépendante du contexte pour les collections ? Si oui quelle est sa valeur ? Il n'est pas possible de déclarer des types définis par l'utilisateur de cette maniÚre.

@Merovius Cela signifie-t-il que Go ne peut pas effectuer d'envois multiples et rend le premier argument d'une "fonction" spécial? Cela suggÚre que les types associés seraient mieux adaptés que les interfaces à paramÚtres multiples. Quelque chose comme ça:

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

Cependant, cela pose toujours des problĂšmes car rien n'oblige les deux collections Ă  ĂȘtre du mĂȘme type ... Vous auriez besoin de quelque chose comme:

func[A] useIt(c A, e A.Element) requires A:Collection

Pour tenter d'expliquer la diffĂ©rence, les interfaces multiparamĂštres ont des types _input_ supplĂ©mentaires qui participent Ă  la sĂ©lection d'instance (d'oĂč la connexion avec la rĂ©partition multiple), alors que les types associĂ©s sont des types _output_, seul le type rĂ©cepteur participe Ă  la sĂ©lection d'instance, puis les types associĂ©s dĂ©pendent du type de rĂ©cepteur.

@dc0d a et b sont des paramĂštres de type de l'interface, tout comme dans une classe de type Haskell. Pour que quelque chose soit considĂ©rĂ© comme un Collection , il doit dĂ©finir les mĂ©thodes qui correspondent aux types dans l'interface oĂč a et b peuvent ĂȘtre de n'importe quel type. Cependant, comme @Merovius l' a soulignĂ©, les interfaces Go sont basĂ©es sur des mĂ©thodes et ne prennent pas en charge la rĂ©partition multiple, de sorte que les interfaces multiparamĂštres peuvent ne pas convenir. Avec le modĂšle de mĂ©thode Ă  envoi unique de Go, avoir des types associĂ©s dans les interfaces, au lieu de plusieurs paramĂštres, semblerait ĂȘtre un meilleur ajustement. Cependant, l'absence de rĂ©partition multiple rend difficile l'implĂ©mentation de fonctions comme unify(x, y) , et vous devez utiliser le modĂšle de rĂ©partition double qui n'est pas trĂšs agrĂ©able.

Pour expliquer un peu plus la chose multi-paramĂštres:

type Cloneable[A] interface {
   clone(x A) A
}

Ici, a reprĂ©sente n'importe quel type, peu importe ce que c'est, tant que les fonctions correctes sont dĂ©finies, nous le considĂ©rons comme Cloneable . Nous considĂ©rerions les interfaces comme des contraintes sur les types plutĂŽt que les types eux-mĂȘmes.

func clone(x int) int {...}

ainsi, dans le cas de 'clone', nous remplaçons a par int dans la définition de l'interface, et nous pouvons appeler clone si la substitution réussit. Cela correspond bien à cette notation :

func[A] test(x A) A requires Cloneable[A] {...}

Cela équivaut à :

type Cloneable interface {
   clone() Cloneable
}

mais dĂ©clare une fonction et non une mĂ©thode et peut ĂȘtre Ă©tendue avec plusieurs paramĂštres. Si vous avez un langage avec rĂ©partition multiple, le premier argument d'une fonction/mĂ©thode n'a rien de spĂ©cial, alors pourquoi l'Ă©crire Ă  un endroit diffĂ©rent.

Comme Go n'a pas d'envoi multiple, tout cela commence à sembler trop difficile à changer d'un coup. Il semble que les types associés seraient mieux adaptés, bien que plus limités. Cela permettrait des collections abstraites, mais pas des solutions élégantes à des choses comme l'unification.

@Merovius Merci d'avoir jetĂ© un coup d'Ɠil Ă  la proposition. Permettez-moi d'essayer de rĂ©pondre Ă  vos prĂ©occupations. Je suis triste que vous ayez rejetĂ© la proposition avant que nous en discutions davantage, j'espĂšre que je pourrai changer d'avis - ou peut-ĂȘtre que vous pourrez changer le mien :)

Anticipation illimitée :
Donc, comme je l'ai mentionnĂ© dans la proposition, il semble actuellement que la grammaire Go ait un bon moyen de dĂ©tecter syntaxiquement la "fin" de presque tout. Et nous le ferions encore Ă  cause des arguments gĂ©nĂ©riques implicites. Une seule lettre minuscule Ă©tant la construction syntaxique qui crĂ©e cet argument gĂ©nĂ©rique - ou tout ce que nous dĂ©cidons de faire de ce jeton en ligne, peut-ĂȘtre mĂȘme nous rabattons-nous sur une chose symbolisĂ©e comme @a dans la proposition si nous aimons suffisamment la syntaxe mais ce n'est pas le cas possible Ă©tant donnĂ© la difficultĂ© du compilateur sans jetons, bien que la proposition perde beaucoup de charme dĂšs que vous faites cela.

Quoi qu'il en soit, le problĂšme avec type LinkedList a sous cette proposition n'est pas si difficile car nous savons que a est un argument de type gĂ©nĂ©rique et donc cela Ă©chouerait avec une erreur de compilation identique Ă  type LinkedList Ă©choue aujourd'hui avec : prog.go:3:16: expected type, found newline (and 1 more errors) . Le message d'origine n'est pas vraiment sorti et le dit, mais vous n'ĂȘtes plus autorisĂ© Ă  nommer un type concret [a-z]{1} qui, je pense, rĂ©sout ce problĂšme et est un sacrifice avec lequel je pense que nous serions tous d'accord making (je ne vois que des inconvĂ©nients dans la crĂ©ation de types rĂ©els avec des noms Ă  une seule lettre dans le code Go aujourd'hui).

C'est juste du polymorphisme sans contrainte
La raison pour laquelle j'ai omis tout type de traits ou de contraintes d'arguments gĂ©nĂ©riques est que je pense que c'est le rĂŽle des interfaces dans Go, si vous souhaitez faire quelque chose avec une valeur, cette valeur doit ĂȘtre un type d'interface et non un type entiĂšrement gĂ©nĂ©rique. Je pense que cette proposition fonctionne bien avec les interfaces aussi.

Selon cette proposition, nous aurions toujours le mĂȘme problĂšme qu'actuellement avec des opĂ©rateurs tels que + , vous ne pourriez donc pas crĂ©er de fonction d'ajout gĂ©nĂ©rique pour tous les types numĂ©riques, mais vous pourriez accepter une fonction d'ajout gĂ©nĂ©rique comme argument. ConsidĂ©rer ce qui suit:

func Sort(slice []a, compare func (a, a) bool) { ... }

Questions sur la portée

Tu as donné un exemple ici :

type Collection e interface {
    Element(e) book
    Insert(e)
}

func (l *(LinkedList e)) Element(el e) book {
    // ...
}

func (l* (LinkedList e)) Insert(el e) {
    // ...
}

En rÚgle générale, la portée de ces identifiants est liée à la déclaration/définition particuliÚre dans laquelle ils se trouvent. Ils ne sont partagés nulle part et je ne vois aucune raison pour qu'ils le soient.

@keean C'est trÚs intéressant bien que, comme d'autres l'ont souligné, vous deviez modifier ce que vous avez montré pour pouvoir implémenter les interfaces (actuellement, dans votre exemple, il n'y a pas de méthodes avec des récepteurs, uniquement des fonctions). J'essaie de réfléchir davantage à la façon dont cela affecte ma proposition initiale.

Une seule lettre minuscule étant la construction syntaxique qui crée cet argument générique

Je ne me sens pas bien à ce sujet; cela nécessite d'avoir des productions séparées pour ce qu'est un identifiant en fonction du contexte et signifie également interdire arbitrairement certains identifiants pour les types. Mais ce n'est pas vraiment le moment de parler de ces détails.

Dans le cadre de cette proposition, nous aurions toujours le mĂȘme problĂšme qu'actuellement avec des opĂ©rateurs tels que +

Je ne comprends pas cette phrase. Actuellement, l'opérateur + n'a aucun de ces problÚmes, car les types de ses opérandes sont connus localement et le message d'erreur est clair et sans ambiguïté et indique la source du problÚme. Ai-je raison de supposer que vous dites que vous souhaitez interdire toute utilisation de valeurs génériques qui n'est pas autorisée pour tous les types possibles (je ne peux pas penser à beaucoup d'opérations de ce type) ? Et créer une erreur de compilation pour l'expression incriminée dans la fonction générique ? OMI qui limiterait trop la valeur des génériques.

si vous souhaitez faire quelque chose avec une valeur, cette valeur doit ĂȘtre un type d'interface et non un type entiĂšrement gĂ©nĂ©rique.

Les deux principales raisons pour lesquelles les gens veulent des gĂ©nĂ©riques sont les performances (Ă©viter l'encapsulation des interfaces) et la sĂ©curitĂ© des types (s'assurer que le mĂȘme type est utilisĂ© Ă  diffĂ©rents endroits, sans se soucier de savoir lequel). Cela semble ignorer ces raisons.

vous pouvez accepter une fonction d'ajout générique comme argument.

Vrai. Mais assez peu ergonomique. Considérez le nombre de plaintes concernant l'API sort . Pour de nombreux conteneurs génériques, le nombre de fonctions que l'appelant devrait implémenter et transmettre semble prohibitif. Considérez, à quoi ressemblerait une implémentation container/heap dans le cadre de cette proposition et en quoi serait-elle meilleure que l'implémentation actuelle, en termes d'ergonomie ? Il semblerait que les gains soient négligeables ici, au mieux. Vous devriez implémenter plus de fonctions triviales (et dupliquer vers/référencer sur chaque site d'utilisation), pas moins.

@Merovius

penser Ă  ce point de @aarondl

vous pouvez accepter une fonction d'ajout générique comme argument.

Il serait préférable d'avoir une interface Addable pour permettre la surcharge de l'addition, étant donné une certaine syntaxe pour définir les opérateurs infixes :

type Addable interface {
   + (x Addable, y Addable) Addable
}

Malheureusement, cela ne fonctionne pas, car cela n'exprime pas que nous nous attendons à ce que tous les types soient identiques. Pour définir addable, nous aurions besoin de quelque chose comme les interfaces multi-paramÚtres :

type Addable[A] interface {
   + (x A, y A) A
}

Ensuite, vous auriez également besoin de Go pour effectuer une répartition multiple, ce qui signifierait que tous les arguments d'une fonction sont traités comme un récepteur pour la correspondance d'interface. Ainsi, dans l'exemple ci-dessus, tout type est Addable s'il y a une fonction + définie dessus qui satisfait les définitions de fonction dans la définition d'interface.

Mais compte tenu de ces changements, vous pouvez maintenant écrire :

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

Bien sĂ»r, la surcharge de fonctions et la distribution multiple peuvent ĂȘtre quelque chose que les gens ne veulent jamais dans Go, mais des choses comme dĂ©finir l'arithmĂ©tique de base sur des types dĂ©finis par l'utilisateur comme les vecteurs, les matrices, les nombres complexes, etc., seront toujours impossibles. Comme je l'ai dit ci-dessus, les "types associĂ©s" sur les interfaces permettraient une certaine augmentation de la capacitĂ© de programmation gĂ©nĂ©rique, mais pas une gĂ©nĂ©ralitĂ© complĂšte. L'envoi multiple (et probablement la surcharge de fonctions) est-il quelque chose qui pourrait arriver dans Go ?

des choses comme définir l'arithmétique de base sur des types définis par l'utilisateur comme les vecteurs, les matrices, les nombres complexes, etc., seront toujours impossibles.

Certains pourraient considérer qu'il s'agit d'une fonctionnalité :) AFAIR, il y a une proposition ou un fil flottant quelque part pour discuter de l'opportunité. FWIW, je pense que c'est - encore une fois - un hors-sujet errant. La surcharge d'opérateurs (ou les idées générales "comment faire Go plus Haskell") n'est pas vraiment le but de ce problÚme :)

L'envoi multiple (et probablement la surcharge de fonctions) est-il quelque chose qui pourrait arriver dans Go ?

Ne jamais dire jamais. Je ne m'y attendais pas, personnellement.

@Merovius

Certains pourraient considérer cela comme une fonctionnalité :)

Bien sĂ»r, et si Go ne le fait pas, il y a d'autres langues qui le feront :-) Go ne doit pas ĂȘtre tout pour tout le monde. J'essayais juste d'Ă©tablir une certaine portĂ©e pour les gĂ©nĂ©riques dans Go. Mon objectif est de crĂ©er des langages entiĂšrement gĂ©nĂ©riques, car j'ai une aversion pour la rĂ©pĂ©tition et le passe-partout (et je n'aime pas les macros). Si j'avais un centime pour chaque fois que j'ai dĂ» Ă©crire une liste chaĂźnĂ©e ou un arbre en 'C' pour un type de donnĂ©es spĂ©cifique. Cela rend en fait certains projets impossibles pour une petite Ă©quipe en raison du volume de code qui doit ĂȘtre conservĂ© dans votre tĂȘte pour le comprendre, puis maintenu au fil des modifications. Parfois, je pense que les gens qui n'ont pas besoin de gĂ©nĂ©riques n'ont tout simplement pas encore Ă©crit un programme assez important. Bien sĂ»r, vous pouvez Ă  la place avoir une grande Ă©quipe de dĂ©veloppeurs travaillant sur quelque chose et n'avoir que chaque dĂ©veloppeur responsable d'une petite partie du code total, mais je suis intĂ©ressĂ© Ă  rendre un dĂ©veloppeur unique (ou une petite Ă©quipe) aussi efficace que possible.

Étant donnĂ© que la surcharge de fonctions et l'envoi multiple sont hors de portĂ©e, et compte tenu Ă©galement des problĂšmes d'analyse avec la suggestion de @ aarondl , il semble que l'ajout de types associĂ©s aux interfaces et de paramĂštres de type aux fonctions serait Ă  peu prĂšs aussi loin que vous voudriez go avec des gĂ©nĂ©riques dans Go.

Quelque chose comme ça semblerait ĂȘtre le bon genre de chose:

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

Ensuite, il y aurait une dĂ©cision dans l'implĂ©mentation d'utiliser des types paramĂ©triques ou des types universellement quantifiĂ©s. Avec les types paramĂ©triques (comme Java), une fonction "gĂ©nĂ©rique" n'est pas rĂ©ellement une fonction mais une sorte de modĂšle de fonction de type sĂ©curisĂ©, et en tant que tel ne peut pas ĂȘtre passĂ© comme argument Ă  moins que son paramĂštre de type ne soit fourni ainsi :

f(useIt) // not okay with parametric types
f(useIt<List>) // okay with parametric types

Avec les types universellement quantifiĂ©s, vous pouvez passer useIt comme argument, et il peut ensuite ĂȘtre fourni avec un paramĂštre de type Ă  l'intĂ©rieur f . La raison de privilĂ©gier les types paramĂ©triques est que vous pouvez monomorphiser le polymorphisme au moment de la compilation, ce qui signifie qu'il n'y a pas d'Ă©laboration de fonctions polymorphes au moment de l'exĂ©cution. Je ne suis pas sĂ»r que ce soit un problĂšme avec Go, car Go effectue dĂ©jĂ  la distribution d'exĂ©cution sur les interfaces, donc tant que le paramĂštre de type pour useIt implĂ©mente Collection, vous pouvez envoyer au bon rĂ©cepteur au moment de l'exĂ©cution, donc universel la quantification est probablement la bonne voie pour le Go.

Je me demande, SFINAE mentionnĂ© uniquement par @bcmills. Pas mĂȘme mentionnĂ© dans la proposition (bien que Sort soit lĂ  comme exemple).
À quoi pourrait ressembler alors le Sort pour la tranche et la liste liĂ©e ?

@keean
Je n'arrive pas Ă  comprendre comment on dĂ©finirait une collection gĂ©nĂ©rique "Slice" avec votre suggestion. Vous semblez dĂ©finir un 'IntSlice' qui pourrait implĂ©menter 'Collection' (bien que Insert renvoie un type diffĂ©rent de celui recherchĂ© par l'interface), mais ce n'est pas une 'tranche' gĂ©nĂ©rique, car cela semble ĂȘtre uniquement pour ints , et les implĂ©mentations de mĂ©thode ne concernent que les ints. Doit-on dĂ©finir une implĂ©mentation spĂ©cifique par type ?

Parfois, je pense que les gens qui n'ont pas besoin de génériques n'ont tout simplement pas encore écrit un programme assez important.

Je peux vous assurer que cette impression est fausse. Et FWIW, ISTM que "l'autre cĂŽtĂ©" met "ne pas voir le besoin" dans le mĂȘme seau que "ne pas voir l'utilitĂ©". J'en vois l'utilitĂ© et ne le rĂ©fute pas. Je n'en vois pourtant pas vraiment la nĂ©cessitĂ© . Je me dĂ©brouille bien sans, mĂȘme dans les grandes bases de code.

Et ne confondez pas non plus "vouloir qu'elles soient bien faites et indiquer oĂč les propositions existantes ne le sont pas" avec "s'opposer fondamentalement Ă  l'idĂ©e mĂȘme".

Ă©galement compte tenu des problĂšmes d'analyse avec la suggestion de @ aarondl .

Comme je l'ai dit, je ne pense pas que parler du problĂšme d'analyse soit vraiment productif en ce moment. Les problĂšmes d'analyse peuvent ĂȘtre rĂ©solus. Le manque de polymorphisme contraint est beaucoup plus grave, sĂ©mantiquement. IMO, ajouter des gĂ©nĂ©riques sans cela ne vaut vraiment pas la peine.

@urandom

Je n'arrive pas à comprendre comment on définirait une collection générique "Slice" avec votre suggestion.

Comme indiqué ci-dessus, vous auriez toujours besoin de définir une implémentation distincte pour chaque type de tranche, mais vous gagneriez toujours à pouvoir écrire des algorithmes en termes d'interface générique. Si vous souhaitez autoriser une implémentation générique pour toutes les tranches, vous devez autoriser les types et méthodes paramétriques associés. Notez que j'ai déplacé le paramÚtre de type aprÚs le mot-clé afin qu'il se produise avant le type de récepteur.

type<T> []T.Element = Int

func<T> ([]T) Member(e T) Bool {...}
func<T> ([]T) Insert(e T) Collection {...}

Cependant, maintenant, vous devez également gérer la spécialisation, car quelqu'un pourrait définir le type et les méthodes associés pour les []int plus spécialisés et vous devrez décider lequel utiliser. Normalement, vous iriez avec l'instance la plus spécifique, mais cela ajoute une autre couche de complexité.

Je ne sais pas combien cela vous rapporte rĂ©ellement. Avec mon exemple original ci-dessus, vous pouvez Ă©crire des algorithmes gĂ©nĂ©riques pour agir sur des collections gĂ©nĂ©rales Ă  l'aide de l'interface, et vous n'auriez qu'Ă  fournir les mĂ©thodes et les types associĂ©s pour les types que vous utilisez rĂ©ellement. La principale victoire pour moi est de pouvoir dĂ©finir des algorithmes comme le tri sur des collections arbitraires et de mettre ces algorithmes dans une bibliothĂšque. Si j'ai ensuite une liste de "formes", il me suffit de dĂ©finir les mĂ©thodes d'interface de collection pour ma liste de formes, et je peux ensuite utiliser n'importe quel algorithme de la bibliothĂšque sur celles-ci. Être capable de dĂ©finir les mĂ©thodes d'interface pour tous les types de tranches m'intĂ©resse moins, et pourrait ĂȘtre trop complexe pour Go ?

@Merovius

Mais je n'en vois pas vraiment la nĂ©cessitĂ©. Je me dĂ©brouille bien sans, mĂȘme dans les grandes bases de code.

Si vous pouvez faire face Ă  un programme de 100 000 lignes, vous pourrez faire plus avec 100 000 lignes gĂ©nĂ©riques qu'avec 100 000 lignes non gĂ©nĂ©riques (en raison de la rĂ©pĂ©tition). Ainsi, vous pouvez ĂȘtre un dĂ©veloppeur super-star capable de gĂ©rer de trĂšs grandes bases de code, mais vous obtiendriez toujours plus avec une trĂšs grande base de code gĂ©nĂ©rique car vous Ă©limineriez la redondance. Ce programme gĂ©nĂ©rique se transformerait en un programme non gĂ©nĂ©rique encore plus vaste. Il me semble juste que vous n'avez pas encore atteint votre limite de complexitĂ©.

Cependant, je pense que vous avez raison, le "besoin" est trop fort, j'écris volontiers du code go, avec seulement une frustration occasionnelle à propos du manque de génériques, et je peux contourner ce problÚme en écrivant simplement plus de code, et dans Go ce code est agréablement direct et littéral.

L'absence de polymorphisme contraint est beaucoup plus grave, sémantiquement. IMO, ajouter des génériques sans cela ne vaut vraiment pas la peine.

Je suis d'accord avec ça.

vous pourrez faire plus avec 100 000 lignes génériques qu'avec 100 000 lignes non génériques (à cause de la répétition)

Je suis curieux, d'aprÚs votre exemple hypothétique, quel % de ces lignes serait une fonction générique ?
D'aprÚs mon expérience, c'est moins de 2% (à partir d'une base de code avec 115k LOC), donc je ne pense pas que ce soit un bon argument sauf si vous écrivez une bibliothÚque pour les "collections"

Je souhaite que nous ayons finalement des génériques

@keean

Concernant votre affirmation selon laquelle vous ne pouvez pas faire cet exemple dans Haskell, voici le code :

Ce code n'est pas moralement Ă©quivalent au code que j'ai Ă©crit. Il introduit un nouveau type de wrapper Cloneable en plus de l'interface ICloneable. Le code Go n'avait pas besoin d'un wrapper ; ni d'autres langues qui prennent en charge le sous-typage.

@andrewcmyers

Ce code n'est pas moralement Ă©quivalent au code que j'ai Ă©crit. Il introduit un nouveau type de wrapper Cloneable en plus de l'interface ICloneable.

N'est-ce pas ce que fait ce code :

type Cloneable interface {...}

Il introduit un data-type 'Cloneable' dérivé de l'interface. Vous ne voyez pas le 'ICloneable' parce que vous n'avez pas de déclarations d'instance pour les interfaces, vous déclarez simplement les méthodes.

Peut-on considĂ©rer qu'il s'agit d'un sous-typage lorsque les types qui implĂ©mentent une interface n'ont pas besoin d'ĂȘtre structurellement compatibles ?

@keean Je considérerais Cloneable n'est qu'un type, pas vraiment un "type de données". Dans un langage comme Java, il n'y aurait pratiquement aucun coût supplémentaire à l'abstraction Cloneable , car il n'y aurait pas de wrapper, contrairement à votre code.

Il me semble limité et indésirable d'exiger une similitude structurelle entre les types implémentant une interface, donc je suis confus quant à ce que vous pensez ici.

@andrewcmyers
J'utilise le type et le type de données de maniÚre interchangeable. Tout type pouvant contenir des données est un type de données.

car il n'y aurait pas de wrapper, contrairement Ă  votre code.

Il y a toujours un wrapper car les types Go sont toujours encadrés, donc le wrapper existe autour de tout. Haskell a besoin que le wrapper soit explicite car il a des types non encadrés.

similitude structurelle entre les types implémentant une interface, donc je ne comprends pas ce que vous pensez ici.

Le sous-typage structurel exige que les types soient « structurellement compatibles ». Comme il n'y a pas de hiĂ©rarchie de type explicite comme dans un langage OO avec hĂ©ritage, le sous-typage ne peut pas ĂȘtre nominal, il doit donc ĂȘtre structurel, s'il existe.

Je vois cependant ce que vous voulez dire, que je décrirais comme considérant une interface comme une classe de base abstraite, pas une interface, avec une sorte de relation de sous-type nominal implicite avec tout type qui implémente les méthodes requises.

Je pense en fait que Go convient aux deux modÚles en ce moment, et cela pourrait aller dans les deux sens à partir d'ici, mais je suggérerais que l'appeler une interface et non une classe suggÚre une façon de penser sans sous-typage.

@keean Je ne comprends pas votre commentaire. D'abord, vous me dites que vous n'ĂȘtes pas d'accord et que je "n'ai tout simplement pas encore atteint ma limite de complexitĂ©", puis vous me dites que vous ĂȘtes d'accord (en ce sens que "besoin" est un mot trop fort). Je pense aussi que votre argument est fallacieux (vous supposez que LOC est la principale mesure de complexitĂ© et que chaque ligne de code est Ă©gale). Mais surtout, je ne pense pas que "qui Ă©crit des programmes plus compliquĂ©s" soit vraiment une ligne de discussion productive. J'essayais juste de clarifier que l'argument "si vous n'ĂȘtes pas d'accord avec moi, cela doit signifier que vous ne travaillez pas sur des problĂšmes aussi difficiles ou intĂ©ressants" n'est pas convaincant et n'est pas de bonne foi. J'espĂšre que vous pouvez simplement croire que les gens peuvent ĂȘtre en dĂ©saccord avec vous sur l'importance de cette fonctionnalitĂ© tout en Ă©tant tout aussi compĂ©tents et en faisant des choses tout aussi intĂ©ressantes.

@merovius
Je disais que vous ĂȘtes probablement un programmeur plus compĂ©tent que moi, et donc capable de travailler avec plus de complexitĂ©. Je ne pense certainement pas que vous travailliez sur des problĂšmes moins intĂ©ressants ou moins complexes, et je suis dĂ©solĂ© que cela se soit passĂ© ainsi. J'ai passĂ© la journĂ©e d'hier Ă  essayer de faire fonctionner un scanner, ce qui Ă©tait un problĂšme trĂšs inintĂ©ressant.

Je peux penser que les gĂ©nĂ©riques m'aident Ă  Ă©crire des programmes plus complexes avec ma capacitĂ© intellectuelle limitĂ©e, et aussi admettre que je n'ai pas "besoin" des gĂ©nĂ©riques. C'est une question de degrĂ©. Je peux toujours programmer sans gĂ©nĂ©riques, mais je ne peux pas nĂ©cessairement Ă©crire des logiciels de la mĂȘme complexitĂ©.

J'espÚre que cela vous rassure j'agis de bonne foi, je n'ai pas d'agenda caché ici, et si Go n'adopte pas les génériques je continuerai à l'utiliser. J'ai une opinion sur la meilleure façon de faire des génériques, mais ce n'est pas la seule opinion, je ne peux parler que de ma propre expérience. Si je n'aide pas, il y a beaucoup d'autres choses sur lesquelles je peux passer mon temps, alors dites juste un mot, et je me recentrerai ailleurs.

@Merovius Merci pour la poursuite du dialogue.

| Les deux principales raisons pour lesquelles les gens veulent des gĂ©nĂ©riques sont les performances (Ă©viter l'encapsulation des interfaces) et la sĂ©curitĂ© des types (s'assurer que le mĂȘme type est utilisĂ© Ă  diffĂ©rents endroits, sans se soucier de savoir lequel). Cela semble ignorer ces raisons.

Peut-ĂȘtre que nous examinons ce que j'ai proposĂ© de maniĂšre trĂšs diffĂ©rente, car de mon point de vue, cela fait ces deux choses pour autant que je sache? Dans l'exemple de liste chaĂźnĂ©e, il n'y a pas d'encapsulation avec les interfaces et elle devrait donc ĂȘtre aussi performante que si elle Ă©tait Ă©crite Ă  la main pour un type donnĂ©. Du cĂŽtĂ© de la sĂ©curitĂ© du type, c'est la mĂȘme chose. Y a-t-il un contre-exemple que vous pouvez donner ici pour m'aider Ă  comprendre d'oĂč vous venez ?

| Vrai. Mais assez peu ergonomique. Considérez le nombre de plaintes concernant l'API de tri. Pour de nombreux conteneurs génériques, le nombre de fonctions que l'appelant devrait implémenter et transmettre semble prohibitif. Considérez, à quoi ressemblerait une implémentation de conteneur/tas dans le cadre de cette proposition et en quoi serait-elle meilleure que l'implémentation actuelle, en termes d'ergonomie ? Il semblerait que les gains soient négligeables ici, au mieux. Vous devriez implémenter plus de fonctions triviales (et dupliquer vers/référencer sur chaque site d'utilisation), pas moins.

En fait, je ne suis pas du tout concernĂ© par cela. Je ne crois pas que le nombre de fonctions serait prohibitif, mais je suis dĂ©finitivement ouvert Ă  voir des contre-exemples. Rappelez-vous que l'API dont les gens se sont plaints n'Ă©tait pas celle pour laquelle vous deviez fournir une fonction mais celle d'origine ici : https://golang.org/pkg/sort/#Interface oĂč vous deviez crĂ©er un nouveau type qui Ă©tait simplement votre tranche + type, puis implĂ©mentez 3 mĂ©thodes dessus. À la lumiĂšre des plaintes et de la douleur associĂ©es Ă  cette interface, ce qui suit a Ă©tĂ© créé : https://golang.org/pkg/sort/#Slice , pour ma part, je n'ai aucun problĂšme avec cette API et nous rĂ©cupĂ©rerions les pĂ©nalitĂ©s de performance de celle-ci dans le cadre de la proposition dont nous discutons en modifiant simplement la dĂ©finition en func Slice(slice []a, less func(a, a) bool) .

En termes de structure de données container/heap , quelle que soit la proposition générique que vous acceptez et qui nécessite une réécriture complÚte. container/heap tout comme le package sort fournit simplement des algorithmes au-dessus de votre propre structure de données, mais aucun package ne possÚde jamais la structure de données car sinon nous aurions []interface{} et le les coûts associés à cela. Vraisemblablement, nous les changerions puisque vous pourriez avoir un Heap qui possÚde une tranche avec un type concret grùce aux génériques, et cela est vrai dans toutes les propositions que j'ai vues ici (y compris la mienne) .

J'essaie de dĂ©mĂȘler les diffĂ©rences dans nos points de vue sur ce que j'ai proposĂ©. Et je pense que la racine du dĂ©saccord (au-delĂ  de toute prĂ©fĂ©rence personnelle syntaxiquement) est qu'il n'y a pas de contraintes sur les types gĂ©nĂ©riques. Mais j'essaie toujours de comprendre ce que cela nous rapporte. Si la rĂ©ponse est que rien en matiĂšre de performances n'est autorisĂ© Ă  utiliser une interface, alors je ne peux pas dire grand-chose ici.

Considérez la définition de table de hachage suivante :

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

Sommes-nous en train de dire que le []Hasher est un non-démarreur en raison de problÚmes de performances/de stockage et que pour avoir une implémentation réussie de Generics dans Go, nous devons absolument avoir quelque chose comme ce qui suit ?

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

J'espĂšre que vous voyez oĂč je veux en venir. Mais il est tout Ă  fait possible que je ne comprenne pas les contraintes que vous souhaitez imposer Ă  certains codes. Peut-ĂȘtre qu'il y a des cas d'utilisation que je n'ai pas pris en compte, quoi qu'il en soit j'espĂšre arriver Ă  une meilleure comprĂ©hension de ce que sont les exigences et de la façon dont la proposition leur Ă©choue.

Peut-ĂȘtre que nous examinons ce que j'ai proposĂ© de maniĂšre trĂšs diffĂ©rente, car de mon point de vue, cela fait ces deux choses pour autant que je sache?

Le "ceci" dans la section que vous citez fait référence à l'utilisation d'interfaces. Le problÚme n'est pas que votre proposition ne le fasse pas non plus, c'est que votre proposition n'autorise pas le polymorphisme contraint, ce qui exclut la plupart de ses utilisations. Et l'alternative que vous avez suggérée pour cela était les interfaces, qui ne traitent pas vraiment non plus le cas d'utilisation principal des génériques (à cause des deux choses que j'ai mentionnées).

Par exemple, votre proposition (telle qu'écrite à l'origine) ne permettait pas d'écrire une carte générique de quelque sorte que ce soit, car cela nécessiterait de pouvoir au moins comparer les clés en utilisant == (ce qui est une contrainte, donc implémenter un map nécessite un polymorphisme contraint).

À la lumiĂšre des plaintes et de la douleur associĂ©es Ă  cette interface, ce qui suit a Ă©tĂ© créé : https://golang.org/pkg/sort/#Slice

Notez que cette interface n'est toujours pas possible dans votre proposition de gĂ©nĂ©riques, car elle repose sur la rĂ©flexion pour la longueur et l'Ă©change (donc, encore une fois, vous avez une contrainte sur les opĂ©rations de tranche). MĂȘme si nous acceptons cette API comme la limite infĂ©rieure de ce que les gĂ©nĂ©riques devraient pouvoir accomplir (beaucoup de gens ne le feraient pas. Il y a encore beaucoup de plaintes concernant le manque de sĂ©curitĂ© de type dans cette API), votre proposition ne passerait pas cette barre.

Mais aussi, encore une fois, vous citez une rĂ©ponse Ă  un point spĂ©cifique que vous avez soulevĂ©, Ă  savoir que vous pourriez obtenir un polymorphisme contraint en passant des littĂ©raux de fonction dans l'API. Et cette maniĂšre spĂ©cifique que vous avez suggĂ©rĂ©e pour contourner le manque de polymorphisme contraint nĂ©cessiterait la mise en Ɠuvre plus ou moins de l'ancienne API. c'est-Ă -dire que vous citez ma rĂ©ponse Ă  cet argument, que vous ne faites que rĂ©pĂ©ter :

nous récupérerions les pénalités de performance de cela dans le cadre de la proposition dont nous discutons en modifiant simplement la définition en func Slice(slice []a, less func(a, a) bool).

C'est l'ancienne API cependant. Vous dites "ma proposition n'autorise pas le polymorphisme contraint, mais ce n'est pas un problĂšme, car nous pouvons tout simplement ne pas utiliser de gĂ©nĂ©riques et utiliser Ă  la place les solutions existantes (rĂ©flexion/interfaces)". Eh bien, rĂ©pondre Ă  "votre proposition ne permet pas les cas d'utilisation les plus Ă©lĂ©mentaires pour lesquels les gens veulent des gĂ©nĂ©riques" par "nous pouvons simplement faire ce que les gens font dĂ©jĂ  sans gĂ©nĂ©riques pour ces cas d'utilisation les plus Ă©lĂ©mentaires" ne semble pas nous comprendre n'importe oĂč, TBH. Une proposition de gĂ©nĂ©riques qui ne vous aide pas Ă  Ă©crire mĂȘme les types de conteneurs de base, sort, max
 ne semble tout simplement pas en valoir la peine.

cela est vrai dans toutes les propositions que j'ai vues ici (y compris la mienne).

La plupart des propositions gĂ©nĂ©riques incluent un moyen de contraindre les paramĂštres de type. c'est-Ă -dire pour exprimer "le paramĂštre de type doit avoir une mĂ©thode Less", ou "le paramĂštre de type doit ĂȘtre comparable". Le vĂŽtre - AFAICT - ne le fait pas.

Considérez la définition de table de hachage suivante :

Votre dĂ©finition est incomplĂšte. a) Le type de clĂ© a Ă©galement besoin d'Ă©galitĂ© et b) vous n'empĂȘchez pas l'utilisation de diffĂ©rents types de clĂ©. c'est-Ă -dire que ce serait lĂ©gal:

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)

Cela ne devrait cependant pas ĂȘtre lĂ©gal, car vous utilisez diffĂ©rents types de clĂ©s. c'est-Ă -dire que le conteneur n'est pas vĂ©rifiĂ© par type dans la mesure oĂč les gens le souhaitent. Vous devez paramĂ©trer la table de hachage sur le type de clĂ© et de valeur

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

Ou, si cela vous aide, imaginez que vous essayez d'implĂ©menter un ensemble de hachage. Vous auriez le mĂȘme problĂšme, mais maintenant le conteneur rĂ©sultant n'a pas de vĂ©rification de type supplĂ©mentaire sur interface{} .

C'est pourquoi votre proposition n'aborde pas les cas d'utilisation les plus élémentaires : elle s'appuie sur des interfaces pour limiter le polymorphisme, mais ne fournit en fait aucun moyen de vérifier la cohérence de ces interfaces. Vous pouvez soit avoir une vérification de type cohérente, soit avoir un polymorphisme contraint, mais pas les deux. Mais vous avez besoin des deux.

que pour avoir une implémentation réussie de Generics dans Go, nous devons absolument avoir quelque chose comme ce qui suit ?

C'est du moins ce que je ressens à ce sujet, ouais, à peu prÚs. Si une proposition ne permet pas d'écrire des conteneurs de type sécurisé ou de trier ou
 cela n'ajoute vraiment rien au langage existant qui soit suffisamment important pour justifier le coût.

@Merovius D'accord. Je pense avoir compris ce que tu veux. Gardez Ă  l'esprit que vos cas d'utilisation sont trĂšs Ă©loignĂ©s de ce que je veux. Je n'ai pas vraiment envie de conteneurs sĂ»rs, bien que je soupçonne - comme vous l'avez dit - que cela puisse ĂȘtre une opinion minoritaire. Quelques-unes des choses les plus importantes que j'aimerais voir sont des types de rĂ©sultats au lieu d'erreurs et une manipulation facile des tranches sans duplication ni rĂ©flexion partout oĂč ma proposition fait un travail raisonnable. Cependant, je peux voir comment, de votre point de vue, cela "ne traite pas les cas d'utilisation les plus Ă©lĂ©mentaires" si votre cas d'utilisation de base consiste Ă  Ă©crire des conteneurs gĂ©nĂ©riques sans utiliser d'interfaces,

Notez que cette interface n'est toujours pas possible dans votre proposition de gĂ©nĂ©riques, car elle repose sur la rĂ©flexion pour la longueur et l'Ă©change (donc, encore une fois, vous avez une contrainte sur les opĂ©rations de tranche). MĂȘme si nous acceptons cette API comme la limite infĂ©rieure de ce que les gĂ©nĂ©riques devraient pouvoir accomplir (beaucoup de gens ne le feraient pas. Il y a encore beaucoup de plaintes concernant le manque de sĂ©curitĂ© de type dans cette API), votre proposition ne passerait pas cette barre.

En lisant ceci, il est clair que vous avez complĂštement mal compris la façon dont les tranches gĂ©nĂ©riques fonctionneraient/devraient fonctionner dans le cadre de cette proposition. C'est Ă  travers ce malentendu que vous ĂȘtes arrivĂ© Ă  la fausse conclusion que "cette interface n'est toujours pas possible dans votre proposition". Dans toute proposition, une tranche gĂ©nĂ©rique doit ĂȘtre possible, c'est ce que je pense. Et len() dans le monde tel que je l'ai vu serait dĂ©fini comme suit : func len(slice []a) , qui est un argument de tranche gĂ©nĂ©rique, ce qui signifie qu'il peut compter la longueur sans rĂ©flexion pour n'importe quelle tranche. C'est en grande partie l'intĂ©rĂȘt de cette proposition comme je l'ai dit ci-dessus (manipulation facile des tranches) et je suis dĂ©solĂ© de ne pas avoir Ă©tĂ© en mesure de bien transmettre cela Ă  travers les exemples que j'ai donnĂ©s et l'essentiel que j'ai fait. Une tranche gĂ©nĂ©rique devrait pouvoir ĂȘtre utilisĂ©e aussi facilement qu'un []int l'est aujourd'hui, je rĂ©pĂšte que toute proposition qui ne traite pas cela (slice/array swaps, assignation, len, cap, etc. ) est insuffisant Ă  mon avis.

Cela dit, nous savons maintenant clairement quels sont les objectifs de chacun. Quand j'ai proposĂ© ce que j'ai fait j'ai bien dit que c'Ă©tait simplement une proposition syntaxique et que les dĂ©tails Ă©taient super flous. Mais nous sommes entrĂ©s dans les dĂ©tails de toute façon et l'un de ces dĂ©tails a fini par ĂȘtre le manque de contraintes, quand je l'ai Ă©crit, je ne les avais tout simplement pas Ă  l'esprit car ils ne sont pas importants pour ce que j'aimerais faire , cela ne veut pas dire que nous ne pouvons pas les ajouter ou qu'ils ne sont pas souhaitables. Le principal problĂšme de continuer avec la syntaxe proposĂ©e et d'essayer d'introduire des contraintes serait que la dĂ©finition d'un argument gĂ©nĂ©rique se rĂ©pĂšte actuellement (intentionnellement), de sorte qu'il n'y a pas de rĂ©fĂ©rence au code ailleurs pour dĂ©terminer les contraintes, etc. Si nous devions introduire des contraintes, je ne vois pas comment on pourrait garder ça.

Le meilleur contre-exemple est cette fonction de tri dont nous parlions plus tĂŽt.

type Sort(slice []a:Lesser, less func(a:Lesser, a:Lesser)) { ... }

Comme vous pouvez le voir, il n'y a pas de bon moyen d'y parvenir, et les approches de jeton de spam pour les génériques recommencent à sonner mieux. Afin de définir des contraintes sur ceux-ci, nous devons changer deux choses par rapport à la proposition d'origine :

  • Il doit y avoir un moyen de pointer vers un argument de type et de lui donner des contraintes.
  • Les contraintes doivent durer plus longtemps qu'une seule dĂ©finition, peut-ĂȘtre que cette portĂ©e est un type, peut-ĂȘtre que cette portĂ©e est un fichier (le fichier semble en fait assez raisonnable).

Avis de non-responsabilité : ce qui suit n'est pas un véritable amendement à la proposition, car je lance simplement des symboles aléatoires, j'utilise simplement ces syntaxes comme exemples pour illustrer ce que nous pourrions faire pour modifier la proposition telle qu'elle est à l'origine.

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

Encore une fois, notez qu'aucun des éléments ci-dessus que je ne souhaite vraiment ajouter à la proposition. Je montre simplement quel type de constructions nous pourrions utiliser pour résoudre le problÚme, et leur apparence est quelque peu hors de propos pour le moment.

La question Ă  laquelle nous devons alors rĂ©pondre est la suivante : tirons-nous toujours profit des arguments gĂ©nĂ©riques implicites ? Le point principal de la proposition Ă©tait de garder la sensation propre de Go-like du langage, de garder les choses simples, de garder les choses suffisamment silencieuses en Ă©liminant les jetons excessifs. Dans les nombreux cas oĂč aucune contrainte n'est nĂ©cessaire, par exemple une fonction de carte ou la dĂ©finition d'un type de rĂ©sultat, est-ce que ça a l'air bien, est-ce que ça ressemble Ă  Go, est-ce utile ? En supposant que les contraintes sont Ă©galement disponibles sous une forme ou une autre.

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 Je vais essayer d'expliquer. La raison pour laquelle vous avez besoin de contraintes de type est que c'est la seule façon d'appeler des fonctions ou des mĂ©thodes sur un type. considĂ©rez le type sans contrainte a quel type cela peut-il ĂȘtre, eh bien cela pourrait ĂȘtre une chaĂźne ou un Int ou quoi que ce soit. Nous ne pouvons donc pas appeler de fonctions ou de mĂ©thodes dessus car nous ne connaissons pas le type. Nous pourrions utiliser un changement de type et une rĂ©flexion d'exĂ©cution pour obtenir le type, puis appeler des fonctions ou des mĂ©thodes dessus, mais c'est quelque chose que nous voulons Ă©viter avec les gĂ©nĂ©riques. Lorsque vous contraignez un type, par exemple a est un animal, nous pouvons alors appeler n'importe quelle mĂ©thode dĂ©finie pour un animal sur a .

Dans votre exemple, oui, vous pouvez transmettre une fonction de mappage, mais cela se traduira par des fonctions prenant beaucoup d'arguments, et est fondamentalement comme un langage sans interfaces, juste des fonctions de premiÚre classe. Passer chaque fonction que vous allez utiliser sur le type a va obtenir une trÚs longue liste de fonctions dans n'importe quel programme réel, surtout si vous écrivez principalement du code générique pour l'injection de dépendances, ce que vous voulez faire pour minimiser le couplage.

Par exemple, que se passe-t-il si la fonction qui appelle map est également générique ? Que se passe-t-il si la fonction qui appelle est générique, etc. Comment définissons-nous le mappeur si nous ne connaissons pas encore le type de a ?

func m(slice []a) []b {
   mapper := func(x a) b {...}
   return map(slice, mapper)
}

Quelles fonctions pouvons-nous appeler sur x lorsque nous essayons de définir mapper ?

@keean Je comprends le but et la fonction des contraintes. Je ne les apprĂ©cie tout simplement pas autant que des choses simples comme les structures de conteneurs gĂ©nĂ©riques (et non les conteneurs gĂ©nĂ©riques pour ainsi dire) et les tranches gĂ©nĂ©riques et ne les ai donc mĂȘme pas incluses dans la proposition d'origine.

Je crois toujours que les interfaces sont la bonne rĂ©ponse Ă  des problĂšmes comme celui dont vous parlez oĂč vous faites l'injection de dĂ©pendances, cela ne semble tout simplement pas ĂȘtre le bon endroit pour les gĂ©nĂ©riques, mais qui suis-je pour dire. Le chevauchement entre leurs responsabilitĂ©s est assez important Ă  mes yeux, d'oĂč la raison pour laquelle @Merovius et moi avons dĂ» discuter pour savoir si nous pouvions ou non vivre sans eux, et il m'a Ă  peu prĂšs convaincu qu'ils seraient utiles dans certains cas d'utilisation, donc je explorĂ© un peu ce que nous pourrions faire pour ajouter la fonctionnalitĂ© Ă  la proposition que j'avais initialement faite.

Quant Ă  votre exemple, vous ne pouvez appeler aucune fonction sur x. Mais vous pouvez toujours opĂ©rer sur la tranche comme n'importe quelle autre tranche qui est extrĂȘmement utile en elle-mĂȘme. Vous ne savez pas non plus quelle est la fonction Ă  l'intĂ©rieur de la fonction ... peut-ĂȘtre vouliez-vous attribuer Ă  un var?

@aarondl
Merci, j'ai corrigé la syntaxe, mais je pense que le sens était toujours clair.

Les exemples que j'ai donnĂ©s ci-dessus utilisaient Ă  la fois le polymorphisme paramĂ©trique et les interfaces pour atteindre un certain niveau de programmation gĂ©nĂ©rique, mais l'absence de rĂ©partition multiple plafonnera toujours le niveau de gĂ©nĂ©ralitĂ© rĂ©alisable. En tant que tel, il semble que Go ne fournira pas les fonctionnalitĂ©s que je recherche dans une langue, cela ne signifie pas que je ne peux pas utiliser Go pour certaines tĂąches, et en fait je le suis dĂ©jĂ  et cela fonctionne bien, mĂȘme si j'ai eu au code copier-coller qui n'a vraiment besoin que d'une seule dĂ©finition. J'espĂšre juste qu'Ă  l'avenir, si ce code doit ĂȘtre modifiĂ©, le dĂ©veloppeur pourra en trouver toutes les instances collĂ©es.

Je suis alors dans deux esprits quant Ă  savoir si la gĂ©nĂ©ralitĂ© limitĂ©e possible sans de si grands changements dans la langue est une bonne idĂ©e, compte tenu de la complexitĂ© que cela ajoutera. Peut-ĂȘtre que Go vaut mieux rester simple, et les gens peuvent ajouter des macros comme le prĂ©traitement, ou d'autres langages qui compilent en Go, pour fournir ces fonctionnalitĂ©s ? D'autre part, l'ajout de polymorphisme paramĂ©trique serait une bonne premiĂšre Ă©tape. Permettre Ă  ces paramĂštres de type d'ĂȘtre contraints serait une bonne prochaine Ă©tape. Ensuite, vous pourriez ajouter des paramĂštres de type associĂ©s aux interfaces, et vous auriez quelque chose de raisonnablement gĂ©nĂ©rique, mais c'est probablement tout ce que vous pouvez obtenir sans envoi multiple. En les divisant en fonctionnalitĂ©s plus petites, je suppose que vous augmenteriez les chances de les faire accepter ?

@keean
L'expĂ©dition multiple est-elle nĂ©cessaire ? TrĂšs peu de langues le supportent nativement. MĂȘme C++ ne le supporte pas. C# le supporte un peu via dynamic mais je ne l'ai jamais utilisĂ© dans la pratique et le mot-clĂ© en gĂ©nĂ©ral est trĂšs trĂšs rare dans le code rĂ©el. Les exemples dont je me souviens traitent de quelque chose comme l'analyse JSON, pas l'Ă©criture de gĂ©nĂ©riques.

L'expédition multiple est-elle nécessaire ?

À mon humble avis, je pense que @keean parle de rĂ©partition multiple statique fournie par les classes de types/interfaces.
Ceci est mĂȘme fourni en C++ par surcharge de mĂ©thode (je ne sais pas pour C#)

Ce que vous voulez dire, c'est la distribution multiple dynamique qui est assez lourde dans les langages statiques sans types d'union. Les langages dynamiques contournent ce problÚme en omettant la vérification de type statique (inférence de type partielle pour les langages dynamiques, identique pour le type "dynamique" de C#).

Un type pourrait-il ĂȘtre fourni comme "juste" un paramĂštre ?

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 a Ă©crit :

Un type pourrait-il ĂȘtre fourni comme "juste" un paramĂštre ?

On peut se demander dans quelle mesure cela serait possible ou souhaité en aller

Ce que vous voulez pourrait ĂȘtre rĂ©alisĂ© Ă  la place si les contraintes gĂ©nĂ©riques sont prises en charge :

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)

Cela devrait Ă©galement ĂȘtre possible avec des contraintes:

func convert(value s) t requires Convertible<s,t>{
    return t(value);
}

f:float64:=2.0

i:int64=convert(f)

Pour ce que ça vaut, notre langage Genus prend en charge l'envoi multiple. Les modÚles d'une contrainte peuvent fournir plusieurs implémentations qui sont distribuées.

Je comprends que la notation Convertible<s,t> est nĂ©cessaire pour la sĂ©curitĂ© du temps de compilation, mais pourrait peut-ĂȘtre ĂȘtre dĂ©gradĂ©e en une vĂ©rification d'exĂ©cution

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)

Mais cela ressemble plus Ă  du sucre de syntaxe pour reflect .

@Inuart , le compilateur peut vérifier que le type implémente la classe de types au moment de la compilation, de sorte que la vérification de l'exécution n'est pas nécessaire. L'avantage est une meilleure performance (ce que l'on appelle l'abstraction à coût zéro). S'il s'agit d'une vérification d'exécution, vous pouvez également utiliser reflect .

@creker

L'expédition multiple est-elle nécessaire ?

Je suis trop dans l'esprit Ă  ce sujet. D'une part, l'envoi multiple (avec des classes de types multi-paramĂštres) ne fonctionne pas bien avec les existentiels, ce que 'Go' appelle des 'valeurs d'interface'.

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

Nous ne pouvons pas dĂ©finir la tranche de Equals car nous n'avons aucun moyen d'indiquer que le paramĂštre de droite provient de la mĂȘme collection. Nous ne pouvons mĂȘme pas faire cela dans Haskell :

data Equals = forall a . IEquals a a => Equals a

Ce n'est pas bon car cela ne permet qu'Ă  un type d'ĂȘtre comparĂ© Ă  lui-mĂȘme

data Equals = forall a b . IEquals a b => Equals a

Ce n'est pas bon car nous n'avons aucun moyen de contraindre b Ă  ĂȘtre un autre existentiel dans la mĂȘme collection que a (si a even est dans une collection).

Cependant, il est trÚs facile d'étendre avec un nouveau type :

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

Et ce serait encore plus concis avec des instances par défaut ou une spécialisation.

D'autre part, nous pouvons réécrire ceci dans 'Go' qui fonctionne en ce moment :

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

Cela fonctionne bien avec l'existentiel (valeur d'interface), mais c'est beaucoup plus complexe, plus difficile de voir ce qui se passe et comment cela fonctionne, et il y a la grande restriction que nous avons besoin d'une interface par type et nous devons coder en dur l'acceptable types de cÎté droit comme celui-ci :

type EqualsRight interface {
    EqualsInt
    EqualsFloat
}

Ce qui signifie que nous devrions modifier la source de la bibliothĂšque pour ajouter un nouveau type car l'interface EqualsRight n'est pas extensible.

Ainsi, sans interfaces multi-paramÚtres, nous ne pouvons pas définir d'opérateurs génériques extensibles comme l'égalité. Avec les interfaces multiparamÚtres, les existentiels (valeurs d'interface) deviennent problématiques.

Mon principal problÚme avec un grand nombre des syntaxes proposées (syntaces ?) Blah[E] est que le type sous-jacent ne montre aucune information sur le contenu des génériques.

Par exemple:

type Comparer[C] interface {
    Compare(other C) bool
}
// or
type Comparer c interface {
    Compare(other c) bool
}
...

Cela signifie que nous déclarons un nouveau type qui ajoute plus d'informations sur le type sous-jacent. Le but de la déclaration type n'est-il pas de définir un nom basé sur un autre type ?

Je proposerais une syntaxe plus dans le sens de

type Comparer interface[C] {
    Compare(other C) bool
}

Cela signifie que vraiment Comparer est juste un type basé sur interface[C] { ... } , et interface[C] { ... } est bien sûr son propre type distinct de interface { ... } . Cela vous permet d'utiliser une interface générique sans la nommer, si vous le souhaitez (ce qui est autorisé avec les interfaces normales). Je pense que cette solution est un peu plus intuitive et fonctionne bien avec le systÚme de type de Go, mais corrigez-moi si je me trompe.

Remarque : La déclaration d'un type générique ne serait autorisée que sur les interfaces, les structures et les fonctions avec les syntaxes suivantes :
interface[G] { ... }
struct[G] { ... }
func[G] (vars...) { ... }

Ensuite, "implémenter" les génériques aurait les syntaxes suivantes :
interface[G] { ... }[string]
struct[G] { ... }[string]
func[G] (vars...) { ... }[int](args...)

Et avec quelques exemples pour le rendre un peu plus clair :

Interfaces

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

Structures

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

Les fonctions

func[A] AddManyTo(a Adder[A], many ...A) {
    for _, each := range a {
        a.Add(each)
    }
}

Ceci est en réponse au projet de contrats Go2 et j'utiliserai sa syntaxe, mais je le poste ici car il s'applique à toute proposition de polymorphisme paramétrique.

L'incorporation de paramĂštres de type ne devrait pas ĂȘtre autorisĂ©e.

Envisager

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.

pour un type arbitraire R et un contrat arbitraire C qui ne contient pas Foo() .

T aura tous les sélecteurs requis par C mais une instanciation particuliÚre de T peut également avoir d'autres sélecteurs arbitraires, y compris Foo .

Disons que Bar est une structure, admissible sous C , qui a un champ nommé Foo .

X(Bar) pourrait ĂȘtre une instanciation illĂ©gale. Sans un moyen de spĂ©cifier le contrat selon lequel un type n'a pas de sĂ©lecteur, cela devrait ĂȘtre une propriĂ©tĂ© dĂ©duite.

Les mĂ©thodes de X(Bar) pourraient continuer Ă  rĂ©soudre les rĂ©fĂ©rences Ă  Foo comme X(Bar).R.Foo . Cela rend possible l'Ă©criture du type gĂ©nĂ©rique, mais pourrait prĂȘter Ă  confusion pour un lecteur non familiarisĂ© avec la tatillonne des rĂšgles de rĂ©solution. En dehors des mĂ©thodes de X , le sĂ©lecteur resterait ambigu donc, alors que interface { Foo() } ne dĂ©pend pas des paramĂštres de X , certaines instanciations de X pas le satisfaire.

Interdire l'intégration d'un paramÚtre de type est plus simple.

(Si cela doit ĂȘtre autorisĂ©, cependant, le nom du champ serait T pour la mĂȘme raison que le nom du champ d'un S intĂ©grĂ© dĂ©fini comme type S = io.Reader est S et non Reader mais aussi parce que le type instanciant T n'a pas nĂ©cessairement besoin d'avoir un nom du tout.)

@jimmyfrasche Je pense que les champs intĂ©grĂ©s avec des types gĂ©nĂ©riques sont suffisamment utiles pour qu'il soit bon de les autoriser, mĂȘme s'il peut y avoir un peu de maladresse par endroits. Ma suggestion serait de supposer dans tout code gĂ©nĂ©rique que le type intĂ©grĂ© a dĂ©fini tous les champs et mĂ©thodes possibles Ă  tous les niveaux possibles, de sorte que dans le code gĂ©nĂ©rique, toutes les mĂ©thodes et champs intĂ©grĂ©s de types non gĂ©nĂ©riques soient effacĂ©s.

Donc donné :

type R struct(type T) {
    io.Reader
    T
}

les méthodes sur R ne pourraient pas invoquer Read sur R sans passer par Reader. Par example:

func (r R) Do() {
     r.Read(buf)     // Illegal
     r.Reader.Read(buf)  // ok
}

Le seul inconvénient que je peux voir est que le type dynamique peut contenir plus de membres que le type statique. Par example:

func (r R) Do() {
    var x interface{} = r
    x.(io.Reader)    // Succeeds
}

@rogpeppe

Le seul inconvénient que je peux voir est que le type dynamique peut contenir plus de membres que le type statique.

C'est le cas avec les paramĂštres de type directement, donc je pense que cela devrait Ă©galement convenir avec les types paramĂ©triques. Je pense que la solution au problĂšme prĂ©sentĂ© par @jimmyfrasche pourrait ĂȘtre de mettre l'ensemble de mĂ©thodes souhaitĂ© du type paramĂ©trĂ© dans le contrat.

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.

Cela permettrait Ă  Foo d'ĂȘtre appelĂ© directement sur X . Bien sĂ»r, cela irait Ă  l'encontre de la rĂšgle "pas de noms locaux dans les contrats"...

@stevenblenkinsop Hmm, il est possible, si gĂȘnant, de le faire sans se rĂ©fĂ©rer Ă  X

contract C(t T) {
  struct{ R; T }{}.Foo
}

C est toujours lié à l'implémentation de X bien qu'un peu plus vaguement.

Si vous ne le faites pas et que vous Ă©crivez

func (x X(T)) Fooer() interface { Foo() } {
  return x
}

ça compile ? Ce ne serait pas sous la rĂšgle de @rogpeppe qui semble devoir ĂȘtre adoptĂ©e Ă©galement lorsque vous ne faites pas la garantie dans le contrat. Mais alors s'applique-t-il uniquement lorsque vous intĂ©grez un argument de type sans contrat suffisant ou pour toutes les intĂ©grations ?

Il serait plus facile de simplement l'interdire.

J'ai commencé à travailler sur cette proposition avant l'annonce du projet Go2.

J'Ă©tais prĂȘt Ă  abandonner le mien avec joie quand j'ai vu l'annonce, mais je suis toujours instable face Ă  la complexitĂ© du brouillon, alors j'ai terminĂ© le mien. C'est moins puissant mais plus simple. Si rien d'autre, il peut avoir quelques morceaux qui valent la peine d'ĂȘtre volĂ©s.

Il dĂ©veloppe la syntaxe des propositions prĂ©cĂ©dentes de @ianlancetaylor , car c'est ce qui Ă©tait disponible lorsque j'ai commencĂ©. Ce n'est pas fondamental. Il pourrait ĂȘtre remplacĂ© par une syntaxe (type T etc. ou quelque chose d'Ă©quivalent. J'avais juste besoin d'une syntaxe comme notation pour la sĂ©mantique.

Il se trouve ici : https://gist.github.com/jimmyfrasche/656f3f47f2496e6b49e041cd8ac716e4

La rĂšgle devrait ĂȘtre que toute mĂ©thode promue Ă  partir d'une profondeur supĂ©rieure Ă  celle d'un paramĂštre de type intĂ©grĂ© ne peut ĂȘtre appelĂ©e que si (1) l'identitĂ© de l'argument de type est connue ou (2) la mĂ©thode est affirmĂ©e ĂȘtre appelable sur le type par le contrat contraignant le paramĂštre type. Le compilateur pourrait Ă©galement dĂ©terminer les limites supĂ©rieure et infĂ©rieure de la profondeur qu'une mĂ©thode promue doit avoir dans le type externe O et les utiliser pour dĂ©terminer si la mĂ©thode est appelable sur un type qui incorpore O , c'est-Ă -dire s'il existe un potentiel de conflit avec d'autres mĂ©thodes promues ou non. Quelque chose de similaire s'appliquerait Ă©galement Ă  tout paramĂštre de type qui est affirmĂ© avoir des mĂ©thodes appelables, oĂč les plages de profondeur des mĂ©thodes dans le paramĂštre de type seraient [0, inf).

L'intégration de paramÚtres de type semble tout simplement trop utile pour l'interdire complÚtement. D'une part, il permet une composition transparente, ce que le modÚle d'interfaces d'intégration ne permet pas.

J'ai Ă©galement trouvĂ© une utilisation potentielle dans la dĂ©finition des contrats. Si vous voulez pouvoir accepter une valeur de type T (qui pourrait ĂȘtre un type de pointeur) qui pourrait avoir des mĂ©thodes dĂ©finies sur *T , et vous voulez pouvoir mettre cette valeur dans une interface, vous ne pouvez pas nĂ©cessairement mettre T dans l'interface, puisque les mĂ©thodes peuvent ĂȘtre sur *T , et vous ne pouvez pas nĂ©cessairement mettre *T dans l'interface parce que T pourrait lui-mĂȘme ĂȘtre un type pointeur (et donc *T pourrait avoir un jeu de mĂ©thodes vide). Cependant, si vous aviez un emballage comme

type Wrapper(type T) { T }

vous pouvez mettre un *Wrapper(T) dans l'interface dans tous les cas si votre contrat indique qu'il satisfait l'interface.

Tu ne peux pas juste faire

type Interface interface {
  SomeMethod(int) error
}

contract MightBeAPointer(t T) {
  Interface(t)
}

func Example(type T MightBeAPointer)(v T) {
  var i Interface = v
  // ...
}

J'essaie de gĂ©rer le cas oĂč quelqu'un appelle

type S struct{}
func (s *S) SomeMethod(int) error { ... }
...
var s S
Example(S)(s)

Cela ne fonctionnera pas car S ne peut pas ĂȘtre converti en Interface , seul *S peut.

Évidemment, la rĂ©ponse pourrait ĂȘtre « ne fais pas ça ». Cependant, la proposition de contrats dĂ©crit des contrats tels que :

contract Contract(t T) {
    var _ error = t.SomeMethod(int(0))
}

S satisferait ce contrat en raison de l'adressage automatique, tout comme *S . Ce que j'essaie de résoudre, c'est l'écart de capacité entre les appels de méthode et les conversions d'interface dans les contrats.

Quoi qu'il en soit, c'est un peu une tangente, montrant une utilisation potentielle pour l'intégration de paramÚtres de type.

En ce qui concerne l'intĂ©gration, je pense que "peut ĂȘtre intĂ©grĂ© dans une structure" est une autre restriction que les contrats devraient capturer s'ils Ă©taient autorisĂ©s.

Envisager:

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)

@bcmills incorporer des types avec des sĂ©lecteurs ambigus est autorisĂ©, donc je ne sais pas comment ce contrat est censĂ© ĂȘtre interprĂ©tĂ©.

Dans tous les cas, si vous n'intégrez que des types connus, c'est bien. Si vous n'intégrez que des paramÚtres de type, c'est bien. Le seul cas qui devient étrange est lorsque vous intégrez un ou plusieurs types connus ET un ou plusieurs paramÚtres de type et uniquement lorsque les sélecteurs du ou des types connus et des arguments de type ne sont pas disjoints

@bcmills incorporer des types avec des sĂ©lecteurs ambigus est autorisĂ©, donc je ne sais pas comment ce contrat est censĂ© ĂȘtre interprĂ©tĂ©.

Hum, bon point. Il me manque une contrainte supplĂ©mentaire pour dĂ©clencher l'erreur.Âč

contract Embeddable(type X, Y) {
    type S struct {
        X
        Y
    }
    var _ io.Reader = S{}
}

Âč https://play.golang.org/p/3wSg5aRjcQc

Cela nĂ©cessite l'un des X ou Y mais pas les deux pour ĂȘtre un io.Reader . Il est intĂ©ressant de noter que le systĂšme de contrat est suffisamment expressif pour permettre cela. Je suis content de ne pas avoir Ă  comprendre les rĂšgles d'infĂ©rence de type pour une telle bĂȘte.

Mais lĂ  n'est pas vraiment le problĂšme.

C'est quand tu fais

type S (type T C) struct {
  io.Reader
  T
}
func (s *S(T)) X() io.Reader {
  return s
}

Cela devrait échouer à compiler car T pourrait avoir un sélecteur Read moins que C n'ait

struct{ io.Reader; T }.Read

Mais alors, quelles sont les rÚgles lorsque C ne garantit pas que les ensembles de sélecteurs sont disjoints et que S ne fait pas référence aux sélecteurs ? Est-il possible que chaque instanciation S satisfasse une interface à l'exception des types qui créent un sélecteur ambigu ?

Est-il possible que chaque instanciation S satisfasse une interface à l'exception des types qui créent un sélecteur ambigu ?

Oui, cela semble ĂȘtre le cas. Je me demande si cela implique quelque chose de plus profond... đŸ€”

Je n'ai rien pu construire d'irrémédiablement méchant, mais l'asymétrie est assez désagréable et me met mal à l'aise :

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

Je m'inquiĂšte des messages d'erreur lorsque G0(B) utilise un G1(B) utilise un . . . utilise un Gn(B) et Gn est celui qui provoque l'erreur. . . .

FTR, vous n'avez pas besoin de vous soucier des sélecteurs ambigus pour déclencher des erreurs de type avec l'intégration.

// Error: Duplicate field name Reader
type Boom = Embedded(*bytes.Reader, *strings.Reader)

Vous supposez que le nom du champ intégré est basé sur le type d'argument, alors qu'il est plus probable qu'il s'agisse du nom du paramÚtre de type intégré. C'est comme lorsque vous intégrez un alias de type et que le nom du champ est l'alias plutÎt que le nom du type qu'il alias.

Ceci est en fait spécifié dans le projet de conception dans la section sur les types paramétrés :

Lorsqu'un type paramétré est une structure et que le paramÚtre de type est incorporé en tant que champ dans la structure, le nom du champ est le nom du paramÚtre de type, et non le nom de l'argument de type.

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
}

(Remarque : cela fonctionne mal si vous Ă©crivez Lockable(X) dans la dĂ©claration de la mĂ©thode : la mĂ©thode doit-elle renvoyer lT ou lX ? Peut-ĂȘtre devrions-nous simplement interdire l'intĂ©gration d'un paramĂštre de type dans une structure.)

Je suis juste assis ici sur la touche et j'observe. Mais aussi s'inquiéter un peu.

Une chose que je ne suis pas gĂȘnĂ© de dire, c'est que 90% de cette discussion est au-dessus de ma tĂȘte.

Il semble que 20 ans Ă  gagner sa vie en Ă©crivant des logiciels sans savoir ce qu'est le polymorphisme gĂ©nĂ©rique ou paramĂ©trique, ne m'ont pas empĂȘchĂ© de faire le travail.

Malheureusement, je n'ai pris le temps qu'il y a environ un an d'apprendre le go. J'ai fait la fausse hypothĂšse qu'il s'agissait d'une courbe d'apprentissage abrupte et qu'il faudrait trop de temps pour devenir productif.

Je n'aurais pas pu me tromper davantage.

J'ai pu apprendre suffisamment de Go pour créer un microservice qui a complÚtement détruit le service node.js avec lequel j'avais des problÚmes de performances en moins d'un week-end.

Ironiquement, je ne faisais que jouer. Je n'étais pas particuliÚrement sérieux au sujet de conquérir le monde avec Go.

Et pourtant, en quelques heures, je me suis retrouvé assis de ma posture affaissée et vaincue, comme si j'étais sur le bord de mon siÚge en train de regarder un thriller d'action. L'API que je construisais s'est réunie si rapidement. J'ai réalisé que c'était en effet un langage dans lequel investir mon temps précieux, car il était évidemment si pragmatique dans sa conception.

Et c'est ce que j'aime chez Go. C'est trĂšs rapide..... Pour apprendre. Nous connaissons tous ici ses capacitĂ©s de performance. Mais la vitesse Ă  laquelle il peut ĂȘtre appris est inĂ©galĂ©e par les 8 autres langues que j'ai apprises au fil des ans.

Depuis lors, j'ai chantĂ© les louanges de Go et j'ai convaincu 4 autres dĂ©veloppeurs d'en tomber amoureux. Je m'assois juste avec eux pendant quelques heures et je construis quelque chose. Les rĂ©sultats parlent d'eux-mĂȘmes.

Simplicité et rapidité d'apprentissage. Ce sont les véritables caractéristiques tueuses de la langue.

Les langages de programmation qui nĂ©cessitent des mois d'apprentissage intensif ne retiennent souvent pas les dĂ©veloppeurs mĂȘmes qu'ils cherchent Ă  attirer. Nous avons du travail Ă  faire et des employeurs qui veulent voir des progrĂšs au quotidien (merci agile, apprĂ©ciez-le)

Donc, il y a deux choses que j'espÚre que l'équipe Go pourra prendre en considération :

1) Quel problÚme quotidien cherchons-nous à résoudre ?

Je n'arrive pas à trouver un exemple concret, avec un bouchon de spectacle qui serait résolu par des génériques, ou quel que soit leur nom.

Exemples de style livre de recettes de tĂąches quotidiennes qui posent problĂšme, avec un exemple de la façon dont elles pourraient ĂȘtre amĂ©liorĂ©es avec ces propositions de changement de langue.

2) Restez simple, comme toutes les autres fonctionnalités intéressantes de Go

Il y a des commentaires incroyablement intelligents ici. Mais je suis certain que la majorité des développeurs qui utilisent Go au quotidien pour la programmation générale, comme moi, sont parfaitement satisfaits et productifs avec les choses telles qu'elles sont.

Peut-ĂȘtre un argument du compilateur pour activer ces fonctionnalitĂ©s avancĂ©es ? '--hardcore'

Je serais vraiment triste si nous avions un impact négatif sur les performances du compilateur. dis juste'n

Et c'est ce que j'aime chez Go. C'est trĂšs rapide..... Pour apprendre. Nous connaissons tous ici ses capacitĂ©s de performance. Mais la vitesse Ă  laquelle il peut ĂȘtre appris est inĂ©galĂ©e par les 8 autres langues que j'ai apprises au fil des ans.

Je suis complĂštement d'accord. La combinaison de la puissance et de la simplicitĂ© dans un langage entiĂšrement compilĂ© est quelque chose de complĂštement unique. Je ne veux certainement pas que Go perde cela, et mĂȘme si je veux des gĂ©nĂ©riques, je ne pense pas qu'ils en valent la peine Ă  ce prix. Je ne pense pas qu'il soit nĂ©cessaire de perdre cela, cependant.

Je n'arrive pas à trouver un exemple concret, avec un bouchon de spectacle qui serait résolu par des génériques, ou quel que soit leur nom.

J'ai deux principaux cas d'utilisation principaux pour les génériques : l'élimination passe-partout de type sécurisé des structures de données complexes, telles que les arbres binaires, les ensembles et sync.Map , et la possibilité d'écrire des fonctions de type sécurisé _compile-time_ qui fonctionnent sur la base uniquement sur la fonctionnalité de leurs arguments, plutÎt que sur leur disposition en mémoire. Il y a des choses plus fantaisistes que je ne verrais pas d'inconvénient à pouvoir faire, mais cela ne me dérangerait pas de pouvoir les faire s'il est impossible d'ajouter un support pour elles sans casser complÚtement la simplicité du langage.

Pour ĂȘtre honnĂȘte, il existe dĂ©jĂ  des fonctionnalitĂ©s dans le langage qui sont assez abusables. Je pense que la principale raison pour laquelle ils ne sont _pas_ abusĂ©s est la culture Go de l'Ă©criture de code "idiomatique", combinĂ©e Ă  la bibliothĂšque standard fournissant des exemples propres et faciles Ă  trouver d'un tel code, pour la plupart. Obtenir une bonne utilisation des gĂ©nĂ©riques dans la bibliothĂšque standard devrait certainement ĂȘtre une prioritĂ© lors de leur mise en Ɠuvre.

@camstuart

Je n'arrive pas à trouver un exemple concret, avec un bouchon de spectacle qui serait résolu par des génériques, ou quel que soit leur nom.

Les gĂ©nĂ©riques vous Ă©vitent d'avoir Ă  Ă©crire le code vous-mĂȘme. Ainsi, vous n'aurez plus jamais besoin d'implĂ©menter vous-mĂȘme une autre liste chaĂźnĂ©e, un arbre binaire, un deque ou une file d'attente prioritaire. Vous n'aurez jamais besoin d'implĂ©menter un algorithme de tri, un algorithme de partitionnement ou un algorithme de rotation, etc. Les structures de donnĂ©es deviennent des collections standard composant (une carte de listes par exemple), et le traitement devient des algorithmes standard composant et tourner). Si vous pouvez rĂ©utiliser ces composants, le taux d'erreur diminue, car chaque fois que vous rĂ©implĂ©mentez une file d'attente prioritaire ou un algorithme de partitionnement, vous risquez de vous tromper et d'introduire un bogue.

Les gĂ©nĂ©riques signifient que vous Ă©crivez moins de code et que vous en rĂ©utilisez plus. Ils signifient que les fonctions de bibliothĂšque standard et bien entretenues et les types de donnĂ©es abstraits peuvent ĂȘtre utilisĂ©s dans plus de situations, vous n'avez donc pas Ă  Ă©crire les vĂŽtres.

Mieux encore, tout cela peut techniquement ĂȘtre fait dans Go dĂšs maintenant, mais seulement avec une perte presque complĂšte de la sĂ©curitĂ© des types au moment de la compilation _et_ avec une surcharge d'exĂ©cution potentiellement majeure. Les gĂ©nĂ©riques vous permettent de le faire sans aucun de ces inconvĂ©nients.

Implémentation de la fonction générique :

/*

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

Appelant de fonction :

Sum(1, 1) // 2
// same as:
Sum(T{int}(1), T{int}(1)) // 2

Implémentation de la structure générique :

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

Votre interlocuteur:

list := NewList(ItemT{int})
list.Push(42)

En tant que personne apprenant Swift et ne l'aimant pas, mais avec beaucoup d'expérience dans d'autres langages comme Go, C, Java, etc. Je crois vraiment que les génériques (ou les modÚles, ou peu importe comment vous voulez l'appeler) ne sont pas une bonne chose à ajouter au langage Go.

Peut-ĂȘtre que je suis juste plus expĂ©rimentĂ© avec la version actuelle de Go, mais pour moi, cela ressemble Ă  une rĂ©gression vers C++ dans la mesure oĂč il est plus difficile de comprendre le code que d'autres personnes ont Ă©crit. L'espace rĂ©servĂ© T classique pour les types rend si difficile la comprĂ©hension de ce qu'une fonction essaie de faire.

Je sais que c'est une demande de fonctionnalité populaire, donc je peux y faire face si elle atterrit, mais je voulais ajouter mes 2 cents (opinion).

@jlubawy
Connaissez-vous un autre moyen de ne jamais implémenter une liste chaßnée ou un algorithme de tri rapide ? Comme le souligne Alexander Stepanov, la plupart des programmeurs ne peuvent pas définir correctement les fonctions "min" et "max", alors quel espoir avons-nous d'implémenter correctement des algorithmes plus complexes sans beaucoup de temps de débogage. Je préférerais de loin extraire les versions standard de ces algorithmes d'une bibliothÚque et les appliquer uniquement aux types que j'ai. Quelle alternative existe-t-il ?

@jlubawy

ou modĂšle, ou comme vous voulez l'appeler

Tout dĂ©pend de la rĂ©alisation. si nous parlons de modĂšles C++, alors oui, ils sont difficiles Ă  comprendre en gĂ©nĂ©ral. MĂȘme les Ă©crire est difficile. D'un autre cĂŽtĂ©, si nous prenons des gĂ©nĂ©riques C #, c'est complĂštement autre chose. Le concept lui-mĂȘme n'est pas un problĂšme ici.

Si vous ne le saviez pas, la Go Team a annoncé un brouillon de Go 2.0 :
https://golang.org/s/go2designs

Il existe un brouillon de la conception des gĂ©nĂ©riques dans Go 2.0 (contrat). Vous voudrez peut-ĂȘtre jeter un coup d'Ɠil et donner votre avis sur leur Wiki .

Voici la section pertinente :

Génériques

AprĂšs avoir lu le brouillon, je demande :

Pourquoi

T : Ajoutable

signifie « un type T mettant en Ɠuvre le contrat Addable » ? Pourquoi ajouter un nouveau
concept alors qu'on a déjà des INTERFACES pour ça ? L'affectation des interfaces est
vérifié au moment de la construction, nous avons donc déjà les moyens de ne pas avoir besoin de
concept supplémentaire ici. Nous pouvons utiliser ce terme pour dire quelque chose comme : Tout
type T implémentant l'interface Addable. De plus, T:_ ou T:Any
(ĂȘtre Any un mot-clĂ© spĂ©cial ou un alias intĂ©grĂ© d'interface{}) ferait l'affaire
l'astuce.

Juste je ne sais pas pourquoi réimplémenter la plupart des choses comme ça. Ne fait pas
sens et SERA redondant (car redondant est le nouveau traitement des erreurs par rapport
la gestion des paniques).

2018-09-14 6:15 GMT-05:00 Koala Yeung [email protected] :

Si vous ne le saviez pas, la Go Team a annoncé un brouillon de Go 2.0 :
https://golang.org/s/go2designs

Il existe un brouillon de la conception des gĂ©nĂ©riques dans Go 2.0 (contrat). Vous voudrez peut-ĂȘtre
pour jeter un oeil et donner votre avis
https://github.com/golang/go/wiki/Go2GenericsFeedback sur leur Wiki
https://github.com/golang/go/wiki/Go2GenericsFeedback .

Voici la section pertinente :

Génériques

—
Vous recevez ceci parce que vous avez commenté.
RĂ©pondez directement Ă  cet e-mail, consultez-le sur GitHub
https://github.com/golang/go/issues/15292#issuecomment-421326634 , ou muet
le fil
https://github.com/notifications/unsubscribe-auth/AlhWhS8xmN5Y85_aUKT5VnutoOKUAaLLks5ua4_agaJpZM4IG-xv
.

--
Ceci est un test pour les signatures de courrier Ă  utiliser dans TripleMint

Edit : "[...] ferait l'affaire SI VOUS N'AVEZ BESOIN D'AUCUNE EXIGENCE PARTICULIÈRE SUR
L'ARGUMENT DE TYPE".

2018-09-17 11:10 GMT-05:00 Luis Masuelli [email protected] :

AprĂšs avoir lu le brouillon, je demande :

Pourquoi

T : Ajoutable

signifie « un type T mettant en Ɠuvre le contrat Addable » ? Pourquoi ajouter un nouveau
concept alors qu'on a déjà des INTERFACES pour ça ? L'affectation des interfaces est
vérifié au moment de la construction, nous avons donc déjà les moyens de ne pas avoir besoin de
concept supplémentaire ici. Nous pouvons utiliser ce terme pour dire quelque chose comme : Tout
type T implémentant l'interface Addable. De plus, T:_ ou T:Any
(ĂȘtre Any un mot-clĂ© spĂ©cial ou un alias intĂ©grĂ© d'interface{}) ferait l'affaire
l'astuce.

Juste je ne sais pas pourquoi réimplémenter la plupart des choses comme ça. Ne fait pas
sens et SERA redondant (car redondant est le nouveau traitement des erreurs par rapport
la gestion des paniques).

2018-09-14 6:15 GMT-05:00 Koala Yeung [email protected] :

Si vous ne le saviez pas, la Go Team a annoncé un brouillon de Go 2.0 :
https://golang.org/s/go2designs

Il existe un brouillon de la conception des génériques dans Go 2.0 (contrat). Tu peux
je veux jeter un coup d'oeil et donner mon avis
https://github.com/golang/go/wiki/Go2GenericsFeedback sur leur Wiki
https://github.com/golang/go/wiki/Go2GenericsFeedback .

Voici la section pertinente :

Génériques

—
Vous recevez ceci parce que vous avez commenté.
RĂ©pondez directement Ă  cet e-mail, consultez-le sur GitHub
https://github.com/golang/go/issues/15292#issuecomment-421326634 , ou muet
le fil
https://github.com/notifications/unsubscribe-auth/AlhWhS8xmN5Y85_aUKT5VnutoOKUAaLLks5ua4_agaJpZM4IG-xv
.

--
Ceci est un test pour les signatures de courrier Ă  utiliser dans TripleMint

--
Ceci est un test pour les signatures de courrier Ă  utiliser dans TripleMint

@luismasuelli-jobsity Si je lis correctement l'historique des implémentations génériques dans Go, il semble que la raison d'introduire les contrats soit parce qu'ils ne voulaient pas de surcharge d'opérateur dans les interfaces.

Une proposition antérieure qui a finalement été rejetée utilisait des interfaces pour contraindre le polymorphisme paramétrique, mais semble avoir été rejetée car vous ne pouviez pas utiliser d'opérateurs communs comme '+' dans de telles fonctions car il n'est pas définissable dans une interface. Les contrats vous permettent d'écrire t == t ou t + t afin que vous puissiez indiquer que le type doit prendre en charge l'égalité ou l'addition, etc.

Edit: De plus, Go ne prend pas en charge plusieurs interfaces de paramÚtres de type, donc d'une certaine maniÚre, Go a séparé la classe de types en deux choses distinctes, les contrats qui relient les paramÚtres de type des fonctions les uns aux autres et les interfaces qui fournissent des méthodes. Ce qu'il perd, c'est la possibilité de sélectionner une implémentation de classe de types basée sur plusieurs types. C'est sans doute plus simple si vous n'avez besoin d'utiliser que des interfaces ou des contrats, mais plus complexe si vous devez utiliser les deux ensemble.

Pourquoi T:Addable signifie « un type T mettant en Ɠuvre le contrat Addable » ?

Ce n'est en fait pas ce que cela signifie; il ressemble juste à cela pour un argument de type. Ailleurs dans le brouillon, il est fait le commentaire que vous ne pouvez avoir qu'un seul contrat par fonction, et c'est là que la principale différence entre en jeu. Les contrats sont en fait des déclarations sur les types de fonction, pas seulement les types indépendamment. Par exemple, si vous avez

func Example(type K, V someContract)(k K, v V) V

vous pouvez faire quelque chose comme

contract someContract(k K, v V) {
  k.someMethod(v)
}

Cela simplifie considĂ©rablement la coordination de plusieurs types sans avoir Ă  spĂ©cifier de maniĂšre redondante les types dans la signature de la fonction. N'oubliez pas qu'ils essaient d'Ă©viter le "modĂšle gĂ©nĂ©rique qui se rĂ©pĂšte curieusement". Par exemple, la mĂȘme fonction avec des interfaces paramĂ©trĂ©es utilisĂ©es pour contraindre les types serait quelque chose comme

type someMethoder(V) interface {
  someMethod(V)
}

func Example(type K: someMethoder(V), V)(k K, v V) V

C'est un peu gĂȘnant. La syntaxe du contrat vous permet de le faire si vous en avez besoin, car les "arguments" du contrat sont remplis automatiquement par le compilateur si le contrat en a le mĂȘme nombre que les paramĂštres de type de la fonction. Vous pouvez cependant les spĂ©cifier manuellement si vous le souhaitez, ce qui signifie que vous _pourriez_ faire func Example(type K, V someContract(K, V))(k K, v V) V si vous le vouliez vraiment, bien que ce ne soit pas particuliĂšrement utile dans cette situation.

Une façon de préciser que les contrats concernent des fonctions entiÚres, et non des arguments individuels, serait de simplement les associer en fonction de leur nom. Par example,

contract Example(k K, v V) {
  k.someMethod(v)
}

func Example(type K, V)(k K, v V) V

serait le mĂȘme que ci-dessus. L'inconvĂ©nient, cependant, est que les contrats ne seraient pas rĂ©utilisables et vous perdriez cette possibilitĂ© de spĂ©cifier manuellement les arguments du contrat.

Edit : Pour montrer davantage pourquoi ils veulent résoudre le motif curieusement répétitif, considérons le problÚme du chemin le plus court auquel ils se référaient sans cesse. Avec des interfaces paramétrées, la définition finit par ressembler à

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

Personnellement, j'aime plutĂŽt la façon dont les contrats sont spĂ©cifiĂ©s pour les fonctions. Je ne suis pas _ trop _ dĂ©sireux d'avoir simplement des corps de fonction "normaux" comme spĂ©cification de contrat rĂ©elle, mais je pense que beaucoup de problĂšmes potentiels pourraient ĂȘtre rĂ©solus en introduisant une sorte de simplificateur de type gofmt qui simplifie automatiquement les contrats pour vous, supprimant parties Ă©trangĂšres. Ensuite, vous _pourriez_ simplement y copier le corps d'une fonction, le simplifier et le modifier Ă  partir de lĂ . Je ne sais pas comment cela sera possible Ă  mettre en Ɠuvre, cependant, malheureusement.

Cependant, certaines choses seront encore un peu difficiles à spécifier, et le chevauchement apparent entre les contrats et les interfaces semble toujours un peu étrange.

Je trouve la version "CRTP" beaucoup plus claire, plus explicite et plus facile Ă  travailler (pas besoin de crĂ©er des contrats qui n'existent que pour dĂ©finir la relation entre des contrats prĂ©existants sur un ensemble de variables). Certes, cela pourrait simplement ĂȘtre les nombreuses annĂ©es de familiaritĂ© avec l'idĂ©e.

PrĂ©cisions. Par la conception prĂ©liminaire , le contrat peut ĂȘtre appliquĂ© Ă  la fois aux fonctions et aux types .

"""
C'est sans doute plus simple si vous n'avez besoin d'utiliser que des interfaces ou des contrats, mais plus complexe si vous devez utiliser les deux ensemble.
"""

Tant qu'ils vous permettent, Ă  l'intĂ©rieur d'un contrat, de rĂ©fĂ©rencer une ou plusieurs interfaces (au lieu de seulement des opĂ©rateurs et des fonctions, permettant ainsi DRY), ce problĂšme (et ma rĂ©clamation) sera rĂ©solu. Il y a une chance que j'aie mal lu ou que je n'aie pas complĂštement lu les contrats, et aussi une chance que ladite fonctionnalitĂ© soit prise en charge et que je ne l'aie pas remarquĂ©. Si ce n'est pas le cas, ça devrait l'ĂȘtre.

Ne pouvez-vous pas faire ce qui suit ?

contract Example(t T, v V) {
  t.(interface{
    SomeMethod() V
  })
}

Vous ne pouvez pas utiliser une interface dĂ©clarĂ©e ailleurs en raison de la restriction selon laquelle vous ne pouvez pas rĂ©fĂ©rencer les identifiants du mĂȘme package dans lequel le contrat est dĂ©clarĂ©, mais vous pouvez le faire. Ou ils pourraient simplement supprimer cette restriction; cela semble un peu arbitraire.

@DeedleFake Non, car tout type d'interface peut ĂȘtre affirmĂ© par type (et ensuite potentiellement paniquer au moment de l'exĂ©cution, mais les contrats ne sont pas exĂ©cutĂ©s). Mais vous pouvez utiliser une affectation Ă  la place.

t.(someInterface) signifierait Ă©galement qu'il doit s'agir d'une interface

Bon point. Oups.

Plus j'en vois d'exemples, plus le «comprendre Ă  partir d'un corps de fonction» semble ĂȘtre sujet aux erreurs.

Il y a beaucoup de cas oĂč c'est dĂ©routant pour une personne, mĂȘme syntaxe pour diffĂ©rentes opĂ©rations, nuances d'implications de diffĂ©rentes constructions, etc., mais un outil serait capable de prendre cela et de le rĂ©duire Ă  une forme normale. Mais alors la sortie d'un tel outil devient de facto un sous-langage pour exprimer des contraintes de type que nous devons apprendre par cƓur, ce qui rend d'autant plus surprenant que quelqu'un dĂ©vie et rĂ©dige un contrat Ă  la main.

je note aussi que

contract I(t T) {
  var i interface { Foo() }
  i = t
  t.(interface{})
}

exprime que T doit ĂȘtre une interface avec au moins Foo() mais il pourrait aussi avoir n'importe quel autre nombre de mĂ©thodes supplĂ©mentaires.

T doit ĂȘtre une interface avec au moins Foo() mais elle peut aussi avoir n'importe quel autre nombre de mĂ©thodes supplĂ©mentaires

Est-ce un problÚme, cependant? Ne voulez-vous généralement pas contraindre les choses afin qu'elles autorisent des fonctionnalités spécifiques, mais vous ne vous souciez pas des autres fonctionnalités ? Sinon, un contrat comme

contract Example(t T) {
  t + t
}

ne permettrait pas la soustraction, par exemple. Mais du point de vue de tout ce que j'implĂ©mente, peu m'importe qu'un type autorise ou non la soustraction. Si je l'empĂȘchais d'effectuer des soustractions, les gens ne pourraient tout simplement pas arbitrairement, par exemple, passer tout ce qui fait Ă  une fonction Sum() ou quelque chose du genre. Cela semble arbitrairement restrictif.

Non, ce n'est pas du tout un problĂšme. C'Ă©tait juste une propriĂ©tĂ© peu intuitive (pour moi), mais c'Ă©tait peut-ĂȘtre dĂ» Ă  un manque de cafĂ©.

Il est juste de dire que la dĂ©claration de contrat actuelle doit avoir de meilleurs messages de compilateur avec lesquels travailler. Et les rĂšgles d'un contrat valide doivent ĂȘtre strictes.

salut
J'ai fait une proposition de contraintes pour les gĂ©nĂ©riques que j'ai postĂ©e dans ce fil il y a environ Âœ an.
Maintenant, j'ai fait une version 2 . Les principaux changements sont :

  • La syntaxe a Ă©tĂ© adaptĂ©e Ă  celle proposĂ©e par la go-team.
  • Les contraintes par champs ont Ă©tĂ© omises, ce qui permet pas mal de simplifications.
  • Les paragraphes jugĂ©s non strictement nĂ©cessaires ont Ă©tĂ© supprimĂ©s.

J'ai rĂ©cemment pensĂ© Ă  une question intĂ©ressante (mais peut-ĂȘtre plus dĂ©taillĂ©e qu'appropriĂ©e Ă  ce stade de la conception ?) concernant l'identitĂ© de type :

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. Imprime true , car les types des valeurs renvoyĂ©es proviennent de la mĂȘme dĂ©claration de type.
  2. Estampes
 ?
  3. Imprime false , je suppose.

c'est-à-dire que la question est de savoir quand deux types déclarés dans une fonction générique sont identiques et quand ils ne le sont pas. Je ne pense pas que cela soit décrit dans la conception ~spec~ ? Du moins je ne le trouve pas pour l'instant :)

@merovius Je suppose que le cas du milieu Ă©tait censĂ© ĂȘtre :

fmt.Println(Bar(int)() == Bar(int)()) // 2

C'est un cas intĂ©ressant, et cela dĂ©pend si les types sont "gĂ©nĂ©ratifs" ou "applicatifs". Il existe en fait plusieurs variantes de ML qui adoptent diffĂ©rentes approches. Les types applicatifs voient le gĂ©nĂ©rique comme une fonction de type, et donc f(int) == f(int). Les types gĂ©nĂ©ratifs voient le gĂ©nĂ©rique comme un modĂšle de type qui crĂ©e un nouveau type "d'instance" unique chaque fois qu'il est utilisĂ©, donc t<int> != t<int>. Cela doit ĂȘtre abordĂ© Ă  un niveau de systĂšme de type complet car il a des implications subtiles pour l'unification, l'infĂ©rence et la justesse. Pour plus de dĂ©tails et d'exemples de ce type de problĂšmes, je recommande de lire l'article "F-ing modules" d'Andreas Rossberg : https://people.mpi-sws.org/~rossberg/f-ing/ bien que l'article parle de ML " foncteurs", c'est parce que ML sĂ©pare son systĂšme de types en deux niveaux, et que les foncteurs sont des Ă©quivalents ML d'un gĂ©nĂ©rique et ne sont disponibles qu'au niveau du module.

@keean Vous pensez mal.

@merovius Oui, mon erreur, je vois que la question est parce que le paramÚtre de type n'est pas utilisé (un type fantÎme).

Avec les types gĂ©nĂ©ratifs, chaque instanciation se traduirait par un type unique diffĂ©rent pour 'S', donc mĂȘme si le paramĂštre n'est pas utilisĂ©, ils ne seraient pas Ă©gaux.

Avec les types applicatifs, les 'S' de chaque instanciation seraient du mĂȘme type, et donc ils seraient Ă©gaux.

Ce serait bizarre si le rĂ©sultat dans le cas 2 changeait en fonction des optimisations du compilateur. Ça ressemble Ă  UB.

C'est 2018 les gens, je n'arrive pas à croire que je doive taper ça comme en 1982 :

func min(x, y entier) entier {
si x < y {
retour x
}
retour y
}

func max(x, y entier) entier {
si x > y {
retour x
}
retour y
}

Je veux dire, sérieusement, les mecs MIN(INT,INT) INT, comment c'est PAS dans la langue ?
Je suis en colĂšre.

@ dataf3l Si vous voulez que ceux-ci fonctionnent comme prévu avec les précommandes, alors :

func min(x, y int) int {
   if x <= y {
      return x
   }
   return y
}

C'est ainsi que la paire (min(x, y), max(x, y)) est toujours distincte et est soit (x, y) soit (y, x), et c'est donc une sorte stable de deux éléments.

Donc, une autre raison pour laquelle ceux-ci devraient ĂȘtre dans la langue ou dans une bibliothĂšque est que les gens se trompent gĂ©nĂ©ralement :-)

J'ai pensé au < vs <=, pour les nombres entiers, je ne suis pas sûr de bien voir la différence.
Peut-ĂȘtre que je suis juste stupide...

Je ne suis pas sûr de bien voir la différence.

Il n'y en a pas dans ce cas.

@cznic true dans ce cas car ce sont des entiers, mais comme le fil portait sur les gĂ©nĂ©riques, j'ai supposĂ© que le commentaire de la bibliothĂšque concernait les dĂ©finitions gĂ©nĂ©riques de min et max afin que les utilisateurs n'aient pas Ă  les dĂ©clarer eux-mĂȘmes. En relisant l'OP, je peux voir qu'ils veulent juste un minimum et un maximum simples pour les entiers, donc mon mauvais, mais ils Ă©taient hors sujet en demandant des fonctions d'intĂ©gration simples dans un fil sur les gĂ©nĂ©riques :-)

Les gĂ©nĂ©riques sont un ajout crucial Ă  ce langage, en particulier compte tenu de l'absence de structures de donnĂ©es intĂ©grĂ©es. Jusqu'Ă  prĂ©sent, mon expĂ©rience avec Go est qu'il s'agit d'un langage formidable et facile Ă  apprendre. Il y a cependant un Ă©norme compromis Ă  faire, Ă  savoir que vous devez coder les mĂȘmes choses encore et encore et encore.

Peut-ĂȘtre qu'il me manque quelque chose, mais cela semble ĂȘtre un dĂ©faut assez important dans la langue. En fin de compte, il existe peu de structures de donnĂ©es intĂ©grĂ©es, et chaque fois que nous crĂ©ons une structure de donnĂ©es, nous devons copier et coller le code pour prendre en charge chaque T .

Je ne sais pas comment contribuer autrement que de poster mon observation ici en tant qu '«utilisateur». Je ne suis pas un programmeur suffisamment expĂ©rimentĂ© pour contribuer Ă  la conception ou Ă  la mise en Ɠuvre, donc je peux seulement dire que les gĂ©nĂ©riques amĂ©lioreraient considĂ©rablement la productivitĂ© dans le langage (tant que le temps de construction et les outils resteraient gĂ©niaux comme ils le sont maintenant).

@webern Merci. Voir https://go.googlesource.com/proposal/+/master/design/go2draft.md .

@ianlancetaylor , aprÚs avoir posté, une idée assez radicale/unique m'est venue à l'esprit qui, je pense, serait "légÚre" en ce qui concerne le langage et l'outillage. Je n'ai pas encore lu entiÚrement votre lien, je le ferai. Mais si je voulais soumettre une idée/proposition de programmation générique au format MD, comment ferais-je ?

Merci.

@webern Rédigez-le (la plupart des gens utilisent des points essentiels pour le format de démarquage) et mettez à jour le wiki ici https://github.com/golang/go/wiki/Go2GenericsFeedback

Beaucoup d'autres l'ont déjà fait.

J'ai fusionnĂ© (contre le dernier conseil) et tĂ©lĂ©chargĂ© le CL de notre implĂ©mentation de prototype prĂ©-Gophercon d'un analyseur (et d'une imprimante) implĂ©mentant la conception de l'Ă©bauche des contrats. Si vous souhaitez essayer la syntaxe, jetez un coup d'Ɠil : https://golang.org/cl/149638 .

Pour jouer avec :

1) Sélectionnez le CL dans un dépÎt récent :
git chercher https://go.googlesource.com/go refs/changes/38/149638/2 && git cherry-pick FETCH_HEAD

2) Reconstruisez et installez le compilateur :
allez installer cmd/compiler

3) Utilisez le compilateur :
aller outil compiler foo.go

Voir la description CL pour plus de détails. Profitez!

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
}

Cette conception de génériques, func Sum(type T Addable)(x []T) T , est TRÈS TRÈS TRÈS Laide !!!

Pour ĂȘtre comparĂ© Ă  func Sum(type T Addable)(x []T) T , je pense que func Sum<T: Addable> (x []T) T est plus clair et n'a aucun fardeau pour le programmeur venant d'autres langages de programmation.

Tu veux dire que la syntaxe est plus verbeuse ?
Il doit y avoir une raison pour laquelle ce n'est pas func Sum(T Addable)(x []T) T .

sans le mot-clĂ© type , il n'y aura aucun moyen de faire la diffĂ©rence entre une fonction gĂ©nĂ©rique et une autre qui renvoie une autre fonction, qui est elle-mĂȘme appelĂ©e.

@urandom Ce n'est qu'un problÚme au moment de l'instanciation et là, nous n'avons pas besoin du mot-clé type , mais vivons simplement avec l'ambiguïté AIUI.

Le problĂšme est que sans le mot-clĂ© type , func Foo(x T) (y T) pourrait ĂȘtre analysĂ© soit comme dĂ©clarant une fonction gĂ©nĂ©rique prenant un T et ne retournant rien, soit comme une fonction non gĂ©nĂ©rique prenant un T et retournant un T .

fonction Somme(x []T) T

Je suis d'accord, je préfÚre quelque chose dans ce sens. Compte tenu de l'expansion de la portée linguistique représentée par les génériques, je pense qu'il serait raisonnable d'introduire cette syntaxe pour "attirer l'attention" sur une fonction générique.

Je pense aussi que cela rendrait le code un peu plus facile (lire: moins Lisp-y) à analyser pour les lecteurs humains, ainsi que réduire les chances de rencontrer une ambiguïté d'analyse obscure plus loin sur la ligne (voir "Most Vexing Parse" de C++, pour aider à motiver une abondance de prudence).

C'est 2018 les gens, je n'arrive pas à croire que je doive taper ça comme en 1982 :

func min(x, y entier) entier {
si x < y {
retour x
}
retour y
}

func max(x, y entier) entier {
si x > y {
retour x
}
retour y
}

Je veux dire, sérieusement, les mecs MIN(INT,INT) INT, comment c'est PAS dans la langue ?
Je suis en colĂšre.

Il y a une raison Ă  cela.
Si vous ne comprenez pas, vous pouvez apprendre ou partir.
Votre choix.

J'espÚre sincÚrement qu'ils l'amélioreront.
Mais votre attitude "vous pouvez apprendre ou partir", n'est pas un bon exemple à suivre pour les autres. il lit inutilement abrasif. Je ne pense pas que cette communauté parle de @petar-dambovaliev. cependant, ce n'est pas à moi de vous dire quoi faire, ou comment se comporter en ligne, ce n'est pas ma place.

Je sais qu'il y a beaucoup de sentiments forts à propos des génériques, mais s'il vous plaßt gardez à l'esprit nos valeurs Gopher . Veuillez garder la conversation respectueuse et accueillante de tous les cÎtés.

@bcmills merci, vous faites de la communauté un meilleur endroit.

@katzdm était d'accord, la langue a déjà tellement de parenthÚses, ce nouveau truc me semble vraiment ambigu

Définir generics semble inévitable en introduisant des choses comme type's type , ce qui rend Go plutÎt compliqué.

J'espÚre que ce n'est pas trop hors sujet, mais une fonctionnalité de function overload me semble suffisante.

BTW, je sais qu'il y a eu des discussions sur la surcharge .

@xgfone D'accord, que la langue a déjà tellement de parenthÚses, ce qui rend le code peu clair.
func Sum<T: Addable> (x []T) T ou func Sum<type T Addable> (x []T) T est meilleur et plus clair.

Pour des raisons de cohérence (avec les génériques intégrés), func Sum[T: Addable] (x []T) T vaut mieux que func Sum<T: Addable> (x []T) T .

Je peux ĂȘtre influencĂ© par des travaux antĂ©rieurs dans d'autres langues, mais Sum<T: Addable> (x []T) T semble plus distinct et lisible Ă  premiĂšre vue.

Je suis également d'accord avec @katzdm en ce sens qu'il est préférable d'attirer l'attention sur quelque chose de nouveau dans la langue. Il est également assez familier aux développeurs non-Go qui se lancent dans Go.

FWIW, il y a environ 0 % de chances que Go utilise des crochets angulaires pour les génériques. La grammaire de C++ est inanalysable car vous ne pouvez pas distinguer a < b > c (une série de comparaisons légale mais sans signification) d'une invocation générique sans comprendre les types de a, b et c. D'autres langues évitent d'utiliser des crochets angulaires pour les génériques pour cette raison.

func a < b Addable> (...
Je suppose que vous le pouvez si vous réalisez qu'aprÚs func vous ne pouvez avoir que le nom de la fonction, un ( ou un < .

@carlmjohnson j'espĂšre que tu as raison

f := sum<int>(10)

Mais ici vous savez que sum est un contrat..

La grammaire de C++ est inanalysable car vous ne pouvez pas distinguer a < b > c (une série de comparaisons légale mais sans signification) d'une invocation générique sans comprendre les types de a, b et c.

Je pense qu'il vaut la peine de souligner que mĂȘme si Go, contrairement Ă  C++, interdit cela dans le systĂšme de type, puisque les opĂ©rateurs < et > renvoient bool s dans Go et < et > ne peuvent pas ĂȘtre utilisĂ©s avec des bool s, c'est syntaxiquement lĂ©gal, donc c'est toujours un problĂšme.

Un autre problÚme avec les chevrons est List<List<int>> , dans lequel le >> est symbolisé comme un opérateur de décalage vers la droite.

Quels étaient les problÚmes avec l'utilisation [] ? Il me semble que la plupart des problÚmes ci-dessus sont résolus en les utilisant:

  • Syntaxiquement, f := sum[int](10) , pour utiliser l'exemple ci-dessus, est sans ambiguĂŻtĂ© car il a la mĂȘme syntaxe qu'un tableau ou un accĂšs Ă  la carte, puis le systĂšme de type peut le comprendre plus tard, comme il doit dĂ©jĂ  le faire pour la diffĂ©rence entre les accĂšs au tableau et Ă  la carte, par exemple. Ceci est diffĂ©rent du cas de <> car un seul < est lĂ©gal, ce qui conduit Ă  l'ambiguĂŻtĂ©, mais un seul [ ne l'est pas.
  • func Example[T](v T) T est Ă©galement sans ambiguĂŻtĂ©.
  • ]] n'est pas son propre jeton, ce problĂšme est donc Ă©galement Ă©vitĂ©.

Le projet de conception mentionne une ambiguĂŻtĂ© dans les dĂ©clarations de type , comme dans type A [T] int , mais je pense que cela pourrait ĂȘtre rĂ©solu relativement facilement de diffĂ©rentes maniĂšres. Par exemple, la dĂ©finition gĂ©nĂ©rique pourrait ĂȘtre dĂ©placĂ©e vers le mot-clĂ© lui-mĂȘme, plutĂŽt que vers le nom du type, c'est-Ă -dire :

  • func[T] Example(v T) T
  • type[T] A int

La complication ici pourrait provenir de l'utilisation de blocs de déclaration de type, comme

type (
  A int
)

mais je pense que c'est assez rare pour dire que si vous avez besoin de génériques, vous ne pouvez pas utiliser l'un de ces blocs.

Je pense qu'il serait trĂšs malheureux d'Ă©crire

type[T] A []T
var s A[int]

parce que les crochets se dĂ©placent d'un cĂŽtĂ© de A Ă  l'autre. Bien sĂ»r, cela pourrait ĂȘtre fait, mais nous devrions viser mieux.

Cela dit, l'utilisation du mot-clé type dans la syntaxe actuelle signifie que nous pourrions remplacer les parenthÚses par des crochets.

Cela ne semble pas si diffĂ©rent du type de tableau par rapport Ă  la syntaxe d'expression Ă©tant [N]T par rapport arr[i] , en termes de la façon dont quelque chose est dĂ©clarĂ© ne correspondant pas Ă  la façon dont il est utilisĂ©. Oui, dans var arr [N]T , les crochets se retrouvent du mĂȘme cĂŽtĂ© de arr que lors de l'utilisation arr , mais nous pensons normalement Ă  la syntaxe en termes de syntaxe type vs expression Ă©tant en face.

J'ai étendu et amélioré certaines de mes anciennes idées immatures pour essayer d'unifier les génériques personnalisés et intégrés.

Je ne sais pas si discuter ( vs < vs [ et l'utilisation de type est du bikeshedding ou s'il y a vraiment un problĂšme avec la syntaxe

@ianlancetaylor ... s'est demandĂ© si les commentaires justifiaient des ajustements Ă  la conception proposĂ©e ? Mon propre sentiment des commentaires Ă©tait que beaucoup pensaient que les interfaces et les contrats pouvaient ĂȘtre combinĂ©s, du moins au dĂ©but. Semblait ĂȘtre un changement aprĂšs un certain temps que les deux concepts devraient ĂȘtre sĂ©parĂ©s. Mais je peux mal lire les tendances. J'adorerais voir une option expĂ©rimentale dans une version cette annĂ©e !

Oui, nous envisageons de modifier le projet de conception, notamment en examinant les nombreuses contre-propositions que les gens ont faites. Rien n'est finalisé.

Juste pour ajouter un rapport d'expérience pratique :
J'ai implémenté des génériques comme extension de langage dans mon interpréteur Go https://github.com/cosmos72/gomacro. Fait intéressant, les deux syntaxes

type[T] Pair struct { First T; Second T }
type Pair[T] struct { First T; Second T }

s'est avĂ©rĂ© introduire de nombreuses ambiguĂŻtĂ©s dans l'analyseur : le second pourrait ĂȘtre analysĂ© comme une dĂ©claration indiquant que Pair est un tableau de structures T , oĂč T est un entier constant. Lorsque Pair est utilisĂ©, il y a aussi des ambiguĂŻtĂ©s : Pair[int] pourrait Ă©galement ĂȘtre analysĂ© comme une expression au lieu d'un type : il pourrait indexer un tableau/tranche/carte nommĂ© Pair avec l'expression d'index int (remarque : int et les autres types de base ne sont PAS des mots-clĂ©s rĂ©servĂ©s dans Go), j'ai donc dĂ» recourir Ă  une nouvelle syntaxe - certes laide, mais qui fait l'affaire :

template[T] type Pair struct { First T; Second T }
type pairOfInt = Pair#[int]
var p Pair#[int]

et de mĂȘme pour les fonctions :

template[T] func Sum(args ...T) T { /*...*/ }
Sum#[int] (1,2,3)

Ainsi, bien qu'en théorie je sois d'accord que la syntaxe est une question superficielle, je dois souligner que :
1) d'un cĂŽtĂ©, la syntaxe est celle Ă  laquelle les programmeurs Go seront exposĂ©s - elle doit donc ĂȘtre expressive, simple et Ă©ventuellement agrĂ©able au goĂ»t
2) de l'autre cÎté, un mauvais choix de syntaxe compliquera l'analyseur, le vérificateur de type et le compilateur afin de résoudre les ambiguïtés introduites

Pair[int] pourrait Ă©galement ĂȘtre analysĂ© comme une expression au lieu d'un type : il pourrait indexer un tableau/tranche/carte nommĂ© Pair avec l'expression d'index int

Ce n'est pas une ambiguĂŻtĂ© d'analyse, juste une ambiguĂŻtĂ© sĂ©mantique (jusqu'Ă  aprĂšs la rĂ©solution du nom) ; la structure syntaxique est la mĂȘme dans les deux cas. Notez que Sum#[int] peut aussi ĂȘtre un type ou une expression selon ce qu'est Sum . Il en va de mĂȘme pour (*T) dans le code existant. Tant que la rĂ©solution de nom n'affecte pas la structure de ce qui est analysĂ©, tout va bien.

Comparez cela aux problĂšmes avec <> :

f ( a < b , c < d >> (e) )

Vous ne pouvez mĂȘme pas tokeniser cela, puisque >> pourrait ĂȘtre un ou deux jetons. Ensuite, vous ne pouvez pas dire s'il y a un ou deux arguments Ă  f ... la structure de l'expression change de maniĂšre significative en fonction de ce qui est dĂ©signĂ© par a .

Quoi qu'il en soit, je suis intéressé de voir quelle est la réflexion actuelle dans l'équipe sur les génériques, en particulier, si "les contraintes ne sont que du code" ont été itérées ou abandonnées. Je peux comprendre que je veuille éviter de définir un langage de contraintes distinct, mais il s'avÚre que l'écriture de code qui contraint suffisamment les types impliqués force un style non naturel, et vous devez également mettre des limites sur ce que le compilateur peut réellement déduire sur les types basés sur le code car sinon ces inférences peuvent devenir arbitrairement complexes, ou peuvent s'appuyer sur des faits concernant la langue qui pourraient changer à l'avenir.

@cosmos72

Peut-ĂȘtre que je me trompe, mais Ă  cĂŽtĂ© de ce qui a Ă©tĂ© dit par @stevenblenkinsop , est-il possible qu'un terme :

a b

pourrait Ă©galement impliquer que b n'est pas un type si b est connu pour ĂȘtre un alphanumĂ©rique (pas d'opĂ©rateur/pas de sĂ©parateur) avec [identifier] facultatif ajoutĂ© dessus et a n'est pas un mot-clĂ© spĂ©cial/alphanumĂ©rique spĂ©cial (par exemple pas d'importation/ paquet/type/fonction) ?.

Je ne connais pas trop la grammaire de go.

D'une certaine maniÚre, des types comme int et Sum[int] seraient de toute façon traités comme des expressions :

type (
    nodeList = []*Node  // nodeList and []*Node are identical types
    Polar    = polar    // Polar and polar denote identical types
)

Si go autorise les fonctions infixes, alors en effet a type tag serait ambigu car type pourrait ĂȘtre une fonction infixe ou un type.

J'ai remarqué aujourd'hui que l' aperçu des problÚmes de cette proposition prétend Swift :

DĂ©clarer que T satisfait le protocole Equatable rend valide l'utilisation de == dans le corps de la fonction. Equatable semble ĂȘtre un Ă©lĂ©ment intĂ©grĂ© Ă  Swift, impossible Ă  dĂ©finir autrement.

Cela semble ĂȘtre plus un apartĂ© que quelque chose qui affecte profondĂ©ment les dĂ©cisions prises sur ce sujet, mais au cas oĂč cela donnerait de l'inspiration Ă  des gens beaucoup plus intelligents que moi, je voulais souligner qu'il n'y a en fait rien de spĂ©cial environ Equatable autre que le fait qu'il soit prĂ©dĂ©fini dans le langage (principalement pour que de nombreux autres types intĂ©grĂ©s puissent "s'y conformer"). Il est tout Ă  fait possible de crĂ©er des protocoles similaires :

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
Je parlais des ambiguïtés de la syntaxe a[b] proposée pour les génériques, puisqu'elle est déjà utilisée pour indexer les tranches et les cartes - pas à propos a b .

Entre-temps, j'ai étudié Haskell, et bien que je sache qu'il utilisait largement l'inférence de type, l'expressivité et la sophistication de ses génériques m'ont surpris.

Malheureusement, il a un schĂ©ma de nommage assez particulier, il n'est donc pas toujours facile Ă  comprendre au premier coup d'Ɠil. Par exemple, un class est en fait une contrainte pour les types (gĂ©nĂ©riques ou non). La classe Eq est la contrainte pour les types dont les valeurs peuvent ĂȘtre comparĂ©es Ă  '==' et '/=' :

class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool

signifie qu'un type a satisfait la contrainte Eq si une "spécialisation" existe (en fait une "instance" dans le langage Haskell) des fonctions infixées == et /= qui accepte deux arguments, chacun avec le type a et renvoie un résultat Bool .

J'essaie actuellement d'adapter certaines des idĂ©es trouvĂ©es dans les gĂ©nĂ©riques Haskell Ă  une proposition de gĂ©nĂ©riques Go, et de voir Ă  quel point elles s'intĂšgrent. Je suis vraiment heureux de voir que cette enquĂȘte est en cours avec d'autres langages au-delĂ  de C++ et Java :

l'exemple Swift ci-dessus, et mon exemple Haskell, montrent que les contraintes sur les types génériques sont déjà utilisées en pratique par plusieurs langages de programmation, et qu'une quantité non négligeable d'expérience sur diverses approches des génériques et des contraintes existe et est disponible parmi les programmeurs de ces (et d'autres) langues.

À mon avis, cela vaut certainement la peine d'Ă©tudier une telle expĂ©rience avant de finaliser une proposition de gĂ©nĂ©riques de Go.

PensĂ©e parasite : si la forme de contrainte que vous souhaitez que le type gĂ©nĂ©rique satisfasse se trouve ĂȘtre plus ou moins conforme Ă  une dĂ©finition d'interface, vous pouvez utiliser la syntaxe d'assertion de type existante Ă  laquelle nous sommes dĂ©jĂ  habituĂ©s :

type Comparer interface {
  Compare(v interface{}) (*int, error)
}
type PriorityQueue<T.(Comparer)> struct {
  things []T
}

Toutes mes excuses si cela a déjà été discuté de maniÚre exhaustive ailleurs ; Je ne l'ai pas vu, mais je suis toujours rattrapé par la littérature. Je l'ignore depuis un moment parce que, eh bien, je ne veux pas de génériques dans aucune version de Go. Mais l'idée semble prendre de l'ampleur et un sentiment d'inévitabilité dans la communauté dans son ensemble.

@jesse-amano Il est intĂ©ressant de noter que vous ne voulez pas de gĂ©nĂ©riques dans aucune version de Go. Je trouve cela difficile Ă  comprendre car en tant que programmeur, je n'aime vraiment pas me rĂ©pĂ©ter. Chaque fois que je programme en 'C', je me retrouve Ă  devoir implĂ©menter les mĂȘmes choses de base comme une liste ou un arbre sur un nouveau type de donnĂ©es, et inĂ©vitablement mes implĂ©mentations sont pleines de bogues. Avec les gĂ©nĂ©riques, nous ne pouvons avoir qu'une seule version de n'importe quel algorithme, et toute la communautĂ© peut contribuer Ă  faire de cette version la meilleure. Quelle est votre solution pour ne pas vous rĂ©pĂ©ter ?

Concernant l'autre point, Go semble introduire une nouvelle syntaxe pour les contraintes génériques car les interfaces ne permettent pas de surcharger les opérateurs (comme '==' et '+'). Il y a deux façons d'avancer à partir de cela, définir un nouveau mécanisme pour les contraintes génériques, ce qui est la voie que semble suivre Go, ou permettre aux interfaces de surcharger les opérateurs, ce qui est la voie que je préfÚre.

Je prĂ©fĂšre la deuxiĂšme option car elle maintient la syntaxe du langage plus petite et plus simple, et permet de dĂ©clarer de nouveaux types numĂ©riques qui peuvent utiliser les opĂ©rateurs habituels, par exemple des nombres complexes que vous pouvez additionner avec '+'. L'argument contre cela semble ĂȘtre que les gens pourraient abuser de la surcharge de l'opĂ©rateur pour faire faire des choses Ă©tranges Ă  '+', mais cela ne me semble pas un argument car je peux dĂ©jĂ  abuser de n'importe quel nom de fonction, par exemple je peux Ă©crire une fonction appelĂ©e 'print ' qui efface toutes les donnĂ©es de mon disque dur et termine le programme. J'aimerais avoir la possibilitĂ© de restreindre les surcharges des opĂ©rateurs et des fonctions pour se conformer Ă  certaines propriĂ©tĂ©s axiomatiques telles que la commutativitĂ© ou l'associativitĂ©, mais si cela ne s'applique pas aux opĂ©rateurs et aux fonctions, je ne vois pas grand-chose. Un opĂ©rateur n'est qu'une fonction infixe, et une fonction n'est qu'un opĂ©rateur prĂ©fixe aprĂšs tout.

Un autre point Ă  mentionner est que les contraintes gĂ©nĂ©riques qui rĂ©fĂ©rencent plusieurs paramĂštres de type sont trĂšs utiles, si les contraintes gĂ©nĂ©riques Ă  paramĂštre unique sont des prĂ©dicats sur les types, les contraintes multiparamĂštres sont des relations sur les types. Les interfaces Go ne peuvent pas avoir plus d'un paramĂštre de type, donc soit une nouvelle syntaxe doit ĂȘtre introduite, soit les interfaces doivent ĂȘtre repensĂ©es.

Donc, d'une certaine maniĂšre, je suis d'accord avec vous, Go n'a pas Ă©tĂ© conçu comme un langage gĂ©nĂ©rique, et toute tentative de renforcer les gĂ©nĂ©riques sera sous-optimale. Peut-ĂȘtre vaut-il mieux garder Go sans gĂ©nĂ©riques et concevoir un nouveau langage autour des gĂ©nĂ©riques Ă  partir de zĂ©ro pour garder le langage petit avec une syntaxe simple.

@keean Je n'ai pas une aversion aussi forte à me répéter plusieurs fois quand j'en ai besoin, et l'approche de Go en matiÚre de gestion des erreurs, de récepteurs de méthode, etc. semble généralement faire du bon travail pour garder la plupart des bogues à distance.

Dans une poignĂ©e de cas au cours des quatre derniĂšres annĂ©es, je me suis retrouvĂ© dans des situations oĂč un algorithme complexe mais gĂ©nĂ©ralisable devait ĂȘtre appliquĂ© Ă  plus de deux structures de donnĂ©es complexes mais auto-cohĂ©rentes, et dans tous les cas -- et je le dis avec tout Ă  fait sĂ©rieux - j'ai trouvĂ© que la gĂ©nĂ©ration de code via go:generate Ă©tait plus que suffisante.

En lisant les rapports d'expĂ©rience, dans de nombreux cas, je pense que go:generate ou un outil similaire aurait pu rĂ©soudre le problĂšme, et dans d'autres cas, j'ai l'impression que peut-ĂȘtre que Go1 n'Ă©tait tout simplement pas le bon langage, et quelque chose d'autre aurait pu ĂȘtre utilisĂ© Ă  la place (peut-ĂȘtre avec un wrapper de plugin si du code Go avait besoin de l'utiliser). Mais je suis conscient qu'il est assez facile pour moi de spĂ©culer sur ce que j'aurais pu faire, ce qui aurait pu marcher ; Jusqu'Ă  prĂ©sent, je n'ai eu aucune expĂ©rience pratique qui m'a fait souhaiter que Go1 ait plus de façons d'exprimer des types gĂ©nĂ©riques, mais il se peut que j'aie une façon Ă©trange de penser aux choses, ou il se peut que j'aie Ă©tĂ© extrĂȘmement chanceux de ne travailler que sur des projets qui n'avaient pas vraiment besoin de gĂ©nĂ©riques.

J'espĂšre que si Go2 finit par prendre en charge une syntaxe gĂ©nĂ©rique, il aura une correspondance assez simple avec la logique qui sera gĂ©nĂ©rĂ©e, sans cas extrĂȘmes Ă©tranges pouvant rĂ©sulter du boxing/unboxing, de la "rĂ©ification", des chaĂźnes d'hĂ©ritage, etc. dont les autres langues doivent se prĂ©occuper.

@jesse-amano D'aprĂšs mon expĂ©rience, ce n'est pas seulement quelques fois, chaque programme est une composition d'algorithmes bien connus. Je ne me souviens pas de la derniĂšre fois que j'ai Ă©crit un algorithme original, peut-ĂȘtre un problĂšme d'optimisation complexe qui nĂ©cessitait une connaissance du domaine.

Lors de l'écriture d'un programme, la premiÚre chose que je fais est d'essayer de décomposer le problÚme en morceaux bien connus que je peux composer, un analyseur d'arguments, un flux de fichiers, une disposition de l'interface utilisateur basée sur des contraintes. Ce ne sont pas seulement des algorithmes complexes dans lesquels les gens font des erreurs, presque personne ne peut écrire une implémentation correcte de "min" et "max" la premiÚre fois (Voir : http://componentsprogramming.com/writing-min-function-part5/ ).

Le problÚme avec go:generate est qu'il s'agit essentiellement d'un processeur de macros, il n'a pas de sécurité de type, vous devez d'une maniÚre ou d'une autre vérifier le type et vérifier les erreurs du code généré, ce que vous ne pouvez pas faire tant que vous n'avez pas exécuté la génération. Ce type de méta-programmation est trÚs difficile à déboguer. Je ne veux pas écrire un programme pour écrire le programme, je veux juste écrire le programme :-)

Ainsi, la diffĂ©rence avec les gĂ©nĂ©riques est que je peux Ă©crire un programme _direct_ simple qui peut ĂȘtre vĂ©rifiĂ© par erreur et par type vĂ©rifiĂ© par ma comprĂ©hension de la signification, sans avoir Ă  gĂ©nĂ©rer le code, et le dĂ©boguer et renvoyer les bogues au gĂ©nĂ©rateur.

Un exemple trÚs simple est "swap", je veux juste échanger deux valeurs, peu m'importe ce qu'elles sont :

swap<A>(x: *A, y: *A) {
   let tmp = *x
   *x = *y
   *y = tmp
}

Maintenant, je pense qu'il est trivial de voir si cette fonction est correcte, et qu'il est trivial de voir qu'elle est gĂ©nĂ©rique et peut ĂȘtre appliquĂ©e Ă  n'importe quel type. Pourquoi voudrais-je jamais taper cette fonction encore et encore pour chaque type de pointeur vers une valeur sur laquelle je pourrais vouloir utiliser swap. Bien sĂ»r, je peux alors crĂ©er de plus grands algorithmes gĂ©nĂ©riques Ă  partir de cela, comme un tri sur place. Je ne pense pas que le code go:generate, mĂȘme pour un algorithme simple, soit facile Ă  voir s'il est correct.

Je pourrais facilement faire une erreur comme:

let tmp = *x
*y = *x
*x = tmp

en tapant ceci Ă  la main chaque fois que je voulais Ă©changer le contenu de deux pointeurs.

Je comprends que la façon idiomatique de faire ce genre de chose dans Go est d'utiliser une interface vide, mais ce n'est pas sûr et lent. Cependant, il me semble que Go n'a pas les bonnes fonctionnalités pour prendre en charge avec élégance ce type de programmation générique, et les interfaces vides fournissent une trappe de sortie pour contourner les problÚmes. PlutÎt que de changer complÚtement le style de go, il semble préférable de développer un langage adapté à ce genre de génériques en partant de zéro. Fait intéressant, "Rust" obtient une grande partie des éléments génériques, mais comme il utilise la gestion de la mémoire statique plutÎt que la récupération de place, il ajoute beaucoup de complexité qui n'est pas vraiment nécessaire pour la plupart des programmes. Je pense qu'entre Haskell, Go et Rust, il y a probablement tous les éléments nécessaires pour créer un langage générique grand public décent, tous mélangés.

Pour information : je suis en train d'écrire une wishlist sur les génériques de Go,

avec l'intention de l'implémenter dans mon interpréteur Go gomacro , qui a déjà une implémentation différente des génériques Go (modélisés d'aprÚs les modÚles C++).

Ce n'est pas encore terminé, les commentaires sont les bienvenus :)

@keean

J'ai lu l'article de blog que vous avez liĂ© Ă  propos de la fonction min et les quatre articles qui y ont prĂ©cĂ©dĂ©. Je n'ai mĂȘme pas observĂ© une tentative de faire valoir que "presque personne ne peut Ă©crire une implĂ©mentation correcte de 'min'...". L'auteur semble en fait reconnaĂźtre que leur premiĂšre implĂ©mentation _est_ correcte... tant que le domaine est limitĂ© aux nombres. C'est l'introduction d'objets et de classes, et l'exigence qu'ils soient comparĂ©s selon une seule dimension, Ă  moins que les valeurs de cette dimension soient les mĂȘmes, sauf quand — et ainsi de suite, qui crĂ©e une complexitĂ© supplĂ©mentaire. Les exigences cachĂ©es subtiles impliquĂ©es dans la nĂ©cessitĂ© de dĂ©finir soigneusement les fonctions de comparaison et de tri sur un objet complexe sont exactement la raison pour laquelle je _n'aime pas_ les gĂ©nĂ©riques en tant que concept (au moins dans Go ; Java avec Spring semble ĂȘtre dĂ©jĂ  un environnement assez bon pour composer ensemble un tas de bibliothĂšques matures dans une application).

Personnellement, je ne trouve pas le besoin de sĂ©curitĂ© de type dans les gĂ©nĂ©rateurs de macros ; s'ils gĂ©nĂšrent du code lisible ( gofmt aide Ă  placer la barre assez bas), alors la vĂ©rification des erreurs au moment de la compilation devrait ĂȘtre suffisante. Cela ne devrait pas avoir d'importance pour l'utilisateur du gĂ©nĂ©rateur (ou du code l'invoquant) pour la production, de toute façon ; dans le nombre certes restreint de fois oĂč j'ai Ă©tĂ© appelĂ© Ă  Ă©crire un algorithme gĂ©nĂ©rique sous forme de macro, une poignĂ©e de tests unitaires (gĂ©nĂ©ralement flottant, chaĂźne et pointeur vers structure - s'il existe des types codĂ©s en dur qui devraient 't ĂȘtre codĂ© en dur, l'un de ces trois sera incompatible avec lui ; si l'un de ces trois ne peut pas ĂȘtre utilisĂ© dans l'algorithme gĂ©nĂ©rique, alors ce n'est pas un algorithme gĂ©nĂ©rique) Ă©tait suffisant pour s'assurer que la macro fonctionnait correctement.

swap est un mauvais exemple. Désolé, mais ça l'est. C'est déjà un one-liner dans Go, pas besoin d'une fonction générique pour l'envelopper et pas de place pour qu'un programmeur fasse une erreur non évidente.

*y, *x = *x, *y

Il existe également déjà un sort en place dans la bibliothÚque standard . Il utilise des interfaces. Pour créer une version spécifique à votre type, définissez :

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

C'est certes plusieurs octets de plus à taper que SortableList<mytype>(myThings).Sort() , mais c'est _beaucoup_ moins dense à lire, il n'est pas aussi susceptible de "bégayer" dans le reste d'une application, et si des bogues surviennent, il est peu probable avoir besoin de quelque chose d'aussi lourd qu'une trace de pile pour trouver la cause. L'approche actuelle présente plusieurs avantages, et je crains que nous ne les perdions si nous nous appuyons trop sur les génériques.

@jesse-amano
Les problĂšmes avec 'min/max' s'appliquent mĂȘme si vous ne comprenez pas la nĂ©cessitĂ© d'un tri stable. Par exemple, un dĂ©veloppeur implĂ©mente min/max pour un type de donnĂ©es dans un module, puis il est utilisĂ© dans un tri ou un autre algorithme par un autre membre de l'Ă©quipe sans vĂ©rification appropriĂ©e des hypothĂšses, et conduit Ă  des bogues Ă©tranges car ce n'est pas stable.

Je pense que la programmation consiste principalement Ă  composer des algorithmes standard, trĂšs rarement les programmeurs crĂ©ent de nouveaux algorithmes innovants, donc min/max et sort ne sont que des exemples. Choisir des trous dans les exemples spĂ©cifiques que j'ai choisis montre simplement que je n'ai pas choisi de trĂšs bons exemples, cela n'aborde pas le problĂšme rĂ©el. J'ai choisi "swap" parce que c'est trĂšs simple et rapide pour moi Ă  taper. J'aurais pu en choisir bien d'autres, trier, faire pivoter, partitionner, qui sont des algorithmes trĂšs gĂ©nĂ©raux. Cela ne prend pas longtemps lorsque vous Ă©crivez un programme qui utilise une collection comme un arbre rouge/noir pour en avoir marre de devoir refaire l'arbre pour chaque type de donnĂ©es diffĂ©rent dont vous voulez une collection, parce que vous voulez la sĂ©curitĂ© de type, et une interface vide vaut Ă  peine mieux que "void*" en 'C'. Ensuite, vous auriez Ă  refaire la mĂȘme chose pour chaque algorithme qui utilise chacun de ces arbres, comme la prĂ©-commande, la commande, l'itĂ©ration post-commande, la recherche, et c'est avant que nous n'abordions des choses sophistiquĂ©es comme les algorithmes de rĂ©seau de Tarjan (disjoint ensembles, tas, arbres couvrant minimum, chemins les plus courts, flux, etc.)

Je pense que les générateurs de code ont leur place, par exemple en générant un validateur à partir d'un schéma json ou un analyseur à partir d'une définition de grammaire, mais je ne pense pas qu'ils remplacent convenablement les génériques. Pour la programmation générique, je veux pouvoir écrire n'importe quel algorithme une fois, et le rendre clair, simple et direct.

En tout cas, je suis d'accord avec vous Ă  propos de 'Go', je ne pense pas qu'un 'Go' ait Ă©tĂ© conçu dĂšs le dĂ©part pour ĂȘtre un bon langage gĂ©nĂ©rique, et l'ajout de gĂ©nĂ©riques maintenant n'aboutira probablement pas Ă  un bon langage gĂ©nĂ©rique, et va perdre une partie de la franchise et de la simplicitĂ© qu'il a dĂ©jĂ . Personnellement, si vous devez rechercher un gĂ©nĂ©rateur de code (au-delĂ  de choses comme gĂ©nĂ©rer des validateurs Ă  partir de json-schema ou des analyseurs Ă  partir d'un fichier de grammaire), vous utilisez probablement de toute façon le mauvais langage.

Edit: En ce qui concerne le test des gĂ©nĂ©riques avec "float" "string" "pointer-to-struct", je ne pense pas qu'il existe de nombreux algorithmes gĂ©nĂ©riques qui fonctionnent sur un ensemble de types aussi divers, sauf peut-ĂȘtre "swap". Les vĂ©ritables fonctions "gĂ©nĂ©riques" sont vraiment limitĂ©es aux mĂ©langes et ne se produisent pas trĂšs souvent. Les gĂ©nĂ©riques contraints sont beaucoup plus intĂ©ressants, oĂč les types gĂ©nĂ©riques sont contraints par une interface. Comme vous pouvez le voir, avec l'exemple de tri sur place de la bibliothĂšque standard, vous pouvez faire fonctionner certains gĂ©nĂ©riques contraints dans 'Go' dans des cas limitĂ©s. J'aime la façon dont les interfaces Go fonctionnent, et vous pouvez faire beaucoup avec elles. J'aime encore plus les vrais gĂ©nĂ©riques contraints. Je n'aime pas vraiment ajouter un deuxiĂšme mĂ©canisme de contrainte comme le fait la proposition actuelle de gĂ©nĂ©riques. Un langage oĂč les interfaces contraignent directement les types serait beaucoup plus Ă©lĂ©gant.

Il est intĂ©ressant de noter que pour autant que je sache, la seule raison pour laquelle les nouvelles contraintes ont Ă©tĂ© introduites est que Go ne permet pas de dĂ©finir des opĂ©rateurs dans les interfaces. Les propositions antĂ©rieures de gĂ©nĂ©riques permettaient aux types d'ĂȘtre contraints par des interfaces, mais ont Ă©tĂ© abandonnĂ©es parce qu'elles ne gĂ©raient pas les opĂ©rateurs comme '+'.

@keean
Il y a peut-ĂȘtre un meilleur endroit pour une discussion prolongĂ©e. (Peut-ĂȘtre pas ; j'ai regardĂ© autour de moi et cela semble ĂȘtre l'endroit idĂ©al pour discuter des gĂ©nĂ©riques dans Go2.)

Je comprends certainement la nécessité d'un tri stable! Je soupçonne que les auteurs de la bibliothÚque standard originale Go1 l'ont compris aussi, puisque sort.Stable y est depuis sa sortie publique.

Je pense que la grande chose à propos du package sort de la bibliothÚque standard est qu'il ne fonctionne pas uniquement sur les tranches. C'est certainement le plus simple lorsque le récepteur est une tranche, mais tout ce dont vous avez vraiment besoin est un moyen de savoir combien de valeurs se trouvent dans le conteneur (la méthode Len() int ), comment les comparer (la méthode Less(int, int) bool méthode), et comment les échanger (la méthode Swap(int, int) , bien sûr). Vous pouvez implémenter sort.Interface en utilisant des canaux ! C'est lent, bien sûr, car les canaux ne sont pas conçus pour une indexation efficace, mais cela peut s'avérer correct étant donné un budget de temps d'exécution généreux.

Je ne veux pas pinailler, mais le problÚme avec un mauvais exemple, c'est que... c'est mauvais. Des choses comme sort et min ne sont que _pas_ des points en faveur d'une fonctionnalité de langage à fort impact comme les génériques. Je pense assez fortement que faire des trous dans ces exemples _does_ adresse le point réel; _mon_ point est qu'il n'y a pas besoin de génériques lorsqu'une meilleure solution existe déjà dans le langage.

@jesse-amano

une meilleure solution existe déjà dans la langue

Lequel? Je ne vois rien de mieux que les gĂ©nĂ©riques contraints de type sĂ©curisĂ©. Les gĂ©nĂ©rateurs ne sont pas Go, purement et simplement. Les interfaces et la rĂ©flexion produisent un code dangereux, lent et sujet Ă  la panique. Ces solutions sont assez bonnes parce qu'il n'y a rien d'autre. Les gĂ©nĂ©riques rĂ©soudraient le problĂšme avec les constructions d'interface vides et dangereuses et, le pire de tout, Ă©limineraient de nombreuses utilisations de la rĂ©flexion qui sont encore plus sujettes aux paniques d'exĂ©cution. MĂȘme la nouvelle proposition de package d'erreurs souffre du manque de gĂ©nĂ©riques et son API en bĂ©nĂ©ficierait grandement. Vous pouvez regarder As comme exemple - non idiomatique, sujet aux paniques, difficile Ă  utiliser, nĂ©cessite une vĂ©rification vĂ©tĂ©rinaire pour ĂȘtre utilisĂ© correctement. Tout cela parce que Go manque de tout type de gĂ©nĂ©riques.

sort , min et d'autres algorithmes gĂ©nĂ©riques sont d'excellents exemples car ils montrent le principal avantage des gĂ©nĂ©riques - la composabilitĂ©. Ils permettent de crĂ©er une vaste bibliothĂšque de routines de transformation gĂ©nĂ©riques pouvant ĂȘtre enchaĂźnĂ©es. Et surtout, il serait facile Ă  utiliser, sĂ»r, rapide (du moins c'est possible avec les gĂ©nĂ©riques), pas besoin de passe-partout, de gĂ©nĂ©rateurs, d'interface{}, de rĂ©flexion et d'autres fonctionnalitĂ©s de langage obscures utilisĂ©es uniquement parce qu'il n'y a pas d'autre moyen.

@creker

Lequel?

Pour trier les choses, le package sort . Tout ce qui implĂ©mente sort.Interface peut ĂȘtre triĂ© (avec un algorithme stable ou instable de votre choix ; certaines versions sur place sont fournies via le package sort , mais vous ĂȘtes libre d'Ă©crire le vĂŽtre avec un API similaires ou diffĂ©rentes). Étant donnĂ© que la bibliothĂšque standard sort.Sort et sort.Stable fonctionnent toutes deux sur la valeur transmise par la liste d'arguments, la valeur que vous rĂ©cupĂ©rez est la mĂȘme que la valeur avec laquelle vous avez commencĂ© - et donc, nĂ©cessairement, le type vous obtenez en retour est le mĂȘme que le type avec lequel vous avez commencĂ©. C'est parfaitement sĂ»r, et le compilateur fait tout le travail pour dĂ©duire si votre type implĂ©mente l'interface nĂ©cessaire et est capable de _au moins_ autant d'optimisations au moment de la compilation qu'il serait possible avec une fonction gĂ©nĂ©rique sort<T> .

Pour échanger des trucs, le one-liner x, y = y, x . Encore une fois, aucune assertion de type, conversion d'interface ou réflexion n'est nécessaire. Il s'agit simplement d'échanger deux valeurs. Le compilateur peut facilement s'assurer que vos opérations sont de type sécurisé.

Il n'y a pas un seul outil spĂ©cifique que je considĂ©rerais comme une meilleure solution que les gĂ©nĂ©riques dans tous les cas, mais pour tout problĂšme donnĂ©, les gĂ©nĂ©riques sont censĂ©s rĂ©soudre, je pense qu'il existe une meilleure solution. Je peux me tromper ici; Je suis toujours ouvert Ă  voir un exemple de quelque chose que les gĂ©nĂ©riques peuvent faire lĂ  oĂč toutes les solutions existantes auraient Ă©tĂ© terribles. Mais si je peux y faire des trous, alors ce n'est pas un de ces exemples.

Je n'aime pas beaucoup non plus le package xerrors , mais xerrors.As ne me semble pas non idiomatique ; c'est une API trĂšs similaire Ă  json.Unmarshal , aprĂšs tout. Il peut avoir besoin d'une meilleure documentation et/ou d'un exemple de code, mais sinon, tout va bien.

Mais non, sort et min sont, Ă  eux seuls, des exemples assez terribles. Le premier existe dĂ©jĂ  dans Go et est parfaitement composable, le tout sans avoir besoin de gĂ©nĂ©riques. Ce dernier est dans son sens le plus large l'un des rĂ©sultats de sort (que nous avons dĂ©jĂ  rĂ©solu), et dans les cas oĂč une solution plus spĂ©cialisĂ©e ou optimisĂ©e pourrait ĂȘtre nĂ©cessaire, vous Ă©cririez quand mĂȘme la solution spĂ©cialisĂ©e plutĂŽt que de vous appuyer sur gĂ©nĂ©riques. Encore une fois, il n'y a pas de gĂ©nĂ©rateurs, d'interface{}, de rĂ©flexion ou de fonctionnalitĂ©s de langage "obscures" utilisĂ©es dans le package sort de la bibliothĂšque standard. Il existe des interfaces non vides (qui sont bien dĂ©finies dans l'API afin que vous obteniez des erreurs de compilation si vous les utilisez de maniĂšre incorrecte, dĂ©duites afin que vous n'ayez pas besoin de transtypages et vĂ©rifiĂ©es au moment de la compilation afin que vous n'ayez pas besoin affirmations). Il peut y avoir un passe-partout _si_ la collection que vous triez est une tranche, mais s'il s'agit d'une structure (comme celle reprĂ©sentant le nƓud racine d'un arbre de recherche binaire ?), Vous pouvez faire en sorte que cela satisfasse le sort.Interface aussi, donc c'est en fait _plus_ flexible qu'une collection gĂ©nĂ©rique.

@jesse-amano

mon point est qu'il n'y a pas besoin de génériques quand une meilleure solution existe déjà dans la langue

Je pense en quelque sorte qu'une meilleure solution est vraiment relativement basée sur la façon dont vous la voyez. Si nous avons un meilleur langage, nous pourrions avoir une meilleure solution, c'est pourquoi nous voulons améliorer ce langage. Par exemple, si un meilleur générique existe, nous pourrions avoir un meilleur sort dans notre stdlib, au moins la façon actuelle d'implémenter l'interface de tri n'est pas une bonne expérience utilisateur pour moi, je dois encore taper beaucoup de code similaire dont je suis convaincu que nous pourrions l'abstraire.

@jesse-amano

Je pense que l'avantage du package de tri de la bibliothĂšque standard est qu'il ne fonctionne pas uniquement sur les tranches.

Je suis d'accord, j'aime le tri standard.

Le premier existe déjà dans Go et est parfaitement composable, le tout sans avoir besoin de génériques.

C'est une fausse dichotomie. Les interfaces dans Go sont dĂ©jĂ  une forme de gĂ©nĂ©riques. Le mĂ©canisme n'est pas la chose elle-mĂȘme. Regardez au-delĂ  de la syntaxe et voyez l'objectif, qui est la capacitĂ© d'exprimer n'importe quel algorithme de maniĂšre gĂ©nĂ©rique sans limitations. L'abstraction d'interface de 'sort' est gĂ©nĂ©rique, elle permet de trier n'importe quel type de donnĂ©es pouvant implĂ©menter les mĂ©thodes requises. La notation est simplement diffĂ©rente. Nous pourrions Ă©crire :

f<T>(x: T) requires Sortable(T)

Ce qui signifierait que le type 'T' doit implĂ©menter l'interface 'Sortable'. Dans 'Go' cela pourrait ĂȘtre Ă©crit func f(x Sortable) . Ainsi, au moins l'application des fonctions dans Go peut ĂȘtre gĂ©rĂ©e de maniĂšre gĂ©nĂ©rique, mais il existe des opĂ©rations qui ne peuvent pas aimer l'arithmĂ©tique ou le dĂ©rĂ©fĂ©rencement. Go s'en sort plutĂŽt bien, car les interfaces peuvent ĂȘtre considĂ©rĂ©es comme des prĂ©dicats de type, mais Go n'a pas de rĂ©ponse pour les relations sur les types.

Il est facile de voir les limites avec Go, considérez :

func merge(x, y Sortable)

oĂč nous allons fusionner deux choses triables, cependant Go ne nous laisse pas imposer que ces deux choses doivent ĂȘtre identiques. Comparez cela avec :

merge<T>(x: T, y: T) requires Sortable(T)

Ici, nous sommes clairs que nous fusionnons deux types triables qui sont identiques. 'Go' supprime les informations de type sous-jacentes et traite simplement tout ce qui est "triable" comme identique.

Essayons un meilleur exemple : disons que je veux écrire un arbre rouge/noir qui peut contenir n'importe quel type de données, en tant que bibliothÚque, afin que d'autres personnes puissent l'utiliser.

Les interfaces dans Go sont déjà une forme de génériques.

Si tel est le cas, ce problĂšme peut ĂȘtre clos comme dĂ©jĂ  rĂ©solu, car la dĂ©claration d'origine Ă©tait :

Ce numéro propose que Go prenne en charge une certaine forme de programmation générique.

L'Ă©quivoque rend un mauvais service Ă  toutes les parties. Les interfaces sont en effet _une_ forme de programmation gĂ©nĂ©rique, et elles ne rĂ©solvent en effet pas nĂ©cessairement, Ă  elles seules, tous les problĂšmes que d'autres formes de programmation gĂ©nĂ©rique peuvent rĂ©soudre. Alors, pour simplifier, permettons Ă  tout problĂšme qui peut ĂȘtre rĂ©solu avec des outils en dehors du champ d'application de cette proposition/problĂšme d'ĂȘtre considĂ©rĂ© comme "rĂ©solu sans gĂ©nĂ©riques". (Je crois qu'une majoritĂ© Ă©crasante de problĂšmes solubles rencontrĂ©s dans le monde rĂ©el, sinon tous, sont dans cet ensemble, mais c'est juste pour s'assurer que nous parlons tous la mĂȘme langue.)

Considérez : func merge(x, y Sortable)

Je ne comprends pas pourquoi la fusion de deux choses triables (ou des choses qui implĂ©mentent sort.Interface ) serait en quelque sorte diffĂ©rente de la fusion de deux collections _en gĂ©nĂ©ral_. Pour les tranches, c'est append ; pour les cartes, c'est for k, v := range m { n[k] = v } ; et pour les structures de donnĂ©es plus complexes, il existe nĂ©cessairement des stratĂ©gies de fusion plus complexes en fonction de la structure (dont le contenu peut ĂȘtre nĂ©cessaire pour mettre en Ɠuvre certaines mĂ©thodes dont la structure a besoin). En supposant que vous parliez d'un algorithme de tri plus compliquĂ© qui partitionne et choisit des sous-algorithmes pour les partitions avant de les fusionner, ce dont vous avez besoin n'est pas que les partitions soient "triables", mais plutĂŽt une sorte de garantie que vos partitions sont dĂ©jĂ  _triĂ©s_ avant de fusionner. C'est un type de problĂšme trĂšs diffĂ©rent, et ce n'est pas un problĂšme que la syntaxe du modĂšle aide Ă  rĂ©soudre de maniĂšre Ă©vidente ; naturellement, vous voudriez des tests unitaires assez rigoureux pour garantir la fiabilitĂ© de votre ou vos algorithmes de tri par fusion, mais vous ne voudriez sĂ»rement pas exposer une API _exportĂ©e_ qui surcharge le dĂ©veloppeur avec ce genre de choses.

Vous soulevez un point intĂ©ressant sur le fait que Go n'a pas un bon moyen de vĂ©rifier si deux valeurs sont du mĂȘme type sans rĂ©flexion, changement de type, etc. J'ai l'impression que l'utilisation interface{} est une solution parfaitement acceptable dans le cas de conteneurs Ă  usage gĂ©nĂ©ral (par exemple, une liste liĂ©e circulaire) car le passe-partout impliquĂ© dans l'emballage de l'API pour la sĂ©curitĂ© de type est absolument trivial :

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
}

J'ai du mal Ă  imaginer pourquoi ce passe-partout serait un problĂšme, mais si c'est le cas, une alternative raisonnable pourrait ĂȘtre un modĂšle (text/). Vous pouvez annoter les types pour lesquels vous souhaitez dĂ©finir des piles avec un commentaire //go:generate stackify MyType github.com/me/myproject/mytype et laisser go generate produire le passe-partout pour vous. Tant que cmd/stackify/stackify_test.go l'essaie avec au moins une structure et au moins un type intĂ©grĂ©, et qu'il compile et passe, je ne vois pas pourquoi ce serait un problĂšme - et c'est probablement assez proche Ă  ce que n'importe quel compilateur aurait fini par faire "sous le capot" si vous aviez dĂ©fini un modĂšle. La seule diffĂ©rence est que les erreurs sont plus utiles car elles sont moins denses.

(Il peut aussi y avoir des cas oĂč nous voulons un _quelque chose_ gĂ©nĂ©rique qui se soucie plus du fait que deux choses soient du mĂȘme type que de leur comportement, qui n'entrent pas dans la catĂ©gorie "conteneurs de trucs". Ce serait trĂšs intĂ©ressant, mais l'ajout d'une syntaxe de construction de modĂšle gĂ©nĂ©rique au langage n'est peut-ĂȘtre pas la seule solution possible disponible.)

En supposant que le passe-partout _n'est pas_ un problÚme, je suis intéressé à résoudre le problÚme de la création d'un arbre rouge/noir aussi facile à utiliser pour les appelants que des packages comme sort ou encoding/json . Je vais certainement échouer parce que... eh bien, je ne suis pas trÚs intelligent. Mais je suis ravi de savoir à quel point je pourrais m'en approcher.

Edit: Les dĂ©buts d'un exemple peuvent ĂȘtre vus ici , bien qu'il soit loin d'ĂȘtre complet (le mieux que je puisse faire en quelques heures). Bien sĂ»r, il existe Ă©galement d'autres tentatives de structures de donnĂ©es similaires.

@jesse-amano

Si tel est le cas, ce problĂšme peut ĂȘtre fermĂ© comme dĂ©jĂ  > rĂ©solu, car la dĂ©claration d'origine Ă©tait :

Ce n'est pas seulement que les interfaces _sont_ une forme de gĂ©nĂ©riques, mais que l'amĂ©lioration de l'approche des interfaces peut nous mener jusqu'aux gĂ©nĂ©riques. Par exemple, les interfaces multi-paramĂštres (oĂč vous pouvez avoir plus d'un "rĂ©cepteur") autoriseraient les relations sur les types. Permettre aux interfaces de remplacer les opĂ©rateurs tels que l'addition et le dĂ©rĂ©fĂ©rencement supprimerait le besoin de toute autre forme de contrainte sur les types. Les interfaces _peuvent_ ĂȘtre toutes les contraintes de type dont vous avez besoin, si elles sont conçues avec une comprĂ©hension du point final de gĂ©nĂ©riques entiĂšrement gĂ©nĂ©raux.

Les interfaces sont sĂ©mantiquement similaires aux classes de types de Haskell et aux traits de Rust qui _rĂ©solvent_ ces problĂšmes gĂ©nĂ©riques. Les classes de type et les traits rĂ©solvent tous les mĂȘmes problĂšmes gĂ©nĂ©riques que les modĂšles C++, mais de maniĂšre sĂ©curisĂ©e (mais peut-ĂȘtre pas toutes les utilisations de la mĂ©ta-programmation, ce qui, je pense, est une bonne chose).

J'ai du mal Ă  imaginer pourquoi ce passe-partout serait un problĂšme, mais si c'est le cas, une alternative raisonnable pourrait ĂȘtre un modĂšle (text/).

Personnellement, je n'ai pas de problÚme avec autant de passe-partout, mais je comprends le désir de ne pas avoir de passe-partout du tout, en tant que programmeur, c'est ennuyeux et répétitif, et c'est exactement le genre de tùche que nous écrivons pour éviter. Donc, encore une fois, personnellement, je pense qu'écrire une implémentation pour une interface/classe de type "pile" est exactement la _bonne_ façon de rendre votre type de données "empilable".

Il existe deux limitations avec Go qui entravent la programmation gĂ©nĂ©rique ultĂ©rieure. Le problĂšme d'Ă©quivalence de "type", par exemple la dĂ©finition de fonctions mathĂ©matiques de sorte que le rĂ©sultat et tous les arguments doivent ĂȘtre identiques. On pourrait imaginer :

mul<T>(x, y T) T requires Addable(T) {
    r := 0
    for i := 0; i < y; ++i  {
        r = r + x
    }
    return r
}

Pour satisfaire les contraintes sur '+', nous devons nous assurer que x et y sont numĂ©riques, mais aussi tous les deux du mĂȘme type sous-jacent.

L'autre est la limitation des interfaces à un seul type de "récepteur". Cette limitation signifie que vous n'avez pas à taper le passe-partout ci-dessus une fois (ce qui, je pense, est raisonnable), mais pour chaque type différent que vous souhaitez mettre dans MyStack. Ce que nous voulons, c'est déclarer le type contenu dans le cadre de l'interface :

type Stack<T> interface {...}

Cela permettrait, entre autres, de dĂ©clarer une implĂ©mentation paramĂ©trique dans T afin que nous puissions mettre n'importe quel T dans MyStack en utilisant l'interface Stack, tant que toutes les utilisations de Push et Pop sur la mĂȘme instance de MyStack fonctionne sur le mĂȘme type de 'valeur'.

Avec ces deux modifications, nous devrions ĂȘtre en mesure de crĂ©er un arbre rouge/noir gĂ©nĂ©rique. Cela devrait ĂȘtre possible sans eux, mais comme le Stack, vous devrez dĂ©clarer une nouvelle instance de l'interface pour chaque type que vous souhaitez mettre dans l'arbre rouge/noir.

De mon point de vue, les deux extensions ci-dessus pour les interfaces sont tout ce qui est nécessaire pour que Go prenne pleinement en charge les "génériques".

@jesse-amano
En regardant l'exemple de l'arbre rouge/noir, ce que nous voulons vraiment génériquement, c'est la définition d'une 'Carte' l'arbre rouge/noir n'est qu'une implémentation possible. En tant que tel, nous pourrions nous attendre à une interface comme celle-ci :

type Map<Key, Value> interface {
   put(x Key, y Value) 
   get(x Key) Value
}

Ensuite, l'arbre rouge/noir pourrait ĂȘtre fourni comme implĂ©mentation. IdĂ©alement, nous voulons Ă©crire du code qui ne dĂ©pend pas de l'implĂ©mentation, vous pouvez donc fournir une table de hachage, ou un arbre rouge-noir, ou un BTree. Nous Ă©crirons alors notre code :

f<K, V, T>(index T) T requires Map<K, V> {
   ...
}

Maintenant, quel que soit f , cela peut fonctionner indĂ©pendamment de l'implĂ©mentation de Map, f peut ĂȘtre une fonction de bibliothĂšque Ă©crite par quelqu'un d'autre, qui n'a pas besoin de savoir si mon application utilise un red/ arbre noir ou une carte de hachage.

Dans le go tel qu'il est actuellement, nous aurions besoin de définir une carte spécifique comme celle-ci :

type MapIntString interface {
   put(x Int, y String)
   get(x Int) String
}

Ce qui n'est pas si mal, mais cela signifie que la fonction 'library' f doit ĂȘtre Ă©crite pour chaque combinaison possible de types de clĂ© et de valeur si nous voulons pouvoir l'utiliser dans une application oĂč nous ne le faisons pas. t connaĂźtre les types des clĂ©s et des valeurs lorsque nous Ă©crivons la bibliothĂšque.

Bien que je sois d'accord avec le dernier commentaire de @keean , la difficultĂ© est d' Ă©crire un arbre rouge/noir en Go qui implĂ©mente une interface connue, comme par exemple celle qui vient d'ĂȘtre suggĂ©rĂ©e.

Sans génériques, il est bien connu que pour implémenter des conteneurs indépendants du type, il faut utiliser interface{} et/ou la réflexion - malheureusement, les deux approches sont lentes et sujettes aux erreurs.

@keean

Ce n'est pas seulement que les interfaces sont une forme de génériques, mais que l'amélioration de l'approche des interfaces peut nous mener jusqu'aux génériques.

Je ne considÚre aucune des propositions liées à cette question, à ce jour, comme une amélioration. Il semble assez peu controversé de dire qu'ils sont tous défectueux d'une maniÚre ou d'une autre. Je crois que ces défauts l'emportent largement sur tout avantage, et bon nombre des avantages _réclamés_ sont en fait déjà pris en charge par les fonctionnalités existantes. Ma conviction est basée sur l'expérience pratique, pas sur la spéculation, mais elle reste anecdotique.

Personnellement, je n'ai pas de problÚme avec autant de passe-partout, mais je comprends le désir de ne pas avoir de passe-partout du tout, en tant que programmeur, c'est ennuyeux et répétitif, et c'est exactement le genre de tùche que nous écrivons pour éviter.

Je ne suis pas d'accord avec ça non plus. En tant que professionnel rĂ©munĂ©rĂ©, mon objectif est de rĂ©duire les coĂ»ts temps/effort _pour moi et les autres_, tout en augmentant les gains de mon employeur, quelle que soit leur mesure. Une tĂąche « ennuyeuse » n'est mauvaise que si elle prend aussi du temps ; cela ne peut pas ĂȘtre difficile, sinon ce ne serait pas ennuyeux. Si cela ne prend qu'un peu de temps au dĂ©part, mais Ă©limine les futures activitĂ©s chronophages et/ou met le produit en production plus tĂŽt, cela en vaut toujours la peine.

Ensuite, l'arbre rouge/noir pourrait ĂȘtre fourni comme implĂ©mentation.

Je pense avoir fait des progrĂšs dĂ©cents ces derniers jours sur l'implĂ©mentation d'un arbre rouge/noir (c'est inachevĂ© ; il manque mĂȘme un fichier readme) mais je crains d'avoir dĂ©jĂ  Ă©chouĂ© Ă  illustrer mon propos si ce n'est pas abondamment clair que mon but n'est pas de travailler vers une interface mais plutĂŽt de travailler vers une implĂ©mentation. J'Ă©cris un arbre rouge/noir, et bien sĂ»r je veux qu'il soit _utile_, mais je me fiche des choses _spĂ©cifiques_ que d'autres dĂ©veloppeurs pourraient vouloir utiliser.

Je sais que l'interface minimale requise par une bibliothĂšque d'arbres rouges/noirs est celle oĂč un ordre "faible" existe sur ses Ă©lĂ©ments, donc j'ai besoin de quelque chose _comme_ une fonction nommĂ©e Less(v interface{}) bool , mais si l'appelant a une mĂ©thode qui fait quelque chose de similaire mais n'est pas nommĂ© Less(v interface{}) bool , c'est Ă  eux d'Ă©crire les wrappers/shims passe-partout pour le faire fonctionner.

Lorsque vous accĂ©dez aux Ă©lĂ©ments contenus par l'arbre rouge/noir, vous obtenez interface{} , mais si vous ĂȘtes prĂȘt Ă  faire confiance Ă  ma garantie que la bibliothĂšque fournie _est_ un arbre rouge/noir, je ne comprends pas pourquoi vous le feriez Ne croyez pas que les types d'Ă©lĂ©ments que vous mettez seront exactement les types d'Ă©lĂ©ments que vous sortez. Si vous _faites_ confiance Ă  ces deux garanties, alors la bibliothĂšque n'est pas du tout sujette aux erreurs. Écrivez (ou collez) simplement une douzaine de lignes de code pour couvrir les assertions de type.

Vous avez maintenant une bibliothĂšque parfaitement sĂ»re (encore une fois, en supposant que le niveau de confiance que vous devriez ĂȘtre prĂȘt Ă  donner pour tĂ©lĂ©charger la bibliothĂšque en premier lieu ne dĂ©passe pas le niveau de confiance) qui a mĂȘme les noms de fonction exacts que vous souhaitez. C'est important. Dans un Ă©cosystĂšme de style Java oĂč les auteurs de bibliothĂšques se plient en quatre pour coder par rapport Ă  une dĂ©finition d'interface _exacte_ (ils doivent presque le faire, car le langage l'applique au moyen d'une syntaxe class MyClassImpl extends AbstractMyClass implements IMyClass ) et il y a un tas de bureaucratie supplĂ©mentaire, vous devez faire tout votre possible pour crĂ©er une façade pour que la bibliothĂšque tierce s'adapte aux normes de codage de votre organisation (qui est la mĂȘme quantitĂ© de passe-partout, sinon plus), ou bien permettre que cela soit une "exception" Ă  les normes de codage de votre organisation (et Ă©ventuellement votre organisation a autant d'exceptions dans ses normes que dans ses bases de code), ou bien renoncer Ă  utiliser une bibliothĂšque parfaitement bonne (en supposant, pour les besoins de l'argumentation, que la bibliothĂšque est rĂ©ellement bonne).

Idéalement, nous voulons écrire du code qui ne dépend pas de l'implémentation, vous pouvez donc fournir une table de hachage, ou un arbre rouge-noir, ou un BTree.

Je suis d'accord avec cet idéal, mais je pense que Go le satisfait déjà. Avec une interface comme :

type MyStorage interface {
  Get(KeyType) (ValueType, error)
  Put(KeyType, ValueType) error
}

la seule chose qui manque est la possibilité de paramétrer ce que sont KeyType et ValueType , et je ne suis pas convaincu que ce soit particuliÚrement important.

En tant que responsable (hypothĂ©tique) d'une bibliothĂšque d'arbres rouges/noirs, peu m'importe vos types. Je vais simplement utiliser interface{} pour toutes mes fonctions principales qui gĂšrent "certaines donnĂ©es", et _peut-ĂȘtre_ fournir des exemples de fonctions exportĂ©es qui vous permettent de les utiliser plus facilement avec des types courants comme string et int . Mais c'est Ă  l'appelant de fournir la couche extrĂȘmement fine autour de cette API pour la rendre sĂ»re pour tous les types personnalisĂ©s qu'ils pourraient finir par dĂ©finir. Mais la seule chose importante Ă  propos de l'API que je fournis est qu'elle permet Ă  l'appelant de faire tout ce qu'il pourrait s'attendre Ă  ce qu'un arbre rouge/noir soit capable de faire.

En tant qu'appelant (hypothĂ©tique) d'une bibliothĂšque d'arbres rouges/noirs, je le veux probablement juste pour un stockage et un temps de recherche rapides. Je me fiche que ce soit un arbre rouge/noir. Je me soucie de pouvoir Get des choses Ă  partir de ça et Put des choses dedans, et – surtout – je me soucie de ce que sont ces choses. Si la bibliothĂšque n'offre pas de fonctions nommĂ©es Get et Put , ou ne peut pas interagir parfaitement avec les types que j'ai dĂ©finis, cela m'est Ă©gal tant que c'est facile pour moi pour Ă©crire moi-mĂȘme les mĂ©thodes Get et Put et faire en sorte que mon propre type satisfasse l'interface dont la bibliothĂšque a besoin pendant que j'y suis. Si ce n'est pas facile, je trouve gĂ©nĂ©ralement que c'est la faute de l'auteur de la bibliothĂšque, pas celle du langage, mais encore une fois, il est possible qu'il y ait des contre-exemples dont je ne suis tout simplement pas au courant.

Au fait, le code pourrait s'emmĂȘler beaucoup plus s'il n'Ă©tait pas comme ça. Comme vous le dites, il existe de nombreuses implĂ©mentations possibles d'un magasin clĂ©/valeur. Passer autour d'un "concept" abstrait de stockage clĂ©/valeur masque la complexitĂ© de la façon dont le stockage clĂ©/valeur est accompli, et un dĂ©veloppeur de mon Ă©quipe pourrait choisir le mauvais pour sa tĂąche (y compris une future version de moi-mĂȘme dont la connaissance de la clĂ© /l'implĂ©mentation du stockage de valeurs a dĂ©passĂ© la mĂ©moire !). L'application ou ses tests unitaires peuvent, malgrĂ© tous nos efforts en matiĂšre de rĂ©vision du code, contenir un code subtil dĂ©pendant de l'implĂ©mentation qui cesse de fonctionner de maniĂšre fiable lorsque certains magasins clĂ©/valeur dĂ©pendent d'une connexion Ă  une base de donnĂ©es et d'autres non. C'est pĂ©nible lorsque le rapport d'erreur est accompagnĂ© d'une grande trace de pile, et que la seule ligne de la trace de pile faisant rĂ©fĂ©rence Ă  quelque chose dans la base de code _real_ pointe sur une ligne qui utilise une valeur d'interface, tout cela parce que l'implĂ©mentation de cette interface est du code gĂ©nĂ©rĂ© (qui vous ne pouvez voir que dans le runtime) au lieu d'une structure ordinaire, avec des mĂ©thodes renvoyant des valeurs d'erreur lisibles.

@jesse-amano
Je suis d'accord avec vous, et j'aime la façon "Go" de faire les choses oĂč le code "utilisateur" dĂ©clare une interface qui rĂ©sume son fonctionnement, puis vous Ă©crivez l'implĂ©mentation de cette interface pour la bibliothĂšque/dĂ©pendance. C'est Ă  l'envers de la façon dont la plupart des autres langages pensent les interfaces. mais une fois que vous l'obtenez, il est trĂšs puissant.

J'aimerais toujours voir les choses suivantes dans un langage générique :

  • types paramĂ©triques, comme : RBTree<Int, String> car cela renforcerait la sĂ©curitĂ© des types des collections d'utilisateurs.
  • variables de type, comme: f<T>(x, y T) T , car cela est nĂ©cessaire pour dĂ©finir des familles de fonctions liĂ©es comme l'addition, la soustraction, etc. oĂč la fonction est polymorphe, mais nous exigeons que tous les arguments soient du mĂȘme type sous-jacent.
  • contraintes de type, comme : f<T: Addable>(x, y T) T , qui applique des interfaces aux variables de type, car une fois que nous avons introduit les variables de type, nous avons besoin d'un moyen de contraindre ces variables de type au lieu de traiter Addable comme un type. Si nous considĂ©rons Addable comme un type et Ă©crivons f(x, y Addable) Addable , nous n'avons aucun moyen de savoir si les types sous-jacents originaux de x et y sont les mĂȘmes que entre eux ou le type retournĂ©.
  • interfaces multi-paramĂštres, comme : type<K, V> Map<K, V> interface {...} , qui pourraient ĂȘtre utilisĂ©es comme merge<K, V, T: Map<K, V>>(x, y T) T qui nous permettent de dĂ©clarer des interfaces paramĂ©trĂ©es non seulement par le type de conteneur, mais dans ce cas Ă©galement par la clĂ© et la valeur types de carte.

Je pense que chacun d'entre eux augmenterait le pouvoir abstrait du langage.

Tout progrÚs ou calendrier à ce sujet ?

@leaxoy Il y a une conférence prévue sur "Generics in Go" par @ianlancetaylor à GopherCon . Je m'attendrais à en savoir plus sur l'état actuel des choses dans cette conférence.

@griesemer Merci pour ce lien.

@keean J'aimerais aussi voir la clause Where de Rust ici, qui peut ĂȘtre une amĂ©lioration de votre proposition type constraints . Il permet d'utiliser le systĂšme de type pour contraindre un comportement tel que "dĂ©marrer une transaction avant la requĂȘte" Ă  vĂ©rifier par rapport au type sans rĂ©flexion d'exĂ©cution. Regardez cette vidĂ©o dessus : https://www.youtube.com/watch?v=jSpio0x7024

@jadbox dĂ©solĂ© si mon explication n'Ă©tait pas claire, mais la clause "oĂč" est presque exactement ce que je proposais. Les choses aprĂšs 'oĂč' dans rust sont des contraintes de type, mais je pense que j'ai utilisĂ© le mot-clĂ© 'requires' dans un article prĂ©cĂ©dent Ă  la place. Tout cela a Ă©tĂ© fait dans Haskell il y a au moins une dĂ©cennie, sauf que Haskell utilise l'opĂ©rateur '=>' dans les signatures de type pour indiquer les contraintes de type, mais c'est le mĂȘme mĂ©canisme sous-jacent.

J'ai laissé cela en dehors de mon post récapitulatif ci-dessus parce que je voulais garder les choses simples, mais j'aimerais quelque chose comme ça :

merge<K, V, T>(x, y T) T requires T: Map<K, V>

Mais cela n'ajoute vraiment rien Ă  ce que vous pouvez faire Ă  part une syntaxe qui peut ĂȘtre plus lisible pour les longs ensembles de contraintes. Vous pouvez reprĂ©senter tout ce que vous pouvez avec la clause 'where' en mettant la contrainte aprĂšs qu'ils aient tapĂ© la variable dans la dĂ©claration initiale comme ceci :

merge<K, V, T: Map<K, V>>(x, y T) T

À condition que vous puissiez rĂ©fĂ©rencer les variables de type avant qu'elles ne soient dĂ©clarĂ©es, vous pouvez y mettre toutes les contraintes et vous utiliseriez une liste sĂ©parĂ©e par des virgules pour appliquer plusieurs contraintes Ă  la mĂȘme variable de type.

Donc, autant que je sache, le seul avantage d'une clause 'where'/'requires' est que toutes les variables de type sont déjà déclarées à l'avance, ce qui peut faciliter la tùche de l'analyseur et de l'inférence de type.

Est-ce toujours le bon fil de discussion pour les commentaires/discussions sur la proposition actuelle/derniÚre de fonctionnement de Go 2 Generics qui a été récemment annoncée ?

Bref, j'aime beaucoup la direction que prend la proposition en gĂ©nĂ©ral et le mĂ©canisme des contrats en particulier. Mais je suis prĂ©occupĂ© par ce qui semble ĂȘtre une hypothĂšse unique selon laquelle les paramĂštres gĂ©nĂ©riques au moment de la compilation doivent (toujours) ĂȘtre des paramĂštres de type. J'ai Ă©crit quelques commentaires sur ce problĂšme ici:

Seuls les paramÚtres de type sont-ils suffisamment génériques pour les génériques Go 2 ?

Certes, les commentaires ici sont acceptables, mais en général, je ne pense pas que les problÚmes de GitHub soient un bon format de discussion, car ils ne prévoient aucune sorte de thread. Je pense que les listes de diffusion sont meilleures.

Je ne pense pas qu'il soit encore clair à quelle fréquence les gens voudront paramétrer des fonctions sur des valeurs constantes. Le cas le plus évident serait pour les dimensions du tableau - mais vous pouvez déjà le faire en passant le type de tableau souhaité comme argument de type. En dehors de ce cas, que gagnons-nous vraiment en passant un const comme argument de compilation plutÎt que comme argument d'exécution ?

Go offre dĂ©jĂ  de nombreuses et excellentes façons de rĂ©soudre les problĂšmes et nous ne devrions jamais ajouter quoi que ce soit de nouveau Ă  moins qu'il ne rĂ©solve un trĂšs gros problĂšme et une lacune, ce que cela ne fait clairement pas, et mĂȘme dans de telles circonstances, la complexitĂ© supplĂ©mentaire qui suit est un trĂšs prix Ă©levĂ© Ă  payer.

Go est unique exactement à cause de la façon dont il est. S'il n'est pas cassé , n'essayez pas de le réparer !

Les personnes qui ne sont pas satisfaites de la façon dont Go a été conçu devraient utiliser l'un des nombreux autres langages qui possÚdent déjà cette complexité supplémentaire et ennuyeuse.

Go est unique exactement à cause de la façon dont il est. S'il n'est pas cassé, n'essayez pas de le réparer !

Il est cassĂ©, il devrait donc ĂȘtre rĂ©parĂ©.

Il est cassĂ©, il devrait donc ĂȘtre rĂ©parĂ©.

Il se peut que cela ne fonctionne pas comme vous le pensez, mais une langue ne le peut jamais. Il n'est certainement pas cassé. Compte tenu des informations disponibles et du débat, prendre le temps de prendre une décision éclairée et sensée est toujours la meilleure option. De nombreuses autres langues ont souffert, à mon avis, en raison de l'ajout de plus en plus de fonctionnalités pour résoudre de plus en plus de problÚmes potentiels. N'oubliez pas que "non" est temporaire, "oui" est pour toujours.

Ayant participĂ© Ă  des mĂ©ga-problĂšmes passĂ©s, puis-je suggĂ©rer qu'un canal soit ouvert sur Gopher Slack pour ceux qui veulent en discuter, le problĂšme est temporairement verrouillĂ©, puis des heures sont affichĂ©es lorsque le problĂšme sera dĂ©gelĂ© pour quiconque souhaite consolider le discussion depuis Slack ? Les problĂšmes Github ne fonctionnent plus comme un forum une fois que le lien redoutĂ© "478 Ă©lĂ©ments cachĂ©s Charger plus
" est entrĂ©.

puis-je suggérer qu'un canal soit ouvert sur Gopher Slack pour ceux qui veulent en discuter
Les listes de diffusion sont meilleures car elles fournissent une archive consultable. Un rĂ©sumĂ© peut encore ĂȘtre postĂ© sur cette question.

Ayant participé à des méga-questions passées, puis-je suggérer qu'une chaßne soit ouverte sur Gopher Slack pour ceux qui veulent en discuter

Veuillez ne pas dĂ©placer entiĂšrement la discussion vers des plates-formes fermĂ©es. Si n'importe oĂč, golang-nuts est disponible pour tous (ish? Je ne sais pas si cela fonctionne sans compte Google non plus en fait, mais au moins c'est une mĂ©thode de communication standard que tout le monde a ou peut obtenir) et il devrait ĂȘtre dĂ©placĂ© lĂ -bas . GitHub est dĂ©jĂ  assez mauvais, mais j'accepte Ă  contrecƓur que nous soyons coincĂ©s avec lui pour la communication, tout le monde ne peut pas obtenir un compte Slack ou utiliser ses terribles clients.

tout le monde ne peut pas obtenir un compte Slack ou utiliser ses terribles clients

Que veut dire « peut » ici ? Y a-t-il de réelles restrictions sur Slack que je ne connais pas ou les gens n'aiment tout simplement pas l'utiliser ? Ce dernier est bien, je suppose, mais certaines personnes boycottent également Github parce qu'elles n'aiment pas Microsoft, donc vous perdez certaines personnes mais en gagnez d'autres.

tout le monde ne peut pas obtenir un compte Slack ou utiliser ses terribles clients

Que veut dire « peut » ici ? Y a-t-il de réelles restrictions sur Slack que je ne connais pas ou les gens n'aiment tout simplement pas l'utiliser ? Ce dernier est bien, je suppose, mais certaines personnes boycottent également Github parce qu'elles n'aiment pas Microsoft, donc vous perdez certaines personnes mais en gagnez d'autres.

Slack est une sociĂ©tĂ© amĂ©ricaine et, en tant que telle, suivra toutes les politiques Ă©trangĂšres imposĂ©es par les États-Unis.

Github a le mĂȘme problĂšme et vient de faire la une des journaux pour avoir expulsĂ© des Iraniens sans avertissement. C'est malheureux, mais Ă  moins que nous n'utilisions Tor ou IPFS ou quelque chose du genre, nous devrons respecter la loi amĂ©ricaine/europĂ©enne pour tout forum de discussion pratique.

Github a le mĂȘme problĂšme et vient de faire la une des journaux pour avoir expulsĂ© des Iraniens sans avertissement. C'est malheureux, mais Ă  moins que nous n'utilisions Tor ou IPFS ou quelque chose du genre, nous devrons respecter la loi amĂ©ricaine/europĂ©enne pour tout forum de discussion pratique.

Oui, nous sommes coincĂ©s avec GitHub et Google Groups. N'ajoutons pas plus de services problĂ©matiques Ă  la liste. De plus, le chat n'est tout simplement pas une bonne archive ; il est dĂ©jĂ  assez difficile de creuser dans ces discussions quand elles sont bien enfilĂ©es et sur des golang-nuts (oĂč elles arrivent directement dans votre boĂźte de rĂ©ception). Slack signifie que si vous n'ĂȘtes pas dans le mĂȘme fuseau horaire que tout le monde, vous devez parcourir des masses d'archives de chat, des non-sĂ©quences, etc. les listes de diffusion signifient que vous l'avez au moins quelque peu organisĂ© en fils de discussion, et les gens ont tendance Ă  prendre plus de temps dans leurs rĂ©ponses afin que vous ne receviez pas des tonnes de commentaires alĂ©atoires laissĂ©s par hasard. De plus, je n'ai tout simplement pas de compte Slack et leurs stupides clients ne fonctionneront sur aucune des machines que j'utilise. Mutt, d'autre part (ou votre client de messagerie de choix, yay standards) fonctionne partout.

Veuillez garder cette question sur les gĂ©nĂ©riques. Le fait que le suivi des problĂšmes GitHub ne soit pas idĂ©al pour les discussions Ă  grande Ă©chelle comme les gĂ©nĂ©riques mĂ©rite d'ĂȘtre discutĂ©, mais pas sur cette question. J'ai marquĂ© plusieurs commentaires ci-dessus comme "hors sujet".

En ce qui concerne le caractĂšre unique de Go : Go a quelques fonctionnalitĂ©s intĂ©ressantes, mais ce n'est pas aussi unique que certains semblent le penser. À titre d'exemples, CLU et Modula-3 ont des objectifs similaires et des gains similaires, et tous deux prennent en charge les gĂ©nĂ©riques sous une forme ou une autre (depuis ~ 1975 dans le cas de CLU !) Ils n'ont pas de support industriel Ă  l'heure actuelle, mais FWIW, il est possible d'obtenir un compilateur travaillant pour les deux.

quelques questions sur la syntaxe, le mot-clé type dans les paramÚtres de type est-il requis ? et serait-il plus logique d'adopter <> pour les paramÚtres de type comme d'autres langages ? Cela pourrait rendre les choses plus lisibles et familiÚres...

Bien que je ne sois pas contre la façon dont il est dans la proposition, il suffit de soumettre cela à l'examen

Ă  la place de:

type Vector(type Element) []Element
var v Vector(int)
func (v *Vector(Element)) Push(x Element) { *v = append(*v, x) }
type VectorInt = Vector(int)

nous pourrions avoir

type Vector<Element> []Element
var v Vector<int>
func (v *Vector<Element>) Push(x Element) { *v = append(*v, x) }
type VectorInt = Vector<int>

La syntaxe <> est mentionnée dans le brouillon, @jnericks (Votre nom d'utilisateur est parfait pour cette discussion...). L'argument principal contre cela est qu'il augmente massivement la complexité de l'analyseur. Plus généralement, cela rend Go un langage beaucoup plus difficile à analyser pour peu d'avantages. La plupart des gens s'accordent à dire que cela améliore la lisibilité, mais il y a un désaccord sur la question de savoir si cela en vaut la peine ou non. Personnellement, je ne pense pas que ce soit le cas.

L'utilisation du mot-clé type est nécessaire pour lever l'ambiguïté. Sinon, il est difficile de faire la différence entre func Example(T)(arg int) {} et func Example(arg int) (int) {} .

J'ai lu la derniÚre proposition concernant les génériques go. tous sont à mon goût sauf la grammaire de la déclaration de contrat.

comme nous le savons, dans go, nous déclarons toujours une structure ou une interface comme celle-ci :

type MyStruct struct {
        a int
        s string
}

type MyInterface inteface {
    Method1() err
    Method2() string
}

mais la déclaration de contrat dans la derniÚre proposition ressemble à ceci :

contract Ordered(T) {
    T int, int8
}

contract G(Node, Edge) {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

Dans ma pensée, la grammaire contractuelle est incompatible dans sa forme avec l'approche traditionnelle. que diriez-vous de la grammaire comme ci-dessous:

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

maintenant la forme du contrat est conforme à la tradition. nous pouvons déclarer un contrat dans un bloc de type avec struct, interface :

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

Ainsi, le "contrat" ​​devient le mot-clĂ© de mĂȘme niveau que struct, interface. La diffĂ©rence est que le contrat est utilisĂ© pour dĂ©clarer le mĂ©ta-type pour le type.

@bigwhite Nous discutons toujours de cette notation. L'argument en faveur de la notation suggĂ©rĂ©e dans le projet de conception est qu'un contrat n'est pas un type (par exemple, on ne peut pas dĂ©clarer une variable d'un type de contrat), et donc un contrat est un nouveau type d'entitĂ© au mĂȘme titre qu'une constante , fonction, variable ou type. L'argument en faveur de votre suggestion est qu'un contrat est simplement un "type type" (ou un mĂ©tatype) et doit donc suivre une notation cohĂ©rente. Un autre argument en faveur de votre suggestion est qu'elle permettrait l'utilisation de littĂ©raux de contrat "anonymes" sans qu'il soit nĂ©cessaire de les dĂ©clarer explicitement. Bref, Ă  mon humble avis ce n'est pas encore rĂ©glĂ©. Mais il est Ă©galement facile de changer en cours de route.

FWIW, CL 187317 prend en charge les deux notations pour le moment (bien que le paramĂštre de contrat doive ĂȘtre Ă©crit avec le contrat), par exemple :

type C contract(X) { ... }

et

contract C (X) { ... }

sont acceptĂ©s et reprĂ©sentĂ©s de la mĂȘme maniĂšre en interne. L'approche la plus cohĂ©rente serait :

type C(type X) contract { ... }

Un contrat n'est pas un type. Ce n'est mĂȘme pas un mĂ©ta-type, puisque les seuls types qu'il
se préoccupe de ses paramÚtres. Il n'y a pas de type de récepteur séparé
dont le contrat pourrait ĂȘtre considĂ©rĂ© comme le mĂ©ta-type.

Go a également des déclarations de fonction :

func Name(args) { body }

que la syntaxe de contrat proposée reflÚte plus directement.

Quoi qu'il en soit, ces types de discussions sur la syntaxe semblent faibles sur la liste des priorités à
ce point. Il est plus important d'examiner la sémantique du brouillon et
comment ils impactent le code, quel type de code peut ĂȘtre Ă©crit en fonction de ceux-ci
la sémantique, et ce que le code ne peut pas.

Edit : Concernant les contrats en ligne, Go a des littĂ©raux de fonction. Je ne vois aucune raison pour laquelle il ne peut pas y avoir de littĂ©raux contractuels. Il y aurait juste un nombre plus limitĂ© d'endroits oĂč ils pourraient apparaĂźtre, car ce ne sont pas des types ou des valeurs.

@stevenblenkinsop Je n'irais pas jusqu'Ă  affirmer qu'un contrat n'est pas un type (ou un mĂ©ta-type). Je pense qu'il y a des arguments trĂšs raisonnables pour les deux points de vue. Par exemple, un contrat Ă  paramĂštre unique qui ne spĂ©cifie que des mĂ©thodes sert essentiellement de « limite supĂ©rieure » pour un paramĂštre de type : tout argument de type valide doit implĂ©menter ces mĂ©thodes. C'est pour cela que nous utilisons habituellement les interfaces. Il peut ĂȘtre trĂšs logique d'autoriser des interfaces dans ces cas au lieu d'un contrat, a) parce que ces cas peuvent ĂȘtre courants ; et b) parce que satisfaire un contrat dans ce cas signifie simplement satisfaire l'interface Ă©noncĂ©e comme un contrat. C'est-Ă -dire qu'un tel contrat agit tout Ă  fait comme un type auquel un autre type est « comparĂ© ».

@griesemer considĂ©rer les contrats comme des types peut conduire Ă  des problĂšmes avec le paradoxe de Russel (comme dans le type de tous les types qui ne sont pas "membres" d'eux-mĂȘmes). Je pense qu'ils sont mieux considĂ©rĂ©s comme des "contraintes sur les types". Si nous considĂ©rons un systĂšme de types comme une forme de "logique", nous pouvons le prototyper en Prolog. Les variables de type deviennent des variables logiques, les types deviennent des atomes et les contrats/contraintes peuvent ĂȘtre rĂ©solus par la programmation logique par contraintes. Tout est trĂšs soignĂ© et non paradoxal. En termes de syntaxe, nous pourrions considĂ©rer un contrat comme une fonction sur des types qui renvoie un boolĂ©en.

@keean Toute interface sert dĂ©jĂ  de "contrainte sur les types", pourtant ce sont des types. Les thĂ©oriciens des types considĂšrent beaucoup les contraintes de types comme des types, d'une maniĂšre trĂšs formelle. Comme je l'ai mentionnĂ© ci- dessus , il existe des arguments raisonnables qui peuvent ĂȘtre avancĂ©s pour l'un ou l'autre point de vue. Il n'y a pas de "paradoxes logiques" ici - en fait, le prototype de travail en cours actuel modĂ©lise un contrat en tant que type en interne car il simplifie les choses pour le moment.

Les interfaces @griesemer dans Go sont des "sous-types" et non des contraintes sur les types. Cependant, je trouve que le besoin de contrats et d'interfaces est un inconvĂ©nient pour la conception de Go, mais il est peut-ĂȘtre trop tard pour changer les interfaces en contraintes de type plutĂŽt qu'en sous-types. J'ai expliquĂ© ci-dessus que les interfaces Go ne doivent pas nĂ©cessairement ĂȘtre des sous-types, mais je ne vois pas beaucoup de soutien pour cette idĂ©e. Cela permettrait aux interfaces et aux contrats d'ĂȘtre la mĂȘme chose - si les interfaces pouvaient Ă©galement ĂȘtre dĂ©clarĂ©es pour les opĂ©rateurs.

Il y a des paradoxes ici, alors soyez prudent, le paradoxe de Girard est le "codage" le plus courant du paradoxe de Russel dans la thĂ©orie des types. La thĂ©orie des types introduit le concept d'univers pour Ă©viter ces paradoxes, et vous n'ĂȘtes autorisĂ© Ă  rĂ©fĂ©rencer des types que dans l'univers 'U' Ă  partir de l'univers 'U+1'. En interne, ces thĂ©ories de type sont implĂ©mentĂ©es en tant que logiques d'ordre supĂ©rieur (par exemple, Elf utilise lambda-prolog). Cela se rĂ©duit Ă  son tour Ă  la rĂ©solution de contraintes pour le sous-ensemble dĂ©cidable de la logique d'ordre supĂ©rieur.

Ainsi, bien que vous puissiez les considérer comme des types, vous devez ajouter un ensemble de restrictions d'utilisation (syntaxiques ou autres) qui vous ramÚnent efficacement aux contraintes sur les types. Personnellement, je trouve plus facile de travailler directement avec les contraintes et d'éviter les deux autres couches d'abstraction, la logique d'ordre supérieur et les types dépendants. Ces abstractions n'ajoutent rien au pouvoir expressif du systÚme de types et nécessitent des rÚgles ou des restrictions supplémentaires pour éviter les paradoxes.

En ce qui concerne le prototype actuel traitant les contraintes comme des types, le danger survient si vous pouvez utiliser ce "type de contrainte" comme un type normal, puis construire un autre "type de contrainte" sur ce type. Vous aurez besoin de vĂ©rifications pour empĂȘcher l'auto-rĂ©fĂ©rence (ce qui est normalement trivial) et les boucles de rĂ©fĂ©rence mutuelle. Ce type de prototype devrait vraiment ĂȘtre Ă©crit en Prolog, car il permet de se concentrer sur les rĂšgles d'implĂ©mentation. Je crois que les dĂ©veloppeurs de Rust ont finalement rĂ©alisĂ© cela il y a quelque temps (voir Chalk).

@griesemer IntĂ©ressant, modĂ©lisez les contrats en tant que types. À partir de mon propre modĂšle mental, je considĂ©rerais les contraintes comme des mĂ©tatypes et les contrats comme une sorte de structure au niveau du type.

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)

Cela me suggÚre que la syntaxe actuelle de style déclaration de type pour les contrats est la plus correcte des deux. Je pense que la syntaxe définie dans le brouillon est encore meilleure, car elle ne nécessite pas de répondre à la question "si c'est un type, à quoi ressemblent ses valeurs".

@stevenblenkinsop vous m'avez perdu, pourquoi passez-vous T à C contract quand il n'est pas utilisé, et qu'est-ce que les lignes var essaient de faire ?

@griesemer merci pour votre réponse. L'un des principes de conception de Go est de "ne fournir qu'une seule façon de faire quelque chose". Il est préférable de ne conserver qu'un seul formulaire de déclaration de contrat. contrat de type C (type X) { ... } est préférable.

@Goodwine J'ai renommĂ© les types pour les distinguer des paramĂštres du contrat. Peut-ĂȘtre que ça aide ? (int, uint; Foo() int).type est destinĂ© Ă  ĂȘtre le mĂ©tatype de tout type ayant un type sous-jacent de int ou uint et qui implĂ©mente Foo() int . var B est destinĂ© Ă  montrer l'utilisation d'un type comme valeur et son affectation Ă  une variable dont le type est un mĂ©tatype (puisqu'un mĂ©tatype est comme un type dont les valeurs sont des types). var C1 est destinĂ© Ă  montrer une variable dont le type est un contrat et Ă  montrer un exemple de quelque chose qui pourrait ĂȘtre assignĂ© Ă  une telle variable. En gros, essayer de rĂ©pondre Ă  la question "si un contrat est un type, Ă  quoi ressemblent ses valeurs ?". Il s'agit de montrer que cette valeur ne semble pas elle-mĂȘme ĂȘtre un type.

J'ai un problĂšme avec les contrats avec plusieurs types.

Vous pouvez l'ajouter ou le laisser pour le contrat de paramĂštre de type, Ă  la fois
type Graph (type Node, Edge) struct { ... }
et
type Graph (type Node, Edge G) struct { ... } sont OK.

Mais que se passe-t-il si je veux seulement ajouter un contrat sur l'un des deux paramĂštres de type ?

contract G(Node, Edge) {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

VS

contract G(Edge) {
    Edge Nodes() (from Node, to Node)
}

@themez C'est dans le brouillon. Vous pouvez utiliser la syntaxe (type T, U comparable(T)) pour contraindre un seul paramĂštre de type, par exemple.

@stevenblenkinsop Je vois, merci.

@themez Cela est arrivĂ© plusieurs fois maintenant. Je pense qu'il y a une certaine confusion du fait que l'utilisation ressemble Ă  un type pour une dĂ©finition de variable. Ce n'est vraiment pas le cas; un contrat est plus un dĂ©tail de la fonction entiĂšre plutĂŽt qu'une dĂ©finition d'argument. Je pense que l'hypothĂšse est que vous Ă©cririez essentiellement un nouveau contrat, potentiellement composĂ© d'autres contrats pour aider Ă  la rĂ©pĂ©tition, pour pratiquement chaque fonction/type gĂ©nĂ©rique que vous crĂ©ez. Des choses comme ce que @stevenblenkinsop a mentionnĂ© sont vraiment lĂ  pour attraper les cas extrĂȘmes oĂč cette hypothĂšse n'a pas de sens.

Du moins, c'est l'impression que j'ai eue, surtout du fait qu'on les appelle des « contrats ».

@keean Je pense que nous interprĂ©tons le mot "contrainte" diffĂ©remment ; Je l'utilise plutĂŽt de maniĂšre informelle. Par dĂ©finition des interfaces, Ă©tant donnĂ© une interface I et une variable x de type I , seules les valeurs avec des types qui implĂ©mentent I peuvent ĂȘtre assignĂ©es Ă  x . Ainsi, I peut ĂȘtre considĂ©rĂ© comme une "contrainte" sur ces types (bien sĂ»r, il existe encore une infinitĂ© de types qui satisfont cette "contrainte"). De mĂȘme, on pourrait utiliser I comme contrainte pour un paramĂštre de type P d'une fonction gĂ©nĂ©rique ; seuls les arguments de type rĂ©els avec des ensembles de mĂ©thodes qui implĂ©mentent I seraient autorisĂ©s. Ainsi, I limite Ă©galement l'ensemble des types d'arguments rĂ©els possibles.

Dans les deux cas, la raison en est de décrire les opérations disponibles (méthodes) à l'intérieur de la fonction. Si le I est utilisé comme type d'un paramÚtre (valeur), nous savons que ce paramÚtre fournit ces méthodes. Si le I est utilisé comme "contrainte" (à la place d'un contrat), nous savons que toutes les valeurs du paramÚtre de type so contraint fournissent ces méthodes. C'est évidemment assez simple.

J'aimerais un exemple concret de la raison pour laquelle cette idée spécifique d'utiliser des interfaces pour des contrats à paramÚtre unique qui ne déclarent que des méthodes "se décompose" sans certaines restrictions, comme vous l'avez mentionné dans votre commentaire .

Comment la proposition de contrats sera-t-elle introduite ? Utiliser le paramÚtre go modules go1.14 ? Une variable d'environnement GO114CONTRACTS ? Les deux? Autre chose..?

Désolé si cela a déjà été abordé, n'hésitez pas à me rediriger là-bas.

Une chose que j'aime particuliĂšrement dans la conception actuelle du brouillon des gĂ©nĂ©riques est qu'elle met l'eau claire entre contracts et interfaces . Je pense que c'est important car les deux concepts sont facilement confondus mĂȘme s'il existe trois diffĂ©rences fondamentales entre eux :

  1. Contracts décrivent les exigences d'un _ensemble_ de types, tandis que interfaces décrivent les méthodes qu'un _seul_ type doit avoir pour le satisfaire.

  2. Contracts peut gĂ©rer les opĂ©rations intĂ©grĂ©es, les conversions, etc. en rĂ©pertoriant les types qui les prennent en charge ; interfaces ne peut traiter que des mĂ©thodes que les types intĂ©grĂ©s eux-mĂȘmes n'ont pas.

  3. Quoi qu'ils soient en termes thĂ©oriques de type, contracts ne sont pas des types au sens oĂč nous les considĂ©rons normalement dans Go, c'est-Ă -dire que vous ne pouvez pas dĂ©clarer des variables de types contract et leur donner une valeur. D'autre part, interfaces sont des types, vous pouvez dĂ©clarer des variables de ces types et leur attribuer les valeurs appropriĂ©es.

Bien que je puisse voir le sens d'un contract , qui nĂ©cessite un paramĂštre de type unique pour avoir certaines mĂ©thodes, ĂȘtre reprĂ©sentĂ© Ă  la place par un interface (c'est quelque chose que j'ai mĂȘme prĂ©conisĂ© dans mon propre passĂ© propositions), je pense maintenant que ce serait une dĂ©cision malheureuse, car cela brouillerait Ă  nouveau les eaux entre contracts et interfaces .

Il ne m'Ă©tait pas vraiment venu Ă  l'esprit avant que contracts puisse ĂȘtre dĂ©clarĂ© de maniĂšre plausible de la maniĂšre que @bigwhite a suggĂ©rĂ© d'utiliser le modĂšle 'type' existant. Cependant, encore une fois, je ne suis pas enthousiaste Ă  l'idĂ©e car je pense que cela compromettrait (3) ci-dessus. Aussi, s'il est nĂ©cessaire (pour des raisons d'analyse) de rĂ©pĂ©ter le mot-clĂ© type lors de la dĂ©claration d'une structure gĂ©nĂ©rique comme celle-ci :

type List(type Element) struct {
    next *List(Element)
    val  Element
}

il serait probablement également nécessaire de le répéter si contracts était déclaré d'une maniÚre similaire, ce qui est un peu "bégaiement" par rapport à l'approche de conception de brouillon.

Une autre idée qui ne me plaßt pas est celle des "littéraux contractuels" qui permettraient d'écrire contracts "en place" plutÎt que sous forme de constructions séparées. Cela rendrait les définitions de fonctions et de types génériques plus difficiles à lire et, comme certaines personnes pensent qu'elles le sont déjà, cela n'aidera pas à les persuader que les génériques sont une bonne chose.

DĂ©solĂ© d'apparaĂźtre si rĂ©sistant aux changements proposĂ©s au projet de gĂ©nĂ©riques (qui a certes quelques problĂšmes) mais, en tant que dĂ©fenseur enthousiaste des gĂ©nĂ©riques simples pour Go, je pense que ces points valent la peine d'ĂȘtre soulignĂ©s.

Je voudrais suggérer de ne pas appeler les prédicats sur les types "contrats". Il y a deux raisons :

  • Le terme "contrats" est dĂ©jĂ  utilisĂ© en informatique d'une maniĂšre diffĂ©rente. Par exemple, voir : (https://scholar.google.com/scholar?hl=en&as_sdt=0%2C33&q=contracts+languages&btnG=)
  • Il existe dĂ©jĂ  plusieurs noms pour cette idĂ©e dans la littĂ©rature informatique. J'en connais au moins ~trois~ quatre : "compositions", "classes de types", "concepts" et "contraintes". En ajouter un autre ne fera que compliquer davantage les choses.

@griesemer "les contraintes sur les types" sont purement liĂ©es au temps de compilation, car les types sont effacĂ©s avant l'exĂ©cution. Les contraintes font que le code gĂ©nĂ©rique est Ă©laborĂ© en code non gĂ©nĂ©rique qui peut ĂȘtre exĂ©cutĂ©. Les sous-types existent au moment de l'exĂ©cution et ne sont pas des contraintes dans le sens oĂč une contrainte sur les types serait au minimum l'Ă©galitĂ© de type ou la disĂ©galitĂ© de type, avec des contraintes telles que "est un sous-type de" Ă©ventuellement disponibles en fonction du systĂšme de type.

Pour moi, la nature d'exĂ©cution des sous-types est la diffĂ©rence critique, si X <: Y, nous pouvons passer X lĂ  oĂč Y est attendu, mais nous ne connaissons le type que comme Y sans opĂ©rations d'exĂ©cution non sĂ©curisĂ©es. En ce sens, il ne contraint pas le type Y, Y est toujours Y. Le sous-typage est Ă©galement "directionnel" et peut donc ĂȘtre covariant ou contravariant selon qu'il est appliquĂ© Ă  un argument d'entrĂ©e ou de sortie.

Avec une contrainte de type 'pred(X)', nous commençons avec un X entiĂšrement polymorphe, puis nous contraignons les valeurs autorisĂ©es. Donc dites seulement X qui implĂ©mente 'print'. Ceci est non directionnel et n'a donc pas de co-variance ou de contravariance. Il est en fait invariant dans la mesure oĂč nous connaissons le type brut de X au moment de la compilation.

Je pense donc qu'il est dangereux de considérer les interfaces comme des contraintes sur les types car elles ignorent des différences importantes telles que la covariance et la contravariance.

Est-ce que cela répond à votre question, ou ai-je raté le point?

Edit : Je dois souligner que je fais référence aux interfaces "Go" spécifiquement ci-dessus. Les points sur le sous-typage s'appliquent à toutes les langues qui ont des sous-types, mais Go est inhabituel en faisant des interfaces un type et donc en ayant une relation de sous-typage. Dans d'autres langages comme Java, une interface n'est explicitement pas un type (une classe est un type) donc les interfaces _sont_ une contrainte sur les types. Ainsi, alors qu'il est juste en général de considérer les interfaces comme des contraintes sur les types, c'est faux spécifiquement pour 'Go'.

@Inuart Il est beaucoup trop tĂŽt pour dire comment cela serait ajoutĂ© Ă  la mise en Ɠuvre. Il n'y a pas encore de proposition, juste un avant-projet. Ce ne sera certainement pas en 1.14.

@andrewcmyers J'aime le mot "contrat" ​​car il dĂ©crit une relation entre l'auteur de la fonction gĂ©nĂ©rique et son appelant.

Des mots comme "typesets" et "type classes" suggÚrent que nous parlons d'un méta-type, ce que nous sommes bien sûr, mais les contrats décrivent également une relation entre plusieurs types. Je sais que les classes de type, par exemple, Haskell, peuvent avoir plusieurs paramÚtres de type, mais il me semble que le nom ne correspond pas à l'idée décrite.

Je n'ai jamais compris pourquoi C++ appelle cela un "concept". Qu'est ce que ça veut dire?

"Contrainte" ou "contraintes" me conviendrait. Pour le moment, je pense qu'un contrat contient plusieurs contraintes. Mais nous pourrions changer cette façon de penser.

Je ne suis pas trop préoccupé par le fait qu'il existe une construction de langage de programmation existante appelée "contrat". Je pense que cette idée est relativement similaire à l'idée que nous voulons exprimer, en ce sens qu'il s'agit d'une relation entre une fonction et ses appelants. Je comprends que la maniÚre dont cette relation s'exprime est assez différente, mais j'ai l'impression qu'il y a une similitude sous-jacente.

Je n'ai jamais compris pourquoi C++ appelle cela un "concept". Qu'est ce que ça veut dire?

Un concept est une abstraction d'instanciations partageant certains points communs, par exemple des signatures.

Le terme concept convient de loin mieux aux interfaces car cette derniÚre est également utilisée pour désigner une frontiÚre partagée entre deux composants.

@sighoya J'allais Ă©galement mentionner que les «concepts» sont conceptuels car ils incluent des «axiomes» qui sont essentiels pour empĂȘcher les abus des opĂ©rateurs. Par exemple, l'addition '+' doit ĂȘtre associative et commutative. Ces axiomes ne peuvent pas ĂȘtre reprĂ©sentĂ©s en C++, ils existent donc en tant qu'idĂ©es abstraites, d'oĂč des "concepts". Un concept est donc le « contrat » syntaxique plus les axiomes sĂ©mantiques.

@ianlancetaylor "Constraint" est ce que nous l'avons appelé dans Genus (http://www.cs.cornell.edu/~yizhou/papers/genus-pldi2015.pdf), donc je suis partisan de cette terminologie. Le terme « contrat » serait un choix tout à fait raisonnable, sauf qu'il est trÚs utilisé dans la communauté PL pour désigner la relation entre les interfaces et les implémentations, qui a également une saveur contractuelle.

@keean Sans ĂȘtre un expert, je ne pense pas que la dichotomie que vous peignez reflĂšte trĂšs bien la rĂ©alitĂ©. Par exemple, si le compilateur gĂ©nĂšre des versions instanciĂ©es de fonctions gĂ©nĂ©riques est entiĂšrement une question d'implĂ©mentation, il est donc parfaitement raisonnable d'avoir une reprĂ©sentation Ă  l'exĂ©cution des contraintes, par exemple sous la forme d'un tableau de pointeurs de fonction pour chaque opĂ©ration requise. Exactement comme les tables de mĂ©thode d'interface, en fait. De mĂȘme, les interfaces dans Go ne correspondent pas Ă  votre dĂ©finition de sous-type, car vous pouvez les projeter en toute sĂ©curitĂ© (via des assertions de type) et parce que vous n'avez ni co- ni contravariance pour les constructeurs de type dans Go.

Enfin : que la dichotomie que vous peignez soit rĂ©aliste ou non, cela ne change rien au fait qu'une interface n'est, en fin de compte, qu'une liste de mĂ©thodes - et mĂȘme dans votre dichotomie, il n'y a aucune raison pour que cette liste puisse 't ĂȘtre rĂ©utilisĂ© en tant que table reprĂ©sentĂ©e au moment de l'exĂ©cution ou en tant que contrainte de temps de compilation uniquement, selon le contexte dans lequel il est utilisĂ©.

Que diriez-vous de quelque chose comme :

typeContrainte C(T) {
}

ou

typeContrat C(T) {
}

Il est différent des autres déclarations de type de souligner qu'il ne s'agit pas d'une construction d'exécution.

À propos de la nouvelle conception du contrat, j'ai quelques questions.

1.

Lorsqu'un type générique A embarque un autre type générique B,
soit une fonction générique A appelle une autre fonction générique B,
faut-il aussi préciser les contrats de B sur A ?

Si la réponse est vraie, alors si un type générique intÚgre de nombreux autres types génériques,
ou une fonction générique appelle plusieurs autres fonctions génériques,
alors nous devons combiner plusieurs contrats en un seul comme le contrat du type d'incorporation ou de la fonction appelante.
Cela peut causer le mĂȘme problĂšme d'empoisonnement const.

  1. Outre les contraintes actuelles de genre et d'ensemble de méthodes, avons-nous besoin d'autres contraintes ?
    Comme convertible d'un type Ă  l'autre, assignable d'un type Ă  l'autre,
    comparable entre deux types, est un canal envoyable, est un canal recevable,
    a un ensemble de champs spécifié, ...

3.

Si une fonction générique utilise une ligne comme la suivante

v.Foo()

Comment pouvons-nous Ă©crire un contrat qui permet Foo d'ĂȘtre soit une mĂ©thode, soit un champ d'un type de fonction ?

Les contraintes de type @merovius doivent ĂȘtre rĂ©solues au moment de la compilation, sinon le systĂšme de type peut ĂȘtre dĂ©fectueux. C'est parce que vous pouvez avoir un type qui dĂ©pend d'un autre qui n'est pas connu jusqu'Ă  l'exĂ©cution. Vous avez alors deux choix, vous devez implĂ©menter un systĂšme de type dĂ©pendant complet (qui permet Ă  la vĂ©rification de type de se produire au moment de l'exĂ©cution lorsque les types deviennent connus) ou vous devez ajouter des types existentiels au systĂšme de type. Les existentiels encodent la diffĂ©rence de phase des types connus statiquement et des types qui ne sont connus qu'au moment de l'exĂ©cution (types qui dĂ©pendent de la lecture Ă  partir d'IO par exemple).

Les sous-types, comme indiquĂ© ci-dessus, ne sont normalement pas connus avant l'exĂ©cution, bien que de nombreux langages aient des optimisations dans le cas oĂč le type est connu de maniĂšre statique.

Si nous supposons que l'un des changements ci-dessus est introduit dans le langage (types dépendants ou types existentiels), nous devons toujours séparer les concepts de sous-typage et de contraintes de type. Pour Go spécifiquement, les constricteurs de type sont invariants, nous pouvons ignorer ces différences, et nous pouvons considérer que les interfaces Go _sont_ des contraintes sur les types (statiquement).

On peut donc considĂ©rer une Go-interface comme un contrat Ă  paramĂštre unique oĂč le paramĂštre est le rĂ©cepteur de toutes les fonctions/mĂ©thodes. Alors pourquoi Go a-t-il Ă  la fois des interfaces et des contrats ? Il me semble que c'est parce que Go ne veut pas autoriser les interfaces pour les opĂ©rateurs (comme '+'), et parce que Go n'a pas de types dĂ©pendants ni de types existentiels.

Il y a donc deux facteurs qui créent une réelle différence entre les contraintes de type et le sous-typage. L'une est la co/contra-variance, que nous pouvons ignorer dans Go en raison de l'invariance du constructeur de type, et l'autre est le besoin de types dépendants ou de types existentiels pour créer un systÚme de types qui a des contraintes de type si il existe un polymorphisme d'exécution des paramÚtres de type aux contraintes de type.

@keean Cool, donc AIUI nous sommes au moins d'accord sur le fait que les interfaces en Go peuvent ĂȘtre considĂ©rĂ©es comme des contraintes :)

En ce qui concerne le reste : ci-dessus, vous avez déclaré :

Les "contraintes sur les types" sont purement liĂ©es au temps de compilation, car les types sont effacĂ©s avant l'exĂ©cution. Les contraintes font que le code gĂ©nĂ©rique est Ă©laborĂ© en code non gĂ©nĂ©rique qui peut ĂȘtre exĂ©cutĂ©.

Cette affirmation est plus spĂ©cifique que la derniĂšre, que les contraintes doivent ĂȘtre rĂ©solues au moment de la compilation. Tout ce que j'essayais de dire, c'est que le compilateur peut faire cette rĂ©solution (et toutes les mĂȘmes vĂ©rifications de type), mais toujours gĂ©nĂ©rer du code gĂ©nĂ©rique. Ce serait toujours valable, car la sĂ©mantique du systĂšme de type est la mĂȘme. Mais les contraintes auraient toujours une reprĂ©sentation Ă  l'exĂ©cution. C'est un peu pointilleux - mais c'est pourquoi je pense que les dĂ©finir en fonction du temps d'exĂ©cution par rapport au temps de compilation n'est pas la meilleure façon de procĂ©der. Il mĂ©lange les prĂ©occupations d'implĂ©mentation dans une discussion sur la sĂ©mantique abstraite d'un systĂšme de type.

FWIW, j'ai déjà fait valoir que je préférerais utiliser des interfaces pour exprimer des contraintes - et je suis également arrivé à la conclusion que permettre l'utilisation d'opérateurs dans du code générique est le principal obstacle pour le faire et donc la principale raison d'introduire un séparé concept sous forme de contrats.

@keean Merci, mais non, votre réponse n'a pas répondu à ma question. Notez que dans mon commentaire , j'ai décrit un exemple trÚs simple d'utilisation d'une interface à la place d'un contrat/"contrainte" correspondant. J'ai demandé un exemple _simple_ _concrete_ pourquoi ce scénario ne fonctionnerait pas "sans certaines restrictions" comme vous l'avez mentionné dans votre commentaire précédent. Vous n'avez pas fourni un tel exemple.

Notez que je n'ai pas mentionné les sous-types, la co- ou la contra-variance (ce que nous n'autorisons pas dans Go de toute façon, les signatures doivent toujours correspondre), etc. Au lieu de cela, j'ai utilisé une terminologie Go élémentaire et établie (interfaces, outils, paramÚtre de type, etc.) pour expliquer ce que j'entends par "contrainte" car c'est le langage commun que tout le monde comprend ici et que tout le monde peut suivre. (De plus, contrairement à ce que vous prétendez ici , en Java, une interface ressemble à un type pour moi selon la spécification Java : "Une déclaration d'interface spécifie un nouveau type de référence nommé". Si cela ne dit pas qu'une interface est un type alors les gens de Java Spec ont du travail à faire.)

Mais il semble que vous ayez rĂ©pondu indirectement Ă  ma question avec votre dernier commentaire , comme @Merovius l'a dĂ©jĂ  observĂ©, lorsque vous dites : "On peut donc considĂ©rer une Go-interface comme un contrat Ă  paramĂštre unique oĂč le paramĂštre est le rĂ©cepteur de toutes les fonctions/mĂ©thodes .". C'est exactement ce que je voulais dire au dĂ©but, alors merci d'avoir confirmĂ© ce que j'ai dit tout au long.

@dotaheor

Lorsqu'un type générique A embarque un autre type générique B, ou qu'une fonction générique A appelle une autre fonction générique B, faut-il aussi spécifier les contrats de B sur A ?

Si un type gĂ©nĂ©rique A incorpore un autre type gĂ©nĂ©rique B, alors les paramĂštres de type passĂ©s Ă  B doivent satisfaire tout contrat utilisĂ© par B. Pour ce faire, le contrat utilisĂ© par A doit impliquer le contrat utilisĂ© par B. Autrement dit, toutes les contraintes sur le type les paramĂštres passĂ©s Ă  B doivent ĂȘtre exprimĂ©s dans le contrat utilisĂ© par A. Ceci s'applique Ă©galement lorsqu'une fonction gĂ©nĂ©rique appelle une autre fonction gĂ©nĂ©rique.

Si la rĂ©ponse est vraie, alors si un type gĂ©nĂ©rique incorpore de nombreux autres types geneirc, ou si une fonction gĂ©nĂ©rique appelle de nombreuses autres fonctions gĂ©nĂ©riques, alors nous devons combiner plusieurs contrats en un seul comme contrat du type d'incorporation ou de la fonction appelante. Cela peut causer le mĂȘme problĂšme d'empoisonnement const.

Je pense que ce que vous dites est vrai, mais ce n'est pas le problĂšme de l'empoisonnement const. Le problĂšme d'empoisonnement constant est que vous devez rĂ©partir const partout oĂč un argument est passĂ©, puis si vous dĂ©couvrez un endroit oĂč l'argument doit ĂȘtre modifiĂ©, vous devez supprimer const partout. Le cas des gĂ©nĂ©riques ressemble plus Ă  "si vous appelez plusieurs fonctions, vous devez transmettre des valeurs du type correct Ă  chacune de ces fonctions".

Dans tous les cas, il me semble extrĂȘmement peu probable que les gens Ă©crivent des fonctions gĂ©nĂ©riques qui appellent de nombreuses autres fonctions gĂ©nĂ©riques qui utilisent toutes des contrats diffĂ©rents. Comment cela se passerait-il naturellement ?

Outre les contraintes actuelles de genre et d'ensemble de méthodes, avons-nous besoin d'autres contraintes ? Tels que convertible d'un type à un autre, assignable d'un type à un autre, comparable entre deux types, est un canal envoyable, est un canal recevable, a un ensemble de champs spécifié, ...

Les contraintes telles que la convertibilitĂ©, l'assignabilitĂ© et la comparabilitĂ© sont exprimĂ©es sous la forme de types, comme l'explique le projet de conception. Les contraintes telles que le canal envoyable ou recevable ne peuvent ĂȘtre exprimĂ©es que sous la forme chan T oĂč T est un paramĂštre de type, comme l'explique le projet de conception. Il n'y a aucun moyen d'exprimer la contrainte selon laquelle un type a un ensemble de champs spĂ©cifiĂ©, mais je doute que cela se produise trĂšs souvent. Nous devrons voir comment cela fonctionne en Ă©crivant du vrai code pour voir ce qui se passe.

Si une fonction générique utilise une ligne comme la suivante

v.Foo()
Comment pouvons-nous Ă©crire un contrat qui permet Ă  Foo d'ĂȘtre soit une mĂ©thode, soit un champ d'un type de fonction ?

Dans le projet de conception actuel, vous ne pouvez pas. Cela vous semble-t-il un cas d'utilisation important ? (Je sais que le projet de conception précédent le soutenait.)

@griesemer vous avez manquĂ© le point oĂč j'ai dit que cela n'Ă©tait valable que si vous introduisez des types dĂ©pendants ou des types existentiels dans le systĂšme de types.

Sinon, si vous utilisez un contrat comme interface, vous pouvez échouer au moment de l'exécution, car vous devez différer la vérification de type jusqu'à ce que vous connaissiez les types, et la vérification de type peut échouer, ce qui n'est donc pas sûr.

J'ai également vu des interfaces expliquées en tant que sous-types, vous devez donc faire attention à ce que quelqu'un n'essaye pas d'introduire la co/contra-variance dans les constructeurs de types à l'avenir. Mieux vaut ne pas avoir d'interfaces comme types, alors il n'y a aucune possibilité de cela, et les intentions des concepteurs, que ce ne sont pas des sous-types, sont claires.

Pour moi, ce serait une meilleure conception de fusionner les interfaces et les contrats, et de leur faire explicitement des contraintes de type (prédicats sur les types).

@ianlancetaylor

Dans tous les cas, il me semble extrĂȘmement peu probable que les gens Ă©crivent des fonctions gĂ©nĂ©riques qui appellent de nombreuses autres fonctions gĂ©nĂ©riques qui utilisent toutes des contrats diffĂ©rents. Comment cela se passerait-il naturellement ?

Pourquoi serait-ce inhabituel ? Si je définis une fonction sur le type 'T', je voudrai appeler des fonctions sur 'T'. Par exemple, si je définis une fonction 'somme' sur des 'types additionnables' par contrat. Maintenant, je veux construire une fonction de multiplication générique qui appelle sum? Beaucoup de choses dans la programmation ont une structure somme/produit (tout ce qui est un « groupe »).

Je ne comprends pas quel sera le but de l'interface aprĂšs que les contrats soient sur le langage, il semble que les contrats serviront dans le mĂȘme but, pour s'assurer qu'un type a un ensemble de mĂ©thodes dĂ©finies dessus.

@keean Le cas inhabituel concerne les fonctions qui appellent de nombreuses autres fonctions génériques qui utilisent toutes des contrats différents . Votre contre-exemple n'appelle qu'une seule fonction. N'oubliez pas que je conteste la similitude avec l'empoisonnement const.

@mrkaspa La façon la plus simple de penser est que les contrats sont comme les fonctions de modÚle C++ et les interfaces sont comme les méthodes virtuelles C++. Il y a une utilité et un but pour les deux.

@ianlancetaylor d'expérience, il y a deux problÚmes similaires à l'empoisonnement const. Les deux se produisent en raison de la nature arborescente des appels de fonctions imbriquées. La premiÚre est lorsque vous souhaitez ajouter le débogage à une fonction profondément imbriquée, vous devez ajouter imprimable de la feuille jusqu'à la racine, ce qui peut impliquer de toucher plusieurs bibliothÚques tierces. La seconde est que vous pouvez accumuler un grand nombre de contrats à la racine, rendant les signatures de fonction difficiles à lire. Il est souvent préférable que le compilateur infÚre les contraintes comme Haskell le fait avec les classes de types pour éviter ces deux problÚmes.

@ianlancetaylor Je ne m'y connais pas trop en c++, quels seront les cas d'utilisation des interfaces et des contrats en golang ? quand dois-je utiliser l'interface ou le contrat ?

@keean Ce sous-thread concerne un projet de conception spĂ©cifique pour le langage Go. Dans Go, toutes les valeurs sont imprimables. Ce n'est pas quelque chose qui doit ĂȘtre exprimĂ© dans un contrat. Et bien que je sois disposĂ© Ă  voir des preuves que de nombreux contrats peuvent s'accumuler pour une seule fonction ou un seul type gĂ©nĂ©rique, je ne suis pas disposĂ© Ă  accepter l'affirmation selon laquelle cela se produira. Le but du projet de conception est d'essayer d'Ă©crire du vrai code qui l'utilise.

Le projet de conception explique aussi clairement que possible pourquoi je pense que déduire les contraintes est un mauvais choix pour un langage comme Go qui est conçu pour la programmation à grande échelle.

@mrkaspa Par exemple, si vous avez un []io.Reader , vous voulez une valeur d'interface, pas un contrat. Un contrat exigerait que tous les Ă©lĂ©ments de la tranche soient du mĂȘme type. Une interface leur permettra d'ĂȘtre de types diffĂ©rents, tant que tous les types implĂ©mentent io.Reader .

@ianlancetaylor autant que je sache, l'interface crée un nouveau type tandis que les contrats contraignent un type mais n'en créent pas un nouveau, ai-je raison?

@ianlancetaylor :

Ne pourriez-vous pas faire quelque chose comme ce qui suit ?

contract Reader(T) {
  T Read([]byte) (int, error)
}

func ReadAll(type T Reader)(readers []T) ([]byte, error) {
  // Use the readers...
}

Maintenant, ReadAll() devrait accepter un []io.Reader aussi bien qu'il accepterait un []*os.File , n'est-ce pas ? io.Reader semble satisfaire le contrat, et je ne me souviens de rien dans le brouillon concernant les valeurs d'interface ne pouvant pas ĂȘtre utilisĂ©es comme arguments de type.

Édit : Peu importe. J'ai mal compris. C'est toujours un endroit oĂč vous utiliseriez une interface, c'est donc une rĂ©ponse Ă  la question de @mrkaspa . Vous n'utilisez tout simplement pas l'interface dans la signature de la fonction ; vous ne l'utilisez que lĂ  oĂč il est appelĂ©.

@mrkaspa Oui, c'est vrai.

@ianlancetaylor si j'avais une liste de []io.Reader et ce contrat :

contract Reader(T) {
  T Read([]byte) (int, error)
}

func ReadAll(type T Reader)(readers []T) ([]byte, error) {
  // Use the readers...
}

Je pourrais appeler ReadAll sur chaque interface car elles satisfont au contrat ?

@ianlancetaylor bien sûr que les choses sont imprimables, mais il est facile de trouver d'autres exemples, par exemple la journalisation vers un fichier ou vers le réseau, nous voulons que la journalisation soit générique afin que nous puissions changer la cible du journal entre null, fichier local, service réseau, etc. se connecter à une fonction feuille nécessite d'ajouter les contraintes jusqu'au total de la racine, y compris d'avoir à modifier les bibliothÚques tierces utilisées.

Le code n'est pas statique, vous devez également prévoir la maintenance. En fait, le code est en "maintenance" beaucoup plus longtemps qu'il n'en faut pour l'écrire initialement, il y a donc un bon argument selon lequel nous devrions concevoir des langages pour faciliter la maintenance, la refactorisation, l'ajout de fonctionnalités, etc.

En réalité, ces problÚmes ne se manifesteront que dans une grande base de code, qui est maintenue au fil du temps. Ce n'est pas quelque chose que vous pouvez écrire un petit exemple rapide pour démontrer.

Ces problÚmes existent également dans d'autres langages génériques, par exemple Ada. Vous pouvez porter une grande application Ada qui utilise largement les génériques, mais si le problÚme existe dans Ada, je ne vois rien dans Go qui pourrait atténuer ce problÚme.

@mrkaspa Oui.

À ce stade, je suggùre que ce fil de conversation passe aux golang-nuts. Le suivi des problùmes GitHub est un mauvais endroit pour ce genre de discussion.

@keean Vous avez peut-ĂȘtre raison. Le temps nous le dira. Nous demandons explicitement aux gens d'essayer d'Ă©crire du code dans le brouillon de conception. Il y a peu de valeur dans des discussions purement hypothĂ©tiques.

@keean Je ne comprends pas votre exemple de journalisation. Le problÚme que vous décrivez est quelque chose que vous pouvez résoudre avec des interfaces au moment de l'exécution, pas avec des génériques au moment de la compilation.

Les interfaces @bserdar n'ont qu'un seul paramĂštre de type, vous ne pouvez donc pas faire quelque chose oĂč un paramĂštre est la chose Ă  enregistrer et un second paramĂštre de type est le type du journal.

@keean IMO dans cet exemple, vous feriez la mĂȘme chose que vous faites aujourd'hui, sans aucun paramĂštre de type : utilisez la rĂ©flexion pour inspecter la chose Ă  enregistrer et utilisez context.Context pour transmettre la valeur du journal. Je sais que ces idĂ©es sont rĂ©pugnantes pour les amateurs de dactylographie, mais elles s'avĂšrent plutĂŽt pratiques. Bien sĂ»r, il y a de la valeur dans les paramĂštres de type contraints, c'est pourquoi nous avons cette conversation - mais je dirais que la raison pour laquelle les cas qui vous viennent Ă  l'esprit sont les cas qui fonctionnent dĂ©jĂ  assez bien dans les bases de code Go actuelles Ă  grande Ă©chelle , sont que ce ne sont pas les cas qui bĂ©nĂ©ficient vraiment d'une vĂ©rification de type stricte supplĂ©mentaire. Ce qui revient au point Ians - il reste Ă  voir si c'est un problĂšme qui se manifeste dans la pratique.

@merovius Si cela ne tenait qu'à moi, toute réflexion d'exécution serait interdite, car je ne veux pas de logiciels livrés générant des erreurs de frappe au moment de l'exécution qui pourraient affecter l'utilisateur. Cela permet des optimisations plus agressives du compilateur car vous n'avez pas à vous soucier de l'alignement du modÚle d'exécution avec le modÚle statique.

Ayant géré la migration de grands projets à grande échelle de JavaScript vers TypeScript, d'aprÚs mon expérience, le typage strict devient plus important, plus le projet est grand et plus l'équipe qui y travaille est grande. En effet, vous devez vous fier à l'interface/au contrat d'un bloc de code sans avoir à examiner l'implémentation pour maintenir l'efficacité lorsque vous travaillez avec une grande équipe.

À part : bien sĂ»r, cela dĂ©pend de la façon dont vous atteignez l'Ă©chelle, pour le moment je prĂ©fĂšre une approche API-First, en commençant par un fichier OpenAPI/Swagger JSON, puis en utilisant la gĂ©nĂ©ration de code pour crĂ©er les stubs de serveur et le SDK client. En tant que tel, OpenAPI agit en fait comme votre systĂšme de type pour les micro-services.

@ianlancetaylor

Les contraintes telles que la convertibilité, l'assignabilité et la comparabilité sont exprimées sous la forme de types

Étant donnĂ© qu'il y a tellement de dĂ©tails dans les rĂšgles de conversion de type Go, il est vraiment difficile d'Ă©crire un contrat personnalisĂ© C pour satisfaire la fonction de conversion de tranche gĂ©nĂ©rale suivante :

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
}

Un C parfait devrait permettre des conversions :

  • entre n'importe quel entier, types numĂ©riques Ă  virgule flottante
  • entre n'importe quel type numĂ©rique complexe
  • entre deux types dont les types sous-jacents sont identiques
  • Ă  partir d'un type Out qui implĂ©mente In
  • d'un type de canal Ă  un type de canal bidirectionnel et les deux types de canaux ont un type d'Ă©lĂ©ment identique
  • liĂ© Ă  la balise struct, ...
  • ...

D'aprÚs ce que j'ai compris, je ne peux pas rédiger un tel contrat. Avons-nous donc besoin d'un contrat convertible intégré ?

Il n'y a aucun moyen d'exprimer la contrainte qu'un type a un ensemble de champs spécifié, mais je doute que cela se produise trÚs souvent

Considérant que l'intégration de type est souvent utilisée dans la programmation Go, je pense que les besoins ne seraient pas rares.

@keean C'est une opinion valable Ă  avoir, mais ce n'est Ă©videmment pas celle qui guide la conception et le dĂ©veloppement de Go. Pour participer de maniĂšre constructive, veuillez accepter cela et commencer Ă  travailler Ă  partir de lĂ  oĂč nous en sommes et en supposant que tout dĂ©veloppement de la langue doit ĂȘtre un changement progressif par rapport au statu quo. Si vous ne pouvez pas, alors il y a des langues qui correspondent plus Ă©troitement Ă  vos prĂ©fĂ©rences et je pense que tout le monde - vous en particulier - serait plus heureux si vous y apportiez votre Ă©nergie.

@merovius Je suis prĂȘt Ă  accepter que les changements apportĂ©s Ă  Go doivent ĂȘtre progressifs et Ă  accepter le statu quo.

Je répondais juste à votre commentaire dans le cadre d'une conversation, convenant que je suis un passionné de dactylographie. J'ai exprimé une opinion sur la réflexion d'exécution, je n'ai pas suggéré que Go devrait abandonner la réflexion d'exécution. Je travaille sur d'autres langues, j'utilise plusieurs langues dans mon travail. Je développe (lentement) ma propre langue, mais j'espÚre toujours que les développements vers d'autres langues rendront cela inutile.

@dotaheor Je suis d'accord qu'on ne peut pas rĂ©diger un contrat gĂ©nĂ©ral de convertibilitĂ© aujourd'hui. Nous devrons voir si cela semble ĂȘtre un problĂšme dans la pratique.

En réponse à @ianlancetaylor

Je ne pense pas qu'il soit encore clair à quelle fréquence les gens voudront paramétrer des fonctions sur des valeurs constantes. Le cas le plus évident serait pour les dimensions du tableau - mais vous pouvez déjà le faire en passant le type de tableau souhaité comme argument de type. En dehors de ce cas, que gagnons-nous vraiment en passant un const comme argument de compilation plutÎt que comme argument d'exécution ?

Dans le cas des tableaux, le simple fait de passer le type de tableau (entier) comme argument de type semble ĂȘtre extrĂȘmement limitant, car le contrat ne serait pas en mesure de dĂ©composer ni la dimension du tableau ni le type d'Ă©lĂ©ment et de leur imposer des contraintes. Par exemple, un contrat prenant un "type de tableau entier" pourrait-il exiger que le type d'Ă©lĂ©ment du type de tableau implĂ©mente certaines mĂ©thodes ?

Mais votre demande d'exemples plus spécifiques de l'utilité des paramÚtres génériques non typés est bien accueillie. J'ai donc élargi le billet de blog pour inclure une section couvrant quelques classes importantes d'exemples de cas d'utilisation et quelques exemples spécifiques de chacun. Depuis quelques jours, encore une fois le billet de blog est ici :

Seuls les paramÚtres de type sont-ils suffisamment génériques pour les génériques Go 2 ?

La nouvelle section est intitulée "Example Ways Generics Over Non-Types are Useful".

En rĂ©sumĂ©, les contrats pour les opĂ©rations matricielles et vectorielles pourraient imposer des contraintes appropriĂ©es Ă  la fois sur la dimensionnalitĂ© et sur les types d'Ă©lĂ©ments des tableaux. Par exemple, la multiplication matricielle d'une matrice nxm avec une matrice mxp, chacune reprĂ©sentĂ©e sous la forme d'un tableau Ă  deux dimensions, pourrait correctement contraindre le nombre de lignes de la premiĂšre matrice Ă  ĂȘtre Ă©gal au nombre de colonnes de la deuxiĂšme matrice, etc.

Plus gĂ©nĂ©ralement, les gĂ©nĂ©riques pourraient utiliser des paramĂštres non typĂ©s pour permettre la configuration au moment de la compilation et la spĂ©cialisation du code et des algorithmes de nombreuses maniĂšres. Par exemple, une variante gĂ©nĂ©rique de math/big.Int pourrait ĂȘtre configurable au moment de la compilation sur un bit particulier avec et/ou signĂ©, satisfaisant les demandes d'entiers 128 bits et d'autres entiers non natifs Ă  largeur fixe avec une efficacitĂ© raisonnable probablement beaucoup mieux que le big.Int existant oĂč tout est dynamique. Une variante gĂ©nĂ©rique de big.Float pourrait de mĂȘme ĂȘtre spĂ©cialisable au moment de la compilation avec une prĂ©cision particuliĂšre et/ou d'autres paramĂštres de compilation, par exemple, pour fournir des implĂ©mentations gĂ©nĂ©riques raisonnablement efficaces des formats binaires16, binaires128 et binaires256 de IEEE 754-2008 que Go ne prend pas en charge nativement. De nombreux algorithmes de bibliothĂšque peuvent optimiser leur fonctionnement en fonction de la connaissance des besoins de l'utilisateur ou d'aspects particuliers des donnĂ©es en cours de traitement - par exemple, des optimisations d'algorithmes de graphe qui ne fonctionnent que sur des poids d'arĂȘte non nĂ©gatifs ou uniquement sur des DAG ou des arbres, ou des optimisations de traitement matriciel qui s'appuyer sur des matrices triangulaires supĂ©rieures ou infĂ©rieures, ou sur de grands nombres entiers pour la cryptographie devant parfois ĂȘtre implĂ©mentĂ©e en temps constant et parfois non - pourrait utiliser des gĂ©nĂ©riques pour se rendre configurables au moment de la compilation pour dĂ©pendre d'informations dĂ©claratives facultatives telles que ceci, tout en veillant Ă  ce que tous les tests de ces options de compilation dans l'implĂ©mentation soient gĂ©nĂ©ralement compilĂ©s via une propagation constante.

@bford a Ă©crit :

à savoir que les paramÚtres des génériques sont liés à des constantes au moment de la compilation.

C'est le point que je ne comprends pas. Pourquoi vous exigez cette condition.
Théoriquement, on pourrait redéfinir les variables/paramÚtres dans le corps. Cela n'a pas d'importance.
Intuitivement, je suppose que vous voudriez déclarer que la premiÚre application de fonction doit se produire au moment de la compilation.

Mais pour cette exigence, un mot-clé comme comp ou comptime serait mieux adapté.
De plus, si la grammaire de golang n'autorisait que deux tuples de paramĂštres au maximum pour une fonction, alors cette annotation de mot-clĂ© peut ĂȘtre omise car le premier tuple de paramĂštre d'un type et d'une fonction (dans le cas de deux tuples de paramĂštres) sera toujours Ă©valuĂ© au moment de la compilation.

Autre point : que se passe-t-il si const est étendu pour autoriser les expressions d'exécution (véritable authentification unique) ?

Sur les méthodes pointeur vs valeur :

Si une méthode est répertoriée dans un contrat avec un simple T plutÎt que *T , il peut s'agir soit d'une méthode de pointeur, soit d'une méthode de valeur de T . Afin d'éviter de se soucier de cette distinction, dans un corps de fonction générique, tous les appels de méthode seront des appels de méthode de pointeur. ...

Comment cela cadre-t-il avec l'implémentation de l'interface ? Si un T a une méthode de pointeur (comme le MyInt dans l'exemple), peut-on assigner T à l'interface avec cette méthode ( Stringer dans le Exemple)?

L'autoriser signifie avoir une autre opération d'adresse cachée & , ne pas l'autoriser signifie que les contrats et les interfaces ne peuvent interagir que via un commutateur de type explicite. Aucune des deux solutions ne me semble bonne.

(Remarque : nous devrions revoir cette décision si elle entraßne une confusion ou un code incorrect.)

Je vois que l'Ă©quipe a dĂ©jĂ  des rĂ©serves sur cette ambiguĂŻtĂ© dans la syntaxe de la mĂ©thode du pointeur. J'ajoute simplement que l'ambiguĂŻtĂ© affecte Ă©galement la mise en Ɠuvre de l'interface (et ajoute implicitement mes rĂ©serves Ă  ce sujet Ă©galement).

@fJavierZunzunegui Vous avez raison, le texte actuel implique que lors de l'attribution d'une valeur d'un paramĂštre de type Ă  un type d'interface, une opĂ©ration d'adresse implicite peut ĂȘtre requise. Cela peut ĂȘtre une autre raison de ne pas utiliser d'adresses implicites lors de l'appel de mĂ©thodes. Il faudra voir.

Sur les types paramétrés , en particulier en ce qui concerne les paramÚtres de type intégrés en tant que champ dans une structure :

Envisager

type Lockable(type T) struct {
    T
    sync.Locker
}

Et si T avait une méthode nommée Lock ou Unlock ? La structure ne compilerait pas. Le fait de ne pas avoir de condition de méthode X n'est pas pris en charge par les contrats, nous avons donc un code invalide qui ne rompt pas le contrat (vainquant tout l'objectif des contrats).

Cela devient encore plus compliqué si vous avez plusieurs paramÚtres intégrés (disons T1 et T2 ) car ceux-ci ne doivent pas avoir de méthodes communes (encore une fois, non imposées par des contrats). De plus, la prise en charge de méthodes arbitraires en fonction des types intégrés contribue à des restrictions de temps de compilation trÚs limitées sur les commutateurs de type pour ces structures (de maniÚre trÚs similaire aux assertions Type et commutateurs ).

Selon moi, il y a 2 bonnes alternatives:

  • interdire complĂštement l'intĂ©gration des paramĂštres de type : simple, mais Ă  faible coĂ»t (si la mĂ©thode est nĂ©cessaire, il faut l'Ă©crire explicitement dans la structure avec le champ).
  • restreindre les mĂ©thodes appelables aux mĂ©thodes contractuelles : de la mĂȘme maniĂšre que l'intĂ©gration d'une interface. Cela s'Ă©carte du go normal (un non-but) mais sans frais (les mĂ©thodes n'ont pas besoin d'ĂȘtre Ă©crites explicitement dans la structure avec le champ).

La structure ne compilerait pas.

Ça compilerait. Essayez-le. Ce qui ne parvient pas Ă  compiler est un appel Ă  la mĂ©thode ambiguĂ«. Votre argument est toujours valable, cependant.

Votre deuxiĂšme solution, restreignant les mĂ©thodes appelables Ă  celles mentionnĂ©es dans le contrat, ne fonctionnera pas : mĂȘme si le contrat sur T spĂ©cifiait Lock et Unlock , vous pouviez toujours ' t les appeler sur un Lockable .

@jba merci pour les idées sur la compilation.

Par deuxiĂšme solution, j'entends traiter les paramĂštres de type intĂ©grĂ©s comme nous le faisons actuellement avec les interfaces, de sorte que si la mĂ©thode n'est pas dans le contrat, elle n'est pas immĂ©diatement accessible aprĂšs l'intĂ©gration. Dans ce scĂ©nario, puisque T n'a pas de contrat, il est effectivement traitĂ© comme interface{} , donc il ne serait pas en conflit avec le sync.Locker mĂȘme si T Ă©tait instanciĂ© avec un type avec ces mĂ©thodes. Cela pourrait aider Ă  expliquer mon propos .

Quoi qu'il en soit, je préfÚre la premiÚre solution (interdire complÚtement l'intégration), donc si telle est votre préférence, il est inutile de discuter de la seconde ! :smiley:

L'exemple fourni par @JavierZunzunegui couvre Ă©galement un autre cas. Que se passe-t-il si T est une structure qui a un champ noCopy noCopy ? Le compilateur devrait Ă©galement ĂȘtre capable de gĂ©rer ce cas.

Je ne sais pas si c'est exactement le bon endroit pour cela, mais je voulais commenter avec un cas d'utilisation concret et réel pour les types génériques qui permettent "la paramétrisation sur des valeurs non typées telles que des constantes", et spécifiquement pour le cas des tableaux . J'espÚre que ceci est utile.

Dans mon monde sans génériques, j'écris beaucoup de code qui ressemble à ceci :

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
}

Cela fonctionne assez bien, avec une ride. J'ai souvent besoin de centaines de millions de []SigEl s, et leur longueur est souvent de 128 Ă  384 bits au total. Étant donnĂ© que les tranches imposent une surcharge fixe de 192 bits en plus de la taille du tableau sous-jacent, lorsque le tableau est lui-mĂȘme de 384 bits ou moins, cela impose une surcharge de mĂ©moire inutile de 50 Ă  150 %, ce qui est Ă©videmment terrible.

Ma solution consiste à allouer une tranche de Sig _arrays_, puis à les trancher à la volée en tant que paramÚtres à HammingDist ci-dessus :

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][:])

Ce que j'aimerais pouvoir faire au lieu de tout cela, c'est définir un type de signature générique et réécrire tout ce qui précÚde comme (quelque chose comme):

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
}

Alors pour utiliser cette bibliothĂšque:

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

Un ingĂ©nieur peut rĂȘver... đŸ€–

Si vous savez quelle peut ĂȘtre la longueur maximale en bits, vous pouvez utiliser quelque chose comme ceci Ă  la place :

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 Je ne suis pas sĂ»r de comprendre comment vous pensez que cela amĂ©liorera les choses - supposez-vous peut-ĂȘtre que le compilateur gĂ©nĂ©rerait une version instanciĂ©e de cette fonction pour chaque longueur de tableau possible? Parce que ISTM a) ce n'est pas trĂšs probable, donc b) vous vous retrouveriez avec exactement les mĂȘmes caractĂ©ristiques de performance que vous le faites maintenant. L'implĂ©mentation la plus probable, IMO, serait toujours que le compilateur passe la longueur et un pointeur vers le premier Ă©lĂ©ment, donc vous passeriez effectivement une tranche, dans le code gĂ©nĂ©rĂ© (je veux dire, vous ne passeriez pas la capacitĂ©, mais je ne pense pas qu'un mot supplĂ©mentaire sur la pile compte vraiment).

HonnĂȘtement, ce que vous dites Ă  l'OMI est un assez bon exemple d'utilisation excessive de gĂ©nĂ©riques, lĂ  oĂč ils ne sont pas nĂ©cessaires - "un tableau de longueur indĂ©terminĂ©e" est exactement Ă  quoi servent les tranches.

@Merovius Merci, je pense que votre commentaire révÚle quelques points de discussion intéressants.

"un tableau de longueur indéterminée" est exactement à quoi servent les tranches.

D'accord, mais dans mon exemple, il n'y a pas de tableaux de longueur indéterminée. La longueur du tableau est une constante connue au _temps de compilation_. C'est précisément à cela que servent les tableaux, mais ils sont sous-utilisés dans golang IMO car ils sont si rigides.

Pour ĂȘtre clair, je ne suggĂšre pas

type Signature (type Element UnsignedInteger, n int) [n]Element

signifie que n est une variable d'exĂ©cution. Elle doit encore ĂȘtre une constante dans le mĂȘme sens qu'aujourd'hui :

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 

Examinons donc le "coût" de la fonction HammingDist basée sur les tranches. Je suis d'accord que la différence entre passer un tableau comme bitVects[x][:] vs &bitVects[x] est petite (-ish, un facteur de 3 max). La vraie différence réside dans le code et la vérification de l'exécution qui doivent se produire à l'intérieur de cette fonction.

Dans la version basĂ©e sur les tranches, le code d'exĂ©cution doit vĂ©rifier les limites des accĂšs aux tranches pour assurer la sĂ©curitĂ© de la mĂ©moire. Cela signifie que cette version du code peut paniquer (ou qu'un mĂ©canisme explicite de vĂ©rification et de retour d'erreur est nĂ©cessaire pour empĂȘcher cela). Les affectations NOP ( _ = b1[len(b2)-1] ) font une diffĂ©rence de performances significative en indiquant Ă  l'optimiseur du compilateur qu'il n'a pas besoin de vĂ©rifier les limites de chaque accĂšs aux tranches dans la boucle. Mais ces contrĂŽles de limites minimales sont toujours nĂ©cessaires, mĂȘme si les tableaux sous-jacents passĂ©s ont toujours la mĂȘme longueur. De plus, le compilateur peut avoir des difficultĂ©s Ă  optimiser de maniĂšre rentable la boucle for/range (par exemple via unrolling ).

En revanche, la version basée sur un tableau générique de la fonction ne peut pas paniquer au moment de l'exécution (ne nécessitant aucune gestion des erreurs) et contourne le besoin de toute logique de vérification des limites conditionnelles. Je doute fortement qu'une version générique compilée de la fonction ait besoin de "passer" la longueur du tableau comme vous le suggérez, car il s'agit littéralement d'une valeur constante qui fait partie du type instancié au moment de la compilation.

De plus, pour les petites dimensions de tableau (importantes dans mon cas), il serait facile pour le compilateur de dĂ©rouler de maniĂšre rentable ou mĂȘme d'optimiser entiĂšrement la boucle for/range pour un gain de performances dĂ©cent, car il saura au moment de la compilation quelles sont ces dimensions. .

L'autre gros avantage de la version gĂ©nĂ©rique du code est qu'elle permet Ă  l'utilisateur du module HammingDist de dĂ©terminer le type int non signĂ© dans son propre code. La version non gĂ©nĂ©rique nĂ©cessite que le module lui-mĂȘme soit modifiĂ© pour changer le type dĂ©fini SigEl , puisqu'il n'y a aucun moyen de "passer" un type Ă  un module. Une consĂ©quence de cette diffĂ©rence est que la mise en Ɠuvre de la fonction de distance devient plus simple lorsqu'il n'est pas nĂ©cessaire d'Ă©crire un code sĂ©parĂ© pour chacun des cas uint Ă  {8, 16, 32, 64} bits.

Les coûts de la version basée sur les tranches de la fonction et la nécessité de modifier le code de la bibliothÚque pour définir le type d'élément sont des concessions hautement sous-optimales nécessaires pour éviter d'avoir à implémenter et à maintenir des versions "NxM" de cette fonction. La prise en charge générique des types de tableaux paramétrés (constants) résoudrait ce problÚme :

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

Éviter le cauchemar ci-dessus, ou les coĂ»ts trĂšs rĂ©els des alternatives actuelles, me semble ĂȘtre l'opposĂ© de la "surutilisation gĂ©nĂ©rique". Je suis d'accord avec @sighoya que l'Ă©numĂ©ration de toutes les longueurs de tableau autorisĂ©es dans le contrat pourrait fonctionner pour un ensemble trĂšs limitĂ© de cas, mais je pense que c'est trop limitĂ© mĂȘme pour mon cas, car mĂȘme si je mets le seuil supĂ©rieur de support Ă  un faible 384 bits au total, cela nĂ©cessiterait prĂšs de 50 termes dans la clause ArrayType [1]ElemType,[2]ElemType,...,[maxBit]ElemType du contrat pour couvrir le cas uint8 .

D'accord, mais dans mon exemple, il n'y a pas de tableaux de longueur indéterminée. La longueur du tableau est une constante connue au moment de la compilation.

Je comprends cela, mais notez que je n'ai pas dit "au moment de l'exécution" non plus. Vous voulez écrire du code qui ne tient pas compte de la longueur du tableau. Les tranches peuvent déjà le faire.

Je doute fortement qu'une version générique compilée de la fonction ait besoin de "passer" la longueur du tableau comme vous le suggérez, car il s'agit littéralement d'une valeur constante qui fait partie du type instancié au moment de la compilation.

Une version générique de la fonction le ferait - parce que chaque instanciation de ce type utilise une constante différente. C'est pourquoi j'ai l'impression que vous supposez que le code généré ne sera pas générique, mais étendu pour chaque type. c'est-à-dire que vous semblez supposer qu'il y aura plusieurs instanciations de cette fonction générées, pour [1]Element , [2]Element , etc. Je dis que cela me semble peu probable, que cela semble plus probable qu'il y aura une version générée, qui est essentiellement équivalente à la version tranche.

Bien sĂ»r, il ne doit pas en ĂȘtre ainsi. Donc, oui, vous avez raison de dire que vous n'avez pas besoin de passer la longueur du tableau. Je prĂ©dis simplement fortement que cela serait mis en Ɠuvre de cette façon et il semble discutable que ce ne sera pas le cas. (FWIW, je dirais Ă©galement que si vous souhaitez que le compilateur gĂ©nĂšre des corps de fonction spĂ©cialisĂ©s pour des longueurs distinctes, il pourrait tout aussi bien le faire de maniĂšre transparente pour les tranches Ă©galement, mais c'est une discussion diffĂ©rente).

L'autre gros avantage de la version générique du code

Pour clarifier : Par "la version générique", faites-vous référence à l'idée générale de génériques, telle qu'implémentée par exemple dans le projet de conception des contrats actuels, ou faites-vous référence plus spécifiquement aux génériques avec des paramÚtres non-typiques ? Parce que les avantages que vous nommez dans ce paragraphe s'appliquent également au projet de conception des contrats actuels.

Je n'essaie pas ici de plaider contre les génériques en général. J'explique simplement pourquoi je ne pense pas que votre exemple serve à montrer que nous avons besoin d'autres types de paramÚtres que les types.

// With generics + parameterized constant array lengths:
// Without generics

C'est une fausse dichotomie (et une dichotomie tellement évidente que je suis un peu frustré par vous). Il y a aussi "avec des paramÚtres de type, mais sans paramÚtres entiers":

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
}

Ce qui me semble bien. Il est légÚrement moins sûr en ce qui concerne les types, en exigeant une panique d'exécution si les types ne correspondent pas. Mais, et c'est un peu mon propos, c'est le seul avantage d'ajouter des paramÚtres génériques non typés dans votre exemple (et c'est un avantage qui était déjà clair, IMO). Les gains de performances que vous prédisez reposent sur des hypothÚses assez solides sur la maniÚre dont les génériques en général et les génériques sur les paramÚtres non typés en particulier sont implémentés. Que, personnellement, je ne considÚre pas trÚs probable d'aprÚs ce que j'ai entendu de l'équipe Go jusqu'à présent.

Je doute fortement qu'une version générique compilée de la fonction ait besoin de "passer" la longueur du tableau comme vous le suggérez, car il s'agit littéralement d'une valeur constante qui fait partie du type instancié au moment de la compilation.

Vous supposez simplement que les génériques fonctionneraient comme des modÚles C++ et des implémentations de fonctions en double, mais ce n'est tout simplement pas correct. La proposition autorise explicitement des implémentations uniques avec des paramÚtres cachés.

Je pense que si vous avez vraiment besoin de code modĂ©lisĂ© pour un petit nombre de types numĂ©riques, ce n'est pas si lourd d'utiliser un gĂ©nĂ©rateur de code. Les gĂ©nĂ©riques ne valent vraiment que la complexitĂ© du code pour des choses comme les types de conteneurs oĂč l'utilisation de types primitifs prĂ©sente un avantage mesurable en termes de performances, mais vous ne pouvez pas raisonnablement vous attendre Ă  gĂ©nĂ©rer un petit nombre de modĂšles de code Ă  l'avance.

Je n'ai évidemment aucune idée de la façon dont les responsables de golang vont finalement implémenter quoi que ce soit, donc je m'abstiendrai de spéculer davantage et m'en remettrai volontiers à ceux qui ont plus de connaissances d'initiés.

Ce que je sais, c'est que pour l'exemple de problÚme du monde réel que j'ai partagé ci-dessus, la différence de performances potentielle entre l'implémentation actuelle basée sur des tranches et une implémentation générique bien optimisée basée sur un tableau est substantielle.

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

Codez sur : https://github.com/vsivsi/hdtest

C'est une différence de performances potentielle de 5 fois pour le cas 4x64 bits (un point idéal dans mon travail) avec juste un petit déroulement de boucle (et essentiellement aucun code supplémentaire émis) dans le cas du tableau. Ces calculs se trouvent dans les boucles internes de mes algorithmes, effectués littéralement plusieurs billions de fois, donc une différence de performances de 5x est assez énorme. Mais pour réaliser ces gains d'efficacité aujourd'hui, je dois écrire chaque version de la fonction, pour chaque type d'élément et longueur de tableau nécessaires.

Mais oui, si des optimisations telles que celles-ci ne sont jamais mises en Ɠuvre par les mainteneurs, alors tout l'exercice consistant Ă  ajouter des longueurs de tableau paramĂ©trĂ©es aux gĂ©nĂ©riques serait inutile, du moins car cela pourrait profiter Ă  cet exemple.

Quoi qu'il en soit, discussion intéressante. Je sais que ce sont des questions litigieuses, alors merci de rester civil !

@vsivsi FWIW, les gains que vous observez disparaissent si vous ne déroulez pas manuellement vos boucles (ou si vous déroulez également la boucle sur une tranche) - donc cela ne supporte toujours pas votre argument selon lequel les paramÚtres entiers aident parce qu'ils permettent le compilateur pour faire le déroulement pour vous. Cela me semble une mauvaise science d'argumenter X sur Y, basé sur le compilateur devenant arbitrairement intelligent pour X et restant arbitrairement stupide pour Y. Je ne comprends pas pourquoi une heuristique de déroulement différente se déclencherait dans le cas d'une boucle sur un tableau , mais ne se déclenche pas dans le cas d'une boucle sur une tranche de longueur connue au moment de la compilation. Vous ne montrez pas les avantages d'une certaine saveur de génériques par rapport à une autre, vous montrez les avantages de cette heuristique de déroulement différente.

Mais dans tous les cas, personne n'a vraiment soutenu que générer du code spécialisé pour chaque instanciation d'une fonction générique ne serait pas potentiellement plus rapide - juste qu'il y a d'autres compromis à prendre en compte pour décider si vous voulez le faire.

@Merovius Je pense que le cas le plus fort pour les gĂ©nĂ©riques dans ce genre d'exemple est l'Ă©laboration au moment de la compilation (Ă©mettant ainsi une fonction unique pour chaque entier de niveau type) oĂč le code Ă  spĂ©cialiser se trouve dans une bibliothĂšque. Si l'utilisateur de la bibliothĂšque va utiliser un nombre limitĂ© d'instanciations de la fonction, il bĂ©nĂ©ficie alors d'une version optimisĂ©e. Donc, si mon code n'utilise que des tableaux de longueur 64, je peux utiliser des Ă©laborations optimisĂ©es des fonctions de la bibliothĂšque pour la longueur 64.

Dans ce cas précis, cela dépend de la distribution de fréquence des longueurs de tableau, car nous pourrions ne pas vouloir élaborer toutes les fonctions possibles s'il y en a des milliers en raison de contraintes de mémoire et de la suppression du cache de page qui pourrait ralentir les choses. Si, par exemple, les petites tailles sont courantes, mais de plus grandes sont possibles (une distribution de taille à longue traßne), nous pouvons alors élaborer des fonctions spécialisées pour les petits entiers avec des boucles déroulées (disons 1 à 64) et ensuite fournir une seule version généralisée avec un caché -paramÚtre pour le reste.

Je n'aime pas l'idée du "compilateur arbitrairement intelligent" et je pense que c'est un mauvais argument. Combien de temps devrai-je attendre ce compilateur arbitrairement intelligent ? Je n'aime particuliÚrement pas l'idée que le compilateur change de type, par exemple en optimisant une tranche en un tableau en créant des spécialisations cachées dans un langage avec réflexion, car lorsque vous réfléchissez sur cette tranche, quelque chose d'inattendu peut se produire.

En ce qui concerne le "dilemme gĂ©nĂ©rique", personnellement, j'irais avec "rendre le compilateur plus lent/faire plus de travail", mais essayez de le rendre aussi rapide que possible en utilisant une bonne implĂ©mentation et une compilation sĂ©parĂ©e. Rust semble plutĂŽt bien fonctionner, et aprĂšs l'annonce rĂ©cente d'Intel, il semble qu'il pourrait Ă©ventuellement remplacer "C" en tant que langage de programmation systĂšme principal. Le temps de compilation ne semblait mĂȘme pas ĂȘtre un facteur dans la dĂ©cision d'Intel, car la mĂ©moire d'exĂ©cution et la sĂ©curitĂ© de la concurrence avec une vitesse de type «C» semblaient ĂȘtre les facteurs clĂ©s. Les "traits" de Rust sont une implĂ©mentation raisonnable des classes de types gĂ©nĂ©riques, ils ont des cas de coin ennuyeux qui, je pense, proviennent de leur conception de systĂšme de type.

En revenant Ă  notre discussion prĂ©cĂ©dente, je dois faire attention Ă  sĂ©parer la discussion sur les gĂ©nĂ©riques en gĂ©nĂ©ral et sur la maniĂšre dont ils pourraient s'appliquer spĂ©cifiquement Ă  Go. En tant que tel, je ne suis pas sĂ»r que Go devrait mĂȘme avoir des gĂ©nĂ©riques car cela complique ce qui est un langage simple et Ă©lĂ©gant, de la mĂȘme maniĂšre que «C» n'a pas de gĂ©nĂ©riques. Je pense toujours qu'il y a une lacune sur le marchĂ© pour un langage qui a des implĂ©mentations gĂ©nĂ©riques comme fonctionnalitĂ© principale, mais qui reste simple et Ă©lĂ©gant.

Je me demande s'il y a eu des progrĂšs Ă  ce sujet.

Combien de temps je peux essayer les génériques. j'ai attendu longtemps

@Nsgj Vous pouvez consulter ce CL : https://go-review.googlesource.com/c/go/+/187317/

Dans la spécification actuelle, est-ce possible ?

contract Point(T) {
  T struct { X, Y float64 }
}

En d'autres termes, le type doit ĂȘtre une structure avec deux champs, X et Y, de type float64.

edit : avec un exemple d'utilisation

func generate(type T Point)() T {
  return T{X: randomFloat64(), Y: randomFloat64()}
}

@ abuchanan-nr Oui, le projet de conception actuel le permettrait, bien qu'il soit difficile de voir en quoi cela serait utile.

Je ne suis pas sûr non plus que ce soit utile, mais je n'ai pas vu d'exemple clair d'utilisation d'un type de structure personnalisé dans une liste de types d'un contrat. La plupart des exemples utilisent des types intégrés.

FWIW, j'imaginais une bibliothĂšque graphique 2D. Vous voudrez peut-ĂȘtre que chaque sommet ait un certain nombre de champs spĂ©cifiques Ă  l'application, comme la couleur, la force, etc. Mais vous voudrez peut-ĂȘtre aussi une bibliothĂšque gĂ©nĂ©rique de mĂ©thodes et d'algorithmes uniquement pour la partie gĂ©omĂ©trie, qui ne repose vraiment que sur les coordonnĂ©es X,Y. Il peut ĂȘtre intĂ©ressant de transmettre votre type de sommet personnalisĂ© dans cette bibliothĂšque, par exemple

type MyVertex struct {
  X, Y float64
  Color color.Color
  OtherAttr int
}
p := geo.RandomPolygon(MyVertex)()

for _, vert := range p.Vertices() {
  p.Color = randColor()
}

Encore une fois, pas sĂ»r que cela se rĂ©vĂšle ĂȘtre un bon design en pratique, mais c'est lĂ  oĂč en Ă©tait mon imagination Ă  l'Ă©poque :)

Voir https://godoc.org/image#Image pour savoir comment cela se fait dans Go standard aujourd'hui.

Concernant les Opérateurs/Types de contrats :

Cela entraßne une duplication de nombreuses méthodes génériques, car nous en aurions besoin au format opérateur ( + , == , < , ...) et au format méthode ( Plus(T) T , Equal(T) bool , LessThan(T) bool , ...).

Je propose d'unifier ces deux approches en une seule, le format de la mĂ©thode. Pour y parvenir, les types prĂ©-dĂ©clarĂ©s ( int , int64 , string , ...) devraient ĂȘtre convertis en types avec des mĂ©thodes arbitraires. Pour le cas simple (trivial) qui est dĂ©jĂ  possible ( type MyInt int; func (i MyInt) LessThan(o MyInt) bool {return int(i) < int(o)} ), mais la vraie valeur rĂ©side dans les types composites ( []int -> []MyInt , map[int]struct{} -> map[MyInt]struct{} , et ainsi de suite pour le canal, le pointeur, ...), ce qui n'est pas autorisĂ© (voir FAQ ). Permettre ces conversions est un changement important en soi, j'ai donc dĂ©veloppĂ© les aspects techniques de la proposition de conversion de type dĂ©contractĂ©e . Cela permettrait aux fonctions gĂ©nĂ©riques de ne pas traiter les opĂ©rateurs et de toujours prendre en charge tous les types, y compris ceux prĂ©-dĂ©clarĂ©s.

Notez que cette modification profite également aux types non prédéclarés. Dans la proposition actuelle, étant donné type X struct{S string} (qui provient d'une bibliothÚque externe, vous ne pouvez donc pas y ajouter de méthodes), disons que vous avez un []X et que vous voulez le passer à une fonction générique attend []T , pour T satisfaisant le Stringer . Cela nécessiterait un type X2 X; func(x X2) String() string {return x.S} et une copie complÚte de []X dans []X2 . Sous les modifications proposées à cette proposition, vous enregistrez entiÚrement la copie complÚte.

REMARQUE : la proposition de conversion de type assouplie mentionnée nécessite un défi.

@JavierZunzunegui Fournir un "format de mĂ©thode" (ou format d'opĂ©rateur) pour les opĂ©rateurs unaires/binaires de base n'est pas le problĂšme. Il est assez simple d'introduire des mĂ©thodes telles que +(x int) int en autorisant simplement les symboles d'opĂ©rateur comme noms de mĂ©thode, et d'Ă©tendre cela aux types intĂ©grĂ©s (bien que mĂȘme cela Ă©choue pour les dĂ©calages puisque l'opĂ©rateur de droite peut ĂȘtre un type entier arbitraire - nous n'avons pas de moyen d'exprimer cela pour le moment). Le problĂšme est que ce n'est pas suffisant. L'une des choses qu'un contrat doit exprimer est de savoir si une valeur x de type X peut ĂȘtre convertie en type d'un paramĂštre de type T comme dans T(x) (et vice versa). C'est-Ă -dire qu'il faut inventer un "format de mĂ©thode" pour les conversions autorisĂ©es. De plus, il doit y avoir un moyen d'exprimer qu'une constante non typĂ©e c peut ĂȘtre assignĂ©e Ă  (ou convertie en) une variable de type paramĂštre de type T : est-il lĂ©gal d'assigner, disons, 256 Ă  t de type T ? Et si T est byte ? Il y a quelques autres choses comme ça. On peut inventer une notation de "format de mĂ©thode" pour ces choses, mais cela se complique rapidement, et il n'est pas clair que ce soit plus comprĂ©hensible ou lisible.

Je ne dis pas que ce n'est pas possible, mais nous n'avons pas trouvé d'approche satisfaisante et claire. Le projet de conception actuel qui énumÚre simplement les types d'autre part est assez simple à comprendre.

@griesemer Cela peut ĂȘtre difficile en Go en raison d'autres prioritĂ©s, mais c'est un problĂšme assez bien rĂ©solu en gĂ©nĂ©ral. C'est l'une des raisons pour lesquelles je considĂšre les conversions implicites comme mauvaises. Il y a d'autres raisons comme la magie qui se produit qui n'est pas visible pour quelqu'un qui lit le code.

S'il n'y a pas de conversions implicites dans le systÚme de types, je peux utiliser la surcharge pour contrÎler précisément la plage de types acceptés, et les interfaces contrÎlent la surcharge.

J'aurais tendance Ă  exprimer la similitude entre les types Ă  l'aide d'interfaces, donc des opĂ©rations comme '+' seraient exprimĂ©es de maniĂšre gĂ©nĂ©rique comme des opĂ©rations sur une interface numĂ©rique plutĂŽt qu'un type. Vous devez avoir des variables de type ainsi que des interfaces pour exprimer la contrainte selon laquelle les arguments et le rĂ©sultat de l'addition doivent ĂȘtre du mĂȘme type.

Donc, ici, l'opĂ©rateur d'addition est dĂ©clarĂ© pour fonctionner sur des types avec une interface numĂ©rique. Cela correspond bien aux mathĂ©matiques, oĂč les "entiers" et "l'addition" forment un "groupe" par exemple.

Vous vous retrouveriez avec quelque chose comme :

+(T Addable)(x T, y T) T

Si vous autorisez la sĂ©lection d'interface implicite, l'opĂ©rateur '+' peut simplement ĂȘtre une mĂ©thode de l'interface numĂ©rique, mais je pense que cela causerait des problĂšmes avec la sĂ©lection de mĂ©thode dans Go?

@griesemer sur votre point sur les conversions :

L'une des choses qu'un contrat doit exprimer est de savoir si une valeur x de type X peut ĂȘtre convertie en type d'un paramĂštre de type T comme dans T(x) (et vice versa). Autrement dit, il faut inventer un "format de mĂ©thode" pour les conversions autorisĂ©es

Je peux voir comment ce serait une complication, mais je ne pense pas que ce soit nécessaire. Selon moi, de telles conversions se produiraient en dehors du code générique, par l'appelant. Un exemple (en utilisant Stringify selon le projet de conception):

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

Ci-dessus, en ce qui concerne Stringify , l'argument est de type []MyInt et respecte le contrat. Le code générique ne peut pas convertir les types génériques en quoi que ce soit d'autre (autre que les interfaces qu'ils implémentent, conformément au contrat), précisément parce que leur contrat ne dit rien à ce sujet.

@JavierZunzunegui Je ne vois pas comment l'appelant peut faire de telles conversions sans les exposer dans l'interface/le contrat. Par exemple, je pourrais vouloir implémenter un algorithme numérique générique (une fonction paramétrée) fonctionnant sur divers types entiers ou à virgule flottante. Dans le cadre de cet algorithme, le code de la fonction doit affecter des valeurs constantes c1 , c2 , etc. aux valeurs du type de paramÚtre T . Je ne vois pas comment le code peut faire cela sans savoir qu'il est correct d'affecter ces constantes à une variable de type T . (On ne voudrait certainement pas avoir à passer ces constantes dans la fonction.)

func NumericAlgorithm(type T SomeContract)(vector []T) T {
   ...
   vector[i] = 3.1415  // <<< how do we know this is valid without the contract telling us?
   ...
}

doit attribuer des valeurs constantes c1 , c2 , etc. aux valeurs du type de paramĂštre T

@griesemer Je dirais (Ă  mon avis sur la façon dont les gĂ©nĂ©riques sont/devraient ĂȘtre) que ce qui prĂ©cĂšde est le mauvais Ă©noncĂ© du problĂšme. Vous exigez que T soit dĂ©fini comme un float32 , mais un contrat indique uniquement quelles mĂ©thodes sont disponibles pour T , pas ce qu'il est dĂ©fini comme. Si vous en avez besoin, vous pouvez soit conserver vector comme []T et exiger un argument func(float32) T ( vector[i] = f(c1) ), soit mieux conserver vector comme []float32 et exige T par contrat d'avoir une mĂ©thode DoSomething(float32) ou DoSomething([]float32) , puisque je suppose que le T et le les flotteurs doivent interagir Ă  un moment donnĂ©. Cela signifie que T peut ou non ĂȘtre dĂ©fini comme type T float32 , tout ce que nous pouvons dire, c'est qu'il a les mĂ©thodes requises par le contrat.

@JavierZunzunegui Je ne dis pas du tout que T soit dĂ©fini comme un float32 - cela pourrait ĂȘtre un float32 , un float64 , ou mĂȘme l'un des types complexes. Plus gĂ©nĂ©ralement, si la constante Ă©tait un entier, il pourrait y avoir une variĂ©tĂ© de types d'entiers valides Ă  transmettre Ă  cette fonction, et certains qui ne le sont pas. Ce n'est certainement pas un "mauvais Ă©noncĂ© de problĂšme". Le problĂšme est rĂ©el - ce n'est certainement pas artificiel du tout de vouloir pouvoir Ă©crire de telles fonctions - et le problĂšme ne disparaĂźt pas en le dĂ©clarant "faux".

@griesemer je vois, je pensais que vous étiez uniquement concerné par la conversion, je n'ai pas enregistré l'élément clé qu'il traite des constantes non typées.

Vous pouvez faire selon ma réponse ci-dessus, avec T ayant une méthode DoSomething(X) , et la fonction prenant un argument supplémentaire func(float64) X , donc la forme générique est définie par deux types ( T,X ). La façon dont vous décrivez le problÚme X est normalement float32 ou float64 et l'argument de la fonction est func(f float64) float32 {return float32(f)} ou func(f float64) float64 {return f} .

Plus important encore, comme vous le soulignez, pour le cas des nombres entiers, il y a le problÚme que des formats entiers moins précis peuvent ne pas suffire pour une constante donnée. L'approche la plus sûre consiste à garder privée la fonction générique à deux types ( T,X ) et à n'exposer publiquement que MyFunc32 / MyFunc64 /etc.

Je concĂšde que MyFunc32(int32) / MyFunc64(int64) /etc. est moins pratique qu'un seul MyFunc(type T Numeric) (le contraire est indĂ©fendable !). Mais ce n'est que pour les implĂ©mentations gĂ©nĂ©riques reposant sur une constante, et principalement une constante entiĂšre - combien y en a-t-il? Pour le reste, vous bĂ©nĂ©ficiez de la libertĂ© supplĂ©mentaire de ne pas ĂȘtre limitĂ© Ă  quelques types intĂ©grĂ©s pour T .

Et bien sĂ»r, si la fonction n'est pas chĂšre, vous pourriez ĂȘtre parfaitement d'accord pour faire le calcul comme int64 / float64 et n'exposer que cela, en le gardant Ă  la fois simple et sans restriction sur T .

Nous ne pouvons vraiment pas dire aux gens "vous pouvez écrire des fonctions génériques sur n'importe quel type T mais ces fonctions génériques ne peuvent pas utiliser de constantes non typées". Go est avant tout un langage simple. Les langues avec des restrictions bizarres comme ça ne sont pas simples.

Chaque fois qu'une approche proposée des génériques devient difficile à expliquer de maniÚre simple, nous devons écarter cette approche. Il est plus important de garder le langage simple que d'ajouter des génériques au langage.

@JavierZunzunegui L'une des propriĂ©tĂ©s intĂ©ressantes du code paramĂ©trĂ© (gĂ©nĂ©rique) est que le compilateur peut le personnaliser en fonction du ou des types avec lesquels le code est instanciĂ©. Par exemple, on peut vouloir utiliser un type byte plutĂŽt que int car cela conduit Ă  des Ă©conomies d'espace importantes (imaginez une fonction qui alloue d'Ă©normes tranches du type gĂ©nĂ©rique). Donc, limiter simplement le code Ă  un type "assez grand" est une rĂ©ponse insatisfaisante, mĂȘme pour un langage "opiniĂątre" tel que Go.

De plus, il ne s'agit pas seulement d'algorithmes qui utilisent de "grandes" constantes non typĂ©es qui peuvent ne pas ĂȘtre si courantes : rejeter de tels algorithmes avec une question "combien y en a-t-il de toute façon" revient simplement Ă  agiter la main pour dĂ©tourner un problĂšme qui existe. Juste pour votre considĂ©ration : il ne semble pas dĂ©raisonnable pour un grand nombre d'algorithmes d'utiliser des constantes entiĂšres telles que -1, 0, 1. Notez que l'on ne peut pas utiliser -1 en conjonction avec des entiers non typĂ©s, juste pour vous donner un exemple simple. De toute Ă©vidence, nous ne pouvons pas simplement ignorer cela. Nous devons pouvoir le prĂ©ciser dans un contrat.

@ianlancetaylor @griesemer merci pour les commentaires - je peux voir qu'il y a un conflit important dans ma proposition de changement avec des constantes non typées et des entiers négatifs, je vais le mettre derriÚre moi.

Puis-je attirer votre attention sur le deuxiÚme point de https://github.com/golang/go/issues/15292#issuecomment -546313279 :

Notez que cette modification profite également aux types non prédéclarés. Dans la proposition actuelle, étant donné le type X struct{S string} (qui provient d'une bibliothÚque externe, vous ne pouvez donc pas y ajouter de méthodes), disons que vous avez un []X et que vous souhaitez le passer à une fonction générique attendant [ ]T, pour T satisfaisant le contrat de Stringer. Cela nécessiterait un type X2 X ; func(x X2) String() string {return xS}, et une copie complÚte de []X dans []X2. Sous les modifications proposées à cette proposition, vous enregistrez entiÚrement la copie complÚte.

L'assouplissement des rĂšgles de conversion (si cela est techniquement faisable) serait toujours utile.

@JavierZunzunegui Discuter des conversions du type []B([]A) si B(a) (avec a de type A ) est autorisĂ© semble ĂȘtre principalement orthogonal aux fonctionnalitĂ©s gĂ©nĂ©riques. Je pense que nous n'avons pas besoin d'apporter cela ici.

@ianlancetaylor Je ne sais pas Ă  quel point cela est pertinent pour Go, mais je ne pense pas que les constantes soient vraiment non typĂ©es, elles doivent avoir un type car le compilateur doit choisir une reprĂ©sentation machine. Je pense qu'un meilleur terme est les constantes de type indĂ©terminĂ©, car la constante peut ĂȘtre reprĂ©sentable par plusieurs types diffĂ©rents. Une solution consiste Ă  utiliser un type d'union afin qu'une constante telle que 27 ait un type tel que int16|int32|float16|float32 une union de tous les types possibles . Alors T dans un type gĂ©nĂ©rique peut ĂȘtre ce type d'union. La seule exigence est que nous devions Ă  un moment donnĂ© rĂ©soudre l'union en un seul type. Le cas le plus problĂ©matique serait quelque chose comme print(27) car il n'y a jamais un seul type Ă  rĂ©soudre, dans de tels cas, n'importe quel type de l'union ferait l'affaire, et nous pourrions choisir en fonction d'un paramĂštre d'optimisation comme l'espace/la vitesse, etc. .

@keean Le nom exact et la gestion de ce que la spécification appelle des "constantes non typées" sont hors sujet sur ce problÚme. Prenons s'il vous plaßt cette discussion ailleurs. Merci.

@ianlancetaylor J'en suis heureux, cependant c'est l'une des raisons pour lesquelles je pense que Go ne peut pas avoir une implĂ©mentation gĂ©nĂ©rique propre/simple, tous ces problĂšmes sont interconnectĂ©s, et les choix originaux faits pour Go n'ont pas Ă©tĂ© pris avec la programmation gĂ©nĂ©rique Ă  l'esprit. Je pense qu'un autre langage, conçu pour rendre les gĂ©nĂ©riques simples par conception, est nĂ©cessaire, pour Go, les gĂ©nĂ©riques seront toujours quelque chose d'ajoutĂ© au langage plus tard, et la meilleure option pour garder le langage propre et simple peut ĂȘtre de ne pas les avoir du tout.

Si je concevais aujourd'hui un langage simple avec des temps de compilation rapides et une flexibilité comparable, je choisirais la surcharge de méthode et le polymorphisme structurel (sous-typage) via des interfaces golang et pas de génériques. En fait, cela permettrait de surcharger différentes interfaces anonymes avec différents champs.

Le choix des génériques a l'avantage de la réutilisabilité du code propre, mais il introduit plus de bruit qui se complique si des contraintes sont ajoutées conduisant parfois à un code difficilement compréhensible.
Ensuite, si nous avons des gĂ©nĂ©riques, pourquoi ne pas utiliser un systĂšme de contraintes avancĂ© comme une clause where, des types de type supĂ©rieur ou peut-ĂȘtre des types de rang supĂ©rieur et Ă©galement un typage dĂ©pendant ?
Toutes ces questions finiront par se poser si vous allez adopter les génériques, tÎt ou tard.

En clair, je ne suis pas contre les génériques, mais je me demande si c'est la voie à suivre pour conserver la simplicité du go.

Si l'introduction de génériques dans go est inévitable, alors il serait raisonnable de réfléchir à l'impact sur les temps de compilation lors de la monomorphisation de fonctions génériques.
Ne serait-ce pas une bonne valeur par défaut pour les génériques de boßte, c'est-à-dire générer une copie pour tous les types d'entrée ensemble, et ne se spécialiser que si l'utilisateur le demande explicitement avec une annotation au niveau de la définition - ou du site d'appel ?

En ce qui concerne l'impact sur les performances d'exécution, cela réduirait les performances en raison du problÚme de boxing/unboxing, sinon, des ingénieurs experts en c++ recommandent des génériques de boxing comme le fait Java afin d'atténuer les échecs de cache.

@ianlancetaylor @griesemer J'ai reconsidéré la question des constantes non typées et des génériques "non-opérateurs" (https://github.com/golang/go/issues/15292#issuecomment-547166519) et j'ai trouvé une meilleure façon de traiter avec ça.

Donnez les types numĂ©riques ( type MyInt32 int32 , type MyInt64 int64 , ...), ceux-ci ont de nombreuses mĂ©thodes satisfaisant le mĂȘme contrat ( Add(T) T , ...) mais surtout pas d'autres qui risquerait de dĂ©border func(MyInt64) FromI64(int64) MyInt64 mais non ~ func(MyInt32) FromI64(int64) MyInt32 ~. Cela permet d'utiliser des constantes numĂ©riques (explicitement affectĂ©es Ă  la valeur de prĂ©cision la plus faible dont elles ont besoin) en toute sĂ©curitĂ© (1) car les types numĂ©riques de faible prĂ©cision ne satisferont pas au contrat requis, mais tous les plus Ă©levĂ©s le feront. Voir terrain de jeu , en utilisant des interfaces Ă  la place des gĂ©nĂ©riques.

Un avantage d'assouplir les gĂ©nĂ©riques numĂ©riques au-delĂ  des types intĂ©grĂ©s (non spĂ©cifiques Ă  cette derniĂšre rĂ©vision, j'aurais donc dĂ» le partager la semaine derniĂšre) est qu'il permet d'instancier des mĂ©thodes gĂ©nĂ©riques avec des types de contrĂŽle de dĂ©bordement - voir playground . La vĂ©rification des dĂ©bordements est elle-mĂȘme une demande/proposition trĂšs populaire (https://github.com/golang/go/issues/31500 et problĂšmes connexes).


(1) : La garantie de non-dĂ©bordement Ă  la compilation pour les constantes non typĂ©es est forte dans la mĂȘme 'branche' ( int[8/16/32/64] et uint[8/16/32/64] ). Traversant les branches, une constante uint[X] n'est instanciĂ©e en toute sĂ©curitĂ© qu'en int[2X+] et une constante int[X] ne peut pas ĂȘtre instanciĂ©e en toute sĂ©curitĂ© par n'importe quel uint[X] . MĂȘme les assouplir (autoriser int[X]<->uint[X] ) serait simple et sĂ»r en suivant certaines normes minimales, et de maniĂšre critique, toute complexitĂ© incombe Ă  l'auteur du code gĂ©nĂ©rique, et non Ă  l'utilisateur du gĂ©nĂ©rique (qui n'est concernĂ© que par le contrat , et peut s'attendre Ă  ce que tout type numĂ©rique qui le rencontre soit valide).

Les méthodes génériques - c'était la chute de Java !

@ianlancetaylor J'en suis heureux, cependant c'est l'une des raisons pour lesquelles je pense que Go ne peut pas avoir une implĂ©mentation gĂ©nĂ©rique propre/simple, tous ces problĂšmes sont interconnectĂ©s, et les choix originaux faits pour Go n'ont pas Ă©tĂ© pris avec la programmation gĂ©nĂ©rique Ă  l'esprit. Je pense qu'un autre langage, conçu pour rendre les gĂ©nĂ©riques simples par conception, est nĂ©cessaire, pour Go, les gĂ©nĂ©riques seront toujours quelque chose d'ajoutĂ© au langage plus tard, et la meilleure option pour garder le langage propre et simple peut ĂȘtre de ne pas les avoir du tout.

Je suis d'accord à 100 %. Autant j'aimerais voir une sorte de génériques implémentés, autant je pense que ce que vous préparez actuellement va détruire la simplicité du langage Go.

L'idée actuelle d'étendre les interfaces ressemble à ceci :

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

Désolé tout le monde, mais s'il vous plait ne faites pas ça ! Cela enlaidit la beauté du Go en grand.

Ayant écrit prÚs de 100 000 lignes de code Go maintenant, je suis d'accord pour ne pas avoir de génériques.

Cependant, de petites choses comme soutenir

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

aiderait.

Proposition de syntaxe pour pouvoir séparer complÚtement les génériques du code Go régulier

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 Cependant, de petites choses comme soutenir
... autoriser plusieurs types dans les déclarations Slices et Maps

Cela ne répond pas aux besoins de certaines fonctions de tranche génériques fonctionnelles, par exemple head() , tail() , map(slice, func) , filter(slice, func)

Vous pouvez simplement l'Ă©crire vous-mĂȘme pour chaque projet dans lequel vous en avez besoin, mais Ă  ce stade, il y a un risque de devenir obsolĂšte en raison de la rĂ©pĂ©tition du copier-coller et encourage la complexitĂ© du code Go pour Ă©conomiser la simplicitĂ© du langage.

(Sur le plan personnel, c'est aussi un peu fatigant de savoir que j'ai un ensemble de fonctionnalités que je veux implémenter et de ne pas avoir une maniÚre propre de les exprimer sans répondre également aux contraintes linguistiques)

Considérez ce qui suit dans le go actuel et non générique :

J'ai une variable x de type externallib.Foo , obtenue Ă  partir d'une bibliothĂšque externallib que je ne contrĂŽle pas.
Je veux le passer à une fonction SomeFunc(fmt.Stringer) , mais externallib.Foo n'a pas de méthode String() string . Je peux simplement faire :

type MyFoo externallib.Foo
func (mf MyFoo) String() string {...}
// ...
SomeFunc(MyFoo(x))

ConsidĂ©rez la mĂȘme chose avec les gĂ©nĂ©riques.

J'ai une variable x de type []externallib.Foo . Je veux le passer Ă  AnotherFunc(type T Stringer)(s []T) . Cela ne peut pas ĂȘtre fait sans une copie profonde coĂ»teuse de la tranche dans un nouveau []MyFoo . Si au lieu d'une tranche, il s'agissait d'un type plus complexe (par exemple, un chan ou une carte), ou si la mĂ©thode modifiait le rĂ©cepteur, cela devient encore plus inefficace et fastidieux, si possible.

Ce n'est peut-ĂȘtre pas un problĂšme dans la bibliothĂšque standard, mais c'est uniquement parce qu'elle n'a pas de dĂ©pendances externes. C'est un luxe que pratiquement aucun autre projet n'aura.

Ma suggestion est d'assouplir la conversion pour autoriser []Foo([]Bar{}) pour tout Foo défini comme type Foo Bar , ou vice versa, et également pour les cartes, les tableaux, les canaux et les pointeurs, de maniÚre récursive. Notez que ce sont toutes des copies peu profondes bon marché. Plus de détails techniques dans Proposition de conversion de type assoupli .


Cela a été évoqué pour la premiÚre fois en tant que fonctionnalité secondaire dans https://github.com/golang/go/issues/15292#issuecomment -546313279.

@JavierZunzunegui Je ne pense pas que ce soit vraiment liĂ© aux gĂ©nĂ©riques. Oui, vous pouvez fournir un exemple en utilisant des gĂ©nĂ©riques, mais vous pouvez fournir un exemple similaire sans utiliser de gĂ©nĂ©riques. Je pense que cette question devrait ĂȘtre discutĂ©e sĂ©parĂ©ment, pas ici. Voir aussi https://golang.org/doc/faq#convert_slice_with_same_underlying_type. Merci.

Sans génériques, une telle conversion n'a pratiquement aucune valeur, car en général []Foo ne rencontrera aucune interface, ou du moins aucune interface qui l'utilise comme une tranche. L'exception concerne les interfaces qui ont un modÚle trÚs spécifique pour l'utiliser, comme sort.Interface , pour lesquelles vous n'avez de toute façon pas besoin de convertir la tranche.

La version non générique de ce qui précÚde ( func AnotherFunc(type T Stringer)(s []T) ) est

type SliceOfStringers interface {
  Len() int
  Get(int) fmt.Stringer
}
func AnotherFunc(s SliceOfStringers) {...}

C'est peut-ĂȘtre moins pratique que l'approche gĂ©nĂ©rique, mais on peut faire en sorte qu'elle gĂšre bien n'importe quelle tranche et le fasse sans la copier, que le type sous-jacent soit en fait un fmt.Stringer . Dans l'Ă©tat actuel des choses, les gĂ©nĂ©riques ne le peuvent pas, mĂȘme s'ils sont en principe un outil beaucoup plus adaptĂ© Ă  la tĂąche. Et sĂ»rement, si nous ajoutons des gĂ©nĂ©riques, c'est prĂ©cisĂ©ment pour rendre les tranches, les cartes, etc. plus courantes dans les API, et pour les manipuler avec moins de passe-partout. Pourtant, ils introduisent un nouveau problĂšme, sans Ă©quivalence dans un monde d'interface uniquement, qui _peut-ĂȘtre_ mĂȘme pas inĂ©vitable mais artificiellement imposĂ© par le langage.

La conversion de type que vous mentionnez revient assez souvent dans du code non générique pour qu'il s'agisse d'une FAQ. Déplaçons cette discussion ailleurs. Merci.

Quel est l'Ă©tat de ceci ? Un brouillon MISE À JOUR ? J'attends les gĂ©nĂ©riques depuis
il y a presque 2 ans. Quand aurons-nous des génériques ?

Le mar., 4 fév. de 2020 à la(s) 13:28, Ian Lance Taylor (
[email protected]) Ă©crit :

La conversion de type que vous mentionnez revient assez souvent dans du code non générique
qu'il s'agit d'une FAQ. Déplaçons cette discussion ailleurs. Merci.

—
Vous recevez ceci parce que vous avez été mentionné.
RĂ©pondez directement Ă  cet e-mail, consultez-le sur GitHub
https://github.com/golang/go/issues/15292?email_source=notifications&email_token=AJMFNBN3MFHDMENAFXIKBLDRBGXUTA5CNFSM4CA35RX2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEKYV5RI#issuecomment-577,049
ou désabonnez-vous
https://github.com/notifications/unsubscribe-auth/AJMFNBO5UTKNPL3MSA3NESLRBGXUTANCNFSM4CA35RXQ
.

--
Ceci est un test pour les signatures de courrier Ă  utiliser dans TripleMint

Nous y travaillons. Certaines choses prennent du temps.

Le travail est-il effectué hors ligne ? J'aimerais le voir évoluer dans le temps, d'une maniÚre que le "grand public" comme moi ne puisse pas commenter pour éviter le bruit.

Bien qu'il ait Ă©tĂ© fermĂ© depuis pour garder la discussion sur les gĂ©nĂ©riques au mĂȘme endroit, consultez # 36177 oĂč @Griesemer Ă©tablit un lien vers un prototype sur lequel il travaille et fait des commentaires intĂ©ressants sur ses rĂ©flexions Ă  ce sujet jusqu'Ă  prĂ©sent.

Je pense que j'ai raison de dire que le prototype ne traite que des aspects de vérification de type de la proposition de projet de « contrats » à l'heure actuelle, mais le travail me semble certainement prometteur.

@ianlancetaylor Chaque fois qu'une approche proposée des génériques devient difficile à expliquer de maniÚre simple, nous devons abandonner cette approche. Il est plus important de garder le langage simple que d'ajouter des génériques au langage.

C'est un grand idéal à atteindre, mais en réalité, le développement de logiciels n'est parfois pas _simple à expliquer_.

Lorsque le langage est limité pour exprimer de telles idées _pas simples à exprimer_, les ingénieurs logiciels finissent par réinventer ces fonctionnalités encore et encore, parce que ces maudites idées _difficiles à exprimer_ sont parfois essentielles à la logique des programmes.

Regardez Istio, Kubernetes, operator-sdk et, dans une certaine mesure, Terraform et mĂȘme la bibliothĂšque protobuf. Ils Ă©chappent tous au systĂšme de type Go en utilisant la rĂ©flexion, en implĂ©mentant un nouveau systĂšme de type au-dessus de Go en utilisant des interfaces et la gĂ©nĂ©ration de code, ou une combinaison de ceux-ci.

@omeid

Regardez Istio, Kubernetes

Vous est-il déjà venu à l'esprit que la raison pour laquelle ils font ces choses absurdes est que leur conception de base n'a aucun sens, et par conséquent, ils ont dû aboutir aux jeux reflect pour le remplir ?

Je maintiens que de meilleures conceptions pour les programmes golang (à la fois dans la phase de conception et dans l'API) ne nécessitent pas de génériques.

Veuillez ne pas les ajouter Ă  golang.

La programmation est difficile. Kubelet est un endroit sombre. Les génériques divisent les gens plus que la politique américaine. Je veux croire.

Lorsque le langage est limité pour exprimer des idées aussi difficiles à exprimer, les ingénieurs en logiciel finissent par réinventer ces fonctionnalités encore et encore, car ces idées sacrément difficiles à exprimer sont parfois essentielles à la logique des programmes.

Regardez Istio, Kubernetes, operator-sdk et, dans une certaine mesure, Terraform et mĂȘme la bibliothĂšque protobuf. Ils Ă©chappent tous au systĂšme de type Go en utilisant la rĂ©flexion, en implĂ©mentant un nouveau systĂšme de type au-dessus de Go en utilisant des interfaces et la gĂ©nĂ©ration de code, ou une combinaison de ceux-ci.

Je ne trouve pas que ce soit un argument convaincant. Le langage Go devrait idĂ©alement ĂȘtre facile Ă  lire, Ă  Ă©crire et Ă  comprendre, tout en permettant d'effectuer des opĂ©rations arbitrairement complexes. Cela correspond Ă  ce que vous dites : les outils que vous mentionnez doivent faire quelque chose de complexe, et Go leur donne un moyen de le faire.

Le langage Go devrait idĂ©alement ĂȘtre facile Ă  lire, Ă  Ă©crire et Ă  comprendre, tout en permettant d'effectuer des opĂ©rations arbitrairement complexes.

Je suis d'accord avec cela, mais parce que ce sont des objectifs multiples, ils seront parfois en tension les uns avec les autres. Un code qui "veut" naturellement ĂȘtre Ă©crit dans un style gĂ©nĂ©rique devient souvent moins facile Ă  lire qu'il ne le serait autrement lorsqu'il doit recourir Ă  des techniques comme la rĂ©flexion.

Un code qui "veut" naturellement ĂȘtre Ă©crit dans un style gĂ©nĂ©rique devient souvent moins facile Ă  lire qu'il ne le serait autrement lorsqu'il doit recourir Ă  des techniques comme la rĂ©flexion.

C'est pourquoi cette proposition reste ouverte et pourquoi nous avons un projet de conception pour une éventuelle implémentation de génériques (https://blog.golang.org/why-generics).

Regardez ... mĂȘme la bibliothĂšque protobuf. Ils Ă©chappent tous au systĂšme de type Go en utilisant la rĂ©flexion, en implĂ©mentant un nouveau systĂšme de type au-dessus de Go en utilisant des interfaces et la gĂ©nĂ©ration de code, ou une combinaison de ceux-ci.

Parlant d'expĂ©rience avec les protobufs, il y a quelques cas oĂč les gĂ©nĂ©riques peuvent amĂ©liorer la convivialitĂ© et/ou la mise en Ɠuvre de l'API, mais la grande majoritĂ© de la logique ne bĂ©nĂ©ficiera pas des gĂ©nĂ©riques. Les gĂ©nĂ©riques supposent que les informations de type concret sont connues au moment de la compilation . Pour protobufs, la plupart des situations impliquent des cas oĂč les informations de type ne sont connues qu'au moment de l'exĂ©cution .

En général, je remarque que les gens pointent souvent du doigt toute utilisation de la réflexion et prétendent que c'est une preuve de la nécessité des génériques. Ce n'est pas si simple. Une distinction cruciale est de savoir si les informations de type sont connues au moment de la compilation ou non. Dans un certain nombre de cas, ce n'est fondamentalement pas le cas.

@dsnet IntĂ©ressant merci, je n'ai jamais pensĂ© Ă  protobuf pour ne pas ĂȘtre conforme au gĂ©nĂ©rique. Toujours supposĂ© que chaque outil qui gĂ©nĂšre du code passe-partout comme par exemple le protocole, basĂ© sur un schĂ©ma prĂ©dĂ©fini, serait capable de gĂ©nĂ©rer du code gĂ©nĂ©rique sans rĂ©flexion en utilisant la proposition gĂ©nĂ©rique actuelle. Cela vous dĂ©rangerait-il de mettre Ă  jour cela dans la spĂ©cification avec un exemple ou dans un nouveau billet de blog oĂč vous dĂ©crivez ce problĂšme plus en dĂ©tail ?

les outils que vous mentionnez doivent faire quelque chose de complexe, et Go leur donne un moyen de le faire.

L'utilisation de modÚles de texte pour générer du code Go n'est pas une installation de par sa conception, je dirais qu'il s'agit d'un pansement ad hoc, idéalement, au moins les packages ast et parser standard devraient permettre de générer du code Go arbitraire.

La seule chose que vous pouvez affirmer que Go donne pour gĂ©rer une logique complexe est peut-ĂȘtre la rĂ©flexion, mais cela montre rapidement ses limites, sans parler du code critique pour les performances, mĂȘme lorsqu'il est utilisĂ© dans la bibliothĂšque standard, par exemple la gestion JSON de Go est primitive au mieux.

Il est difficile d'affirmer que l'utilisation de modÚles de texte ou de réflexion pour faire _quelque chose de déjà complexe_ correspond à l'idéal de :

Chaque fois qu'une approche proposée pour ~génériques~ quelque chose de complexe devient difficile à expliquer de maniÚre simple, nous devons abandonner cette approche.

Je pense que la solution que les projets ont mentionnée pour résoudre leur problÚme est trop complexe et pas facile à comprendre. Donc, à cet égard, Go ne dispose pas des fonctionnalités permettant aux utilisateurs d'exprimer des problÚmes complexes en termes aussi simples et directs que possible.

En général, je remarque que les gens pointent souvent du doigt toute utilisation de la réflexion et prétendent que c'est une preuve de la nécessité des génériques.

Peut-ĂȘtre y a-t-il une telle idĂ©e fausse gĂ©nĂ©rale, mais la bibliothĂšque protobuf, en particulier la nouvelle API, pourrait ĂȘtre beaucoup plus simple avec _generics_, ou une sorte de _sum type_.

L'un des auteurs de cette nouvelle API protobuf vient de dire "la grande majoritĂ© de la logique ne bĂ©nĂ©ficiera pas des gĂ©nĂ©riques", donc je ne sais pas oĂč vous obtenez que "spĂ©cialement la nouvelle API pourrait ĂȘtre beaucoup plus importante simple avec des gĂ©nĂ©riques". Sur quoi est-ce basĂ©? Pouvez-vous prouver que ce serait beaucoup plus simple?

En tant que personne ayant utilisé les API protobuf dans quelques langages incluant des génériques (Java, C++), je ne peux pas dire que j'ai remarqué des différences d'utilisation significatives avec l'API Go et leurs API. Si votre affirmation était vraie, je m'attendrais à ce qu'il y ait une telle différence.

@dsnet a Ă©galement dĂ©clarĂ© "il y a quelques cas oĂč les gĂ©nĂ©riques peuvent amĂ©liorer la convivialitĂ© et/ou la mise en Ɠuvre de l'API".

Mais si vous voulez un exemple de la façon dont les choses peuvent ĂȘtre plus simples, commencez par supprimer le type Value car il s'agit en grande partie d'un type de somme ad hoc.

@omeid Ce problÚme concerne les génériques, pas les types de somme. Je ne sais donc pas en quoi cet exemple est pertinent.

Plus précisément, ma question est la suivante: comment le fait d'avoir des génériques se traduirait-il par une implémentation ou une API de protobuf qui est "beaucoup plus simple" que la nouvelle (ou l'ancienne, d'ailleurs) API?

Cela ne semble pas correspondre à ma lecture de ce que @dsnet a dit ci-dessus, ni à mon expérience avec les API protobuf Java et C++.

De plus, votre commentaire sur la gestion JSON primitive dans Go me semble également tout aussi étrange. Pouvez-vous expliquer comment vous pensez que l'API encoding/json serait améliorée par les génériques ?

AFAIK, les implémentations de l'analyse JSON en Java utilisent la réflexion (et non les génériques). Il est vrai que l'API de niveau supérieur dans la plupart des bibliothÚques JSON utilisera probablement une méthode générique (par exemple Gson ), mais une méthode qui prend un paramÚtre générique sans contrainte T et renvoie une valeur de type T fournit trÚs peu de vérification de type supplémentaire par rapport à json.Unmarshal . En fait, je pense que la seule erreur que le seul scénario d'erreur supplémentaire non détecté par json.Unmarshal au moment de la compilation est si vous transmettez une valeur non pointeur. (Notez également les mises en garde dans la documentation de l'API de Gson pour utiliser une fonction différente pour les types génériques et non génériques. Encore une fois, cela montre que les génériques ont compliqué leur API, plutÎt que de la simplifier ; dans ce cas, il s'agit de prendre en charge la sérialisation/désérialisation générique les types).

(La prise en charge de JSON en C++ est pire que l'AFAICT ; les différentes approches que je connais utilisent soit des quantités importantes de macros, soit impliquent l'écriture manuelle de fonctions d'analyse/sérialisation. Encore une fois, ce n'est pas le cas)

Si vous vous attendez à ce que les génériques ajoutent beaucoup au support de Go pour JSON, je crains que vous ne soyez déçu.


@gertcuykens Chaque implémentation de protobuf dans tous les langages que je connais utilise la génération de code, qu'ils aient ou non des génériques. Cela inclut Java, C++, Swift, Rust, JS (et TS). Je ne pense pas que le fait d'avoir des génériques supprime automatiquement toutes les utilisations de la génération de code (comme preuve d'existence, j'ai écrit des générateurs de code qui génÚrent du code Java et du code C++); il semble illogique de s'attendre à ce que toute solution pour les génériques respecte cette barre.


Juste pour ĂȘtre absolument clair : je soutiens l'ajout de gĂ©nĂ©riques Ă  Go. Mais je pense que nous devrions ĂȘtre lucides quant Ă  ce que nous allons en retirer. Je ne pense pas que nous obtiendrons des amĂ©liorations significatives pour les API protobuf ou JSON.

Je ne pense pas que protobuf soit un cas particuliÚrement bon pour les génériques. Vous n'avez pas besoin de génériques dans le langage cible car vous pouvez simplement générer directement du code spécialisé. Cela s'appliquerait également à d'autres systÚmes similaires comme Swagger/OpenAPI.

LĂ  oĂč les gĂ©nĂ©riques me sembleraient utiles et pourraient offrir Ă  la fois une simplification et une sĂ©curitĂ© de type, ce serait en Ă©crivant le compilateur protobuf lui-mĂȘme.

Ce dont vous auriez besoin, c'est d'un langage capable d'une reprĂ©sentation sĂ©curisĂ©e de type de son propre arbre de syntaxe abstraite. D'aprĂšs ma propre expĂ©rience, cela nĂ©cessite au moins des gĂ©nĂ©riques et des types de donnĂ©es abstraits gĂ©nĂ©ralisĂ©s. Vous pouvez ensuite Ă©crire un compilateur protobuf de type sĂ©curisĂ© pour un langage dans le langage lui-mĂȘme.

LĂ  oĂč les gĂ©nĂ©riques me sembleraient utiles et pourraient offrir Ă  la fois une simplification et une sĂ©curitĂ© de type, ce serait en Ă©crivant le compilateur protobuf lui-mĂȘme.

Je ne vois pas vraiment comment. Le package go/ast fournit dĂ©jĂ  une reprĂ©sentation de l'AST de Go. Le compilateur Go protobuf ne l'utilise pas car travailler avec un AST est beaucoup plus fastidieux que de simplement Ă©mettre des chaĂźnes, mĂȘme s'il est plus sĂ»r.

Peut-ĂȘtre avez-vous un exemple du compilateur protobuf pour un autre langage ?

@neild J'ai commencĂ© par dire que je ne pensais pas que le protobuf Ă©tait un trĂšs bon exemple. Il y a des gains Ă  faire en utilisant des gĂ©nĂ©riques, mais ils dĂ©pendent beaucoup de l'importance que vous accordez Ă  la sĂ©curitĂ© des types, et cela serait contrebalancĂ© par l'intrusion de la mise en Ɠuvre des gĂ©nĂ©riques. Une implĂ©mentation idĂ©ale vous Ă©chapperait, Ă  moins que vous ne commettiez une erreur, auquel cas les avantages l'emporteraient sur le coĂ»t d'un plus grand nombre de cas d'utilisation.

En regardant le package go/ast, il n'a pas de reprĂ©sentation typĂ©e de l'AST car cela nĂ©cessite des gĂ©nĂ©riques et des GADT. Par exemple, un nƓud "ajouter" devrait ĂȘtre gĂ©nĂ©rique dans le type des termes ajoutĂ©s. Avec un AST non sĂ©curisĂ©, toute la logique de vĂ©rification de type doit ĂȘtre codĂ©e Ă  la main, ce qui la rendrait encombrante.

Avec une bonne syntaxe de modÚle et des expressions sûres de type, vous pouvez le rendre aussi simple que d'émettre des chaßnes, mais aussi de type sûr. Par exemple, voir (c'est plus sur le cÎté analyse): https://stackoverflow.com/questions/11104536/how-to-parse-strings-to-syntax-tree-using-gadts

Par exemple, considérez JSX comme une syntaxe littérale pour le HTML Dom dans JavaScript Vs TSX comme une syntaxe littérale pour le Dom dans TypeScript.

Nous pouvons écrire des expressions génériques typées qui se spécialisent dans le code final. Aussi facile à écrire que les chaßnes, mais le type est vérifié (dans leur forme générique).

L'un des principaux problÚmes des générateurs de code est que la vérification de type ne se produit que sur le code émis, ce qui rend difficile l'écriture de modÚles corrects. Avec les génériques, vous pouvez écrire les modÚles sous forme d'expressions vérifiées par type, de sorte que la vérification est effectuée directement sur le modÚle, et non sur le code émis, ce qui facilite grandement la mise au point et la maintenance.

Les paramĂštres de type variadiques manquent dans la conception actuelle, ce qui ressemble Ă  un Ă©norme manque de fonctionnalitĂ© des gĂ©nĂ©riques. Une conception complĂ©mentaire (peut-ĂȘtre) suit la conception actuelle du contrat :

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

Exemple inspiré d ' ici .

Je ne comprends pas comment cela ajoute une sécurité au-dessus simplement en utilisant interface{} . Y a-t-il un vrai problÚme avec les gens qui passent des non-comparables dans une métrique ?

Je ne comprends pas comment cela ajoute une sécurité au-dessus simplement en utilisant interface{} . Y a-t-il un vrai problÚme avec les gens qui passent des non-comparables dans une métrique ?

Comparables dans cet exemple nĂ©cessite Keys doit ĂȘtre composĂ© d'une sĂ©rie de types comparables. L'idĂ©e clĂ© est de montrer la conception des paramĂštres de type variadique, et non la signification du type lui-mĂȘme.

Je ne veux pas trop m'attarder sur l'exemple, mais je le prends parce que je pense que de nombreux exemples d '"extension de type" finissent par pousser la comptabilité sans ajouter de sécurité pratique. Dans ce cas, si vous voyez un mauvais type au moment de l'exécution ou potentiellement avec go vet, vous pouvez alors vous plaindre.

De plus, je suis un peu inquiet que le fait d'autoriser des types ouverts comme celui-ci conduirait au problÚme des références paradoxales, comme cela se produit dans la logique du second ordre. Pourriez-vous définir C comme le contrat de tous les types qui ne sont pas en C ?

De plus, je suis un peu inquiet que le fait d'autoriser des types ouverts comme celui-ci conduirait au problÚme des références paradoxales, comme cela se produit dans la logique du second ordre. Pourriez-vous définir C comme le contrat de tous les types qui ne sont pas en C ?

Désolé mais je ne comprends pas comment cet exemple autorise les types ouverts et se rapporte au paradoxe de Russell, Comparables est défini par une liste de Comparable .

Je n'aime pas l'idée d'écrire du code Go dans un contrat. Si je peux écrire une instruction if , puis-je écrire une instruction for ? Puis-je appeler une fonction ? Puis-je déclarer des variables ? Pourquoi pas?

Cela semble Ă©galement inutile. func F(a ...int) signifie que a vaut []int . Par analogie, func F(type Ts ...comparable) signifierait que chaque type de la liste est comparable .

Dans ces lignes

type Keys(type Ts ...Comparables) struct {
    fs ...Ts
}

vous semblez définir une structure avec plusieurs champs tous nommés fs . Je ne sais pas comment cela est censé fonctionner. Existe-t-il un moyen d'utiliser la référence aux champs de cette structure autre que d'utiliser la réflexion ?

La question est donc : que peut-on faire avec des paramĂštres de type variadique ? Que veut-on faire ?

Ici, je pense que vous utilisez des paramÚtres de type variadique pour définir un type de tuple avec un nombre arbitraire de champs.

Que pourrait-on vouloir faire d'autre ?

Je n'aime pas l'idée d'écrire du code Go dans un contrat. Si je peux écrire une instruction if , puis-je écrire une instruction for ? Puis-je appeler une fonction ? Puis-je déclarer des variables ? Pourquoi pas?

Cela semble Ă©galement inutile. func F(a ...int) signifie que a vaut []int . Par analogie, func F(type Ts ...comparable) signifierait que chaque type de la liste est comparable .

AprÚs avoir examiné l'exemple un jour plus tard, je pense que vous avez absolument raison. Le Comparables est une idée stupide. L'exemple veut seulement transmettre le message d'utiliser len(args) pour déterminer le nombre de paramÚtres. Il s'avÚre que pour les fonctions, func F(type Ts ...Comparable) est assez bon.

L'exemple découpé :

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

vous semblez définir une structure avec plusieurs champs tous nommés fs . Je ne sais pas comment cela est censé fonctionner. Existe-t-il un moyen d'utiliser la référence aux champs de cette structure autre que d'utiliser la réflexion ?

La question est donc : que peut-on faire avec des paramĂštres de type variadique ? Que veut-on faire ?

Ici, je pense que vous utilisez des paramÚtres de type variadique pour définir un type de tuple avec un nombre arbitraire de champs.

Que pourrait-on vouloir faire d'autre ?

Les paramÚtres de type variadique sont destinés aux tuples par sa définition si nous utilisons ... pour cela, ce qui ne signifie pas que les tuples sont le seul cas d'utilisation, mais on peut l'utiliser dans n'importe quelle structure et n'importe quelle fonction.

Puisqu'il n'y a que deux endroits qui apparaissent avec des paramÚtres de type variadique : struct ou fonction, nous avons donc facilement ce qui est clair avant pour les fonctions :

func F(type Ts ...Comparable) (args ...Ts) {
    if len(args) > 1 {
        F(args[1:])
        return
    }
    // ... do stuff with args[0]
}

Par exemple, la fonction variadique Min n'est pas possible dans la conception actuelle, mais possible avec des paramÚtres de type variadique :

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

Pour définir un Tuple avec des paramÚtres de type variadique :

type Tuple(type Ts ...Comparable) struct {
    fs ...Ts
}

Lorsque trois paramĂštres de type sont instanciĂ©s par "Ts", il peut ĂȘtre traduit en

type Tuple(type T1, T2, T3 Comparable) struct {
    fs_1 T1
    fs_2 T2
    fs_3 T3
}

comme représentation intermédiaire. Pour utiliser le fs , il y a plusieurs façons :

  1. paramÚtres déballer
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. utiliser la boucle for
for idx, f := range k.fs {
    println(idx, ": ", f)
}
// Output:
// 0: 1
// 1: 2.0
// 2: variadic
  1. utiliser l'index (je ne sais pas si les gens voient qu'il s'agit d'une ambiguïté pour le tableau/tranche ou la carte)
k.fs[0] = ... // translated to k.fs_1 = ...
f2 := k.fs[1] // translated to f2 := k.fs_2
  1. utiliser le package reflect , fonctionne essentiellement comme un tableau
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())
        }
    }
}

Rien de bien nouveau par rapport Ă  l'utilisation d'un tableau.

Par exemple, la fonction Min variadique n'est pas possible dans la conception actuelle, mais possible avec des paramÚtres de type variadique :

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

Cela n'a pas de sens pour moi. Les paramĂštres de type variadique n'ont de sens que si les types peuvent ĂȘtre de types diffĂ©rents. Mais appeler Min sur une liste de types diffĂ©rents n'a pas de sens. Go ne prend pas en charge l'utilisation >= sur des valeurs de types diffĂ©rents. MĂȘme si nous le permettions d'une maniĂšre ou d'une autre, on pourrait nous demander Min(int, string)(1, "a") . Cela n'a aucune sorte de rĂ©ponse.

S'il est vrai que la conception actuelle n'autorise pas Min d'un nombre variadique de types diffĂ©rents, elle prend en charge l'appel Min sur un nombre variadique de valeurs du mĂȘme type. Je pense que c'est la seule façon raisonnable d'utiliser Min toute façon.

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
}

Pour certains des autres exemples dans https://github.com/golang/go/issues/15292#issuecomment -599040081, il est important de noter que dans Go, les tranches et les tableaux ont des Ă©lĂ©ments qui sont tous du mĂȘme type. Lorsque vous utilisez des paramĂštres de types variadiques, les Ă©lĂ©ments sont de types diffĂ©rents. Ce n'est donc vraiment pas la mĂȘme chose qu'une tranche ou un tableau.

Bien qu'il soit vrai que la conception actuelle n'autorise pas Min d'un nombre variadique de types diffĂ©rents, elle prend en charge l'appel Min sur un nombre variadique de valeurs du mĂȘme type. Je pense que c'est la seule façon raisonnable d'utiliser Min toute façon.

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
}

Vrai. Min était un mauvais exemple. Il a été ajouté tardivement et n'avait pas d'idée claire, comme vous pouvez le voir dans l'historique de modification des commentaires. Un vrai exemple est le Metric que vous avez ignoré.

il est important de noter que dans Go, les tranches et les tableaux ont des Ă©lĂ©ments qui sont tous du mĂȘme type. Lorsque vous utilisez des paramĂštres de types variadiques, les Ă©lĂ©ments sont de types diffĂ©rents. Ce n'est donc vraiment pas la mĂȘme chose qu'une tranche ou un tableau.

Voir? Vous ĂȘtes ceux qui voient qu'il s'agit d'une ambiguĂŻtĂ© de tableau/tranche ou de carte. Comme je l'ai dit dans https://github.com/golang/go/issues/15292#issuecomment -599040081, la syntaxe est assez similaire Ă  array/slice et map, mais elle accĂšde Ă  des Ă©lĂ©ments de types diffĂ©rents. Est-ce que c'est vraiment important? Ou peut-on prouver qu'il s'agit d'ambiguĂŻtĂ© ? Ce qui est possible en Go 1 c'est :

m := map[interface{}]int{1: 2, "2": 3, 3.0: 4}
for i, e := range m {
    println(i, e)
}

i est-il considĂ©rĂ© comme le mĂȘme type ? Apparemment, nous disons que i est interface{} , mĂȘme type. Mais une interface exprime-t-elle vraiment le type ? Les programmeurs doivent vĂ©rifier manuellement quels sont les types possibles. Lorsque vous utilisez for , [] et dĂ©compressez, importent-ils vraiment Ă  l'utilisateur qu'ils n'accĂšdent pas au mĂȘme type ? Quels sont les arguments contre cela ? Idem pour les fs :

for idx, f := range k.fs {
    switch f.(type) { // compare to interface{}, here is zero overhead.
    case int:
        // ...
    case float64:
        // ...
    case string:
        // ...
    }
}

Si vous devez utiliser un commutateur de type pour accĂ©der Ă  un Ă©lĂ©ment de type gĂ©nĂ©rique variadique, je ne vois pas l'avantage. Je peux voir comment, avec certains choix de technique de compilation, il pourrait ĂȘtre lĂ©gĂšrement plus efficace au moment de l'exĂ©cution que d'utiliser interface{} . Mais je pense que la diffĂ©rence serait assez petite, et je ne vois pas pourquoi ce serait plus sĂ»r. Il n'est pas immĂ©diatement Ă©vident que cela vaut la peine de complexifier le langage.

Je n'avais pas l'intention d'ignorer l'exemple Metric , je ne vois tout simplement pas encore comment utiliser les types génériques variadiques pour simplifier l'écriture. Si j'ai besoin d'utiliser un commutateur de type dans le corps de Metric , alors je pense que je préférerais écrire Metric2 et Metric3 .

Quelle est la définition de "rendre le langage plus complexe" ? Nous sommes tous d'accord sur le fait que les génériques sont une chose complexe, et cela ne rendra jamais le langage plus simple que Go 1. Vous avez déjà déployé d'énormes efforts pour le concevoir et l'implémenter, mais les utilisateurs de Go ne savent pas trÚs bien : quelle est la définition de "ressemble à écrire... Allez" ? Existe-t-il une métrique quantifiée pour le mesurer ? Comment une proposition de langage pourrait-elle prétendre qu'elle ne rend pas le langage plus complexe ? Dans le modÚle de proposition de langue Go 2, les objectifs sont assez simples à premiÚre vue :

  1. aborder un problĂšme important pour de nombreuses personnes,
  2. avoir un impact minimal sur tout le monde, et
  3. venir avec une solution claire et bien comprise.

Mais, les questions pourraient ĂȘtre : combien est "beaucoup" ? Qu'est-ce qui signifie "important" ? Comment mesurer l'impact sur une population inconnue ? Quand un problĂšme est-il bien compris ? Go domine le cloud, mais la domination d'autres domaines comme le calcul numĂ©rique scientifique (par exemple, l'apprentissage automatique), le rendu graphique (par exemple, l'Ă©norme marchĂ© de la 3D) deviendra-t-elle l'une des cibles de Go ? Est-ce que le problĂšme correspond plus Ă  "Je prĂ©fĂšre faire A que B dans Go & Il n'y a pas de cas d'utilisation car on peut le faire d'une autre façon" ou "B n'est pas proposĂ©, donc on n'utilise pas Go & Le cas d'utilisation n'est pas encore lĂ  parce que le langage ne peut pas l'exprimer facilement" ? ... J'ai trouvĂ© ces questions douloureuses et interminables, et parfois mĂȘme pas la peine d'y rĂ©pondre.

Revenons à l'exemple Metric , il ne montre aucun besoin d'accéder aux individus. Le déballage du jeu de paramÚtres ne semble pas vraiment nécessaire ici, bien que les solutions qui "coïncident" avec le langage existant utilisent l'indexation et la déduction de type [ ] peuvent résoudre le problÚme de type-safe :

f2 := k.fs[1] // f2 is a float64

@changkun S'il y avait des métriques claires et objectives pour décider quelles fonctionnalités du langage sont bonnes et mauvaises, nous n'aurions pas besoin de concepteurs de langage - nous pourrions simplement écrire un programme pour concevoir un langage optimal pour nous. Mais il n'y en a pas - cela revient toujours aux préférences personnelles d'un certain groupe de personnes. C'est aussi, BTW, pourquoi cela n'a aucun sens de se chamailler pour savoir si une langue est "bonne" ou non - la seule question est de savoir si vous, personnellement, l'aimez. Dans le cas de Go, les personnes qui décident des préférences sont les membres de l'équipe Go et les choses que vous citez ne sont pas des mesures, ce sont des questions directrices pour vous aider à les convaincre.

Personnellement, FWIW, je pense que les paramĂštres de type variadiques Ă©chouent sur deux de ces trois. Je ne pense pas qu'ils abordent un problĂšme important pour beaucoup de gens - l'exemple des mĂ©triques pourrait en bĂ©nĂ©ficier, mais l'OMI n'est que lĂ©gĂšrement et c'est un cas d'utilisation trĂšs spĂ©cialisĂ©. Et je ne pense pas qu'ils viennent avec une solution claire et bien comprise. Je ne connais aucune langue prenant en charge quelque chose comme ça. Mais je peux me tromper. Ce serait certainement utile si quelqu'un avait des exemples d'autres langages prenant en charge cela - cela pourrait fournir des informations sur la façon dont il est gĂ©nĂ©ralement implĂ©mentĂ© et, plus important encore, comment il est utilisĂ©. Peut-ĂȘtre est-il utilisĂ© plus largement que je ne peux l'imaginer.

@Merovius Haskell a des fonctions polyvariadiques comme nous l'avons démontré dans l'article HList : http://okmij.org/ftp/Haskell/polyvariadic.html#polyvar -fn
C'est clairement complexe Ă  faire dans Haskell, mais pas impossible.

L'exemple motivant est l'accĂšs Ă  la base de donnĂ©es de type sĂ©curisĂ© oĂč des choses telles que les jointures et les projections de type sĂ©curisĂ© peuvent ĂȘtre effectuĂ©es, et le schĂ©ma de base de donnĂ©es dĂ©clarĂ© dans le langage.

Par exemple, une table de base de donnĂ©es ressemble beaucoup Ă  un enregistrement, oĂč il y a des noms et des types de colonnes. L'opĂ©ration de jointure relationnelle prend deux enregistrements arbitraires et produit un enregistrement avec les types des deux. Vous pouvez bien sĂ»r le faire Ă  la main, mais c'est sujet aux erreurs, c'est trĂšs fastidieux, obscurcit la signification du code avec tous les types d'enregistrements dĂ©clarĂ©s Ă  la main, et bien sĂ»r la grande caractĂ©ristique d'une base de donnĂ©es SQL est qu'elle prend en charge ad-hoc requĂȘtes, vous ne pouvez donc pas prĂ©-crĂ©er tous les types d'enregistrements possibles, car vous ne savez pas nĂ©cessairement quelles requĂȘtes vous souhaitez tant que vous ne les avez pas effectuĂ©es.

Ainsi, un opérateur de jointure relationnelle de type sécurisé sur les enregistrements et les tuples serait un bon cas d'utilisation. Nous ne pensons qu'au type de la fonction ici - c'est au programmeur de décider ce que la fonction fait réellement, qu'il s'agisse d'une jointure en mémoire de deux tableaux de tuples, ou qu'elle génÚre du SQL pour s'exécuter sur une base de données externe et marshaler les résultats retour d'une maniÚre sûre.

Ce genre de chose obtient une intégration beaucoup plus soignée dans C# avec LINQ. La plupart des gens semblent penser que LINQ ajoute des fonctions lambda et des monades à C #, mais cela ne fonctionnerait pas pour son cas d'utilisation principal sans polyvariadiques, car vous ne pouvez tout simplement pas définir un opérateur de jointure de type sécurisé sans fonctionnalité similaire.

Je pense que les opérateurs relationnels sont importants. AprÚs les opérateurs de base sur les types booléens, binaires, int, float et string, les ensembles viennent probablement ensuite, puis les relations.

BTW, C++ l'offre également bien que nous ne voulions pas discuter, nous voulons cette fonctionnalité dans Go parce que XXX l'a :)

Je pense que ce serait trÚs étrange si k.fs[0] et k.fs[1] avaient des types différents. Ce n'est pas ainsi que fonctionnent les autres valeurs indexables dans Go.

L'exemple de métrique est basé sur https://medium.com/@sameer_74231/go -experience-report-for-generics-google-metrics-api-b019d597aaa4. Je pense que le code nécessite une réflexion pour récupérer les valeurs. Je pense que si nous allons ajouter des génériques variadiques à Go, nous devrions obtenir quelque chose de mieux que la réflexion pour récupérer les valeurs. Sinon, ça n'a pas l'air d'aider tant que ça.

Je pense que ce serait trÚs étrange si k.fs[0] et k.fs[1] avaient des types différents. Ce n'est pas ainsi que fonctionnent les autres valeurs indexables dans Go.

L'exemple de métrique est basé sur https://medium.com/@sameer_74231/go -experience-report-for-generics-google-metrics-api-b019d597aaa4. Je pense que le code nécessite une réflexion pour récupérer les valeurs. Je pense que si nous allons ajouter des génériques variadiques à Go, nous devrions obtenir quelque chose de mieux que la réflexion pour récupérer les valeurs. Sinon, ça n'a pas l'air d'aider tant que ça.

Bien. Vous demandez quelque chose qui n'existe pas. Si vous n'aimez pas [``] , il reste deux options : ( ) ou {``} , et je vois que vous pouvez dire que la parenthÚse ressemble à un appel de fonction et les accolades ressemblent à une initialisation variable. Personne n'aime args.0 args.1 car cela ne ressemble pas à Go. La syntaxe est triviale.

En fait, je passe un week-end à lire le livre "la conception et l'évolution du C++", il y a beaucoup d'idées intéressantes sur les décisions et les leçons bien qu'il ait été écrit en 1994 :

_"[...] Rétrospectivement, j'ai sous-estimé l'importance des contraintes dans la lisibilité et la détection précoce des erreurs."_ ==> Excellente conception de contrat

"_la syntaxe de la fonction à premiÚre vue est également plus agréable sans mot-clé supplémentaire :_

T& index<class T>(vector<T>& v, int i) { /*...*/ }
int i = index(v1, 10);

_Il semble y avoir des problÚmes persistants avec cette syntaxe plus simple. C'est trop intelligent. Il est relativement difficile de repérer une déclaration de modÚle dans un programme car [...] Les parenthÚses <...> ont été préférées aux parenthÚses car les utilisateurs les trouvaient plus faciles à lire. [...] Il se trouve que Tom Pennello a prouvé que les parenthÚses auraient été plus faciles à analyser, mais cela ne change rien à l'observation clé que les lecteurs (humains) préfÚrent <...> _
" ==> n'est-ce pas similaire à func F(type T C)(v T) T ?

_"Je pense cependant que j'ai été trop prudent et conservateur en ce qui concerne la spécification des fonctionnalités du modÚle. J'aurais pu inclure des fonctionnalités telles que [...]. Ces fonctionnalités n'auraient pas beaucoup alourdi le fardeau des implémenteurs, et les utilisateurs auraient été aidés."_

Pourquoi est-ce si familier ?

L'indexation des paramĂštres de type variadique (ou tuple) doit ĂȘtre sĂ©parĂ©e de l'indexation Ă  l'exĂ©cution et de l'indexation Ă  la compilation. Je suppose que vous pouvez simplement dire que le manque de prise en charge de l'indexation au moment de l'exĂ©cution peut dĂ©router les utilisateurs car il n'est pas cohĂ©rent avec l'indexation au moment de la compilation. MĂȘme pour l'indexation au moment de la compilation, un paramĂštre "template" non typĂ© est Ă©galement manquant dans la conception actuelle.

Avec tous les éléments de preuve, la proposition (à l'exception du rapport d'expérience) tente d'éviter de discuter de cette fonctionnalité, et je commence à croire qu'il ne s'agit pas d'ajouter des génériques variadiques à Go, mais simplement supprimés par conception.

Je suis d'accord que Design and Evolution of C++ est un bon livre, mais C++ et Go ont des objectifs diffĂ©rents. La citation finale est bonne; Stroustrup ne mentionne mĂȘme pas le coĂ»t de la complexitĂ© du langage pour les utilisateurs du langage. Au Go, nous essayons toujours de tenir compte de ce coĂ»t. Go est destinĂ© Ă  ĂȘtre un langage simple. Si nous ajoutions toutes les fonctionnalitĂ©s qui aideraient les utilisateurs, ce ne serait pas simple. Comme C++ n'est pas simple.

Avec tous les éléments de preuve, la proposition (à l'exception du rapport d'expérience) tente d'éviter de discuter de cette fonctionnalité, et je commence à croire qu'il ne s'agit pas d'ajouter des génériques variadiques à Go, mais simplement supprimés par conception.

Je suis désolé, je ne sais pas ce que vous voulez dire ici.

Personnellement, j'ai toujours envisagé la possibilité de types génériques variadiques, mais je n'ai jamais pris le temps de comprendre comment cela fonctionnerait. La façon dont cela fonctionne en C++ est trÚs subtile. J'aimerais voir si nous pouvons d'abord faire fonctionner les génériques non variadiques. Il est certainement temps d'ajouter des génériques variadiques, si possible, plus tard.

Quand je critique les pensĂ©es prĂ©cĂ©dentes, je ne dis pas que les types variadiques ne peuvent pas ĂȘtre faits. Je signale des problĂšmes qui, selon moi, doivent ĂȘtre rĂ©solus. S'ils ne peuvent pas ĂȘtre rĂ©solus, alors je ne suis pas convaincu que les types variadiques en valent la peine.

Stroustrup ne mentionne mĂȘme pas le coĂ»t de la complexitĂ© du langage pour les utilisateurs du langage. Au Go, nous essayons toujours de tenir compte de ce coĂ»t. Go est destinĂ© Ă  ĂȘtre un langage simple. Si nous ajoutions toutes les fonctionnalitĂ©s qui aideraient les utilisateurs, ce ne serait pas simple. Comme C++ n'est pas simple.

Pas vrai OMI. Il faut noter que C++ est le premier praticien qui reporte les gĂ©nĂ©riques (Well ML est le premier langage). D'aprĂšs ce que j'ai lu dans le livre, je reçois le message que C++ Ă©tait censĂ© ĂȘtre un langage simple (ne pas proposer de gĂ©nĂ©riques au dĂ©but, boucle Experiment-Simplify-Ship pour la conception du langage, mĂȘme histoire). C++ a Ă©galement connu une phase de gel des fonctionnalitĂ©s pendant plusieurs annĂ©es, ce que nous avons dans Go "The Compatability Promise". Mais il devient un peu incontrĂŽlable au fil du temps pour de nombreuses raisons raisonnables, ce qui n'est pas clair pour Go s'il reprend l'ancien chemin de C++ aprĂšs la publication des gĂ©nĂ©riques.

Il est certainement temps d'ajouter des génériques variadiques, si possible, plus tard.

MĂȘme sentiment pour moi. Les gĂ©nĂ©riques variadiques manquent Ă©galement dans la premiĂšre version standardisĂ©e des modĂšles.

Je signale des problĂšmes qui, selon moi, doivent ĂȘtre rĂ©solus. S'ils ne peuvent pas ĂȘtre rĂ©solus, alors je ne suis pas convaincu que les types variadiques en valent la peine.

Je comprends vos prĂ©occupations. Mais le problĂšme est fondamentalement rĂ©solu mais doit juste ĂȘtre correctement traduit en Go (et je suppose que personne n'aime le mot "traduire"). Ce que j'ai lu de votre proposition de gĂ©nĂ©riques historiques, ils suivent essentiellement ce qui a Ă©chouĂ© dans la premiĂšre proposition de C++ et compromis par rapport Ă  ce que Stroustrup a regrettĂ©. Je suis intĂ©ressĂ© par vos contre-arguments Ă  ce sujet.

Nous devrons ĂȘtre en dĂ©saccord sur les objectifs de C++. Peut-ĂȘtre que les objectifs initiaux Ă©taient plus similaires, mais en regardant C++ aujourd'hui, je pense qu'il est clair que leurs objectifs sont trĂšs diffĂ©rents de ceux de Go, et je pense que c'est le cas depuis au moins 25 ans.

En Ă©crivant diverses propositions pour ajouter des gĂ©nĂ©riques Ă  Go, j'ai bien sĂ»r examinĂ© le fonctionnement des modĂšles C++, ainsi que de nombreux autres langages (aprĂšs tout, C++ n'a pas inventĂ© les gĂ©nĂ©riques). Je n'ai pas regardĂ© ce que Stroustrup regrettait, donc si nous venions au mĂȘme endroit, alors, super. Je pense que les gĂ©nĂ©riques en Go ressemblent plus aux gĂ©nĂ©riques en Ada ou D qu'Ă  C++. MĂȘme aujourd'hui, C++ n'a pas de contrats, qu'ils appellent des concepts mais n'ont pas encore Ă©tĂ© ajoutĂ©s au langage. De plus, C++ permet intentionnellement une programmation complexe au moment de la compilation, et en fait les modĂšles C++ sont eux-mĂȘmes un langage complet de Turing (bien que je ne sache pas si c'Ă©tait intentionnel). J'ai toujours considĂ©rĂ© que c'Ă©tait quelque chose Ă  Ă©viter pour Go, car la complexitĂ© est extrĂȘme (bien qu'elle soit plus complexe en C++ qu'elle ne le serait en Go en raison de la surcharge et de la rĂ©solution des mĂ©thodes, que Go n'a pas).

AprĂšs avoir essayĂ© la mise en Ɠuvre du contrat actuel pendant environ un mois, je me demande un peu quel est le destin des fonctions intĂ©grĂ©es existantes. Tous peuvent ĂȘtre implĂ©mentĂ©s de maniĂšre gĂ©nĂ©rique :

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

Disparaßtront-ils dans Go2 ? Comment Go 2 a-t-il pu gérer un impact aussi énorme sur la base de code Go 1 existante ? Ces questions semblent ouvertes.

De plus, ces deux-là sont un peu spéciaux :

func Len(type T C)(t T) int {...}
func Cap(type T C)(t T) int {...}

Comment implĂ©menter un tel contrat C avec la conception actuelle, de sorte qu'un paramĂštre de type ne peut ĂȘtre qu'une tranche gĂ©nĂ©rique []Ts , une carte map[Tk]Tv et un canal chan Tc oĂč T Ts Tk Tv Tc sont diffĂ©rents ?

@changkun Je ne pense pas que "ils puissent ĂȘtre implĂ©mentĂ©s avec des gĂ©nĂ©riques" soit une raison convaincante pour les supprimer. Et vous mentionnez une raison assez claire et solide pour laquelle ils ne devraient pas ĂȘtre supprimĂ©s. Je ne pense donc pas qu'ils le seront. Je pense que cela rend le reste des questions obsolĂštes.

@changkun Je ne pense pas que "ils puissent ĂȘtre implĂ©mentĂ©s avec des gĂ©nĂ©riques" soit une raison convaincante pour les supprimer. Et vous mentionnez une raison assez claire et solide pour laquelle ils ne devraient pas ĂȘtre supprimĂ©s.

Oui, je suis d'accord que ce n'est pas la raison pour les supprimer, c'est pourquoi je l'ai dit explicitement. Cependant, les garder avec des génériques "viole" la philosophie existante de Go, dont les caractéristiques linguistiques sont orthogonales. La compatibilité est la principale préoccupation, mais l'ajout de contrats est susceptible de tuer un énorme code "obsolÚte" actuel.

Je ne pense donc pas qu'ils le seront. Je pense que cela rend le reste des questions obsolĂštes.

Essayons de ne pas ignorer la question et de la considĂ©rer comme un cas d'utilisation rĂ©el des contrats. Si l'on propose des exigences similaires, comment pourrions-nous les mettre en Ɠuvre avec la conception actuelle ?

Il est clair que nous n'allons pas nous débarrasser des fonctions prédéclarées existantes.

Bien qu'il soit possible d'écrire une signature de fonction paramétrée pour delete , close , panic , recover , print et println , je ne pense pas qu'il soit possible de les implémenter sans s'appuyer sur des fonctions magiques internes.

Il existe des versions partielles de Append et Copy sur https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-contracts.md#append. Il n'est pas complet, car append et copy ont des cas particuliers pour un deuxiĂšme argument de type string , qui n'est pas pris en charge par le brouillon de conception actuel.

Notez que la signature pour Make , ci-dessus, n'est pas valide selon le projet de conception actuel. New n'est pas tout Ă  fait la mĂȘme chose que new , mais assez proche.

Avec le projet de conception actuel, Len et Cap devraient prendre un argument de type interface{} , et en tant que tel ne serait pas sûr pour le type au moment de la compilation.

https://go-review.googlesource.com/c/go/+/187317

S'il vous plaßt, n'utilisez pas les extensions de fichier .go2 , nous avons des modules pour faire ce genre de chose ? Je comprends si vous le faites comme une solution temporaire pour vous faciliter la vie tout en expérimentant des contrats, mais assurez-vous qu'à la fin, le fichier go.mod prendra soin de mélanger go pacakges sans le besoin de .go2 extensions de fichiers. Ce serait un coup dur pour les développeurs de modules qui s'efforcent de s'assurer que les modules fonctionnent aussi bien que possible. Utiliser des extensions de fichier .go2 , c'est comme dire, non, je m'en fiche que vos trucs de module le fassent de toute façon parce que je ne veux pas que mon compilateur pré-module dinosaure go ans tombe en panne .

Les fichiers @gertcuykens .go2 sont uniquement destinés à l'expérience ; ils ne seront pas utilisés lorsque les génériques atterriront dans le compilateur.

(Je vais cacher nos commentaires car ils n'ajoutent pas vraiment Ă  la discussion et c'est assez long tel quel.)

Récemment, j'ai exploré une nouvelle syntaxe générique dans le langage K que j'ai conçu, car K a emprunté beaucoup de grammaire à Go, donc cette grammaire générique peut également avoir une valeur de référence pour Go.

Le problÚme identifier<T> est qu'il est en conflit avec les opérateurs de comparaison et aussi les opérateurs de bits, donc je ne suis pas d'accord avec cette conception.

Le identifier[T] de Scala a une meilleure apparence que la conception précédente, mais aprÚs avoir résolu le conflit ci-dessus, il a un nouveau conflit avec la conception de l'index identifier[index] .
Pour cette raison, la conception de l'index de Scala a été changée en identifier(index) . Cela ne fonctionne pas bien pour les langages qui utilisent déjà [] comme index.

Dans le brouillon de Go, il a été déclaré que les génériques utilisent (type T) , ce qui ne causera pas de conflits, car type est un mot-clé, mais le compilateur a encore besoin de plus de jugement lorsqu'il est appelé pour résoudre le identifier(type)(params) . Bien que ce soit mieux que les solutions ci-dessus, cela ne me satisfait toujours pas.

Par hasard, je me suis souvenu de la conception spéciale de l'invocation de méthode dans OC, qui m'a donné l'inspiration pour une nouvelle conception.

Et si nous mettions l'identifiant et le générique dans leur ensemble et les mettions ensemble dans [] ?
Nous pouvons obtenir les [identifier T] . Cette conception n'entre pas en conflit avec l'index, car il doit avoir au moins deux éléments, séparés par des espaces.
Lorsqu'il y a plusieurs génériques, nous pouvons écrire [identifier T V] comme ceci, et cela n'entrera pas en conflit avec la conception existante.

En remplaçant cette conception dans Go, nous pouvons obtenir l'exemple suivant.
Par exemple

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

Cela semble trĂšs clair.

Un autre avantage de l'utilisation de [] est qu'il a un certain héritage de la conception originale de Slice and Map de Go, et ne provoquera pas de sensation de fragmentation.

[]int  ->  [slice int]

map[string]int  ->  [map string int]

On peut faire un exemple plus compliqué

var a map[int][]map[string]map[string][]string

var b [map int [slice [map string [map string [slice string]]]]]

Cet exemple conserve toujours un effet relativement clair, et en mĂȘme temps a un petit impact sur la compilation.

J'ai implémenté et testé cette conception en K et cela fonctionne bien.

Je pense que cette conception a une certaine valeur de rĂ©fĂ©rence et peut ĂȘtre digne de discussion.

Récemment, j'ai exploré une nouvelle syntaxe générique dans le langage K que j'ai conçu, car K a emprunté beaucoup de grammaire à Go, donc cette grammaire générique peut également avoir une valeur de référence pour Go.

Le problÚme identifier<T> est qu'il est en conflit avec les opérateurs de comparaison et aussi les opérateurs de bits, donc je ne suis pas d'accord avec cette conception.

Le identifier[T] de Scala a une meilleure apparence que la conception précédente, mais aprÚs avoir résolu le conflit ci-dessus, il a un nouveau conflit avec la conception de l'index identifier[index] .
Pour cette raison, la conception de l'index de Scala a été changée en identifier(index) . Cela ne fonctionne pas bien pour les langages qui utilisent déjà [] comme index.

Dans le brouillon de Go, il a été déclaré que les génériques utilisent (type T) , ce qui ne causera pas de conflits, car type est un mot-clé, mais le compilateur a encore besoin de plus de jugement lorsqu'il est appelé pour résoudre le identifier(type)(params) . Bien que ce soit mieux que les solutions ci-dessus, cela ne me satisfait toujours pas.

Par hasard, je me suis souvenu de la conception spéciale de l'invocation de méthode dans OC, qui m'a donné l'inspiration pour une nouvelle conception.

Et si nous mettions l'identifiant et le générique dans leur ensemble et les mettions ensemble dans [] ?
Nous pouvons obtenir les [identifier T] . Cette conception n'entre pas en conflit avec l'index, car il doit avoir au moins deux éléments, séparés par des espaces.
Lorsqu'il y a plusieurs génériques, nous pouvons écrire [identifier T V] comme ceci, et cela n'entrera pas en conflit avec la conception existante.

En remplaçant cette conception dans Go, nous pouvons obtenir l'exemple suivant.
Par exemple

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

Cela semble trĂšs clair.

Un autre avantage de l'utilisation de [] est qu'il a un certain héritage de la conception originale de Slice and Map de Go, et ne provoquera pas de sensation de fragmentation.

[]int  ->  [slice int]

map[string]int  ->  [map string int]

On peut faire un exemple plus compliqué

var a map[int][]map[string]map[string][]string

var b [map int [slice [map string [map string [slice string]]]]]

Cet exemple conserve toujours un effet relativement clair, et en mĂȘme temps a un petit impact sur la compilation.

J'ai implémenté et testé cette conception en K et cela fonctionne bien.

Je pense que cette conception a une certaine valeur de rĂ©fĂ©rence et peut ĂȘtre digne de discussion.

super

AprĂšs quelques allers-retours et plusieurs relectures, je soutiens globalement le projet de conception actuel de Contracts in Go. J'apprĂ©cie le temps et les efforts qui y ont Ă©tĂ© consacrĂ©s. Bien que la portĂ©e, les concepts, la mise en Ɠuvre et la plupart des compromis semblent solides, ma prĂ©occupation est que la syntaxe doit ĂȘtre rĂ©visĂ©e pour amĂ©liorer la lisibilitĂ©.

J'ai rédigé une série de modifications proposées pour résoudre ce problÚme :

Les points clés sont :

  • Appel de mĂ©thode/syntaxe d'assertion de type pour la dĂ©claration de contrat
  • Le "contrat vide"
  • DĂ©limiteurs non parenthĂšses

Au risque de devancer l'essai, je vais donner quelques morceaux de syntaxe sans explication, convertis à partir d'échantillons dans l'ébauche de conception actuelle des contrats. Notez que la forme F«T» des délimiteurs est illustrative et non prescriptive ; voir la rédaction pour plus de détails.

type List«type Element contract{}» struct {
    next *List«Element»
    val  Element
}

et

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
}

et

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

et

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
}

Sans vraiment changer les contrats sous le capot, c'est beaucoup plus lisible pour moi en tant que développeur Go. Je me sens également beaucoup plus à l'aise d' enseigner cette forme à quelqu'un qui apprend le go (quoique tard dans le programme).

@ianlancetaylor Sur la base de votre commentaire sur https://github.com/golang/go/issues/36533#issuecomment -579484523 Je poste dans ce fil plutÎt que de commencer un nouveau problÚme. Il est également répertorié sur la page de commentaires sur les génériques . Je ne sais pas si je dois faire autre chose pour qu'il soit "officiellement considéré" (c'est-à-dire le groupe d'examen de la proposition Go 2 ?) Ou si les commentaires sont toujours activement recueillis.

À partir du projet de conception des contrats :

Pourquoi ne pas utiliser la syntaxe F<T> comme C++ et Java ?
Lors de l'analyse du code dans une fonction, telle que v := F<T> , au moment de voir le < , il est ambigu de savoir si nous voyons une instanciation de type ou une expression utilisant l'opérateur < . Résoudre cela nécessite une anticipation effectivement illimitée. En général, nous nous efforçons de garder l'analyseur Go simple.

Pas particuliÚrement en conflit avec mon dernier article : Angle Brace Delimiters for Go Contracts

Juste quelques idées sur la façon de contourner ce point de confusion de l'analyseur. Quelques échantillons :

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

// ...

Essentiellement, juste une position diffĂ©rente pour les paramĂštres de type dans les scĂ©narios oĂč < pourrait ĂȘtre ambigu.

@toolbox Concernant votre commentaire sur l'équerre. Merci, mais pour moi personnellement, cette syntaxe ressemble d'abord à la décision d'utiliser des crochets angulaires pour les paramÚtres de type et les arguments de type, puis à trouver un moyen de les enfoncer. Je pense que si nous ajoutons des génériques à Go, nous devons viser pour quelque chose qui s'intÚgre proprement et facilement dans le langage existant. Je ne pense pas que le déplacement des équerres à l'intérieur des accolades permette d'atteindre cet objectif.

Oui, c'est un détail mineur, mais je pense qu'en ce qui concerne la syntaxe, les détails mineurs sont trÚs importants. Je pense que si nous allons ajouter des arguments de type et des paramÚtres, ils doivent fonctionner de maniÚre simple et intuitive.

Je ne prétends certainement pas que la syntaxe du projet de conception actuel est parfaite, mais je prétends qu'elle s'intÚgre facilement dans le langage existant. Ce que nous devons faire maintenant, c'est écrire plus d'exemples de code pour voir à quel point cela fonctionne dans la pratique. Un point clé est le suivant : à quelle fréquence les utilisateurs doivent-ils réellement écrire des arguments de type en dehors des déclarations de fonction, et à quel point ces cas sont-ils déroutants ? Je ne pense pas que nous sachions.

Est-ce une bonne idée d'utiliser [] pour les types génériques et d'utiliser () pour les fonctions génériques ? Cela serait plus cohérent avec les génériques de base actuels.

La communauté pourrait-elle voter? Personnellement, je préférerais _n'importe quoi_ plutÎt que d'ajouter plus de parenthÚses, il est déjà difficile de lire certaines définitions de fonctions pour les fermetures, etc., cela ajoute plus d'encombrement

Je ne pense pas qu'un vote soit un bon moyen de concevoir un langage. Surtout avec un ensemble trÚs difficile (probablement impossible) à déterminer et incroyablement large d'électeurs éligibles.

Je fais confiance aux concepteurs et à la communauté Go pour converger vers la meilleure solution et
donc je n'ai pas ressenti le besoin de peser sur quoi que ce soit dans cette conversation.
Cependant, je n'avais qu'Ă  dire Ă  quel point j'Ă©tais Ă©tonnamment ravi de la
suggestion de la syntaxe F«T».

(Autres parenthÚses Unicode :
https://unicode-search.net/unicode-namesearch.pl?term=BRACKET.)

Acclamations,

  • Bob

Le vendredi 1er mai 2020 Ă  19h43, Matt Mc [email protected] a Ă©crit :

AprĂšs quelques allers-retours et plusieurs relectures, je soutiens globalement la
brouillon de conception actuel pour les contrats en Go. J'apprécie le temps
et les efforts qui y sont consacrés. Alors que la portée, les concepts,
mise en Ɠuvre, et la plupart des compromis semblent judicieux, ma prĂ©occupation est que le
la syntaxe doit ĂȘtre rĂ©visĂ©e pour amĂ©liorer la lisibilitĂ©.

J'ai rédigé une série de modifications proposées pour résoudre ce problÚme :

Les points clés sont :

  • Appel de mĂ©thode/syntaxe d'assertion de type pour la dĂ©claration de contrat
  • Le "contrat vide"
  • DĂ©limiteurs non parenthĂšses

Au risque de devancer l'essai, je vais donner quelques morceaux de non pris en charge
syntaxe, convertie Ă  partir d'exemples dans l'Ă©bauche de conception actuelle des contrats. Noter
que la forme F« T » des délimiteurs est illustrative et non prescriptive ; voir
la rédaction pour plus de détails.

type List« type Element contract{}» struct {
suivant *Liste«ÉlĂ©ment»
ÉlĂ©ment val
}

et

contrat viaStrings«À, De» {
To.Set(string)
ChaĂźne From.String()
}
func SetViaStrings"type To, From viaStrings"(s []From) []To {
r := faire([]À, len(s))
pour je, v := gamme s {
r[i].Set(v.String())
}
retour r
}

et

func Keys"type K comparable, V contract{}"(m map[K]V) []K {
r := faire([]K, 0, len(m))
pour k := gamme m {
r = ajouter(r, k)
}
retour r
}
k := maps.Keys(map[int]int{1:2, 2:4})

et

contrat Numérique« T » {
T.(int, int8, int16, int32, int64,
uint, uint8, uint16, uint32, uint64, uintptr,
float32, float64,
complexe64, complexe128)
}
func DotProduct« type T Numeric »(s1, s2 []T) T {
si len(s1) != len(s2) {
panique("DotProduct : tranches de longueur inégale")
}
var r T
pour je := plage s1 {
r += s1[i] * s2[i]
}
retour r
}

Sans vraiment changer les contrats sous le capot, c'est beaucoup plus
lisible pour moi en tant que développeur Go. Je me sens aussi beaucoup plus confiant
enseigner cette forme Ă  quelqu'un qui apprend Go (bien que tard dans le
cursus).

@ianlancetaylor https://github.com/ianlancetaylor Basé sur votre commentaire
au #36533 (commentaire)
https://github.com/golang/go/issues/36533#issuecomment-579484523 Je suis
poster dans ce fil plutÎt que de créer un nouveau problÚme. Il est également répertorié
sur la page de commentaires sur les génériques
https://github.com/golang/go/wiki/Go2GenericsFeedback . Je ne sais pas si je
besoin de faire quoi que ce soit d'autre pour qu'il soit "officiellement considéré" (c'est-à-dire Go 2
groupe d'examen des propositions https://github.com/golang/go/issues/33892 ?) ou si
les commentaires sont toujours activement recueillis.

—
Vous recevez ceci parce que vous ĂȘtes abonnĂ© Ă  ce fil.
RĂ©pondez directement Ă  cet e-mail, consultez-le sur GitHub
https://github.com/golang/go/issues/15292#issuecomment-622657596 , ou
Se désabonner
https://github.com/notifications/unsubscribe-auth/AACQ2NJRBNLLDGY2XGCCQCLRPOCEHANCNFSM4CA35RXQ
.

Nous voulons tous la meilleure syntaxe possible pour Go. Le brouillon de conception utilise des parenthÚses car il a fonctionné avec le reste de Go sans provoquer d'ambiguïtés d'analyse significatives. Nous sommes restés avec eux parce qu'ils étaient la meilleure solution dans notre esprit à l'époque et parce qu'il y avait de plus gros poissons à faire frire. Jusqu'à présent, ils (entre parenthÚses) ont plutÎt bien résisté.

En fin de compte, si une bien meilleure notation est trouvĂ©e, elle est trĂšs facile Ă  changer tant que nous n'avons pas de garantie de compatibilitĂ© Ă  respecter (l'analyseur est trivialement ajustĂ© et n'importe quel corps de code peut ĂȘtre converti facilement avec gofmt).

@ianlancetaylor Merci pour la réponse, c'est apprécié.

Vous avez raison; cette syntaxe Ă©tait "n'utilisez pas de parenthĂšses pour les arguments de type" et choisissez ce que je pensais ĂȘtre le meilleur candidat, puis apportez des modifications pour essayer de rĂ©soudre les problĂšmes d'implĂ©mentation avec l'analyseur.

Si la syntaxe est difficile à lire, (difficile de savoir ce qui se passe d'un coup d'Ɠil) s'intùgre-t-elle vraiment facilement dans le langage existant ? C'est là que je pense que la position est insuffisante.

Il est vrai, comme vous l'évoquez, que l'inférence de type pourrait réduire considérablement le nombre d'arguments de type à transmettre dans le code client. Je crois personnellement qu'un auteur de bibliothÚque devrait s'efforcer d'exiger que des arguments de type zéro soient passés lors de l'utilisation de son code, et pourtant cela se produira dans la pratique.

Hier soir, par hasard, je suis tombé sur la syntaxe du modÚle pour D qui est étonnamment similaire à certains égards :

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

Il y a deux différences essentielles que je vois :

  1. Ils ont ! comme opérateur d'instanciation pour utiliser les modÚles.
  2. Leur style de dĂ©claration (pas de valeurs de retour multiples, mĂ©thodes imbriquĂ©es dans des classes) signifie qu'il y a nativement moins de parenthĂšses dans le code ordinaire, donc l'utilisation de parenthĂšses pour les paramĂštres de type ne crĂ©e pas la mĂȘme ambiguĂŻtĂ© visuelle.

Opérateur d'instanciation

Lors de l'utilisation de Contracts, l'ambiguïté visuelle principale se situe entre une instanciation et un appel de fonction (ou une conversion de type, ou... ?). Une partie de la raison pour laquelle cela est problématique est que les instanciations sont au moment de la compilation et les appels de fonction sont au moment de l'exécution. Go a beaucoup d'indices visuels qui indiquent au lecteur à quel camp appartient chaque clause, mais la nouvelle syntaxe les brouille, donc ce n'est pas évident si vous regardez les types ou le déroulement du programme.

Un exemple artificiel :

// Instantiation with unexported types and then function call,
// or chained method call?
a := draw(square, ellipse)(canvas, color)

Proposition : utiliser un opérateur d'instanciation pour spécifier les paramÚtres de type. Le ! utilisé par D semble parfaitement acceptable. Quelques exemples de syntaxe :

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

// ...

De mon point de vue personnel, le code ci-dessus est d'un ordre de grandeur plus facile Ă  lire. Je pense que cela lĂšve toutes les ambiguĂŻtĂ©s, Ă  la fois visuellement et pour l'analyseur. De plus, je me demande s'il s'agit du changement le plus important qui pourrait ĂȘtre apportĂ© aux contrats.

Style de déclaration

Lors de la déclaration de types, de fonctions et de méthodes, il y a moins de "run-time ou compile-time?" problÚme. Un Gopher voit une ligne commençant par type ou func et sait qu'il regarde une déclaration, pas un comportement de programme.

Cependant, certaines ambiguïtés visuelles subsistent :

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

Les pensées:

  • Je pense que ces problĂšmes sont moins importants que le problĂšme d'instanciation.
  • La solution la plus Ă©vidente serait de changer les dĂ©limiteurs utilisĂ©s pour les arguments de type.
  • Peut-ĂȘtre mettre une autre sorte d'opĂ©rateur ou de caractĂšre lĂ -dedans ( ! pourrait se perdre, qu'en est-il de # ?) pourrait dĂ©sambiguĂŻser les choses.

EDIT : @griesemer merci pour la clarification supplémentaire !

Merci. Juste pour poser la question naturelle : pourquoi est-il important de savoir si un appel particulier est évalué à l'exécution ou à la compilation ? Pourquoi est-ce la question clé?

@toolbox

// Instantiation with unexported types and then function call,
// or chained method call?
a := draw(square, ellipse)(canvas, color)

Pourquoi serait-ce important de toute façon? Pour un lecteur occasionnel, peu importe qu'il s'agisse d'un morceau de code exĂ©cutĂ© pendant la compilation ou l'exĂ©cution. Pour tous les autres, ils peuvent simplement jeter un coup d'Ɠil Ă  la dĂ©finition de la fonction pour savoir ce qui se passe. Vos exemples ultĂ©rieurs ne semblent pas du tout ambigus.

En fait, utiliser () pour les paramÚtres de type a du sens, car il semble que vous appeliez une fonction qui renvoie une fonction - et c'est plus ou moins correct. La différence étant que la premiÚre fonction accepte des types, qui sont généralement en majuscules ou trÚs connus.

À ce stade, il est beaucoup plus important de dĂ©terminer les dimensions de la remise, pas sa couleur.

Je ne pense pas que ce dont parle @toolbox soit vraiment une différence entre le temps de compilation et le temps d'exécution. Oui c'est une différence, mais ce n'est pas la plus importante. L'important est : est-ce un appel de fonction ou une déclaration de type ? Vous voulez savoir parce qu'ils se comportent différemment et vous ne voulez pas avoir à déduire si une expression fait deux appels de fonction ou un seul, car c'est une grande différence. C'est-à-dire qu'une expression comme a := draw(square, ellipse)(canvas, color) est ambiguë sans faire de travail pour examiner le milieu environnant.

Il est important de pouvoir analyser visuellement le flux de contrĂŽle du programme. Je pense que Go en est un excellent exemple.

Merci. Juste pour poser la question naturelle : pourquoi est-il important de savoir si un appel particulier est évalué à l'exécution ou à la compilation ? Pourquoi est-ce la question clé?

Désolé, il semble que j'ai raté ma communication. C'est le point clé que j'essayais de faire passer:

ce n'est pas évident si vous regardez les types ou le déroulement du programme

(Pour le moment, l'un est trié lors de la compilation et l'autre se produit au moment de l'exécution, mais ce sont ... des caractéristiques, pas le point clé, que @infogulch a correctement relevées - merci!)


J'ai vu l'opinion Ă  quelques endroits que les gĂ©nĂ©riques du brouillon peuvent ĂȘtre assimilĂ©s Ă  des appels de fonction : c'est une sorte de fonction de compilation qui renvoie la fonction ou le type rĂ©el . Bien que cela soit utile en tant que modĂšle mental de ce qui se passe lors de la compilation, cela ne se traduit pas syntaxiquement. Syntaxiquement, elles doivent alors ĂȘtre nommĂ©es comme des fonctions. Voici un exemple :

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

LĂ , cela ressemble en fait Ă  une fonction qui renvoie une fonction ; Je pense que c'est assez lisible.

Une autre façon de procéder serait de tout suffixer avec Type , il est donc clair d'aprÚs le nom que lorsque vous "appelez" la fonction, vous obtenez un type. Sinon, il n'est pas évident que (par exemple) Pair(...) produit un type struct plutÎt qu'un struct. Mais si cette convention est en place, ce code devient clair : a := drawType(square, ellipse)(canvas, color)

(Je me rends compte qu'un précédent est la convention "-er" pour les interfaces.)

Notez que je ne soutiens pas particuliÚrement ce qui précÚde comme solution, j'illustre simplement comment je pense que "les génériques en tant que fonctions" ne sont pas pleinement et sans ambiguïté exprimés par la syntaxe actuelle.


Encore une fois, @infogulch a trÚs bien résumé mon propos. Je suis en faveur de la différenciation visuelle des arguments de type afin qu'il soit clair qu'ils font partie du type .

Peut-ĂȘtre que la partie visuelle de celui-ci sera amĂ©liorĂ©e par la coloration syntaxique de l'Ă©diteur.

Je ne connais pas grand-chose aux analyseurs et à la façon dont vous ne pouvez pas faire trop d'anticipation.

Du point de vue des utilisateurs, je ne veux pas voir encore un autre caractÚre dans mon code, donc «» n'obtiendrait pas mon support (je ne les ai pas trouvés sur mon clavier !).

Cependant, voir des parenthÚses suivies de parenthÚses n'est pas non plus trÚs agréable à regarder.

Que diriez-vous d'utiliser simplement des accolades?

a := draw{square, ellipse}(canvas, color)

Cependant, dans Print(int)([]int{1,2,3}) , la seule différence de comportement est "le temps de compilation par rapport au temps d'exécution". Oui, MakePrintFunc au lieu de Print soulignerait davantage cette similitude, mais
 n'est-ce pas un argument pour ne pas utiliser MakePrintFunc ? Parce qu'il cache en fait la vraie différence de comportement.

FWIW, si quoi que ce soit, vous semblez argumenter en faveur de l'utilisation de diffĂ©rents sĂ©parateurs pour les fonctions paramĂ©triques et les types paramĂ©triques. Parce que Print(int) peut en fait ĂȘtre considĂ©rĂ© comme Ă©quivalent Ă  une fonction renvoyant une fonction (Ă©valuĂ©e au moment de la compilation), alors que Pair(int, string) ne le peut pas - c'est une fonction renvoyant un type . Print(int) est en fait une expression valide qui correspond Ă  une func , alors que Pair(int, string) n'est pas une expression valide, c'est une spĂ©cification de type. Ainsi, la vraie diffĂ©rence d'utilisation n'est pas "fonctions gĂ©nĂ©riques vs non gĂ©nĂ©riques", c'est "fonctions gĂ©nĂ©riques vs types gĂ©nĂ©riques". Et Ă  partir de ce point de vue, je pense qu'il y a de bonnes raisons d'utiliser () au moins pour les fonctions paramĂ©triques de toute façon, car cela met l'accent sur la nature des fonctions paramĂ©triques pour reprĂ©senter rĂ©ellement des valeurs - et peut-ĂȘtre devrions-nous utiliser <> pour les types paramĂ©triques.

Je pense que l'argument pour () pour les types paramĂ©triques provient de la programmation fonctionnelle, oĂč ces types de retour de fonctions sont un concept rĂ©el appelĂ© constructeurs de type et peuvent en fait ĂȘtre utilisĂ©s et rĂ©fĂ©rencĂ©s en tant que fonctions. Et FWIW, c'est aussi pourquoi je ne dirais pas de ne pas utiliser () pour les types paramĂ©triques. Personnellement, je suis trĂšs Ă  l'aise avec ce concept et je prĂ©fĂ©rerais l'avantage de moins de sĂ©parateurs diffĂ©rents, Ă  l'avantage de dĂ©sambiguĂŻser les fonctions paramĂ©triques des types paramĂ©triques - aprĂšs tout, nous n'avons aucun problĂšme avec les identificateurs purs faisant Ă©galement rĂ©fĂ©rence Ă  des types ou Ă  des valeurs .

Je ne pense pas que ce dont parle @toolbox soit vraiment une différence entre le temps de compilation et le temps d'exécution. Oui c'est une différence, mais ce n'est pas la plus importante. L'important est : est-ce un appel de fonction ou une déclaration de type ? Vous _voulez_ savoir parce qu'ils se comportent différemment et vous ne voulez pas avoir à déduire si une expression fait deux appels de fonction ou un seul, car c'est une grande différence. C'est-à-dire qu'une expression comme a := draw(square, ellipse)(canvas, color) est ambiguë sans faire de travail pour examiner le milieu environnant.

Il est important de pouvoir analyser visuellement le flux de contrĂŽle du programme. Je pense que Go en est un excellent exemple.

Les déclarations de type seraient trÚs faciles à voir, car elles commencent toutes par le mot-clé type . Votre exemple n'en fait évidemment pas partie.

Peut-ĂȘtre que la partie visuelle de celui-ci sera amĂ©liorĂ©e par la coloration syntaxique de l'Ă©diteur.

Je pense que, idĂ©alement, la syntaxe devrait ĂȘtre claire, quelle que soit sa couleur. Cela a Ă©tĂ© le cas pour Go, et je ne pense pas qu'il serait bon de s'Ă©loigner de cette norme.

Que diriez-vous d'utiliser simplement des accolades?

Je crois que cela est malheureusement en conflit avec un littéral de structure.

Dans Print(int)([]int{1,2,3}) , la seule différence de comportement est "le temps de compilation par rapport au temps d'exécution". Oui, MakePrintFunc au lieu de Print soulignerait davantage cette similitude, mais
 n'est-ce pas un argument pour ne pas utiliser MakePrintFunc ? Parce qu'il cache en fait la vraie différence de comportement.

Eh bien, pour commencer, c'est pourquoi je soutiendrais Print!(int)([]int{1,2,3}) sur MakePrintFunc(int)([]int{1,2,3}) . Il est clair que quelque chose d'unique se passe.

Mais encore une fois, la question que @ianlancetaylor a posée plus tÎt : pourquoi est-ce important si le type instanciation/fonction-retour-fonction est à la compilation ou à l'exécution ?

En y rĂ©flĂ©chissant, si vous Ă©criviez des appels de fonction et que le compilateur Ă©tait capable de les optimiser et de calculer leur rĂ©sultat au moment de la compilation, vous seriez heureux du gain de performances ! Au contraire, l'aspect important est ce que fait le code, quel est le comportement ? Cela devrait ĂȘtre Ă©vident en un coup d'Ɠil.

Quand je vois Print(...) mon premier rĂ©flexe est "c'est un appel de fonction qui Ă©crit quelque part". Il ne communique pas "ceci retournera une fonction". À mon avis, n'importe lequel d'entre eux est meilleur car il peut communiquer le comportement et l'intention :

  • MakePrintFunc(...)
  • Print!(...)
  • Print<...>

En d'autres termes, ce morceau de code "rĂ©fĂ©rence" ou d'une certaine maniĂšre "me donne" une fonction qui peut maintenant ĂȘtre appelĂ©e dans le morceau de code suivant.

FWIW, si quoi que ce soit, vous semblez argumenter en faveur de l'utilisation de différents séparateurs pour les fonctions paramétriques et les types paramétriques. ...

Non, je sais que les derniers exemples concernaient les fonctions, mais je préconiserais une syntaxe cohérente pour les fonctions paramétriques et les types paramétriques. Je ne crois pas que l'équipe Go ajouterait des génériques dans Go à moins qu'il ne s'agisse d'un concept unifié avec une syntaxe unifiée.

Quand je vois Print(...) mon premier réflexe est "c'est un appel de fonction qui écrit quelque part". Il ne communique pas "ceci retournera une fonction".

func Print(
) func(
) non plus , lorsqu'il est appelé comme Print(
) . Pourtant, nous sommes collectivement d'accord avec cela. Sans syntaxe d'appel spéciale, si une fonction renvoie un func .
La syntaxe Print(
) vous dit Ă  peu prĂšs exactement ce qu'elle fait aujourd'hui : que Print est une fonction qui renvoie une valeur, qui correspond Ă  l'Ă©valuation de Print(
) . Si vous ĂȘtes intĂ©ressĂ© par le type renvoyĂ© par la fonction, regardez sa dĂ©finition.
Ou, bien plus probablement, utilisez le fait qu'il s'agit en fait Print(
)(
) comme indicateur qu'il renvoie une fonction.

En y réfléchissant, si vous écriviez des appels de fonction et que le compilateur était capable de les optimiser et de calculer leur résultat au moment de la compilation, vous seriez heureux du gain de performances !

SĂ»r. Nous avons dĂ©jĂ  cela. Et je suis trĂšs heureux de ne pas avoir besoin d'annotations syntaxiques spĂ©cifiques pour les rendre spĂ©ciales, mais je peux simplement ĂȘtre sĂ»r que le compilateur fournira des heuristiques en constante amĂ©lioration sur les fonctions dont il s'agit.

À mon avis, n'importe lequel d'entre eux est meilleur car il peut communiquer le comportement et l'intention :

A noter que le premier au moins est 100% compatible avec le design. Il ne prescrit aucun formulaire pour les identifiants utilisĂ©s et j'espĂšre que vous ne suggĂ©rez pas de le prescrire (et si vous le faites, je serais intĂ©ressĂ© de savoir pourquoi les mĂȘmes rĂšgles ne s'appliquent pas simplement au retour d'un func ).

Non, je sais que les derniers exemples concernaient les fonctions, mais je préconiserais une syntaxe cohérente pour les fonctions paramétriques et les types paramétriques.

Eh bien, je suis d'accord, comme je l'ai dit :) Je dis juste que je ne comprends pas comment les arguments que vous avancez peuvent ĂȘtre appliquĂ©s le long de l'axe "gĂ©nĂ©rique vs non gĂ©nĂ©rique", car il n'y a pas de changements de comportement importants entre les deux. Ils auraient du sens le long de l'axe "type vs fonction", car le fait que quelque chose soit une spĂ©cification de type ou une expression est trĂšs important pour le contexte dans lequel il peut ĂȘtre utilisĂ©. Je ne serais toujours pas d'accord, mais au moins je comprendrais eux :)

@Merovius merci pour votre commentaire.

func Print(
) func(
) non plus , lorsqu'il est appelé en tant que Print(
) . Pourtant, nous sommes collectivement d'accord avec cela. Sans syntaxe d'appel spéciale, si une fonction renvoie un func.
La syntaxe Print(
) vous dit Ă  peu prĂšs exactement ce qu'elle fait aujourd'hui : que Print est une fonction qui renvoie une valeur, qui correspond Ă  l'Ă©valuation de Print(
) . Si vous ĂȘtes intĂ©ressĂ© par le type renvoyĂ© par la fonction, regardez sa dĂ©finition.

Je suis d'avis que le nom d'une fonction devrait ĂȘtre liĂ© Ă  ce qu'elle fait. Par consĂ©quent, je m'attends Ă  ce que Print(...) imprime quelque chose, quel que soit ce qu'il renvoie. Je pense qu'il s'agit d'une attente raisonnable, et qui pourrait ĂȘtre satisfaite dans la majoritĂ© du code Go existant.

Si je vois Print(...)(...) , cela indique que le premier () a imprimé quelque chose, et que la fonction a renvoyé une fonction quelconque, et que le second () exécute ce comportement supplémentaire .

(Je serais surpris s'il s'agissait d'une opinion inhabituelle ou rare, mais je ne contesterais pas certains rĂ©sultats d'enquĂȘte.)

A noter que le premier au moins est 100% compatible avec le design. Il ne prescrit aucune forme pour les identifiants utilisĂ©s et j'espĂšre que vous ne suggĂ©rez pas de le prescrire (et si vous le faites, je serais intĂ©ressĂ© de savoir pourquoi les mĂȘmes rĂšgles ne s'appliquent pas simplement au retour d'un func).

tu as raison j'ai proposé ça :)

Écoutez, j'ai Ă©numĂ©rĂ© les 3 façons auxquelles je pouvais penser pour corriger l'ambiguĂŻtĂ© visuelle introduite par les paramĂštres de type sur les fonctions et les types. Si vous ne voyez aucune ambiguĂŻtĂ©, alors vous n'aimerez aucune des suggestions !

Je dis simplement que je ne comprends pas comment les arguments que vous avancez peuvent ĂȘtre appliquĂ©s le long de l'axe "gĂ©nĂ©rique vs non gĂ©nĂ©rique", car il n'y a pas de changements de comportement importants entre les deux. Ils auraient du sens le long de l'axe "type vs fonction", car le fait que quelque chose soit une spĂ©cification de type ou une expression est trĂšs important pour le contexte dans lequel il peut ĂȘtre utilisĂ©.

Voir ci-dessus les points sur l'ambiguïté et les 3 solutions proposées.

Les paramÚtres de type sont une nouveauté.

  • Si nous voulons raisonner Ă  leur sujet comme une nouveautĂ©, je propose de changer les dĂ©limiteurs ou d'ajouter un opĂ©rateur d'instanciation pour les diffĂ©rencier complĂštement du code normal : appels de fonctions, conversions de types, etc.
  • Si nous voulons raisonner Ă  leur sujet comme une autre fonction , je propose de nommer ces fonctions clairement, de sorte que identifier dans identifier(...) communique le comportement et la valeur de retour.

Je préfÚre l'ancien. Dans les deux cas, les modifications seraient globales dans la syntaxe du paramÚtre de type, comme indiqué.

Il y a deux autres façons de faire la lumiÚre sur cela :

  1. Sondage
  2. Didacticiel

1. Sondage

PrĂ©face : Ceci n'est pas une dĂ©mocratie. Je pense que les dĂ©cisions sont basĂ©es sur des donnĂ©es, et une logique articulĂ©e et des donnĂ©es d'enquĂȘte gĂ©nĂ©rales peuvent aider le processus de dĂ©cision.

Je n'ai pas les moyens de le faire, mais je serais intéressé de savoir ce qui se passerait si vous sondiez quelques milliers de Gophers sur "les classer par clarté".

Ligne de base :

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

// ...

Opérateur d'instanciation :

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

// ...

Renforts d'angle : (ou contreventements Ă  double angle, dans les deux sens)

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

// ...

Fonctions bien nommées :

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

// ...

...C'est marrant, j'aime bien le dernier.

(Comment pensez-vous que cela se passerait dans le vaste monde de Gophers @Merovius ?)

2. Tutoriel

Je pense que ce serait un exercice trÚs utile : écrivez un didacticiel adapté aux débutants pour votre syntaxe préférée, et demandez à certaines personnes de le lire et de l'appliquer. Avec quelle facilité les concepts sont-ils communiqués ? Quelles sont les FAQ et comment y répondre ?

Le projet de conception est destiné à communiquer le concept aux Gophers expérimentés. Il suit la chaßne de la logique, vous plongeant lentement. Quelle est la version concise ? Comment expliquez-vous les rÚgles d'or des contrats dans un article de blog facilement assimilable ?

Cela pourrait présenter une sorte d'angle ou de tranche de données différent des rapports de rétroaction typiques.

@toolbox Je pense que ce à quoi vous n'avez pas encore répondu est: Pourquoi est-ce un problÚme pour les fonctions paramétriques, mais pas pour les fonctions non paramétriques renvoyant un func ? Je peux, aujourd'hui, écrire

func Print(a string) func(string) {
    return func(b string) {
        fmt.Println(a+b)
    }
}

func main() {
    Print("foo")("bar")
}

Pourquoi est-ce correct et ne vous rend-il pas super confus par l'ambiguĂŻtĂ©, mais dĂšs que Print prend un paramĂštre de type au lieu d'un paramĂštre de valeur, cela devient insupportable ? Et suggĂ©reriez-vous (en laissant de cĂŽtĂ© les questions de compatibilitĂ© Ă©videntes) que nous ajoutions une restriction pour que cela fonctionne correctement, que cela ne devrait pas ĂȘtre possible, Ă  moins que Print ne soit renommĂ© en MakeXFunc pour certains X ? Si non, pourquoi pas ?

@toolbox serait-ce vraiment un problÚme lorsque l'hypothÚse est que l'inférence de type pourrait trÚs bien supprimer le besoin de spécifier les types paramétriques pour les fonctions, ne laissant qu'un simple appel de fonction?

@Merovius Je ne pense pas que le problĂšme soit avec la syntaxe Print("foo")("bar") elle-mĂȘme, car c'est dĂ©jĂ  possible dans Go 1, prĂ©cisĂ©ment parce qu'il a une seule interprĂ©tation possible . Le problĂšme est qu'avec la proposition non modifiĂ©e, l'expression Foo(X)(Y) est maintenant ambiguĂ« et pourrait signifier que vous faites deux appels de fonction (comme dans Go 1), ou cela pourrait signifier que vous faites un appel de fonction avec des arguments de type . Le problĂšme est de pouvoir dĂ©duire localement ce que fait le programme, et ces deux interprĂ©tations sĂ©mantiques possibles sont trĂšs diffĂ©rentes .

@urandom Je suis d'accord que l'infĂ©rence de type peut ĂȘtre en mesure d'Ă©liminer la majeure partie des paramĂštres de type explicitement fournis, mais je ne pense pas que pousser toute la complexitĂ© cognitive dans les coins sombres du langage simplement parce qu'ils ne sont que rarement utilisĂ©s soit une bonne idĂ©e Soit. MĂȘme s'il est assez rare que la plupart des gens ne les rencontrent gĂ©nĂ©ralement pas, ils le rencontreront toujours parfois, et permettre Ă  certains codes d'avoir un flux de contrĂŽle dĂ©routant tant que ce n'est pas "la plupart" du code laisse un mauvais goĂ»t dans ma bouche. D'autant plus que Go est actuellement si accessible lors de la lecture de code "plomberie", y compris stdlib. Peut-ĂȘtre que l'infĂ©rence de type est si bonne que "rare" devient "jamais", et que les programmeurs Go restent trĂšs disciplinĂ©s et ne conçoivent jamais un systĂšme oĂč les paramĂštres de type sont nĂ©cessaires ; alors toute cette question est fondamentalement sans objet. Mais je ne parierais pas dessus.

Je pense que l'idĂ©e maĂźtresse de l'argument de @toolbox est que nous ne devrions pas surcharger allĂšgrement la syntaxe existante avec une sĂ©mantique sensible au contexte, et nous devrions plutĂŽt trouver une autre syntaxe qui n'est pas ambiguĂ« (mĂȘme s'il ne s'agit que d'un petit ajout tel que Foo(X)!(Y) .) Je pense que c'est une mesure importante lors de l'examen des options de syntaxe.

J'ai utilisé et lu un peu de code D , à l'époque (~ 2008-2009), et je dois dire que le ! me faisait toujours trébucher.

laissez-moi peindre ce hangar avec # , $ ou @ Ă  la place (car ils n'ont aucune signification en Go ou C).
cela pourrait alors ouvrir la possibilité d'utiliser des accolades sans aucune confusion avec des cartes, des tranches ou des structures.

  • Foo@{X}(Y)
  • Foo${X}(Y)
  • Foo#{X}(Y)
    ou crochets.

Dans des discussions comme celle-ci, il est essentiel de regarder le vrai code.

Par exemple, considĂ©rez que peu de gens Ă©crivent Foo(X)(Y) . Dans Go, les noms de types, les noms de variables et les noms de fonctions se ressemblent exactement, mais les gens sont rarement confus quant Ă  ce qu'ils regardent. Les gens comprennent que int64(v) est une conversion de type et que F(v) est un appel de fonction, mĂȘme s'ils se ressemblent exactement.

Nous devons examiner le code rĂ©el pour voir si les arguments de type prĂȘtent vraiment Ă  confusion dans la pratique. Si tel est le cas, nous devons ajuster la syntaxe. En l'absence de code rĂ©el, nous ne savons tout simplement pas.

Le mercredi 6 mai 2020 Ă  13h00, Ian Lance Taylor a Ă©crit :

Les gens comprennent que int64(v) est une conversion de type et que F(v) est une
appel de fonction, mĂȘme s'ils se ressemblent exactement.

Je n'ai pas d'opinion dans un sens ou dans l'autre pour le moment sur la proposition
syntaxe, mais je ne pense pas que cet exemple particulier soit trĂšs bon. Cela pourrait
ĂȘtre vrai pour les types intĂ©grĂ©s, mais j'ai en fait Ă©tĂ© confus par cela
problĂšme exact plusieurs fois moi-mĂȘme (je cherchais une fonction
dĂ©finition et ĂȘtre trĂšs confus quant Ă  la façon dont le code fonctionnait avant
J'ai réalisé que c'était probablement un type et je n'ai pas pu trouver la fonction parce que
ce n'Ă©tait pas du tout un appel de fonction). Pas la fin du monde, et
probablement pas un problÚme du tout pour les gens qui aiment les IDE sophistiqués, mais j'ai
perdu environ 5 minutes Ă  s'acharner pour cela plusieurs fois.

— Sam

--
Sam blanc

@ianlancetaylor une chose que j'ai remarquĂ©e par votre exemple est que vous pouvez Ă©crire une fonction qui prend un type et renvoie un autre type avec la mĂȘme signification, donc appeler un type comme une conversion de type de base comme int64(v) a du sens dans le de la mĂȘme maniĂšre que strconv.Atoi(v) a du sens.

Mais alors que vous pouvez faire UseConverter(strconv.Atoi) , UseConverter(int64) n'est pas possible dans Go 1. Avoir la parenthĂšse pour le paramĂštre de type peut ouvrir certaines possibilitĂ©s si le gĂ©nĂ©rique peut ĂȘtre utilisĂ© pour le casting comme :

func StrToNumber(type K)(s string) K {
  asInt := strconb.Atoi(s)
  return K(asInt)
}

Pourquoi est-ce correct et ne vous amĂšne pas Ă  ĂȘtre super confus par l'ambiguĂŻtĂ©

Votre exemple n'est pas bon. Peu m'importe si le premier appel prend des arguments ou des paramÚtres de type. Vous avez une fonction Print qui n'imprime rien. Pouvez-vous imaginer lire/réviser ce code ? Print("foo") avec le deuxiÚme ensemble de parenthÚses omis semble bien mais est secrÚtement interdit.

Si vous me soumettez ce code dans un PR, je vous dirais de changer le nom en PrintFunc ou MakePrintFunc ou PrintPlusFunc ou quelque chose qui communique son comportement.

J'ai utilisé et lu un peu de code D, à l'époque (~ 2008-2009), et je dois dire que le ! me faisait toujours trébucher.

Ha, intĂ©ressant. Je n'ai pas de prĂ©fĂ©rence particuliĂšre pour un opĂ©rateur d'instanciation ; ceux-ci semblent ĂȘtre des options dĂ©centes.

Dans Go, les noms de types, les noms de variables et les noms de fonctions se ressemblent exactement, mais les gens sont rarement confus quant Ă  ce qu'ils regardent. Les gens comprennent que int64(v) est une conversion de type et que F(v) est un appel de fonction, mĂȘme s'ils se ressemblent exactement.

Je suis d'accord, les gens peuvent généralement différencier rapidement les conversions de type et les appels de fonction. Pourquoi pensez-vous que c'est?

Ma théorie personnelle est que les types sont généralement des noms et que les fonctions sont généralement des verbes. Ainsi, lorsque vous voyez Noun(...) , il est assez clair qu'il s'agit d'une conversion de type, et lorsque vous voyez Verb(...) , c'est un appel de fonction.

Nous devons examiner le code rĂ©el pour voir si les arguments de type prĂȘtent vraiment Ă  confusion dans la pratique. Si tel est le cas, nous devons ajuster la syntaxe. En l'absence de code rĂ©el, nous ne savons tout simplement pas.

Ça a du sens.

Personnellement, je suis venu sur ce fil parce que j'ai lu le brouillon des contrats (probablement 5 fois, rebondissant à chaque fois puis allant plus loin quand je suis revenu plus tard) et trouvé la syntaxe déroutante et peu familiÚre. J'ai aimé les concepts quand je les ai finalement compris, mais il y avait une énorme barriÚre à cause de la syntaxe ambiguë.

Il y a beaucoup de "vrai code" au bas du projet de contrats, gérant tous ces cas d'utilisation courants, ce qui est génial ! Cependant, je trouve difficile d'analyser visuellement; Je suis plus lent à lire et à comprendre le code. Il me semble que je dois examiner les arguments des choses et le contexte plus large pour savoir ce que sont les choses et quel est le flux de contrÎle, et il semble que ce soit un pas en avant par rapport au code normal.

Prenons ce vrai code :

import "container/orderedmap"

var m = orderedmap.New(string, string)(strings.Compare)

func Add(a, b string) {
    m.Insert(a, b)
}

Quand je lis orderedmap.New( , je m'attends à ce que ce qui suit soit les arguments de la fonction New , ces informations clés dont la carte ordonnée a besoin pour fonctionner. Mais ceux-ci sont en fait dans la deuxiÚme série de parenthÚses. Je suis renversé par ça. Cela rend le code plus difficile à comprendre.

(Ce n'est qu'un exemple, ce n'est pas tout ce que je vois qui est ambigu, mais il est difficile d'avoir une discussion détaillée sur un large éventail de points.)

Voici ce que je suggérerais :

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

Dans les deux premiers exemples, une syntaxe différente sert à briser mon hypothÚse selon laquelle le premier ensemble de parenthÚses contient les arguments pour New() , donc le code est moins surprenant et le flux est plus observable à un niveau élevé.

La troisiĂšme option utilise la dĂ©nomination pour rendre le flux sans surprise. Je m'attends maintenant Ă  ce que le premier ensemble de parenthĂšses contienne les arguments nĂ©cessaires pour crĂ©er une fonction constructeur et je m'attends Ă  ce que la valeur de retour soit une fonction constructeur qui peut Ă  son tour ĂȘtre appelĂ©e pour produire une carte ordonnĂ©e.


Je peux certainement lire le code dans le style actuel. J'ai pu lire tout le code dans le brouillon des contrats. C'est juste plus lent car il me faut plus de temps pour le traiter. J'ai fait de mon mieux pour analyser pourquoi c'est et le signaler : en plus de l'exemple orderedmap.New , https://github.com/golang/go/issues/15292#issuecomment -623649521 a un bon rĂ©sumĂ© , mĂȘme si je pourrais probablement en trouver plus. Le degrĂ© d'ambiguĂŻtĂ© varie entre les diffĂ©rents exemples.

Je reconnais que je n'obtiendrai pas l'accord de tout le monde, car la lisibilitĂ© et la clartĂ© sont quelque peu subjectives et peut-ĂȘtre influencĂ©es par les antĂ©cĂ©dents et les langues prĂ©fĂ©rĂ©es de la personne. Je pense que 4 types d'ambiguĂŻtĂ©s d'analyse sont un bon indicateur que nous avons un problĂšme, cependant.

import "container/orderedmap"

var m = orderedmap.NewOf(string, string)(strings.Compare)

func Add(a, b string) {
    m.Insert(a, b)
}

Je pense que NewOf se lit mieux que New car New renvoie généralement une instance, pas un générique qui crée une instance.


Vous avez une fonction Print qui n'imprime rien.

Pour ĂȘtre clair, puisqu'il existe une infĂ©rence de type automatique, Print(foo) gĂ©nĂ©rique serait soit un vĂ©ritable appel d'impression via l'infĂ©rence, soit une erreur. Dans Go aujourd'hui, les identifiants nus ne sont pas autorisĂ©s :

package main

import (
    "fmt"
)

func main() {
    fmt.Println
}

./prog.go:8:5: fmt.Println evaluated but not used

Je me demande s'il existe un moyen de rendre l'inférence générique moins déroutante.

@toolbox

Votre exemple n'est pas bon. Peu m'importe si le premier appel prend des arguments ou des paramÚtres de type. Vous avez une fonction d'impression qui n'imprime rien. Pouvez-vous imaginer lire/réviser ce code ?

Vous avez omis les questions de suivi pertinentes ici. Je suis d'accord avec toi que ce n'est pas vraiment lisible. Mais vous plaidez pour une application au niveau du langage de cette contrainte. Je ne disais pas "tu es d'accord avec ça" signifiant "tu es d'accord avec ce code", mais signifiant "tu es d'accord avec la langue permettant ce code".

C'était ma question de suivi. Pensez-vous que Go est un langage pire, car il n'a pas mis de restriction de nom pour les fonctions qui renvoient func ? Si ce n'est pas le cas, pourquoi serait-ce un langage pire si nous ne mettions pas cette restriction sur de telles fonctions, lorsqu'elles prennent un argument de type au lieu d'un argument de valeur ?

@Merovius

Mais vous plaidez pour une application au niveau du langage de cette contrainte.

Non, il soutient que s'appuyer sur des normes de dénomination est une solution potentielle valable au problÚme. Une rÚgle informelle telle que "les auteurs de types sont encouragés à nommer leurs types génériques d'une maniÚre qui se confond moins facilement avec le nom d'une fonction" est une solution valable au problÚme d'ambiguïté, car elle résoudrait littéralement le problÚme dans des cas individuels.

Il ne laisse entendre nulle part que cette solution doit ĂȘtre appliquĂ©e par le langage, il dit que si les responsables dĂ©cident de conserver la proposition actuelle telle quelle, mĂȘme dans ce cas, il existe des solutions pratiques potentielles au problĂšme d'ambiguĂŻtĂ©. Et il affirme que le problĂšme de l'ambiguĂŻtĂ© est rĂ©el et important Ă  prendre en compte.

Edit : je pense qu'on s'égare un peu. Je pense qu'un code d'exemple plus "réel" serait trÚs bénéfique pour la conversation à ce stade.

Non, il soutient que s'appuyer sur des normes de dénomination est une solution potentielle valable au problÚme.

Sont-ils? J'ai essayé de demander spécifiquement:

A noter que le premier au moins est 100% compatible avec le design. Il ne prescrit aucune forme pour les identifiants utilisĂ©s et j'espĂšre que vous ne suggĂ©rez pas de le prescrire (et si vous le faites, je serais intĂ©ressĂ© de savoir pourquoi les mĂȘmes rĂšgles ne s'appliquent pas simplement au retour d'un func).

tu as raison j'ai proposé ça :)

Je conviens que "prescrire" n'est pas extrĂȘmement spĂ©cifique ici, mais c'est au moins la question que je voulais. S'ils ne plaident effectivement pas en faveur d'une exigence de niveau linguistique intĂ©grĂ©e Ă  la conception, je m'excuse bien sĂ»r pour le malentendu. Mais je me sens justifiĂ© de supposer que "prescrire" est au moins plus fort qu'"une rĂšgle informelle", au moins. Surtout si elles sont placĂ©es dans le contexte des deux autres suggestions qu'elles ont avancĂ©es (sur le mĂȘme pied) qui sont des constructions au niveau du langage car elles n'utilisent mĂȘme pas d'identifiants actuellement valides.

Y aura-t-il un plan semblable à vgo pour permettre à la communauté d'essayer la derniÚre proposition générique ?

AprÚs avoir joué un peu avec le terrain de jeu activé par contrat, je ne vois pas vraiment pourquoi il faut faire la différence entre les arguments de type et les arguments normaux.

ConsidĂ©rez cet exemple . J'ai laissĂ© les initialiseurs de type sur toutes les fonctions, mĂȘme si je pouvais tous les omettre et que cela se compilerait toujours trĂšs bien. Cela semble indiquer que la grande majoritĂ© d'un tel code potentiel ne les inclurait mĂȘme pas, ce qui Ă  son tour ne causerait aucune confusion.

Dans le cas oĂč ces paramĂštres de type sont inclus, cependant, certaines observations peuvent ĂȘtre faites :
a) les types sont soit les types intégrés, que tout le monde connaßt et peut identifier immédiatement
b) les types sont tiers, et dans ce cas seront TitleCased, ce qui les ferait ressortir un peu. Oui, il serait possible, bien que peu probable, qu'il s'agisse d'une fonction qui renvoie une autre fonction, et le premier appel consomme des variables exportĂ©es tierces, mais je pense que c'est extrĂȘmement rare.
c) les types sont des types privés. Dans ce cas, ils ressembleraient davantage à des identificateurs de variables ordinaires. Cependant, comme ils ne sont pas exportés, cela signifierait que le code que le lecteur regarde ne fait pas partie de la documentation qu'il essaie de déchiffrer et, plus important encore, qu'il lit déjà le code. Par conséquent, ils peuvent faire l'étape supplémentaire et simplement passer à la définition de la fonction pour supprimer toute ambiguïté.

Le problÚme concerne son apparence sans génériques https://play.golang.org/p/7BRdM2S5dwQ et pour quelqu'un qui est nouveau dans la programmation d'une pile distincte pour chaque type comme StackString, StackInt, ... est beaucoup plus facile à programmer puis un Stack(T) dans la proposition de syntaxe générique actuelle. Je ne doute pas que la proposition actuelle soit bien pensée, comme le montre votre exemple, mais la valeur de la simplicité et de la clarté en est grandement diminuée. Je comprends que la premiÚre priorité est de savoir si cela fonctionne en testant, mais une fois que nous sommes d'accord sur la proposition actuelle couvre la plupart des cas et qu'il n'y a pas de difficultés techniques de compilateur, une priorité encore plus élevée est de la rendre compréhensible pour tout le monde, ce qui a toujours été la raison numéro un de Allez succÚs dÚs le début.

@Merovius Non, c'est comme @infogulch l'a dit, je voulais dire créer une convention à la -er sur les interfaces. Je l'ai mentionné plus haut, désolé pour la confusion. (Je suis un "il" d'ailleurs.)

ConsidĂ©rez cet exemple. J'ai laissĂ© les initialiseurs de type sur toutes les fonctions, mĂȘme si je pouvais tous les omettre et que cela se compilerait toujours trĂšs bien. Cela semble indiquer que la grande majoritĂ© d'un tel code potentiel ne les inclurait mĂȘme pas, ce qui Ă  son tour ne causerait aucune confusion.

Que diriez-vous du mĂȘme exemple dans une version fourchue du terrain de jeu des gĂ©nĂ©riques ?

J'ai utilisé ::<> pour la clause de paramÚtre de type, et s'il y a un seul type, vous pouvez omettre le <> . Il ne devrait pas y avoir d'ambiguïté d'analyseur sur les accolades angulaires, et cela me permet de lire facilement le code, à la fois les génériques et le code utilisant les génériques. (Et si les paramÚtres de type sont déduits, tant mieux.)

Comme je l'ai dit plus tĂŽt, je n'Ă©tais pas bloquĂ© sur ! pour l'instanciation de type (et je pense que :: semble mieux aprĂšs examen). Et cela n'aide que lĂ  oĂč les gĂ©nĂ©riques sont utilisĂ©s, pas tellement dans les dĂ©clarations. Donc, cela combine un peu les deux, en omettant le <> lĂ  oĂč cela n'est pas nĂ©cessaire, un peu comme en omettant () pour les paramĂštres de retour de fonction s'il n'y en a qu'un.

Exemple d'extrait :

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

Pour cet exemple, j'ai également ajusté les noms de variables, je pense que E pour "Element" est plus lisible que T pour "Type".

Comme je l'ai dit, en rendant les éléments génériques différents, le code Go sous-jacent devient visible. Vous savez ce que vous regardez, le flux de contrÎle est évident, il n'y a pas d'ambiguïté, etc.

C'est aussi trÚs bien avec plus d'inférence de type:

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

@toolbox Excuses, alors, nous parlions l'un devant l'autre :)

quelqu'un qui est nouveau dans la programmation d'une pile distincte pour chaque type comme StackString, StackInt, ... est beaucoup plus facile Ă  programmer qu'une pile (T)

Je serais vraiment surpris si c'Ă©tait le cas. Personne n'est infaillible, et le premier bogue qui se faufile mĂȘme dans un simple morceau de code martelera Ă  quel point cette dĂ©claration est fausse Ă  long terme.

Le but de mon exemple Ă©tait d'illustrer l'utilisation des fonctions paramĂ©triques et leur instanciation avec des types concrets, ce qui est au cƓur de cette discussion, et non de savoir si l'implĂ©mentation de l'exemple Stack Ă©tait bonne ou non.

Le but de mon exemple Ă©tait d'illustrer l'utilisation des fonctions paramĂ©triques et leur instanciation avec des types concrets, ce qui est au cƓur de cette discussion, et non de savoir si l'exemple d'implĂ©mentation de Stack Ă©tait bon ou non.

Je ne pense pas que @gertcuykens voulait frapper votre implémentation Stack, il semble qu'il ait estimé que la syntaxe des génériques n'est pas familiÚre et difficile à comprendre.

Dans le cas oĂč ces paramĂštres de type sont inclus, cependant, certaines observations peuvent ĂȘtre faites :
(a B c d)...

Je vois tous vos points, j'apprĂ©cie votre analyse, et ils ne sont pas faux. Vous avez raison de dire que, dans la majoritĂ© des cas, en examinant attentivement le code, vous pouvez dĂ©terminer ce qu'il fait. Je ne pense pas que cela rĂ©fute les rapports des dĂ©veloppeurs Go qui disent que la syntaxe est confuse, ambiguĂ« ou leur prend plus de temps Ă  lire, mĂȘme s'ils peuvent Ă©ventuellement la lire.

D'une maniÚre générale, la syntaxe est dans une étrange vallée. Le code fait quelque chose de différent, mais il ressemble suffisamment aux constructions existantes pour que vos attentes soient levées et que la visibilité diminue. Vous ne pouvez pas non plus établir de nouvelles attentes car (à juste titre) ces éléments sont facultatifs, à la fois dans leur ensemble et en parties.

Pour ces cas pathologiques plus spécifiques, @infogulch l'a bien dit :

Je ne pense pas non plus que mettre toute la complexitĂ© cognitive dans les recoins sombres du langage simplement parce qu'ils ne sont que rarement utilisĂ©s soit une bonne idĂ©e. MĂȘme s'il est assez rare que la plupart des gens ne les rencontrent gĂ©nĂ©ralement pas, ils le rencontreront toujours parfois, et permettre Ă  certains codes d'avoir un flux de contrĂŽle dĂ©routant tant que ce n'est pas "la plupart" du code laisse un mauvais goĂ»t dans ma bouche.

Je pense qu'à ce stade, nous atteignons la saturation de l'articulation sur cette tranche particuliÚre du sujet. Peu importe à quel point nous en parlons, le test décisif sera la rapidité et la qualité avec lesquelles les développeurs Go peuvent l'apprendre, le lire et l'écrire.

(Et oui, avant que cela ne soit soulignĂ©, le fardeau devrait incomber Ă  l'auteur de la bibliothĂšque, pas au dĂ©veloppeur client, mais je ne pense pas que nous voulions l'effet Boost oĂč les bibliothĂšques gĂ©nĂ©riques sont inintelligibles pour l'homme de la rue. Je ne le fais pas non plus ' Je ne veux pas que Go se transforme en Jamboree gĂ©nĂ©rique, mais je suis en partie convaincu que les omissions du design limiteront l' omniprĂ©sence .)

Nous avons un terrain de jeu et nous pouvons faire des fourches pour d'autres syntaxes , ce qui est fantastique. Peut-ĂȘtre avons-nous besoin d'encore plus d'outils !

Les gens ont donnĂ© leur avis . Je suis sĂ»r que davantage de commentaires sont nĂ©cessaires, et peut-ĂȘtre que nous avons besoin de systĂšmes de rĂ©troaction amĂ©liorĂ©s ou plus rationalisĂ©s.

@toolbox Pensez-vous qu'il est possible d'analyser le code lorsque vous omettez toujours <> et type comme ça ? Peut-ĂȘtre nĂ©cessite-t-il une proposition plus stricte sur ce qui peut ĂȘtre fait, mais peut-ĂȘtre que cela vaut la peine de faire un compromis ?

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

Je ne sais pas pourquoi, mais ce Map::string, string (... semble juste bizarre. Il semble que cela crée 2 jetons, un appel de fonction Map::string et un appel de fonction string .

De plus, mĂȘme si cela n'est pas utilisĂ© dans Go, l'utilisation de "Identifier :: Identifier" peut donner une mauvaise impression aux nouveaux utilisateurs, pensant qu'il existe une classe/un espace de noms Filter avec un string fonction dedans. RĂ©utiliser des jetons d'autres langages largement adoptĂ©s pour quelque chose de complĂštement diffĂ©rent causera beaucoup de confusion.

Pensez-vous qu'il est possible d'analyser le code lorsque vous omettez toujours <> et tapez comme ça ? Peut-ĂȘtre nĂ©cessite-t-il une proposition plus stricte sur ce qui peut ĂȘtre fait, mais peut-ĂȘtre que cela vaut la peine de faire un compromis ?

Non je ne pense pas. Je suis d'accord avec @urandom que le caractÚre espace, sans rien englobant, le fait ressembler à deux jetons. J'aime aussi personnellement la portée des contrats et je ne suis pas intéressé par la modification de ses capacités.

De plus, mĂȘme si cela n'est pas utilisĂ© dans Go, l'utilisation de "Identifier::Identifier" peut donner une mauvaise impression aux nouveaux utilisateurs, pensant qu'il existe une classe/un espace de noms Filter avec une fonction de chaĂźne. RĂ©utiliser des jetons d'autres langages largement adoptĂ©s pour quelque chose de complĂštement diffĂ©rent causera beaucoup de confusion.

Je n'ai pas rĂ©ellement utilisĂ© de langage avec :: mais je l'ai dĂ©jĂ  vu. Peut-ĂȘtre que ! est mieux parce qu'il correspondrait Ă  D, bien que je trouve que :: semble mieux visuellement.

Si nous devions emprunter cette voie, il pourrait y avoir beaucoup de discussions sur les caractÚres à utiliser. Voici une tentative pour préciser ce que nous recherchons :

  • Quelque chose d'autre que identifier() pour qu'il ne ressemble pas Ă  un appel de fonction.
  • Quelque chose qui peut contenir plusieurs paramĂštres de type, pour les unir visuellement comme le peuvent les parenthĂšses.
  • Quelque chose qui semble connectĂ© Ă  l'identifiant de sorte qu'il ressemble Ă  une unitĂ©.
  • Quelque chose qui n'est pas ambigu pour l'analyseur.
  • Quelque chose qui n'entre pas en conflit avec un concept diffĂ©rent qui a une forte opinion de dĂ©veloppeur.
  • Si possible, quelque chose qui affectera les dĂ©finitions ainsi que les usages des gĂ©nĂ©riques, afin que ceux-ci deviennent Ă©galement plus faciles Ă  lire.

Il y a beaucoup de choses qui pourraient convenir.

  • identifier!(a, b) ( aire de jeux)
  • identifier@(a, b)
  • identifier#(a, b)
  • identifier$(a, b)
  • identifier<:a, b:>
  • identifier.<a, b> c'est comme une assertion de type !
  • identifier:<a, b>
  • etc.

Quelqu'un a-t-il des idées sur la façon de réduire davantage l'ensemble des potentiels?

Juste une note rapide que nous avons considéré toutes ces idées, et nous avons également considéré des idées comme

func F(T : a, b T) { }
func G() { F(int : 1, 2) }

Mais encore une fois, la preuve du pudding est dans le manger. Les discussions abstraites en l'absence de code valent la peine d'ĂȘtre menĂ©es mais ne conduisent pas Ă  des conclusions dĂ©finitives.

(Je ne sais pas si cela a dĂ©jĂ  Ă©tĂ© Ă©voquĂ©) Je constate que dans les cas oĂč nous recevons une structure, nous ne pourrons pas "Ă©tendre" une API existante pour gĂ©rer des types gĂ©nĂ©riques sans casser le code d'appel existant.

Par exemple, étant donné cette fonction non générique

func Repeat(v, n int) []int {
    var r []int
    for i := n; i > 0; i-- {
        r = append(r, v)
    }
    return r
}

Repeat(4, 4)

Nous pouvons le rendre générique sans casser la rétrocompatibilité

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)

Mais si nous voulons faire la mĂȘme chose avec une fonction qui reçoit un gĂ©nĂ©rique struct

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

il semble que le code d'appel doit ĂȘtre mis Ă  jour

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

Ce serait génial de pouvoir également dériver des types à partir de structures.

@ianlancetaylor

Le projet de contrat mentionne que methods may not take additional type arguments . Cependant, il n'est pas question de remplacer le contrat pour des méthodes particuliÚres. Une telle fonctionnalité serait trÚs utile pour implémenter des interfaces en fonction du contrat auquel un type paramétrique est lié.

Avez-vous évoqué une telle possibilité ?

Une autre question pour le projet de contrat. Les disjonctions de types seront-elles limitées aux types intégrés ? Sinon, serait-il possible d'utiliser des types paramétrés, notamment des interfaces dans la liste de disjonction ?

Quelque chose comme

type Getter(T) interface {
    Get() T
}

contract(G, T) {
    G Getter(T)
}

serait trÚs utile, non seulement pour éviter de dupliquer l'ensemble de méthodes de l'interface au contrat, mais aussi pour instancier un type paramétré lorsque l'inférence de type échoue et que vous n'avez pas accÚs au type concret (par exemple, il n'est pas exporté)

@ianlancetaylor Je ne sais pas si cela a déjà été discuté, mais en ce qui concerne la syntaxe des arguments de type à une fonction, est-il possible de concaténer la liste d'arguments à la liste d'arguments de type ? Donc, pour l'exemple de graphique, au lieu de

var g = graph.New(*Vertex, *FromTo)([]*Vertex{ ... })

tu utiliserais

var g = graph.New(*Vertex, *FromTo, []*Vertex{ ... })

Essentiellement, les K premiers arguments de la liste d'arguments correspondent à une liste d'arguments de type de longueur K. Le reste de la liste d'arguments correspond aux arguments réguliers de la fonction. Cela a l'avantage de refléter la syntaxe de

make(Type, size)

qui prend un Type comme premier argument.

Cela simplifierait la grammaire, mais nĂ©cessite des informations de type pour savoir oĂč se terminent les arguments de type et oĂč commencent les arguments normaux.

@ smasher164 Il a dit quelques commentaires en retour qu'ils l'avaient considéré (ce qui implique qu'ils l'ont jeté, bien que je sois curieux de savoir pourquoi).

func F(T : a, b T) { }
func G() { F(int : 1, 2) }

C'est ce que vous suggĂ©rez, mais avec deux-points pour sĂ©parer les deux types d'arguments. Personnellement, je l'aime moyennement, mĂȘme si c'est une image incomplĂšte; qu'en est-il de la dĂ©claration de type, des mĂ©thodes, de l'instanciation, etc.

Je veux revenir à quelque chose que @Inuart a dit :

Nous pouvons le rendre générique sans casser la rétrocompatibilité

L'Ă©quipe Go envisagerait-elle de modifier la bibliothĂšque standard de cette maniĂšre pour ĂȘtre cohĂ©rent avec la garantie de compatibilitĂ© Go 1 ? Par exemple, que se passerait-il si strings.Repeat(s string, count int) string Ă©tait remplacĂ© par Repeat(type S stringlike)(s S, count int) S ? Vous pouvez Ă©galement ajouter un commentaire //Deprecated Ă  bytes.Repeat mais le laisser lĂ  pour que le code hĂ©ritĂ© puisse l'utiliser. Est-ce quelque chose que l'Ă©quipe Go envisagerait ?

Edit : pour ĂȘtre clair, je veux dire, cela serait-il considĂ©rĂ© dans Go1Compat en gĂ©nĂ©ral ? Ignorez l'exemple spĂ©cifique si vous ne l'aimez pas.

@carlmjohnson Non. Ce code casserait : f := strings.Repeat , car les fonctions polymorphes ne peuvent pas ĂȘtre rĂ©fĂ©rencĂ©es sans les instancier au prĂ©alable.

Et Ă  partir de lĂ , je pense que la concatĂ©nation des arguments de type et des arguments de valeur serait une erreur, car elle empĂȘche une syntaxe naturelle de se rĂ©fĂ©rer Ă  une version instanciĂ©e d'une fonction. Ce serait plus naturel si le go avait dĂ©jĂ  du curry, mais ce n'est pas le cas. Il semble Ă©trange que foo(int, 42) et foo(int) soient des expressions et que les deux aient des types trĂšs diffĂ©rents.

@urandom Oui, nous avons discutĂ© de la possibilitĂ© d'ajouter des contraintes supplĂ©mentaires sur les paramĂštres de type d'une mĂ©thode individuelle. Cela entraĂźnerait la variation de l'ensemble de mĂ©thodes du type paramĂ©trĂ© en fonction des arguments de type. Cela peut ĂȘtre utile, ou cela peut prĂȘter Ă  confusion, mais une chose semble certaine : on pourra l'ajouter plus tard sans rien casser. Nous avons donc remis l'idĂ©e Ă  plus tard. Merci de l'avoir soulevĂ©.

Exactement ce qui peut ĂȘtre rĂ©pertoriĂ© dans la liste des types autorisĂ©s n'est pas aussi clair qu'il pourrait l'ĂȘtre. Je pense que nous avons encore du travail Ă  faire lĂ -bas. Notez qu'au moins dans le projet de conception actuel, lister un type d'interface dans la liste des types signifie actuellement que l'argument de type peut ĂȘtre ce type d'interface. Cela ne signifie pas que l'argument de type peut ĂȘtre un type qui implĂ©mente ce type d'interface. Je pense qu'il est actuellement difficile de savoir s'il peut s'agir d'une instance instanciĂ©e d'un type paramĂ©trĂ©. C'est une bonne question, cependant.

@ smasher164 @toolbox Les cas à considérer lors de la combinaison de paramÚtres de type et de paramÚtres réguliers dans une seule liste sont de savoir comment les séparer (s'ils sont séparés) et comment gérer le cas dans lequel il n'y a pas de paramÚtres réguliers (vraisemblablement, nous pouvons exclure le cas sans paramÚtres de type). Par exemple, s'il n'y a pas de paramÚtres réguliers, comment faites-vous la distinction entre instancier la fonction mais ne pas l'appeler, et instancier la fonction et l'appeler ? Bien que ce dernier soit clairement le cas le plus courant, il est raisonnable que les gens veuillent pouvoir écrire le premier cas.

Si les paramĂštres de type devaient ĂȘtre placĂ©s Ă  l'intĂ©rieur des mĂȘmes parenthĂšses que les paramĂštres rĂ©guliers, alors @griesemer a dĂ©clarĂ© dans # 36177 (son deuxiĂšme message) qu'il aimait bien l'utilisation d'un point-virgule plutĂŽt que de deux-points comme sĂ©parateur parce que (en consĂ©quence d'insertion automatique de points-virgules) cela permettait de rĂ©partir les paramĂštres sur plusieurs lignes de maniĂšre agrĂ©able.

Personnellement, j'aime aussi l'utilisation de barres verticales ( |..| ) pour encadrer les paramÚtres de type, comme on les voit parfois utilisés dans d'autres langages (Ruby, Crystal, etc.) pour encadrer un bloc de paramÚtres. Donc, nous aurions des choses comme:

func F(|T| a, b T) { }
func G() { F(|int| 1, 2) }

Les avantages incluent :

  • Ils fournissent une belle distinction visuelle (du moins Ă  mes yeux) entre le type et les paramĂštres rĂ©guliers.
  • Vous n'auriez pas besoin d'utiliser le mot-clĂ© type .
  • L'absence de paramĂštres rĂ©guliers n'est pas un problĂšme.
  • Le caractĂšre de barre verticale est, bien sĂ»r, dans le jeu ASCII et devrait donc ĂȘtre disponible sur la plupart des claviers.

Vous pourriez mĂȘme ĂȘtre en mesure de l'utiliser en dehors des parenthĂšses, mais vous auriez probablement les mĂȘmes difficultĂ©s d'analyse qu'avec <...> ou [...] car il pourrait ĂȘtre confondu avec l'opĂ©rateur "ou" au niveau du bit. les difficultĂ©s seraient moins aiguĂ«s.

Je ne comprends pas comment les barres verticales aident en l'absence de paramÚtres réguliers. Je ne comprends pas comment vous pouvez distinguer une instanciation de fonction d'un appel de fonction.

Une façon de faire la distinction entre ces deux cas serait d'exiger le mot-clé type si vous instancieriez la fonction mais pas si vous l'appeliez, ce qui, comme vous l'avez dit plus tÎt, est le cas le plus courant.

Je suis d'accord que cela pourrait fonctionner, mais cela semble trĂšs subtil. Je ne pense pas qu'il sera Ă©vident pour le lecteur ce qui se passe.

Je pense qu'en Go, nous devons viser plus haut que simplement avoir un moyen de faire quelque chose. Nous devons viser des approches simples, intuitives et qui cadrent bien avec le reste du langage. La personne qui lit le code doit ĂȘtre capable de comprendre facilement ce qui se passe. Bien sĂ»r, nous ne pouvons pas toujours atteindre ces objectifs, mais nous devons faire de notre mieux.

@ianlancetaylor mis à part le débat sur la syntaxe, qui est intéressant en soi, je me demande s'il y a quelque chose que nous, en tant que communauté, pouvons faire pour vous aider, vous et l'équipe, sur ce sujet.

Par exemple, j'ai l'idée que vous aimeriez plus de code écrit dans le style de la proposition, afin de mieux évaluer la proposition, à la fois syntaxiquement et autrement ? Et/ou d'autres choses ?

@toolbox Oui. Nous travaillons sur un outil pour rendre cela plus facile, mais il n'est pas encore prĂȘt. Vraiment bientĂŽt maintenant.

Pouvez-vous en dire plus sur l'outil ? Cela permettrait-il d'exécuter du code ?

Ce problÚme est-il le lieu privilégié pour les commentaires génériques ? Il semble plus actif que le wiki. Une observation est que la proposition comporte de nombreux aspects, mais le problÚme de GitHub réduit la discussion à un format linéaire.

La syntaxe F(T:) / G() { F(T:)} me semble correcte. Je ne pense pas que l'instanciation qui ressemble à un appel de fonction soit intuitive pour les lecteurs inexpérimentés.

Je ne comprends pas exactement quelles sont les prĂ©occupations concernant la rĂ©trocompatibilitĂ©. Je pense qu'il y a une limitation dans le projet contre la dĂ©claration d'un contrat, sauf au plus haut niveau. Cela pourrait valoir la peine de peser (et de mesurer) la quantitĂ© de code qui casserait rĂ©ellement si cela Ă©tait autorisĂ©. Ma comprĂ©hension est uniquement le code qui utilise le mot-clĂ© contract , qui semble ĂȘtre peu de code (qui pourrait ĂȘtre pris en charge de toute façon en spĂ©cifiant go1 en haut des anciens fichiers). Comparez cela Ă  des dĂ©cennies de plus de puissance pour les programmeurs. En gĂ©nĂ©ral, il semble assez simple de protĂ©ger l'ancien code avec de tels mĂ©canismes, en particulier avec l'utilisation gĂ©nĂ©ralisĂ©e des cĂ©lĂšbres outils de go.

En outre, en ce qui concerne cette restriction, je soupçonne que l'interdiction de déclarer des méthodes dans les corps de fonction est une raison pour laquelle les interfaces ne sont plus utilisées - elles sont beaucoup plus encombrantes que de transmettre des fonctions uniques. Il est difficile de dire si la restriction de niveau supérieur des contrats serait aussi irritante que la restriction des méthodes - ce ne serait probablement pas le cas - mais s'il vous plaßt, n'utilisez pas la restriction des méthodes comme un précédent. Pour moi, c'est un défaut de langage.

J'aimerais Ă©galement voir des exemples de la façon dont les contrats pourraient aider Ă  rĂ©duire la verbositĂ© if err != nil , et surtout oĂč ils seraient insuffisants. Est-ce que quelque chose comme F() (X, error) {return IfError(foo(), func(i, j int) X { return X(i*j}), Identity )} est possible ?

Je me demande Ă©galement si l'Ă©quipe go prĂ©voit que les signatures de fonction implicites sembleront ĂȘtre une fonctionnalitĂ© manquante une fois que Map, Filter et les amis seront disponibles. Est-ce quelque chose qui doit ĂȘtre pris en compte pendant que de nouvelles fonctionnalitĂ©s de typage implicite sont ajoutĂ©es au langage des contrats ? Ou peut-il ĂȘtre ajoutĂ© plus tard? Ou ne fera-t-il jamais partie de la langue ?

Hùte de tester la proposition. Désolé pour tant de sujets.

Personnellement, je suis assez sceptique sur le fait que beaucoup de gens aimeraient écrire des méthodes dans des corps de fonctions. Il est trÚs rare de définir des types dans les corps de fonction aujourd'hui ; déclarer des méthodes serait encore plus rare. Cela dit, voir # 25860 (non lié aux génériques).

Je ne vois pas comment les génériques aident à la gestion des erreurs (déjà un sujet trÚs verbeux en soi). Je ne comprends pas votre exemple, désolé.

Une syntaxe littérale de fonction plus courte, également non liée aux génériques, est #21498.

Quand j'ai posté hier soir, je n'avais pas réalisé qu'il était possible de jouer avec le brouillon
la mise en oeuvre (!!). Wow, c'est génial de pouvoir enfin écrire plus de code abstrait. Je n'ai aucun problÚme avec la syntaxe du brouillon.

Suite de la discussion ci-dessus...


Une partie de la raison pour laquelle les gens n'Ă©crivent pas de types dans les corps de fonctions est qu'ils
ne peut pas écrire de méthodes pour eux. Cette restriction peut piéger le type à l'intérieur du
bloc oĂč il a Ă©tĂ© dĂ©fini, car il ne peut pas ĂȘtre transformĂ© de maniĂšre concise en un
interface pour une utilisation ailleurs. Java permet aux classes anonymes de satisfaire sa version
d'interfaces, et elles sont assez utilisées.

Nous pouvons avoir la discussion sur l'interface dans #25860. Je dirais juste qu'Ă  l'Ă©poque
des contrats, les méthodes deviendront plus importantes, donc je suggÚre de se tromper sur le
cÎté de l'autonomisation des types locaux et des personnes qui aiment écrire des fermetures, pas
les affaiblir.

(Et pour réitérer, veuillez ne pas utiliser la compatibilité go1 stricte [vs virtuellement
Compatibilité à 99,999 %, si je comprends bien] comme facteur de décision à ce sujet
caractéristique.)


En ce qui concerne la gestion des erreurs, j'avais soupçonné que les génériques pourraient permettre l'abstraction
modĂšles communs pour traiter les tuples de retour (T1, T2, ..., error) . Je ne
avoir quelque chose de dĂ©taillĂ© Ă  l'esprit. Quelque chose comme type ErrPair(type T) struct{T T; Err Error} pourrait ĂȘtre utile pour enchaĂźner des actions, comme Promise dans
Java/TypeScript. Peut-ĂȘtre que quelqu'un a rĂ©flĂ©chi davantage Ă  cela. Une tentative de
Ă©crire une bibliothĂšque d'assistance et du code qui utilise la bibliothĂšque peut valoir la peine d'ĂȘtre regardĂ©
à si vous recherchez une utilisation réelle.

Avec quelques expérimentations, je me suis retrouvé avec ce qui suit. j'aimerais essayer ça
technique sur un exemple plus large pour voir si l'utilisation ErrPair(T) aide réellement.

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

(exemple de code complet disponible ici )


Pour une expérimentation générale, j'ai essayé d'écrire un package S-Expression
ici .
J'ai vĂ©cu quelques paniques dans la mise en Ɠuvre expĂ©rimentale en essayant de
travailler avec des types composés comme Form([]*Form(T)) . Je peux donner plus de commentaires
aprÚs avoir travaillé autour de cela, si cela serait utile.

Je ne savais pas non plus comment Ă©crire un type primitif -> fonction de chaĂźne :

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

La fonction réelle que j'essayais d'écrire était celle-ci:

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

J'ai aussi essayé de créer un "Résultat" comme un type de tri

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

par exemple

    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

Idéalement, les fonctions then seraient des méthodes sur le type de résultat.

De plus, l'exemple de différence absolue dans le brouillon ne semble pas compiler.
Je pense ce qui suit :

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

devrait ĂȘtre:

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

J'ai une petite inquiétude quant à la possibilité d'utiliser plusieurs contract pour délimiter un paramÚtre de type.

En Scala, il est courant de définir une fonction comme :

def compute[A: PointLike: HasTime: IsWGS](points: Vector[A]): Map[Int, A] = ???

PointLike , HasTime et IsWGS est un petit contract (Scala les appelle type class ).

Rust a également un mécanisme similaire :

fn f<F: A + B>(a F) {}

Et nous pouvons utiliser une interface anonyme lors de la définition d'une fonction.

type I1 interface {
    A()
}
type I2 interface {
    B()
}
func f(a interface{
    I1
    I2
})

IMO, l'interface anonyme est une mauvaise pratique, car un interface est un type réel , l'appelant de cette fonction peut avoir à déclarer une variable avec ce type. Mais contract juste une contrainte sur le paramÚtre de type, l'appelant joue toujours avec un type réel ou juste un autre paramÚtre de type, je pense qu'il est prudent d'autoriser un contrat anonyme dans une définition de fonction.

Pour les dĂ©veloppeurs de bibliothĂšques, il n'est pas pratique de dĂ©finir un nouveau contract si la combinaison de certains contrats n'est utilisĂ©e qu'Ă  quelques endroits, cela gĂąchera la base de code. Pour l'utilisateur des bibliothĂšques, ils doivent creuser dans les dĂ©finitions pour connaĂźtre les exigences rĂ©elles de celle-ci. Si l'utilisateur dĂ©finit beaucoup de fonction pour appeler la fonction dans la bibliothĂšque, il peut dĂ©finir un contrat nommĂ© pour une utilisation facile, et il peut mĂȘme ajouter plus de contrat Ă  ce nouveau contrat s'il en a besoin car il est valide

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

Je les ai essayĂ©s sur le brouillon du compilateur, tous ne peuvent pas ĂȘtre vĂ©rifiĂ©s.

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)

D'aprĂšs les notes en CL

Un paramÚtre de type qui est contraint par plusieurs contrats n'obtiendra pas le bon type lié.

Je pense que cet extrait étrange est valide aprÚs la résolution de ce problÚme

func f1(type A C1, _ C2(A)) (x A)

Voici quelques-unes de mes réflexions :

  • Si nous traitons contract comme le type d'un paramĂštre de type, type a A <=> var a A , nous pouvons ajouter un sucre de syntaxe comme type a { A1(a); A2(a) } pour dĂ©finir un anonyme contracter rapidement.
  • Sinon, nous pouvons traiter la derniĂšre partie de la liste des types comme une liste d'exigences, type a, b, A1(a), A2(a), A3(a, b) , ce style Ă©tant similaire Ă  l'utilisation interface pour contraindre les paramĂštres de type.

@bobotu Il est courant dans Go de composer des fonctionnalitĂ©s Ă  l'aide de l'intĂ©gration. Il semble naturel de composer des contrats de la mĂȘme maniĂšre que vous le feriez avec des structures ou des interfaces.

@azunymous Personnellement, je ne sais pas ce que je pense de l'ensemble de la communautĂ© Go qui passe de plusieurs retours Ă  Result , bien qu'il semble que la proposition de contrats permettrait cela dans une certaine mesure. L'Ă©quipe Go semble Ă©viter les changements de langue qui compromettent la "sensation" de la langue, ce avec quoi je suis d'accord, mais cela semble ĂȘtre l'un de ces changements.

Juste une pensée; Je me demande s'il y a des opinions sur ce point.

@toolbox Je ne pense pas qu'il soit rĂ©ellement possible d'utiliser quelque chose comme un seul type Result en dehors du cas oĂč vous ne faites que passer par des valeurs, sauf si vous avez une masse de Result gĂ©nĂ©riques s et fonctions de chaque combinaison de nombres de paramĂštres et de types de retour. Avec de nombreuses fonctions numĂ©rotĂ©es ou en utilisant des fermetures, vous perdriez en lisibilitĂ©.

Je pense qu'il serait plus probable que vous voyiez quelque chose d'Ă©quivalent Ă  un errWriter oĂč vous utiliseriez quelque chose comme ça de temps en temps quand cela vous convient, nommĂ© selon le cas d'utilisation.

Personnellement, je ne sais pas ce que je ressens à propos de toute la communauté Go qui passe de plusieurs retours à Résultat

Je ne pense pas que cela arriverait. Comme @azunymous l' a dit, de nombreuses fonctions ont plusieurs types de retour et une erreur, mais un rĂ©sultat ne peut pas contenir toutes ces autres valeurs renvoyĂ©es en mĂȘme temps. Le polymorphisme paramĂ©trique n'est pas la seule fonctionnalitĂ© nĂ©cessaire pour faire quelque chose comme ça ; vous auriez Ă©galement besoin de tuples et de dĂ©structuration.

Merci! Comme je l'ai dit, ce n'est pas quelque chose auquel j'avais profondément réfléchi, mais bon de savoir que mon inquiétude était déplacée.

@toolbox Je ne vise pas à introduire une nouvelle syntaxe, le problÚme clé ici est le manque de capacité à utiliser un contrat anonyme tout comme une interface anonyme.

Dans le brouillon du compilateur, il semble impossible d'Ă©crire quelque chose comme ça. Nous pouvons utiliser une interface anonyme dans la dĂ©finition de la fonction, mais nous ne pouvons pas faire la mĂȘme chose pour le contrat mĂȘme dans le style verbeux.

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)

IMO, c'est une extension naturelle de la syntaxe existante. Il s'agit toujours d'un contrat à la fin de la liste des paramÚtres de type, et nous utilisons toujours l'incorporation pour composer la fonctionnalité. Si Go peut fournir du sucre pour générer automatiquement les paramÚtres de type de contrat comme le premier extrait, le code sera plus facile à lire et à écrire.

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)

Je rencontre des problÚmes lorsque j'essaie d'implémenter un itérateur paresseux sans l'invocation de la méthode dynamique, tout comme l'itérateur de Rust.

Je veux définir un simple Iterator

contract Iterator(T, E) {
    T Next() (E, bool)
}

Parce que Go n'a pas le concept de type member , je dois déclarer E comme paramÚtre de type d'entrée.

Une fonction pour collecter les résultats

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

Une fonction pour mapper des éléments

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

J'ai deux problĂšmes ici:

  1. Je ne peux pas retourner un lazyIterator ici, le compilateur dit cannot convert &(lazyIterator(I, E, R) literal) (value of type *lazyIterator(I, E, R)) to O .
  2. Je dois déclarer un nouveau contrat nommé MapIO qui a besoin de 4 lignes alors que le Map n'a besoin que de 6 lignes. Il est difficile pour les utilisateurs de lire le code.

Supposons que Map puisse ĂȘtre vĂ©rifiĂ©, j'espĂšre pouvoir Ă©crire quelque chose comme

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

Malheureusement, le compilateur se plaint de ne pas pouvoir dĂ©duire les types. Il arrĂȘte de se plaindre aprĂšs avoir changĂ© le code en

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

Le code est trÚs difficile à lire et à écrire, et il y a trop d'indications de type dupliquées.

BTW, le compilateur va paniquer avec :

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

Je trouve également qu'il est impossible de définir une fonction qui fonctionne avec un retour Iterator d'un type particulier.

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

Le deuxiÚme extrait peut fonctionner dans certains scénarios, mais il est difficile à comprendre et le type inutilisé B semble bizarre.

En effet, nous pouvons utiliser une interface pour effectuer cette tĂąche.

type Iterator(type E) interface {
    Next() (E, bool)
}

J'essaie juste d'explorer Ă  quel point le design du Go est expressif.

BTW, le code Rust auquel je me réfÚre est

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>

Voici un résumé pour https://github.com/golang/go/issues/15292#issuecomment -633233479

  1. Nous aurons peut-ĂȘtre besoin de quelque chose pour exprimer existential type pour func Collect(type I, E Iterator) (input I) []E

    • Le type rĂ©el du paramĂštre quantifiĂ© universel E ne peut pas ĂȘtre dĂ©duit, car il n'apparaissait que dans la liste de retour. En raison du manque de type member pour rendre E existentiel par dĂ©faut, je pense que nous pouvons rencontrer ce problĂšme dans de nombreux endroits.

    • Peut-ĂȘtre pouvons-nous utiliser le existential type le plus simple comme le joker de Java ? pour rĂ©soudre l'infĂ©rence de type de func Consume(type I, E Iterator) (input I) . Nous pouvons utiliser _ pour remplacer E , func Consume(type I Iterator(I, _)) (input I) .

    • Mais cela ne peut toujours pas rĂ©soudre le problĂšme d'infĂ©rence de type pour Collect , je ne sais pas s'il est difficile de dĂ©duire E , mais Rust semble ĂȘtre capable de le faire.

    • Ou nous pouvons utiliser _ comme espace rĂ©servĂ© pour les types que le compilateur peut dĂ©duire, et remplir les types manquants manuellement, comme Collect(_, float32) (...) pour collecter sur un itĂ©rateur de float32.

  1. En raison de l'impossibilité de renvoyer un existential type , nous avons également des problÚmes pour des choses comme func Map(type I, E, O, R MapIO) (input I, f func (e E) R) O

    • Rust prend en charge cela en utilisant impl Iterator<E> . Si Go peut fournir quelque chose comme ça, nous pouvons renvoyer un nouvel itĂ©rateur sans boxe, ce qui peut ĂȘtre utile pour certains codes critiques pour les performances.

    • Ou nous pouvons simplement renvoyer un objet encadrĂ©, c'est ainsi que Rust rĂ©sout ce problĂšme avant de prendre en charge existential type Ă  la position de retour. Mais la question est la relation entre contract et interface , peut-ĂȘtre devons-nous dĂ©finir des rĂšgles de conversion et laisser le compilateur les convertir automatiquement. Sinon, nous devrons peut-ĂȘtre dĂ©finir un contract et un interface avec des mĂ©thodes identiques pour ce cas.

    • Sinon, nous ne pouvons utiliser CPS que pour dĂ©placer le paramĂštre de type de la position de retour Ă  la liste d'entrĂ©e. par exemple, func Map(type I, E, O, R MapIO) (input I, f func (e E) R, f1 func (outout O)) . Mais cela est inutile en pratique, simplement parce que nous devons Ă©crire le type rĂ©el de O lorsque nous passons une fonction Ă  Map .

J'ai juste rattrapĂ© un peu cette discussion, et il semble assez clair que les difficultĂ©s syntaxiques avec les paramĂštres de type restent une difficultĂ© majeure avec le projet de proposition. Il existe un moyen d'Ă©viter complĂštement les paramĂštres de type et d'obtenir la plupart des fonctionnalitĂ©s gĂ©nĂ©riques : #32863 -- peut-ĂȘtre est-ce le bon moment pour envisager cette alternative Ă  la lumiĂšre de certaines de ces discussions supplĂ©mentaires ? S'il y avait une chance que quelque chose comme cette conception soit adoptĂ©e, je serais heureux d'essayer de modifier le terrain de jeu de l'assemblage Web pour permettre de le tester.

Mon sentiment est que l'accent est actuellement mis sur l'exactitude de la sémantique de la proposition actuelle, quelle que soit la syntaxe, car la sémantique est trÚs difficile à modifier.

Je viens de voir qu'un article sur Featherweight Go a été publié sur Arxiv et est une collaboration entre l'équipe Go et plusieurs experts en théorie des types. On dirait qu'il y a d'autres articles prévus dans cette veine.

Pour faire suite à mon commentaire précédent, Phil Wadler de la renommée de Haskell et l'un des auteurs de l'article ont une conférence prévue sur "Featherweight Go" le lundi 8 juin à 7h PDT / 10h EDT : http://chalmersfp.org/ . lien youtube

@rcoreilly Je pense que nous ne saurons si les "difficultés syntaxiques" sont un problÚme majeur que lorsque les gens auront plus d'expérience en écriture et, plus important encore, en lecture de code écrit selon le projet de conception. Nous travaillons sur des moyens pour que les gens essaient cela.

En l'absence de cela, je pense que la syntaxe est simplement ce que les gens voient en premier et commentent en premier. C'est peut-ĂȘtre un problĂšme majeur, peut-ĂȘtre pas. Nous ne savons pas encore.

Pour faire suite à mon commentaire précédent, Phil Wadler de la renommée de Haskell et l'un des auteurs du journal ont une conférence prévue sur "Featherweight Go" lundi

La confĂ©rence de Phil Wadler Ă©tait trĂšs accessible et intĂ©ressante. J'Ă©tais ennuyĂ© par le dĂ©lai apparemment inutile d'une heure qui l'empĂȘchait de se lancer dans la monomorphisation.

Il est à noter que Wadler a été invité par Pike à intervenir; apparemment, ils se connaissent depuis Bell Labs. Pour moi, Haskell a un ensemble de valeurs et de paradigmes trÚs différent, et il est intéressant de voir comment son (créateur ? concepteur principal ?) pense de Go et des génériques de Go.

La proposition elle-mĂȘme a une syntaxe trĂšs proche des contrats, mais omet les contrats eux-mĂȘmes, en utilisant uniquement les paramĂštres de type et les interfaces. Une diffĂ©rence clĂ© qui est signalĂ©e est la possibilitĂ© de prendre un type gĂ©nĂ©rique et de dĂ©finir des mĂ©thodes sur celui-ci qui ont des contraintes plus spĂ©cifiques que le type lui-mĂȘme.

Apparemment, l'équipe Go travaille dessus ou en a un prototype ! Ce sera intéressant. En attendant, à quoi cela ressemblerait-il ?

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

Ai-je raison? Je le pense. Si je le fais... pas mal, en fait. Ne résout pas tout à fait le problÚme des parenthÚses bégayantes, mais cela semble amélioré d'une maniÚre ou d'une autre. Une agitation sans nom en moi s'est calmée.

Qu'en est-il de l'exemple de pile de @urandom ? (Aliasant interface{} à Any et utilisant une certaine quantité d'inférence de type.)

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


}

Quelque chose comme ça, je suppose. Je me rends compte qu'il n'y a en fait aucun contrat dans ce code, donc ce n'est pas une bonne représentation de la façon dont cela est géré dans le style FGG, mais je peux y remédier dans un instant.

Impressions :

  • J'aime que le style des paramĂštres de type dans les mĂ©thodes corresponde Ă  celui des dĂ©clarations de type. C'est-Ă -dire en disant "type" et en indiquant explicitement les types, ("type" param paramType, param paramType...) plutĂŽt que (param, param) . Cela le rend visuellement cohĂ©rent, de sorte que le code est plus lisible.
  • J'aime que les paramĂštres de type soient en minuscules. Les variables Ă  une seule lettre dans Go indiquent une utilisation extrĂȘmement locale, mais la capitalisation signifie qu'elle est exportĂ©e, et elles semblent contraires lorsqu'elles sont assemblĂ©es. Les minuscules se sentent mieux puisque les paramĂštres de type sont limitĂ©s Ă  la fonction/au type.

Bon, et les contrats ?

Eh bien, une chose que j'aime, c'est que Stringer est intact ; vous n'allez pas avoir une interface Stringer et un Stringer .

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
}

Nous avons aussi l'exemple viaStrings :

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
}

IntĂ©ressant. Je ne suis pas sĂ»r Ă  100% de ce que le contrat nous a apportĂ© dans ce cas. Peut-ĂȘtre qu'une partie de cela Ă©tait la rĂšgle selon laquelle une fonction pouvait avoir plusieurs paramĂštres de type mais un seul contrat.

L'égalité est couverte dans le document/conversation :

contract equal(T) {
    T Equal(T) bool
}

// becomes

type equal(type t equal(t)) interface{
    Equal(t) bool
}

Etc. Je suis assez pris avec la sĂ©mantique. Les paramĂštres de type sont des interfaces, donc les mĂȘmes rĂšgles d'implĂ©mentation d'une interface s'appliquent Ă  ce qui peut ĂȘtre utilisĂ© comme paramĂštre de type. Ce n'est tout simplement pas "en boĂźte" au moment de l'exĂ©cution - Ă  moins que vous ne lui passiez explicitement une interface, je suppose, que vous ĂȘtes libre de choisir.

La chose la plus importante que je note comme non couverte est le remplacement de la capacité de Contracts à spécifier une gamme de types primitifs. Eh bien, je suis sûr qu'une stratégie pour cela, et bien d'autres choses, viendra :

8 - CONCLUSION

C'est le dĂ©but de l'histoire, pas la fin. Dans des travaux futurs, nous prĂ©voyons d'examiner d'autres mĂ©thodes d'implĂ©mentation en plus de la monomorphisation, et en particulier d'envisager une implĂ©mentation basĂ©e sur le passage de reprĂ©sentations d'exĂ©cution de types, similaire Ă  celle utilisĂ©e pour les gĂ©nĂ©riques .NET. Une approche mixte qui utilise parfois la monomorphisation et le passage de reprĂ©sentations d'exĂ©cution peut parfois ĂȘtre la meilleure, encore une fois similaire Ă  celle utilisĂ©e pour les gĂ©nĂ©riques .NET.

Featherweight Go est limité à un petit sous-ensemble de Go. Nous prévoyons un modÚle d'autres fonctionnalités importantes telles que les affectations, les tableaux, les tranches et les packages, que nous appellerons Bantamweight Go; et un modÚle du mécanisme de concurrence innovant de Go basé sur des « goroutines » et la transmission de messages, que nous appellerons Cruiserweight Go.

Le poids plume Go me va trÚs bien. Excellente idée d'impliquer des experts en théorie des types. Cela ressemble beaucoup plus au genre de chose que je préconisais plus loin dans ce sujet.

C'est bon d'entendre que des experts en théorie des types travaillent activement là-dessus !

Cela ressemble mĂȘme (Ă  l'exception de la syntaxe lĂ©gĂšrement diffĂ©rente) Ă  mon ancienne proposition "les contrats sont des interfaces" https://github.com/cosmos72/gomacro/blob/master/doc/generics-cti.md

@toolbox
En autorisant des méthodes avec des contraintes différentes du type réel (ainsi que des types différents), FGG ouvre un certain nombre de possibilités qui n'étaient pas réalisables avec le projet de contrat actuel. Par exemple, avec FGG, on devrait pouvoir définir à la fois un Iterator et un ReversibleIterator, et faire en sorte que les itérateurs intermédiaires et de fin (map, filter reduce) prennent en charge les deux (par exemple, avec Next() et NextFromBack() pour les réversibles) , en fonction de l'itérateur parent.

Je pense qu'il est important de garder Ă  l'esprit que FGG n'est pas dĂ©finitivement l'endroit oĂč les gĂ©nĂ©riques de Go finiront. C'est une prise sur eux, de l'extĂ©rieur. Et il ignore explicitement un tas de choses qui finissent par compliquer le produit final. De plus, je n'ai pas lu le journal, j'ai juste regardĂ© la confĂ©rence. Dans cet esprit : pour autant que je sache, il existe deux maniĂšres importantes par lesquelles FGG ajoute un pouvoir expressif sur le projet de contrat :

  1. Il permet d'ajouter de nouveaux paramĂštres de type aux mĂ©thodes (comme indiquĂ© dans l'exemple "List and Maps" dans la prĂ©sentation). AFAICT cela permettrait d'implĂ©menter Functor (en fait, c'est son exemple List, si je ne me trompe pas), Monad et leurs amis. Je ne pense pas que ces types spĂ©cifiques soient intĂ©ressants pour Gophers, mais il existe des cas d'utilisation intĂ©ressants pour cela (par exemple, un port Go de Flume ou des concepts similaires en bĂ©nĂ©ficieraient probablement). Personnellement, je pense que c'est un changement positif, mĂȘme si je ne vois pas encore quelles sont les implications pour la rĂ©flexion et autres. J'ai l'impression que les dĂ©clarations de mĂ©thode utilisant this commencent Ă  devenir difficiles Ă  lire - surtout si les paramĂštres de type d'un type gĂ©nĂ©rique doivent Ă©galement ĂȘtre rĂ©pertoriĂ©s dans le rĂ©cepteur.
  2. Il permet aux paramĂštres de type d'avoir des limites plus strictes sur les mĂ©thodes de types gĂ©nĂ©riques que sur le type lui-mĂȘme. Comme mentionnĂ© par d'autres, cela vous permet d'avoir le mĂȘme type gĂ©nĂ©rique implĂ©mentant diffĂ©rentes mĂ©thodes, selon les types avec lesquels il a Ă©tĂ© instanciĂ©. Je ne suis pas sĂ»r que ce soit un bon changement, personnellement. Cela semble ĂȘtre une source de confusion, que Map(int, T) se retrouve avec des mĂ©thodes que Map(string, T) n'a pas. À tout le moins, le compilateur doit fournir d'excellents messages d'erreur, si quelque chose comme cela se produit. Pendant ce temps, l'avantage semble relativement faible - d'autant plus que le facteur de motivation de la conversation (compilation sĂ©parĂ©e) n'est pas super pertinent pour Go : comme les mĂ©thodes doivent ĂȘtre dĂ©clarĂ©es dans le mĂȘme package que leur type de rĂ©cepteur et Ă©tant donnĂ© que les packages sont l'unitĂ© de compilation, vous ne pouvez pas vraiment Ă©tendre le type sĂ©parĂ©ment. Je sais que parler de compilation est plutĂŽt une façon concrĂšte de parler d'un avantage plus abstrait, mais je n'ai pas l'impression que cet avantage aide beaucoup Go.

J'attends avec impatience les prochaines Ă©tapes, en tout cas :)

Je pense qu'il est important de garder Ă  l'esprit que FGG n'est pas dĂ©finitivement l'endroit oĂč les gĂ©nĂ©riques de Go finiront.

@Merovius pourquoi dites-vous cela?

@arl
FG est plus un document de recherche sur ce qui _pourrait_ ĂȘtre fait. Personne n'a dit explicitement que c'est ainsi que le polymorphisme fonctionnera dans Go Ă  l'avenir. MĂȘme si les dĂ©veloppeurs principaux de 2 Go sont rĂ©pertoriĂ©s comme auteurs dans le document, cela ne signifie pas que cela sera implĂ©mentĂ© dans Go.

Je pense qu'il est important de garder Ă  l'esprit que FGG n'est pas dĂ©finitivement l'endroit oĂč les gĂ©nĂ©riques de Go finiront. C'est une prise sur eux, de l'extĂ©rieur. Et il ignore explicitement un tas de choses qui finissent par compliquer le produit final.

Oui, trĂšs bon point.

De plus, je noterai que Wadler travaille en équipe et que le produit résultant s'appuie sur et est trÚs proche de la proposition de contrats, qui est le résultat d'années de travail des principaux développeurs.

En autorisant des méthodes avec des contraintes différentes du type réel (ainsi que des types différents), FGG ouvre un certain nombre de possibilités qui n'étaient pas réalisables avec le projet de contrat actuel. ...

@urandom Je suis curieux de savoir à quoi ressemble cet exemple Iterator; cela vous dérangerait-il de jeter quelque chose ensemble?

Séparément, je suis intéressé par ce que les génériques peuvent faire au-delà des cartes, des filtres et des choses fonctionnelles, et plus curieux de savoir comment ils pourraient bénéficier à un projet comme k8s. (Pas qu'ils iraient refactoriser à ce stade, mais j'ai entendu de maniÚre anecdotique que le manque de génériques a nécessité un jeu de jambes sophistiqué, je pense avec des ressources personnalisées ? Quelqu'un de plus familier avec le projet peut me corriger.)

J'ai l'impression que les dĂ©clarations de mĂ©thode utilisant this commencent Ă  devenir difficiles Ă  lire - surtout si les paramĂštres de type d'un type gĂ©nĂ©rique doivent Ă©galement ĂȘtre rĂ©pertoriĂ©s dans le rĂ©cepteur.

Peut-ĂȘtre que gofmt pourrait aider d'une maniĂšre ou d'une autre ? Peut-ĂȘtre devrions-nous passer Ă  plusieurs lignes. Cela vaut peut-ĂȘtre la peine de jouer avec.

Comme mentionnĂ© par d'autres, cela vous permet d'avoir le mĂȘme type gĂ©nĂ©rique implĂ©mentant diffĂ©rentes mĂ©thodes, selon les types avec lesquels il a Ă©tĂ© instanciĂ©.

Je vois ce que tu dis @Merovius

Cela a Ă©tĂ© appelĂ© par Wadler comme une diffĂ©rence, et cela lui permet de rĂ©soudre son problĂšme d'expression, mais vous faites bien remarquer que le type de packages hermĂ©tiques de Go semble limiter ce que vous pouvez / devriez faire avec cela. Pouvez-vous penser Ă  un cas rĂ©el oĂč vous voudriez faire cela?

Comme mentionnĂ© par d'autres, cela vous permet d'avoir le mĂȘme type gĂ©nĂ©rique implĂ©mentant diffĂ©rentes mĂ©thodes, selon les types avec lesquels il a Ă©tĂ© instanciĂ©.

Je vois ce que tu dis @Merovius

Cela a Ă©tĂ© appelĂ© par Wadler comme une diffĂ©rence, et cela lui permet de rĂ©soudre son problĂšme d'expression, mais vous faites bien remarquer que le type de packages hermĂ©tiques de Go semble limiter ce que vous pouvez / devriez faire avec cela. Pouvez-vous penser Ă  un cas rĂ©el oĂč vous voudriez faire cela?

Ironiquement, ma premiĂšre pensĂ©e a Ă©tĂ© qu'il pourrait ĂȘtre utilisĂ© pour rĂ©soudre certains des dĂ©fis dĂ©crits dans cet article : https://blog.merovius.de/2017/07/30/the-trouble-with-optional-interfaces.html

@boĂźte Ă  outils

Par ailleurs, je m'intéresse à ce que les génériques peuvent faire au-delà des cartes, des filtres et des choses fonctionnelles,

FWIW, il convient de préciser qu'il s'agit en quelque sorte de vendre "des cartes, des filtres et des éléments fonctionnels". Personnellement, je ne veux pas map et filter sur les structures de données intégrées dans mon code, par exemple (je préfÚre les boucles for). Mais cela peut aussi signifier

  1. Fournir un accÚs généralisé à toute structure de données tierce. c'est-à-dire map et filter peuvent fonctionner sur des arbres génériques, ou des cartes triées, ou
 aussi. Ainsi, vous pouvez échanger ce qui est cartographié, pour plus de puissance. Et plus important
  2. Vous pouvez Ă©changer la façon dont il est mappĂ©. Par exemple, vous pouvez construire une version de Compose qui peut gĂ©nĂ©rer plusieurs goroutines pour chaque fonction et les exĂ©cuter simultanĂ©ment, en utilisant des canaux. Cela faciliterait l'exĂ©cution simultanĂ©e de pipelines de traitement de donnĂ©es et la mise Ă  l'Ă©chelle automatique du goulot d'Ă©tranglement, tout en n'ayant besoin que d'Ă©crire func(A) B s. Ou vous pouvez mettre les mĂȘmes fonctions dans un cadre qui exĂ©cute des milliers de copies du programme dans un cluster, en planifiant des lots de donnĂ©es Ă  travers eux (c'est ce Ă  quoi j'ai fait allusion lorsque j'ai liĂ© Ă  Flume ci-dessus).

Ainsi, bien que pouvoir Ă©crire Map et Filter et Reduce puisse sembler ennuyeux Ă  premiĂšre vue, les mĂȘmes techniques ouvrent des possibilitĂ©s vraiment intĂ©ressantes pour faciliter le calcul Ă©volutif.

@ChrisHines

Ironiquement, ma premiĂšre pensĂ©e a Ă©tĂ© qu'il pourrait ĂȘtre utilisĂ© pour rĂ©soudre certains des dĂ©fis dĂ©crits dans cet article : https://blog.merovius.de/2017/07/30/the-trouble-with-optional-interfaces.html

C'est une pensĂ©e intĂ©ressante et il se sent certainement comme il se doit. Mais je ne vois pas encore comment. Si vous prenez l'exemple ResponseWriter , il semble que cela pourrait vous permettre d'Ă©crire des wrappers gĂ©nĂ©riques de type sĂ©curisĂ©, avec diffĂ©rentes mĂ©thodes en fonction de ce que le ResponseWriter enveloppĂ© prend en charge. Mais, mĂȘme si vous pouvez utiliser diffĂ©rentes bornes sur diffĂ©rentes mĂ©thodes, vous devez toujours les Ă©crire. Ainsi, mĂȘme si cela peut rendre la situation sĂ©curisĂ©e en ce sens que vous n'ajoutez pas de mĂ©thodes que vous ne prenez pas en charge, vous devez toujours Ă©numĂ©rer toutes les mĂ©thodes que vous pourriez prendre en charge, de sorte que le middleware peut toujours masquer certaines interfaces facultatives. simplement en ne les connaissant pas. En attendant, vous pouvez Ă©galement (mĂȘme sans cette fonctionnalitĂ©) faire

type Middleware (type RW http.ResponseWriter) struct {
    RW
}

et Ă©crasez les mĂ©thodes sĂ©lectives qui vous intĂ©ressent - et faites promouvoir toutes les autres mĂ©thodes de RW . Ainsi, vous n'avez mĂȘme pas besoin d'Ă©crire des wrappers et mĂȘme d'obtenir de maniĂšre transparente ces mĂ©thodes que vous ne connaissiez pas.

Donc, en supposant que nous obtenions des méthodes promues pour les paramÚtres de type intégrés dans des structures génériques (et j'espÚre que nous le ferons), les problÚmes semblent mieux résolus par cette méthode.

Je pense que la solution spécifique à http.ResponseWriter est quelque chose comme errors.Is/As . Il n'est pas nécessaire de changer de langue, juste un ajout de bibliothÚque pour créer une méthode standard d'encapsulation ResponseWriter et un moyen de demander si l'un des ResponseWriters d'une chaßne peut gérer, par exemple wPush. Je suis sceptique quant au fait que les génériques conviendraient bien à quelque chose comme ça, car le but est d'avoir le choix d'exécution entre les interfaces facultatives, par exemple Push n'est disponible qu'en http2 et pas si je fais tourner un serveur de développement local http1.

En regardant Ă  travers Github, je ne pense pas avoir jamais crĂ©Ă© de problĂšme pour cette idĂ©e, alors peut-ĂȘtre que je vais le faire maintenant.

Modifier : #39558.

@toolbox
Je suppose que cela ressemblerait à quelque chose comme ça, avec son code de monomorphisation interne :

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



Je suppose que cela ressemblerait à quelque chose comme ça, avec son code de monomorphisation interne :

FWIW voici un de mes tweets d'il y a quelques années explorant comment les itérateurs pourraient fonctionner dans Go avec des génériques. Si vous effectuez une substitution globale pour remplacer <T> par (type T) , vous avez quelque chose de proche de la proposition actuelle : https://twitter.com/rogpeppe/status/425035488425037824

FWIW, il convient de préciser qu'il s'agit en quelque sorte de vendre "des cartes, des filtres et des éléments fonctionnels". Personnellement, je ne veux pas de carte et de filtre sur les structures de données intégrées dans mon code, par exemple (je préfÚre les boucles for). Mais cela peut aussi signifier...

Je vois votre point de vue et je ne suis pas en désaccord, et oui, nous bénéficierons des éléments couverts par vos exemples.
Mais je me demande toujours comment quelque chose comme k8s serait affectĂ©, ou une autre base de code avec des types de donnĂ©es "gĂ©nĂ©riques" oĂč les types d'actions effectuĂ©es ne sont pas des cartes ou des filtres, ou du moins vont au-delĂ . Je me demande Ă  quel point les contrats ou FGG sont efficaces pour augmenter la sĂ©curitĂ© de type et les performances dans ce genre de contextes.

Vous vous demandez si quelqu'un peut pointer vers une base de code, espérons-le plus simple que k8s, qui rentre dans ce genre de catégorie ?

@urandom whoa. Donc, si vous instanciez un mapIt avec un parent qui implémente ReversibleIterator alors mapIt a une méthode NextBack() et sinon, ce n'est pas le cas t. Est-ce que j'ai bien lu ?

En y réfléchissant, il semble que ce soit utile du point de vue de la bibliothÚque. Vous avez des types de structure génériques assez ouverts (paramÚtres de type Any ) et ils ont beaucoup de méthodes, contraintes par diverses interfaces. Ainsi, lorsque vous utilisez la bibliothÚque dans votre propre code, le type que vous intégrez dans la structure vous donne la possibilité d'appeler un certain ensemble de méthodes, de sorte que vous obtenez un certain ensemble de fonctionnalités de la bibliothÚque. Ce qu'est cet ensemble de fonctionnalités est déterminé au moment de la compilation en fonction des méthodes de votre type.

... Cela ressemble un peu à ce que @ChrisHines a évoqué en ce sens que vous pourriez en quelque sorte écrire du code qui a plus ou moins de fonctionnalités en fonction de ce que votre type implémente, mais encore une fois, c'est vraiment une question d'augmentation ou de diminution de l'ensemble de méthodes disponibles, pas le comportement d'une seule méthode, donc oui, je ne vois pas comment le pirate de l'air http2 est aidé avec cela.

En tout cas, trÚs intéressant.

Non pas que je ferais cela, mais je suppose que ce serait possible:

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
}

Encore une fois, c'est surtout une blague, mais je pense qu'il est bon d'explorer les limites de ce qui est possible.

Edit: Hm, montre comment vous pouvez avoir diffĂ©rents ensembles de mĂ©thodes en fonction du paramĂštre de type, mais produit exactement le mĂȘme effet qu'en incorporant simplement Parent dans Child . Encore un exemple idiot ;)

Je ne suis pas un grand fan d'avoir des mĂ©thodes qui ne peuvent ĂȘtre appelĂ©es qu'avec un certain type. Compte tenu de l'exemple de @toolbox , il serait probablement difficile de tester en raison du fait que certaines mĂ©thodes ne peuvent ĂȘtre appelĂ©es qu'avec un enfant spĂ©cifique - le testeur risque de manquer certains cas. Il est Ă©galement assez difficile de savoir quelles mĂ©thodes sont disponibles et exiger qu'un IDE fournisse des suggestions n'est pas ce que Go devrait exiger. Cependant, vous pouvez implĂ©menter cela en utilisant uniquement le type donnĂ© par la structure en faisant une assertion de type dans la mĂ©thode.

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

Ce code est également sûr, clair, facile à tester et fonctionne probablement de maniÚre identique à l'original sans confusion.

@TotallyGamerJet
Cet exemple particulier est de type sécurisé, mais d'autres ne le sont pas, et nécessiteront des paniques d'exécution avec des types incompatibles.

De plus, je ne sais pas comment le testeur pourrait manquer des cas, Ă©tant donnĂ© que ce sont probablement ceux qui ont Ă©crit le code gĂ©nĂ©rique en premier lieu. De plus, que ce soit clair ou non est un peu subjectif, mĂȘme si cela ne nĂ©cessite certainement pas un IDE pour en dĂ©duire. Gardez Ă  l'esprit qu'il ne s'agit pas d'une surcharge de fonction, la mĂ©thode peut ĂȘtre appelĂ©e ou non, donc ce n'est pas comme si un cas pouvait ĂȘtre ignorĂ© par accident. N'importe qui peut voir que cette mĂ©thode existe pour un certain type et peut avoir besoin de la relire pour comprendre quel type est requis, mais c'est Ă  peu prĂšs tout.

@urandom Je ne voulais pas nĂ©cessairement dire avec cet exemple spĂ©cifique que quelqu'un manquerait un cas - c'est trĂšs court. Je voulais dire que lorsque vous avez des tonnes de mĂ©thodes qui ne peuvent ĂȘtre appelĂ©es que pour certains types. Je m'en tiens donc Ă  ne pas utiliser de sous-typage (comme j'aime l'appeler). Il est mĂȘme possible de rĂ©soudre le "problĂšme d'expression" sans utiliser d'assertions de type ou de sous-typage. Voici comment:

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

Toute utilisation abusive de la mĂ©thode Eval doit ĂȘtre interceptĂ©e au moment de la compilation car il n'est pas autorisĂ© d'appeler Eval sur Plus avec un type qui n'implĂ©mente pas l'addition. Bien qu'il soit possible d'utiliser incorrectement String() (Ă©ventuellement en ajoutant des structures), de bons tests devraient dĂ©tecter ces cas. And Go privilĂ©gie gĂ©nĂ©ralement la simplicitĂ© Ă  la "correction". La seule chose qui est gagnĂ©e avec le sous-typage est plus de confusion dans les docs et dans l'utilisation. Si vous pouvez fournir un exemple qui nĂ©cessite un sous-typage, je serais peut-ĂȘtre plus enclin Ă  penser que c'est une bonne idĂ©e, mais actuellement, je ne suis pas convaincu.
EDIT : Correction d'une erreur et amélioration

@TotallyGamerJet dans votre exemple, la méthode String doit appeler String de maniÚre récursive, pas Eval

@TotallyGamerJet dans votre exemple, la méthode String doit appeler String de maniÚre récursive, pas Eval

@magique
Je ne suis pas sûr de ce que tu veux dire. Le type de la structure Plus est un Evaler qui ne garantit pas que fmt.Stringer est satisfait. L'appel de String() sur les deux Evalers nécessiterait une assertion de type et ne serait donc pas typesafe.

@TotallyGamerJet
Malheureusement, c'est l'idée de la méthode String. Il doit appeler de maniÚre récursive toutes les méthodes String sur ses membres, sinon cela ne sert à rien. Mais vous voyez déjà qu'il faudrait une assertion de type et une panique si vous ne pouvez pas vous assurer que la méthode sur le type Plug nécessite un type a qui a une méthode String

@urandom
Vous avez raison! Étonnamment, le Sprintf fera cette affirmation de type pour vous. Ainsi, vous pouvez simplement envoyer les champs gauche et droit. Bien qu'il puisse toujours paniquer si les types de Plus n'implĂ©mentent pas Stringer, mais cela me convient car il est possible d'Ă©viter les paniques en utilisant le verbe %v pour imprimer la structure (il appellera String( ) si disponible). Je pense que cette solution est claire et que toute autre incertitude devrait ĂȘtre documentĂ©e dans le code. Je ne suis donc toujours pas convaincu de la nĂ©cessitĂ© du sous-typage.

@TotallyGamerJet
Personnellement, je ne vois toujours pas quels problÚmes peuvent survenir s'il est permis d'avoir des méthodes avec des contraintes différentes. La méthode est toujours là et le code décrit clairement les arguments (et le récepteur, dans le cas particulier) requis.
Tout comme avoir une méthode, accepter un argument string , ou un récepteur MyType , est clairement lisible et sans ambiguïté, la définition suivante le serait également :

func (rec MyType(type T SomeInterface(T)) Foo() T

Les exigences sont clairement indiquĂ©es dans la signature elle-mĂȘme. IE c'est de MyType(type T SomeInterface(T)) et rien d'autre.

Le changement https://golang.org/cl/238003 mentionne ce problÚme : design: add go2draft-type-parameters.md

Le changement https://golang.org/cl/238241 mentionne ce problÚme : content: add generics-next-step article

Noël est en avance !

  • Je peux voir que beaucoup d'efforts ont Ă©tĂ© dĂ©ployĂ©s pour rendre le document de conception accessible, cela se voit et c'est gĂ©nial et trĂšs apprĂ©ciĂ©.
  • Cette itĂ©ration est une amĂ©lioration majeure Ă  mes yeux et je pouvais voir cela ĂȘtre implĂ©mentĂ© tel quel.
  • D'accord avec Ă  peu prĂšs tout le raisonnement et la logique.
  • Ainsi, si vous spĂ©cifiez une contrainte pour un seul paramĂštre de type, vous devez le faire pour tous.
  • Les sons comparables sont bons.
  • Les listes de types dans les interfaces ne sont pas mauvaises ; d'accord, c'est mieux que les mĂ©thodes d'opĂ©rateur, mais dans mon esprit, c'est probablement le plus grand domaine pour une discussion plus approfondie.
  • L'infĂ©rence de type est (toujours) gĂ©niale.
  • L'infĂ©rence pour les contraintes paramĂ©trĂ©es par type Ă  argument unique ressemble Ă  de l'intelligence plutĂŽt qu'Ă  de la clartĂ©.
  • J'aime "Nous ne prĂ©tendons pas que c'est simple" dans l'exemple de graphique. C'est trĂšs bien.
  • (type *T constraint) ressemble Ă  une bonne solution au problĂšme du pointeur.
  • EntiĂšrement d'accord sur le changement func(x(T)) .
  • Je pense que nous voulons une infĂ©rence de type pour les littĂ©raux composites dĂšs le dĂ©part? 😄

Merci Ă  l'Ă©quipe Go! 🎉

https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#comparable-types-in-constraints

Je crois que comparable ressemble plus à un type intégré qu'à une interface. Je crois que c'est un petit bogue dans le brouillon de la proposition.

type ComparableHasher interface {
    comparable
    Hash() uintptr
}

besoin d'ĂȘtre

type ComparableHasher interface {
    type comparable
    Hash() uintptr
}

Le terrain de jeu semble Ă©galement indiquer qu'il doit ĂȘtre type comparable
https://go2goplay.golang.org/p/mhrl0xYsMyj

EDIT : Ian Lance Taylor et Robert Griesemer corrigent l'outil go2go (il y avait un petit bogue dans le traducteur go2go, pas le brouillon. Le brouillon de conception était correct)

Y a-t-il eu des rĂ©flexions sur la possibilitĂ© pour les gens d'Ă©crire leurs propres tables de hachage gĂ©nĂ©riques et autres ? ISTM qui est actuellement trĂšs limitĂ© (surtout par rapport Ă  la carte intĂ©grĂ©e). Fondamentalement, la carte intĂ©grĂ©e a comparable comme contrainte de clĂ©, mais bien sĂ»r, == et != ne suffisent pas pour implĂ©menter une table de hachage. Une interface comme ComparableHasher ne fait que transmettre la responsabilitĂ© d'Ă©crire une fonction de hachage Ă  l'appelant, elle ne rĂ©pond pas Ă  la question de savoir Ă  quoi elle ressemblerait rĂ©ellement (de plus, l'appelant ne devrait probablement pas en ĂȘtre responsable ; Ă©crire de bonnes fonctions de hachage est difficile). Enfin, l'utilisation de pointeurs comme clĂ©s peut ĂȘtre fondamentalement impossible - convertir un pointeur en un uintptr Ă  utiliser comme index risquerait que le GC dĂ©place le pointĂ© et donc le seau Ă  changer (sauf ce problĂšme, exposant un prĂ©dĂ©clarĂ© func hash(type T comparable)(v T) uintptr pourrait ĂȘtre une solution - probablement pas idĂ©ale -).

Je peux bien accepter "ce n'est pas vraiment faisable" comme réponse, je suis juste curieux de savoir si vous y avez pensé :)

@gertcuykens J'ai commis un correctif pour l'outil go2go pour gérer comparable comme prévu.

@Merovius Nous nous attendons Ă  ce que les personnes qui Ă©crivent une table de hachage gĂ©nĂ©rique fournissent leur propre fonction de hachage, et Ă©ventuellement leur propre fonction de comparaison. Lorsque vous Ă©crivez votre propre fonction de hachage, le package https://golang.org/pkg/hash/maphash/ peut ĂȘtre utile. Vous avez raison de dire que le hachage d'une valeur de pointeur doit dĂ©pendre de la valeur vers laquelle pointe ce pointeur ; il ne peut pas dĂ©pendre de la valeur du pointeur converti en uintptr .

Je ne sais pas s'il s'agit d'une limitation de l'implémentation actuelle de l'outil, mais une tentative de retour d'un type générique contraint par une interface renvoie une erreur :
https://go2goplay.golang.org/p/KYRFL-vrcUF

J'ai implémenté un cas d'utilisation réel que j'avais pour les génériques hier . C'est une abstraction de pipeline générique qui permet de mettre à l'échelle les étapes du pipeline de maniÚre indépendante et prend en charge l'annulation et la gestion des erreurs (elle ne s'exécute pas dans le terrain de jeu, car elle dépend de errgroup , mais l'exécuter à l'aide de l'outil go2go semble travail). Quelques remarques :

  • C'Ă©tait plutĂŽt amusant. Avoir un vĂ©rificateur de type fonctionnel a en fait beaucoup aidĂ© lors de l'itĂ©ration de la conception, en traduisant les dĂ©fauts de conception en erreurs de type. Le rĂ©sultat final est d'environ 100 LOC, commentaires compris. Donc, dans l'ensemble, l'expĂ©rience d'Ă©criture de code gĂ©nĂ©rique est agrĂ©able, IMO.
  • Ce cas d'utilisation fonctionne au moins sans problĂšme avec l'infĂ©rence de type, aucune instanciation explicite n'est nĂ©cessaire. Je pense que cela augure bien pour la conception de l'infĂ©rence.
  • Je pense que cet exemple bĂ©nĂ©ficierait de la possibilitĂ© d'avoir des mĂ©thodes avec des paramĂštres de type supplĂ©mentaires. Avoir besoin d'une fonction de niveau supĂ©rieur pour Compose signifie que la construction du pipeline se produit Ă  l'envers - les derniĂšres Ă©tapes du pipeline doivent ĂȘtre construites pour le transmettre aux fonctions construisant les Ă©tapes prĂ©cĂ©dentes. Si les mĂ©thodes pouvaient avoir des paramĂštres de type, vous pourriez avoir Stage un type concret et faire func (s *Stage(A, B)) Compose(type C)(n int, f func(B) C) *Stage(A, C) . Et la construction du pipeline serait dans le mĂȘme ordre que sa plomberie (voir le commentaire dans la cour de rĂ©crĂ©ation). Il pourrait bien sĂ»r aussi y avoir une API plus Ă©lĂ©gante dans le brouillon existant que je ne vois pas - il est difficile de prouver un nĂ©gatif. Je serais intĂ©ressĂ© de voir un exemple de travail de cela.

Dans l'ensemble, j'aime le nouveau projet, FWIW :) L'abandon des contrats de l'OMI est une amélioration, tout comme la nouvelle façon de spécifier les opérateurs requis via des listes de types.

[modifier : Correction d'un bogue dans mon code oĂč un blocage pouvait se produire si une Ă©tape de pipeline Ă©chouait. La simultanĂ©itĂ© est difficile]

Une question pour la branche outil : va-t-elle suivre la derniĂšre version go (donc v1.15, v1.15.1, ...) ?

@urandom : Notez que la valeur que vous renvoyez dans votre code est de type Foo(T). Chaque
une telle instanciation de type produit un nouveau type défini, dans ce cas Foo(T).
(Bien sûr, si vous avez plusieurs Foo(T) dans le code, ils sont tous pareils
type défini).

Mais le type de résultat de votre fonction est V, qui est un paramÚtre de type. Noter
que le paramĂštre de type est contraint par l'interface Valuer, mais il est
_pas_ une interface (ou mĂȘme cette interface). V est un paramĂštre de type qui est
une nouvelle sorte de type dont nous connaissons les choses décrites par sa contrainte.
En ce qui concerne l'assignabilité, il agit comme un type défini nommé V.

Vous essayez donc d'attribuer une valeur de type Foo(T) Ă  une variable de type V
(qui n'est ni Foo(T) ni Valuer(T), il n'a que des propriétés décrites par
Valorisateur(T)). La mission Ă©choue donc.

(En passant, nous affinons encore notre compréhension des paramÚtres de type
et éventuellement besoin de l'épeler assez précisément pour que nous puissions écrire un
spéc. Mais gardez à l'esprit que chaque paramÚtre de type est en fait un nouveau
type défini sur nous ne savons que ce que sa contrainte de type spécifie.)

Peut-ĂȘtre vouliez-vous Ă©crire ceci : https://go2goplay.golang.org/p/8Hz6eWSn8Ek ?

@Inuart Si par branche d'outils, vous entendez la branche dev.go2go: il s'agit d'un prototype, il a Ă©tĂ© construit dans un souci d'opportunitĂ© et Ă  des fins d'expĂ©rimentation. Nous voulons que les gens jouent avec et essaient d'Ă©crire du code, mais ce n'est pas une bonne idĂ©e de _s'appuyer_ sur le traducteur pour le logiciel de production. Beaucoup de choses peuvent changer (mĂȘme la syntaxe, si besoin est). Nous allons corriger les bugs et ajuster la conception au fur et Ă  mesure que nous apprenons des commentaires. Se tenir au courant des derniĂšres versions de Go semble moins important.

J'ai implémenté un cas d'utilisation réel que j'avais pour les génériques hier. Il s'agit d'une abstraction de pipeline générique qui permet de mettre à l'échelle les étapes du pipeline de maniÚre indépendante et prend en charge l'annulation et la gestion des erreurs (elle ne s'exécute pas dans le terrain de jeu, car elle dépend de errgroup, mais l'exécuter à l'aide de l'outil go2go semble fonctionner).

J'aime l'exemple. Je viens de le lire entiĂšrement et la chose qui m'a le plus fait trĂ©bucher (ne vaut mĂȘme pas la peine d'ĂȘtre expliquĂ©e) n'avait rien Ă  voir avec les gĂ©nĂ©riques impliquĂ©s. Je pense que la mĂȘme construction sans gĂ©nĂ©riques ne serait pas beaucoup plus facile Ă  saisir. C'est aussi certainement l'une de ces choses que vous voulez Ă©crire une fois, avec des tests, et ne pas avoir Ă  vous tromper plus tard.

Une chose qui pourrait aider Ă  la lisibilitĂ© et Ă  la rĂ©vision est si l'outil Go avait un moyen d'afficher la version monomorphisĂ©e du code gĂ©nĂ©rique, afin que vous puissiez voir comment les choses se passent. Peut-ĂȘtre irrĂ©alisable, en partie parce que les fonctions pourraient mĂȘme ne pas ĂȘtre monomorphisĂ©es dans l'implĂ©mentation finale du compilateur, mais je pense que ce serait prĂ©cieux si c'Ă©tait rĂ©alisable.

Je pense que cet exemple bénéficierait de la possibilité d'avoir des méthodes avec des paramÚtres de type supplémentaires.

J'ai également vu ce commentaire dans votre cour de récréation; certainement la syntaxe d'appel alternative semble plus lisible et simple. Pourriez-vous expliquer cela plus en détail? Ayant à peine compris votre code d'exemple, j'ai du mal à faire le saut :)

Vous essayez donc d'attribuer une valeur de type Foo(T) Ă  une variable de type V
(qui n'est ni Foo(T) ni Valuer(T), il n'a que des propriétés décrites par
Valorisateur(T)). La mission Ă©choue donc.

Excellente explication.

... Sinon, c'est triste de voir que le poste HN a été détourné par la foule de Rust. Il aurait été bien d'avoir plus de commentaires de Gophers sur la proposition.

Deux questions Ă  l'Ă©quipe Go :

Y a-t-il une différence entre les deux, ou est-ce un bug dans le terrain de jeu go2 ? Le premier compile, le second donne une erreur

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
}

Échec avec : invalid operation: operator + not defined for a (variable of type T)

Eh bien, ce fut une surprise des plus inattendues et agréables. J'espérais un moyen d'essayer cela à un moment donné, mais je ne m'y attendais pas de sitÎt.

Tout d'abord, trouvé un bug : https://go2goplay.golang.org/p/1r0NQnJE-NZ

DeuxiÚmement, j'ai construit un exemple d'itérateur et j'ai été un peu surpris de constater que cette inférence de type ne fonctionnait pas. Je peux simplement lui faire renvoyer directement un type d'interface, mais je ne pensais pas qu'il ne serait pas en mesure de déduire celui-ci puisque toutes les informations de type dont il a besoin passent par l'argument.

Edit : De plus, comme plusieurs personnes l'ont dit, je pense qu'il serait trĂšs utile d'autoriser l'ajout de nouveaux types lors des dĂ©clarations de mĂ©thode. En ce qui concerne l'implĂ©mentation de l'interface, vous pouvez simplement ne pas autoriser l'implĂ©mentation de l'interface, autoriser uniquement l'implĂ©mentation si l'interface appelle Ă©galement des gĂ©nĂ©riques lĂ -bas ( type Example interface { Method(type T someConstraint)(v T) bool } ), ou, Ă©ventuellement, vous pouvez lui faire implĂ©menter l'interface si _any_ possible variante de celui-ci implĂ©mente l'interface, puis l'appelle ĂȘtre contraint Ă  ce que l'interface veut si elle est appelĂ©e via l'interface. Par example,

``` allez
type interface interface {
Obtenir (chaĂźne) chaĂźne
}

type Exemple(type T) struct {
v T
}

// Cela ne fonctionnera que parce que Interface.Get est plus spécifique que Example.Get.
func (e Exemple(T)) Get(type R)(v R) T {
return fmt.Sprintf("%v: %v", v, ev)
}

func DoSomething(inter Interface) {
// Le sous-jacent est Example(string) et Example(string).Get(string) est supposé car il est obligatoire.
fmt.Println(inter.Get("exemple"))
}

fonction principale() {
// Autorisé car Example(string).Get(string) est possible.
FaireQuelquechose(Exemple(chaßne){v : "Un exemple."})
}

@DeedleFake La premiÚre chose que vous signalez n'est pas un bogue. Vous devrez écrire https://go2goplay.golang.org/p/qo3hnviiN4k pour le moment. Ceci est documenté dans le projet de conception. Dans une liste de paramÚtres, écrire a(b) est interprété comme a (b) ( a de type entre parenthÚses b ) pour la rétrocompatibilité. Nous pourrions changer cela à l'avenir.

L'exemple d'Iterator est intéressant - il ressemble à un bogue à premiÚre vue. Veuillez signaler un bogue (instructions dans l'article de blog) et attribuez-le-moi. Merci.

@Kashomon Le billet de blog (https://blog.golang.org/generics-next-step) suggÚre la liste de diffusion pour la discussion et le dépÎt de problÚmes distincts pour les bogues. Merci.

Je pense que le problÚme avec + a déjà été résolu.

@toolbox

Une chose qui pourrait aider Ă  la lisibilitĂ© et Ă  la rĂ©vision est si l'outil Go avait un moyen d'afficher la version monomorphisĂ©e du code gĂ©nĂ©rique, afin que vous puissiez voir comment les choses se passent. Peut-ĂȘtre irrĂ©alisable, en partie parce que les fonctions pourraient mĂȘme ne pas ĂȘtre monomorphisĂ©es dans l'implĂ©mentation finale du compilateur, mais je pense que ce serait prĂ©cieux si c'Ă©tait rĂ©alisable.

L'outil go2go peut le faire. Au lieu d'utiliser go tool go2go run x.go2 , Ă©crivez go tool go2go translate x.go2 . Cela produira un fichier x.go avec le code traduit.

Cela dit, je dois dire que c'est assez difficile Ă  lire. Pas impossible, mais pas facile.

@griesemer

Je comprends que l'argument return peut ĂȘtre une interface Ă  la place, mais je ne comprends pas vraiment pourquoi il ne peut pas s'agir du type gĂ©nĂ©rique lui-mĂȘme.

Vous pouvez, par exemple, utiliser ce mĂȘme type gĂ©nĂ©rique comme paramĂštre d'entrĂ©e, et cela fonctionne trĂšs bien :
https://go2goplay.golang.org/p/LuDrlT3zLRb
Est-ce que cela fonctionne parce que le type a déjà été instancié ?

@urandom a Ă©crit :

Je comprends que l'argument return peut ĂȘtre une interface Ă  la place, mais je ne comprends pas vraiment pourquoi il ne peut pas s'agir du type gĂ©nĂ©rique lui-mĂȘme.

Théoriquement, c'est possible, mais cela n'a pas de sens de rendre un type de retour générique lorsque le type de retour n'est pas générique car il est déterminé par le bloc fonction, c'est-à-dire par la valeur de retour.

Normalement, les paramÚtres génériques sont entiÚrement déterminés par le tuple de la valeur du paramÚtre ou par le type de l'application de la fonction sur le site d'appel (détermine l'instanciation du type de retour générique).

ThĂ©oriquement, vous pouvez Ă©galement autoriser des paramĂštres de type gĂ©nĂ©riques qui ne sont pas dĂ©terminĂ©s par le tuple de valeur de paramĂštre et doivent ĂȘtre fournis explicitement, par exemple :

func f(type S)(i int) int
{
    s S =...
    return 2
}

Je ne sais pas Ă  quel point cela a du sens.

@urandom Je ne voulais pas nĂ©cessairement dire avec cet exemple spĂ©cifique que quelqu'un manquerait un cas - c'est trĂšs court. Je voulais dire que lorsque vous avez des tonnes de mĂ©thodes qui ne peuvent ĂȘtre appelĂ©es que pour certains types. Je m'en tiens donc Ă  ne pas utiliser de sous-typage (comme j'aime l'appeler). Il est mĂȘme possible de rĂ©soudre le "problĂšme d'expression" sans utiliser d'assertions de type ou de sous-typage. Voici comment:

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

Toute utilisation abusive de la mĂ©thode Eval doit ĂȘtre interceptĂ©e au moment de la compilation car il n'est pas autorisĂ© d'appeler Eval sur Plus avec un type qui n'implĂ©mente pas l'addition. Bien qu'il soit possible d'utiliser incorrectement String() (Ă©ventuellement en ajoutant des structures), de bons tests devraient dĂ©tecter ces cas. And Go privilĂ©gie gĂ©nĂ©ralement la simplicitĂ© Ă  la "correction". La seule chose qui est gagnĂ©e avec le sous-typage est plus de confusion dans les docs et dans l'utilisation. Si vous pouvez fournir un exemple qui nĂ©cessite un sous-typage, je serais peut-ĂȘtre plus enclin Ă  penser que c'est une bonne idĂ©e, mais actuellement, je ne suis pas convaincu.
EDIT : Correction d'une erreur et amélioration

Je ne sais pas, pourquoi ne pas utiliser '<>' ?

@99yun
Veuillez consulter la FAQ incluse avec le brouillon mis Ă  jour

Pourquoi ne pas utiliser la syntaxe F\comme C++ et Java ?
Lors de l'analyse de code dans une fonction, telle que v := F\, au moment de voir le <, il est ambigu de savoir si nous voyons une instanciation de type ou une expression utilisant l'opérateur <. Résoudre cela nécessite une anticipation effectivement illimitée. En général, nous nous efforçons de garder l'analyseur Go efficace.

@urandom Un corps de fonction générique est toujours vérifié par type sans instanciation (*) ; en général (s'il est exporté, par exemple) on ne peut pas savoir comment il sera instancié. Lors du contrÎle de type, il ne peut s'appuyer que sur les informations disponibles. Si le type de résultat est un paramÚtre de type et que l'expression de retour est d'un type différent qui n'est pas compatible avec l'affectation, le retour ne peut pas fonctionner. Ou en d'autres termes, si une fonction générique est invoquée avec des arguments de type (éventuellement déduits), le corps de la fonction n'est pas vérifié à nouveau avec ces arguments de type. Il vérifie uniquement que les arguments de type satisfont aux contraintes de la fonction générique (aprÚs avoir instancié la signature de la fonction avec ces arguments de type). J'espÚre que cela pourra aider.

(*) Plus précisément, la fonction générique est typée car elle a été instanciée avec ses propres paramÚtres de type ; les paramÚtres de type sont des types réels ; nous ne savons à leur sujet que ce que leurs contraintes nous disent.

S'il vous plaĂźt, continuons cette discussion ailleurs. Si vous avez d'autres questions avec un morceau de code qui, selon vous, devrait fonctionner, veuillez signaler un problĂšme afin que nous puissions en discuter lĂ -bas. Merci.

Il ne semble pas y avoir de moyen d'utiliser une fonction pour créer une valeur zéro d'une structure générique. Prenons par exemple cette fonction :

func zero(type T)() T {
    var zero T
    return zero
}

Cela semble fonctionner pour les types de base (int, float32 etc.). Cependant, lorsque vous avez une structure qui a un champ générique, les choses deviennent étranges. Prends pour exemple:

type Opt(type T) struct {
    val T
}

func (o Opt(T)) Do() { /*stuff*/ }

Tout semble bon. Cependant, lorsque vous faites :

opt := zero(Opt(int))
opt.Do() 

il ne compile pas en donnant l'erreur : opt.Do undefined (type func() Opt(int) has no field or method Do) Je peux comprendre s'il n'est pas possible de le faire, mais il est étrange de penser qu'il s'agit d'une fonction alors que int est censé faire partie du type Opt. Mais ce qui est plus bizarre, c'est qu'il est possible de faire ceci :

opt := zero(Opt)      //  But somehow this line compiles
opt(int).Do()         // This will panic

Je ne sais pas quelle partie est un bogue et quelle partie est destinée.
Code : https://go2goplay.golang.org/p/M0VvyEYwbQU

@TotallyGamerJet

Votre fonction zero() n'a pas d'arguments donc il n'y a pas d'inférence de type en cours. Vous devez instancier la fonction zero puis l'appeler.

opt := zero(Opt(int))()
opt.Do()

https://go2goplay.golang.org/p/N6ip-nm1BP-

@toolbox
Ah oui. Je pensais que je fournissais le type mais j'ai oublié le deuxiÚme ensemble de parenthÚses pour appeler la fonction. Je m'habitue encore à ces génériques.

J'ai toujours compris que ne pas avoir de gĂ©nĂ©riques dans Go Ă©tait une dĂ©cision de conception et non un oubli. Cela a rendu Go tellement plus simple et je ne peux pas imaginer la paranoĂŻa exagĂ©rĂ©e contre une simple duplication de copie. Dans notre entreprise, nous avons crĂ©Ă© des tonnes de code Go et n'avons jamais trouvĂ© un seul cas oĂč nous prĂ©fĂ©rerions les gĂ©nĂ©riques.

Pour nous, cela fera certainement en sorte que Go se sente moins Go et il semble que la foule à la mode ait finalement réussi à affecter le développement de Go dans la mauvaise direction. Ils ne pouvaient pas simplement laisser Go dans sa beauté simpliste, non, ils devaient continuer à se plaindre et à se plaindre jusqu'à ce qu'ils obtiennent enfin ce qu'ils voulaient.

Je suis désolé, ce n'est pas destiné à dégrader qui que ce soit, mais c'est ainsi que commence la destruction d'un langage magnifiquement conçu. Et aprÚs? Si nous continuons à changer des choses, comme tant de gens le voudraient, nous nous retrouvons avec "C++" ou "JavaScript".

Laissez simplement aller comme il se doit!

@iio7 Je suis le QI le plus bas de tous ici, mon avenir dĂ©pend de ma capacitĂ© Ă  lire le code des autres. Le battage mĂ©diatique vient de commencer non seulement Ă  cause des gĂ©nĂ©riques, mais parce que le nouveau design ne nĂ©cessite pas de changement de langage dans la proposition actuelle, nous sommes donc tous ravis qu'il y ait une fenĂȘtre pour garder les choses simples et avoir encore quelques goodies gĂ©nĂ©riques et fonctionnels. Ne vous mĂ©prenez pas, je sais qu'il y aura toujours quelqu'un dans l'Ă©quipe qui Ă©crit du code comme un spĂ©cialiste des fusĂ©es et moi, le singe, supposons que je le comprenne comme ça ? Donc, les exemples que vous voyez maintenant sont ceux du spĂ©cialiste des fusĂ©es et pour ĂȘtre honnĂȘte, oui, cela me prend un certain temps pour le lire, mais Ă  la fin, avec quelques essais et erreurs, je sais ce qu'ils essaient de programmer. Tout ce que je dis, c'est faire confiance Ă  Ian et Robert et aux autres, ils n'en ont pas encore fini avec le design. Ne serait pas surpris dans un an environ, il existe des outils qui aident le compilateur Ă  parler un langage de singe simple et parfait, quelle que soit la difficultĂ© du code gĂ©nĂ©rique de fusĂ©e que vous lui lancez. Le meilleur retour que vous puissiez donner est de rĂ©Ă©crire quelques exemples et de signaler si quelque chose est trop conçu afin qu'ils puissent s'assurer que le compilateur s'en plaindra ou sera automatiquement rĂ©Ă©crit par quelque chose comme l'outil vĂ©tĂ©rinaire.

J'ai lu la FAQ concernant <> mais pour une personne stupide comme moi, comment est-il plus difficile pour l'analyseur de déterminer s'il s'agit d'un appel générique s'il ressemble à ceci v := F<T> plutÎt qu'à v := F(T) ? N'est-ce pas plus difficile avec les parenthÚses puisqu'il ne saura pas s'il s'agit d'un appel de fonction avec T comme argument régulier ?

En plus de cela, je pense que l'analyseur doit bien sûr rester rapide, mais n'oublions pas non plus ce qui est le plus facile à lire pour le programmeur et qui est tout aussi important pour l'OMI. Est-il plus facile de comprendre immédiatement ce que fait v := F(T) ? Ou est-ce v := F<T> est plus facile ? Important aussi à prendre en considération :)

Ne pas argumenter pour ni contre v := F<T> , juste soulever quelques rĂ©flexions qui pourraient valoir la peine d'ĂȘtre prises en compte.

C'est légal Go aujourd'hui :

    f, c, d, e := 1, 2, 3, 4
    a, b := f < c, d > (e)
    fmt.Println(a, b) // true false

Il est inutile de discuter des crochets angulaires à moins que vous ne fournissiez une proposition sur ce qu'il faut faire à ce sujet (casser la compatibilité ?). C'est à toutes fins utiles une question morte. Il n'y a effectivement aucune chance que les équerres soient adoptées par l'équipe Go. S'il vous plaßt discuter de toute autre chose.

Modifier pour ajouter : Désolé si ce commentaire était trop sec. Il y a beaucoup de discussions sur les crochets angulaires sur Reddit et HN, ce qui est trÚs frustrant pour moi car le problÚme de rétrocompatibilité est bien connu depuis longtemps des personnes qui se soucient des génériques. Je comprends pourquoi les gens préfÚrent les crochets, mais ce n'est pas possible sans un changement radical.

Merci pour votre commentaire @iio7. Il y a toujours un risque non nul que les choses dégénÚrent. C'est pourquoi nous avons fait preuve de la plus grande prudence en cours de route. Je crois que ce que nous avons maintenant est une conception beaucoup plus propre et plus orthogonale que celle que nous avions l'année derniÚre ; et personnellement, j'espÚre que nous pourrons le rendre encore plus simple, en particulier en ce qui concerne les listes de types - mais nous le découvrirons au fur et à mesure que nous en apprendrons davantage. (Assez ironiquement, plus la conception devient orthogonale et propre, plus elle sera puissante et plus le code complexe que l'on pourra écrire.) Les derniers mots n'ont pas encore été prononcés. L'année derniÚre, lorsque nous avons eu le premier design potentiellement viable, la réaction de beaucoup de gens a été similaire à la vÎtre : "Voulons-nous vraiment cela ?" C'est une excellente question et nous devrions essayer d'y répondre aussi bien que possible.

L'observation de @gertcuykens est Ă©galement correcte - naturellement, les personnes jouant avec le prototype go2go explorent ses limites autant que possible (ce que nous voulons), mais dans le processus produisent Ă©galement du code qui ne passerait probablement pas le cap dans une production appropriĂ©e rĂ©glage. À ce jour, j'ai vu beaucoup de code gĂ©nĂ©rique qui est vraiment difficile Ă  dĂ©chiffrer.

Il y a des situations oĂč le code gĂ©nĂ©rique serait clairement une victoire ; Je pense Ă  des algorithmes concurrents gĂ©nĂ©riques qui nous permettraient de mettre du code un peu subtil dans une bibliothĂšque. Il existe bien sĂ»r diverses structures de donnĂ©es de conteneur, et des choses comme le tri, etc. Probablement une grande majoritĂ© de code n'a pas du tout besoin de gĂ©nĂ©riques. Contrairement Ă  d'autres langages, oĂč les fonctionnalitĂ©s gĂ©nĂ©riques sont au cƓur de tout ce que l'on fait dans le langage, dans Go, les fonctionnalitĂ©s gĂ©nĂ©riques ne sont qu'un autre outil de l'ensemble d'outils Go ; pas le bloc de construction fondamental sur lequel tout le reste est construit.

À titre de comparaison : au dĂ©but du Go, nous avions tous tendance Ă  abuser des goroutines et des canaux. Il a fallu un certain temps pour savoir quand ils Ă©taient appropriĂ©s et quand non. Maintenant, nous avons des lignes directrices plus ou moins Ă©tablies et nous ne les utilisons que lorsqu'elles sont vraiment appropriĂ©es. J'espĂšre que la mĂȘme chose se produirait si nous avions des gĂ©nĂ©riques.

Merci.

Dans la section du projet de conception sur les syntaxes basées sur [T] :

Le langage autorise gĂ©nĂ©ralement une virgule de fin dans une liste sĂ©parĂ©e par des virgules, donc A[T,] devrait ĂȘtre autorisĂ© si A est un type gĂ©nĂ©rique, mais ne serait normalement pas autorisĂ© pour une expression d'index. Cependant, l'analyseur ne peut pas savoir si A est un type gĂ©nĂ©rique ou une valeur de type slice, array ou map, donc cette erreur d'analyse ne peut pas ĂȘtre signalĂ©e tant que la vĂ©rification de type n'est pas terminĂ©e. Encore une fois, rĂ©soluble mais compliquĂ©.

Cela ne pourrait-il pas ĂȘtre rĂ©solu assez facilement en rendant la virgule de fin complĂštement lĂ©gale dans les expressions d'index, puis en la supprimant simplement par gofmt ?

@DeedleFake Peut-ĂȘtre. Ce serait certainement une solution de facilitĂ©; mais cela semble aussi un peu moche, syntaxiquement. Je ne me souviens pas de tous les dĂ©tails, mais une version antĂ©rieure prenait en charge les paramĂštres de type de style [type T]. Voir la branche dev.go2go, valider 3d4810b5ba oĂč le support a Ă©tĂ© supprimĂ©. On pourrait dĂ©terrer cela Ă  nouveau et enquĂȘter.

La longueur des arguments gĂ©nĂ©riques dans chaque liste [] peut-elle ĂȘtre limitĂ©e Ă  un maximum pour Ă©viter ce problĂšme, tout comme les types gĂ©nĂ©riques intĂ©grĂ©s :

  • [NT
  • []T
  • carte[K]T
  • Chan T

Veuillez noter que les derniers arguments des types génériques intégrés ne sont pas tous inclus dans [] .
La syntaxe de déclaration générique est la suivante : https://github.com/dotaheor/unify-Go-builtin-and-custom-generics#the -generic-declaration-syntax

@dotaheor Je ne sais pas exactement ce que vous demandez, mais il est clairement nécessaire de prendre en charge plusieurs arguments de type pour un type générique. Par exemple, https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#containers .

@ianlancetaylor
Ce que je veux dire, c'est que chaque paramĂštre de type est entourĂ© d'un [] , donc le type dans votre lien peut ĂȘtre dĂ©clarĂ© comme suit :

type Map[type K][type V] struct

Quand il est utilisé, c'est comme:

var m Map[string]int

Un argument de type non entouré de [] indique la fin de l'utilisation d'un type générique.

En pensant à la commande de tableaux # 39355 en conjonction avec des génériques, j'ai trouvé que "comparable" est traité de maniÚre spéciale dans le projet de génériques actuel (probablement en raison de l'impossibilité de répertorier facilement tous les types comparables dans une liste de types) en tant que contrainte de type prédéclarée .

Ce serait bien si le brouillon des gĂ©nĂ©riques Ă©tait modifiĂ© pour dĂ©finir Ă©galement "commandĂ©"/"commandable" de la mĂȘme maniĂšre que "comparable" est prĂ©dĂ©fini. C'est une relation connexe couramment utilisĂ©e sur les valeurs du mĂȘme type et cela permettrait aux futures extensions du langage go de dĂ©finir l'ordre sur plus de types (tableaux, structures, tranches, types de somme, Ă©numĂ©rations vĂ©rifiĂ©es, ...) sans se heurter Ă  la complication que tous les types ordonnĂ©s ne seraient pas listables dans une liste de types comme "comparable".

Je ne suggĂšre pas que pour ĂȘtre dĂ©cidĂ©, il devrait ĂȘtre commandĂ© pour plus de types dans la spĂ©cification de langage, mais ce changement de gĂ©nĂ©riques le laisse plus compatible avec un tel changement (une contrainte. Le code commandĂ© ne devrait pas ĂȘtre une chose gĂ©nĂ©rĂ©e par un compilateur magique plus tard ou serait obsolĂšte si vous utilisiez une liste de types). Le tri des packages pourrait commencer par la contrainte de type prĂ©dĂ©clarĂ©e "ordonnĂ©e" et plus tard pourrait "juste" fonctionner avec, par exemple, des tableaux si jamais changĂ©s et aucune correction de la contrainte utilisĂ©e.

@martisch Je pense que cela ne devrait se produire qu'une fois les types commandĂ©s Ă©tendus. Actuellement, constraints.Ordered pourrait rĂ©pertorier tous les types (cela ne fonctionne pas pour comparable , car les pointeurs, les structures, les tableaux,
 sont comparables, donc cela doit ĂȘtre magique. Mais ordered est actuellement limitĂ© Ă  un ensemble fini de types sous-jacents intĂ©grĂ©s) et les utilisateurs peuvent s'y fier. Si nous Ă©tendons les commandes aux tableaux (par exemple), nous pouvons toujours ajouter une nouvelle contrainte magique ordered et l'intĂ©grer dans constraints.Ordered . Cela signifie que tous les utilisateurs de constraints.Ordered bĂ©nĂ©ficieraient automatiquement de la nouvelle contrainte. Bien sĂ»r, les utilisateurs qui Ă©crivent leur propre liste de types explicite n'en bĂ©nĂ©ficieraient pas - mais c'est la mĂȘme chose si nous ajoutons ordered maintenant, pour les utilisateurs qui n'intĂšgrent pas cela .

Donc, à mon avis, il n'y a rien de perdu à retarder cela jusqu'à ce que ce soit réellement significatif. Nous ne devrions ajouter aucun jeu de contraintes possible en tant qu'identifiant prédéclaré - et encore moins tout futur jeu de contraintes potentiel :)

Si nous étendons les commandes aux tableaux (par exemple), nous pouvons toujours ajouter une nouvelle contrainte magique ordered et l'intégrer dans constraints.Ordered .

@Merovius C'est un bon point auquel je n'avais pas pensé. Cela permet d'étendre constraints.Ordered dans le futur de maniÚre cohérente. S'il y aura également un constraints.Comparable , il s'intégrera parfaitement dans la structure globale.

@martisch , notez que ordered - contrairement à comparable - n'est pas cohérent en tant que type d'interface à moins que nous ne définissions également un ordre total (global) parmi les types concrets, ou interdisions au code non générique d'utiliser < sur les variables de type ordered , ou interdire l'utilisation de comparable comme type d'interface d'exécution général.

Sinon, la transitivité des "outils" s'effondre. Considérez ce fragment de programme :

    var x constraints.Ordered = int(0)
    var y constraints.Ordered = string("0")
    fmt.Println(x < y)

Que doit-il produire ? (La réponse est-elle intuitive ou arbitraire ?)

@bcmills
Qu'en est-il de fun (<)(type T Ordered)(t1 T,t2 T) Bool?

Pour comparer des types arithmétiques de nature différente :

Si une arithmétique S n'implémente que Ordered(T) pour S<:T , alors :

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

devrait ĂȘtre unique.

Pour le polymorphisme d'exécution, vous auriez besoin que Ordered soit paramétrable.
Ou:
Vous partitionnez Ordered en types de tuples, puis rĂ©Ă©crivez (<) pour ĂȘtre :

//but isn't supported that either
fun(<)(type R Ordered)(s R.0,t R.1)

Salut!
J'ai une question.

Existe-t-il un moyen de créer une contrainte de type qui ne transmet que des types génériques avec un paramÚtre de type?
Quelque chose qui passe seulement Result(T) / Option(T) /etc mais pas seulement T .
J'ai essayé

type Box(type T) interface {
    Val() (T, bool)
}

mais cela nécessite la méthode Val()

type Box(type T) interface{}

est similaire Ă  interface{} , c'est-Ă -dire Any

également essayé https://go2goplay.golang.org/p/lkbTI7yppmh -> la compilation échoue

type Box(type T) interface {
       type Box(T)
}

https://go2goplay.golang.org/p/5NsKWNa3E1k -> la compilation Ă©choue

type Box(type T) interface{}

type Generic(type T) interface {
    type Box(T)
}

https://go2goplay.golang.org/p/CKzE2J-YOpD -> ne fonctionne pas

type Box(type T) interface{}

type Generic(type T Box(T)) interface {}

Ce comportement est-il attendu ou s'agit-il simplement d'un bogue de vérification de type ?

@tdakkota Les contraintes s'appliquent aux arguments de type, et elles s'appliquent à la forme entiÚrement instanciée des arguments de type. Il n'y a aucun moyen d'écrire une contrainte de type qui impose des exigences sur la forme non instanciée d'un argument de type.

Veuillez consulter la FAQ incluse avec le brouillon mis Ă  jour

Pourquoi ne pas utiliser la syntaxe Fcomme C++ et Java ?
Lors de l'analyse de code dans une fonction, telle que v := F, au moment de voir le <, il est ambigu de savoir si nous voyons une instanciation de type ou une expression utilisant l'opérateur <. Résoudre cela nécessite une anticipation effectivement illimitée. En général, nous nous efforçons de garder l'analyseur Go efficace.

@TotallyGamerJet Peu importe !

Comment traiter la valeur zéro de type générique ? Sans enum, comment pouvons-nous gérer la valeur facultative.
Par exemple : la version générique de vector et une fonction nommée First renvoient le premier élément si sa longueur > 0 sinon la valeur zéro du type générique.
Comment Ă©crit-on un tel code ? Parce que nous ne savons pas quel type de vecteur, si chan/slice/map , nous pouvons return (nil, false) , ĂȘtre si struct ou primitive type comme string , int , bool , comment y faire face ?

@leaxoy

var zero T devrait suffire

@leaxoy

var zero T devrait suffire

Une variable magique globale comme nil ?

@leaxoy
var zero T devrait suffire

Une variable magique globale comme nil ?

Une proposition est en cours de discussion pour ce sujet - voir la proposition : Go 2 : valeur zéro universelle avec inférence de type #35966 .

Il examine plusieurs nouvelles syntaxes alternatives pour une expression (pas une instruction comme var zero T ) qui retournera toujours la valeur zéro d'un type.

La valeur zéro semble faisable actuellement, mais peut-elle prendre de la place sur la pile ou le tas ? Devrions-nous envisager d'utiliser enum Option pour terminer cela en une seule étape.
Sinon, si la valeur zéro ne prend pas d'espace, ce serait mieux et il n'est pas nécessaire d'ajouter une énumération.

La valeur zéro semble faisable actuellement, mais peut-elle prendre de la place sur la pile ou le tas ?

Historiquement, je crois, le compilateur Go a optimisé ce genre de cas. Je ne suis pas trop inquiet.

Une valeur de type par dĂ©faut peut ĂȘtre spĂ©cifiĂ©e dans les modĂšles C++. Une construction similaire a-t-elle Ă©tĂ© envisagĂ©e pour les paramĂštres de type gĂ©nĂ©rique ? Potentiellement, cela permettrait de moderniser les types existants sans casser le code existant.

Par exemple, considĂ©rons le type asn1.ObjectIdentifier existant qui est un []int . Un problĂšme avec ce type est qu'il n'est pas conforme Ă  la spĂ©cification ASN.1, qui stipule que chaque sous-oid peut ĂȘtre un INTEGER de longueur arbitraire (par exemple *big.Int ). Potentiellement, ObjectIdentifier pourrait ĂȘtre modifiĂ© pour accepter un paramĂštre gĂ©nĂ©rique, mais cela casserait beaucoup de code existant. S'il y avait un moyen de spĂ©cifier int est la valeur de paramĂštre par dĂ©faut, cela permettrait peut-ĂȘtre de moderniser le code existant.

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}

Juste pour ĂȘtre clair, le asn1.ObjectIdentifier ci-dessus n'est qu'un exemple. Je ne dis pas que l'utilisation de gĂ©nĂ©riques est le seul moyen ou le meilleur moyen de rĂ©soudre le problĂšme de conformitĂ© ASN.1.

De plus, est-il prévu d'autoriser des limites d'interface finies paramétrables ? :

type Ordable(type T, S) interface {
    type S, type T
}

Comment prendre en charge la condition where sur le paramĂštre de type.
Peut-on Ă©crire un tel code :

type Vector(type T) struct {
    vec []T
}

func (v Vector(T)) Sum() T where T: Summable {
      //
}

func (v Vector(T)) First()  (T, bool) {
     //
}

La méthode Sum ne fonctionne que lorsque les paramÚtres de type T sont Summable , sinon nous ne pouvons pas appeler Sum sur Vector.

Salut @leaxoy

Vous pouvez simplement Ă©crire quelque chose comme https://go2goplay.golang.org/p/pRznN30Qu8V

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
}

Je pense que la clause where ne ressemble pas Ă  Go et serait difficile Ă  analyser, elle devrait ĂȘtre quelque chose comme

type Vector(type T) struct {
    vec []T
}

func (v Vector(T Summable)) Sum() T {
      //
}

func (v Vector(T)) First()  (T, bool) {
     //
}

mais cela ressemble à une spécialisation de méthode.

@sebastien-rosset Nous n'avons pas considéré les types par défaut pour les paramÚtres de type générique. Le langage n'a pas de valeurs par défaut pour les arguments de fonction, et il n'est pas évident de comprendre pourquoi les génériques seraient différents. A mon avis, la possibilité de rendre du code existant compatible avec un package qui ajoute des génériques n'est pas une priorité. Si un paquet est réécrit pour utiliser des génériques, il est normal d'exiger que le code existant change, ou d'introduire simplement le code générique en utilisant de nouveaux noms.

@sighoya

De plus, est-il prévu d'autoriser des limites d'interface finie paramétrables ?

Je suis désolé, je ne comprends pas la question.

J'aimerais rappeler aux gens que le billet de blog (https://blog.golang.org/generics-next-step) suggÚre que la discussion sur les génériques ait lieu sur la liste de diffusion golang-nuts, et non sur le suivi des problÚmes. Je continuerai à lire ce numéro, mais il contient prÚs de 800 commentaires et est complÚtement encombrant, en plus des autres difficultés du suivi des problÚmes, telles que l'absence de fil de commentaires. Merci.

Retour d'information : j'ai écouté le podcast Go Time le plus récent, et je dois dire que l'explication de @griesemer sur le problÚme des chevrons était la premiÚre fois que je l' ai vraiment compris, c'est-à-dire que signifie réellement "anticipation illimitée sur l'analyseur" pour Go ? Merci beaucoup pour les détails supplémentaires.

Aussi, je suis en faveur des crochets. 😄

@ianlancetaylor

le billet de blog suggÚre que la discussion sur les génériques ait lieu sur la liste de diffusion golang-nuts, et non sur le suivi des problÚmes

Dans un rĂ©cent article de blog [1], @ddevault souligne que Google Group (oĂč se trouve cette liste de diffusion) nĂ©cessite un compte Google. Vous en avez besoin d'un pour publier, et apparemment certains groupes ont mĂȘme besoin d'un compte pour lire. J'ai un compte Google, donc ce n'est pas un problĂšme pour moi (et je ne dis pas non plus que je suis d'accord avec tout dans ce billet de blog), mais je suis d'accord que si nous voulons avoir une communautĂ© golang plus juste, et si nous voulons Ă©viter une chambre d'Ă©cho, qu'il vaudrait peut-ĂȘtre mieux ne pas avoir ce genre d'exigence.

Je ne savais pas cela à propos des groupes Google, et s'il y a une exception pour les golang-nuts, veuillez accepter mes excuses et ignorer cela. Pour ce que ça vaut, j'ai beaucoup appris en lisant ce fil, et j'ai aussi été assez convaincu (aprÚs avoir utilisé golang pendant plus de six ans) que les génériques sont la mauvaise approche pour le langage. Juste mon opinion personnelle cependant, et merci de nous avoir apporté la langue que j'aime beaucoup !

Acclamations!

[1] https://drewdevault.com/2020/08/01/pkg-go-dev-sucks.html

@purpleidea N'importe quel groupe Google peut ĂȘtre utilisĂ© comme liste de diffusion. Vous pouvez rejoindre et participer sans avoir de compte Google.

@ianlancetaylor

N'importe quel groupe Google peut ĂȘtre utilisĂ© comme liste de diffusion. Vous pouvez rejoindre et participer sans avoir de compte Google.

Quand je vais Ă :

https://groups.google.com/forum/#!forum/golang-nuts

dans une fenĂȘtre de navigateur privĂ©e (pour masquer mon compte google auquel je suis connectĂ©), et cliquez sur "nouveau sujet", cela me redirige vers une page de connexion google. Comment l'utiliser sans compte Google ?

@purpleidea En Ă©crivant un e-mail Ă  [email protected] . C'est une liste de diffusion. Seule l'interface Web nĂ©cessite un compte Google. Ce qui semble juste - Ă©tant donnĂ© qu'il s'agit d'une liste de diffusion, vous avez besoin d'une adresse e-mail et les groupes ne peuvent Ă©videmment envoyer des e-mails qu'Ă  partir d'un compte gmail.

Je pense que la plupart des gens ne comprennent pas ce qu'est une liste de diffusion.

Quoi qu'il en soit, vous pouvez Ă©galement utiliser n'importe quel miroir de liste de diffusion publique, par exemple https://www.mail-archive.com/[email protected]/

C'est trĂšs bien, mais cela ne facilite pas la tĂąche lorsque les gens se connectent Ă 
fils de discussion sur Google Groupes (ce qui arrive fréquemment). C'est incroyablement
irritant d'essayer de trouver un message de l'ID dans une URL.

— Sam

Le dimanche 2 août 2020 à 19:24, Ahmed W. a écrit :
>
>

Je pense que la plupart des gens ne comprennent pas ce qu'est une liste de diffusion.

Quoi qu'il en soit, vous pouvez Ă©galement utiliser n'importe quel miroir de liste de diffusion publique, par exemple
https://www.mail-archive.com/[email protected]/

— Vous recevez ceci parce que vous ĂȘtes abonnĂ© Ă  ce fil.
RĂ©pondez directement Ă  cet e-mail, consultez-le sur GitHub
https://github.com/golang/go/issues/15292#issuecomment-667738419 , ou
Se désabonner
https://github.com/notifications/unsubscribe-auth/AAD5EPNQTEUF5SPT6GMM4JLR6XYUBANCNFSM4CA35RXQ
.

--
Sam blanc

Ce n'est pas vraiment le lieu pour avoir cette discussion.

Des mises Ă  jour Ă  ce sujet ? đŸ€”

@Imperatorn il y en a eu, ils n'ont tout simplement pas été discutés ici. Il a été décidé que les crochets [ ] seraient la syntaxe choisie et que le mot "type" ne serait pas requis lors de l'écriture de types/fonctions génériques. Il existe également un nouvel alias "any" pour l'interface vide.

La derniÚre ébauche de conception de génériques est ici .
Voir aussi ce commentaire concernant les discussions sur ce sujet. Merci.

J'aimerais rappeler aux gens que le billet de blog (https://blog.golang.org/generics-next-step) suggÚre que la discussion sur les génériques ait lieu sur la liste de diffusion golang-nuts, et non sur le suivi des problÚmes. Je continuerai à lire ce numéro, mais il contient prÚs de 800 commentaires et est complÚtement encombrant, en plus des autres difficultés du suivi des problÚmes, telles que l'absence de fil de commentaires. Merci.

À ce sujet, bien que je respecte le fait que l'Ă©quipe Go souhaite Ă©carter de telles discussions d'un problĂšme pour des raisons pratiques, il semble qu'il y ait beaucoup de membres de la communautĂ© sur GitHub qui ne sont pas sur golang-nuts. Je me demande si la nouvelle fonctionnalitĂ© Discussions de GitHub conviendrait ? đŸ€” Il a du filetage, apparemment.

@toolbox L'argument peut Ă©galement ĂȘtre avancĂ© dans l'autre sens - il y a des gens qui n'ont pas de compte github (et refusent d'en avoir un). Vous n'avez pas non plus besoin d'ĂȘtre abonnĂ© Ă  golang-nuts pour pouvoir y publier et y participer.

@Merovius L'une des fonctionnalités que j'aime vraiment dans les problÚmes GitHub est que je peux m'abonner aux notifications uniquement pour les problÚmes qui m'intéressent. Je ne sais pas comment faire cela avec Google Groupes ?

Je suis sĂ»r qu'il y a de bonnes raisons de prĂ©fĂ©rer l'un ou l'autre. Il peut certainement y avoir une discussion sur ce que devrait ĂȘtre le forum prĂ©fĂ©rĂ©. Cependant, encore une fois, je ne pense pas que cette discussion devrait ĂȘtre ici. Ce problĂšme est assez bruyant comme ça.

@toolbox L'argument peut Ă©galement ĂȘtre avancĂ© dans l'autre sens - il y a des gens qui n'ont pas de compte github (et refusent d'en avoir un). Vous n'avez pas non plus besoin d'ĂȘtre abonnĂ© Ă  golang-nuts pour pouvoir poster et participer lĂ -bas.

Je comprends ce que vous dites, et c'est vrai, mais vous manquez la cible. Je ne dis pas qu'il faut dire aux utilisateurs de golang-nuts d'aller sur GitHub (comme cela se passe maintenant Ă  l'envers), je dis que ce serait bien pour les utilisateurs de GitHub d'avoir un forum de discussion.

Je suis sĂ»r qu'il y a de bonnes raisons de prĂ©fĂ©rer l'un ou l'autre. Il peut certainement y avoir une discussion sur ce que devrait ĂȘtre le forum prĂ©fĂ©rĂ©. Cependant, encore une fois, je ne pense pas que cette discussion devrait ĂȘtre ici. Ce problĂšme est assez bruyant comme ça.

Je suis d'accord que c'est complÚtement hors sujet pour ce problÚme, et je m'excuse de l'avoir soulevé, mais j'espÚre que vous voyez l'ironie.

@keean @Merovius @toolbox et les gens Ă  l'avenir.

Pour votre information : il existe un problÚme ouvert pour ce type de discussion, voir # 37469.

Bonjour,

Tout d'abord, merci pour Go. La langue est absolument géniale. L'une des choses les plus étonnantes à propos de Go, pour moi, a été la lisibilité. Je suis nouveau dans la langue, donc j'en suis encore aux premiers stades de la découverte, mais jusqu'à présent, elle s'est avérée incroyablement claire, nette et précise.

Le seul commentaire que j'aimerais présenter est que depuis mon analyse initiale de la proposition de génériques, [T Constraint] n'est pas facile pour moi à analyser rapidement, du moins pas aussi facile qu'un jeu de caractÚres désigné pour les génériques . Je comprends que le style C++ F<T Constraint> n'est pas réalisable en raison de la nature du paradigme multi-retour de go. Tous les caractÚres non-ascii seraient une corvée absolue, donc je suis vraiment reconnaissant que vous ayez rejeté cette idée.

Veuillez envisager d'utiliser une combinaison de caractĂšres. Je ne sais pas si les opĂ©rations au niveau du bit pourraient ĂȘtre mal interprĂ©tĂ©es ou brouiller les pistes d'analyse, mais F<<T Constraint>> serait bien, Ă  mon avis. N'importe quelle combinaison de symboles suffirait cependant. Bien que cela puisse ajouter une taxe initiale sur le balayage oculaire, je pense que cela peut facilement ĂȘtre rĂ©solu avec des ligatures de police comme FireCoda et Iosevka . Il n'y a pas grand-chose Ă  faire pour distinguer clairement et facilement la diffĂ©rence entre Map[T Constraint] et map[string]T .

Je ne doute pas que les gens entraßneront leur esprit à faire la distinction entre les deux applications de [] en fonction du contexte. Je soupçonne juste que cela va accélérer la courbe d'apprentissage.

Merci pour la remarque. Ne pas manquer l'Ă©vidence, mais map[T1]T2 et Map[T1 Constraint] peuvent ĂȘtre distinguĂ©s car le premier n'a pas de contrainte et le second a une contrainte requise.

La syntaxe a Ă©tĂ© longuement discutĂ©e sur golang-nuts et je pense que c'est rĂ©glĂ©. Nous sommes heureux d'entendre des commentaires basĂ©s sur des donnĂ©es rĂ©elles telles que des ambiguĂŻtĂ©s d'analyse. Pour les commentaires basĂ©s sur des sentiments et des prĂ©fĂ©rences, je pense qu'il est temps d'ĂȘtre en dĂ©saccord et de s'engager.

Merci encore.

@ianlancetaylor Assez juste. Je suis sûr que vous en avez assez d'entendre des pinailleries dessus :) Pour ce que ça vaut, je voulais dire différencier facilement la numérisation.

Quoi qu'il en soit, j'ai hĂąte de l'utiliser. Merci.

Une alternative générique à reflect.MakeFunc serait un énorme gain de performances pour l'instrumentation Go. Mais je ne vois aucun moyen de décomposer un type de fonction avec la proposition actuelle.

@ Julio-Guerra Je ne suis pas sûr de ce que vous entendez par "décomposer un type de fonction". Vous pouvez, dans une certaine mesure, paramétrer les types d'arguments et de retour : 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))
}

Cela ne fonctionne que si le nombre des deux est constant.

@ Julio-Guerra Je ne suis pas sûr de ce que vous entendez par "décomposer un type de fonction". Vous pouvez, dans une certaine mesure, paramétrer les types d'arguments et de retour : https://go2goplay.golang.org/p/RwU11S4gC59

En effet, je fais rĂ©fĂ©rence Ă  ce que vous avez fait, mais gĂ©nĂ©ralisĂ© Ă  tout paramĂštre de fonction et liste de types de retour (de la mĂȘme maniĂšre que le tableau de paramĂštres et les types de retour de reflect.MakeFunc). Cela permettrait d'avoir des wrappers de fonction gĂ©nĂ©ralisĂ©s (au lieu d'utiliser la gĂ©nĂ©ration de code outillĂ©e).

Cette page vous a été utile?
0 / 5 - 0 notes