Go: all : prend en charge la réparation progressive du code lors du déplacement d'un type entre les packages

Créé le 1 déc. 2016  ·  225Commentaires  ·  Source: golang/go

Titre original : proposition : prendre en charge la réparation progressive du code lors du déplacement d'un type entre les packages

Go devrait ajouter la possibilité de créer des noms équivalents alternatifs pour les types, afin de permettre la réparation progressive du code lors de la refactorisation de la base de code. C'était la cible de la fonctionnalité d'alias Go 1.8, proposée dans #16339 mais retenue depuis Go 1.8. Parce que nous n'avons pas résolu le problème pour Go 1.8, cela reste un problème, et j'espère que nous pourrons le résoudre pour Go 1.9.

Lors de la discussion de la proposition d'alias, de nombreuses questions ont été posées sur l'importance de cette capacité à créer des noms alternatifs pour les types en particulier. Pour tenter de répondre à ces questions, j'ai écrit et publié un article intitulé « Codebase Refactoring (with help from Go) ». Veuillez lire cet article si vous avez des questions sur la motivation. (Pour un autre, la présentation plus courte, voir Robert de parler de foudre Gophercon Malheureusement, cette vidéo n'a pas été disponible en ligne jusqu'au 9 Octobre Mise à jour, décembre 16:. Voici mon exposé GothamGo , qui était essentiellement le premier projet de l'article.)

Ce problème ne propose _pas_ de solution spécifique. Au lieu de cela, je souhaite recueillir les commentaires de la communauté Go sur l'espace des solutions possibles. Une avenue possible est de limiter les alias aux types, comme mentionné à la fin de l'article. Il y en a peut-être d'autres que nous devrions également considérer.

Veuillez poster vos réflexions sur les alias de type ou d'autres solutions sous forme de commentaires ici.

Merci.

Mise à affichés .
Mise à jour, 9 janvier : Proposition acceptée, dépôt dev.typealias créé, implémentation prévue au début du cycle Go 1.9 pour expérimentation.


Résumé de la discussion (dernière mise à jour 2017-02-02)

Attendons-nous d'avoir besoin d'une solution générale qui fonctionne pour toutes les déclarations ?

Si les alias de type sont nécessaires à 100%, alors les alias var sont peut-être nécessaires à 10%, les alias func sont nécessaires à 1% et les alias const sont nécessaires à 0%. Étant donné que const a déjà = et que func pourrait également utiliser =, la question clé est de savoir si les alias var sont suffisamment importants pour être planifiés ou implémentés.

