Type d'une constante en Go

constant overflows int

Le problème exposé dans ce sujet a été résolu.

Bonjour,

Je suis en train de regarder Go rapidement et j’ai du mal à comprendre un truc sur les constantes.

const foo = 1 << 100  // Pas d'erreur
fmt.Printf("%T\n", foo)  // constant 1267650600228229401496703205376 overflows int
fmt.Printf("%T\n", float32(foo))  // affiche float32

Ici, la constante foo est déclaré sans soucis, mais quand j’essaye d’afficher son type avec fmt.Printf et %T, le compilateur me donne une erreur qui dit que ça rentre pas dans un int.

Est-ce que le compilateur essaye de convertir foo en int à la ligne 2? Si oui, pourquoi? Je pensais que Go n’autorisait pas les conversions implicites. La question qui suis est: quel est le type de foo? Et peut-être de manière générale, si j’ai une variable ou une constante dont je ne connais pas le type, comment est-ce que je peux trouver son type?

Edit: et juste pour être clair, je m’en fiche complètement de comment gérer un nombre aussi grand en Go. Ce qui m’intéresse c’est de comprendre quel mécanisme amène à l’erreur affiché.

+0 -0

Salut,

J’ai eu un peu de mal avec ça aussi et, en fait, c’est exactement comme tu le décris : Go essaye de convertir la constante vers le type int, mais n’y arrive pas, car sa capacité est insuffisante. La constante n’a pas de type en soi, mais un type vers lequel elle sera convertie. Tant que tu ne l’utilises pas dans un contexte nécessitant cette conversion, elle n’a pas de type (au sens de ceux de bases du Go) et a une précision arbitraire, gérée par le compilateur.

Si tu veux les détails côté spécification du langage, c’est là-bas : https://golang.org/ref/spec#Constants.

+1 -0

Salut,

Le type de foo est int. De manière un peu inquiétante, le standard demande à ce que l’IR utilisé par le compilateur utilise au minimum 256 bits pour manipuler les expressions constantes (d’où l’absence d’erreur), mais par contre int est 32 ou 64 bits suivant l’implémentation (en pratique, l’architecture, j’imagine), d’où l’erreur lorsque foo est manipulé comme un int.

De facto, il y a donc bien une conversion implicite à la compilation (!) entre un int256 (ou plus suivant l’implémentation) dans l’IR du compilo et les int qui seront effectivement manipulés au runtime.

EDIT: le passage qui met ça en avant :

Constants may be typed or untyped. Literal constants, true, false, iota, and certain constant expressions containing only untyped constant operands are untyped.

A constant may be given a type explicitly by a constant declaration or conversion, or implicitly when used in a variable declaration or an assignment or as an operand in an expression. It is an error if the constant value cannot be represented as a value of the respective type.

+0 -0

Merci. Nohar m’a expliqué sur le discord que la constante est bien non-typé et que l’inférence se fait au moment où elle est utilisé, potentiellement en inférant un type différent à chaque utilisation.

La clarification supplémentaire, c’est que si l’inférence n’est pas possible à cause d’une ambiguïté (comme à la ligne 2) le compilateur se rabat par défaut en int, d’où l’erreur.

De manière un peu inquiétante

Il n’y a pas vraiment de quoi s’inquiéter. ^^ C’est un choix plutôt logique quand on regarde d’où ça vient.

Je vais répéter ici l’explication que j’ai donnée sur Discord.

Le but des constantes est de remplacer les macros du préprocesseur C en ayant des constantes qui peuvent être typées ou non.

Dans le cas "non typé", on différencie quand même les constantes entières des chaînes. Mais le but du jeu est de pouvoir faire par exemple ceci:

package main

const fourtyTwo = 42

int main() {
  var a uint8 = fourtyTwo
  var b int32 = fourtyTwo
  fmt.Println(a, b)
}

Dans les faits, ça permet de garder la logique "ma constante est juste une expression littérale qui finit inlinée à la compilation", mais en ayant dès le début un typage assez lâche ("c’est un int-quelquechose") qui permet de faire remonter des erreurs assez tôt pendant la compilation.

+3 -1

mais en ayant dès le début un typage assez lâche ("c’est un int-quelquechose") qui permet de faire remonter des erreurs assez tôt pendant la compilation

