Beim Versuch, ein Problem in meinem Programm zu debuggen (2 Kreise mit gleichem Radius werden mit Gloss in unterschiedlichen Größen gezeichnet *
), bin ich auf eine seltsame Situation gestoßen. In meiner Datei, die Objekte verarbeitet, habe ich die folgende Definition für a Player
:
type Coord = (Float,Float)
data Obj = Player { oPos :: Coord, oDims :: Coord }
und in meiner Hauptdatei, die Objects.hs importiert, habe ich die folgende Definition:
startPlayer :: Obj
startPlayer = Player (0,0) 10
Dies geschah, weil ich Felder für den Spieler hinzufügte und änderte und vergaß, danach zu aktualisieren startPlayer
(seine Abmessungen wurden durch eine einzelne Zahl bestimmt, um einen Radius darzustellen, aber ich änderte sie in eine, Coord
um sie darzustellen (Breite, Höhe), falls ich sie jemals machte der Spieler widerspricht einem Nichtkreis).
Das Erstaunliche ist, dass der obige Code kompiliert und ausgeführt wird, obwohl das zweite Feld vom falschen Typ ist.
Ich dachte zuerst, dass ich vielleicht verschiedene Versionen der Dateien geöffnet hatte, aber alle Änderungen an den Dateien wurden im kompilierten Programm wiedergegeben.
Als nächstes dachte ich, dass das vielleicht startPlayer
aus irgendeinem Grund nicht benutzt wird. Kommentierung out startPlayer
ergibt einen Compiler - Fehler obwohl und sogar Fremden, die Änderung der 10
in startPlayer
verursacht eine entsprechende Antwort (ändert sich die Ausgangsgröße des Player
); wieder, obwohl es vom falschen Typ ist. Um sicherzustellen, dass die Datendefinition korrekt gelesen wird, habe ich einen Tippfehler in die Datei eingefügt und einen Fehler ausgegeben. also schaue ich mir die richtige datei an.
Ich habe versucht, die beiden obigen Schnipsel in ihre eigene Datei einzufügen, und es wurde der erwartete Fehler ausgegeben, dass das zweite Feld von Player
in startPlayer
falsch ist.
Was könnte dies möglicherweise zulassen? Sie würden denken, dass dies genau das ist, was Haskells Typprüfung verhindern sollte.
*
Die Antwort auf mein ursprüngliches Problem, zwei Kreise mit angeblich gleichem Radius, die auf unterschiedliche Größen gezeichnet wurden, war, dass einer der Radien tatsächlich negativ war.
Point
einennewtype
oder andere Operatornamen verwendenlinear
)Antworten:
Dies kann möglicherweise nur kompiliert werden, wenn eine
Num (Float,Float)
Instanz vorhanden ist. Dies wird von der Standardbibliothek nicht bereitgestellt, obwohl es möglich ist, dass eine der von Ihnen verwendeten Bibliotheken sie aus irgendeinem wahnsinnigen Grund hinzugefügt hat. Laden Sie Ihr Projekt in ghci und prüfen Sie, ob es10 :: (Float,Float)
funktioniert. Versuchen Sie:i Num
dann herauszufinden, woher die Instanz kommt, und schreien Sie dann denjenigen an, der sie definiert hat.Nachtrag: Es gibt keine Möglichkeit, Instanzen zu deaktivieren. Es gibt nicht einmal eine Möglichkeit, sie nicht aus einem Modul zu exportieren. Wenn dies möglich wäre, würde es führen zu noch mehr Code verwirrend. Die einzige wirkliche Lösung besteht darin, solche Instanzen nicht zu definieren.
quelle
10 :: (Float, Float)
ergibt(10.0,10.0)
und:i Num
enthält die Zeileinstance Num Point -- Defined in ‘Graphics.Gloss.Data.Point’
(Point
ist Gloss 'Alias von Coord). Ernsthaft? Danke dir. Das rettete mich vor einer schlaflosen Nacht.Num
wo dies sinnvoll ist, z. B. einenAngle
Datentyp, der einDouble
zwischen-pi
und einschränktpi
, oder wenn jemand einen Datentyp schreiben möchte Diese Funktion stellt Quaternionen oder einen anderen komplexeren numerischen Typ dar und ist sehr praktisch. Es folgt auch den gleichen Regeln wieString
/Text
/ByteString
, wobei es aus benutzerfreundlicher Sicht sinnvoll ist, diese Instanzen zuzulassen, aber es kann wie in diesem Fall missbraucht werden.newtype
Deklaration fürCoord
anstelle von a verwendentype
.Num (Float, Float)
oder(Floating a) => Num (a,a)
würde die Erweiterung nicht erfordern, würde jedoch zu demselben Verhalten führen.Haskells Typprüfung ist vernünftig. Das Problem ist, dass die Autoren einer Bibliothek, die Sie verwenden, etwas ... weniger Vernünftiges getan haben.
Die kurze Antwort lautet: Ja,
10 :: (Float, Float)
ist vollkommen gültig, wenn es eine Instanz gibtNum (Float, Float)
. Aus Sicht des Compilers oder der Sprache ist daran nichts "sehr Falsches". Es passt einfach nicht zu unserer Intuition darüber, was numerische Literale tun. Da Sie es gewohnt sind, dass das Typsystem die Art von Fehler auffängt, die Sie gemacht haben, sind Sie zu Recht überrascht und enttäuscht!Num
Instanzen und dasfromInteger
ProblemSie sind überrascht, dass der Compiler akzeptiert
10 :: Coord
, dh10 :: (Float, Float)
. Es ist vernünftig anzunehmen, dass numerische Literale wie10
"numerische" Typen haben. Out of the box, kann Zahlenliterale als interpretiert werdenInt
,Integer
,Float
, oderDouble
. Ein Tupel von Zahlen ohne anderen Kontext scheint keine Zahl zu sein, so wie diese vier Typen Zahlen sind. Wir reden nicht darüberComplex
.Glücklicherweise oder unglücklicherweise ist Haskell jedoch eine sehr flexible Sprache. Der Standard gibt an, dass ein ganzzahliges Literal wie
10
folgt interpretiert wirdfromInteger 10
, das einen Typ hatNum a => a
. Man10
könnte also auf jeden Typ schließen, für den eineNum
Instanz geschrieben wurde. Ich erkläre dies in einer anderen Antwort etwas ausführlicher .Als Sie Ihre Frage gestellt haben, hat ein erfahrener Haskeller sofort festgestellt
10 :: (Float, Float)
, dass es eine Instanz wieNum a => Num (a, a)
oder geben muss, um akzeptiert zu werdenNum (Float, Float)
. Es gibt keine solche Instanz in derPrelude
, also muss sie woanders definiert worden sein. Mit:i Num
haben Sie schnell erkannt, woher es kam: dasgloss
Paket.Geben Sie Synonyme und verwaiste Instanzen ein
Aber warte eine Minute.
gloss
In diesem Beispiel verwenden Sie keine Typen. Warum hat die Instanzgloss
Sie beeinflusst? Die Antwort erfolgt in zwei Schritten.Erstens erstellt ein mit dem Schlüsselwort eingeführtes Typensynonym
type
keinen neuen Typ . In Ihrem Modul ist SchreibenCoord
einfach eine Abkürzung für(Float, Float)
. Ebenso inGraphics.Gloss.Data.Point
,Point
Mittel(Float, Float)
. Mit anderen Worten : IhreCoord
undgloss
‚sPoint
sind buchstäblich gleichwertig.Als die
gloss
Betreuer sich für das Schreiben entschieden habeninstance Num Point where ...
, haben sie auch IhrenCoord
Typ zu einer Instanz von gemachtNum
. Das entsprichtinstance Num (Float, Float) where ...
oderinstance Num Coord where ...
.(Standardmäßig erlaubt Haskell nicht, dass Typensynonyme Klasseninstanzen sind. Die
gloss
Autoren mussten ein Paar Spracherweiterungen aktivierenTypeSynonymInstances
undFlexibleInstances
die Instanz schreiben.)Zweitens ist dies überraschend, da es sich um eine verwaiste Instanz handelt , dh eine Instanzdeklaration,
instance C A
in der beideC
undA
in anderen Modulen definiert sind. Hier ist es besonders heimtückisch, weil jeder beteiligte Teil, dhNum
,(,)
undFloat
, von der stammtPrelude
und wahrscheinlich überall im Umfang ist.Ihre Erwartung ist, dass
Num
definiert inPrelude
und Tupel undFloat
definiert sind inPrelude
, so dass alles darüber, wie diese drei Dinge funktionieren, in definiert istPrelude
. Warum sollte der Import eines völlig anderen Moduls etwas ändern? Im Idealfall würde dies nicht der Fall sein, aber verwaiste Instanzen brechen diese Intuition.(Beachten Sie, dass GHC vor verwaisten Instanzen warnt - die Autoren
gloss
haben diese Warnung speziell überschrieben. Dies hätte eine rote Fahne setzen und zumindest eine Warnung in der Dokumentation auslösen müssen.)Klasseninstanzen sind global und können nicht ausgeblendet werden
Darüber hinaus sind Klasseninstanzen global : Jede Instanz, die in einem Modul definiert ist, das transitiv aus Ihrem Modul importiert wird, befindet sich im Kontext und steht dem Typechecker bei der Instanzauflösung zur Verfügung. Dies macht globales Denken bequem, da wir (normalerweise) davon ausgehen können, dass eine Klassenfunktion wie
(+)
für einen bestimmten Typ immer dieselbe ist. Dies bedeutet jedoch auch, dass lokale Entscheidungen globale Auswirkungen haben. Durch das Definieren einer Klasseninstanz wird der Kontext des nachgeschalteten Codes unwiderruflich geändert, ohne dass er hinter Modulgrenzen maskiert oder verborgen werden kann.Sie können keine Importlisten verwenden, um den Import von Instanzen zu vermeiden . Ebenso können Sie das Exportieren von Instanzen aus von Ihnen definierten Modulen nicht vermeiden.
Dies ist ein problematischer und viel diskutierter Bereich des Haskell-Sprachdesigns. In diesem reddit-Thread gibt es eine faszinierende Diskussion verwandter Themen . Siehe zum Beispiel Edward Kmotts Kommentar zum Zulassen der Sichtbarkeitskontrolle für Instanzen: "Sie werfen im Grunde die Richtigkeit von fast dem gesamten Code, den ich geschrieben habe, weg."
(By the way, wie diese Antwort demonstriert , Sie können die globale Instanz Annahme in mancher Hinsicht brechen durch Waise Instanzen verwenden!)
Was zu tun ist - für Bibliotheksimplementierer
Überlegen Sie zweimal, bevor Sie implementieren
Num
. Sie können dasfromInteger
Problem nicht umgehen - nein, das DefinierenfromInteger = error "not implemented"
macht es nicht besser. Werden Ihre Benutzer verwirrt oder überrascht sein - oder schlimmer noch, bemerken Sie es nie -, wenn versehentlich auf ihre ganzzahligen Literale geschlossen wird, um den Typ zu haben, den Sie instanziieren? Ist das Bereitstellen(*)
und(+)
das kritisch - besonders wenn Sie es hacken müssen?Erwägen Sie die Verwendung alternativer arithmetischer Operatoren, die in einer Bibliothek wie der von Conal Elliott
vector-space
(für Arten von Arten*
) oder der von Edward Kmettlinear
(für Arten von Arten* -> *
) definiert sind. Das mache ich meistens selbst.Verwenden Sie
-Wall
. Implementieren Sie keine verwaisten Instanzen und deaktivieren Sie die Warnung für verwaiste Instanzen nicht.Folgen Sie alternativ dem Beispiel
linear
vieler anderer gut erzogener Bibliotheken und stellen Sie verwaiste Instanzen in einem separaten Modul bereit, das mit.OrphanInstances
oder endet.Instances
. Und importieren Sie dieses Modul nicht von einem anderen Modul . Dann können Benutzer die Waisenkinder explizit importieren, wenn sie möchten.Wenn Sie feststellen, dass Sie Waisen definieren, sollten Sie die vorgelagerten Betreuer bitten, diese stattdessen zu implementieren, wenn dies möglich und angemessen ist. Ich habe die verwaiste Instanz häufig geschrieben
Show a => Show (Identity a)
, bis sie hinzugefügt wurdetransformers
. Möglicherweise habe ich sogar einen Fehlerbericht darüber erstellt. Ich erinnere mich nicht.Was zu tun ist - für Bibliothekskonsumenten
Sie haben nicht viele Möglichkeiten. Wenden Sie sich höflich und konstruktiv an die Bibliotheksverwalter. Zeigen Sie sie auf diese Frage. Möglicherweise hatten sie einen besonderen Grund, das problematische Waisenkind zu schreiben, oder sie haben es einfach nicht bemerkt.
Allgemeiner: Seien Sie sich dieser Möglichkeit bewusst. Dies ist einer der wenigen Bereiche in Haskell, in denen es echte globale Auswirkungen gibt. Sie müssten überprüfen, ob jedes Modul, das Sie importieren, und jedes Modul, das diese Module importieren, keine verwaisten Instanzen implementiert. Typanmerkungen können Sie manchmal auf Probleme aufmerksam machen, und natürlich können Sie diese
:i
in GHCi überprüfen.Definieren Sie Ihre eigenen
newtype
s anstelle vontype
Synonymen, wenn dies wichtig genug ist. Sie können ziemlich sicher sein, dass sich niemand mit ihnen anlegen wird.Wenn Sie häufig Probleme mit einer Open-Source-Bibliothek haben, können Sie natürlich Ihre eigene Version der Bibliothek erstellen, aber die Wartung kann schnell zu Kopfschmerzen werden.
quelle