Verwaiste Instanzen in Haskell

85

Beim Kompilieren meiner Haskell-Anwendung mit der -WallOption beschwert sich GHC über verwaiste Instanzen, zum Beispiel:

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

Die Typklasse ToSElemgehört nicht mir, sondern wird von HStringTemplate definiert .

Jetzt weiß ich, wie ich das beheben kann (verschieben Sie die Instanzdeklaration in das Modul, in dem das Ergebnis deklariert ist), und ich weiß, warum GHC verwaiste Instanzen lieber vermeiden möchte , aber ich glaube immer noch, dass mein Weg besser ist. Es ist mir egal, ob der Compiler unangenehm ist - eher als ich.

Der Grund, warum ich meine ToSElemInstanzen im Publisher-Modul deklarieren möchte, ist, dass das Publisher-Modul von HStringTemplate abhängt und nicht von den anderen Modulen. Ich versuche, eine Trennung der Bedenken aufrechtzuerhalten und zu vermeiden, dass jedes Modul von HStringTemplate abhängt.

Ich dachte, dass einer der Vorteile von Haskells Typklassen im Vergleich zu Javas Schnittstellen darin besteht, dass sie offen und nicht geschlossen sind und daher die Instanzen nicht an derselben Stelle wie der Datentyp deklariert werden müssen. Der Rat von GHC scheint zu sein, dies zu ignorieren.

Was ich also suche, ist entweder eine Bestätigung, dass mein Denken vernünftig ist und dass ich berechtigt wäre, diese Warnung zu ignorieren / zu unterdrücken, oder ein überzeugenderes Argument dagegen, Dinge auf meine Weise zu tun.

Dan Dyer
quelle
Die Diskussion in den Antworten und Kommentaren zeigt, dass es einen großen Unterschied zwischen der Definition verwaister Instanzen in einer ausführbaren Datei wie in einer Bibliothek gibt , die anderen zugänglich ist. Diese äußerst beliebte Frage zeigt, wie verwirrend verwaiste Instanzen für Endbenutzer einer Bibliothek sein können, die sie definiert.
Christian Conkle

Antworten:

94

Ich verstehe, warum Sie dies tun möchten, aber leider kann es nur eine Illusion sein, dass Haskell-Klassen so "offen" zu sein scheinen, wie Sie es sagen. Viele Leute glauben, dass die Möglichkeit, dies zu tun, ein Fehler in der Haskell-Spezifikation ist, aus Gründen, die ich unten erläutern werde. Wenn es für die Instanz wirklich nicht geeignet ist, müssen Sie entweder in dem Modul deklariert werden, in dem die Klasse deklariert ist, oder in dem Modul, in dem der Typ deklariert ist. Dies ist wahrscheinlich ein Zeichen dafür, dass Sie einen newtypeoder einen anderen Wrapper verwenden sollten um Ihren Typ.

Die Gründe, warum verwaiste Instanzen vermieden werden müssen, liegen weit über dem Komfort des Compilers. Dieses Thema ist ziemlich kontrovers, wie Sie anderen Antworten entnehmen können. Um die Diskussion auszugleichen, werde ich den Standpunkt erläutern, dass man niemals verwaiste Instanzen schreiben sollte, was meiner Meinung nach die Mehrheitsmeinung unter erfahrenen Haskellern ist. Meine eigene Meinung ist irgendwo in der Mitte, was ich am Ende erklären werde.

Das Problem ergibt sich aus der Tatsache, dass es in Standard-Haskell keinen Mechanismus gibt, um anzugeben, welche verwendet werden soll, wenn mehr als eine Instanzdeklaration für dieselbe Klasse und denselben Typ vorhanden ist. Vielmehr wird das Programm vom Compiler abgelehnt.

Der einfachste Effekt davon ist, dass Sie ein perfekt funktionierendes Programm haben könnten, das plötzlich aufgrund einer Änderung, die jemand anderes in einer weit entfernten Abhängigkeit von Ihrem Modul vornimmt, nicht mehr kompiliert wird.

Schlimmer noch, es ist möglich, dass ein Arbeitsprogramm zur Laufzeit aufgrund einer entfernten Änderung abstürzt . Sie könnten eine Methode verwenden, von der Sie annehmen, dass sie aus einer bestimmten Instanzdeklaration stammt, und sie könnte stillschweigend durch eine andere Instanz ersetzt werden, die gerade so unterschiedlich ist, dass Ihr Programm unerklärlich abstürzt.

