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 gibt Num (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!
NumInstanzen und das fromIntegerProblem
Sie sind überrascht, dass der Compiler akzeptiert 10 :: Coord, dh 10 :: (Float, Float). Es ist vernünftig anzunehmen, dass numerische Literale wie 10"numerische" Typen haben. Out of the box, kann Zahlenliterale als interpretiert werden Int, Integer, Float, oder Double. Ein Tupel von Zahlen ohne anderen Kontext scheint keine Zahl zu sein, so wie diese vier Typen Zahlen sind. Wir reden nicht darüber Complex.
Glücklicherweise oder unglücklicherweise ist Haskell jedoch eine sehr flexible Sprache. Der Standard gibt an, dass ein ganzzahliges Literal wie 10folgt interpretiert wird fromInteger 10, das einen Typ hat Num a => a. Man 10könnte also auf jeden Typ schließen, für den eine NumInstanz 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 wie Num a => Num (a, a)oder geben muss, um akzeptiert zu werden Num (Float, Float). Es gibt keine solche Instanz in der Prelude, also muss sie woanders definiert worden sein. Mit :i Numhaben Sie schnell erkannt, woher es kam: das glossPaket.
Geben Sie Synonyme und verwaiste Instanzen ein
Aber warte eine Minute. glossIn diesem Beispiel verwenden Sie keine Typen. Warum hat die Instanz glossSie beeinflusst? Die Antwort erfolgt in zwei Schritten.
Erstens erstellt ein mit dem Schlüsselwort eingeführtes Typensynonym typekeinen neuen Typ . In Ihrem Modul ist Schreiben Coordeinfach eine Abkürzung für (Float, Float). Ebenso in Graphics.Gloss.Data.Point, PointMittel (Float, Float). Mit anderen Worten : Ihre Coordund gloss‚s Pointsind buchstäblich gleichwertig.
Als die glossBetreuer sich für das Schreiben entschieden haben instance Num Point where ..., haben sie auch Ihren CoordTyp zu einer Instanz von gemacht Num. Das entspricht instance Num (Float, Float) where ...oder instance Num Coord where ....
(Standardmäßig erlaubt Haskell nicht, dass Typensynonyme Klasseninstanzen sind. Die glossAutoren mussten ein Paar Spracherweiterungen aktivieren TypeSynonymInstancesund FlexibleInstancesdie Instanz schreiben.)
Zweitens ist dies überraschend, da es sich um eine verwaiste Instanz handelt , dh eine Instanzdeklaration, instance C Ain der beide Cund Ain anderen Modulen definiert sind. Hier ist es besonders heimtückisch, weil jeder beteiligte Teil, dh Num, (,)und Float, von der stammt Preludeund wahrscheinlich überall im Umfang ist.
Ihre Erwartung ist, dass Numdefiniert in Preludeund Tupel und Floatdefiniert sind in Prelude, so dass alles darüber, wie diese drei Dinge funktionieren, in definiert ist Prelude. 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 glosshaben 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 das fromIntegerProblem nicht umgehen - nein, das Definieren fromInteger = 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 Kmett linear(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 linearvieler anderer gut erzogener Bibliotheken und stellen Sie verwaiste Instanzen in einem separaten Modul bereit, das mit .OrphanInstancesoder 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 wurde transformers. 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 :iin GHCi überprüfen.
Definieren Sie Ihre eigenen newtypes anstelle von typeSynonymen, 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.