Ben non justement. Au lieu d’avoir l’erreur à la déclaration, tu as l’erreur à l’utilisation un peu (ou beaucoup !) plus loin qui force le compilo à caster une IR à précision arbitraire qui ne devrait même pas jaillir au nez de l’utilisateur (leaking of abstractions, ce genre de choses…). C’est à la fois source de confusion (cf l’existence même de ce sujet), et ça n’apporte en pratique rien de plus qu’un type explicite (ou à minima attribuer un type par défaut qui est représentable avec les types exposés par le langage, genre int64, plutôt qu’un truc complètement arbitraire qui sent le choix fait au doigt mouillé en se disant "ça passera dans le futur proche, personne ne manipule des trucs plus gros que 256 bits").

Utiliser une telle constante rend le code fragile à l’utilisation de la constante ailleurs ("c’est quoi déjà la capacité qu’il me faut pour utiliser cette constante ?"), et fragile vis-à-vis des modifications de façon opaque ("j’ai ajouté 1 à cette constante, et maintenant mon code pète ailleurs parce que ça passe plus dans le type explicite choisi pour la manipuler"). Le moindre mal, c’est que ça casse à la compilation. Mais c’est une source de couplage (entre l’IR du compilo, la valeur de la constante, et le type à la manipulation) et un point de friction tellement simple à éviter qu’il n’est pas tellement réjouissant de la voir… :-° C’est pas pour rien que la plupart des langages modernes essaient d’éliminer ou de museler à mort les trucs qui ressemblent à des directives de préprocesseurs d’une part, et les conversions implicites d’autre part.

+0 -0

Mais du coup, mis à part "faire comme le "C", quel est l’intérêt réel de ne pas avoir complètement typé les constantes ? Je n’arrive pas à voir de cas d’application où ça serait utile.

SpaceFox

Mais on peut tout à fait typer les constantes. C’est juste qu’il existe de nombreux cas dans lesquels on peut vouloir créer une constante sans avoir encore décidé quel sera le type dont on se servira pour la manipuler.

Si je prends par exemple le nombre maximum de joueurs qu’on autorise à se connecter à un serveur relais avant d’en spawner d’autres (le genre de constante que j’utilise quotidiennement en Go). Ce genre de quantité a peu de chances de dépasser 8 bits, même signés, mais tu peux très bien avoir une API (disons l’API Kubernetes) qui passe ce nombre sous la forme d’un int et une autre (disons le service de matchmaking qui indique à chaque utilisateur à quelle adresse il doit se connecter) qui le comprend comme une int64. Quand ce cas se présente on veut être capable de comparer les valeurs des deux API à cette constante commune. Et dans ce cas ça n’a pas de sens de donner à cette constante le type de l’un ou de l’autre.

Très franchement, du moment que l’erreur pète à la compilation (et donc, dans le cas de Go, détectée probablement à peine quelques secondes après avoir écrit le code), je ne vois pas vraiment de raison de se prendre la tête là-dessus ni d’exiger plus que ça.

Tout ce que je vois, c’est que quand j’ai besoin de déclarer une constante, je n’ai pas besoin de réfléchir à son type, et quand je veux changer le type du code qui utilise cette constante, je n’ai pas à retoucher à la déclaration de la constante. Dans la pratique la charge cognitive associée à l’utilisation de ces constantes est nulle, et c’est tout ce que j’attends d’elles. Ce sont des constantes, quoi.

Je comprends vraiment pas cette propension à se prémunir à tout prix contre des problématiques qui, quand elles existent (ce qui est déjà rare de base), ont un impact au mieux anecdotique sur le quotidien des développeurs : autrement dit, l’inférence de type qui provoque une erreur à la compilation, en 30 secondes c’est corrigé, en 1 minute c’est oublié, et l’erreur dans le code n’a strictement aucune chance n’atterrir en prod. On peut pinailler pendant des heures sur la phase de la compilation pendant laquelle cette erreur devrait remonter, mais très franchement ça ne m’intéresse pas. Je suis un développeur, je suis l’utilisateur du compilateur, et tout ce que je veux c’est qu’il me montre quand mon code comporte une incohérence au moment où je sauvegarde mon fichier : c’est exactement ce qui se passe avec ce système.

C’est pas pour rien que la plupart des langages modernes essaient d’éliminer ou de museler à mort les trucs qui ressemblent à des directives de préprocesseurs d’une part, et les conversions implicites d’autre part.