Personen, die garantieren möchten, dass ihnen diese Probleme niemals passieren, müssen die Regel befolgen, dass, wenn irgendjemand irgendwo jemals eine Instanz einer bestimmten Klasse für einen bestimmten Typ deklariert hat, keine andere Instanz in einem geschriebenen Programm jemals wieder deklariert werden darf von jemandem. Natürlich gibt es die Problemumgehung newtype, eine neue Instanz mit de zu deklarieren, aber das ist immer zumindest eine kleine und manchmal eine große Unannehmlichkeit. In diesem Sinne sind diejenigen, die absichtlich verwaiste Instanzen schreiben, eher unhöflich.

Was ist also gegen dieses Problem zu tun? Das Anti-Orphan-Instance-Camp gibt an, dass die GHC-Warnung ein Fehler ist. Es muss sich um einen Fehler handeln, der jeden Versuch, eine Orphan-Instanz zu deklarieren, ablehnt. In der Zwischenzeit müssen wir Selbstdisziplin üben und sie um jeden Preis vermeiden.

Wie Sie gesehen haben, gibt es diejenigen, die sich über diese potenziellen Probleme nicht so viele Sorgen machen. Sie fördern tatsächlich die Verwendung von verwaisten Instanzen als Instrument zur Trennung von Bedenken, wie Sie vorschlagen, und sagen, dass man nur von Fall zu Fall sicherstellen sollte, dass es kein Problem gibt. Die verwaisten Instanzen anderer Leute haben mich oft genug belästigt, um davon überzeugt zu sein, dass diese Haltung zu unbekümmert ist.

Ich denke, die richtige Lösung wäre, dem Importmechanismus von Haskell eine Erweiterung hinzuzufügen, die den Import von Instanzen steuert. Das würde die Probleme nicht vollständig lösen, aber es würde helfen, unsere Programme vor Schäden durch die bereits auf der Welt existierenden verwaisten Instanzen zu schützen. Und dann könnte ich mit der Zeit davon überzeugt sein, dass in bestimmten begrenzten Fällen eine verwaiste Instanz möglicherweise nicht so schlecht ist. (Und genau diese Versuchung ist der Grund, warum einige im Lager gegen Waisen gegen meinen Vorschlag sind.)

Meine Schlussfolgerung aus all dem ist, dass ich zumindest vorerst dringend empfehlen würde, keine Orphan-Instanzen zu deklarieren, um anderen gegenüber rücksichtsvoll zu sein, wenn auch aus keinem anderen Grund. Verwenden Sie a newtype.

Yitz
quelle
4
Dies ist insbesondere beim Wachstum der Bibliotheken zunehmend ein Problem. Mit> 2200 Bibliotheken in Haskell und Zehntausenden von einzelnen Modulen steigt das Risiko, Instanzen aufzunehmen, dramatisch an.
Don Stewart
16
Betreff: "Ich denke, die richtige Lösung wäre, den Importmechanismus von Haskell um eine Erweiterung zu erweitern, die den Import von Instanzen steuert." Falls diese Idee jemanden interessiert, lohnt es sich möglicherweise, die Scala-Sprache als Beispiel zu betrachten. Es verfügt über ähnliche Funktionen zur Steuerung des Umfangs von 'Implicits', die sehr ähnlich wie Typklasseninstanzen verwendet werden können.
Matt
5
Meine Software ist eher eine Anwendung als eine Bibliothek, daher ist die Möglichkeit, anderen Entwicklern Probleme zu bereiten, so gut wie null. Sie könnten das Publisher-Modul als Anwendung und den Rest der Module als Bibliothek betrachten, aber wenn ich die Bibliothek verteilen würde, wäre dies ohne den Publisher und daher die verwaisten Instanzen. Wenn ich die Instanzen jedoch in die anderen Module verschieben würde, würde die Bibliothek mit einer unnötigen Abhängigkeit von HStringTemplate ausgeliefert. In diesem Fall denke ich, dass die Waisenkinder in Ordnung sind, aber ich werde Ihren Rat befolgen, wenn ich in einem anderen Kontext auf dasselbe Problem stoße.
Dan Dyer
1
Das klingt nach einem vernünftigen Ansatz. Das einzige, worauf Sie achten müssen, ist, wenn der Autor eines von Ihnen importierten Moduls diese Instanz in einer späteren Version hinzufügt. Wenn diese Instanz mit Ihrer identisch ist, müssen Sie Ihre eigene Instanzdeklaration löschen. Wenn sich diese Instanz von Ihrer unterscheidet, müssen Sie einen Newtype-Wrapper um Ihren Typ legen. Dies kann eine erhebliche Umgestaltung Ihres Codes sein.
Yitz
@Matt: In der Tat, überraschenderweise macht Scala das richtig, wo Haskell es nicht tut! (außer natürlich, dass Scala keine erstklassige Syntax für Maschinen der Typklasse hat, was noch schlimmer ist ...)
Erik Kaplun
44

