En essayant de déboguer un problème dans mon programme (2 cercles avec un rayon égal sont dessinés à différentes tailles en utilisant Gloss *
), je suis tombé sur une situation étrange. Dans mon fichier qui gère les objets, j'ai la définition suivante pour un Player
:
type Coord = (Float,Float)
data Obj = Player { oPos :: Coord, oDims :: Coord }
et dans mon fichier principal, qui importe Objects.hs, j'ai la définition suivante:
startPlayer :: Obj
startPlayer = Player (0,0) 10
Cela s'est produit parce que j'ai ajouté et modifié des champs pour le joueur, et startPlayer
que j'ai oublié de mettre à jour après (ses dimensions étaient déterminées par un seul nombre pour représenter un rayon, mais je l'ai changé en un Coord
pour représenter (largeur, hauteur); au cas où je ferais jamais le joueur objecte un non-cercle).
Ce qui est étonnant, c'est que le code ci-dessus se compile et s'exécute, bien que le deuxième champ soit du mauvais type.
J'ai d'abord pensé que j'avais peut-être différentes versions des fichiers ouvertes, mais toutes les modifications apportées aux fichiers étaient reflétées dans le programme compilé.
Ensuite, j'ai pensé que cela startPlayer
n'était peut-être pas utilisé pour une raison quelconque. La mise en commentaire startPlayer
produit une erreur de compilation, et même plus étrange, la modification de 10
in startPlayer
provoque une réponse appropriée (change la taille de départ de Player
); encore une fois, bien qu'il soit du mauvais type. Pour m'assurer qu'il lit correctement la définition des données, j'ai inséré une faute de frappe dans le fichier, et cela m'a donné une erreur; donc je regarde le bon fichier.
J'ai essayé de coller les 2 extraits ci-dessus dans leur propre fichier, et cela a craché l'erreur attendue que le deuxième champ de Player
in startPlayer
est incorrect.
Qu'est-ce qui pourrait permettre que cela se produise? On pourrait penser que c'est précisément ce que le vérificateur de type de Haskell devrait empêcher.
*
La réponse à mon problème initial, deux cercles de rayon supposé égal étant dessinés à des tailles différentes, était que l'un des rayons était en fait négatif.
Point
unnewtype
ou utiliser d'autres noms d'opérateurs alalinear
)Réponses:
La seule façon dont cela pourrait éventuellement se compiler est s'il existe une
Num (Float,Float)
instance. Cela n'est pas fourni par la bibliothèque standard, bien qu'il soit possible que l'une des bibliothèques que vous utilisez l'ait ajoutée pour une raison insensée. Essayez de charger votre projet dans ghci et voyez si cela10 :: (Float,Float)
fonctionne, puis essayez:i Num
de savoir d'où vient l'instance, puis criez à celui qui l'a définie.Addendum: il n'y a aucun moyen de désactiver les instances. Il n'y a même pas moyen de ne pas les exporter à partir d'un module. Si cela était possible, cela conduirait à un code encore plus déroutant. La seule vraie solution ici est de ne pas définir de telles instances.
la source
10 :: (Float, Float)
renvoie(10.0,10.0)
et:i Num
contient la ligneinstance Num Point -- Defined in ‘Graphics.Gloss.Data.Point’
(Point
est l'alias de Gloss de Coord). Sérieusement? Je vous remercie. Cela m'a sauvé d'une nuit sans sommeil.Num
là où cela a du sens, comme unAngle
type de données qui contraint unDouble
entre-pi
etpi
, ou si quelqu'un voulait écrire un type de données représentant des quaternions ou un autre type numérique plus complexe, cette fonction est très pratique. Il suit également les mêmes règles queString
/Text
/ByteString
, autoriser ces instances a du sens du point de vue de la facilité d'utilisation, mais il peut être mal utilisé comme dans ce cas.newtype
déclaration pourCoord
au lieu d'untype
.Num (Float, Float)
ou même(Floating a) => Num (a,a)
ne nécessiterait pas l'extension mais entraînerait le même comportement.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 instanceNum (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 lefromInteger
problèmeVous êtes surpris que le compilateur accepte
10 :: Coord
, c'est à dire10 :: (Float, Float)
. Il est raisonnable de supposer que les littéraux numériques comme10
seront déduits pour avoir des types "numériques". Hors de la boîte, les littéraux numériques peuvent être interprétées commeInt
,Integer
,Float
ouDouble
. 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 deComplex
.Heureusement ou malheureusement, Haskell est un langage très flexible. La norme spécifie qu'un entier littéral comme
10
sera interprété commefromInteger 10
, qui a un typeNum a => a
. Ainsi10
pourrait être déduit comme n'importe quel type pour lequel uneNum
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 commeNum a => Num (a, a)
ouNum (Float, Float)
. Il n'y a pas d'exemple de ce genre dans lePrelude
, donc il doit avoir été défini ailleurs. En utilisant:i Num
, vous avez rapidement repéré d'où il venait: legloss
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 questiongloss
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'écritureCoord
est simplement un raccourci pour(Float, Float)
. De même dansGraphics.Gloss.Data.Point
,Point
signifie(Float, Float)
. En d' autres termes, vosCoord
etgloss
« sPoint
sont littéralement équivalent.Ainsi, lorsque les
gloss
responsables ont choisi d'écrireinstance Num Point where ...
, ils ont également fait de votreCoord
type une instance deNum
. C'est équivalent àinstance Num (Float, Float) where ...
ouinstance 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 langageTypeSynonymInstances
etFlexibleInstances
, 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 deuxC
etA
sont définis dans d'autres modules. Ici, c'est particulièrement insidieux car chaque partie impliquée, c'est-à-direNum
,(,)
etFloat
, vient duPrelude
et est susceptible d'être présente partout.Vous vous attendez à ce que ce
Num
soit défini dansPrelude
, et les tuples etFloat
sont définis dansPrelude
, donc tout ce qui concerne le fonctionnement de ces trois éléments est défini dansPrelude
. 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 lefromInteger
problème - non, définirfromInteger = 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 Kmettlinear
(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'ajoutenttransformers
. 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 detype
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.
la source