Le vérificateur de type de Haskell est raisonnable. Le problème est que les auteurs d'une bibliothèque que vous utilisez ont fait quelque chose ... de moins raisonnable.
La réponse brève est: Oui, 10 :: (Float, Float)
c'est parfaitement valable s'il y a une instance Num (Float, Float)
. Il n'y a rien de "très mal" à ce sujet du point de vue du compilateur ou du langage. Cela ne correspond tout simplement pas à notre intuition sur ce que font les littéraux numériques. Puisque vous êtes habitué au système de typage qui détecte le genre d'erreur que vous avez fait, vous êtes à juste titre surpris et déçu!
Num
les instances et le fromInteger
problème
Vous êtes surpris que le compilateur accepte 10 :: Coord
, c'est à dire 10 :: (Float, Float)
. Il est raisonnable de supposer que les littéraux numériques comme 10
seront déduits pour avoir des types "numériques". Hors de la boîte, les littéraux numériques peuvent être interprétées comme Int
, Integer
, Float
ou Double
. Un tuple de nombres, sans autre contexte, ne semble pas être un nombre au sens où ces quatre types sont des nombres. Nous ne parlons pas de Complex
.
Heureusement ou malheureusement, Haskell est un langage très flexible. La norme spécifie qu'un entier littéral comme 10
sera interprété comme fromInteger 10
, qui a un type Num a => a
. Ainsi 10
pourrait être déduit comme n'importe quel type pour lequel une Num
instance a été écrite. J'explique cela un peu plus en détail dans une autre réponse .
Ainsi, lorsque vous avez posté votre question, un Haskeller expérimenté l'a immédiatement repérée pour 10 :: (Float, Float)
être acceptée, il doit y avoir une instance comme Num a => Num (a, a)
ou Num (Float, Float)
. Il n'y a pas d'exemple de ce genre dans le Prelude
, donc il doit avoir été défini ailleurs. En utilisant :i Num
, vous avez rapidement repéré d'où il venait: le gloss
paquet.
Saisissez des synonymes et des instances orphelines
Mais attendez une minute. Vous n'utilisez aucun gloss
type dans cet exemple; pourquoi l'instance en question gloss
vous a-t-elle affecté? La réponse se fait en deux étapes.
Premièrement, un synonyme de type introduit avec le mot type
- clé ne crée pas de nouveau type . Dans votre module, l'écriture Coord
est simplement un raccourci pour (Float, Float)
. De même dans Graphics.Gloss.Data.Point
, Point
signifie (Float, Float)
. En d' autres termes, vos Coord
et gloss
« s Point
sont littéralement équivalent.
Ainsi, lorsque les gloss
responsables ont choisi d'écrire instance Num Point where ...
, ils ont également fait de votre Coord
type une instance de Num
. C'est équivalent à instance Num (Float, Float) where ...
ou instance Num Coord where ...
.
(Par défaut, Haskell n'autorise pas les synonymes de type à être des instances de classe. Les gloss
auteurs devaient activer une paire d'extensions de langage TypeSynonymInstances
et FlexibleInstances
, pour écrire l'instance.)
Deuxièmement, c'est surprenant car c'est une instance orpheline , c'est-à-dire une déclaration d'instance instance C A
où les deux C
et A
sont définis dans d'autres modules. Ici, c'est particulièrement insidieux car chaque partie impliquée, c'est-à-dire Num
, (,)
et Float
, vient du Prelude
et est susceptible d'être présente partout.
Vous vous attendez à ce que ce Num
soit défini dans Prelude
, et les tuples et Float
sont définis dans Prelude
, donc tout ce qui concerne le fonctionnement de ces trois éléments est défini dans Prelude
. Pourquoi importer un module complètement différent changerait-il quelque chose? Idéalement, ce ne serait pas le cas, mais les instances orphelines brisent cette intuition.
(Notez que GHC met en garde contre les instances orphelines - les auteurs ont gloss
spécifiquement ignoré cet avertissement. Cela aurait dû déclencher un drapeau rouge et déclencher au moins un avertissement dans la documentation.)
Les instances de classe sont globales et ne peuvent pas être masquées
De plus, les instances de classe sont globales : toute instance définie dans un module importé de manière transitoire depuis votre module sera en contexte et disponible pour le vérificateur de type lors de la résolution d'instance. Cela rend le raisonnement global pratique, car nous pouvons (généralement) supposer qu'une fonction de classe comme (+)
sera toujours la même pour un type donné. Cependant, cela signifie également que les décisions locales ont des effets globaux; la définition d'une instance de classe modifie irrévocablement le contexte du code en aval, sans aucun moyen de le masquer ou de le cacher derrière les limites du module.
Vous ne pouvez pas utiliser de listes d'importation pour éviter d'importer des instances . De même, vous ne pouvez pas éviter d'exporter des instances à partir des modules que vous définissez.
C'est un domaine problématique et très discuté de la conception du langage Haskell. Il y a une discussion fascinante sur les problèmes connexes dans ce fil de discussion reddit . Voir, par exemple, le commentaire d'Edward Kmett sur l'autorisation du contrôle de visibilité pour les instances: "Vous rejetez fondamentalement l'exactitude de presque tout le code que j'ai écrit."
(En passant, comme cette réponse l'a démontré , vous pouvez casser l'hypothèse d'instance globale à certains égards en utilisant des instances orphelines!)
Que faire - pour les développeurs de bibliothèques
Réfléchissez à deux fois avant de mettre en œuvre Num
. Vous ne pouvez pas contourner le fromInteger
problème - non, définir fromInteger = error "not implemented"
ne l’ améliore pas . Vos utilisateurs seront-ils confus ou surpris - ou pire, ne le remarqueront jamais - si leurs littéraux entiers sont supposés accidentellement avoir le type que vous instanciez? Est-ce que fournir (*)
et (+)
que cela est essentiel - en particulier si vous devez le pirater?
Pensez à utiliser d'autres opérateurs arithmétiques définis dans une bibliothèque comme celle de Conal Elliott vector-space
(pour les types de genre *
) ou d'Edward Kmett linear
(pour les types de genre * -> *
). C'est ce que j'ai tendance à faire moi-même.
Utilisez -Wall
. N'implémentez pas d'instances orphelines et ne désactivez pas l'avertissement d'instance orpheline.
Sinon, suivre l'exemple de linear
et beaucoup d' autres bibliothèques, et fournir des instances orphelines se sont bien comportés dans un module séparé se terminant par .OrphanInstances
ou .Instances
. Et n'importez pas ce module à partir d'un autre module . Ensuite, les utilisateurs peuvent importer les orphelins explicitement s'ils le souhaitent.
Si vous vous trouvez en train de définir des orphelins, pensez à demander aux responsables en amont de les implémenter à la place, si possible et approprié. J'écrivais fréquemment l'instance orpheline Show a => Show (Identity a)
, jusqu'à ce qu'ils l'ajoutent transformers
. J'ai peut-être même signalé un bug à ce sujet; Je ne m'en souviens pas.
Que faire - pour les utilisateurs de bibliothèques
Vous n'avez pas beaucoup d'options. Contactez - poliment et constructivement! - les responsables de la bibliothèque. Dirigez-les vers cette question. Ils peuvent avoir eu une raison particulière d'écrire l'orphelin problématique, ou ils peuvent simplement ne pas s'en rendre compte.
Plus largement: soyez conscient de cette possibilité. C'est l'une des rares régions de Haskell où il y a de véritables effets globaux; vous devez vérifier que chaque module que vous importez, et chaque module que ces modules importent, n'implémente pas d'instances orphelines. Les annotations de type peuvent parfois vous alerter en cas de problèmes, et vous pouvez bien sûr les utiliser :i
dans GHCi pour vérifier.
Définissez vos propres newtype
s au lieu de type
synonymes si elle est assez important. Vous pouvez être sûr que personne ne les dérangera.
Si vous rencontrez fréquemment des problèmes liés à une bibliothèque open-source, vous pouvez bien sûr créer votre propre version de la bibliothèque, mais la maintenance peut rapidement devenir un casse-tête.