Gehen Sie voran und unterdrücken Sie diese Warnung!

Sie sind in guter Gesellschaft. Conal macht es in "TypeCompose". "chp-mtl" und "chp-transformers" machen es, "control-monad-exception-mtl" und "control-monad-exception-monadsfd" machen es usw.

Übrigens wissen Sie das wahrscheinlich schon, aber für diejenigen, die es nicht tun und Ihre Frage bei einer Suche stolpern:

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

Bearbeiten:

Ich erkenne die Probleme, die Yitz in seiner Antwort erwähnte, als echte Probleme an. Ich sehe es jedoch auch als Problem an, verwaiste Instanzen nicht zu verwenden, und ich versuche, das "geringste Übel" auszuwählen, was imho ist, um verwaiste Instanzen umsichtig zu verwenden.

Ich habe in meiner kurzen Antwort nur ein Ausrufezeichen verwendet, weil Ihre Frage zeigt, dass Sie sich der Probleme bereits bewusst sind. Sonst wäre ich weniger begeistert gewesen :)

Ein bisschen Ablenkung, aber was ich glaube, ist die perfekte Lösung in einer perfekten Welt ohne Kompromisse:

Ich glaube, dass die Probleme, die Yitz erwähnt (ohne zu wissen, welche Instanz ausgewählt wurde), in einem "ganzheitlichen" Programmiersystem gelöst werden könnten, in dem:

  • Sie bearbeiten nicht nur Textdateien primitiv, sondern werden von der Umgebung unterstützt (z. B. schlägt die Code-Vervollständigung nur Dinge von relevantem Typ usw. vor).
  • Die Sprache "unterer Ebene" unterstützt Typklassen nicht speziell, stattdessen werden Funktionstabellen explizit weitergegeben
  • In der Programmierumgebung "höherer Ebene" wird der Code jedoch ähnlich wie in Haskell angezeigt (die Funktionstabellen werden normalerweise nicht angezeigt), und es werden die expliziten Typklassen für Sie ausgewählt, wenn sie offensichtlich sind (z Beispiel: Alle Fälle von Functor haben nur eine Auswahl. Wenn mehrere Beispiele vorhanden sind (Zipping-Liste Applicative oder List-Monad Applicative, First / Last / Lift, möglicherweise Monoid), können Sie auswählen, welche Instanz verwendet werden soll.
  • Selbst wenn die Instanz automatisch für Sie ausgewählt wurde, können Sie in der Umgebung auf einfache Weise sehen, welche Instanz verwendet wurde, und zwar über eine einfache Oberfläche (eine Hyperlink- oder Hover-Schnittstelle oder etwas anderes).

Zurück aus der Fantasiewelt (oder hoffentlich der Zukunft), jetzt: Ich empfehle, verwaiste Instanzen zu vermeiden, während Sie sie weiterhin verwenden, wenn Sie dies "wirklich brauchen"

Yairchu
quelle
5
Ja, aber wohl ist jedes dieser Ereignisse ein Fehler in irgendeiner Reihenfolge. Die schlechten Instanzen in control-monad-exception-mtl und monads-fd für Beide fallen mir ein. Es wäre weniger aufdringlich, wenn jedes dieser Module gezwungen wäre, seine eigenen Typen zu definieren oder Newtype-Wrapper bereitzustellen. Fast jede verwaiste Instanz bereitet Kopfschmerzen, und wenn nichts anderes erforderlich ist, müssen Sie ständig wachsam sein, um sicherzustellen, dass sie importiert wird oder nicht.
Edward KMETT
2
Vielen Dank. Ich denke, ich werde sie in dieser besonderen Situation verwenden, aber dank Yitz weiß ich jetzt besser, welche Probleme sie verursachen können.
Dan Dyer
37

Verwaiste Instanzen sind ein Ärgernis, aber meiner Meinung nach sind sie manchmal notwendig. Ich kombiniere oft Bibliotheken, bei denen ein Typ aus einer Bibliothek und eine Klasse aus einer anderen Bibliothek stammt. Natürlich kann nicht erwartet werden, dass die Autoren dieser Bibliotheken Instanzen für jede denkbare Kombination von Typen und Klassen bereitstellen. Also muss ich sie versorgen, und so sind sie Waisen.

Die Idee, dass Sie den Typ in einen neuen Typ einschließen sollten, wenn Sie eine Instanz bereitstellen müssen, ist eine Idee mit theoretischem Wert, aber unter vielen Umständen einfach zu langweilig. Es ist die Art von Idee, die von Leuten vorgebracht wird, die keinen Haskell-Code für ihren Lebensunterhalt schreiben. :) :)