Tousse en macro Rust.

+1 -0

C’est juste qu’il existe de nombreux cas dans lesquels on peut vouloir créer une constante sans avoir encore décidé quel sera le type dont on se servira pour la manipuler.

nohar

C’est un cas qui ne me parle pas, mais si j’en crois ce que tu dis ici, c’est probablement parce que les langages que j’utilise semblent plus souples que Go :

Si je prends par exemple le nombre maximum de joueurs qu’on autorise à se connecter à un serveur relais avant d’en spawner d’autres (le genre de constante que j’utilise quotidiennement en Go). Ce genre de quantité a peu de chances de dépasser 8 bits, même signés, mais tu peux très bien avoir une API (disons l’API Kubernetes) qui passe ce nombre sous la forme d’un int et une autre (disons le service de matchmaking qui indique à chaque utilisateur à quelle adresse il doit se connecter) qui le comprend comme une int64. Quand ce cas se présente on veut être capable de comparer les valeurs des deux API à cette constante commune. Et dans ce cas ça n’a pas de sens de donner à cette constante le type de l’un ou de l’autre.

Par exemple, je peux très bien écrire :

private static final byte MAX_PLAYERS = 122;
private long matchmakingMaxPlayers = MAX_PLAYERS;

Et ça fonctionnera sans problème de compilation. J’ai l’impression, d’après ce que tu décris, que ça n’est pas le cas de Go.

Évidemment l’inverse plantera, mais si je spécifie d’avance ma constante dans le type dont je pense avoir besoin et que je me rends compte que ce type ne rentre pas dans une API, c’est que j’ai sans doute un problème de conception ou au moins une adaptation à faire quelque part.

Je comprends vraiment pas cette propension à se prémunir à tout prix contre des problématiques qui, quand elles existent (ce qui est déjà rare de base), ont un impact au mieux anecdotique sur le quotidien des développeurs : autrement dit, l’inférence de type qui provoque une erreur à la compilation, en 30 secondes c’est corrigé, en 1 minute c’est oublié, et l’erreur dans le code n’a strictement aucune chance n’atterrir en prod.

nohar

Pour moi : le principe de moindre surprise, qui est une solution que j’apprécie particulièrement – sans jugement de valeur sur d’autres façons de faire. PS : la question est souvent de savoir comment créer une facilité d’écriture en évitant les cas pièges, et c’est très souvent une problématique qui revient à déterminer : quand est-ce que ça ne fonctionne pas comme on pourrait s’y attendre au premier coup d’œil ?

Si j’en crois ton expérience, ce cas-ci n’est pas vraiment un problème en Go. Je le rapproche donc de l’auto(un)boxing en Java, qui fonctionne exactement comme prévu dans 99,99 % des cas et qui, dans certains cas très précis, ajoute des subtilités qui doivent être prises en compte – avec le défaut que dans ce cas particulier, ça plante à l’exécution. Inversement, un cas très connu de simplicité d’écriture qui pose problème, c’est le == en Javascript, dont le comportement est pourri de pièges.

Et ça fonctionnera sans problème de compilation. J’ai l’impression, d’après ce que tu décris, que ça n’est pas le cas de Go.

Effectivement, en Go t’aurais une erreur à la compilation qui te forcerait à faire une conversion de byte vers int64 pour faire la comparaison. Alors que la comparaison avec une constante littérale ne pose aucun problème.

Dans ce cas pour moi ça a du sens de pouvoir "nommer" une constante littérale. C’est exactement cette logique qui est implémentée dans les "constantes non typées" de Go, ce qui les rend intuitives à utiliser quotidiennement.

Rappelons que la question de ce thread c’était "comment ça se fait que l’erreur porte sur telle ligne et pas telle autre ?", plutôt que "comment ça se fait que j’aie une erreur ?". Dans les faits peu importe la ligne que précise l’erreur, on comprend vite que ce qui se passe est un cas limite du mécanisme d’inférence de type, et donc on réagit en typant explicitement la variable incriminée.

C’est pour ça que ce "débat" me fait hausser les épaules. J’ai l’impression qu’on est en train de faire une montagne d’un mécanisme qui rend le code plus intuitif à lire 99% du temps, et qui se règle en deux coups de cuiller à pot le jour où on tombe par mégarde sur un cas limite (et encore, replaçons les choses dans leur contexte, quand on fait du Go, on est conscient des subtilités sur les conversions entre types entiers, puisqu’on est amené à les choisir chaque fois qu’on déclare une structure, donc ce genre d’erreur ne laisse personne démuni, on comprend immédiatement ce qui se passe).

