Die Typprüfung ermöglicht einen sehr falschen Typwechsel, und das Programm wird weiterhin kompiliert

99

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, Coordum 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 startPlayeraus irgendeinem Grund nicht benutzt wird. Kommentierung out startPlayerergibt einen Compiler - Fehler obwohl und sogar Fremden, die Änderung der 10in startPlayerverursacht 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 Playerin startPlayerfalsch 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.

Carcigenicate
quelle
26
Wie @Cubic feststellte, sollten Sie dieses Problem auf jeden Fall den Gloss-Betreuern melden. Ihre Frage zeigt deutlich, wie die falsche Orphan-Instanz einer Bibliothek Ihren Code durcheinander gebracht hat .
Christian Conkle
1
Getan. Ist es möglich, Instanzen auszuschließen? Sie benötigen es möglicherweise, damit die Bibliothek funktioniert, aber ich brauche es nicht. Mir ist auch aufgefallen, dass sie Num Color definiert haben. Es ist nur eine Frage der Zeit, bis mich das erwischt.
Carcigenicate
@ Cubic Na ja, zu spät. Und ich habe es erst vor ungefähr einer Woche mit einer aktualisierten, aktuellen Kabale heruntergeladen. also sollte es aktuell sein.
Carcigenicate
2
@ChristianConkle Es besteht die Möglichkeit, dass der Autor von Gloss nicht verstanden hat, was TypeSynonymInstances tut. In jedem Fall muss dies wirklich verschwinden (entweder Pointeinen newtypeoder andere Operatornamen verwenden linear)
Cubic
1
@Cubic: TypeSynonymInstances ist an sich nicht so schlecht (obwohl nicht völlig harmlos), aber wenn Sie es mit OverlappingInstances kombinieren, werden die Dinge sehr lustig.
John L

Antworten:

128

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 es 10 :: (Float,Float)funktioniert. Versuchen Sie :i Numdann 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.

Kubisch
quelle
53
BEEINDRUCKEND. 10 :: (Float, Float)ergibt (10.0,10.0)und :i Numenthält die Zeile instance Num Point -- Defined in ‘Graphics.Gloss.Data.Point’( Pointist Gloss 'Alias ​​von Coord). Ernsthaft? Danke dir. Das rettete mich vor einer schlaflosen Nacht.
Carcigenicate
6
@Carcigenicate Obwohl es leichtfertig erscheint, solche Instanzen zuzulassen, ist der Grund dafür, dass Entwickler ihre eigenen Instanzen schreiben können, Numwo dies sinnvoll ist, z. B. einen AngleDatentyp, der ein Doublezwischen -piund einschränkt pi, 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 wie String/ Text/ ByteString, wobei es aus benutzerfreundlicher Sicht sinnvoll ist, diese Instanzen zuzulassen, aber es kann wie in diesem Fall missbraucht werden.
bheklilr
4
@bheklilr Ich verstehe die Notwendigkeit, Instanzen von Num zuzulassen. Das "WOW" ergab sich aus einigen Dingen. Ich wusste nicht, dass Sie Instanzen von Typ-Aliasen erstellen können. Das Erstellen einer Num-Instanz eines Coords scheint nur kontraintuitiv zu sein, und ich habe nicht daran gedacht. Na ja, Lektion gelernt.
Carcigenicate
3
Sie können Ihr Problem mit der verwaisten Instanz aus Ihrer Bibliothek umgehen, indem Sie eine newtypeDeklaration für Coordanstelle von a verwenden type.
Benjamin Hodgson
3
@Carcigenicate Ich glaube, Sie benötigen -XTypeSynonymInstances , um Instanzen für Typensynonyme zuzulassen, aber das ist nicht erforderlich, um die problematische Instanz zu erstellen . Eine Instanz für Num (Float, Float)oder (Floating a) => Num (a,a)würde die Erweiterung nicht erfordern, würde jedoch zu demselben Verhalten führen.
Crockeea
64

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.

Christian Conkle
quelle