Stellen Sie also verwaiste Instanzen bereit. Sie sind harmlos.
Wenn Sie ghc mit verwaisten Instanzen zum Absturz bringen können, ist dies ein Fehler und sollte als solcher gemeldet werden. (Der Fehler, den ghc hatte / hat, mehrere Instanzen nicht zu erkennen, ist nicht so schwer zu beheben.)

Beachten Sie jedoch, dass in Zukunft möglicherweise jemand anderes die bereits vorhandene Instanz hinzufügt und möglicherweise ein Fehler (Kompilierungszeit) auftritt.

August
quelle
2
Ein gutes Beispiel ist die (Ord k, Arbitrary k, Arbitrary v) ⇒ Arbitrary (Map k v)Verwendung von QuickCheck.
Erik Kaplun
17

In diesem Fall denke ich, dass die Verwendung von verwaisten Instanzen in Ordnung ist. Die allgemeine Faustregel für mich lautet: Sie können eine Instanz definieren, wenn Sie die Typklasse "besitzen" oder wenn Sie den Datentyp (oder eine Komponente davon "besitzen" - dh eine Instanz für Vielleicht ist MyData auch in Ordnung. zumindest manchmal). Innerhalb dieser Einschränkungen ist es Ihr eigenes Geschäft, wo Sie sich entscheiden, die Instanz zu platzieren.

Es gibt noch eine weitere Ausnahme: Wenn Sie weder die Typklasse noch den Datentyp besitzen, aber eine Binärdatei und keine Bibliothek erstellen, ist das auch in Ordnung.

sclv
quelle
5

(Ich weiß, dass ich zu spät zur Party komme, aber dies kann für andere immer noch nützlich sein.)

Sie können die verwaisten Instanzen in ihrem eigenen Modul behalten. Wenn jemand dieses Modul importiert, liegt dies speziell daran, dass er sie benötigt, und er kann den Import vermeiden, wenn er Probleme verursacht.

Trystan Spangler
quelle
3

In diesem Sinne verstehe ich die Position der WRT-Bibliotheken des Anti-Orphan-Instance-Camps, aber für ausführbare Ziele sollten Orphan-Instanzen nicht in Ordnung sein?

mxc
quelle
3
In Bezug auf die Unhöflichkeit gegenüber anderen haben Sie Recht. Sie öffnen sich jedoch potenziellen zukünftigen Problemen, wenn dieselbe Instanz in Zukunft jemals irgendwo in Ihrer Abhängigkeitskette definiert wird. In diesem Fall liegt es an Ihnen, zu entscheiden, ob sich das Risiko lohnt.
Yitz
5
In fast allen Fällen, in denen eine verwaiste Instanz in einer ausführbaren Datei implementiert wird, muss eine Lücke geschlossen werden, die bereits für Sie definiert wurde. Wenn die Instanz also vorgelagert angezeigt wird, ist der resultierende Kompilierungsfehler nur ein nützliches Signal, um Ihnen mitzuteilen, dass Sie Ihre Deklaration der Instanz entfernen können.
Ben