Partant de là j’ai vraiment l’impression qu’on est en plein bike shedding dogmatique sur la pureté de ce que devrait faire un compilateur, en oubliant juste que le but de Go et de ses utilisateurs est à des années lumière de ces considérations.

+0 -0

Le problème, c’est surtout que l’inférence est faite dans le sens contraire de celui-attendu. Au lieu d’avoir une constante typée qui transfère son type au code client de la constante, on a une constante non-typée et on essaie de la forcer à l’utilisation. C’est absurde, une telle constante perd de sa généralité : au lieu d’être une valeur complètement abstraite utilisable sans l’observer, t’es obligé en tant que client de choisir volontairement le bon type à l’utilisation et croiser les doigts pour que ça supporte les futurs valeurs potentielles. C’est un cas d’école flagrant de leaky abstraction.

Toi, tu t’en fous peut être royalement et grand bien t’en fasses. Moi je trouve ça inquiétant de trouver ce problème dans le design même du langage parce que je suis un bike shedder dogmatique, ou peut être que c’est parce que j’ai pas les même exigences de qualité sur les outils que j’utilise. Tu peux personnellement hausser les épaules face à la question, c’est pas franchement une raison pour dire que la critique ne mérite pas d’être soulevée.

+0 -0

Toi, tu t’en fous peut être royalement et grand bien t’en fasses.

Je m’en fous royalement parce mon travail, ma responsabilité première, c’est d’écrire un code qui réponde au besoin de mes utilisateurs et que je puisse maintenir dans la durée, en étant capable de le comprendre au premier coup d’oeil quand je reviens dessus 6 mois après l’avoir écrit.

Ça s’appelle le pragmatisme, et c’est la valeur centrale de la façon dont j’exerce mon métier, aussi bien que du langage que j’utilise pour cela. Et c’est quelque chose que j’aimerais qu’on garde à l’esprit. Je me tape royalement de "l’élégance" théorique de mon système de type : ce qui m’importe c’est que les gens puissent s’authentifier et se connecter à mon application, et que si ça tombe en panne je mette moins de 5 minutes à comprendre ce qui se passe et publier un fix.

Et c’est pour ça que Go a été créé et c’est pour ça que les gens l’utilisent. Partant de là, oui, venir pinailler sur les soit-disant problèmes de conception de son compilateur, c’est à la fois hors sujet et faire preuve de dogmatisme et de mauvaise foi, surtout si c’est pour le comparer à des langages prétendument "modernes" (sous-entendu Rust1) dont les objectifs n’ont rien à voir (c’est pas comme si tous les acteurs de l’industrie qui en sont utilisateurs disaient depuis quelques années que les deux langages sont complémentaires dans leur utilisation et que l’utilisation conjointe des deux était un win pour eux…).

Alors certes, c’est ton droit de pinailler en vertu de ta conception personnelle de ce qu’est la "qualité", mais personnellement j’y vois surtout un cruel manque de recul et/ou de compréhension du métier des gens qui utilisent cette techno.

Tout ceci rend cette discussion à la fois désagréable et stérile.


  1. À ce propos, la remarque sur les langages "modernes" qui font tout pour nerfer ce qui ressemble au preproc du C me fait beaucoup rire parce que les macros de Rust sont non seulement l’unique tentative de les reproduire qu’on aie vu dans un langage "moderne" ces 20 dernières années, mais en plus à l’utilisation on se retrouve finalement face à la dure réalité : c’est tout aussi sale conceptuellement, et limite encore plus délicat à utiliser correctement, sans parler de l’écueil en termes de complexité, de courbe d’apprentissage et de charge cognitive pour les utilisateurs, qui consiste à créer un langage dans un langage.
+2 -1

On va peut-être se calmer sur les jugements et accusations qui ne relèvent pas du sujet, vous pensez pas ? :)

Merci de rester courtois et d’éviter les jugements malvenus pour se concentrer sur le sujet dans la bonne humeur.


N’hésitez pas à prendre un moment pour respirer si une remarque ne vous plait pas : ce n’est pas nécessairement une critique de votre personne.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte