Instances orphelines dans Haskell

86

Lors de la compilation de mon application Haskell avec l' -Walloption, GHC se plaint des instances orphelines, par exemple:

Publisher.hs:45:9:
    Warning: orphan instance: instance ToSElem Result

La classe de type ToSElemn'est pas la mienne, elle est définie par HStringTemplate .

Maintenant, je sais comment résoudre ce problème (déplacez la déclaration d'instance dans le module où Result est déclaré), et je sais pourquoi GHC préférerait éviter les instances orphelines , mais je pense toujours que mon chemin est meilleur. Peu m'importe si le compilateur est incommodé - plutôt lui que moi.

La raison pour laquelle je souhaite déclarer mes ToSEleminstances dans le module Publisher est que c'est le module Publisher qui dépend de HStringTemplate, et non des autres modules. J'essaie de maintenir une séparation des préoccupations et d'éviter que chaque module dépende de HStringTemplate.

Je pensais que l'un des avantages des classes de types de Haskell, par rapport par exemple aux interfaces de Java, est qu'elles sont ouvertes plutôt que fermées et que les instances n'ont donc pas à être déclarées au même endroit que le type de données. Le conseil de GHC semble être d'ignorer cela.

Donc, ce que je recherche, c'est soit une confirmation que ma pensée est saine et que je serais justifié d'ignorer / supprimer cet avertissement, ou un argument plus convaincant contre faire les choses à ma façon.

Dan Dyer
la source
La discussion dans les réponses et les commentaires montre qu'il existe une grande différence entre la définition d'instances orphelines dans un exécutable , comme vous le faites, et dans une bibliothèque exposée à d'autres. Cette question extrêmement populaire illustre à quel point les instances orphelines peuvent être déroutantes pour les utilisateurs finaux d'une bibliothèque qui les définit.
Christian Conkle

Réponses:

94

Je comprends pourquoi vous voulez faire cela, mais malheureusement, ce n'est peut-être qu'une illusion que les classes Haskell semblent être "ouvertes" comme vous le dites. Beaucoup de gens pensent que la possibilité de faire cela est un bogue dans la spécification Haskell, pour des raisons que je vais expliquer ci-dessous. Quoi qu'il en soit, si ce n'est vraiment pas approprié pour l'instance, vous devez être déclaré soit dans le module où la classe est déclarée, soit dans le module où le type est déclaré, c'est probablement un signe que vous devriez utiliser un newtypeou un autre wrapper autour de votre type.

Les raisons pour lesquelles les instances orphelines doivent être évitées sont bien plus profondes que la commodité du compilateur. Ce sujet est plutôt controversé, comme vous pouvez le voir à partir d'autres réponses. Pour équilibrer la discussion, je vais expliquer le point de vue qu'il ne faut jamais, jamais, écrire des instances orphelines, ce qui, je pense, est l'opinion majoritaire parmi les Haskeller expérimentés. Ma propre opinion se situe quelque part au milieu, ce que j'expliquerai à la fin.

Le problème vient du fait que lorsqu'il existe plus d'une déclaration d'instance pour la même classe et le même type, il n'y a pas de mécanisme dans Haskell standard pour spécifier lequel utiliser. Au contraire, le programme est rejeté par le compilateur.

L'effet le plus simple de cela est que vous pourriez avoir un programme parfaitement fonctionnel qui arrêterait soudainement la compilation à cause d'un changement que quelqu'un d'autre fait dans une dépendance éloignée de votre module.

Pire encore, il est possible qu'un programme fonctionnel commence à planter au moment de l'exécution en raison d'un changement distant. Vous pourriez utiliser une méthode dont vous supposez qu'elle provient d'une certaine déclaration d'instance, et elle pourrait être silencieusement remplacée par une instance différente qui est juste assez différente pour que votre programme se mette inexplicablement en panne.

Les personnes qui veulent des garanties que ces problèmes ne leur arriveront jamais doivent suivre la règle selon laquelle si quelqu'un, n'importe où, a déjà déclaré une instance d'une certaine classe pour un certain type, aucune autre instance ne doit plus jamais être déclarée dans un programme écrit par n'importe qui. Bien sûr, il existe une solution de contournement consistant à utiliser a newtypepour déclarer une nouvelle instance, mais c'est toujours au moins un inconvénient mineur, et parfois majeur. Donc, dans ce sens, ceux qui écrivent intentionnellement des instances orphelines sont plutôt impolis.

Alors, que faut-il faire pour résoudre ce problème? Le camp anti-instance orpheline dit que l'avertissement GHC est un bogue, il doit s'agir d'une erreur qui rejette toute tentative de déclaration d'une instance orpheline. En attendant, nous devons faire preuve d'autodiscipline et les éviter à tout prix.

Comme vous l'avez vu, il y a ceux qui ne s'inquiètent pas autant de ces problèmes potentiels. Ils encouragent en fait l'utilisation d'instances orphelines comme outil de séparation des préoccupations, comme vous le suggérez, et disent qu'il faut simplement s'assurer au cas par cas qu'il n'y a pas de problème. J'ai été assez souvent incommodé par les instances orphelines d'autres personnes pour être convaincu que cette attitude est trop cavalière.

Je pense que la bonne solution serait d'ajouter une extension au mécanisme d'importation de Haskell qui contrôlerait l'importation des instances. Cela ne résoudrait pas complètement les problèmes, mais cela aiderait à protéger nos programmes contre les dommages causés par les instances orphelines qui existent déjà dans le monde. Et puis, avec le temps, je pourrais devenir convaincu que dans certains cas limités, peut-être qu'une instance orpheline pourrait ne pas être si grave. (Et cette tentation même est la raison pour laquelle certains membres du camp anti-orphelin sont opposés à ma proposition.)

Ma conclusion à partir de tout cela est qu'au moins pour le moment, je vous conseillerais fortement d'éviter de déclarer des instances orphelines, d'être prévenant envers les autres si ce n'est pour une autre raison. Utilisez un newtype.

Yitz
la source
4
En particulier, c'est de plus en plus un problème avec la croissance des bibliothèques. Avec plus de 2200 bibliothèques sur Haskell et des dizaines de milliers de modules individuels, le risque de récupérer des instances augmente considérablement.
Don Stewart
16
Re: "Je pense que la bonne solution serait d'ajouter une extension au mécanisme d'importation de Haskell qui contrôlerait l'importation des instances" Au cas où cette idée intéresse quelqu'un, il pourrait être intéressant de regarder le langage Scala pour un exemple; il a des fonctionnalités très similaires à celles-ci pour contrôler la portée des «implicits», qui peuvent être utilisées comme des instances de classe de types.
Matt
5
Mon logiciel est une application plutôt qu'une bibliothèque, donc la possibilité de causer des problèmes à d'autres développeurs est quasiment nulle. Vous pourriez considérer le module Publisher comme l'application et le reste des modules comme une bibliothèque, mais si je devais distribuer la bibliothèque, ce serait sans l'éditeur et, par conséquent, les instances orphelines. Mais si je déplaçais les instances dans les autres modules, la bibliothèque serait livrée avec une dépendance inutile sur HStringTemplate. Donc, dans ce cas, je pense que les orphelins vont bien, mais j'écouterai votre avis si je rencontre le même problème dans un contexte différent.
Dan Dyer
1
Cela semble être une approche raisonnable. La seule chose à surveiller est alors si l'auteur d'un module que vous importez ajoute cette instance dans une version ultérieure. Si cette instance est la même que la vôtre, vous devrez supprimer votre propre déclaration d'instance. Si cette instance est différente de la vôtre, vous devrez mettre un wrapper newtype autour de votre type - ce qui pourrait être une refactorisation significative de votre code.
Yitz
@Matt: en effet, étonnamment, Scala obtient celui-ci là où Haskell ne le fait pas! (sauf bien sûr que Scala manque de syntaxe de première classe pour les machines de classe de type, ce qui est encore pire ...)
Erik Kaplun
44

Allez-y et supprimez cet avertissement!

Vous êtes en bonne compagnie. Conal le fait dans "TypeCompose". "chp-mtl" et "chp-transformers" le font, "control-monad-exception-mtl" et "control-monad-exception-monadsfd" le font, etc.

btw vous le savez probablement déjà, mais pour ceux qui ne le font pas et trébuchent sur votre question sur une recherche:

{-# OPTIONS_GHC -fno-warn-orphans #-}

Éditer:

Je reconnais que les problèmes mentionnés par Yitz dans sa réponse sont de vrais problèmes. Cependant, je ne vois pas non plus l'utilisation d'instances orphelines comme un problème, et j'essaie de choisir le "moindre de tous les maux", qui est à mon humble avis d'utiliser prudemment les instances orphelines.

Je n'ai utilisé qu'un point d'exclamation dans ma réponse courte parce que votre question montre que vous êtes déjà bien conscient des problèmes. Sinon, j'aurais été moins enthousiaste :)

Un peu de diversion, mais ce que je crois est la solution parfaite dans un monde parfait sans compromis:

Je crois que les problèmes mentionnés par Yitz (ne sachant pas quelle instance est choisie) pourraient être résolus dans un système de programmation "holistique" où:

  • Vous n'éditez pas de simples fichiers texte de manière primitive, mais êtes plutôt aidé par l'environnement (par exemple, l'achèvement de code ne suggère que des éléments de types pertinents, etc.)
  • Le langage de "niveau inférieur" n'a pas de support spécial pour les classes de types, et à la place les tables de fonctions sont transmises explicitement
  • Mais, l'environnement de programmation "de niveau supérieur" affiche le code de la même manière que la façon dont Haskell est présenté maintenant (vous ne verrez généralement pas les tables de fonctions transmises), et choisit les classes de types explicites pour vous quand elles sont évidentes (pour exemple tous les cas de Functor n'ont qu'un seul choix) et quand il y a plusieurs exemples (zipping list Applicative ou list-monad Applicative, First / Last / Lift peut-être Monoid) il vous permet de choisir quelle instance utiliser.
  • Dans tous les cas, même lorsque l'instance a été sélectionnée automatiquement pour vous, l'environnement vous permet facilement de voir quelle instance a été utilisée, avec une interface simple (un lien hypertexte ou une interface de survol ou quelque chose)

De retour du monde fantastique (ou, espérons-le, du futur), maintenant: je recommande d'essayer d'éviter les instances orphelines tout en continuant de les utiliser lorsque vous "avez vraiment besoin"

Yairchu
la source
5
Oui, mais chacun de ces événements est sans doute une erreur d'un certain ordre. Les mauvaises instances dans control-monad-exception-mtl et monads-fd pour Either viennent à l'esprit. Il serait moins gênant que chacun de ces modules soit forcé de définir ses propres types ou de fournir des wrappers de nouveaux types. Presque chaque instance orpheline est un casse-tête en attente de se produire, et si rien d'autre ne nécessitera votre vigilance constante pour vous assurer qu'elle est importée ou non comme il convient.
Edward KMETT
2
Merci. Je pense que je vais les utiliser dans cette situation particulière, mais grâce à Yitz, j'ai maintenant une meilleure idée des problèmes qu'ils peuvent causer.
Dan Dyer
37

Les instances orphelines sont une nuisance, mais à mon avis elles sont parfois nécessaires. Je combine souvent des bibliothèques où un type provient d'une bibliothèque et une classe d'une autre bibliothèque. Bien entendu, on ne peut pas s'attendre à ce que les auteurs de ces bibliothèques fournissent des instances pour chaque combinaison imaginable de types et de classes. Je dois donc les fournir, et ils sont donc orphelins.

L'idée que vous devez envelopper le type dans un nouveau type lorsque vous devez fournir une instance est une idée qui a un mérite théorique, mais c'est trop fastidieux dans de nombreuses circonstances; c'est le genre d'idée proposée par les gens qui n'écrivent pas de code Haskell pour gagner leur vie. :)

Alors allez-y et fournissez des instances orphelines. Ils sont inoffensifs.
Si vous pouvez planter ghc avec des instances orphelines, c'est un bogue qui doit être signalé comme tel. (Le bogue que ghc avait / a à propos de ne pas détecter plusieurs instances n'est pas si difficile à corriger.)

Mais sachez que dans le futur, quelqu'un d'autre pourrait ajouter une instance comme vous l'avez déjà, et vous pourriez obtenir une erreur (lors de la compilation).

augustes
la source
2
Un bon exemple est l' (Ord k, Arbitrary k, Arbitrary v) ⇒ Arbitrary (Map k v)utilisation de QuickCheck.
Erik Kaplun
17

Dans ce cas, je pense que l'utilisation d'instances orphelines est bien. La règle générale pour moi est la suivante: vous pouvez définir une instance si vous "possédez" la classe de types ou si vous "possédez" le type de données (ou un composant de celui-ci - c'est-à-dire qu'une instance pour Maybe MyData convient également, au moins parfois). Dans ces limites, l'endroit où vous décidez de placer l'instance est votre propre entreprise.

Il y a une autre exception: si vous ne possédez ni la classe de types ni le type de données, mais que vous produisez un binaire et non une bibliothèque, alors c'est bien aussi.

sclv
la source
5

(Je sais que je suis en retard à la fête mais cela peut être utile aux autres)

Vous pouvez conserver les instances orphelines dans leur propre module, alors si quelqu'un importe ce module, c'est spécifiquement parce qu'il en a besoin et il peut éviter de les importer s'ils posent des problèmes.

Trystan Spangler
la source
3

Dans ce sens, je comprends les bibliothèques WRT de position du camp d'instances anti-orphelines, mais pour les cibles exécutables, les instances orphelines ne devraient-elles pas convenir?

mxc
la source
3
Pour ce qui est d'être impoli envers les autres, vous avez raison. Mais vous vous ouvrez à des problèmes futurs potentiels si la même instance est définie dans le futur quelque part dans votre chaîne de dépendances. Donc, dans ce cas, c'est à vous de décider si cela vaut le risque.
Yitz le
5
Dans presque tous les cas d'implémentation d'une instance orpheline dans un exécutable, c'est pour combler une lacune que vous souhaiteriez déjà définie pour vous. Donc, si l'instance apparaît en amont, l'erreur de compilation qui en résulte n'est qu'un signal utile pour vous dire que vous pouvez supprimer votre déclaration de l'instance.
Ben