Warum werden Instanzen nur von ihren Köpfen abgeglichen?

8

Ich werde mit der Einführung eines konkreten Problems beginnen (StackOverflow-Leute wie dieses). Angenommen, Sie definieren einen einfachen Typ

%Vor%

Dieser Typ ist ein Functor , Applicative und ein Monad . Ignorieren automatische Ableitung, um diese Instanzen zu bekommen, müssen Sie jede von ihnen schreiben, obwohl Monad impliziert Applicative , was Functor bedeutet. Mehr als das, könnte ich eine Klasse wie diese definieren

%Vor%

Das ist eine ziemlich starke Bedingung und es bedeutet definitiv Monad , aber ich kann nicht schreiben

%Vor%

weil dies aus irgendeinem Grund bedeutet "alles ist ein Monad (jedes f ), nur wenn es ein Wrapper " ist, anstatt "alles was ein Wrapper ist, ist Monad ".

Ebenso können Sie die Monad a => Applicative a und Applicative a => Functor a Instanzen nicht definieren.

Eine andere Sache, die Sie nicht tun können (die wahrscheinlich nur verwandt ist, ich weiß es wirklich nicht), ist, dass eine Klasse eine Oberklasse eines anderen ist und eine Standardimplementierung der Unterklasse bereitstellt. Sicher, es ist großartig, dass class Applicative a => Monad a , aber es ist viel weniger groß, dass ich noch die Applicative -Instanz definieren muss, bevor ich Monad eins definieren kann.

Das ist keine Tirade. Ich habe viel geschrieben, weil sonst das schnell als "zu breit" oder "unklar" markiert würde. Die Frage läuft auf den Titel hinaus. Ich weiß (zumindest bin ich mir ziemlich sicher), dass es einen theoretischen Grund dafür gibt, also frage ich mich, was genau die Vorteile hier sind.

Als eine Unterfrage möchte ich fragen, ob es praktikable Alternativen gibt, die immer noch alle (oder die meisten) dieser Vorteile behalten, aber zulassen, was ich geschrieben habe.

Zusatz: Ich vermute, eine der Antworten könnte etwas in den folgenden Zeilen sein: "Was ist, wenn mein Typ ein Wrapper ist, aber ich möchte nicht die Monad Instanz verwenden, die das beinhaltet?". Dazu frage ich, warum konnte der Compiler nicht einfach den spezifischsten auswählen? Wenn es ein instance Monad MyType gibt, ist es sicherlich spezifischer als instance Wrapper a => Monad a .

    
Luka Horvat 14.05.2015, 23:26
quelle

2 Antworten

12

Hier werden viele Fragen in einem zusammengefasst. Aber nehmen wir sie nacheinander.

Erstens: Warum betrachtet der Compiler bei der Auswahl der zu verwendenden Instanz nicht Instanzkontexte? Dies dient dazu, die Instanzsuche effizient zu halten. Wenn der Compiler nur Instanzen berücksichtigen soll, deren Instanz-Heads erfüllt sind, müssen Sie Ihren Compiler im Wesentlichen dazu veranlassen, eine Back-Tracking-Suche zwischen allen möglichen Instanzen durchzuführen. An diesem Punkt haben Sie 90% von Prolog implementiert. Wenn Sie andererseits die Haltung einnehmen (wie Haskell), dass Sie nur Instanzköpfe betrachten, wenn Sie die zu verwendende Instanz auswählen, und dann einfach den Instanzkontext erzwingen, gibt es kein Backtracking: in jedem Moment gibt es nur eine Wahl, die Sie treffen können.

Als Nächstes: Warum können Sie nicht eine Klasse als Oberklasse einer anderen Klasse haben und eine Standardimplementierung der Unterklasse bereitstellen? Für diese Einschränkung gibt es keinen grundlegenden Grund, daher bietet GHC diese Funktion als Erweiterung an. Du könntest so etwas schreiben:

%Vor%

Wenn Sie einmal instance Monad M where ... angegeben haben, können Sie einfach instance Applicative M ohne where -Klausel schreiben und Just Work haben. Ich weiß nicht wirklich, warum das in der Standardbibliothek nicht gemacht wurde.

