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!
Num
Instanzen und das fromInteger
Problem
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 10
folgt interpretiert wird fromInteger 10
, das einen Typ hat Num a => a
. Man 10
könnte also auf jeden Typ schließen, für den eine Num
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 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 Num
haben Sie schnell erkannt, woher es kam: das gloss
Paket.
Geben Sie Synonyme und verwaiste Instanzen ein
Aber warte eine Minute. gloss
In diesem Beispiel verwenden Sie keine Typen. Warum hat die Instanz gloss
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 Schreiben Coord
einfach eine Abkürzung für (Float, Float)
. Ebenso in Graphics.Gloss.Data.Point
, Point
Mittel (Float, Float)
. Mit anderen Worten : Ihre Coord
und gloss
‚s Point
sind buchstäblich gleichwertig.
Als die gloss
Betreuer sich für das Schreiben entschieden haben instance Num Point where ...
, haben sie auch Ihren Coord
Typ 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 gloss
Autoren mussten ein Paar Spracherweiterungen aktivieren TypeSynonymInstances
und FlexibleInstances
die Instanz schreiben.)
Zweitens ist dies überraschend, da es sich um eine verwaiste Instanz handelt , dh eine Instanzdeklaration, instance C A
in der beide C
und A
in anderen Modulen definiert sind. Hier ist es besonders heimtückisch, weil jeder beteiligte Teil, dh Num
, (,)
und Float
, von der stammt Prelude
und wahrscheinlich überall im Umfang ist.
Ihre Erwartung ist, dass Num
definiert in Prelude
und Tupel und Float
definiert 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 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 das fromInteger
Problem 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 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 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 :i
in GHCi überprüfen.
Definieren Sie Ihre eigenen newtype
s anstelle von type
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.