Comme le soutiennent @rogpeppe (https://github.com/golang/go/issues/16339#issuecomment-258771806) et @ianlancetaylor (https://github.com/golang/go/issues/16339#issuecomment-233644777) dans la proposition d'alias d'origine et comme mentionné dans l'article, une var globale mutante est généralement une erreur. Cela n'a probablement pas de sens de compliquer la solution pour s'adapter à ce qui est généralement un bogue. (En fait, si nous pouvons comprendre comment, cela ne me surprendrait pas si, à long terme, Go s'oriente vers l'exigence que les variables globales soient immuables.)

Étant donné que les alias var plus riches ne sont probablement pas assez importants pour être planifiés, il semble que le bon choix ici soit de se concentrer uniquement sur les alias de type. La plupart des commentaires ici semblent d'accord. Je ne vais pas énumérer tout le monde.

Avons-nous besoin d'une nouvelle syntaxe (= vs => vs export) ?

L'argument le plus fort pour une nouvelle syntaxe est la nécessité de prendre en charge les alias var, maintenant ou à l'avenir (https://github.com/golang/go/issues/18130#issuecomment-264232763 par @Merovius). Il semble normal de prévoir de ne pas avoir d'alias var (voir la section précédente).

Sans alias var, réutiliser = est plus simple que d'introduire une nouvelle syntaxe, que ce soit => comme dans la proposition d'alias, ~ (https://github.com/golang/go/issues/18130#issuecomment-264185142 par @joegrasse), ou exporter (https://github.com/golang/go/issues/18130#issuecomment-264152427 par @cznic).

L'utilisation de = in correspondrait également exactement à la syntaxe des alias de type en Pascal et Rust. Dans la mesure où d'autres langages ont les mêmes concepts, c'est bien d'utiliser la même syntaxe.

À l'avenir, il pourrait y avoir un futur Go dans lequel des alias de fonction existent également (voir https://github.com/golang/go/issues/18130#issuecomment-264324306 par @nigeltao), et alors toutes les déclarations autoriseraient la même forme :

const C2 = C1
func F2 = F1
type T2 = T1
var V2 = V1

Le seul d'entre eux qui n'établirait pas un véritable alias serait la déclaration var, car V2 et V1 peuvent être redéfinis indépendamment pendant l'exécution du programme (contrairement aux déclarations const, func et type qui sont immuables). Comme l'une des principales raisons des variables est de leur permettre de varier, cette exception serait au moins facile à expliquer. Si Go se dirige vers des variables globales immuables, alors même cette exception disparaîtra.

Pour être clair, je ne suggère pas ici d'alias de fonction ou de variables globales immuables, je travaille simplement sur les implications de tels ajouts futurs.

@jimmyfrasche a suggéré (https://github.com/golang/go/issues/18130#issuecomment-264278398) des alias pour tout sauf consts, de sorte que const serait l'exception au lieu de var :

const C2 = C1 // no => form
func F2 => F1
type T2 => T1
var V2 => V1
var V2 = V1 // different from => form

Avoir des incohérences avec const et var semble plus difficile à expliquer que d'avoir juste une incohérence pour var.

Cela peut-il être un changement d'outil ou de compilateur uniquement au lieu d'un changement de langue ?

Il vaut certainement la peine de se demander si la réparation progressive du code peut être activée uniquement grâce aux informations fournies au compilateur (par exemple, https://github.com/golang/go/issues/18130#issuecomment-264205929 par @btracey).

Ou peut-être si le compilateur peut appliquer une sorte de prétraitement basé sur des règles pour transformer les fichiers d'entrée avant la compilation (par exemple, https://github.com/golang/go/issues/18130#issuecomment-264329924 par @tux21b).

Malheureusement, non, le changement ne peut vraiment pas être confiné de cette façon. Il y a au moins deux compilateurs (gc et gccgo) qui devraient se coordonner, mais il en serait de même pour tout autre outil qui analyse des programmes, comme go vet, guru, goimports, gocode (achèvement de code) et autres.

Comme l'a dit @bcmills (https://github.com/golang/go/issues/18130#issuecomment-264275574), « un mécanisme de 'non-changement de langue' qui doit être pris en charge par toutes les implémentations est un changement de langue de facto — c'est juste un avec une documentation plus pauvre.

Quelles autres utilisations les alias pourraient-ils avoir ?

Nous savons ce qui suit. Étant donné que les alias de type en particulier ont été jugés suffisamment importants pour être inclus dans Pascal et Rust, il y en a probablement d'autres.

  1. Les alias (ou simplement les alias de type) permettraient de créer des remplacements instantanés qui étendent d'autres packages. Par exemple, consultez https://go-review.googlesource.com/#/c/32145/ , en particulier l'explication dans le message de validation.

  2. Les alias (ou simplement les alias de type) permettraient de structurer un package avec une petite surface d'API mais une grande implémentation en tant que collection de packages pour une meilleure structure interne tout en ne présentant qu'un seul package à importer et à utiliser par les clients. Il y a un exemple quelque peu abstrait décrit sur https://github.com/golang/go/issues/16339#issuecomment -232813695.

  3. Les tampons de protocole ont une fonctionnalité « import public » dont la sémantique est triviale à implémenter dans le code C++ généré mais impossible à implémenter dans le code Go généré. Cela provoque une frustration pour les auteurs de définitions de tampon de protocole partagées entre les clients C++ et Go. Les alias de type fourniraient à Go un moyen d'implémenter cette fonctionnalité. En fait, le cas d'utilisation d'origine pour l'importation publique était la réparation progressive du code . Des problèmes similaires peuvent survenir dans d'autres types de générateurs de code.

  4. Abréger les noms longs. Les alias locaux (non exportés ou sans portée de paquet) peuvent être utiles pour abréger un nom de type long sans introduire la surcharge d'un tout nouveau type. Comme pour toutes ces utilisations, la clarté du code final influencerait fortement s'il s'agit d'une utilisation suggérée.

Quels autres problèmes une proposition d'alias de type doit-elle résoudre ?

Les énumérer pour référence. Nous n'essayons pas de les résoudre ou de les discuter dans cette section, bien que quelques-unes aient été abordées plus tard et soient résumées dans des sections distinctes ci-dessous.

  1. Manipulation dans godoc. (https://github.com/golang/go/issues/18130#issuecomment-264323137 par @nigeltao et https://github.com/golang/go/issues/18130#issuecomment-264326437 par @jimmyfrasche)

  2. Les méthodes peuvent-elles être définies sur des types nommés par alias ? (https://github.com/golang/go/issues/18130#issuecomment-265077877 par @ulikunitz)

  3. Si les alias à alias sont autorisés, comment gérons-nous les cycles d'alias ? (https://github.com/golang/go/issues/18130#issuecomment-264494658 par @thwd)

  4. Les alias doivent-ils pouvoir exporter des identifiants non exportés ? (https://github.com/golang/go/issues/18130#issuecomment-264494658 par @thwd)

  5. Que se passe-t-il lorsque vous intégrez un alias (comment accédez-vous au champ intégré) ? (https://github.com/golang/go/issues/18130#issuecomment-264494658 par @thwd , également #17746)

  6. Les alias sont-ils disponibles sous forme de symboles dans le programme construit ? (https://github.com/golang/go/issues/18130#issuecomment-264494658 par @thwd)

  7. Injection de chaîne Ldflags : et si on se référait à un alias ? (https://github.com/golang/go/issues/18130#issuecomment-264494658 par @thwd ; cela ne se produit que s'il existe des alias var.)

Le versioning est-il une solution en soi ?

"Dans ce cas, la gestion des versions est peut-être la réponse complète, pas les alias de type."
(https://github.com/golang/go/issues/18130#issuecomment-264573088 par @iainmerrick)

Comme indiqué dans l'article , je pense que le versioning est une préoccupation complémentaire. La prise en charge de la réparation progressive du code, comme avec les alias de type, donne à un système de gestion des versions plus de flexibilité dans la façon dont il construit un programme volumineux, ce qui peut faire la différence entre être capable de construire le programme et non.

Le problème de refactorisation plus large peut-il être résolu à la place ?

Dans https://github.com/golang/go/issues/18130#issuecomment -265052639, @niemeyer souligne qu'il y a eu en fait deux changements pour déplacer os.Error to error : le nom a changé mais la définition aussi (le La méthode d'erreur était une méthode String).

@niemeyer suggère que nous pouvons peut-être trouver une solution au problème de refactorisation plus large qui corrige les types se déplaçant entre les packages comme un cas particulier, mais gère également des choses comme le changement de nom de méthode, et il propose une solution construite autour des "adaptateurs".

Il y a pas mal de discussions dans les commentaires que je ne peux pas facilement résumer ici. La discussion n'est pas terminée, mais jusqu'à présent, il n'est pas clair si les "adaptateurs" peuvent s'intégrer dans le langage ou être mis en œuvre dans la pratique. Il semble clair que les adaptateurs sont au moins un ordre de grandeur plus complexes que les alias de type.

Les adaptateurs ont également besoin d'une solution cohérente aux problèmes de sous-typage mentionnés ci-dessous.

Les méthodes peuvent-elles être déclarées sur des types d'alias ?

Certes, les alias ne permettent pas de contourner les restrictions habituelles de définition de méthode : si un package définit le type T1 = otherpkg.T2, il ne peut pas définir de méthodes sur T1, tout comme il ne peut pas définir de méthodes directement sur otherpkg.T2. C'est-à-dire que si type T1 = otherpkg.T2, alors func (T1) M() est équivalent à func (otherpkg.T2) M(), qui est invalide aujourd'hui et reste invalide. Cependant, si un package définit le type T1 = T2 (les deux dans le même package), la réponse est moins claire. Dans ce cas, func (T1) M() serait équivalent à func (T2) M(); puisque ce dernier est autorisé, il existe un argument pour autoriser le premier. La documentation de conception actuelle n'impose pas de restriction ici (en accord avec l'évitement général des restrictions), de sorte que func (T1) M() est valide dans cette situation.

Dans https://github.com/golang/go/issues/18130#issuecomment -267694112, @jimmyfrasche suggère qu'à la place, définir "aucune utilisation d'alias dans les définitions de méthode" serait une règle claire et éviterait d'avoir besoin de savoir ce que T est défini pour savoir si func (T) M() est valide. Dans https://github.com/golang/go/issues/18130#issuecomment -267997124, @rsc souligne qu'aujourd'hui encore il y a certains T pour lesquels func (T) M() n'est pas valide : https://play .golang.org/p/bci2qnldej. En pratique, cela ne se produit pas parce que les gens écrivent du code raisonnable.

Nous garderons cette éventuelle restriction à l'esprit, mais attendrons qu'il y ait des preuves solides qu'elle est nécessaire avant de l'introduire.

Existe-t-il un moyen plus simple de gérer l'intégration et, plus généralement, les renommages de champs ?

Dans https://github.com/golang/go/issues/18130#issuecomment -267691816, @Merovius souligne qu'un type intégré qui change de nom lors d'un déplacement de package posera des problèmes lorsque ce nouveau nom devra finalement être adopté au utiliser des sites. Par exemple, si le type d'utilisateur U a un io.ByteBuffer intégré qui passe à bytes.Buffer, alors que U intègre io.ByteBuffer, le nom du champ est U.ByteBuffer, mais lorsque U est mis à jour pour faire référence à bytes.Buffer, le nom du champ est nécessairement les modifications apportées à U.Buffer.

Dans https://github.com/golang/go/issues/18130#issuecomment -267710478, @neild souligne qu'il existe au moins une solution de contournement si les références à io.ByteBuffer doivent être excisées : le package P qui définit U peut également définissez 'type ByteBuffer = bytes.Buffer' et intégrez ce type dans U. Ensuite, U a toujours un U.ByteBuffer, même après la disparition complète de io.ByteBuffer.

Dans https://github.com/golang/go/issues/18130#issuecomment -267703067, @bcmills suggère l'idée d'alias de champ, pour permettre à un champ d'avoir plusieurs noms lors d'une réparation progressive. Les alias de champ permettraient de définir quelque chose comme type U struct { bytes.Buffer; ByteBuffer = Buffer } au lieu d'avoir à créer l'alias de type de niveau supérieur.

Dans https://github.com/golang/go/issues/18130#issuecomment -268001111, @rsc soulève encore une autre possibilité : une syntaxe pour « intégrer ce type avec ce nom », afin qu'il soit possible d'intégrer un octet. Buffer comme nom de champ ByteBuffer, sans avoir besoin d'un type de niveau supérieur ou d'un autre nom. Si cela existait, alors le nom du type pourrait être mis à jour de io.ByteBuffer à bytes.Buffer tout en préservant le nom d'origine (et sans introduire un second, ni un type exporté maladroit).

Tout cela semble valoir la peine d'être exploré une fois que nous aurons plus de preuves de refactorisations à grande échelle bloquées par des problèmes de changement de nom des champs. Comme @rsc l'a écrit, "Si les alias de type nous aident à arriver au point où le manque d'alias de champ est le prochain grand obstacle pour les refactorisations à grande échelle, ce sera un progrès!"

Il a été suggéré de restreindre l'utilisation d'alias dans les champs intégrés ou de modifier le nom intégré pour utiliser le nom du type cible, mais ceux-ci font que l'introduction d'alias brise les définitions existantes qui doivent ensuite être corrigées de manière atomique, empêchant essentiellement toute réparation progressive. @rsc : "Nous en avons discuté assez longuement dans #17746. J'étais à l'origine du côté du nom d'un alias io.ByteBuffer intégré étant Buffer, mais l'argument ci-dessus m'a convaincu que j'avais tort. @jimmyfrasche en particulier a fait du bien arguments sur le code ne changeant pas en fonction de la définition de la chose intégrée. Je ne pense pas qu'il soit tenable d'interdire complètement les alias intégrés. "

Quel est l'effet sur les programmes utilisant la réflexion ?

Les programmes utilisant la réflexion voient à travers les alias. Dans https://github.com/golang/go/issues/18130#issuecomment -267903649, @atdiar souligne que si un programme utilise la réflexion pour, par exemple, trouver le package dans lequel un type est défini ou même le nom d'un type, il observera le changement lorsque le type est déplacé, même si un alias de transfert est laissé de côté. Dans https://github.com/golang/go/issues/18130#issuecomment -268001410, @rsc l'a confirmé et a écrit "Comme la situation avec l'intégration, ce n'est pas parfait. Contrairement à la situation avec l'intégration, je n'en ai pas réponses, sauf peut-être que le code ne devrait pas être écrit en utilisant reflect pour être aussi sensible à ces détails."

L'utilisation de packages vendus aujourd'hui modifie également les chemins d'importation de packages vus par reflect, et nous n'avons pas été informés des problèmes importants causés par cette ambiguïté. Cela suggère que les programmes n'inspectent généralement pas reflect.Type.PkgPath d'une manière qui serait cassée par l'utilisation d'alias. Même ainsi, c'est un écart potentiel, tout comme l'intégration.

Quel est l'effet sur la compilation séparée des programmes et des plugins ?

Dans https://github.com/golang/go/issues/18130#issuecomment -268524504, @atdiar pose la question de l'effet sur les fichiers objets et la compilation séparée. Dans https://github.com/golang/go/issues/18130#issuecomment -268560180, @rsc répond qu'il ne devrait pas être nécessaire d'apporter des modifications ici : si X importe Y et Y change et est recompilé, alors X doit être recompilé aussi. C'est vrai aujourd'hui sans alias, et cela le restera avec les alias. Une compilation séparée signifie être capable de compiler X et Y en étapes distinctes (le compilateur n'a pas à les traiter dans la même invocation), non pas qu'il soit possible de changer Y sans recompiler X.

Les types de somme ou une sorte de sous-typage seraient-ils une solution alternative ?

Dans https://github.com/golang/go/issues/18130#issuecomment -264413439, @iand suggère des "types substituables", "une liste de types qui peuvent être substitués au type nommé dans les arguments de fonction, les valeurs de retour, etc. ". Dans https://github.com/golang/go/issues/18130#issuecomment -268072274, @j7b suggère d'utiliser des types algébriques "ainsi, nous obtenons également une interface vide équivalente avec la vérification du type au moment de la compilation en bonus". D'autres noms pour ce concept sont les types de somme et les types de variante.

En général, cela ne suffit pas pour permettre le déplacement de types avec une réparation progressive du code. Il y a deux façons de penser à cela.

Dans https://github.com/golang/go/issues/18130#issuecomment -268075680, @bcmills prend la voie concrète, soulignant que les types algébriques ont une représentation différente de l'originale, ce qui ne permet pas de traiter la somme et l'original comme interchangeable : ce dernier a des étiquettes de type.

Dans https://github.com/golang/go/issues/18130#issuecomment -268585497, @rsc prend la voie théorique, en développant https://github.com/golang/go/issues/18130#issuecomment -265211655 par @gri soulignant que dans une réparation de code progressive, vous avez parfois besoin que T1 soit un sous-type de T2 et parfois vice versa. La seule façon pour les deux d'être des sous-types l'un de l'autre est qu'ils soient du même type, ce qui n'est pas par hasard ce que font les alias de type.

En tant que tangente latérale, en plus de ne pas résoudre le problème de réparation progressive du code, les types algébriques / types somme / types union / types variants sont en eux-mêmes difficiles à ajouter à Go. Voir
la réponse à la FAQ et la discussion Go 1.6 AMA pour en savoir plus.

Dans https://github.com/golang/go/issues/18130#issuecomment -265206780, @thwd suggère que puisque Go a une relation de sous-typage entre les types concrets et les interfaces (bytes.Buffer peut être considéré comme un sous-type de io.Reader ) et entre les interfaces (io.ReadWriter est un sous-type de io.Reader de la même manière), rendre les interfaces "récursivement covariantes (selon les règles de variance actuelles) jusqu'à leurs arguments de méthode" résoudrait le problème à condition que tous les futurs packages ne utilisez des interfaces, jamais des types concrets comme des structs ("encourage aussi une bonne conception").

Il y a trois problèmes avec cela comme solution. Premièrement, il présente les problèmes de sous-typage ci-dessus, il ne résout donc pas la réparation progressive du code. Deuxièmement, cela ne s'applique pas au code existant, comme @thwd l'a noté dans cette suggestion. Troisièmement, forcer l'utilisation d'interfaces partout peut ne pas être une bonne conception et introduire des frais généraux de performances (voir par exemple https://github.com/golang/go/issues/18130#issuecomment-265211726 par @Merovius et https://github .com/golang/go/issues/18130#issuecomment-265224652 par @zombiezen).

Restrictions

Cette section rassemble les restrictions proposées pour référence, mais gardez à l'esprit que les restrictions ajoutent de la complexité. Comme je l'ai écrit dans https://github.com/golang/go/issues/18130#issuecomment -264195616, "nous ne devrions probablement mettre en œuvre ces restrictions qu'après une expérience réelle avec la conception plus simple et sans restriction nous aide à comprendre si la restriction apporterait suffisamment avantages pour en payer le prix."

En d'autres termes, toute restriction devrait être justifiée par la preuve qu'elle empêcherait une utilisation abusive ou une confusion grave. Comme nous n'avons pas encore mis en œuvre de solution, il n'y a pas de preuve de ce genre. Si l'expérience a fourni cette preuve, cela vaudra la peine d'y revenir.

Restriction? Les alias des types de bibliothèque standard ne peuvent être déclarés que dans la bibliothèque standard.

(https://github.com/golang/go/issues/18130#issuecomment-264165833 et https://github.com/golang/go/issues/18130#issuecomment-264171370 par @iand)

La préoccupation est "le code qui a renommé les concepts de bibliothèque standard pour s'adapter à une convention de nommage personnalisée", ou "de longues chaînes d'alias spaghetti sur plusieurs packages qui se retrouvent dans la bibliothèque standard", ou "des éléments d'alias comme l'interface{} et l'erreur" .

Comme indiqué, la restriction interdirait le cas de « progiciel d'extension » décrit ci-dessus impliquant x/image/draw.

On ne sait pas pourquoi la bibliothèque standard devrait être spéciale : les problèmes existeraient avec n'importe quel code. De plus, ni interface{} ni error n'est un type de la bibliothèque standard. Reformuler la restriction en « alias de types prédéfinis » interdirait les erreurs d'alias, mais la nécessité d'aliaser l'erreur était l'un des exemples motivants de l'article.

Restriction? La cible d'alias doit être un identificateur qualifié de package.

(https://github.com/golang/go/issues/18130#issuecomment-264188282 par @jba)

Cela rendrait impossible la création d'un alias lors du renommage d'un type dans un package, ce qui peut être suffisamment utilisé pour nécessiter une réparation progressive (https://github.com/golang/go/issues/18130#issuecomment-264274714 par @ bcmills).

Cela interdirait également les erreurs d'alias comme dans l'article.

Restriction? La cible d'alias doit être un identifiant qualifié de package avec le même nom que l'alias.

(proposé lors de la discussion sur les alias dans Go 1.8)

En plus des problèmes de la section précédente avec la limitation aux identifiants qualifiés de package, forcer le nom à rester le même interdirait la conversion de io.ByteBuffer en bytes.Buffer dans l'article.

Restriction? Les alias doivent être découragés d'une manière ou d'une autre.

« Que diriez-vous de cacher les alias derrière une importation, tout comme pour « C » et « unsafe », pour décourager davantage son utilisation ? Dans la même veine, j'aimerais que la syntaxe des alias soit détaillée et se démarque comme un échafaudage pour la refactorisation en cours ." - https://github.com/golang/go/issues/18130#issuecomment -264289940 par @xiegeo

« Devrions-nous également automatiquement déduire qu'un type d'alias est hérité et doit être remplacé par le nouveau type ? Si nous appliquons golint, godoc et des outils similaires pour visualiser l'ancien type comme obsolète, cela limiterait très considérablement l'abus d'alias de type. Et le dernier problème d'utilisation abusive de la fonction d'alias serait résolu." - https://github.com/golang/go/issues/18130#issuecomment -265062154 par @rakyll

Jusqu'à ce que nous sachions qu'ils seront mal utilisés, il semble prématuré de décourager l'utilisation. Il peut y avoir de bonnes utilisations non temporaires (voir ci-dessus).

Même en cas de réparation de code, l'ancien ou le nouveau type peut être l'alias lors de la transition, en fonction des contraintes imposées par le graphe d'import. Être un alias ne signifie pas que le nom est obsolète.

Il existe déjà un mécanisme pour marquer certaines déclarations comme obsolètes (voir https://github.com/golang/go/issues/18130#issuecomment-265294564 par @jimmyfrasche).

Restriction? Les alias doivent cibler les types nommés.

"Les alias ne devraient pas s'appliquer aux types sans nom. Il n'y a pas d'histoire de "réparation de code" en passant d'un type sans nom à un autre. Autoriser les alias sur des types sans nom signifie que je ne peux plus enseigner Go en tant que types simplement nommés et sans nom. " - https://github.com/golang/go/issues/18130#issuecomment -276864903 par @davecheney

Jusqu'à ce que nous sachions qu'ils seront mal utilisés, il semble prématuré de décourager l'utilisation. Il peut y avoir de bonnes utilisations avec des cibles sans nom (voir ci-dessus).

Comme indiqué dans le document de conception, nous prévoyons de changer la terminologie pour rendre la situation plus claire.

FrozenDueToAge Proposal Proposal-Accepted

Commentaire le plus utile

@cznic , @iand , autres : veuillez noter que les _restrictions ajoutent de la complexité_. Ils compliquent l'explication de la fonctionnalité et ajoutent une charge cognitive pour tout utilisateur de la fonctionnalité : si vous oubliez une restriction, vous devez vous demander pourquoi quelque chose que vous pensiez devrait fonctionner ne fonctionne pas.

C'est souvent une erreur de mettre en œuvre des restrictions sur un essai d'une conception uniquement en raison d'une mauvaise utilisation hypothétique. Cela s'est produit dans les discussions sur la proposition d'alias, et cela a rendu les alias de l'essai incapables de gérer la conversion io.ByteBuffer => bytes.Buffer partir de l'article. Une partie de l'objectif de la rédaction de l'article est de définir certains cas que nous savons que nous voulons pouvoir gérer, afin de ne pas les restreindre par inadvertance.

Comme autre exemple, il serait facile de créer un argument de mauvaise utilisation pour interdire les récepteurs non pointeurs ou pour interdire les méthodes sur les types non struct. Si nous avions fait l'un ou l'autre de ceux-ci, vous ne pourriez pas créer d'énumérations avec les méthodes String() pour s'imprimer elles-mêmes, et vous ne pourriez pas faire en sorte que http.Headers soit à la fois une simple carte et fournisse des méthodes d'aide. Il est souvent facile d'imaginer des abus ; les utilisations positives convaincantes peuvent prendre plus de temps à apparaître, et il est important de créer un espace d'expérimentation.

Encore un autre exemple, la conception et l'implémentation originales des méthodes pointeur vs valeur ne faisaient pas de distinction entre les ensembles de méthodes sur T et *T : si vous aviez un *T, vous pouviez appeler les méthodes valeur (récepteur T), et si vous aviez a T, vous pouvez appeler les méthodes de pointeur (récepteur *T). C'était simple, sans aucune restriction à expliquer. Mais ensuite, l'expérience réelle nous a montré que le fait d'autoriser les appels de méthode de pointeur sur des valeurs entraînait une classe spécifique de bogues déroutants et surprenants. Par exemple, vous pourriez écrire :

var buf bytes.Buffer
io.Copy(buf, reader)

et io.Copy réussirait, mais buf n'aurait rien dedans. Nous avons dû choisir entre expliquer pourquoi ce programme ne fonctionnait pas correctement ou expliquer pourquoi ce programme ne s'était pas compilé. Quoi qu'il en soit, il allait y avoir des questions, mais nous avons décidé d'éviter une exécution incorrecte. Même ainsi, nous avons quand même dû écrire une entrée de FAQ expliquant pourquoi le design a un trou découpé.

Encore une fois, n'oubliez pas que les restrictions ajoutent de la complexité. Comme toute complexité, les restrictions nécessitent une justification significative. À ce stade du processus de conception, il est bon de penser aux restrictions qui pourraient être appropriées pour une conception particulière, mais nous ne devrions probablement mettre en œuvre ces restrictions qu'après une expérience réelle avec la conception plus simple et sans restriction nous aide à comprendre si la restriction apporterait suffisamment d'avantages à payer pour son coût.

Tous les 225 commentaires

J'aime à quel point cela semble visuellement uniforme.

const OldAPI => NewPackage.API
func  OldAPI => NewPackage.API
var   OldAPI => NewPackage.API
type  OldAPI => NewPackage.API

Mais comme nous pouvons déplacer presque progressivement la plupart des éléments, peut-être le plus simple
solution _is_ juste pour permettre un = pour les types.

const OldAPI = NewPackage.API
func  OldAPI() { NewPackage.API() }
var   OldAPI = NewPackage.API
type  OldAPI = NewPackage.API

Alors tout d'abord, je voulais juste vous remercier pour cet excellent article. Je pense que la meilleure solution est d'introduire des alias de type avec un opérateur d'affectation. Cela ne nécessite aucun nouveau mot-clé/opérateur, utilise une syntaxe familière et devrait résoudre le problème de refactorisation pour les grandes bases de code.

Comme le souligne l'article de Russ, toute solution de type alias doit résoudre avec élégance https://github.com/golang/go/issues/17746 et https://github.com/golang/go/issues/17784

Merci pour la rédaction de cet article.

Je trouve que les alias de type uniquement utilisant l'opérateur d'affectation sont les meilleurs :

type OldAPI = NewPackage.API

Mes raisons :

  • C'est plus simple.
    La solution alternative => ayant une signification subtilement différente en fonction de son opérande ne semble pas à sa place pour Go.
  • C'est concentré et conservateur.
    Le problème des types est résolu et vous n'avez pas à vous soucier d'imaginer les complications de la solution généralisée.
  • C'est esthétique.
    Je pense que ça a l'air plus agréable.

Tout cela ci-dessus : le résultat étant simple, ciblé, conservateur et esthétique, il me permet d'imaginer facilement qu'il fait partie de Go.

Si la solution était limitée aux types uniquement, la syntaxe

type NewFoo = old.Foo

déjà envisagé auparavant, comme discuté dans l' article de

Si nous aimerions pouvoir faire la même chose pour les constantes, les variables et les fonctions, ma syntaxe préférée serait (comme proposé précédemment)

package newfmt

import (
    "fmt"
)

// No renaming.
export fmt.Printf // Note: Same as `export Printf fmt.Printf`.

export (
        fmt.Sprintf
        fmt.Formatter
)

// Renaming.
export Foo fmt.Errorf // Foo must be exported, ie. `export foo fmt.Errorf` would be invalid.

export (
    Bar fmt.Fprintf
    Qux fmt.State
)

Comme indiqué précédemment, l'inconvénient est qu'un nouveau mot-clé de niveau supérieur est introduit, ce qui est certes maladroit, même s'il est techniquement faisable et entièrement rétrocompatible. J'aime cette syntaxe car elle reflète le modèle des importations. Il me semblerait naturel que les exportations ne soient autorisées que dans la même section où les importations sont autorisées, c'est-à-dire. entre la clause package et tout TLD var, type, constante ou fonction.

Les identifiants de changement de nom seraient déclarés dans la portée du package, cependant, les nouveaux noms ne sont pas visibles dans le package les déclarant (newfmt dans l'exemple ci-dessus) ci-dessus en ce qui concerne la redéclaration, qui est interdite comme d'habitude. Compte tenu de l'exemple précédent, les TLD

var v = Printf // undefined: Printf.
var Printf int // Printf redeclared, previous declaration at newfmt.go:8.

Dans le package d'importation, les identifiants de renommage sont visibles normalement, comme tout autre identifiant exporté du bloc de package (newftm).

package foo

import "newfmt"

type bar interface {
    baz(qux newfmt.Qux) // qux type is identical to fmt.State.
}

En conclusion, cette approche n'introduit aucune nouvelle liaison de nom local dans newfmt, ce qui, je pense, évite au moins certains des problèmes discutés dans #17746 et résout complètement #17784.

Ma première préférence est pour un type NewFoo = old.Foo type uniquement.

Si une solution plus générale est souhaitée, je suis d'accord avec @cznic qu'un mot-clé dédié est meilleur qu'un nouvel opérateur (en particulier un opérateur asymétrique avec une directionnalité déroutante[1]). Cela étant dit, je ne pense pas que le mot-clé export véhicule le bon sens. Ni la syntaxe, ni la sémantique ne reflètent import . Qu'en est-il de alias ?

Je comprends pourquoi @cznic ne veut pas que les nouveaux noms soient accessibles dans le package les déclarant, mais, pour moi du moins, cette restriction semble inattendue et artificielle (bien que je comprenne parfaitement la raison qui la sous-tend).

[1] J'utilise Unix depuis près de 20 ans et je n'arrive toujours pas à créer un lien symbolique du premier coup. Et j'échoue généralement même au deuxième essai, après avoir lu le manuel.

Je voudrais proposer une contrainte supplémentaire : les alias de type vers les types de bibliothèque standard ne peuvent être déclarés que dans la bibliothèque standard.

Mon raisonnement est que je ne veux pas travailler avec du code qui a renommé des concepts de bibliothèque standard pour s'adapter à une convention de nommage personnalisée. Je ne veux pas non plus traiter de longues chaînes d'alias spaghetti sur plusieurs packages qui se retrouvent dans la bibliothèque standard.

@iand : Cette contrainte bloquerait l'utilisation de cette fonctionnalité pour migrer quoi que ce soit dans la bibliothèque standard. Exemple concret, la migration actuelle de Context dans la bibliothèque standard. L'ancienne maison de Context devrait devenir un alias pour le Context dans la bibliothèque standard.

@quentinmit c'est malheureusement vrai. Cela limite également le cas d'utilisation de golang.org/x/image/draw dans cette CL https://go-review.googlesource.com/#/c/32145/

Ma vraie préoccupation concerne les gens qui créent des alias comme interface{} et error

S'il est décidé d'introduire un nouvel opérateur, j'aimerais proposer ~ . En anglais, il est généralement compris comme signifiant « similaire à », « approximativement », « environ » ou « environ ». Comme @4ad l'a indiqué ci-dessus, le => est un opérateur asymétrique avec une directionnalité déroutante.

Par exemple:

const OldAPI ~ NewPackage.API
func  OldAPI ~ NewPackage.API
var   OldAPI ~ NewPackage.API
type  OldAPI ~ NewPackage.API

@iand si nous

Cela signifierait également que vous ne pourriez pas avoir d'alias vers aucun type dans le package actuel, ou vers des expressions de type longues comme map[string]map[int]interface{} . Mais ces utilisations n'ont rien à voir avec l'objectif principal de la réparation progressive du code, alors peut-être qu'elles ne sont pas une grande perte.

@cznic , @iand , autres : veuillez noter que les _restrictions ajoutent de la complexité_. Ils compliquent l'explication de la fonctionnalité et ajoutent une charge cognitive pour tout utilisateur de la fonctionnalité : si vous oubliez une restriction, vous devez vous demander pourquoi quelque chose que vous pensiez devrait fonctionner ne fonctionne pas.

C'est souvent une erreur de mettre en œuvre des restrictions sur un essai d'une conception uniquement en raison d'une mauvaise utilisation hypothétique. Cela s'est produit dans les discussions sur la proposition d'alias, et cela a rendu les alias de l'essai incapables de gérer la conversion io.ByteBuffer => bytes.Buffer partir de l'article. Une partie de l'objectif de la rédaction de l'article est de définir certains cas que nous savons que nous voulons pouvoir gérer, afin de ne pas les restreindre par inadvertance.

Comme autre exemple, il serait facile de créer un argument de mauvaise utilisation pour interdire les récepteurs non pointeurs ou pour interdire les méthodes sur les types non struct. Si nous avions fait l'un ou l'autre de ceux-ci, vous ne pourriez pas créer d'énumérations avec les méthodes String() pour s'imprimer elles-mêmes, et vous ne pourriez pas faire en sorte que http.Headers soit à la fois une simple carte et fournisse des méthodes d'aide. Il est souvent facile d'imaginer des abus ; les utilisations positives convaincantes peuvent prendre plus de temps à apparaître, et il est important de créer un espace d'expérimentation.

Encore un autre exemple, la conception et l'implémentation originales des méthodes pointeur vs valeur ne faisaient pas de distinction entre les ensembles de méthodes sur T et *T : si vous aviez un *T, vous pouviez appeler les méthodes valeur (récepteur T), et si vous aviez a T, vous pouvez appeler les méthodes de pointeur (récepteur *T). C'était simple, sans aucune restriction à expliquer. Mais ensuite, l'expérience réelle nous a montré que le fait d'autoriser les appels de méthode de pointeur sur des valeurs entraînait une classe spécifique de bogues déroutants et surprenants. Par exemple, vous pourriez écrire :

var buf bytes.Buffer
io.Copy(buf, reader)

et io.Copy réussirait, mais buf n'aurait rien dedans. Nous avons dû choisir entre expliquer pourquoi ce programme ne fonctionnait pas correctement ou expliquer pourquoi ce programme ne s'était pas compilé. Quoi qu'il en soit, il allait y avoir des questions, mais nous avons décidé d'éviter une exécution incorrecte. Même ainsi, nous avons quand même dû écrire une entrée de FAQ expliquant pourquoi le design a un trou découpé.

Encore une fois, n'oubliez pas que les restrictions ajoutent de la complexité. Comme toute complexité, les restrictions nécessitent une justification significative. À ce stade du processus de conception, il est bon de penser aux restrictions qui pourraient être appropriées pour une conception particulière, mais nous ne devrions probablement mettre en œuvre ces restrictions qu'après une expérience réelle avec la conception plus simple et sans restriction nous aide à comprendre si la restriction apporterait suffisamment d'avantages à payer pour son coût.

De plus, j'espère que nous pourrons prendre une décision provisoire sur ce qu'il faut essayer, puis avoir quelque chose de prêt pour l'expérimentation au début du cycle Go 1.9 (idéalement le jour de l'ouverture du cycle). Avoir plus de temps pour expérimenter aura de nombreux avantages, parmi lesquels une opportunité de savoir si une restriction particulière est impérieuse. Une erreur avec l'alias n'a pas été de commettre une implémentation complète avant la fin du cycle Go 1.8.

Une chose à propos de la proposition d'alias d'origine est que dans le cas d'utilisation prévu (activation de la refactorisation), l'utilisation réelle du type d'alias ne devrait être que temporaire. Dans l'exemple protobuffer, le stub io.BytesBuffer a été supprimé une fois la réparation progressive terminée.

Si le mécanisme d'alias ne doit être vu que temporairement, nécessite-t-il réellement un changement de langue ? Peut-être qu'à la place, il pourrait y avoir un mécanisme pour fournir à gc une liste d'"alias". gc pourrait temporairement effectuer les substitutions, et l'auteur de la base de code en aval pourrait progressivement supprimer les éléments de ce fichier au fur et à mesure de la fusion des correctifs. Je me rends compte que cette suggestion a aussi des conséquences délicates, mais elle encourage au moins un mécanisme temporaire.

Je ne participerai pas au bikeshedding sur la syntaxe (je m'en fiche fondamentalement), à une exception près : si l'ajout d'alias est décidé et s'il est décidé de les restreindre aux types, veuillez utiliser une syntaxe qui est systématiquement extensible à au moins var , sinon aussi func et const (toutes les constructions syntaxiques proposées autorisent tout, sauf type Foo = pkg.Bar ). La raison en est que, bien que je convienne que les cas où les alias pour var font la différence pourraient être rares, je ne pense pas qu'ils soient inexistants et en tant que tels, je pense que nous pourrions bien à un moment donné décider d'ajouter eux aussi. À ce stade, nous voudrons certainement que toutes les déclarations d'alias soient cohérentes, ce serait mauvais si c'était type Foo = pkg.Bar et var Foo => pkg.Bar .

Je plaiderais aussi légèrement pour avoir les quatre. Les raisons sont

1) il y a une distinction pour var et je l'utilise parfois. Par exemple, j'expose souvent un var Debug *log.Logger global ou je réaffecte des singletons globaux comme http.DefaultServeMux pour intercepter/supprimer les enregistrements de packages qui y ajoutent des gestionnaires.

2) Je pense aussi que, alors que func Foo() { pkg.Bar() } fait la même chose que func Foo => pkg.Bar , l'intention de ce dernier est beaucoup plus claire (surtout si vous connaissez déjà les alias). Il indique clairement "ce n'est pas vraiment censé être ici". Ainsi, bien que techniquement identique, la syntaxe d'alias peut servir de documentation.

Ce n'est pas la colline sur laquelle je mourrais, cependant; les alias de type seuls pour l'instant me conviendraient, tant qu'il y a la possibilité de les étendre plus tard.

Je suis aussi super content que cela ait été écrit comme ça l'était. Il résume un tas d'opinions que j'avais sur la conception et la stabilité de l'API pendant un certain temps et servira, à l'avenir, de simple référence pour relier les gens aussi :)

Cependant, je tiens également à souligner qu'il existe des cas d'utilisation supplémentaires couverts par des alias différents de la doc (et AIUI l'intention plus générale de ce problème, qui est de trouver une solution pour résoudre la réparation progressive). Je suis très heureux si la communauté peut s'entendre sur le concept d'activation de la réparation progressive, mais si une décision différente des alias est décidée pour y parvenir, je pense également que dans ce cas, il devrait y avoir simultanément discuter si et comment soutenir des choses comme les importations publiques de protobuf ou le cas d'utilisation de x/image/draw de packages de remplacement (tous deux un peu proches de mon cœur également) avec une solution différente. La proposition de @btracey d'un indicateur go-tool/gc pour les alias est un exemple où je pense que, bien qu'elle couvre relativement bien la réparation progressive, elle n'est pas vraiment acceptable pour ces autres cas d'utilisation. Vous ne pouvez pas vraiment vous attendre à ce que tous ceux qui veulent compiler quelque chose qui utilise x/image/draw passent ces drapeaux, ils devraient juste pouvoir go get .

@jba

@iand si nous

Cela signifierait également que vous ne pourriez pas avoir d'alias pour aucun type dans le package actuel, […]. Mais ces utilisations n'ont rien à voir avec l'objectif principal de la réparation progressive du code, alors peut-être qu'elles ne sont pas une grande perte.

Renommer au sein d'un package (par exemple en un nom plus idiomatique ou cohérent) est certainement un type de refactorisation que l'on pourrait raisonnablement vouloir faire, et si le package est largement utilisé, cela nécessite une réparation progressive.

Je pense qu'une restriction aux seuls noms qualifiés de paquet serait une erreur. (Une restriction aux seuls noms exportés pourrait être plus tolérable.)

@btracey

Peut-être qu'à la place, il pourrait y avoir un mécanisme pour fournir à gc une liste d'"alias". gc pourrait temporairement effectuer les substitutions, et l'auteur de la base de code en aval pourrait progressivement supprimer les éléments de ce fichier au fur et à mesure de la fusion des correctifs.

Un mécanisme pour gc signifierait soit que le code n'est constructible qu'avec gc pendant le processus de réparation, soit que le mécanisme devrait être supporté par les autres compilateurs (par exemple gccgo et llgo ) aussi. Un mécanisme "sans changement de langue" qui doit être pris en charge par toutes les implémentations est un changement de langue de facto - c'est juste un avec une documentation plus pauvre.

@btracey et @bcmills , et pas seulement les compilateurs : tout outil qui analyse le code source, comme le gourou ou tout ce que les gens ont construit. C'est certainement un changement de langue, peu importe comment vous le découpez.

D'accord merci.

Une autre possibilité est les alias pour tout sauf les consts (et @rsc s'il vous plaît pardonnez-moi de proposer une restriction !)

Pour les consts, => n'est en fait qu'une manière plus longue d'écrire = . Il n'y a pas de nouvelle sémantique, comme avec les types et les vars. Il n'y a pas de frappes enregistrées comme avec funcs.

Cela résoudrait au moins #17784.

Le contre-argument serait que l'outillage pourrait traiter les cas différemment et qu'il pourrait être un indicateur d'intention. C'est un bon contre-argument, mais je ne pense pas que cela l'emporte sur le fait qu'il s'agit essentiellement de deux façons de faire exactement la même chose.

Cela dit, je suis d'accord avec les alias de type pour l'instant, ils sont certainement les plus importants. Je suis tout à fait d'accord avec @Merovius sur le fait que nous devrions fortement envisager de conserver la possibilité d'ajouter des alias var et func à l'avenir, même si cela ne se produit pas avant un certain temps.

Que diriez-vous de cacher des alias derrière une importation, tout comme pour "C" et "unsafe", pour décourager davantage son utilisation ? Dans la même veine, j'aimerais que la syntaxe des alias soit verbeuse et se démarque comme un échafaudage pour la refactorisation en cours.

Pour tenter d'ouvrir un peu l'espace de conception, voici quelques idées. Ils ne sont pas étoffés. Ils sont probablement mauvais et/ou impossibles ; l'espoir est principalement de déclencher des idées nouvelles/meilleures chez les autres. Et s'il y a un intérêt, nous pouvons explorer davantage.

L'idée motivante pour (1) et (2) est d'utiliser en quelque sorte la conversion au lieu d'alias. Dans #17746, les alias ont rencontré des problèmes liés au fait d'avoir plusieurs noms pour le même type (ou plusieurs façons d'épeler le même nom, selon que vous pensiez aux alias comme #define ou comme des liens physiques). L'utilisation de la conversion évite cela en gardant les types distincts.

  1. Ajoutez plus de conversion automatique.

Lorsque vous appelez fmt.Println("abc") ou écrivez var e interface{} = "abc" , "abc" est automatiquement converti en interface{} . Nous pourrions changer le langage de sorte que lorsque vous avez déclaré type T struct { S } , et que T n'a pas de méthodes non promues, le compilateur convertit automatiquement entre S et T si nécessaire, y compris de manière récursive à l'intérieur d'autres structures. T pourrait alors servir d'alias de facto de S (ou vice versa) à des fins de refactorisation progressive.

  1. Ajoutez un nouveau type de type "ressemble".

Soit type T ~S déclarer un nouveau type T qui est un type qui "ressemble à S". Plus précisément, T est "tout type convertible vers et depuis le type S". (Comme toujours, la syntaxe pourrait être discutée plus tard.) Comme les types d'interface, T ne peut pas avoir de méthodes ; pour faire pratiquement n'importe quoi avec T, vous devez le convertir en S (ou en un type convertible vers/depuis S). Contrairement aux types d'interface, il n'y a pas de "type concret", la conversion entre S en T et T en S n'implique aucun changement de représentation. Pour une refactorisation progressive, ces types « ressemble » permettraient aux auteurs d'écrire des API acceptant à la fois les anciens et les nouveaux types. (Les types "On dirait" sont fondamentalement un type d'union simplifié et très restreint.)

  1. Tapez les balises

Idée super hideuse en prime. (S'il vous plaît, ne vous embêtez pas à me dire que c'est affreux - je le sais. J'essaie seulement de stimuler de nouvelles idées chez les autres.) Et si nous introduisions des balises de type (comme les balises struct) et utilisions des balises de type spéciales pour configurer et contrôler les alias, comme par exemple type T S "alias:\"T\"" . Les balises de type auront également d'autres utilisations et elles permettent à l'auteur du package de spécifier davantage d'alias que simplement « ce type est un alias » ; par exemple, l'auteur du code peut spécifier le comportement d'intégration.

Si nous réessayons les alias, cela pourrait valoir la peine de réfléchir à "que fait godoc", similaire aux problèmes "que fait iota" et "que fait l'intégration".

Concrètement, si nous avons

type  OldAPI => NewPackage.API

et NewPackage.API a un commentaire doc, devons-nous copier/coller ce commentaire à côté de "type OldAPI", devons-nous le laisser sans commentaire (avec godoc fournissant automatiquement un lien ou copier/coller automatiquement), ou y a-t-il une autre convention?

Quelque peu tangentielle, alors que la motivation principale est et devrait être de prendre en charge la réparation progressive du code, un cas d'utilisation mineur (en revenant à la proposition d'alias, puisqu'il s'agit d'une proposition concrète) pourrait être d'éviter une double surcharge d'appel de fonction lors de la présentation d'une seule fonction soutenu par plusieurs implémentations dépendantes des balises de construction. Je ne fais qu'agiter la main pour le moment, mais j'ai l'impression que les alias auraient pu être utiles dans le récent https://groups.google.com/d/topic/golang-nuts/wb5I2tjrwoc/discussion "Éviter la surcharge d'appel de fonction dans les packages avec la discussion sur les implémentations go+asm".

@nigeltao re godoc, je pense :

Il doit toujours être lié à l'original, peu importe.

S'il y a des documents sur l'alias, ceux-ci devraient être affichés, peu importe.

S'il n'y a pas de docs sur l'alias, il est tentant que godoc affiche les docs d'origine, mais le nom du type serait faux si l'alias changeait également le nom, les docs pourraient faire référence à des éléments qui ne figurent pas dans le package actuel, et, s'il est utilisé pour une refactorisation progressive, il peut y avoir un message indiquant "Déprécié : utilisez X" lorsque vous regardez X.

Cependant, cela n'aurait peut-être pas d'importance pour la majorité des cas d'utilisation. Ce sont des choses qui pourraient mal tourner, pas des choses qui vont mal tourner. Et certains d'entre eux pourraient être détectés par linting, comme des alias renommés et la copie accidentelle d'avertissements de dépréciation.

Je ne sais pas si l'idée suivante a déjà été publiée, mais qu'en est-il d'une approche de type "gofix" / "gorename" principalement basée sur des outils ? Élaborer:

  • tout package peut contenir un ensemble de règles de réécriture (par exemple mappage pkg.Ident => otherpkg.Ident )
  • ces règles de réécriture peuvent être spécifiées avec des balises //+rewrite ... dans des fichiers go arbitraires
  • ces règles de réécriture ne se limitent pas aux changements compatibles ABI, il est également possible de faire d'autres choses (par exemple pkg.MyFunc(a) => pkg.MyFunc(context.Contex(), a) )
  • un outil de type gofix peut être utilisé pour appliquer toutes les transformations au référentiel actuel. Cela permet aux utilisateurs d'un package de mettre à jour facilement leur code.
  • il n'est pas nécessaire d'appeler l'outil gofix pour réussir la compilation. Une bibliothèque qui souhaite toujours utiliser l'ancienne API d'une dépendance X (pour rester compatible avec les anciennes et les nouvelles versions de X) peut toujours le faire. La commande go build doit appliquer les transformations (spécifiées dans les balises de réécriture du package X) à la volée sans modifier les fichiers sur le disque.

Les dernières étapes peuvent compliquer / ralentir un peu le compilateur, mais il ne s'agit essentiellement que d'un pré-processeur et le nombre de règles de réécriture doit être limité de toute façon. Alors, assez de brainstorming pour aujourd'hui :)

L'utilisation d'alias pour éviter la surcharge des appels de fonction semble être un hack pour contourner l'incapacité du compilateur à insérer des fonctions non-feuille. Je ne pense pas que les déficiences de l'implémentation devraient influencer la spécification du langage.

@josharian Bien que vous ne les vouliez pas comme des propositions complètes, permettez-moi de répondre (même si seulement, afin que quiconque s'inspire de vous puisse prendre en compte la critique immédiate):

  1. Ne résout pas vraiment le problème, car les conversions ne sont pas vraiment le problème. x/net/context.Context est assignable/convertible/n'importe quoi en context.Context . Le problème, ce sont les types d'ordre supérieur ; à savoir les types func (ctx x/net/context.Context) et func (ctx context.Context) ne sont pas les mêmes, même si les arguments sont assignables. Ainsi, pour que 1 résolve le problème, type T struct { S } devrait signifier que T et S sont des types identiques. Ce qui signifie que vous utilisez simplement une syntaxe différente pour les alias après tout (juste que cette syntaxe a déjà une signification différente).

  2. A encore un problème avec les types d'ordre supérieur, car les types assignables/convertibles n'ont pas nécessairement la même représentation en mémoire (et s'ils le font, l'interprétation peut changer de manière significative). Par exemple, un uint8 est convertible en un uint64 et vice-versa. Mais cela signifierait que, par exemple avec type T ~uint8 , le compilateur ne peut pas savoir comment appeler un func(T) ; a-t-il besoin de pousser 1, 2,4 ou 8 octets sur la pile ? Il existe peut-être des moyens de contourner ce problème, mais cela me semble assez compliqué (et plus difficile à comprendre que les alias).

Merci @Merovius.

  1. Oui, j'ai raté la satisfaction de l'interface ici. Vous avez raison, cela ne fait pas le travail.

  2. J'avais en tête "avoir la même représentation de la mémoire". Le va-et-vient convertible n'est clairement pas la bonne explication de cela - merci.

@uluyol oui, il s'agit en grande partie de l'incapacité du compilateur à insérer des fonctions non feuilles, mais l'alias explicite peut être moins surprenant quant à savoir si les appels intégrés à des non-feuilles doivent apparaître dans les traces de pile, runtime.Callers, etc.

En tout cas, comme je l'ai dit, c'est une tangente mineure.

@josharian Problème similaire : [2]uintptr et interface{} ont la même représentation mémoire ; ainsi, se fier uniquement à la représentation en mémoire permettra de contourner la sécurité de type. uint64 et float64 ont tous deux la même représentation de la mémoire et sont convertibles dans les deux sens, mais conduiraient toujours à des résultats vraiment étranges au moins, si vous ne savez pas lequel est lequel.

Cependant, vous pourriez vous en tirer avec "le même type sous-jacent". Je ne sais pas quelles seraient les implications pour cela. Du haut de mon chapeau, cela pourrait conduire à une erreur si un type est utilisé dans les champs, par exemple. Si vous avez type S1 struct { T1 } et type S2 struct { T2 } (avec T1 et T2 le même type de sous-jacent), alors sous type L1 ~T1 deux peuvent fonctionner comme type S struct { L1 } , mais comme T1 et T2 ont toujours un type de sous-jacent différent (bien que semblable), avec type L2 ~S1 vous n'aurez pas S2 ressemblant S1 et ne pas être utilisable comme L2 .

Vous devrez donc, à plusieurs endroits dans la spécification, remplacer ou modifier "types identiques" par "même type sous-jacent" pour que cela fonctionne, ce qui semble lourd et aura probablement des conséquences imprévues pour la sécurité des types. Les types "sosies" semblent également avoir un potentiel d'abus et de confusion encore plus grand que les alias, à mon humble avis, qui semblent être les principaux arguments contre les alias.

Si quelqu'un peut proposer une règle simple pour cela, cependant, qui n'a pas ces problèmes, cela devrait certainement être considéré comme une alternative :)

Suite à l' idéation de

Autoriser la spécification de "types substituables". Il s'agit d'une liste de types qui peuvent être substitués au type nommé dans les arguments de fonction, les valeurs de retour, etc. Le compilateur permettrait d'appeler une fonction avec un argument du type nommé ou de l'un de ses substituts. Les types de substitution doivent avoir une définition compatible avec le type nommé. Compatible signifie ici des représentations de mémoire identiques et des déclarations identiques après avoir autorisé d'autres types de substitution dans la déclaration.

Un problème immédiat est que la directionnalité de cette relation est opposée à la proposition d'alias qui inverse le graphe de dépendance. Cela seul pourrait le rendre impraticable, mais je le propose ici parce que d'autres pourraient penser à un moyen de contourner cela. Une façon pourrait être de déclarer les substituts en tant que commentaires //go plutôt que via le graphique d'importation. De cette façon, ils deviennent peut-être plus comme des macros.

Inversement, il y a certains avantages à cette inversion de directionnalité :

  • l'ensemble des types substituables est contrôlé par l'auteur du nouveau package qui est mieux placé pour garantir la sémantique
  • aucune modification de code n'est requise dans le package d'origine afin que les clients n'aient pas à mettre à jour jusqu'à ce qu'ils commencent à utiliser le nouveau package

En appliquant ceci à la refactorisation de contexte : le package de contexte de bibliothèque standard déclarerait que context.Context peut être remplacé par golang.org/x/net/context.Context . Cela signifie toute utilisation qui accepte le contexte. Le contexte peut également accepter un golang.org/x/net/context.Context à sa place. Cependant, les fonctions du package de contexte qui renvoient un Context renvoient toujours un context.Context .

Cette proposition contourne le problème d'incorporation (#17746) car le nom du type incorporé ne change jamais. Cependant, un type incorporé pourrait être initialisé à l'aide d'une valeur d'un type de substitution.

@iand @josharian vous demandez une certaine variante de types covariants.

@josharian , merci pour les suggestions.

Re type T struct { S } , cela ressemble à une syntaxe différente pour l'alias, et pas nécessairement plus claire.

En ce qui concerne type T ~S , je ne sais pas en quoi cela diffère de l'alias ou je ne sais pas comment cela aide la refactorisation. Je suppose que dans une refactorisation (disons, io.ByteBuffer -> bytes.Buffer), vous écririez :

package io
type ByteBuffer ~bytes.Buffer

mais alors si, comme vous le dites, "pour faire fondamentalement n'importe quoi avec T, vous devez le convertir en S", alors tout le code faisant quoi que ce soit avec io.ByteBuffer se casse toujours.

Re type T S "alias" : Un point clé que @bcmills a fait ci-dessus est que le fait d'avoir plusieurs noms équivalents pour les types est un changement de langue, quelle que soit l'orthographe. Tous les compilateurs doivent savoir que, disons, io.ByteBuffer et bytes.Buffer sont les mêmes, tout comme tous les outils qui analysent ou même vérifient le code. L'élément clé de votre suggestion me semble quelque chose comme "peut-être devrions-nous planifier à l'avance pour d'autres ajouts". Peut-être, mais il n'est pas clair qu'une chaîne soit le meilleur moyen de les décrire, et il n'est pas non plus clair que nous voulions concevoir une syntaxe (comme des annotations généralisées Java) sans un besoin clair. Même si nous avions une forme générale, nous aurions toujours besoin d'examiner attentivement toutes les implications de toute nouvelle sémantique que nous avons introduite, et la plupart seraient encore des changements de langage qui nécessiteraient la mise à jour de tous les outils (sauf gofmt, certes). Dans l'ensemble, il semble plus simple de continuer à trouver le moyen le plus clair d'écrire les formes dont nous avons besoin une par une au lieu de créer un métalangage d'une sorte ou d'une autre.

@Merovius FWIW, je dirais que [2]uintptr et interface{} n'ont pas la même représentation de la mémoire. Une interface{} est un [2]unsafe.Pointer et non un [2]uintptr. Un uintptr et un pointeur sont des représentations différentes. Mais je pense que votre point général est juste, que nous ne voulons pas nécessairement permettre la conversion directe de ce genre de chose. Je veux dire, pouvez-vous également convertir l'interface{} en [2]*octet ? C'est beaucoup plus qu'il n'en faut ici.

@jimmyfrasche et @nigeltao , re godoc : Je suis d'accord que nous avons besoin que cela fonctionne aussi tôt. Je suis d'accord que nous ne devrions pas coder en dur l'hypothèse "la nouvelle fonctionnalité - quelle qu'elle soit - ne sera utilisée que pour la refactorisation de la base de code". Il peut avoir d'autres utilisations importantes, comme Nigel trouvé pour aider à écrire un package d'extension de dessin avec des alias. Je m'attends à ce que les choses obsolètes soient marquées comme obsolètes dans leurs commentaires de documentation explicitement, comme l'a dit Jimmy. J'ai pensé à générer automatiquement un commentaire de doc s'il n'y en a pas, mais il n'y a rien d'évident à dire qui ne devrait pas déjà être clair à partir de la syntaxe (en général). Pour faire un exemple spécifique, considérons les anciens alias Go 1.8. Étant donné

type ByteBuffer => bytes.Buffer

nous pourrions synthétiser un commentaire doc disant "ByteBuffer est un alias pour bytes.Buffer", mais cela semble redondant avec l'affichage de la définition. Si quelqu'un écrit "type X struct{}" aujourd'hui, nous ne synthétisons pas "X est un type nommé pour une struct{}".

@iand , merci. Il semble que votre proposition nécessite que l'auteur du nouveau package écrive la définition exacte de l'ancien package, puis également une déclaration liant les deux, comme (constituant la syntaxe):

package old
type T { x int }

package new
import "old"
type T1 { x int }
substitutable T1 <- old.T

Je conviens que l'inversion des importations est problématique et peut être un obstacle en soi, mais oublions cela. À ce stade, la base de code semble être dans un état fragile : maintenant, le package new peut être rompu par une modification pour ajouter un champ de structure dans le package old. Étant donné la ligne substituable, il n'y a qu'une seule définition possible pour T1 : exactement la même que old.T. Si les deux types ont toujours des définitions distinctes, vous devez également vous soucier des méthodes : les implémentations des méthodes doivent-elles également correspondre ? Sinon, que se passe-t-il lorsque vous placez un T dans une interface{}, puis le retirez en utilisant une assertion de type en tant que T1 et appelez M() ? Recevez-vous T1.M ? Que se passe-t-il si vous le retirez en tant qu'interface { M() }, sans nommer directement T1 et appelez M() ? Comprenez-vous la MT ? Il y a beaucoup de complexité causée par l'ambiguïté d'avoir les deux définitions dans l'arborescence source.

Bien sûr, vous pourriez dire que la ligne substituable rend le reste redondant et ne nécessite pas de définition pour le type T1 ou toute autre méthode. Mais alors c'est fondamentalement la même chose que d'écrire (dans l'ancienne syntaxe d'alias) type T1 => old.T .

Pour en revenir au problème du graphique d'importation, bien que les exemples de l'article aient tous fait l'ancien code défini en termes de nouveau code, si le graphique du package était tel que new devait importer l'ancien à la place, il est tout aussi efficace de mettre la redirection dans le nouveau paquet pendant la transition.

Je pense que cela montre que dans toute transition comme celle-ci, il n'y a probablement pas de distinction utile entre l'auteur du nouveau package et l'auteur de l'ancien package. À la fin, l'objectif est que le code ait été ajouté au nouveau et supprimé de l'ancien, de sorte que les deux auteurs (s'ils sont différents) doivent alors être impliqués. Et les deux ont également besoin d'une sorte de compatibilité coordonnée au milieu, qu'elle soit explicite (une sorte de redirection) ou implicite (les définitions de type doivent correspondre exactement, comme dans l'exigence de substituabilité).

@rsc ce scénario de rupture suggère que tout type d'alias doit être bidirectionnel. Même dans le cadre de la proposition d'alias précédente, tout changement dans un nouveau package pourrait potentiellement casser un nombre quelconque de packages ayant créé un alias du type.

@iand S'il n'y a qu'une seule définition (parce que l'autre dit "identique à _celle_ une"), alors il n'y a pas à craindre qu'elles ne soient pas synchronisées.

Dans #13467, @joegrasse souligne qu'il serait bien que cette proposition fournisse un mécanisme permettant à des types C identiques de devenir des types Go identiques lors de l'utilisation de cgo dans plusieurs packages. Ce n'est pas du tout le même problème que celui auquel ce problème est destiné, mais les deux problèmes sont liés à l'aliasing de type.

Existe-t-il un résumé des restrictions/limitations proposées/acceptées/rejetées sur les alias ? Certaines questions qui viennent à l'esprit sont :

  • Le RHS est-il toujours pleinement qualifié ?
  • Si les alias à alias sont autorisés, comment gérons-nous les cycles d'alias ?
  • Les alias doivent-ils pouvoir exporter des identifiants non exportés ?
  • Que se passe-t-il lorsque vous intégrez un alias ? (comment accéder au champ intégré)
  • Les alias sont-ils disponibles sous forme de symboles dans le programme construit ?
  • Injection de chaîne ldflags : et si on se référait à un alias ?

@rsc Je ne veux pas trop détourner la conversation, mais sous la proposition d'alias, si "nouveau" supprime un champ sur lequel "ancien" s'appuyait, cela signifie que les clients de "ancien" ne peuvent plus compiler.

Cependant, dans le cadre de la proposition de substitution, je pense qu'il pourrait être arrangé pour que seuls les clients qui utilisent à la fois l'ancien et le nouveau se cassent. Pour que cela soit possible, la directive de substitution ne devrait être validée que lorsque le compilateur détecte une utilisation des "anciens" types dans le "nouveau" package.

@thwd Je ne pense pas qu'il y ait encore de bonne rédaction. Mes notes:

  • Les cycles d'alias ne sont pas un problème. En cas d'alias de croisement de packages, un cycle est déjà interdit en raison d'un cycle d'importation. En cas d'alias non transversaux, ils doivent évidemment être interdits, ce qui est très similaire aux cycles dans l'ordre d'initialisation. Personnellement, j'aimerais avoir des alias vers des alias, car je ne pense pas qu'ils devraient être limités à des cas d'utilisation de réparation progressive (voir mon commentaire ci-dessus) et ce serait triste, si le package A pouvait se briser par quelqu'un déplaçant un type dans package B avec un alias (imaginez que x/image/draw.Image crée draw.Image et que quelqu'un décide ensuite de déplacer draw.Image dans image.Draw via un alias, en supposant que c'est sûr. Soudainement x/image/draw pauses, car les alias à alias ne sont pas autorisés).
  • Je pense que les partisans des alias ont convenu que l'exportation d'identifiants non exportés par alias est probablement une mauvaise idée en raison de l'étrangeté que cela peut provoquer. En fait, cela signifie que les alias vers des identifiants non exportés sont inutiles et peuvent être complètement interdits.
  • La question de l'intégration, AFAIK, n'est pas encore résolue. Il y a toute une discussion dans #17746, je m'attends à ce que cette discussion se poursuive si/quand/avant qu'il soit décidé d'aller de l'avant avec des alias (mais il y a toujours la possibilité d'une solution alternative ou la décision de ne pas faire de réparations graduelles un objectif du tout)

@iand , re "seuls les clients qui utilisent à la fois l'ancien et le nouveau seraient cassés", c'est le seul cas intéressant. Ce sont les clients mixtes qui en font une réparation de code progressive. Les clients qui utilisent uniquement le nouveau code ou uniquement l'ancien code fonctionneront aujourd'hui.

Il y a autre chose à considérer, que je n'ai pas encore vu mentionné ailleurs :

Étant donné qu'un objectif explicite ici est de permettre une refactorisation volumineuse et progressive dans de grandes bases de code décentralisées, il y aura des situations où un propriétaire de bibliothèque voudra effectuer une sorte de nettoyage qui nécessitera un nombre inconnu de clients pour modifier leur code (au final " retirer l'ancienne API"). Une façon courante de le faire consiste à ajouter un avertissement de dépréciation, mais le compilateur Go n'a aucun avertissement.

Sans aucun avertissement du compilateur, comment un propriétaire de bibliothèque peut-il être sûr qu'il est sûr de terminer la refactorisation ?

Une réponse pourrait être une sorte de schéma de version : il s'agit d'une nouvelle version de la bibliothèque avec une nouvelle API incompatible. Dans ce cas, la gestion des versions est peut-être la réponse complète, pas les alias de type.

Sinon, que diriez-vous de permettre à l'auteur de la bibliothèque d'ajouter un "avertissement de dépréciation" qui provoque en fait une _erreur_ de compilation pour les clients, mais avec un algorithme explicite pour la refactorisation qu'ils doivent effectuer ? J'imagine quelque chose comme :

Error: os.time is obsolete, use time.time instead. Run "go upgrade" to fix this.

Pour les alias de type, je suppose que l'algorithme de refactorisation serait simplement "remplacer toutes les instances de OldType par NewType", mais il pourrait y avoir des subtilités, je ne suis pas sûr.

Quoi qu'il en soit, cela permettrait à l'auteur de la bibliothèque de faire de son mieux pour avertir tous les clients que leur code est sur le point de casser, et leur donner un moyen simple de le réparer, avant de supprimer complètement l'ancienne API.

@iainmerrick Il y a des bogues ouverts pour ceux-ci : golang/lint#238 et golang/gddo#456

Résoudre le problème de réparation progressive du code, comme indiqué dans l' article de

Cela nécessite soit un outil, soit un changement de langue.

Étant donné que rendre deux types interchangeables change, par définition, le fonctionnement du langage, tout outil serait un mécanisme pour simuler l'équivalence en dehors du compilateur, probablement en réécrivant toutes les instances de l'ancien type dans le nouveau type. Mais cela signifie qu'un tel outil devrait réécrire du code que vous ne possédez pas, comme un package fournisseur qui utilise golang.org/x/net/context au lieu du package de contexte stdlib. La spécification de la modification devrait être soit dans un fichier manifeste distinct, soit dans un commentaire lisible par machine. Si vous n'exécutez pas l'outil, vous obtenez des erreurs de génération. Tout cela devient compliqué à gérer. Il semble qu'un outil créerait autant de problèmes qu'il en résoudrait. Ce serait toujours un problème auquel tous ceux qui utilisent ces packages doivent faire face, bien qu'un peu plus agréable car une partie est automatisée.

Si la langue est modifiée, le code n'a besoin d'être modifié que par ses responsables et, pour la plupart des gens, les choses fonctionnent. L'outillage pour aider les mainteneurs est toujours une option, mais ce serait beaucoup plus simple puisque la source est la spécification, et seuls les mainteneurs d'un paquet auraient besoin de l'invoquer.

Comme @griesemer l'a souligné (je ne me souviens pas où, il y a eu tant de discussions à ce sujet) Go a déjà un alias, pour des trucs comme byteuint8 , et quand vous importez un package deux fois, avec des noms locaux différents, dans le même fichier source.

L'ajout d'un moyen d'alias explicitement les types dans le langage nous permet simplement d'utiliser une sémantique qui existe déjà. Cela résout un problème réel d'une manière gérable.

Un changement de langue est toujours un gros problème et beaucoup de choses doivent être réglées, mais je pense que c'est finalement la bonne chose à faire ici.

Pour autant que je sache, un "éléphant dans la pièce" est le fait que pour les alias de type, leur introduction permettra des utilisations non temporaires (c'est-à-dire "sans refactoring"). J'ai vu ceux mentionnés au passage (par exemple, "réexporter les identifiants de type dans un package différent pour simplifier l'API"). Conformément à la bonne tradition des propositions précédentes, veuillez énumérer toutes les utilisations alternatives connues des alias de type sous la sous-section « impact » . Cela devrait aussi avoir l'avantage d'alimenter l'imagination des gens pour inventer d'autres usages alternatifs possibles et les mettre en lumière dans la discussion actuelle. Dans l'état actuel des choses, la proposition semble prétendre que les auteurs ignorent totalement les autres utilisations possibles des alias de type. De plus, en ce qui concerne la réexportation, Rust/OCaml peut avoir une certaine expérience de la façon dont cela fonctionne pour eux.

Question supplémentaire : veuillez préciser si les alias de type permettraient d'ajouter des méthodes au type dans le nouveau package (sans doute en cassant l'encapsulation) ou non ? De plus, le nouveau package aurait-il accès aux champs privés des anciennes structures, ou non ?

Question supplémentaire : veuillez préciser si les alias de type permettraient d'ajouter des méthodes au type dans le nouveau package (sans doute en cassant l'encapsulation) ou non ? De plus, le nouveau package aurait-il accès aux champs privés des anciennes structures, ou non ?

Un alias est juste un autre nom pour un type. Cela ne change pas le package du type. Donc non à vos deux questions (sauf si nouveau package == ancien package).

@akavel Pour l'instant, il n'y a aucune proposition du tout. Mais nous connaissons deux possibilités intéressantes qui se sont présentées lors des essais d'alias Go 1.8.

  1. Les alias (ou simplement les alias de type) permettraient de créer des remplacements instantanés qui étendent d'autres packages. Par exemple, consultez https://go-review.googlesource.com/#/c/32145/ , en particulier l'explication dans le message de validation.

  2. Les alias (ou simplement les alias de type) permettraient de structurer un package avec une petite surface d'API mais une grande implémentation en tant que collection de packages pour une meilleure structure interne tout en ne présentant qu'un seul package à importer et à utiliser par les clients. Il y a un exemple quelque peu abstrait décrit sur https://github.com/golang/go/issues/16339#issuecomment -232813695.

L'objectif sous-jacent des alias est excellent, mais il semble toujours que nous ne soyons pas tout à fait honnêtes quant à l'objectif de refactoriser le code, bien qu'il s'agisse de la principale motivation de la fonctionnalité. Certaines des propositions suggèrent de verrouiller le nom, et je n'ai pas encore vu que les types changent généralement de surface avec de telles refactorisations. Même l'exemple de os.Error => error souvent mentionné autour des alias ignore le fait que os.Error avait une méthode String et non Error . Si nous déplacions simplement le type et le renommions, tout le code de gestion des erreurs serait brisé de toute façon. C'est courant lors des refactorisations. Les anciennes méthodes sont renommées, déplacées, supprimées, et nous ne voulons pas qu'elles soient dans le nouveau type car cela préserverait l'incompatibilité avec le nouveau code.

Pour aider, voici une idée de départ : et si on regardait le problème en termes d'adaptateurs, plutôt que d'alias ? Un adaptateur donnerait à un type existant un nom alternatif _et interface_, et il peut être utilisé sans fioritures dans des endroits où le type d'origine a déjà été vu. L'adaptateur devrait définir explicitement les méthodes qu'il prend en charge, plutôt que de supposer que la même interface du type adapté sous-jacent est présente. Cela ressemblerait beaucoup au comportement type foo bar existant, mais avec une sémantique supplémentaire.

io.ByteBuffer

Par exemple, voici un exemple de squelette abordant le cas io.ByteBuffer , en utilisant pour le moment le mot-clé "adapts" temporaire :

type ByteBuffer adapts bytes.Buffer

func (old *ByteBuffer) Write(b []byte) (n int, err error) {
        buf := (*bytes.Buffer)(old)
        return buf.Write(b)
}

(... etc ...)

Donc, avec cet adaptateur en place, ce code serait tout valide :

func newfunc(b *bytes.Buffer) { ... }
func oldfunc(b *io.ByteBuffer) { ... }

func main() {
        var newvar bytes.Buffer
        var oldvar io.BytesBuffer

        // New code using the new type obviously just works.
        newfunc(&newvar)

        // New code using the old type receive the underlying value that was adapted.
        newfunc(&oldvar)

        // Old code using the old type receive the adapted value unchanged.
        oldfunc(&oldvar)

        // Old code gets new variable adapted on the way in. 
        oldfunc(&newvar)
}

Les interfaces de newfunc et oldfunc sont compatibles. Les deux acceptent en fait *bytes.Buffer , avec oldfunc adaptant à *io.BytesBuffer sur le chemin. Le même concept fonctionne pour les devoirs, les résultats, etc.

os.Erreur

La même logique devrait probablement fonctionner sur l'interface, bien que l'implémentation du compilateur soit un peu plus délicate. Voici un exemple pour os.Error => error , qui gère le fait que la méthode a été renommée :

package os

type Error adapts error

func (e Error) String() string { return error(e).Error() }

Ce cas nécessite cependant une réflexion plus approfondie, car des méthodes telles que :

func (v *T) Read(b []byte) (int, os.Error) { ... }`

Retournera un type qui a une méthode String , donc nous voudrions généralement nous adapter dans la direction opposée afin que le code puisse être progressivement corrigé.

_MISE À JOUR : Nécessite une réflexion plus approfondie._

Problème d'intégration

En ce qui concerne le bogue d'intégration qui a fait sortir la fonctionnalité de la version 1.8, le résultat est un peu plus clair avec les adaptateurs, car ce ne sont pas seulement de nouveaux noms pour la même chose : si l'adaptateur est intégré, le nom de champ utilisé est celui de l'adaptateur donc l'ancienne logique continue de fonctionner et l'accès au champ utilisera l'interface de l'adaptateur à moins qu'il ne soit explicitement remis dans un contexte qui prend le type sous-jacent. Si le type non adapté est intégré, l'habituel se produit.

kubernetes, docker

Les problèmes énoncés dans le message semblent être des variantes des problèmes ci-dessus et résolus par la proposition.

vars, const

Cela n'aurait pas beaucoup de sens d'adapter des variables ou des constantes dans ce scénario, car nous ne pouvons pas vraiment leur associer directement des méthodes. Ce sont leurs types qui seraient adaptés ou non.

godoc

Nous serions explicites sur le fait qu'il s'agit d'un adaptateur et montrerions la documentation correspondante comme d'habitude, car il contient une interface indépendante de l'objet adapté.

syntaxe

Veuillez choisir quelque chose de gentil. ;)

@iainmerrick @zombiezen

Devrions-nous également déduire automatiquement qu'un type aliasé est hérité et doit être remplacé par le nouveau type ? Si nous appliquons golint, godoc et des outils similaires pour visualiser l'ancien type comme obsolète, cela limiterait très considérablement l'abus d'alias de type. Et le dernier problème d'utilisation abusive de la fonction d'alias serait résolu.

Deux constats :

1. La sémantique des références de type dépend du cas d'utilisation de la refactorisation pris en charge

La proposition de Gustavo montre qu'il faut encore travailler sur le cas d'utilisation des références de type et la sémantique qui en résulte.

La nouvelle proposition de Ross inclut une nouvelle syntaxe type OldAPI = newpkg.newAPI . Mais quelle est la sémantique ? Est-il impossible d'étendre OldAPI avec des méthodes ou des champs publics hérités ? En supposant que oui comme réponse qui nécessite que newAPI prenne en charge toutes les méthodes et tous les champs publics de OldAPI pour maintenir la compatibilité. Veuillez noter que tout code dans le package avec OldAPI qui repose sur des champs et des méthodes privés doit être réécrit pour n'utiliser que la newAPI publique en supposant que la modification des contraintes de visibilité des packages n'est pas envisageable.

L'autre chemin serait de permettre à des méthodes supplémentaires d'être définies pour OldAPI. Cela pourrait alléger le fardeau de NewAPI de fournir toutes les anciennes méthodes publiques. Mais cela ferait de OldAPI un type différent de NewAPI. Une certaine forme d'assignabilité entre les valeurs des deux types doit être maintenue, mais les règles deviendraient complexes. Permettre l'ajout de champs entraînerait plus de complexité.

2. Le package avec NewAPI ne peut pas importer de package avec OldAPI

La redéfinition de l'OldAPI nécessite que le package O contenant la définition de l'OldAPI importe le package N avec la NewAPI. Cela implique que le package N ne peut pas importer O. C'est peut-être tellement évident qu'il n'a pas été mentionné, mais cela me semble une contrainte importante pour le cas d'utilisation du refactoring.

Mise à jour : le package N ne peut avoir aucune dépendance sur le package O. Par exemple, il ne peut pas importer un package qui importe O.

@niemeyer Des changements comme renommer une méthode sont déjà progressivement possibles : a) Ajouter la nouvelle méthode, appeler l'ancienne sous le capot (ou vice versa), b) changer progressivement tous les utilisateurs vers la nouvelle méthode, c) supprimer l'ancienne méthode. Vous pouvez combiner cela avec un alias de type. La raison pour laquelle cela se concentre sur le déplacement de type est que c'est la seule chose identifiée, qui n'est pas encore possible. Tous les autres changements identifiés sont possibles, même s'ils peuvent passer par plusieurs étapes (par exemple changer le jeu d'arguments d'une méthode sans la renommer). Je pense que choisir un correctif avec moins de surface (moins de choses à comprendre) est préférable.

@rakyll Personnellement, si je considérais les alias utiles pour quelque chose de non-refactoring (comme les packages wrapper, que je trouve un excellent cas d'utilisation), je les utiliserais simplement, les avertissements de dépréciation soient damnés. Je serais énervé contre quiconque les a paralysés artificiellement et les a rendus déroutants pour mes utilisateurs, mais je ne serais pas découragé.

Je pense qu'à un moment donné, il faut débattre pour savoir si nous considérons réellement les packages wrapper, les importations publiques de protobuf ou l'exposition des API de packages internes comme une si mauvaise chose (et je ne sais pas comment débattre au mieux de quelque chose d'aussi subjectif sans qu'un côté ne répète encore et encore qu'ils sont illisibles et l'autre dit "non ils ne le sont pas". Il n'y a pas beaucoup d'argument objectif à avoir ici, il me semble).

Je pense au moins (évidemment) qu'ils sont une bonne chose et je suis également d'avis qu'ajouter une fonctionnalité de langage et la restreindre artificiellement à un seul cas d'utilisation est une mauvaise chose; un langage orthogonal et bien conçu vous permet d'en faire le plus possible avec le moins de fonctionnalités possible. Vous voulez que vos fonctionnalités étendent autant que possible "l'espace vectoriel étendu des programmes possibles", donc ajouter une fonctionnalité qui n'ajoute qu'un seul point à l'espace me semble étrange.

J'aimerais qu'un autre cas d'utilisation légèrement différent soit pris en compte lors du développement de toute proposition d'alias de type.

Bien que le principal cas d'utilisation dont nous discutons dans ce numéro soit le type _remplacement_, les alias de type seraient également très utiles pour sevrer un corps de code d'une dépendance à un type.

Par exemple, supposons qu'un type s'avère "instable" (c'est-à-dire qu'il continue d'être modifié, peut-être de manière incompatible). Ensuite, certains de ses utilisateurs voudront peut-être migrer vers un type de remplacement "stable". Je pense au développement sur github etc. où les propriétaires d'un type et ses utilisateurs ne travaillent pas forcément en étroite collaboration ou ne sont pas d'accord sur l'objectif de stabilité.

D'autres exemples seraient lorsqu'un seul type est la seule chose qui empêche la suppression d'une dépendance sur un paquet volumineux ou problématique, par exemple lorsqu'une incompatibilité de licence a été découverte.

Le processus ici serait donc :

  1. Définir l'alias de type
  2. Modifiez le corps de code approprié pour utiliser l'alias de type
  3. Remplacez l'alias de type par une définition de type.

A la fin de ce processus, il y aurait deux types indépendants qui seraient libres d'évoluer dans leurs propres directions.

Notez que dans ce cas d'utilisation :

  • il n'y a aucune possibilité de modifier le package contenant la définition de type d'origine pour y ajouter un alias de type (puisqu'il est peu probable que les propriétaires soient d'accord avec cela)
  • le type d'origine n'est pas déprécié (bien qu'il puisse être considéré comme tel dans le corps du code en train d'être "sevré" du type).

@Merovius Au moment où vous supprimez ou renommez l'ancienne méthode, vous tuez tous les clients qui l'utilisaient, à la fois. Si vous êtes prêt à le faire, tout l'exercice non trivial consistant à ajouter une fonctionnalité de langue pour empêcher la casse d'un seul coup est sans objet. Autant dire exactement la même chose pour déplacer le code : il suffit de renommer le type sur chaque site d'appel à la fois. Terminé. Les deux actions sont simplement des renommages atomiques, qui ont en commun le fait qu'elles supposent un accès complet à chaque ligne de code dans les sites d'appel. C'est peut-être le cas pour Google, mais en tant que mainteneur de grandes applications et bibliothèques open source, ce n'est pas le monde dans lequel je vis.

Dans la plupart des cas, je trouve cette critique injuste, car l'équipe Go fait de grands efforts pour rendre le projet inclusif aux parties externes, mais au moment où vous supposez que vous avez accès à chaque ligne de code qui appelle un package donné, c'est un mur jardin qui ne correspond pas au contexte d'une communauté open source. L'ajout d'une fonctionnalité de refactorisation au niveau du langage qui ne fonctionne qu'à l'intérieur des jardins clos serait pour le moins atypique.

@niemeyer, je n'ai apparemment pas été clair. Je ne préconisais en aucun cas la suppression de l'ancienne API, je soulignais simplement que tout workflow que nous souhaitons activer avec des alias de type est déjà possible avec des méthodes de renommage (que ce soit en même temps ou non). Alors, peu importe ce que vous voulez faire, pour

  1. Ajouter une nouvelle API, interchangeable avec l'ancienne API
  2. Faites passer progressivement les consommateurs à la nouvelle API
    3a. Une fois que tout est migré ou que la période d'obsolescence est terminée, supprimez l'ancienne API
    3b. Fournir une stabilité indéfinie en conservant les deux API pour toujours (voir par exemple cette partie de l'article )

Vous semblez vous disputer pour faire 3a contre 3b. Mais ce que je soulignais, que 1. est déjà possible pour les noms de méthode mais pas possible pour les types, c'est de cela qu'il s'agit.

Cependant, je me rends compte maintenant que je pense que je vous ai mal compris :) Vous avez peut-être souligné que os.Error sont des définitions d'interface différentes, donc le mouvement ne se déroule pas vraiment. Je pense que c'est vrai; si vous interdisez de supprimer les API, les alias de type ne permettraient pas de renommer les méthodes des types d'interface.

Peut-être pouvez-vous me clarifier quelque chose sur votre idée d'adaptateur : cela ne permettrait-il pas également d'utiliser (par exemple, dans le cas os.Error) n'importe quel fmt.Stringer comme os.Error ?

En tout cas, l'idée de l'adaptateur semble mériter d'être développée, même si je suis un peu sceptique à son sujet. Mais avoir un moyen de refactoriser progressivement les interfaces sans casser les implémenteurs et/ou les consommateurs possibles est un bon objectif.

@niemeyer Oui, vous

Je suis d'accord que s'il y avait une solution générale qui gérait les deux types de changements, ce serait formidable. Je ne vois pas quelle est cette solution. En particulier, je ne comprends pas comment les commutateurs de type fonctionnent avec les adaptateurs que vous avez décrits : la valeur est-elle en quelque sorte automatiquement convertie pendant le changement de type ? Et la réflexion ? Le fait de n'avoir qu'un seul type avec deux noms évite de nombreux problèmes liés au fait d'avoir deux types qui se convertissent automatiquement dans les deux sens.

@rsc Oui, l'adaptateur serait systématiquement converti automatiquement dans toutes les situations, donc les commutateurs de type ne seraient pas différents. Nous interdirons les commutateurs de type contenant à la fois l'adaptateur et son type sous-jacent, car cela serait ambigu. Il me manque peut-être quelque chose, mais je ne vois pas encore de problème de réflexion, car chaque contexte de code doit nécessairement utiliser explicitement le type adapté ou son type sous-jacent. Tout comme aujourd'hui, nous ne pouvons pas entrer dans un interface{} sans savoir comment nous y sommes arrivés, si cela a du sens.

@Merovius Mes deux commentaires ci-dessus abordent précisément les points que vous soulevez toujours. Si vous déplacez un type aujourd'hui, vous cassez du code qui doit être corrigé. Si vous renommez une méthode, vous cassez du code qui doit être corrigé. Si vous supprimez une méthode, modifiez ses arguments, vous cassez le code qui doit être corrigé. Lors de la refactorisation du code dans l'un de ces cas, les correctifs doivent être effectués de manière atomique avec la rupture dans chaque site d'appel pour que les choses continuent de fonctionner. Permettre au type d'être déplacé mais complètement intact est un cas très limité de refactorisation, dont l'IMO ne justifie pas une fonctionnalité de langage.

@niemeyer Cela .(interface{String() string}) vs .(interface{Error() string}) ou tout élément spécifique de l'interface modifié ? La vérification doit-elle prendre en compte les deux types sous-jacents possibles d'une manière ou d'une autre ?

@niemeyer Non. Renommer une méthode est possible de manière non atomique. par exemple pour déplacer une méthode de A.Foo à A.Bar , faites

  1. Ajouter la méthode A.Bar comme wrapper autour de A.Foo
  2. Migrer les utilisateurs pour n'appeler que A.Bar via un nombre arbitraire de commits
  3. Supprimez A.Foo , ou ne le faites pas, selon si vous êtes prêt à appliquer une dépréciation.

Changer les arguments des fonctions est possible de manière non atomique. par exemple pour ajouter un paramètre x int à un func Foo() , faites

  1. Ajouter func FooWithInt(x int) { Foo(); // use x somehow; }
  2. Migrer les utilisateurs pour ajouter le paramètre via un nombre arbitraire de commits
  3. Si vous n'êtes pas prêt à appliquer une dépréciation (ou si vous n'êtes pas dérangé par le WithInt), vous avez terminé. Sinon, modifiez Foo en func Foo(x int) { FooWithInt(x) } .
  4. Migrez les utilisateurs avec s/FooWithInt/Foo/g via un nombre arbitraire de commits.
  5. Supprimer FooWithInt .

La même chose fonctionne pour à peu près tous les cas, à l' exception des types mobiles (et, à proprement parler, vars). l'atomicité n'est pas requise. Soit vous rompez la compatibilité lors de l'application de la dépréciation, soit vous ne le faites pas, mais c'est complètement orthogonal à l'atomicité. La possibilité d'utiliser deux noms différents pour faire référence à la même chose est ce qui vous permet de contourner l'atomicité lorsque vous effectuez des modifications fondamentalement arbitraires et vous avez cette capacité pour tous les cas, à l'exception des types. Oui, pour faire un mouvement réel, au lieu d'un amendement, vous devez être prêt à appliquer la dépréciation (donc briser la construction de code potentiellement inconnu, ce qui signifie que cela nécessite une annonce large et opportune). Mais même si vous ne l'êtes pas, la possibilité d'augmenter les API avec un nom plus pratique ou un autre emballage utile (voir x/image/draw) dépend également de la capacité de faire référence à l'ancien par le nouveau nom et vice-versa.

La différence entre déplacer des types aujourd'hui et renommer une fonction aujourd'hui est que dans le premier cas, vous avez en fait besoin d'un changement atomique, alors que pour le second, vous pouvez effectuer le changement progressivement, via des dépôts et des commits indépendants. Pas comme un "Je vais faire un commit qui fait s/Foo/Bar/", mais il y a un processus pour le faire.

De toute façon. Je ne sais pas où nous sommes, apparemment, en train de nous parler. Je trouve le document de @rsc assez clair pour transmettre mon POV et ne comprends pas vraiment le vôtre :)

@rsc Je peux voir deux réponses raisonnables. Le simple que l'interface porte le type qui est entré, adaptateur ou autre, et la sémantique habituelle s'applique lors de l'assertion d'interface. L'autre est que la valeur peut ne pas être adaptée si elle ne satisfait pas l'interface mais que la valeur sous-jacente le fait. Le premier est plus simple et peut-être suffisant pour les cas d'utilisation de refactorisation que nous avons en tête, tandis que le dernier est peut-être plus cohérent avec l'idée que nous pouvons également l'affirmer par type au type sous-jacent.

@Merovius Bien sûr, il est possible de renommer une méthode tant que _vous ne la renommez pas réellement_ et forcez les sites d'appels à utiliser une nouvelle API à la place. De même, le déplacement d'un type est possible tant que _vous ne le déplacez pas réellement_ et forcez les sites d'appels à utiliser une nouvelle API à la place. Nous avons tous fait ces deux choses pendant des années pour préserver le fonctionnement de l'ancien code.

@niemeyer Mais encore une fois : pour les types, vous ne pouvez même pas ajouter des choses de manière décente. Voir x/image/dessiner. Et tout le monde n'a peut-être pas une vision aussi absolue de la stabilité ; Moi-même, je suis d'accord pour dire "dans 6,12,… mois, $function,$type,… va disparaître, assurez-vous que vous en êtes éloigné à ce moment-là", puis cassez simplement le code non maintenu qui ne fonctionne pas parviennent à suivre cet avis de dépréciation (si quelqu'un pense avoir besoin d'un support à long terme pour les API, il peut sûrement trouver quelqu'un à payer pour le fournir). Je dirais même que la plupart des gens n'ont pas cette vision absolue de la stabilité ; voir la récente poussée pour les versions sémantiques, ce qui n'a vraiment de sens que si vous voulez avoir la possibilité de rompre la compatibilité. Et le doc explique très bien comment, même dans ce cas, vous profiteriez toujours de la possibilité d'avoir des réparations progressives et comment cela peut atténuer, sinon résoudre essentiellement le problème de la dépendance aux diamants.

Vous pouvez ignorer la plupart des cas d'utilisation d'alias pour des réparations progressives, car votre position sur la stabilité est absolue. Mais je dirais que pour la plupart de la communauté du go, c'est différent, qu'il y a un besoin pour les casses et qu'il est utile de les rendre aussi fluides que possible lorsqu'elles se produisent.

@niemeyer @rsc @Merovius J'ai suivi votre discussion (et toute la discussion) et j'aimerais claquer ouvertement ce message en plein milieu de celui-ci.

Plus nous itérons sur le problème, plus nous nous rapprochons d'une certaine forme de sémantique de covariance étendue. Alors, voici une pensée : nous avons déjà une sémantique de sous-type ("is-a") définie à partir des types concrets vers les interfaces et parmi les interfaces. Ma proposition est de rendre les interfaces covariantes récursivement (selon les règles de variance actuelles) jusqu'aux arguments de leurs méthodes.

Cela ne résout pas le problème pour tous les packages actuels. Mais cela peut résoudre le problème pour tous les futurs packages, encore à écrire, dans la mesure où les "parties mobiles" de l'API peuvent être des interfaces (cela encourage également une bonne conception).

Je pense que nous pouvons résoudre toutes les exigences en utilisant (ab) des interfaces de cette manière. Sommes-nous en train de casser le Go 1.0 ? Je ne sais pas mais je pense que non.

@thwd Je pense que vous devez définir plus précisément ce que vous entendez par "rendre les interfaces covariantes récursivement". Habituellement, dans le sous-typage, les arguments de méthode doivent changer de manière contravariante et les résultats de manière covariante. De plus, d'après ce que vous dites, cela ne résoudrait aucun problème existant avec les types concrets (sans interface).

@thwd Je ne suis pas d'accord, que les interfaces (même covariantes) sont une bonne solution à l'un de ces problèmes (uniquement pour des cas très spécifiques). Pour en faire un, vous auriez besoin de faire de tout dans votre API une interface (car vous ne savez jamais ce que vous pourriez vouloir déplacer/changer à un moment donné), y compris vars/consts/funcs/… et je ne pense pas à tout ça, c'est du bon design (j'ai vu ça en java. Ça m'agace). Si quelque chose est une structure, faites-en simplement une structure. Tout le reste ajoute simplement une surcharge syntaxique étrange dans votre package et chaque dépendance inverse pour pratiquement aucun avantage. C'est aussi la seule façon de rester sain d'esprit lorsque vous commencez ; commencez simplement et passez à quelque chose de plus général plus tard. Beaucoup de complications dans l'API que j'ai vues jusqu'à présent proviennent de personnes qui réfléchissent trop à la conception de l'API et planifient beaucoup plus de généralité qu'il ne sera jamais nécessaire. Et puis, dans 80% (ce chiffre est un mensonge évident) des cas, il ne se passe rien du tout, car il n'y a pas de "conception d'API propre".

(pour être clair : je ne dis pas que les interfaces covariantes ne sont pas une bonne idée. Je dis juste qu'elles ne sont pas une bonne solution à ces problèmes)

Pour ajouter au point de

package foo

type Authority struct {
  Host string
  Port int
}

Au fil du temps, le paquet foo grandit et finit par gagner plus de responsabilités (et de taille de code) que quelqu'un qui a juste besoin du type Authority veut vraiment. Donc, avoir un moyen de créer un package fooauthority qui ne contient que Authority et que les utilisateurs existants de foo.Authority fonctionnent toujours est un cas d'utilisation souhaitable. Notez que toute solution qui ne considère que les types d'interface n'aiderait pas ici.

@Merovius Votre dernier commentaire a été entièrement subjectif et s'adresse à moi personnellement au lieu de ma proposition. Cela ne se terminera pas bien, alors je vais arrêter cette ligne de discussion ici.

@griesemer @Merovius Je suis d'accord avec vous deux. Pour boucler la boucle, nous pouvons donc convenir que la discussion jusqu'à présent nous a conduit à une certaine notion de sous-types/covariance. En outre, toute implémentation de celui-ci ne devrait entraîner aucune indirection d'exécution. C'est un peu ce que proposait @niemeyer (si je l'ai bien compris). Mais j'aimerais lire plus d'idées. Je vais réfléchir au problème aussi.

@niemeyer Il n'y avait rien de _ad hominem_ dans les commentaires de @Merovius . Son affirmation selon laquelle "votre position sur la stabilité est absolue" est une observation sur votre position, pas sur vous, et est une déduction raisonnable de certaines de vos déclarations, comme

Au moment où vous supprimez ou renommez l'ancienne méthode, vous tuez d'un coup tous les clients qui l'utilisaient.

et

Bien sûr, il est possible de renommer une méthode tant que vous ne la renommez pas et forcez les sites d'appels à utiliser une nouvelle API à la place. De même, le déplacement d'un type est possible tant que vous ne le déplacez pas réellement et forcez les sites d'appels à utiliser une nouvelle API à la place. Nous avons tous fait ces deux choses pendant des années pour préserver le fonctionnement de l'ancien code.

J'ai eu la même impression que Merovius à partir de ces déclarations - que vous n'êtes pas disposé à déprécier quelque chose pendant un certain temps, puis à le supprimer éventuellement ; que vous vous engagez à faire fonctionner le code dans la nature indéfiniment ; que "votre position sur la stabilité est absolue". (Et pour éviter d'autres malentendus, j'utilise « vous » pour faire référence à vos idées, pas à votre personnalité.)

@niemeyer La adapts que vous proposez semble étroitement liée à instance des classes de types Haskell. En traduisant grossièrement cela en Go, cela pourrait ressembler à quelque chose comme :

package os

type Error interface {
  String() string
}

instance error Error (
  func (e error) String() string { return e.Error() }
)

Malheureusement (comme le note @zombiezen ), il n'est pas clair en quoi cela aiderait pour les types sans interface.

Il n'est pas non plus évident pour moi de savoir comment il interagirait avec les types de fonction (arguments et valeurs de retour); par exemple, comment la sémantique de adapts aiderait-elle à migrer Context vers la bibliothèque standard ?

J'ai eu la même impression que Merovius à partir de ces déclarations - que vous n'êtes pas disposé à déprécier quelque chose pendant un certain temps

@jba Ce sont des faits absolus, pas des opinions absolues. Si vous supprimez une méthode ou un type, le code Go qui l'utilise se rompt, ces modifications doivent donc être effectuées de manière atomique. Ma proposition, cependant, concerne la refactorisation progressive du code, qui est le sujet ici et implique la dépréciation. Ce processus de dépréciation, cependant, n'est pas une question de sympathie. J'ai plusieurs packages Go publics avec des milliers de dépendances dans la nature chacun et plusieurs API indépendantes en raison de cette évolution progressive. Lorsque nous cassons une API, il est bon de faire de tels cassages par lots, au lieu de les diffuser, si nous nous attendons à ne pas rendre les gens fous. À moins, bien sûr, que vous viviez dans un jardin clos et que vous puissiez contacter chaque site d'appel pour le réparer. Mais je me répète... tout cela peut être lu dans la proposition originale ci-dessus d'une manière plus articulée.

@Merovius

Personnellement, si je considérais les alias utiles pour quelque chose de non-refactoring (comme les packages wrapper, que je trouve un excellent cas d'utilisation), je les utiliserais simplement, les avertissements de dépréciation soient damnés.

Nous maintenons des packages avec un nombre extrêmement important d'API nouvelles et obsolètes et le fait d'avoir des alias sans explication claire de l'état de l'ancien type (aliasé) n'aidera pas à la réparation progressive du code et ne fera que contribuer à l'écrasante surface de l'API. Je suis d'accord avec @niemeyer que notre solution doit répondre aux exigences d'une communauté de développeurs distribués qui n'a actuellement aucun autre signal que le texte godoc de forme libre indiquant qu'une API est "obsolète". L'ajout d'une fonctionnalité de langage pour aider à déprécier les anciens types est le sujet de ce fil, donc cela conduit naturellement à se demander quel est l'état de l'ancien type (aliasé).

J'aimerais discuter de l'alias de type sous un thème différent, comme fournir une extension à un type ou des packages partiels, mais pas sur ce fil. Ce sujet lui-même présente divers problèmes spécifiques à l'encapsulation à traiter avant toute considération.

Un opérateur spécifique ou impliquant que l'alias saisi est quelque peu remplacé pourrait être sain pour communiquer aux utilisateurs qu'ils doivent changer. Une telle différentiabilité permettrait aux outils de signaler automatiquement les API remplacées.

Pour être clair, la politique de dépréciation n'est techniquement pas possible pour les types en dehors de la bibliothèque standard. Un type n'est ancien que du point de vue d'un package d'alias. Étant donné que nous ne pouvons jamais appliquer cela dans l'écosystème, j'aimerais toujours que les alias de bibliothèque standard impliquent strictement la dépréciation (indiqué par des avis de dépréciation appropriés).

Je suggère également que nous standardisions la notion de dépréciation dans une discussion parallèle et que nous les soutenions dans nos outils de base (golint, godoc, etc.). L'absence d'avis de dépréciation est le plus gros problème de l'écosystème Go et est plus répandu que le problème de la réparation progressive du code.

@rakyll Je suis sympathique au cas d'utilisation d'avis de dépréciation lisibles par ordinateur; Je m'oppose simplement à la notion a) d'alias étant cela et b) de les émettre en tant qu'avertissements du compilateur.

Pour a), outre le fait que j'aimerais utiliser des alias de manière productive pour d'autres choses que les déplacements, cela ne s'appliquerait également qu'à un très petit ensemble de dépréciations. Par exemple, disons que je voudrais supprimer certains paramètres d'une fonction dans quelques versions ; Je ne peux pas vraiment utiliser d'alias, car la signature de la nouvelle API sera différente, mais je voudrais quand même l'annoncer. Pour b), les avertissements du compilateur IMHO sont universellement mauvais. Je pense que cela est principalement conforme à ce que go fait déjà, donc je ne pense pas que cela nécessite une justification.

Je suis d'accord avec tout ce que vous dites sur les avis de dépréciation. Il existe déjà une syntaxe pour cela, apparemment : #10909, donc la prochaine étape pour la rendre plus utile serait d'améliorer la prise en charge des outils en les mettant en surbrillance dans godoc et d'avoir une vérification qui avertit de leur utilisation (disons go vet, golint ou un outil séparé tous ensemble).

@rakyll Je suis d'accord pour dire que la stdlib devrait commencer par une utilisation conservatrice des alias de type, s'ils étaient introduits.


Barre latérale :

Contexte pour ceux qui ne connaissent pas l'état des commentaires d'obsolescence dans Go et les outils associés, car ils sont plutôt dispersés :

Comme @Merovius le mentionne ci-dessus, il existe une convention standard pour marquer les éléments comme obsolètes, #10909, voir https://blog.golang.org/godoc-documenting-go-code

TL ; DR : créez un paragraphe dans la documentation de l'élément obsolète qui commence par « Deprecated : » et explique ce qu'est le remplacement.

Il existe une proposition acceptée pour que godoc affiche les éléments obsolètes d'une manière plus utile : #17056.

@rakyll a proposé que golint avertisse lorsque des éléments obsolètes sont utilisés : golang/lint#238.


Même si la stdlib adopte une position conservatrice sur l'utilisation des alias dans la stdlib, je ne pense pas que l'existence d'un alias de type devrait impliquer (de quelque manière que ce soit détecté mécaniquement ou visuellement) que l'ancien type est obsolète, même si cela signifie toujours cela dans la pratique.

Cela signifierait l'un des éléments suivants :

  • analyser d'autres paquets stdlib pour voir si un type, non explicitement marqué comme obsolète, est alias ailleurs
  • coder en dur tous les alias stdlib dans des outils automatisés
  • signalant uniquement que l'ancien type est obsolète lorsque vous examinez déjà son remplacement, ce qui ne facilite pas la découverte

Lorsqu'un alias de type est introduit parce que l'ancien type a été déprécié, il doit être traité en marquant l'ancien type déprécié, avec une référence au nouveau type, peu importe.

Cela permet d'avoir un meilleur outillage en lui permettant d'être plus simple et plus général : il n'a pas besoin de cas particulier ni même de connaître les alias de type : il a juste besoin de correspondre à « Deprecated : » dans les commentaires de la doc.

Une politique officielle, peut-être temporaire, selon laquelle un alias dans la stdlib est uniquement destiné à l'obsolescence est bonne, mais elle ne devrait être appliquée qu'avec les commentaires d'obsolescence standard et en interdisant d'autres utilisations pour passer la révision du code.

@niemeyer Ma réponse précédente s'est perdue en raison d'une panne de courant :( hors service :

Mais je me répète..

FWIW, j'ai trouvé votre dernière réponse très utile. Cela m'a convaincu que nous sommes plus d'accord qu'il n'y paraissait auparavant (et que cela peut encore vous sembler). Cependant, il semble toujours y avoir un problème de communication quelque part.

Ma proposition, cependant, concerne la refactorisation progressive du code

Ce n'est pas litigieux, je pense. :) J'ai convenu, dès le début, que votre proposition est une alternative intéressante à considérer pour résoudre le problème. Ce qui m'embrouille, ce sont des déclarations comme celle-ci :

Si vous supprimez une méthode ou un type, le code Go qui l'utilise se rompt, ces modifications doivent donc être effectuées de manière atomique.

Je me demande encore quel est votre raisonnement ici. Je comprends que l'unité d'atomicité est un seul commit. Avec cette hypothèse, je ne comprends tout simplement pas pourquoi vous êtes convaincu que la suppression d'une méthode ou d'un type ne peut pas d'abord se produire dans des commits séparés et arbitrairement nombreux dans les référentiels dépendants, puis, une fois qu'il n'y a plus d'utilisateur apparent (et une ample dépréciation intervalle est passé) la méthode ou le type est supprimé dans un commit en amont (sans rien casser, car plus personne n'en dépend). Je suis d'accord qu'il existe un certain facteur de flou autour des dépendances inverses qui n'adhèrent pas à la dépréciation ou que vous ne pouvez pas trouver (ou corriger raisonnablement), mais qui, pour moi, semble largement indépendant du problème en question ; vous aurez ce problème chaque fois que vous appliquerez un changement décisif et quelle que soit la manière dont vous essayez de l'orchestrer.

Et, pour être juste : la confusion n'est pas vraiment aidée par des phrases comme

À moins, bien sûr, que vous viviez dans un jardin clos et que vous puissiez contacter chaque site d'appel pour le réparer.

Si quelque chose que j'ai dit vous a donné l'impression que c'est le point à partir duquel je discute, j'espère que vous pourrez prendre du recul et peut-être le relire en supposant que je discute complètement de la position de l'open source communauté (si vous ne me croyez pas, n'hésitez pas à consulter mes contributions précédentes sur ce sujet ; je suis toujours le premier à souligner qu'il s'agit de loin plus d'un problème de communauté que d'un problème de monorepo. Les monorepos ont des moyens de contourner ce problème , comme vous l'avez souligné).

De toute façon. Je trouve ça aussi épuisant que toi. J'espère que je comprendrai votre position à un moment donné, cependant.

parler simultanément de si et comment soutenir des choses comme les importations publiques de protobuf ...
Je pense qu'à un moment donné, il faut débattre pour savoir si nous considérons réellement les packages wrapper, les importations publiques de protobuf ou l'exposition des API de packages internes comme une si mauvaise chose

nit : Je ne pense pas que les importations publiques de protobuf doivent être mentionnées comme un cas d'utilisation secondaire spécial. Ils ont été conçus pour une réparation progressive du code, comme mentionné explicitement à la fois dans le document de conception interne et même dans la documentation publique , ils tombent donc déjà sous l'égide des problèmes décrits dans ce numéro. De plus, je pense que les alias de type seraient suffisants pour implémenter les importations publiques de protobuf. (Le compilateur proto génère des vars, mais ils sont logiquement constants, donc "var Enum_name = import.Enum_name" devrait être suffisant.)

@Merovius Merci pour la réponse productive. Je vais essayer de mettre un peu de contexte :

Je me demande encore quel est votre raisonnement ici. Je comprends que l'unité d'atomicité est un seul commit. Avec cette hypothèse, je ne comprends tout simplement pas pourquoi vous êtes convaincu que la suppression d'une méthode ou d'un type ne peut pas d'abord se produire séparément,

Je n'ai jamais dit que cela ne pouvait pas arriver. Permettez-moi de prendre du recul et de reformuler plus clairement.

Nous sommes probablement tous d'accord pour dire que l'objectif final est double : nous voulons un logiciel fonctionnel et nous voulons l'améliorer afin de pouvoir continuer à travailler dessus de manière saine. Certains de ces derniers sont des changements décisifs, ce qui les met en contradiction avec le premier objectif. Il y a donc une tension, ce qui signifie qu'il y a une certaine subjectivité quant à l'endroit où se situe le point idéal. La partie intéressante de notre débat se situe ici.

Un moyen utile de rechercher ce point idéal est de penser aux interventions humaines. C'est-à-dire qu'une fois que vous faites quelque chose qui oblige les gens à modifier manuellement le code pour qu'il continue de fonctionner, l'inertie se produit. Il faut beaucoup de temps pour que la partie pertinente de toutes les bases de code dépendantes passe par ce processus. Nous demandons aux gens occupés de faire des choses que dans la plupart des cas, ils préfèrent ne pas déranger.

Une autre façon de voir ce point idéal est la probabilité que le logiciel fonctionne. Peu importe combien nous demandons aux gens de ne pas utiliser une méthode obsolète. S'il est facilement accessible et qu'il résout leur problème ici et maintenant, la plupart des développeurs l'utiliseront simplement. Le contre-argument commun ici est : _oh, mais alors c'est leur problème quand ça casse !_ Mais cela va à l'encontre de l'objectif affiché : nous voulons un logiciel qui fonctionne, sans avoir raison.

Donc, j'espère que cela donne un meilleur aperçu des raisons pour lesquelles le simple déplacement d'un type semble inutile. Pour que les gens utilisent réellement ce nouveau type dans leur nouveau domicile, nous avons besoin d'une intervention humaine. Lorsque les gens se donnent la peine de modifier manuellement leur code, il est préférable d'avoir une intervention qui _utilise le nouveau type_ au lieu de quelque chose qui changera bientôt sous leurs pieds dans un futur proche. Si nous nous donnons la peine d'ajouter une fonctionnalité de langage pour aider aux refactorisations, idéalement, cela permettrait aux gens de déplacer progressivement leur code _vers ce nouveau type_, pas simplement vers une nouvelle maison, pour les raisons ci-dessus.

Merci pour l'explication. Je pense que je comprends mieux votre position maintenant et que je suis d'accord avec vos hypothèses (à savoir que les gens utiliseront des trucs obsolètes quoi qu'il en soit, il est donc primordial de fournir toute l'aide possible pour les guider vers le remplacement). FWIW, mon plan naïf pour résoudre ce problème (quelle que soit la solution de réparation progressive que nous utiliserons) est un outil de type go-fix pour migrer automatiquement le code package par package pendant la période de dépréciation, mais j'admets librement chapeau Je n'ai pas encore essayé comment et si cela fonctionne dans la pratique.

@niemeyer Je ne pense pas que votre suggestion soit réalisable sans une grave perturbation du système de type Go.

Considérez le dilemme présenté par ce code :

package old
import "new"
type A adapts new.A
func (a A) NewA() {}

package new
type A struct{}
func (a A) OldA() {}

package main
import (
    "new"
    "old"
    "reflect"
)
func main() {
    oldv := reflect.ValueOf(old.A{})
    newv := reflect.ValueOf(new.A{})
    if oldv.Type() == newv.Type() {
        // The two types are equal, therefore they must
        // have exactly the same method set, so either
        // oldv doesn't have the OldA method or newv doesn't
        // have the NewA method - both of which imply a contradiction
        // in the type system.
    } else {
         // The two types are not equal, which means that the
         // old adapted type is not fully compatible with the old
         // one. Any type that includes either new.A or new.B will
         // be incompatible as one of its components will likewise be
         // unequal, so any code that relies on dynamic type checking
         // will fail when presented with the type that's not using the
         // expected version.
    }
 }

L'un des axiomes actuels du package reflect est que si deux types sont identiques, leurs valeurs reflect.Type sont égales. C'est l'un des fondements de l'efficacité de la conversion de type d'exécution de Go. Autant que je sache, il n'y a aucun moyen d'implémenter le mot-clé "adapts" sans le casser.

@rogpeppe Voir la conversation avec @rsc sur la réflexion ci-dessus. Les deux types ne sont pas les mêmes, alors Reflect dirait simplement la vérité et fournirait des détails sur l'adaptateur lorsqu'on lui posait des questions à ce sujet.

@niemeyer Si les deux types ne sont pas les mêmes, alors je ne pense pas que nous puissions prendre en charge la réparation progressive du code tout en déplaçant un type entre les packages. Par exemple, supposons que nous voulions créer un nouveau package d'images qui préserve la compatibilité des types.

On pourrait faire :

package newimage
import "image"
type RGBA adapts image.RGB
func (r *RGBA) At(x, y) color.Color {
    return (*image.Buffer)(r).At(x, y)
}
etc for all the methods

Compte tenu de l'objectif de la réparation progressive du code, je pense qu'il est raisonnable de s'attendre à ce que
une image créée dans le nouveau package est compatible avec les fonctions existantes
qui utilisent l'ancien type d'image.

Supposons pour des raisons d'argument que le package image/png a
été converti pour utiliser newimage mais pas image/jpeg.

Je pense que nous devrions nous attendre à ce que ce code fonctionne :

img, err := png.Decode(r)
if err != nil { ... }
err = jpeg.Encode(w, img, nil)

mais, puisqu'il fait une assertion de type contre *image.RGBA et non *nouvelleimage.RGBA,
il échouera AFAICS, car les types sont différents.

Supposons que le type assert ci-dessus réussisse, que le type soit *image.RGBA
ou pas. Cela briserait l'invariant actuel que :

reflect.TypeOf(x) == reflect.TypeOf(x.(anyStaticType))

C'est-à-dire que l'utilisation d'une assertion de type statique n'affirmerait pas seulement le type statique d'un
valeur, mais parfois cela la changerait réellement.

Disons que nous avons décidé que c'était OK, alors nous aurions probablement aussi besoin
pour permettre de convertir un type adapté à n'importe quelle interface que l'un de ses compatibles
prise en charge des types adaptés, sinon le nouveau ou l'ancien code s'arrêterait
fonctionne lors de la conversion vers des types d'interface compatibles avec le
type qu'ils utilisent.

Cela conduit à une autre situation contradictoire :

// oldInterface is some interface with methods that
// are only supported by the old type.
type oldInterface interface {
    OldMethod()
}
var x = interface{} = newpackage.Type{}
switch x.(type) {
case oldInterface:
    // This would fail because the newpackage.Type
    // does not implement OldMethod, even though we
    // we just supposedly checked that x implements OldMethod.
    reflect.TypeOf(x).Method("OldMethod")
}

Dans l'ensemble, je pense qu'avoir deux types qui sont à la fois identiques mais différents
conduirait à un système de types très difficile à expliquer et à des incompatibilités inattendues
dans du code qui utilise des types dynamiques.

Je soutiens la proposition "type X = Y". C'est simple à expliquer et ne
trop perturber le système de types.

@rogpeppe : Je crois que la suggestion de @niemeyer est de convertir implicitement un type adapté en son type de base, similaire aux suggestions précédentes de @josharian .

Pour que cela fonctionne pour une refactorisation progressive, il faudrait également convertir implicitement les fonctions avec des arguments de types adaptés ; en substance, il faudrait ajouter de la covariance au langage. Ce n'est certainement pas une tâche impossible - de nombreux langages autorisent la covariance, en particulier pour les types ayant la même structure sous-jacente - mais cela ajoute beaucoup de complexité au système de types, en particulier pour les types d'interface .

Cela conduit à des cas limites intéressants, comme vous l'avez noté, mais ils ne sont pas nécessairement « contradictoires » en soi :

type oldInterface interface {
    OldMethod()
}
var x = interface{} = newpackage.Type{}
switch y := x.(type) {
case oldInterface:
    reflect.TypeOf(y).Method("OldMethod")  // ok
    reflect.TypeOf(x).Method("NewMethod")  // ok

    // This would fail because y has been implicitly converted to oldInterface.
    reflect.TypeOf(y).Method("NewMethod")

    // This would fail because accessing OldMethod on newpackage.Type requires
    // a conversion to oldInterface.
    reflect.TypeOf(x).Method("OldMethod")
}
// This would fail because accessing OldMethod on newpackage.Type requires
// a conversion to oldInterface.

Cela me semble encore contradictoire. Le modèle actuel est très simple : une valeur d'interface a un type statique sous-jacent bien défini. Dans le code ci-dessus, nous déduisons quelque chose sur ce type sous-jacent, mais lorsque nous jetons un coup d'œil à la valeur, cela ne ressemble pas à ce que nous avons déduit. Il s'agit d'un changement sérieux (et difficile à expliquer) dans la langue à mon avis.

La discussion ici semble tirer à sa fin. Sur la base d'une suggestion de @egonelbre dans https://github.com/golang/go/issues/16339#issuecomment -247536289, j'ai mis à jour le commentaire du problème d'origine (en haut) pour inclure un résumé lié de la discussion afin loin. Je posterai un nouveau commentaire, comme celui-ci, à chaque fois que j'aurai mis à jour le résumé.

Dans l'ensemble, il semble que le sentiment ici soit pour les alias de type plutôt que pour les alias généralisés. Peut-être que l'idée d'adaptateur de Gustavo déplacera les alias de type, mais peut-être pas. Cela semble un peu complexe pour le moment, même si peut-être qu'à la fin de la discussion, une forme plus simple sera atteinte. Je suggère que la discussion se poursuive encore un peu.

Je ne suis toujours pas convaincu que les variables globales mutables soient "généralement un bug" (et dans les cas où elles sont un bug, le détecteur de course est l'outil de choix pour trouver ce genre de bug). Je demanderais que, si cet argument est utilisé pour justifier l'absence d'une syntaxe extensible, un vet-check soit implémenté qui - disons - vérifie les affectations aux variables globales dans le code qui n'est pas exclusivement accessible par init () ou leurs déclarations. Je penserais naïvement que ce n'est pas particulièrement difficile à mettre en œuvre et que cela ne devrait pas demander beaucoup de travail pour l'exécuter - disons - tous les packages enregistrés sur godoc.org pour voir quels sont les cas d'utilisation des variables globales mutables et si nous le faisons considérer tous les bugs.

(J'aimerais aussi croire que, si go développe des variables globales immuables, elles devraient faire partie des déclarations const, car c'est ce qu'elles sont conceptuellement et parce que ce serait rétrocompatible, mais je reconnais que cela conduira probablement à des complications autour du type d'expressions pouvant être utilisées dans les types de tableaux, par exemple et nécessiteraient plus de réflexion)

Re "Restriction ? Les alias des types de bibliothèque standard ne peuvent être déclarés que dans la bibliothèque standard." -- notamment, cela empêcherait le drop-in usecase pour x/image/draw , un package existant qui a exprimé son intérêt pour l'utilisation d'alias. Je pourrais aussi très bien imaginer, par exemple, des packages de routeurs ou similaires utilisant des alias dans net/http de la même manière ( vagues de mains ).

Je suis également d'accord avec les contre-arguments concernant toutes les restrictions, c'est-à-dire que je suis en faveur de ne pas en avoir.

@Merovius , qu'en est-il des variables globales _exportées_ mutables ? Il est vrai qu'un global non exporté peut convenir puisque tout le code du package sait comment le gérer correctement. Il est moins évident que les globals mutables exportés aient un sens. Nous avons fait cette erreur nous-mêmes un certain nombre de fois dans la bibliothèque standard. Par exemple, il n'existe aucun moyen totalement sûr de mettre à jour runtime.MemProfileRate. Le mieux que vous puissiez faire est de le définir au début de votre programme et d'espérer qu'aucun package que vous avez importé n'ait lancé une goroutine d'initialisation qui pourrait allouer de la mémoire. Vous avez peut-être raison à propos de var vs const, mais nous pouvons laisser cela pour un autre jour.

Bon point sur x/image/draw. Ajoutera au résumé à la prochaine mise à jour.

J'aimerais beaucoup assembler un corpus représentatif de code Go que nous pourrions analyser pour répondre à des questions comme celles que vous soulevez. J'ai commencé à essayer de le faire il y a quelques semaines et j'ai rencontré quelques problèmes. C'est un peu plus de travail qu'il n'y paraît, mais il est très important d'avoir cet ensemble de données, et je pense que nous y arriverons.

@rsc votre présentation GothamGo sur ce sujet a été publiée sur youtube https://www.youtube.com/watch?v=h6Cw9iCDVcU et ferait un bon ajout au premier post.

Dans la section « Quels autres problèmes une proposition d'alias de type doit-elle résoudre ? » section, il serait utile de spécifier que la réponse à « Les méthodes peuvent-elles être définies sur des types nommés par alias ? » est un non catégorique. Je me rends compte que cela va à l'encontre de l'esprit décrété de la section, mais j'ai remarqué que, dans beaucoup de conversations sur les alias, ici et ailleurs, il y a des gens qui rejettent d'emblée le concept car ils pensent que les alias permettraient forcément cela et donc causent problèmes qu'il n'en résout. C'est implicite dans la définition, mais le mentionner explicitement court-circuiterait beaucoup de va-et-vient inutiles. Bien que cela appartienne peut-être à une FAQ sur les alias dans la nouvelle proposition d'alias, cela devrait-il être le résultat de ce fil.

@Merovius toute variable mutable globale de package exportée peut être simulée par les fonctions getter et setter au niveau du package.

Étant donné la version n d'un package p ,

package p
var Global = 0

à la version n+1, les getters et setters peuvent être introduits et la variable dépréciée

package p
//Deprecated: use GetGlobal and SetGlobal.
var Global = 0
func GetGlobal() int {
    return Global
}
func SetGlobal(n int) {
   Global = n
}

et la version n+2 pourrait désexporter Global

package p
var global = 0
func GetGlobal() int {
    return global
}
func SetGlobal(n int) {
   global = n
}

(Exercice laissé au lecteur : vous pourriez aussi envelopper l'accès à global dans un mutex en n+2 et déprécier GetGlobal() au profit du plus idiomatique Global() .)

Ce n'est pas une solution rapide, mais cela réduit le problème de sorte que seuls les alias de fonction (ou leur solution de contournement actuelle) sont strictement nécessaires pour la réparation progressive du code.

@rsc Une utilisation triviale pour les alias que vous avez

@jimmyfrasche Vous avez raison. Je n'aime pas l'idée d'utiliser des getters et des setters (tout comme je n'aime pas les avoir pour les champs de structure) mais votre analyse est, bien sûr, correcte.

Il y a un point à faire sur les utilisations non-réparatrices des alias (par exemple, la création de packages de remplacement instantanés), mais je concède que cela affaiblit le cas des var-alias.

@Merovius était d'accord sur tous les points. Je ne suis pas content non plus mais je dois suivre la logique v☹v

@niemeyer pouvez-vous préciser comment les adaptateurs aideraient à migrer des types où les anciens et les nouveaux ont une méthode avec le même nom mais des signatures différentes. L'ajout d'un argument à une méthode ou la modification du type d'un argument semblent être des évolutions courantes d'une base de code.

@rogpeppe Notez que c'est exactement comme ça que ça se passe aujourd'hui :

type two one

Cela fait one et two des types indépendants, et qu'ils reflètent ou sous un interface{} , c'est ce que vous voyez. Vous pouvez également convertir entre one et two . La proposition d'adaptateur ci-dessus rend simplement cette dernière étape automatique pour les adaptateurs. Vous n'aimez peut-être pas la proposition pour plusieurs raisons, mais il n'y a rien de contradictoire à cela.

@iand Comme dans le cas de type two one , les deux types ont des ensembles de méthodes complètement indépendants, il n'y a donc rien de spécial à faire correspondre les noms. Avant que les anciennes bases de code ne soient migrées, elles continueraient à utiliser l'ancienne signature sous le type précédent (maintenant un adaptateur). Un nouveau code utilisant le nouveau type utiliserait la nouvelle signature. Passer une valeur du nouveau type dans l'ancien code l'adapte automatiquement car le compilateur sait que ce dernier est un adaptateur du premier, et utilise donc l'ensemble de méthodes respectif.

@niemeyer Il semble qu'il y ait beaucoup de complexité derrière ces adaptateurs qui ne soit pas entièrement spécifiée. À ce stade, je pense que la simplicité des alias de type pèse fortement en leur faveur. Je me suis assis pour énumérer toutes les choses qui devront être mises à jour uniquement pour les alias de type, et c'est une très longue liste. La liste serait certainement plus longue pour les adaptateurs, et je ne comprends toujours pas bien tous les détails. J'aimerais suggérer que nous tapions des alias pour l'instant et que nous laissions la décision concernant les adaptateurs relativement plus lourds à une date ultérieure, si vous souhaitez élaborer une proposition complète (mais encore une fois, je suis sceptique quant au fait qu'il n'y ait pas de dragons qui s'y cachent) .

@jimmyfrasche Concernant les méthodes sur les alias, les alias ne permettent certainement pas de contourner les restrictions habituelles de définition de méthode : si un package définit le type T1 = otherpkg.T2, il ne peut pas définir de méthodes sur T1, tout comme il ne peut pas définir de méthodes directement sur otherpkg.T2. Cependant, si un package définit le type T1 = T2 (les deux dans le même package), la réponse est moins claire. Nous pourrions introduire une restriction mais il n'y a pas (encore) de besoin évident pour cela.

Mise à jour du résumé de la discussion de haut niveau . Changements:

  • Ajout d'un lien vers la vidéo GothamGo
  • Ajout de "noms longs abrégés" comme utilisation possible, par @jba.
  • Ajout de x/image/draw comme argument contre la restriction de bibliothèque standard, par @Merovius.
  • Ajout de plus de texte sur les méthodes sur les alias, par @jimmyfrasche.

Doc de conception ajouté : golang.org/design/18130-type-alias

Comme c'était le cas il y a une semaine, il semble toujours y avoir un consensus général pour les alias de type. Robert et moi avons rédigé un document de conception formel, que je viens de vérifier (lien ci-dessus).

Après le processus de proposition , veuillez poster des commentaires de fond sur la proposition _ici_ sur cette question. Orthographe/grammaire/etc peut accéder à la page de révision de code Gerrit https://go-review.googlesource.com/#/c/34592/. Merci.

J'aimerais que l'"Effet sur l'enfouissement" soit reconsidéré. Cela limite l'utilisation des alias de type pour la réparation progressive du code. À savoir, si p1 veut renommer un type type T1 = T2 et que le package p2 intègre p1.T2 dans une structure, ils ne pourront jamais mettre à jour cette définition en p1.T1 , car un importateur p3 peut faire référence à la structure intégrée par son nom. p2 alors impossible de passer à p1.T1 sans casser p3 ; p3 ne peut pas mettre à jour le nom en p1.T1 , sans rompre avec le p2 actuel.

Une solution serait de a) limiter en général toute promesse de compatibilité/période de dépréciation au code qui ne fait pas référence aux champs intégrés par nom, ou b) ajouter une étape de dépréciation distincte, donc p1 ajoute type T1 = T2 et désapprouve T2 , puis p2 désapprouve (disons) s2.T2 par son nom, tous les importateurs de p2 seront réparés pour ne pas le faire, alors p2 fait le changement.

Or, en théorie, le problème peut se reproduire indéfiniment ; p4 peut importer p3 , qui lui-même embarque le type de p2 ; il me semble que p3 doit également avoir une période de dépréciation, pour faire référence au champ deux fois intégré par son nom ? Dans ce cas, la période de dépréciation la plus interne devient infinitésimale ou la période la plus externe devient infinie. Mais même sans considérer le problème comme récursif, il me semblerait que b) serait assez difficile à chronométrer (la période de dépréciation de p2 devrait être entièrement contenue dans la période de dépréciation de p1 . Donc, si T est une "période de dépréciation standard", vous devrez choisir au moins 2T lors du renommage des types, afin que les versions s'alignent).

a) me semble également peu pratique ; par exemple, si un type intègre un *byte.Buffer et que je veux définir ce champ (ou passer ce tampon à une autre fonction), il n'y a tout simplement aucun moyen de le faire, sans y faire référence par son nom (sauf en utilisant des initialiseurs de structure sans noms, ce qui perd également les garanties de compatibilité :) ).

Je comprends l'intérêt d'être compatible avec byte et rune comme alias. Mais, personnellement, je placerais cela au second plan par rapport à la préservation de l'utilité des alias de type pour les réparations progressives. Un exemple (probablement mauvais) d'une idée pour obtenir les deux serait, pour les noms exportés, autoriser l'utilisation de n'importe quel alias pour faire référence à un champ intégré et pour les noms non exportés (intrinsèquement limités au même package, donc sous plus de contrôle de l'auteur ) conserver la sémantique actuellement proposée ? Oui, je n'aime pas cette distinction aussi. Peut-être que quelqu'un a une meilleure idée.

@rsc re méthodes sur un alias

Si vous avez un type S qui est un alias pour le type T, tous deux définis dans le même package, et que vous autorisez la définition de méthodes sur S, que se passe-t-il si T est un alias pour pF défini dans un package différent ? Bien que cela devrait également échouer, il y a des subtilités dans l'application, la mise en œuvre et la lisibilité de la source à considérer (si T est dans un fichier différent de S, il n'est pas immédiatement clair si vous pouvez définir une méthode sur T en regardant le définition de T).

La règle — si vous avez type T = S , alors vous ne pouvez pas déclarer de méthodes sur T — est absolue et il ressort clairement de cette seule ligne dans la source qu'elle s'applique, sans avoir à rechercher la source de S, comme vous le feriez dans le cas de l'alias de l'alias.

De plus, autoriser des méthodes sur un alias de type local brouille la distinction entre un alias de type et une définition de type. Puisque les méthodes seraient de toute façon définies à la fois sur S et T, la restriction selon laquelle elles ne peuvent être écrites que sur une seule ne restreint pas ce qui peut être exprimé. Cela rend les choses plus simples et plus uniformes.

@jimmyfrasche Si nous écrivons type T1 = T2 et que T2 est dans le même package, alors nous déprécions probablement le nom T2. Dans ce cas, nous voulons aussi peu d'occurrences de T2 dans le godoc que possible. Nous aimerions donc déclarer toutes les méthodes comme func (T1) M() .

@jba une modification de godoc pour signaler les méthodes d'un alias comme étant déclarées sur cet alias remplirait cette exigence sans modifier la lisibilité de la source. En général, ce serait bien si godoc affichait l'ensemble complet des méthodes d'un type lorsque l'alias et/ou l'intégration sont impliqués, en particulier lorsque le type provient d'un autre package. Le problème devrait être résolu avec des outils plus intelligents et non plus de sémantique de langage.

@jba Dans ce cas, pourquoi n'inverseriez-vous pas simplement la direction de l'alias ? type T2 = T1 vous permet déjà de définir des méthodes sur T1 avec la même structure de package ; la seule différence est le nom de type signalé par le package reflect , et vous pouvez commencer la migration en fixant les sites d'appel sensibles au nom pour qu'ils soient insensibles au nom avant d'ajouter l'alias.

@jimmyfrasche Extrait du document de proposition :

"Comme T1 n'est qu'une autre façon d'écrire T2, il n'a pas son propre ensemble de déclarations de méthode. Au lieu de cela, l'ensemble de méthodes de T1 est le même que celui de T2. Au moins pour l'essai initial, il n'y a aucune restriction contre les déclarations de méthode utilisant T1 comme un type de récepteur, fourni en utilisant T2 dans la même déclaration serait valable. "

L'utilisation de pF comme type de récepteur de méthode n'est jamais valide.

@mdempsky Je n'ai pas été très clair, mais j'ai dit que c'était invalide.

Ce que je veux dire, c'est qu'il est moins évident de savoir si c'est valide ou non en regardant simplement cette ligne de code spécifique.

Étant donné type S = T , vous devez également examiner T pour vous assurer qu'il ne s'agit pas également d'un alias qui aliase un type dans un autre package. Le seul gain est la complexité.

Toujours interdire les méthodes sur un alias est plus simple et plus facile à lire et vous ne perdez rien. Je n'imagine pas qu'un cas déroutant se présenterait très souvent, mais il n'est pas nécessaire d'introduire la possibilité lorsque vous ne gagnez rien qui ne puisse être mieux traité ailleurs ou par une approche différente mais équivalente.

@Merovius

si p1 veut renommer un type de type T1 = T2 et que le package p2 intègre p1.T2 dans une structure, ils ne pourront jamais mettre à jour cette définition en p1.T1, car un importateur p3 peut faire référence à la structure intégrée par son nom.

Il est possible aujourd'hui de contourner ce problème dans de nombreux cas en remplaçant le champ anonyme par un champ nommé et en transférant explicitement les méthodes. Cependant, cela ne fonctionnerait pas pour les méthodes non exportées.

Une autre option pourrait être d'ajouter une deuxième fonctionnalité pour compenser. Si vous pouviez adopter l'ensemble de méthodes d'un champ sans le rendre anonyme (ou avec un renommage explicite), cela permettrait au nom du champ de rester inchangé même si le type sous-jacent est modifié.

Considérant la déclaration de votre exemple :

package p2

type S struct {
  p1.T2
}

Une caractéristique de compensation pourrait être les "alias de champ", qui suivraient une syntaxe similaire aux alias de type :

package p2

type S struct {
  p1.T1
  T2 = T1  // field T2 is an alias for field T1.
}

var s S  // &s.T2 == &s.T1

Une autre caractéristique compensatrice pourrait être la "délégation", qui adopterait explicitement l'ensemble de méthodes d'un champ anonyme :

package p2

type S struct {
  T2 p1.T1 delegated  // T2 is a field of type T1.
  // The method set of S includes the method set of T1 and forwards those calls to field T2.
}

Je pense que je préfère moi-même les alias de champs, car ils permettraient également un autre type de réparation progressive : renommer les champs d'une structure sans introduire d'alias de pointeur ou de bugs de cohérence.

@Merovius Le problème principal est lorsque le type est renommé par un alias.

Je n'ai pas examiné cela en entier, à peine en passant, juste une pensée aléatoire :

Que se passe-t-il si vous introduisez un alias dans votre package qui le nomme en retour et l'incorporez ?

Je ne sais pas si cela résout quelque chose, mais peut-être que cela fait gagner du temps pour rompre la boucle?

@bcmills Je n'ai pas pensé à cette solution de contournement, merci. Je pense que la mise en garde concernant les méthodes non exportées semble (pour moi) se poser assez rarement dans la pratique pour ne pas influencer mon opinion en général (à moins que je ne la comprenne pas complètement. N'hésitez pas à clarifier, si vous pensez que c'est utile ). Je ne pense pas qu'accumuler plus de changements soit justifié (ou une bonne idée).

@Merovius Plus j'y pense, plus j'aime l'idée des alias de champ.

La transmission explicite des méthodes est fastidieuse même si elles sont exportées, et casse d' autres types de refactorisation (par exemple, ajouter des méthodes au type intégré et s'attendre à ce que le type qui l'intègre continue à satisfaire la même interface). Et renommer les champs de structure s'inscrit également dans le cadre général de l'activation de la réparation progressive du code.

@Merovius

si p1 veut renommer un type de type T1 = T2 et que le package p2 intègre p1.T2 dans une structure, ils ne pourront jamais mettre à jour cette définition en p1.T1, car un importateur p3 peut faire référence à la structure intégrée par son nom. p2 ne peut alors pas passer à p1.T1 sans casser p3 ; p3 ne peut pas mettre à jour le nom en p1.T1, sans rompre avec le p2 actuel.

Si je comprends ton exemple, on a :

package p1

type T2 struct {}
type T1 = T2
package p2

import "p1"

type S struct {
  p1.T2
  F2 string // see below
}

Je pense qu'il ne s'agit que d'un exemple spécifique du cas général où nous souhaitons renommer un champ de structure ; le même problème s'applique si nous voulons renommer S.F2 en S.F1.

Dans ce cas précis, nous pouvons mettre à jour le package p2 pour utiliser la nouvelle API de p1 avec un alias de type local :

package p2

import "p1"

type T2 = p1.T1

type S struct {
  T2
}

Ce n'est bien sûr pas une bonne solution à long terme. Je ne pense pas qu'il y ait un moyen de contourner le fait que p2 devra changer son API exportée pour éliminer le nom T2, cependant, ce qui se déroulera de la même manière que tout changement de nom de champ.

Juste une note concernant le "déplacement des types entre les packages". Cette formulation n'est-elle pas légèrement problématique ?

Autant que je sache, la proposition permet de "se référer" à une définition d'objet qui se trouve dans un autre package via un nouveau nom.

Cela ne déplace pas la définition de l'objet, n'est-ce pas ? (à moins que l'on n'écrive du code en utilisant des alias en premier lieu, auquel cas, l'utilisateur est libre de changer l'emplacement auquel l'alias se réfère, tout comme dans le paquet de dessin).

@atdiar Faire référence à un type dans un autre package peut être utilisé comme une étape pour déplacer le type. Oui, un alias ne déplace pas le type, mais il peut être utilisé comme un outil pour le faire.

@Merovius Faire cela risque de casser la réflexion et les plugins.

@atdiar, je suis désolé, mais je ne comprends pas ce que vous essayez de dire. Avez-vous lu le commentaire original de ce fil, l'article sur les réparations progressives qui y sont liées et la discussion jusqu'à présent ? Si vous essayez d'ajouter un argument jusqu'ici non pris en compte à la discussion, je pense que vous devez être plus clair.

Enfin, une proposition utile et bien rédigée. Nous avons besoin d'alias de type, j'ai de gros problèmes pour créer une seule API sans alias de type, jusqu'à présent, je dois écrire mon code d'une manière que je n'aime pas tellement pour accomplir cela. Cela devrait être inclus sur go v1.8 mais il n'est jamais trop tard, alors allez-y pour 1.9.

@Merovius
Je parle explicitement de "types de déplacement" entre les packages. Il modifie la définition de l'objet. Par exemple, dans pkg reflect, certaines informations sont liées au package dans lequel un objet a été défini.
Si vous déplacez la définition, elle peut se casser.

@kataras il ne s'agit pas vraiment d'une bonne documentation et de bons commentaires, c'est simplement que les définitions de type ne doivent pas être déplacées. Autant j'apprécie la proposition d'alias, autant je crains que les gens pensent qu'ils peuvent faire ça.

@atdiar encore, veuillez lire l'article du commentaire original et la discussion jusqu'à présent. Les types de déménagement et la façon de répondre à vos préoccupations sont la principale préoccupation de ce fil. Si vous pensez que l'article de Russ ne répond pas adéquatement à vos préoccupations, veuillez préciser pourquoi son explication n'est pas satisfaisante. :)

@kataras Bien que personnellement, je sois d'accord, je ne pense pas qu'il soit particulièrement utile d'affirmer simplement à quel point nous trouvons cette fonctionnalité importante. Il doit y avoir un argument constructif pour répondre aux préoccupations des gens. :)

@Merovius J'ai lu le document. Cela ne répond pas à ma question. Je pense avoir été assez explicite. C'est lié au même problème qui nous a dissuadés de mettre en œuvre l'ancienne proposition d'alias.

@atdiar Moi, au moins, je ne comprends pas. Vous dites que déplacer un type casserait des choses ; la proposition porte sur la façon d'éviter de telles ruptures avec une réparation progressive, en utilisant un alias, puis de mettre à jour chaque dépendance inverse jusqu'à ce qu'aucun code n'utilise l'ancien type, puis de supprimer l'ancien type. Je ne vois pas en quoi votre affirmation selon laquelle "la réflexion et les plugins" sont brisés tient sous ces hypothèses. Si vous voulez remettre en question les hypothèses, cela a déjà été discuté.

Je ne vois pas non plus comment l'un des problèmes empêchant les alias d'entrer dans 1.8 se connecte à ce que vous avez dit. Les numéros respectifs, au meilleur de ma connaissance, sont #17746 et #17784. Si vous faites référence au problème d'intégration (qui pourrait être interprété comme étant lié à des ruptures ou à une réflexion, bien que je ne sois pas d'accord), alors cela est traité dans la proposition formelle (bien que, voir ci-dessus, je pense que la solution proposée mérite plus de discussion) et vous devriez être précis sur les raisons pour lesquelles vous ne le croyez pas.

Donc, je suis désolé, mais non, tu n'as pas été assez précis. Avez-vous un numéro de problème pour "le même problème qui nous a dissuadés de mettre en œuvre l'ancienne proposition d'alias" auquel vous faites référence, qui se rapporte à ce que vous avez mentionné jusqu'à présent, pour aider à comprendre ? Pouvez-vous donner un exemple spécifique des cassures dont vous parlez (voir les exemples pour ce upthread ; donner une séquence de packages, des définitions de types et du code et décrire comment il se casse lorsqu'il est transformé comme proposé) ? Si vous voulez que vos préoccupations soient traitées, vous devez d'abord aider les autres à les comprendre.

@Merovius Donc, dans le cas des dépendances transitives où l'une de ces dépendances regarde reflect.Type.PkgPath(), que se passe-t-il ?
C'est le même problème qui se produit dans le problème d'intégration.

@atdiar Je suis désolé, je ne vois pas en quoi il s'agit d'une préoccupation compréhensible, à la lumière de la discussion dans ce fil jusqu'à présent et de l'objet de cette proposition. Je vais maintenant sortir de ce sous-thème particulier et donner à d'autres, qui pourraient mieux comprendre votre objection, l'opportunité d'y répondre.

Permettez-moi de le reformuler de manière concise :

Le problème concerne l'égalité des types étant donné que la définition de type code en dur son propre emplacement.
Étant donné que l'égalité des types peut être et est testée au moment de l'exécution, je ne vois pas en quoi le déplacement de types est si facile à faire.

Je lance simplement un avertissement selon lequel ce cas d'utilisation de "types mobiles" peut potentiellement casser de nombreux packages à l'état sauvage, à distance. Problème similaire avec les plugins.

(de la même manière, changer le type d'un pointeur dans un package casserait beaucoup d'autres packages, si ce parallèle peut rendre les choses plus claires.)

@atdiar Encore

@niemeyer

Cela fait un et deux types indépendants, et qu'ils reflètent ou sous une interface{}, c'est ce que
tu vois. Vous pouvez également convertir entre un et deux. La proposition d'adaptateur ci-dessus ne fait que durer
étape automatique pour les adaptateurs. Vous n'aimez peut-être pas la proposition pour plusieurs raisons, mais il n'y a rien
contradictoire à ce sujet.

Vous ne pouvez pas convertir entre

 func() one

et

func() two

@Merovius Vous ne pouvez pas envisager de changer tous les importateurs d'un package réparé par code qui existent dans la nature. Et je n'ai pas trop envie de commencer à approfondir la gestion des versions de packages ici.

Pour être clair, je ne suis pas contre la proposition d'alias mais la formulation "déplacement de types entre packages" qui implique un cas d'utilisation qui n'est pas encore prouvé sûr.

@jimmyfrasche concernant la prévisibilité de la validité de la méthode sur alias :

Il est déjà vrai que func (t T) M() est parfois valide, parfois invalide. Cela ne revient pas beaucoup parce que les gens ne repoussent pas très souvent ces limites. C'est-à-dire que cela fonctionne bien dans la pratique. https://play.golang.org/p/bci2qnldej. En tout état de cause, cela figure sur la liste des restrictions _possibles_. Comme toutes les restrictions possibles, cela ajoute de la complexité et nous voulons voir des preuves concrètes du monde réel avant d'ajouter cette complexité.

@Merovius , réintégration des noms :

Je reconnais que la situation n'est pas parfaite. Cependant, si j'ai une base de code pleine de références à io.ByteBuffer et que je veux la déplacer vers bytes.Buffer, alors je veux pouvoir introduire

package io
type ByteBuffer = bytes.Buffer

_sans_ mettre à jour les références existantes à io.ByteBuffer. Si tous les endroits où io.ByteBuffer sont intégrés changent automatiquement le nom du champ en Buffer en raison du remplacement d'une définition de type par un alias, alors j'ai brisé le monde et il n'y a pas de réparation progressive. En revanche, si le nom d'un io.ByteBuffer intégré est toujours ByteBuffer, alors les utilisations peuvent être mises à jour une à la fois dans leurs propres réparations progressives (éventuellement devant effectuer plusieurs étapes ; là encore, ce n'est pas idéal).

Nous en avons discuté assez longuement dans #17746. J'étais à l'origine du côté du nom d'un alias io.ByteBuffer intégré étant Buffer, mais l'argument ci-dessus m'a convaincu que j'avais tort. @jimmyfrasche en particulier a fait de bons arguments sur le fait que le code ne change pas en fonction de la définition de la chose intégrée. Je ne pense pas qu'il soit tenable d'interdire complètement les alias intégrés.

Notez qu'il existe une solution de contournement dans p2 dans votre exemple. Si p2 veut vraiment un champ intégré nommé ByteBuffer sans faire référence à io.ByteBuffer, il peut définir :

type ByteBuffer = bytes.Buffer

puis incorporer un ByteBuffer (c'est-à-dire un p2.ByteBuffer) au lieu d'un io.ByteBuffer. Ce n'est pas parfait non plus, mais cela signifie que les réparations peuvent continuer.

Il est certain que ce n'est pas parfait et que les renommages de champs en général ne sont pas abordés par cette proposition. Il se peut que l'incorporation ne soit pas sensible au nom sous-jacent, qu'il y ait une sorte de syntaxe pour 'embed X as name N'. Il se peut aussi que nous devions ajouter des alias de champs plus tard. Les deux semblent être des idées raisonnables a priori et les deux devraient probablement être des propositions distinctes, évaluées ultérieurement sur la base de preuves réelles d'un besoin. Si les alias de type nous aident à en arriver au point où le manque d'alias de champ est le prochain grand obstacle aux refactorisations à grande échelle, ce sera un progrès !

(/cc @neild et @bcmills)

@atdiar , oui, il est vrai que reflect verra à travers ce genre de changements, et si le code dépend des résultats de reflect, il va casser. Comme dans le cas de l'intégration, ce n'est pas parfait. Contrairement à la situation avec l'intégration, je n'ai aucune réponse, sauf peut-être que le code ne devrait pas être écrit à l'aide de reflect pour être aussi sensible à ces détails.

@rsc Ce que j'avais à l'esprit était a) interdire d'incorporer à la fois un alias et son type de définition dans la même structure (pour éviter toute ambiguïté de b), b) permettre de faire référence à un champ par l'un ou l'autre nom dans le code source, c) en choisir un ou l'autre dans le type d'information/réflexion généré et autres (peu importe lequel).

Je dirais d'un geste de la main que cela permet d'éviter le genre de ruptures que j'ai essayé de décrire, tout en faisant un choix clair pour le cas où un choix est requis ; et, personnellement, je me soucie moins de ne pas casser le code qui repose sur la réflexion, que le code qui ne le fait pas.

Je ne sais pas pour l'instant si je comprends votre argument ByteBuffer, mais je suis aussi à la fin d'une longue journée de travail, donc pas besoin d'expliquer davantage, si je ne le trouve pas convaincant, je finirai par répondre :)

@Merovius Je pense qu'il est logique d'essayer les règles simples et de voir jusqu'où nous allons avant d'en introduire des plus complexes. Nous pouvons ajouter (a) et (b) plus tard si nécessaire ; (c) est une donnée quoi qu'il arrive.

Je suis d'accord que peut-être (b) est une bonne idée dans certaines circonstances, mais peut-être pas dans d'autres. Si vous utilisez des alias de type pour le cas d'utilisation « structurer une API à un package en plusieurs packages d'implémentation » mentionné précédemment, vous ne voulez peut-être pas que l'incorporation de l'alias expose l'autre nom (qui peut être dans un package interne et autrement inaccessible à la plupart des utilisateurs). J'espère que nous pourrons acquérir plus d'expérience.

@rsc

Peut-être que l'ajout d'informations au niveau du package sur l'alias aux fichiers objet pourrait aider.
(Tout en tenant compte du fait que les plugins go doivent continuer à fonctionner correctement ou non.)

@Merovius @rsc

a) interdire d'incorporer à la fois un alias et son type de définition dans la même structure

Notez que dans de nombreux cas, cela est déjà interdit en raison de la manière dont l'intégration interagit avec les ensembles de méthodes. (Si le type intégré a un jeu de méthodes non vide et que l'une de ces méthodes est appelée, le programme échouera à compiler : https://play.golang.org/p/XkaB2a0_RK.)

Ainsi, l'ajout d'une règle explicite interdisant le double plongement semble ne faire une différence que dans un petit sous-ensemble de cas ; ne me semble pas valoir la complexité.

Pourquoi ne pas aborder les alias de type comme des types algébriques à la place et prendre en charge les alias vers un ensemble de types afin que nous obtenions également une interface vide équivalente avec la vérification du type au moment de la compilation en bonus, à la

type Stringeroonie = {string,fmt.Stringer}

@j7b

Pourquoi ne pas aborder les alias de type comme des types algébriques à la place et prendre en charge les alias vers un ensemble de types

Les alias sont sémantiquement et structurellement équivalents au type d'origine. Les types de données algébriques ne le sont pas : dans le cas général, ils nécessitent un stockage supplémentaire pour les balises de type. (Les types d'interface Go transportent déjà ces informations de type, mais les structs et autres types sans interface ne le font pas.)

@bcmills

Cela peut être un raisonnement erroné, mais je pensais que le problème pouvait être abordé car l'alias A de type T équivaut à déclarer A comme interface {} et à laisser le compilateur convertir de manière transparente les variables de type A en T dans des portées où les variables de type A sont déclarées , ce que je pensais être principalement un coût de compilation linéaire, sans ambiguïté, et créer une base pour les pseudotypes gérés par le compilateur, y compris les algébriques utilisant la syntaxe type T = , et éventuellement aussi permettre l'implémentation de types comme des références immuables au moment de la compilation qui, comme en ce qui concerne le code utilisateur, il ne s'agirait que d'interfaces {} "sous le capot".

Des lacunes dans ce train de pensée seraient probablement le produit de l'ignorance, et comme je ne suis pas en mesure d'offrir une preuve de concept pratique, je suis heureux d'accepter qu'elle soit déficiente et de reporter.

@j7b Même si ADT était une solution à un problème de réparation graduelle, ils créent alors le leur; il est impossible d'ajouter ou de supprimer des membres d'un ADT sans rompre les dépendances. Donc, vous créeriez essentiellement plus de problèmes que vous n'en résoudriez.

Votre idée de traduction transparente vers et depuis l'interface{} ne fonctionne pas non plus pour les types d'ordre supérieur comme []interface{} . Et finalement, vous finirez par perdre l'un des points forts de go, qui est de donner aux utilisateurs le contrôle de la mise en page des données et à la place de faire le travail java de tout envelopper.

ADT n'est pas la solution ici.

@Merovius Je suis à peu près sûr que si une construction de type algébrique inclut le renommage (ce qui serait cohérent avec une définition raisonnable de celle-ci), c'est une solution, cette interface{} peut servir de proxy pour la forme de projection et de sélection gérées par le compilateur décrit, et je ne sais pas comment la mise en page des données est pertinente ni comment vous définissez les types "d'ordre supérieur", un type n'est qu'un type s'il peut être déclaré et []interface{} n'est qu'un type.

Tout cela mis à part, je suis sûr que type T = a le potentiel d'être surchargé de manière intuitive et utile au-delà du renommage, les types algébriques et les références publiquement immuables semblent les applications les plus évidentes, donc j'espère que la spécification finira par déclarer cette syntaxe indique un méta ou un pseudo-type géré par le compilateur et une attention particulière est accordée à toutes les manières dont un type géré par le compilateur pourrait être utile et à la syntaxe qui exprime le mieux ces utilisations. Puisqu'une nouvelle syntaxe n'a pas besoin de se préoccuper de l'ensemble des mots globalement réservés lorsqu'elle est utilisée comme qualificatif, quelque chose comme type A = alias Type serait clair et extensible.

@j7b

Tout cela mis à part, je suis positif de type T = a le potentiel d'être surchargé de manière intuitive et utile au-delà du renommage,

J'espère bien que non. Go est (surtout) joliment orthogonal aujourd'hui, et maintenir cette orthogonalité est une bonne chose.

La façon dont on déclare aujourd'hui un nouveau type T dans Go est type T def , où def est la définition du nouveau type. Si l'on devait implémenter des types de données algébriques (alias unions étiquetées), je m'attendrais à ce qu'ils suivent cette syntaxe plutôt que la syntaxe des alias de type.

J'aime apporter un point de vue différent (à l'appui) des alias de type, ce qui peut donner un aperçu des cas d'utilisation alternatifs en plus de la refactorisation :

Revenons un instant en arrière et supposons que nous n'avions pas d'anciennes déclarations de type Go régulières de la forme type T <a type> , mais seulement des déclarations d'alias de type type A = <a type> .

(Pour compléter l'image, supposons également que les méthodes soient déclarées différemment - pas via une association au type nommé utilisé comme récepteur, car nous ne pouvons pas. Par exemple, on pourrait imaginer la notion d'un type de classe avec les méthodes littéralement à l'intérieur et nous n'avons donc pas besoin de compter sur un type nommé pour déclarer des méthodes. Deux de ces types qui sont structurellement identiques mais qui ont des méthodes différentes seraient des types différents. Les détails ne sont pas importants ici pour cette expérience de pensée.)

Je prétends que dans un tel monde, nous pourrions écrire à peu près le même code que nous écrivons maintenant : nous utilisons les noms de type (alias) afin que nous n'ayons pas à nous répéter, et les types eux-mêmes s'assurent que nous utilisons des données dans un type - manière sûre.

En d'autres termes, si Go avait été conçu de cette façon, nous aurions probablement été bien aussi, dans l'ensemble.

Plus encore, dans un tel monde, parce que les types sont identiques s'ils sont structurellement identiques (peu importe le nom), les problèmes que nous avons maintenant avec la refactorisation ne se seraient pas manifestés en premier lieu, et il n'y aurait pas besoin de changements de langue.

Mais nous n'aurions pas le mécanisme de sécurité que nous avons dans le Go actuel : nous ne serions pas en mesure d'introduire un nom pour un type et d'indiquer qu'il devrait maintenant s'agir d'un nouveau type différent. (Cependant, il est important de garder à l'esprit qu'il s'agit essentiellement d'un mécanisme de sécurité.)

Dans d'autres langages de programmation, la notion de création d'un nouveau type différent d'un type existant est appelée « branding » : un type get est une marque qui lui est attachée qui le rend différent de tous les autres types. Par exemple, dans Modula-3, il y avait un mot-clé spécial BRANDED pour que cela se produise (par exemple, TYPE T = BRANDED REF T0 créerait une nouvelle référence différente à T0). En Haskell, le mot new devant un type a un effet similaire.

Pour en revenir à notre monde Go alternatif, nous pourrions nous trouver dans une position où nous n'avons aucun problème avec le refactoring, mais où nous voulions améliorer la sécurité de notre code afin que type MyBuffer = []byte et type YourBuffer = []byte dénotent différents types afin que nous n'utilisions pas accidentellement le mauvais. Nous pourrions proposer d'introduire une forme de marque de type dans ce but précis. Par exemple, nous pourrions vouloir écrire type MyBuffer = new []byte , ou même type MyBuffer = new YourBuffer avec pour effet que MyBuffer est maintenant un type différent de YourBuffer.

C'est essentiellement le double problème de ce que nous avons maintenant. Il se trouve qu'à Go, dès le premier jour, nous avons toujours travaillé avec des types "marqués" dès qu'ils ont un nom. En d'autres termes, type T <a type> est effectivement type T = new <a type> .

Pour résumer : dans Go existant, les types nommés sont toujours des types "marqués", et il nous manque la notion d'un simple nom pour un type (que nous appelons maintenant des alias de type). Dans plusieurs autres langages, les alias de type sont la norme et il faut utiliser un mécanisme de « branding » pour créer un type explicitement nouveau et différent.

Le fait est que les deux mécanismes sont intrinsèquement utiles, et avec les alias de type, nous parvenons enfin à les prendre en charge tous les deux.

@griesemer L'extension de cette fonctionnalité est la proposition d'alias initiale qui devrait idéalement nettoyer la refactorisation. Je crains que seuls les alias de type ne créent des cas limites de refactorisation difficiles en raison de sa portée restreinte.

Dans les deux propositions, je me demande si la collaboration de l'éditeur de liens ne devrait pas être requise car le nom fait partie de la définition de type dans Go, comme vous l'avez expliqué.

Je ne suis pas du tout familiarisé avec le code objet, ce n'est donc qu'une idée, mais il semble qu'il soit possible d'ajouter des sections personnalisées aux fichiers objets. Si par hasard, il était possible de garder une sorte de liste chaînée déroulée, remplie au moment du lien des noms de type et de leurs alias peut-être que cela pourrait aider. Le runtime aurait toutes les informations dont il a besoin sans sacrifier la compilation séparée.

L'idée étant que le runtime soit capable de retourner dynamiquement les différents alias pour un type donné afin que les messages d'erreur restent clairs (puisque l'alias introduit une divergence de nommage entre le code en cours d'exécution et le code écrit).

Une alternative à l'utilisation de l'alias de traçage serait d'avoir une histoire de versioning concrète dans le grand, pour pouvoir "déplacer" les définitions d'objets à travers les packages comme cela a été fait pour le package de contexte. Mais c'est un tout autre problème.

Au final, c'est toujours une bonne idée d'avoir laissé l'équivalence structurelle aux interfaces et l'équivalence de nom aux types.
Étant donné qu'un type peut être considéré comme une interface avec plus de contraintes, il semble que la déclaration d'un alias devrait/pourrait être implémentée en conservant une tranche par paquet de tranches de chaînes de noms de type.

@atdiar Je ne suis pas sûr que vous vouliez dire ce que je fais quand vous dites "compilation séparée". Si le package P importe io et octets, les trois peuvent être compilés en tant qu'étapes distinctes. Cependant, si io ou octets changent, alors P doit être recompilé. Ce n'est _pas_ le cas où vous pouvez apporter des modifications à io ou aux octets et ensuite simplement utiliser une ancienne compilation de P. Même en mode plugin, c'est vrai. En raison d'effets tels que l'inlining entre packages, même les modifications non visibles de l'API apportées à l'implémentation de io ou d'octets modifient l'ABI effective, c'est pourquoi P doit être recompilé. Les alias de type n'aggravent pas ce problème.

@j7d , au niveau du système de types, les types de somme ou tout type de sous-typage (comme suggéré par d'autres plus tôt dans la discussion) n'aident qu'avec certains types d'utilisations. Il est vrai que nous pouvons considérer bytes.Buffer comme un sous-type de io.Reader ("un Buffer est un Lecteur", ou dans votre exemple "une chaîne est un Stringeroonie"). Les problèmes surviennent lors de la construction de types plus complexes à l'aide de ceux-ci. Le reste de ce commentaire parle des types Go mais parle de leurs relations fondamentales à un niveau de sous-typage, pas de ce que le langage Go implémente réellement. Go doit cependant mettre en œuvre des règles cohérentes avec les relations fondamentales.

Un constructeur de type (une manière sophistiquée de dire "une manière d'utiliser un type") est covariant s'il préserve la relation de sous-typage, contravariant s'il inverse la relation.

L'utilisation d'un type dans un résultat de fonction est covariante. Un tampon func() "est un" lecteur func(), car renvoyer un tampon signifie que vous avez renvoyé un lecteur. L'utilisation d'un type dans un argument de fonction n'est _pas_ covariant. Un func(Buffer) n'est pas un func(Reader), car le func a besoin d'un Buffer, et certains Readers ne sont pas des Buffers.

L'utilisation d'un type dans un argument de fonction est contravariante. Un func(Reader) est un func(Buffer), car le func n'a besoin que d'un Reader, et un Buffer est un Reader. L'utilisation d'un type dans un résultat de fonction n'est _pas_ contravariante. Un lecteur func() n'est pas un tampon func(), car le func renvoie un lecteur, et certains lecteurs ne sont pas des tampons.

En combinant les deux, un lecteur func(Reader) n'est pas un tampon func(Buffer), ni vice versa, car soit les arguments ne fonctionnent pas, soit les résultats ne fonctionnent pas. (La seule combinaison qui fonctionne le long de ces lignes serait qu'un tampon func (Reader) est un lecteur func (Buffer).)

En général, si func(X1) X2 est un (sous-type de) func(X3) X4, alors il doit être que X3 est un (sous-type de) X1 et de même X2 est un (sous-type de) X4. Dans le cas de l'utilisation d'alias où l'on veut que T1 et T2 soient interchangeables, un func(T1) T1 est un sous-type de func(T2) T2 uniquement si T1 est un sous-type de T2 _et_ T2 est un sous-type de T1. Cela signifie essentiellement que T1 est le même type que T2, pas un type plus général.

J'ai utilisé des arguments de fonction et des résultats parce que c'est l'exemple canonique (et un bon), mais il en va de même pour d'autres façons de créer des résultats complexes. En général, vous obtenez une covariance pour les sorties (comme func() T, ou <-chan T, ou map[...]T) et une contravariance pour les entrées (comme func(T), ou chan<- T, ou map[T ]...) et égalité de type forcée pour entrée+sortie (comme func(T) T, ou chan T, ou *T, ou [10]T, ou []T, ou struct {Field T}, ou une variable de type T). En fait, le cas le plus courant dans Go, comme vous pouvez le voir dans les exemples, est entrée+sortie.

Concrètement, un []Buffer n'est pas un []Reader (car vous pouvez stocker un Fichier dans un []Reader mais pas dans un []Buffer), et un []Reader n'est pas un []Buffer (car extraire d'un [] Le lecteur peut renvoyer un fichier, tandis que la récupération à partir d'un []Buffer doit renvoyer un tampon).

Une conclusion de tout cela est que, si vous voulez résoudre le problème général de réparation de code afin que le code puisse utiliser T1 ou T2, vous ne pouvez pas le faire avec un schéma qui fait de T1 uniquement un sous-type de T2 (ou vice versa). Chacun doit être un sous-type de l'autre - c'est-à-dire qu'ils doivent être du même type - sinon certaines de ces utilisations répertoriées seront invalides.

Autrement dit, le sous-typage n'est pas suffisant pour résoudre le problème de réparation progressive du code. C'est pourquoi les alias de type introduisent un nouveau nom pour le même type, de sorte que T1 = T2, au lieu de tenter un sous-typage.

Ce commentaire s'applique également à la suggestion de réponse de

Mise à jour du résumé de la discussion de haut niveau. Changements:

  • Suppression de TODO pour mettre à jour le résumé de la discussion sur l'adaptateur, qui semble s'être estompé.
  • Ajout d'un résumé de la discussion sur l'intégration et les renommages de champs.
  • Déplacement du résumé des « méthodes sur les alias » dans sa propre section hors de la liste de questions de conception, élargi pour inclure les commentaires récents.
  • Ajout d'un résumé de la discussion sur l'effet sur les programmes à l'aide de la réflexion.
  • Ajout d'un résumé de la discussion sur la compilation séparée.
  • Ajout d'un résumé de la discussion sur diverses approches basées sur le sous-typage.

@rsc concernant la compilation séparée, mon commentaire est relatif à savoir si les définitions de type doivent conserver une liste de leurs alias (ce qui n'est pas traitable à grande échelle, en raison de l'exigence de compilation séparée) ou chaque alias implique la construction itérative d'une liste de noms d'alias suivant le graphique d'importation, tous liés au nom de type initial donné fourni dans la définition de type. (et comment et où conserver ces informations afin que le runtime y ait accès).

@atdiar Il n'existe aucune telle liste de noms d'alias dans le système. Le runtime n'y a pas accès. Les alias n'existent pas au moment de l'exécution.

@rsc Euh, désolé. Je suis coincé avec la proposition d'alias initiale dans head et je pensais à l'alias pour func (tout en discutant de l'alias pour les types). Dans ce cas, il y aurait une différence entre les noms dans le code et les noms à l'exécution.
L'utilisation des informations dans runtime.Frame pour la journalisation nécessiterait une réflexion dans ce cas.
Ne me garde jamais.

@rsc merci pour le nouveau résumé. Le nom du champ intégré m'agace toujours ; toutes les solutions de contournement proposées reposent sur des kludges permanents pour conserver les anciens noms. Bien que le point le plus important de ce commentaire , à savoir qu'il s'agisse d'un cas particulier de renommage des champs, ce qui n'est pas possible non plus, me convainc que cela devrait en effet être considéré (et résolu) comme un problème distinct. Serait-il logique d'ouvrir un problème séparé pour une demande/proposition/discussion afin de prendre en charge les changements de nom de champ pour une réparation progressive (éventuellement abordée dans la même version de go) ?

@Merovius , je suis d'accord pour dire que la réparation progressive du code pour le renommage du champ ressemble au prochain problème de la séquence. Pour démarrer cette discussion, je pense que quelqu'un aurait besoin de rassembler une série d'exemples du monde réel, à la fois pour que nous ayons des preuves qu'il s'agit d'un problème répandu et également pour vérifier les solutions potentielles. En réalité, je ne vois pas cela se produire pour la même version.

De retour de deux semaines d'absence. La discussion semble avoir convergé. Même la mise à jour de la discussion il y a deux semaines était assez mineure.

Je propose que nous :

  • accepter la proposition d'alias de type comme solution provisoire au problème exposé ci-dessus,
    à condition qu'une implémentation puisse être prête à être essayée au début de Go 1.9 (1er février).
  • créer une branche dev.typealias dev afin que les CL puissent être examinées maintenant (janvier) et fusionnées dans master au début de Go 1.9.
  • prendre une décision finale sur le maintien des alias de type près du début du gel de Go 1.9 (comme nous l'avons fait pour les alias généralisés dans le cycle de Go 1.8).

+1

J'apprécie l'historique des discussions derrière ce changement. Disons qu'il est mis en œuvre. Sans aucun doute, cela deviendra un détail plutôt marginal de la langue, plutôt qu'une caractéristique principale. En tant que tel, il ajoute une complexité au langage et à l'outillage disproportionnée par rapport à sa fréquence d'utilisation réelle. Cela ajoute également plus de surface dans laquelle la langue pourrait être maltraitée par inadvertance. Pour cette raison, être trop prudent est une bonne chose, et je suis heureux qu'il y ait eu de nombreuses discussions jusqu'à présent.

@Merovius : Désolé d'avoir édité mon message ! Je pensais que personne ne lisait. Initialement dans ce commentaire, j'ai exprimé un certain scepticisme quant à la nécessité de ce changement de langage alors qu'il existe déjà des outils comme l'outil gorename .

@ jcao219 Cela a déjà été discuté, mais étonnamment, je n'arrive pas à trouver cela rapidement ici. Il est longuement discuté dans le fil de discussion original pour les alias généraux #16339 et les fils de discussion golang-nuts associés. En bref : ce type d'outillage ne traite que de la façon de préparer les commits de réparation, pas de la façon de séquencer les modifications pour éviter les casses. Que les modifications soient effectuées par un outil ou par un humain est sans importance pour le problème, qu'il n'y ait actuellement aucune séquence de commits qui ne cassera pas un code ou un autre (le commentaire original de ce problème et la doc associée justifient cette déclaration plus en -profondeur).

Pour un outillage plus automatisé (par exemple intégré dans l'outil go ou similaire), le commentaire d'origine traite de cela sous le titre "Est-ce que cela peut être un changement d'outillage ou de compilateur uniquement au lieu d'un changement de langue ?".

En conclusion, disons que le changement est mis en œuvre. Sans aucun doute, cela deviendra un détail plutôt marginal de la langue, plutôt qu'une caractéristique principale.

Je voudrais exprimer un doute. :) Je ne considère pas cela comme une fatalité.

@Merovius

Je voudrais exprimer un doute. :) Je ne considère pas cela comme une fatalité.

Je suppose que je voulais dire que les personnes qui utiliseraient cette fonctionnalité seront principalement les mainteneurs d'importants packages Go avec de nombreux clients dépendants. En d'autres termes, cela profite à ceux qui sont déjà experts en Go. En même temps, il présente un moyen tentant de rendre le code moins lisible pour les nouveaux programmeurs Go. L'exception est le cas d'utilisation du renommage des noms longs, mais les noms de type Go naturels ne sont généralement pas trop longs ou complexes.

Tout comme dans le cas de la fonctionnalité d'importation de points, il serait sage que les didacticiels et les documents accompagnent leurs mentions de cette fonctionnalité d'une déclaration sur les directives d'utilisation.

Par exemple, disons que je voulais utiliser "github.com/gonum/graph/simple".DirectedGraph , et que je voulais l'alias avec digraph pour éviter de taper simple.DirectedGraph , serait-ce un bon cas d'utilisation? Ou ce genre de renommage devrait-il être limité aux noms déraisonnablement longs générés par des choses comme protobuf ?

@jcao219 , le résumé de la discussion en haut de cette page répond à vos questions. En particulier, consultez ces sections :

  • Cela peut-il être un changement d'outil ou de compilateur uniquement au lieu d'un changement de langue ?
  • Quelles autres utilisations les alias pourraient-ils avoir ?
  • Restrictions (les notes générales commençant cette section)

Pour votre point plus général sur les experts Go par rapport aux nouveaux programmeurs Go, un objectif explicite de Go est de faciliter la programmation dans de grandes bases de code. Que vous soyez un expert n'a aucun rapport avec la taille de la base de code dans laquelle vous travaillez. (Peut-être que vous venez de commencer un nouveau projet que quelqu'un d'autre a commencé. Vous devrez peut-être encore faire ce genre de travail.)

OK, sur la base de l'unanimité / silence ici, je vais (comme je l'ai suggéré la semaine dernière dans https://github.com/golang/go/issues/18130#issuecomment-268614964) marquer cette proposition comme approuvée et créer une branche dev.typealias .

L'excellent résumé comporte une section « Quels autres problèmes une proposition d'alias de type doit-elle résoudre ? » Quels sont les plans pour résoudre ces problèmes une fois que la proposition a été déclarée acceptée ?

CL https://golang.org/cl/34986 mentionne ce problème.

CL https://golang.org/cl/34987 mentionne ce problème.

CL https://golang.org/cl/34988 mentionne ce problème.

@ulikunitz concernant les problèmes (toutes ces citations du document de conception supposent 'type T1 = T2'):

  1. Manipulation dans godoc. Les spécifications du document de conception apportent des modifications minimales à godoc. Une fois cela fait, nous pouvons voir si un soutien supplémentaire est nécessaire. Peut-être, mais peut-être pas.
  2. Les méthodes peuvent-elles être définies sur des types nommés par alias ? Oui. Doc de conception : « Étant donné que T1 n'est qu'une autre façon d'écrire T2, il n'a pas son propre ensemble de déclarations de méthode. Au lieu de cela, l'ensemble de méthodes de T1 est le même que celui de T2. Au moins pour l'essai initial, il n'y a aucune restriction contre les déclarations de méthode en utilisant T1 comme type de récepteur, à condition que l'utilisation de T2 dans la même déclaration soit valide."
  3. Si les alias à alias sont autorisés, comment gérons-nous les cycles d'alias ? Pas de cycles. Doc de conception : « Dans une déclaration d'alias de type, contrairement à une déclaration de type, T2 ne doit jamais faire référence, directement ou indirectement, à T1.
  4. Les alias doivent-ils pouvoir exporter des identifiants non exportés ? Oui. Doc de conception : « Il n'y a aucune restriction sur la forme de T2 : il peut s'agir de n'importe quel type, y compris, mais sans s'y limiter, les types importés d'autres packages. »
  5. Que se passe-t-il lorsque vous intégrez un alias (comment accédez-vous au champ intégré) ? Le nom est tiré de l'alias (le nom visible dans le programme). Document de conception : https://golang.org/design/18130-type-alias#effect -on-embedding.
  6. Les alias sont-ils disponibles sous forme de symboles dans le programme construit ? Non. Document de conception : "Les alias de type sont pour la plupart invisibles à l'exécution." (La réponse en découle mais n'est pas appelée explicitement.)
  7. Injection de chaîne Ldflags : et si on se référait à un alias ? Il n'y a pas d'alias de var, donc cela ne se produit pas.

CL https://golang.org/cl/35091 mentionne ce problème.

CL https://golang.org/cl/35092 mentionne ce problème.

CL https://golang.org/cl/35093 mentionne ce problème.

@rsc Merci beaucoup pour les éclaircissements.

Assumons:

package a

import "b"

type T1 = b.T2

Autant que je sache, T1 est essentiellement identique à b.T2 et est donc un type non local et aucune nouvelle méthode ne peut être définie. L'identifiant T1 est cependant réexporté dans le package a. Est-ce une interprétation correcte ?

@ulikunitz c'est correct

T1 désigne exactement le même type que b.T2. C'est simplement un nom différent. Que quelque chose soit exporté ou non est basé sur son nom seul (n'a rien à voir avec le type qu'il désigne).

Pour rendre explicite la réponse de

CL https://golang.org/cl/35099 mentionne ce problème.

CL https://golang.org/cl/35100 mentionne ce problème.

CL https://golang.org/cl/35101 mentionne ce problème.

CL https://golang.org/cl/35102 mentionne ce problème.

CL https://golang.org/cl/35104 mentionne ce problème.

CL https://golang.org/cl/35106 mentionne ce problème.

CL https://golang.org/cl/35108 mentionne ce problème.

CL https://golang.org/cl/35120 mentionne ce problème.

CL https://golang.org/cl/35121 mentionne ce problème.

CL https://golang.org/cl/35129 mentionne ce problème.

CL https://golang.org/cl/35191 mentionne ce problème.

CL https://golang.org/cl/35233 mentionne ce problème.

CL https://golang.org/cl/35268 mentionne ce problème.

CL https://golang.org/cl/35269 mentionne ce problème.

CL https://golang.org/cl/35670 mentionne ce problème.

CL https://golang.org/cl/35671 mentionne ce problème.

CL https://golang.org/cl/35575 mentionne ce problème.

CL https://golang.org/cl/35732 mentionne ce problème.

CL https://golang.org/cl/35733 mentionne ce problème.

CL https://golang.org/cl/35831 mentionne ce problème.

CL https://golang.org/cl/36014 mentionne ce problème.

Ceci est maintenant en master, avant l'ouverture de Go 1.9. N'hésitez pas à synchroniser sur le maître et à essayer des choses. Merci.

Redirigé depuis #18893

package main

import (
        "fmt"
        "q"
)

func main() {
        var a q.A
        var b q.B // i'm a named unnamed type !!!

        fmt.Printf("%T\t%T\n", a, b)
}

Que vous attendiez-vous à voir ?

deadwood(~/src) % go run main.go
q.A     q.B

Qu'avez-vous vu à la place ?

deadwood(~/src) % go run main.go
q.A     []int

Discussion

Les alias ne doivent pas s'appliquer au type sans nom. Il n'y a pas d'histoire de "réparation de code" en passant d'un type sans nom à un autre. Autoriser les alias sur les types sans nom signifie que je ne peux plus enseigner Go en tant que types simplement nommés et sans nom. Au lieu de cela, je dois dire

oh, à moins qu'il ne s'agisse d'un alias, auquel cas vous devez vous rappeler qu'il _pourrait_ être_ un type sans nom, même lorsque vous importez depuis un autre package.

Et pire, cela permettra aux gens de promulguer des anti-modèles de lisibilité comme

type Any = interface{}

Veuillez ne pas autoriser les alias de types sans nom.

@davecheney

Il n'y a pas d'histoire de "réparation de code" en passant d'un type sans nom à un autre.

Pas vrai. Que se passe-t-il si vous souhaitez modifier le type d'un paramètre de méthode d'un type nommé à un type non nommé ou vice-versa ? L'étape 1 consiste à ajouter l'alias ; l'étape 2 consiste à mettre à jour les types qui implémentent cette méthode pour utiliser le nouveau type ; l'étape 3 consiste à supprimer l'alias.

(Il est vrai que vous pouvez le faire aujourd'hui en renommant la méthode deux fois. Le double-renommer est au mieux fastidieux.)

Et pire, cela permettra aux gens de promulguer des anti-modèles de lisibilité comme
type Any = interface{}

Les gens peuvent déjà écrire type Any interface{} aujourd'hui. Quel préjudice supplémentaire les alias introduisent-ils dans ce cas ?

Les gens peuvent déjà écrire type Any interface{} aujourd'hui. Quel préjudice supplémentaire les alias introduisent-ils dans ce cas ?

Je l'ai appelé un anti-modèle parce que c'est précisément ce que c'est. type Any interface{} , parce que c'est la personne qui _écrit_ le code tape quelque chose d'un peu plus court, cela a un peu plus de sens pour elle.

D'un autre côté, _tous_ les lecteurs, qui ont de l'expérience dans la lecture du code Go et reconnaissent interface{} aussi instinctivement que leur visage dans un miroir, doivent apprendre et réapprendre chaque variante de Any , Object , T , et associez-les à des éléments tels que type Any interface{} , type Any map[interface{}]interface{} , type Any struct{} par forfait.

Vous êtes sûrement d'accord pour dire que les noms spécifiques aux packages pour les idiomes Go courants sont un net négatif pour la lisibilité ?

Vous êtes sûrement d'accord pour dire que les noms spécifiques aux packages pour les idiomes Go courants sont un net négatif pour la lisibilité ?

Je suis d'accord, mais puisque l'exemple en question (de loin l'occurrence la plus courante de cet anti-modèle que j'ai rencontré) peut être fait sans alias, je ne comprends pas comment cet exemple se rapporte à la proposition d'alias de type.

Le fait que l'anti-pattern soit possible sans alias de type signifie que nous devons déjà éduquer les programmeurs Go à l'éviter, que des alias vers des types sans nom puissent exister.

Et, en fait, les alias de type permettent la _suppression progressive_ de cet anti-modèle des bases de code dans lesquelles il existe déjà.

Envisager:

package antipattern

type Any interface{}  // not an alias

type Widget interface{
  Frozzle(Any) error
}

func Bozzle(w Widget) error {
  …
}

Aujourd'hui, les utilisateurs de antipattern.Bozzle seraient bloqués en utilisant antipattern.Any dans leurs implémentations Widget , et il n'y a aucun moyen de supprimer antipattern.Any avec des réparations progressives. Mais avec les alias de type, le propriétaire du package antipattern pourrait le redéfinir comme ceci :

// Any is deprecated; please use interface{} directly.
type Any = interface{}

Et maintenant, les appelants peuvent migrer de Any à interface{} progressivement, permettant au responsable de antipattern de le supprimer éventuellement.

Mon point est qu'il n'y a aucune justification pour aliaser des types sans nom, donc
rejeter cette option continuerait de souligner l'inadéquation de
la pratique.

Au contraire, autoriser l'aliasing de types sans nom permet non pas un, mais deux
formes de cet anti-modèle.

Le jeu. 2 février 2017, 16:34 Bryan C. Mills [email protected] a écrit :

Vous êtes sûrement d'accord pour dire que les noms spécifiques aux packages pour les idiomes Go courants sont
un net négatif pour la lisibilité ?

Je suis d'accord, mais puisque l'exemple en question (de loin le plus courant
l'occurrence de cet antipattern que j'ai rencontré) peut être fait sans
alias, je ne comprends pas en quoi cet exemple se rapporte à la proposition de
alias de type.

Le fait que l'anti-modèle soit possible sans alias de type signifie que
nous devons déjà éduquer les programmeurs de Go à l'éviter, peu importe si
des alias vers des types sans nom peuvent exister.

-
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/18130#issuecomment-276872714 , ou couper le son
le fil
https://github.com/notifications/unsubscribe-auth/AAAcA6BGrFjjTi7eW1BPp7o81XIekbGXks5rYWr-gaJpZM4LBBEL
.

@davecheney Je ne pense pas que nous ayons encore la preuve que pouvoir donner un nom à un littéral de type arbitraire est nuisible. Ce n'est pas non plus une fonctionnalité "surprise" inattendue - elle a été discutée en détail dans le document de conception . À ce stade, il est logique de l'utiliser pendant un certain temps et de voir où cela nous mène.

À titre de contre-exemple, il existe des API publiques qui utilisent des littéraux de type uniquement parce que l'API ne veut pas restreindre un client à un type spécifique (voir https://golang.org/pkg/go/types/#Info par exemple ). Avoir ce type littéral explicite peut être une documentation utile. Mais en même temps, il peut être assez ennuyeux de devoir répéter le même type de littéral partout ; et en fait être un obstacle à la lisibilité. Être capable de parler facilement d'un IntSet plutôt que d'un map[int]struct{} sans être enfermé dans cette définition et seulement IntSet est un plus dans mon esprit. C'est là que type IntSet = map[int]struct{} est tout à fait exact.

Enfin, j'aime me référer à https://github.com/golang/go/issues/18130#issuecomment-268411811 au cas où vous l'auriez manqué. Les déclarations de type sans restriction utilisant = sont vraiment la déclaration de type "élémentaire", et je suis heureux que nous les ayons enfin dans Go.

Peut-être que type intSet = map[int]struct{} (non exporté) serait un meilleur moyen d'utiliser des alias de type sans nom, mais cela ressemble au domaine de CodeReviewComments et des pratiques de programmation recommandées, plutôt que de limiter la fonctionnalité.

Cela dit, %T est un outil pratique pour voir les types lors du débogage ou de l'exploration du système de types. Je me demande s'il devrait y avoir un verbe de format similaire qui inclut l'alias? q.B = []int dans l'exemple de @davecheney .

@nathany Comment implémentez-vous ce verbe ? Les informations d'alias ne sont pas présentes au moment de l'exécution. (En ce qui concerne le package reflect , l'alias est _du même type_ que la chose à laquelle il est alias.)

@bcmills J'ai pensé que cela pourrait être le cas...

J'imagine que les outils d'analyse statique et les plugins d'éditeur sont toujours présents pour aider à travailler avec les alias, donc ça va.

Le 2 février 2017 à 17h01, "Nathan Youngman" [email protected] a écrit :

Cela dit, %T est un outil pratique pour voir les types lors du débogage ou de l'exploration du
système de types. Je me demande s'il devrait y avoir un verbe de format similaire qui
inclut l'alias ? qB = []int dans @davecheney
Exemple de https://github.com/davecheney .

Je pense qu'une meilleure solution est d'ajouter un mode de requête au gourou pour répondre à cela
question:

qui sont les alias déclarés dans tout le GOPATH (ou un package donné) pour
ce type donné sur la ligne de commande?

Je ne suis pas inquiet de l'abus d'alias de types sans nom, mais potentiel
alias dupliqués au même type sans nom.

@davecheney J'ai ajouté votre suggestion à la section "Restrictions" du résumé de la discussion en haut. Comme toutes les restrictions, notre position générale est que les restrictions ajoutent de la complexité (voir les notes ci-dessus) et nous aurions probablement besoin de voir des preuves réelles d'un préjudice généralisé afin d'introduire une restriction. Devoir changer la façon dont vous enseignez le Go n'est pas suffisant : tout changement que nous apportons à la langue nécessitera de changer la façon dont vous enseignez le Go.

Comme indiqué dans le document de conception et sur la liste de diffusion, nous travaillons sur une meilleure terminologie pour faciliter les explications.

@minux , comme l' a souligné

Le 2 février 2017, 20h33, "Russ Cox" [email protected] a écrit :

@minux https://github.com/minux , comme @bcmills
https://github.com/bcmills a souligné, les informations d'alias n'existent pas
au moment de l'exécution (complètement fondamental à la conception). Il n'y a aucun moyen de
implémenter un "%T qui inclut l'alias".

Je suggère un mode de requête Go gourou (https://golang.org/x/tools/cmd/guru)
pour le mappage d'alias inversé, qui est basé sur une analyse de code statique. Ce
peu importe si les informations d'alias sont disponibles au moment de l'exécution ou non.

@minux , oh je vois, vous répondez par e-mail et Github fait ressembler le texte cité au texte que vous avez écrit vous-même. Je répondais au texte que vous avez cité de Nathan Youngman, pensant que c'était le vôtre. Désolé pour la confusion.

En ce qui concerne la terminologie et l'enseignement, j'ai trouvé le contexte des types de marque publié par @griesemer assez instructif. Merci pour ça.

Lorsqu'ils expliquent les types et les conversions de types, les bébés gaufres pensent d'abord que je parle d'un alias de type, probablement en raison de la familiarité avec d'autres langues.

Quelle que soit la terminologie finale, je pourrais imaginer introduire des alias de type avant les types nommés (de marque), d'autant plus que la déclaration de nouveaux types nommés est susceptible de venir après l'introduction de byte et rune dans n'importe quel livre ou curriculum. Cependant, je tiens à garder à l'esprit le @davecheney de ne pas encourager les anti-modèles.

Pour type intSet map[int]struct{} nous disons que map[int]struct{} est le type _sous-jacent_. Comment appelle-t-on chaque côté de type intSet = map[int]struct{} ? Alias ​​et type d'alias ?

Quant à %T , j'ai déjà besoin d'expliquer qu'un byte et rune donnent un uint8 et int32 , donc ce n'est pas différent.

Si quoi que ce soit, je pense que les alias de type rendront byte et rune plus faciles à expliquer. OMI, le défi sera de savoir quand utiliser les types nommés par rapport aux alias de type, puis de pouvoir communiquer cela.

@nathany Je pense qu'il est très logique d'introduire d'abord les "types d'alias" - bien que je n'utiliserais pas nécessairement le terme. Les déclarations "alias" nouvellement introduites sont simplement des déclarations régulières qui ne font rien de spécial. L'identifiant à gauche et le type à droite sont identiques, ils désignent des types identiques. Je ne suis même pas sûr que nous ayons besoin des termes alias ou type aliasé (nous n'appelons pas un nom de constante un alias et la valeur constante la constante aliasée).

La déclaration de type traditionnelle (sans alias) est plus efficace : elle crée d'abord un nouveau type à partir du type de droite avant de lui lier l'identifiant de gauche. Ainsi l'identifiant et le type à droite ne sont pas les mêmes (ils ne partagent que le même type sous-jacent). C'est clairement le concept le plus compliqué.

Nous avons besoin d'un nouveau terme pour ces types nouvellement créés car tout type peut désormais avoir un nom. Et nous devons pouvoir nous y référer car il existe des règles de spécification s'y rapportant (identité de type, assignabilité, types de base de récepteur).

Voici une autre façon de décrire cela, qui peut être utile dans un environnement d'enseignement : un type peut être coloré ou non. Tous les types prédéclarés et tous les littéraux de type ne sont pas colorés. La seule façon de créer un nouveau type coloré est via une déclaration de type traditionnelle (sans alias) qui peint d'abord (une copie de) le type sur la droite avec une toute nouvelle couleur jamais utilisée (en supprimant l'ancienne couleur, le cas échéant, entièrement en cours) avant de lui lier l'identifiant de gauche. Là encore, l'identifiant et le type coloré (créé de manière implicite et invisible) sont identiques, mais ils sont différents du type (différemment coloré ou non) inscrit à droite.

En utilisant cette analogie, nous pouvons également reformuler diverses autres règles existantes :

  • Un type coloré est toujours différent de tout autre type (car chaque déclaration de type utilise une toute nouvelle couleur jamais utilisée auparavant).
  • Les méthodes ne peuvent être associées qu'à des types de base de récepteur qui sont colorés.
  • Le type sous-jacent d'un type est ce type dépouillé de toute sa couleur.
    etc.

nous n'appelons pas un nom de constante un alias, et la valeur constante la constante aliasée

bon point

Je ne sais pas si l'analogie colorée vs non colorée est plus facile à comprendre, mais cela démontre qu'il existe plus d'une façon d'expliquer les concepts.

Les types traditionnels nommés/marqués/colorés nécessitent certainement plus d'explications. Surtout lorsqu'un type nommé peut être déclaré à l'aide d'un type nommé existant. Il y a des différences assez subtiles à garder à l'esprit.

type intSet map[int]struct{} // a new type with an underlying type map[int]struct{}

type myIntSet intSet // a new type with an underlying type map[int]struct{}

type otherIntSet = intSet // just another name (alias) for intSet, add methods to intSet (only in the same package)

type literalIntSet = map[int]struct{} // just another name for map[int]struct{}, no adding methods

Ce n'est pourtant pas insurmontable. En supposant que cela atterrisse dans Go 1.9, je soupçonne que nous verrons les 2e éditions de plusieurs livres Go. ??

Je me réfère régulièrement à Go spec pour la terminologie acceptée, donc je suis très curieux de savoir quels termes sont choisis à la fin.

Nous avons besoin d'un nouveau terme pour ces types nouvellement créés car tout type peut désormais avoir un nom.

Quelques idées:

  • "distingué" ou "distinct" (comme dans, peut être distingué des autres types)
  • "unique" (comme dans, c'est un type différent de tous les autres types)
  • "concret" (comme dans, c'est une entité qui existe dans le runtime)
  • "identifiable" (comme dans, le type a une identité)

@bcmills Nous avons pensé à des types distincts, uniques, distincts, marqués, colorés, définis, sans alias, etc. "Concret" est trompeur car une interface peut également être colorée, et une interface est l'incarnation d'un type abstrait. "Identifiable" semble également trompeur car un "struct{int}" est tout aussi identifiable que n'importe quel type nommé explicitement (sans alias).

Je déconseille :

  • "coloré" (dans des contextes de non-programmation, l'expression "types colorés" porte de fortes connotations de préjugés raciaux)
  • "non-alias" (c'est déroutant, puisque la cible de l'alias peut ou non être ce qu'on appelait autrefois un "type nommé")
  • "défini" (les alias sont définis aussi, ils sont juste définis comme des alias)

« de marque » pourrait fonctionner : il porte une connotation « types comme du bétail », mais cela ne me semble pas intrinsèquement mauvais.

Les options uniques et distinctes semblent être les options les plus remarquables jusqu'à présent.

Ils sont simples et compréhensibles sans beaucoup de contexte ou de connaissances supplémentaires. Si je ne connaissais pas la distinction, je pense que j'aurais au moins une idée générale de ce qu'elles impliquent. Je ne peux pas en dire autant des autres choix.

Une fois que vous avez appris le terme, cela n'a pas d'importance, mais un nom connotatif évite les obstacles inutiles à l'intériorisation de la distinction.

C'est la définition d'un argument bikeshed. Robert a une CL en attente sur https://go-review.googlesource.com/#/c/36213/ qui semble parfaitement bien.

CL https://golang.org/cl/36213 mentionne ce problème.

Je veux soulever à nouveau le problème de go fix .

Pour être clair, je ne suggère pas de "supprimer" l'alias. Peut-être que c'est quelque chose d'utile et adapté à d'autres emplois, c'est une autre histoire.

C'est quelque chose de très important pour l'OMI que le titre parle de caractères mobiles. Je n'ai aucune envie de compliquer la question. Notre objectif est de traiter une sorte de changements d'interface dans un projet. Lorsque nous arrivons à un changement d'interface, il n'est pas vrai que nous espérons que tous les utilisateurs utiliseront ces deux interfaces (ancienne et nouvelle) comme la même éventuellement , et c'est pourquoi nous disons "réparation progressive du code". Nous espérons que les utilisateurs suppriment/modifient l'utilisation de l'ancien.

Je considère toujours l'outil comme la meilleure méthode pour réparer le code, quelque chose comme l'idée suggérée par @tux21b . Par exemple:

$ cat "$GOROOT"/RENAME
# This file could be used for `go fix`
[package]
x/net/context=context
[type]
io.ByteBuffer=bytes.Buffer

$ go fix -rename "$GOROOT"/RENAME [packages]
# -- or --
# use a standard libraries rename table as default
$ go fix -rename [packages]
# -- or --
# include this fix as default
$ go fix [packages]

La seule raison pour laquelle Mais je pense que ce n'est pas vrai dans ce flux de travail : s'il y a un package obsolète (par exemple une dépendance) utilise le nom/chemin obsolète du package, par exemple x/net/context , nous pouvons corriger le code dans un premier temps , tout comme le doc dit comment migrer le code vers la nouvelle version, mais pas le codage en dur, via un tableau configurable au format texte. Ensuite, vous pouvez utiliser n'importe quel outil quand vous le souhaitez, comme Go de la nouvelle version. Il y a un effet secondaire : cela modifiera le code.

@LionNatsu , je pense que vous avez raison, mais je pense que c'est un problème distinct : devrions-nous adopter des conventions pour les packages afin d'expliquer aux clients potentiels comment mettre à jour leur code en réponse aux modifications de l'API de manière mécanique ? Peut-être, mais il faudrait trouver quelles sont ces conventions. Pouvez-vous ouvrir un numéro distinct pour ce sujet, en rappelant cette conversation ? Merci.

CL https://golang.org/cl/36691 mentionne ce problème.

Avec cette proposition en pointe, je peux maintenant créer ce package :

package safe

import "unsafe"

type Pointer = unsafe.Pointer

qui permet aux programmes de créer des valeurs unsafe.Pointer sans importer directement unsafe :

package main

import "safe"

func main() {
    x := []int{4, 9}
    y := *(*int)(safe.Pointer(uintptr(safe.Pointer(&x[0])) + 8))
    println(y)
}

Le document de conception des déclarations d'alias d'origine indique que cela est explicitement pris en charge. Ce n'est pas explicite dans cette nouvelle proposition d'alias de type, mais cela fonctionne.

Sur le problème de la déclaration d'alias, le rationnel est le suivant : _"La raison pour laquelle nous autorisons l'alias pour unsafe.Pointer est qu'il est déjà possible de définir un type qui a unsafe.Pointer comme type sous-jacent."_ https://github.com/ golang/go/issues/16339#issuecomment -232435361

Bien que cela soit vrai, je pense qu'autoriser un alias de unsafe.Pointer introduit quelque chose de nouveau : les programmes peuvent désormais créer des valeurs unsafe.Pointer sans importer explicitement unsafe.

Pour écrire le programme ci-dessus avant cette proposition, je devrais déplacer le cast safe.Pointer dans un package qui importe unsafe. Cela peut rendre un peu plus difficile l'audit des programmes pour leur utilisation d'unsafe.

@crawshaw , tu ne pouvais pas faire ça avant ?

package safe

import (
  "reflect"
  "unsafe"
)

func Pointer(p interface {}) unsafe.Pointer {
  switch v := reflect.ValueOf(p); v.Kind() {
  case reflect.Uintptr:
    return unsafe.Pointer(uintptr(v.Uint()))
  default:
    return unsafe.Pointer(v.Pointer())
  }
}

Je pense que cela permettrait exactement au même programme de se compiler, avec le même manque d'importation dans le package main .

(Ce ne serait pas nécessairement un programme valide : la conversion uintptr -to- Pointer inclut un appel de fonction, elle ne respecte donc pas la contrainte de package unsafe qui " les deux conversions doivent apparaître dans la même expression, avec seulement l'arithmétique intermédiaire entre elles". Cependant, je soupçonne qu'il serait possible de construire un programme valide équivalent sans importer unsafe de main en faisant utilisation de choses comme reflect.SliceHeader .)

On dirait que l'exportation d'un type non sécurisé caché n'est qu'une autre règle à ajouter à l'audit.

Oui, je voulais souligner que l'aliasing direct unsafe.Pointer rend le code plus difficile à auditer, suffisamment pour que j'espère que personne ne finira par le faire.

@crawshaw Selon mon commentaire, c'était également vrai avant que nous ayons un alias de type. Ce qui suit est valable :

package a

import "unsafe"

type P unsafe.Pointer
package main

import "./a"
import "fmt"

var x uint64 = 0xfedcba9876543210
var h = *(*uint32)(a.P(uintptr(a.P(&x)) + 4))

func main() {
    fmt.Printf("%x\n", h)
}

C'est-à-dire que dans le package main, je peux effectuer des calculs non sécurisés en utilisant a.P même s'il n'y a pas unsafe package a.P n'est pas un alias. Cela a toujours été possible.

Y a-t-il autre chose à laquelle vous faites référence ?

Mon erreur. Je pensais que ça ne fonctionnait pas. (J'avais l'impression que les règles spéciales s'appliquaient à unsafe.Pointer ne se propagerait pas aux nouveaux types définis à partir de celui-ci.)

La spécification n'est en fait pas claire à ce sujet. En regardant l'implémentation de go/types, il s'avère que mon implémentation initiale nécessitait exactement unsafe.Pointer , pas seulement un type qui avait un type sous-jacent de unsafe.Pointer . Je viens de trouver #6326, c'est quand j'ai changé go/types pour être conforme à gc.

Peut-être devrions-nous interdire cela pour les définitions de type standard et également interdire les alias de unsafe.Pointer . Je ne vois aucune bonne raison de l'autoriser et cela compromet le caractère explicite d'avoir à importer unsafe pour du code dangereux.

C'est arrivé. Je pense qu'il ne reste rien ici.

Cette page vous a été utile?
0 / 5 - 0 notes