Last: Warum kann der Compiler nicht viele Instanzen zulassen und nur den spezifischsten auswählen? Die Antwort auf diese Frage ist eine Mischung aus den beiden vorherigen: Es gibt sehr gute Gründe, warum dies nicht gut funktioniert, aber GHC bietet trotzdem eine Erweiterung, die das tut. Der Grund, warum dies nicht gut funktioniert, ist, dass die spezifischste Instanz für einen gegebenen Wert nicht vor der Laufzeit bekannt sein kann. Die Antwort von GHC ist für polymorphe Werte, die spezifischste zu wählen, die mit dem gesamten verfügbaren Polymorphismus kompatibel ist. Wenn später das Ding Ding monomorphisiert wird, gut, schade für dich. Das hat zur Folge, dass einige Funktionen bei einigen Daten mit einer Instanz arbeiten und andere bei denselben Daten mit einer anderen Instanz arbeiten können. Dies kann zu sehr kleinen Fehlern führen. Wenn Sie nach all dieser Diskussion immer noch denken, dass das eine gute Idee ist, und sich weigern, aus den Fehlern anderer zu lernen, können Sie IncoherentInstances aktivieren.

Ich denke, das deckt alle Fragen ab.

    
Daniel Wagner 15.05.2015, 01:15
quelle
4

Konsistenz und separate Kompilierung.

Wenn wir zwei Instanzen haben, deren Köpfe beide übereinstimmen, aber unterschiedliche Einschränkungen haben, sagen Sie:

%Vor%

Dann ist dies entweder ein gültiger Code, der eine Applicative -Instanz für Foo erzeugt, oder es ist ein Fehler, der zwei verschiedene Applicative -Instanzen für Foo erzeugt. Welche davon ist, hängt davon ab, ob eine Monadeninstanz für Foo existiert. Das ist ein Problem, weil es schwierig ist zu garantieren, dass Wissen darüber, ob Monad Foo hält, es dem Compiler ermöglicht, wenn es dieses Modul kompiliert.

Ein anderes Modul (zB Bar.hs ) kann eine Monad Instanz für Foo erzeugen. Wenn Foo.hs dieses Modul nicht importiert (auch nicht indirekt), wie sollte der Compiler dann wissen? Schlimmer noch, wir können ändern , ob dies ein Fehler oder eine gültige Definition ist, indem wir ändern, ob wir später Bar.hs in das endgültige Programm aufnehmen oder nicht!

Damit dies funktioniert, müssen wir wissen, dass alle Instanzen, die im endgültigen kompilierten Programm vorhanden sind, in jedem Modul sichtbar sind, was zu der Schlussfolgerung führt, dass jedes Modul eine Abhängigkeit von jedem anderen Modul ist ob das Modul tatsächlich das andere importiert . Sie müssten ziemlich weit gehen, um eine vollständige Programmanalyse zu benötigen, um ein solches System zu unterstützen, was das Verbreiten vorkompilierter Bibliotheken schwierig bis unmöglich macht.

Der einzige Weg, dies zu vermeiden, besteht darin, dass GHC niemals Entscheidungen aufgrund von negativen Informationen trifft . Sie können keine Instanz basierend auf dem Nicht -Erhalten einer anderen Instanz auswählen.

Dies bedeutet, dass die Einschränkungen für eine Instanz für die Instanzauflösung ignoriert werden müssen. Sie müssen eine Instanz auswählen, unabhängig davon, ob die Einschränkungen gelten. Wenn dies mehr als eine möglicherweise anwendbare Instanz zurücklässt, dann würden Sie negative Informationen benötigen (nämlich dass alle außer einer von ihnen Einschränkungen erfordern, die nicht gelten), um den Code als gültig zu akzeptieren.

Wenn Sie nur eine Instanz haben, die sogar ein Kandidat ist, und Sie können keinen Beweis für ihre Einschränkungen sehen, können Sie den Code akzeptieren, indem Sie die Einschränkungen an die Stelle weitergeben, an der die Instanz verwendet wird (wir können darauf vertrauen) Informationen zu anderen Modulen, weil sie diese importieren müssen, wenn auch nur indirekt); Wenn diese -Positionen eine erforderliche Instanz auch nicht sehen können, erzeugen sie einen entsprechenden Fehler bezüglich einer nicht erfüllten Einschränkung.

Indem wir die Einschränkungen ignorieren, stellen wir sicher, dass ein Compiler korrekte Entscheidungen über Instanzen treffen kann, indem er nur über andere Module informiert, die er (transitiv) importiert; es muss nicht alles wissen, was in jedem anderen Modul definiert ist, um zu wissen, welche Einschränkungen nicht beinhalten.

    
Ben 15.05.2015 02:48
quelle