Arbeitsgruppe Programmiersprachen und Überse erkonstruktion Institut für Informatik Christian-Albrechts-Universität zu Kiel Seminararbeit Guided Type Debugging Karsten Pietrzyk 3. Februar 2015 Inhaltsverzeichnis 1 Einleitung 1 2 Guided Type Debugging 2.1 Variantenausdrücke und Variantentypen . . . . . . . . . . 2.2 Berechnung aller typkorrekt-machenden Änderungen . . . 2.2.1 Matching von Variantentypen mit ◃▹ . . . . . . . . 2.2.2 Matching von Variantentypen in unserem Beispiel . 2.2.3 Beispiel-Sessions . . . . . . . . . . . . . . . . . . . . 2.3 Filtern von Vorschlägen, die zum angegebenen Typ passen 2.3.1 Beispiel zu e = id(3 :: Bool) . . . . . . . . . . . . . . 2.3.2 TCL . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.3.3 Benu ung des TCLs in Debugging-Sessions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 5 7 8 9 10 10 11 12 13 3 Andere Ansätze 3.1 Repairing single locations [HageHeeren] 3.2 Explain type conflicts [Chitil] . . . . . . . 3.3 Interactive debugging . . . . . . . . . . . 3.4 Error slicing [HaackWells] . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 15 16 17 17 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Zusammenfassung 19 Literaturverzeichnis 21 ii Listings 1.1 1.2 1.3 Funktion middle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fehlerhafte Definition von rev und withoutLast . . . . . . . . . . . . . . rev mit Typannotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 2 2 2.1 2.2 2.3 Beispiel-Session mit Guided Type Debugging . . . . . . . . . . . . . . . . Erstes GTD-Beispiel für e . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zweites GTD-Beispiel für e . . . . . . . . . . . . . . . . . . . . . . . . . . 4 10 10 3.1 3.2 3.3 3.4 Beispiel für Reparing single locations . Beispiel für Explain type conflicts . . . Interactive debugging . . . . . . . . . Beispiel für Error slicing . . . . . . . . . . . . 15 16 17 17 4.1 4.2 Vertauschte Parameter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Beispiel-Session mit Guided Type Debugging . . . . . . . . . . . . . . . . 19 19 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Abbildungsverzeichnis 2.1 2.2 Graph mit Knoten 2AlleD und Kanten ⊂1 . . . . . . . . . . . . . . . . . . . TCL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iii 11 13 1 Einleitung Bei der Programmierung mit einigen funktionalen Sprachen generiert ein Mechanismus namens Typ-Inferenz (nach Hindley-Milner) automatisch Typen für Ausdrücke und Funktionen. So müssen nicht (wie z.B. in objektorientierten Sprachen) Typen für Parameter und lokale Variablen sowie Rückgabetypen der Methoden explizit geschrieben werden. Bei typkorrekten Programmen ist dies ein schönes Feature. Diesen Luxus bezahlt man leider mit wenig aufschlussreichen Fehlermeldungen, wenn der Programmierer einen Typfehler macht. Fehlermeldungen können aus vielen Gründen wenig hilfreich sein: • Sie beziehen sich auf die falsche Stelle, d.h. die, an welcher der Fehler im TypInferenz-Prozess auftaucht und nicht auf die Stelle, an welcher der Programmierer den Fehler korrigieren kann. • Sie sind in „Compilerjargon“ formuliert und beziehen sich auf die Unifikation des Typ-Inferenz-Algorithmus. • Sie sind zu lang. Wir befassen uns in dieser Arbeit konkret mit der funktionalen Programmiersprache Haskell und seinem Typsystem. Die folgenden Beispiele zeigen verschiedene Arten von Compilerfehler. Das erste Beispiel erzeugt eine Fehlermeldung, mit dem sich der Typfehler schnell beheben lässt. Das zweite Beispiel ist problematischer und erzeugt einen Fehler, der nicht auf die Stelle zeigt, an der der Fehler korrigiert werden kann. Listing 1.1: Funktion middle 1 -- Berechnet den mittleren Index einer Liste 2 mid :: [a] -> Int 3 mid ls = length / 2 Übergibt man das Programm dem GHC-Compiler, wird folgender Fehler ausgegeben: Couldn't match expected type `Int' with actual type `[a0] -> Int' In the first argument of `(/)', namely `length' In the expression: length / 2 1 1 Einleitung In an equation for `mid': mid ls = length / 2 Hinweise der Form Couldn't match expected type `res' with actual type `arg -> res' deuten darauf hin, dass ein Argument vom Typ arg vergessen wurde. Die Lösung ist hier, length durch length ls zu erse en. Manchmal ist es aber nicht so einfach, weil Funktionen andere Typen haben als erwartet. Das sehen wir im folgenden Beispiel, bei dem bewusst keine Typannotationen angegeben wurden. Listing 1.2: Fehlerhafte Definition von rev und withoutLast 1 2 3 4 5 6 7 -- Dreht eine Liste um. rev [] = [] rev (x:xs) = rev xs ++ x -- Gibt die Liste ohne das letzte Element zurück . withoutLast = rev . tail . rev -- Test: Wir erwarten ['A','B ']. test = withoutLast ['A','B','C '] Die Fehlernachricht des GHC lautet: Couldn't match expected type `[[a0]]' with actual type `Char' In the expression: 'A' In the first argument of `withoutLast', namely `['A', 'B', 'C']' In the expression: withoutLast ['A', 'B', 'C'] Der Fehler liegt nicht an der Stelle, die in der Nachricht angegeben ist sondern bei der Definition von rev. Der inferierte Typ von rev ist [[a]] -> [a], was an einem Tippfehler in Zeile 3 liegt. Die Lösung ist, x durch [x] zu erse en. Das Problem ist hier, dass sich ein Fehler über mehrere Definitionen fortse t und seinen Ursprung versteckt. Man kann der Nachricht entnehmen, dass withoutLast den falschen Typ hat. Wir wissen nicht welchen, aber, dass es nicht der erwartete Typ ist. Generell ist es sinnvoll – auch zur Dokumentation – Funktionen mit Typ-Annotationen zu versehen. Dies verhindert, dass sich Fehler wie im eben genannten Beispiel fortsetzen. Geben wir beim eben genannten Beispiel die erwarteten Typen an, gibt es eine bessere Fehlerausgabe. Listing 1.3: rev mit Typannotation 1 rev :: [a] -> [a] 2 rev [] = [] 3 rev (x:xs) = rev xs ++ x 2 1 Einleitung Dieser GHC-Fehler erscheint bereits bei der Definition von rev und zeigt explizit auf die fehlerhafte Stelle x: Couldn't match type `a' with `[a]' `a' is a rigid type variable bound by the type signature for rev :: [a] -> [a] at rev.hs In the second argument of `(++)', namely `x' In the expression: rev xs ++ x In an equation for `rev': rev (x:xs) = rev xs ++ x Wir haben in diesem Abschni gesehen, dass Fehlernachrichten zur Lokalisation von Fehlern nur bedingt geeignet sind. Das liegt daran, dass sich falsche Typen fortse en können und der Fehler an einer anderen Stelle auftaucht als der, an der man den Fehler beheben kann. Zu diesem Thema wurde viel geforscht und in [ErwigCF] und [ErwigGTD] ein Verfahren entwickelt, mit dem zu einem typfehlerhaften Programm unter Angabe eines Zieltyps Vorschläge gemacht werden, wie der Typfehler behoben werden kann. Dieses Verfahren nennt sich „Guided Type Debugging“ und ist Hauptgegenstand dieser Arbeit. Im Folgenden werden wir uns mithilfe von Beispielen mit dem Verfahren und den zugrundeliegenden Techniken auseinanderse en. Dazu werden wir uns Konzepte wie Varianten und einen speziellen Verband namens „Type Change La ice“ vertraut machen. Zum Schluss werden wir andere Ansä e, mit typfehlerhaften Programmen umzugehen, zur Übersicht darstellen. Ein Fazit schließt diese Arbeit ab. Diese Arbeit richtet sich an Leser, die mit Haskell und Typinferenz vertraut sind. In [ErwigGTD] werden Inferenzsysteme und diverse Algorithmen verwendet, die hier nicht dargestellt werden, daher ist die Kenntnis von Inferenzsystemen nicht erforderlich. Allgemeine formale Notationen und Mengenlehre werden vorausgese t. Die Konzepte werden au auend einführt, wenn sie das erste Mal benötigt werden. 3 2 Guided Type Debugging Fangen wir mit einem Beispiel an, das die Funktionsweise von Guided Type Debugging zeigt: Listing 2.1: Beispiel-Session mit Guided Type Debugging 1 rev [] = [] 2 rev (x:xs) = rev xs ++ x 4 withoutLast = rev . tail . rev 6 test = withoutLast ['A','B','C '] > What is the expected type of test? [Char] > Potential fixes: > 1. Change x from type a to [a]. > 2. Change (++) from type [a] -> [a] -> [a] to type [a] -> a -> [a] There are no more 1-step fixes. > Show 2-step fixes? (y/n) An diesem Beispiel können wir Folgendes beobachten: • Es wird nach dem Zieltyp einer Funktion mit Typfehler (hier die 0-stellige Funktion test) gefragt. • Nach Eingabe des Zieltypen werden Änderungen vorgeschlagen, die zu einem typkorrekten Programm führen. • Die Änderungen sind nach Anzahl der zu ändernden Stellen sortiert (eine Stelle ändern, zwei Stellen, usw.). • Jede Änderung wird als Vorschlag einer bestimmten Form gegeben: Ändere Variable von Typ Momentaner Typ nach Typ Vorgeschlagener Typ. • Sind keine sinnvollen Vorschläge unter den gezeigten, kann man sich kompliziertere Vorschläge anzeigen lassen. • Für unser Beispiel ist der richtige Vorschlag dabei, nämlich x von Typ a nach Typ [a] zu ändern. Das heißt konkret, x durch [x] zu erse en. 4 2 Guided Type Debugging Damit Guided Type Debugging die gerade dargestellten Informationen ermi elt, sind folgende Schri e notwendig: 1. Alle Typ-Änderungen berechnen, die das Programm typkorrekt machen. 2. Alle Vorschläge rausgefiltert, die zu dem angegebenen erwarteten Typ passen. 3. Diese Vorschläge nach Anzahl der Änderungen sortieren und dem Benu er anzeigen. 2.1 Variantenausdrücke und Variantentypen Angenommen wir haben einen Ausdruck e, der in unserem Programm einmal mit 1 und einmal mit T rue verglichen wird (z.B. e == True && e == 1), so würde sich ergeben, dass e in dem einen Kontext Int und in einem anderen Bool als Typ hat. Um darzustellen, dass ein Ausdruck mehrere Typen hat, kann man Varianten verwenden [ErwigCF]. Die Vorteile gegenüber einer Abbildung eines Ausdrucks auf eine Menge von Typen ist die eindeutige Zuordnung von Variantenausdrücken zu ihren Typen, was später deutlich wird. Diesen Kontext können wir mit D (für Dimension) bezeichnen und dann den Typen von e ausdrücken als t = D⟨Bool, Int⟩. Wir können uns D auch als einen Entscheidungspunkt vorstellen, der bestimmt, ob e den Typ Bool oder Int hat. Varianten haben immer zwei Komponenten, die man mit den sogenannten „Selektoren“ Dimension.1 und Dimension.2 auswählt. Sollten die Komponenten wieder Varianten enthalten, wird in diesen auch die Selektion vorgenommen. Die Selektion ist eine Funktionsanwendung dieser Form: Selektor(VariantenAusdruck) In unserem Beispiel ist Dimension = D und wir können t = D⟨Bool, Int⟩ selektieren: D.1(t) = Bool, D.2(t) = Int. Varianten lassen sich auch schachteln, z.B. in A⟨1, B⟨2, 3⟩⟩. Wählt man die Dimension geschickt, kann man formulieren, dass Teilausdrücke bestimmte Typen haben. Als Beispiel dafür wollen wir für den Variantenausdruck x = A⟨not, odd⟩ 1 den Typ berechnen. Das machen wir, indem wir die Typen alle Werte für x berechnen und diese wieder als Varianten darstellen. Alle Werte von x sind: A.1(x) = not 1 und A.2(x) = odd 1. Die Funktionen seien vordefiniert und haben die Typen not :: Bool → Bool und odd :: Int → Bool. 5 2 Guided Type Debugging Der Ausdruck A.1(x) = not 1 ist falsch getypt, weil eine Funktion vom Typ Bool → Bool auf ein Argument vom Typ Int angewendet wird. Wir vergeben für falsch getypte Ausdrücke den Typ ⊥ (bo om). Der andere Ausdruck A.2(x) = odd 1 hat den Typ Bool. Damit können wir auch x einen Typen geben: A⟨⊥, Bool⟩. Formal können wir diese Inhalte kurz darstellen: Ein Variantentyp ist entweder eine Variante mit einer Dimension A, ein Typ τ , bo om oder ein Funktionstyp über Variantentypen: ϕ ::= A⟨ϕ, ϕ⟩ | τ | ⊥ | ϕ → ϕ Ein Typ ist entweder ein konkreter Typ τ (z.B. Int), eine Typvariable α oder ein Funktionstyp über Typen: τ ::= γ | α | τ → τ Ein Variantenausdruck ist entweder eine Variante von Ausdrücken, eine Konstante c, eine Variable v, eine Funktionsanwendung, eine Bindung oder ein typannotierter Ausdruck: e ::= A⟨e, e⟩ | c | v | e e | let v = e in e | e :: τ Eine Selektion auf Variantentypen wird rekursiv über Variantentypen definiert: { A.i(x), falls i = 1 A.i(A⟨x, y⟩) = A.i(y), falls i = 2 A.i(τ ) = τ A.i(⊥) = ⊥ A.i(x → y)) = (A.i(x)) → (A.i(y)) A.i(B⟨x, y⟩) = B⟨A.i(x), A.i(y)⟩ Eine Selektion auf Variantenausdrücken wird rekursiv über Variantenausdrücken definiert: { A.i(x), falls i = 1 A.i(A⟨x, y⟩) = A.i(y), falls i = 2 A.i(c) = c A.i(v) = v A.i(e e) = (A.i(e)) (A.i(e)) A.i(e :: τ ) = (A.i(e)) :: τ A.i(let v = e in e) = (let v = A.i(e) in A.i(e)) A.i(B⟨x, y⟩) = B⟨A.i(x), A.i(y)⟩ 6 2 Guided Type Debugging 2.2 Berechnung aller typkorrekt-machenden Änderungen Mit diesen formalen Grundlagen, können wir Guided Type Debugging auf e = not 1 anwenden. Dieses Beispiel besteht bewusst nur aus zwei Ausdrücken, weil in dem Prozess relativ viele Typvariablen entstehen. Der erste Schri ist, über Typinferenz den Typ von Variablen und Konstanten zu inferieren. Das sind hier: not :: Bool → Bool und 1 :: Int. Dann stellen wir dar, dass evtl. die falsche Funktion und das falsche Argument verwendet wurden, indem wir e als Variantenausdruck umschreiben:1 e = A⟨not, f ⟩ B⟨1, g⟩ Wir haben f :: α1 → α2 als Variante von not und g :: α3 als Variante von 1 hinzugenommen, um die Typen an den Stellen von not und 1 zu erhalten, die sich allein aus der Benu ung ergeben. Die Struktur des Ausdrucks e ist eine Funktionsapplikation, dessen Funktion eine Variable und Argument eine Konstante ist. Die Typen der Teilausdrücke sind: A⟨not, f ⟩ :: A⟨Bool → Bool, α1 → α2 ⟩ B⟨1, g⟩ :: B⟨Int, α3 ⟩ Wir brauchen noch den Typ von e, den wir α4 nennen: e :: α4 Inwiefern Argument und Funktion zueinanderpassen, überprüfen wir mit der Funktion ◃▹ .2 Dazu nehmen wir den Typ der Funktion, wandeln diesen mit ↑ in einen Funktionstyp um und „matchen“ diesen mit einem Funktionstyp, den wir wie folgt konstruieren: Der Parametertyp ist der Typ des Argumentes (B⟨Int, α3 ⟩) und der Ergebnistyp ist der Typ des Gesamtausdrucks (α4 ): B⟨Int, α3 ⟩ → α4 Formal ist zu berechnen: ↑ (A⟨Bool → Bool, α1 → α2 ⟩) ◃▹ B⟨Int, α3 ⟩ → α4 1 In [ErwigGTD] wird der Variantenausdruck nicht umgeschrieben. Dort erhält e = not 1 den Typen A⟨B⟨⊥, Bool⟩, α3 ⟩. Das ist so nicht ganz nachvollziehbar, mit f und g wird das klarer. 2 Anmerkung: Um auch noch die Bindung der Typvariablen zu ermi eln, wird die Unifikation verwendet. Unifikation auf Variantentypen ist nicht Gegenstand dieser Arbeit und der genaue Algorithmus ist in [ErwigVar] dargestellt. Wir wollen nur darstellen, inwiefern zwei Variantenausdrücke zueinanderpassen und verwenden dafür ◃▹ . 7 2 Guided Type Debugging 2.2.1 Matching von Variantentypen mit ◃▹ Um anzugeben, inwiefern zwei Typen zusammenpassen, definieren wir uns ein sogenanntes Muster, das Varianten enthält und angibt, unter welcher Selektion Typen zusammenpassen oder nicht zusammenpassen. Ein Muster ist entweder ⊤ (top, die Typen passen zusammen), ⊥ (bo om, die Typen passen nicht zusammen) oder eine Variante mit einer Dimension A über Muster. m ::= ⊤ | ⊥ | A⟨m, m⟩ Das Matching passiert mit den Funktionen ◃▹, ⊗ und ↑ [ErwigGTD]. Die folgenden Gleichungen sind geordnet und von oben nach unten anzuwenden, d.h. die erste passende Gleichung soll angewendet werden. Die Funktion ◃▹ erhält zwei Variantentypen und gibt ein Muster m zurück. v, w, x, y, z sind im Folgenden Variantentypen. A⟨v, w⟩ ◃▹ A⟨x, y⟩ =A⟨v ◃▹ x, w ◃▹ y⟩ A⟨v, w⟩ ◃▹ x =A⟨v ◃▹ x, w ◃▹ x⟩ x ◃▹ A⟨v, w⟩ (v → w) ◃▹ (x → y) =A⟨v ◃▹ x, w ◃▹ x⟩ =(v ◃▹ x) ⊗ (w ◃▹ y) x ◃▹ ⊥ =⊥ ⊥ ◃▹ x =⊥ ⊤, = ⊤, ⊥, x ◃▹ y falls x oder y Typvariablen sind falls x = y sonst Die Funktion ↑ soll einen Variantentyp, der noch nicht die Form x → y hat in einen solchen umwandeln: ↑ (x → y) = x → y ↑ (A⟨x → y, v → w⟩) = A⟨x, v⟩ → A⟨y, w⟩ ↑ (A⟨x, y⟩) = ↑ (A⟨↑ (x), ↑ (y)⟩) ↑ (x) = ⊥ → ⊥ sonst Die Funktion ⊗ übernimmt zwei Variantentypen und verschachtelt sie gegebenenfalls. Ansonsten verhält sich die Funktion wie das logische Und, wobei ⊥ der Wahrheitswert falsch und ⊤ wahr ist: ⊤⊗y =y ⊥⊗y =⊥ A⟨x, y⟩ ⊗ z = A⟨x ⊗ z, y ⊗ z⟩ 8 2 Guided Type Debugging 2.2.2 Matching von Variantentypen in unserem Beispiel Nun berechnen wir: ↑ (A⟨Bool → Bool, α1 → α2 ⟩) ◃▹ B⟨Int, α3 ⟩ → α4 Wir berechnen dafür erst einmal: ↑ (A⟨Bool → Bool, α1 → α2 ⟩) = A⟨Bool, α1 ⟩ → A⟨Bool, α2 ⟩ Dann berechnen wir A⟨Bool, α1 ⟩ → A⟨Bool, α2 ⟩ ◃▹ B⟨Int, α3 ⟩ → α4 =A⟨Bool, α1 ⟩ ◃▹ B⟨Int, α3 ⟩ ⊗A⟨Bool, α2 ⟩ ◃▹ α4 =A⟨Bool ◃▹ B⟨Int, α3 ⟩, α1 ◃▹ B⟨Int, α3 ⟩⟩ ⊗A⟨Bool ◃▹ α4 , α2 ◃▹ α4 ⟩ =A⟨B⟨Bool ◃▹ Int, Bool ◃▹ α3 ⟩, B⟨α1 ◃▹ Int, α1 ◃▹ α3 ⟩⟩ ⊗A⟨⊤, ⊤⟩ =A⟨B⟨⊥, ⊤⟩, B⟨⊤, ⊤⟩⟩ ⊗A⟨⊤, ⊤⟩ =A⟨B⟨⊥, ⊤⟩ ⊗ ⊤, B⟨⊤, ⊤⟩ ⊗ ⊤⟩ =A⟨B⟨⊥ ⊗ ⊤, ⊤ ⊗ ⊤⟩, B⟨⊤ ⊗ ⊤, ⊤ ⊗ ⊤⟩⟩ =A⟨B⟨⊥, ⊤⟩, B⟨⊤, ⊤⟩⟩ (Anmerkung: B⟨x, x⟩ = x) =A⟨B⟨⊥, ⊤⟩, ⊤⟩ Dieses Muster ist folgendermaßen zu interpretieren: Unter der Selektion A.1, B.1 passen die beiden verglichenen Typen nicht zusammen (⊥), bei allen anderen Selektionen passen sie zusammen. Je t, da wir das Muster ausgerechnet haben, berechnen wir auch noch die Ergebnistypen aus. Zur Erinnerung waren diese Teilausdrücke gegeben: A⟨not, f ⟩ :: A⟨Bool → Bool, α1 → α2 ⟩ B⟨1, g⟩ :: B⟨Int, α3 ⟩ Bei A.2 ergibt sich immer α2 als Ergebnis (das ist der Ergebnistyp von f :: α1 → α2 ). Der einzig andere Fall ist A.1, B.2, welcher zu Bool führt (das ist der Ergebnistyp von not :: Bool → Bool). Damit ergibt sich für e = A⟨not, f ⟩ B⟨1, g⟩ folgender Typ: :: A⟨B⟨⊥, Bool⟩, α2 ⟩ e :: ⊥ d.h. not 1 not g :: Bool f1 :: α2 fg :: α2 9 2 Guided Type Debugging 2.2.3 Beispiel-Sessions Je t, da wir alle Typen von e kennen, können wir diese in einer Debugging-Session verwenden. Dafür machen wir zwei Beispiele, die zeigen, unter welchen Eingaben welche Variante gewählt werden und wie das Ergebnis interpretiert wird. Listing 2.2: Erstes GTD-Beispiel für e > e = not 1 > What is the expected type of test? Bool > Potential fixes: > 1. Change 1 from type Int to type Bool > 2. Change not from type Bool -> Bool to type Int -> Bool > Show 2-step fixes? (y/n) 1. entspricht not g :: Bool. D.h.: „Ändere das Argument!“ 2. entspricht f 1 :: α2 . D.h.: „Ändere die Funktion!“ Wenn wir eine andere Eingabe machen: Listing 2.3: Zweites GTD-Beispiel für e > e = not 1 > What is the expected type of test? Int > Potential fixes: > 1. Change not from type Bool -> Bool to type Int -> Int > Show 2-step fixes? (y/n) 1. entspricht f 1 :: α2 . D.h.: „Ändere die Funktion!“ 2.3 Filtern von Vorschlägen, die zum angegebenen Typ passen In größeren Ausdrücken kann es viele Möglichkeiten geben, diesen typkorrekt zu machen. Es stellt sich die Frage, wie man man aus allen Änderungen diejenigen rausfiltert, die relevant sind, d.h. zum angegeben Zieltyp passen. Dafür stellt man die Entscheidungen und den resultierenden Typ als Graph, den sogenannten Type Change La ice (kurz TCL) dar. 10 2 Guided Type Debugging Abbildung 2.1: Graph mit Knoten 2AlleD und Kanten ⊂1 Ein TCL wird für einen Ausdruck e :: ϕ erstellt und ist ein Graph, dessen Knoten folgendermaßen aufgebaut sind: (KnotenD, ResultierenderTyp) Sei AlleD die Menge aller Dimensionen im Ausdruck, z.B. ist AlleD = {A, B} für e = A⟨not, f ⟩ B⟨1, g⟩ Dabei sind die Teile folgendermaßen zu berechnen: KnotenD ⊆ AlleD ResultierenderTyp = deltaKnotenD (ϕ) deltaKnotenD = {Dimension.2 | Dimension ∈ KnotenD} ∪ {Andere.1 | Andere ∈ AlleD \ KnotenD} Zwischen zwei KnotenD-Mengen befindet sich eine Kante, wenn sie in der EchtenTeilmengen-Beziehung (⊂) stehen und sich in ihrer Größe nur um 1 unterscheidet (anders ausgedrückt lassen wir transitive Kanten weg). Diese Relation nennen wir ⊂1 . 2.3.1 Beispiel zu e = id(3 :: Bool) Wir wollen uns ein Beispiel für einen Type-Change-La ice ansehen, um die Konstruktion und le tlich den Nu en des Graphen zu verstehen. Dafür nehmen wir einen etwas 11 2 Guided Type Debugging komplizierteren falschgetypten Ausdruck, der eine Konstante, eine Typ-Annotation, eine Variable und eine Funktionsapplikation enthält. e = id(3 :: Bool) Für diesen Ausdruck ergibt sich der folgende Typ: ϕ = A⟨B⟨C⟨⊥, Int⟩, C⟨Bool, α1 ⟩⟩, B⟨C⟨⊥, α2 ⟩, α2 ⟩⟩ Für ein besseres Verständnis des TCLs sind auch die Typen der Teilausdrücke interessant. Denn hier sehen wir auch die ursprünglichen Typen der Teilausdrücke in den ersten Komponenten ihrer Typen. id(3 :: Bool) mit den Typen der Teilausdrücke: Bool :: C⟨Bool, B⟨Int, A⟨α1 , α3 ⟩⟩⟩ 3 :: B⟨Int, C⟨Bool, A⟨α1 , α3 ⟩⟩⟩ id :: A⟨α1 → α1 , B⟨Int → α2 , C⟨Bool → α2 , α3 → α2 ⟩⟩⟩ Wir stellen nur den TCL-Graphen dar. Dafür berechnen wir die Knoten 2AlleD = {{}, {A}, {B}, {C}, {A, B}, {A, C}, {B, C}, {A, B, C}} und als Kanten die Relation ⊂1 . Dieser Graph ist in Abb. 2.1 dargestellt. 2.3.2 TCL Um aus dem Graphen ein TCL zu machen, müssen wir für jeden Knoten KnotenD noch ResultierenderTyp = deltaKnotenD (ϕ) hinzufügen. Ausgerechnet sind das: deltaD : deltaD (ϕ) : {} 7→ {A.1, B.1, C.1} delta{} (ϕ) = ⊥ {A} 7→ {A.2, B.1, C.1} delta{A} (ϕ) = ⊥ {B} 7→ {A.1, B.2, C.1} delta{B} (ϕ) = Bool {C} 7→ {A.1, B.1, C.2} delta{C} (ϕ) = Int {A, B} 7→ {A.2, B.2, C.1} delta{A,B} (ϕ) = α2 {A, C} 7→ {A.2, B.1, C.2} delta{A,C} (ϕ) = α2 {B, C} 7→ {A.1, B.2, C.2} delta{B,C} (ϕ) = α1 {A, B, C} 7→ {A.2, B.2, C.2} delta{A,B,C} (ϕ) = α2 12 2 Guided Type Debugging Abbildung 2.2: TCL Der TCL ist in Abb. 2.2 dargestellt. Nun kann nach dem resultierenden Typ gefiltert werden und dem Benu er die entsprechenden Änderungen vorgeschlagen werden. Das schauen wir uns in einem Beispiel an. 2.3.3 Benutzung des TCLs in Debugging-Sessions Bei Debugging-Sessions werden diese Informationen verwendet. > What is the expected Type of e? Int • Suche im TCL nach allen Knoten, die Int enthalten, zuerst alle Knoten, in denen die Menge nur ein Element enthält (d.h. nur eine Stelle ändern), dann die mit 2 Elementen, usw. • Für den Knoten {C}, Int meldet der Debugger, dass der Ausdruck an Stelle C (das ist die Annotation :: Bool) geändert werden kann, um den Zieltyp Int zu erhalten. Wir ha en die Typen der Teilausdrücke ebenfalls angegeben. Die Annotation ha e den Typ Bool:: C⟨Bool, B⟨Int, A⟨α1 , α3 ⟩⟩⟩. Selektieren wir {C}, d.h. nehmen wir {A.1, B.1, C.2} erhalten wir Int. Also soll die Annotation zu Int geändert werden. > > > > Portential fixes: 1. Change :: Bool from type Bool to Int. There are no more 1-step fixes. Show 2-step fixes? (y/n) 13 2 Guided Type Debugging • In diesem Beispiel würden alle 2- und 3-Schri -Änderungen zum Ergebnis α1 bzw. α2 führen und weil diese mit Int unifizierbar sind, würden sie das Programm typkorrekt machen. Dies schließt das Kapitel über Guided Type Debugging ab. Wir kennen nun die Konzepte „Varianten“ (sowohl in Ausdrücken als auch in Typen) um darzustellen, dass der inferierte Typ evtl. nicht stimmt und wir haben den „Type Change La ice“ benu t, um nach Änderung bei einem bestimmten Zielltyp zu suchen. 14 3 Andere Ansätze Es gibt auch andere Ansä e, um mit Typfehlern umzugehen. Diese unterscheiden sich in der Benu ung und darin, wie viel Gewicht sie dem Unifikationsprozess beimessen. Beispielsweise nimmt „Explain type conflicts“ an, dass der Fehler, der bei der Unifikation auftri zum Fehler führt. Wir wollen die Charakteristika der einzelnen Ansä e kurz nennen, bevor wir uns etwas genauer mit ihnen befassen: Repairing single locations Es wird nur eine Stelle im typfehlerhaften Programm bei einem Fehler gemeldet. Üblicherweise ist diese dort, wo eine Unifikation im Typinferenzalgorithmus fehlschlägt. Explain type conflicts Es soll nachvollziehbar gemacht werden, warum die Typinferenz fehlschlug. Das heißt, aus welchen Ausdrücken der Algorithmus welche Typen ableitet und wo es zum Fehlschlag kam. Ein Beispiel sind textuelle Ansä e. Interactive debugging Man erse t im typfehlerhaften Programm einen Teilausdruck durch eine „frische“ Variable. Wenn das Programm danach typkorrekt ist, dann wurde der Fehler an der erse ten Position gefunden. Ansonsten muss der Benu er den eben erse ten Teilausdruck wiederherstellen und den nächsten ersetzen. Error slicing Es wird nur ein Teil des fehlerhaften Programms angezeigt, der mit den Typfehlern zu tun hat. So soll klarer werden, welche Stellen zum Fehler geführt haben. 3.1 Repairing single locations [HageHeeren] Vorgehen Wende den Typinferenzalgorithmus an und brich ab, wenn die erste Unifikation fehlschlägt. Gib dann aus, welche Typen nicht zueinander passen. Beispiel Listing 3.1: Beispiel für Reparing single locations 1 simplify :: Prop -> Prop 2 simplify p = (...) 15 3 Andere Ansä e 4 simplifyAnd :: [Prop] -> [Prop] 5 simplifyAnd (p:ps) = [ simplify p, simplifyAnd ps] Error: Type error in element of list, expression: [simplify p, simplifyAnd ps] term : simplifyAnd ps type : [Prop] does not match : Prop Vorteile Die Fehlernachrichten sind kurz, weil nur eine Stelle gemeldet wird und an dieser Stelle, welche Typen nicht zueinanderpassen. Nachteile Möglicherweise wird nicht die Ursache des Typfehlers, sondern nur eine Auswirkung des Fehlers angezeigt. In dem Beispiel sieht man, dass der Algorithmus simplify p als korrekt annimmt und bei der Listenkonstruktion der Typ des zweiten Elementes simplifyAnd ps nicht zum Typ des ersten passt. 3.2 Explain type conflicts [Chitil] Vorgehen Man kann durch das Programm navigieren und sich die Typen der Ausdrücke anschauen. So soll nachvollziehbar gemacht werden, welche Typen von der Typinferenz herkommen. Beispiel Listing 3.2: Beispiel für Explain type conflicts 1 reverse [] = [] 2 reverse (x:xs) = reverse xs ++ x reverse :: [[a]]->[a] because Equation : .. [] = [] .. (x:xs) = .. with reverse [b]->[c] [[a]]->[a] (wähle Gleichung 2 zur weiteren Analyse ) Equation : .. (x:xs) = .. with reverse [[a]]->[a] because Lhs/Rhs : reverse (x:xs) ( reverse xs) ++ x Types : b [a] with reverse [c]->b d->[a] x c [a] xs [c] d Vorteile Mit diesem Ansa sind Typen von Teilausdrücken und Gleichungen prü ar. Nachteile Es werden zu viele Informationen geliefert. Es ist sehr mühselig durch das Programm zu navigieren (mit einer GUI wäre das kein Problem). 16 3 Andere Ansä e 3.3 Interactive debugging Vorgehen Man erse t einen Teilausdruck durch ungebundene Variable oder durch etwas, das jeden Typ annehmen kann (z.B. undefined :: a). Wenn Programm dadurch typkorrekt gemacht wird, dann ist der Fehler an der erse ten Position gefunden. Ansonsten muss ein anderer Ausdruck erse t werden (d.h. der alte Ausdruck wird wiederhergestellt und ein anderer erse t). Beispiel Listing 3.3: Interactive debugging 1 2 3 4 rev :: [a] -> [a] rev [] = [] rev (x:xs) = xs ++ x -- Typfehler . Erse e Teilausdrücke durch undefined. Hier x in Gleichung 2. 1 2 3 4 rev :: [a] -> [a] rev [] = [] rev (x:xs) = xs ++ undefined -- Kein Typfehler mehr. Vorteile Intuitives Vorgehen. Nachteile Von Hand sehr mühselig, weil es sehr viele Positionen gibt. Auch nachdem man den Fehler lokalisiert hat, weiß man noch nicht, wie der Fehler zu beheben ist. 3.4 Error slicing [HaackWells] Vorgehen Es wird ein Teil des fehlerhaften Programms angezeigt, der mit den Typfehlern zu tun hat. So soll klarer werden, welche Stellen zum Fehler geführt haben und eingeschränkt, was zu ändern ist. Beispiel (hier in Haskell-Syntax, im Original: Standard ML) Listing 3.4: Beispiel für Error slicing f = \ x -> \y -> let w = y + 1 in w : y Das Error-slicing-Werkzeug von [HaackWells] liefert folgende Ausgabe: y -> (... y + (...) ... (...) : y ...) type constructor clash: endpoints int vs. list 17 3 Andere Ansä e Vorteile Weniger Ausgabe (nur Fragmente, kein ganzer Programmcode) und dazu ein kurzer Hinweis, welche Typen nicht passen. Als Ausgabe kann man auch das Originalprogramm verwenden und die gelieferten Stellen (hier y, + und :) farblich hervorheben. Nachteile Keine Hinweise zur Lösung, nur die kompakte Darstellung der Stellen. Stellen, die mit (...) dargestellt sind, können evtl. länger sein, wodurch die Darstellung kompakt, aber nicht besonders aufschlussreich ist. 18 4 Zusammenfassung Guided Type Debugging ist ein Ansa zur Lösung von Typfehlern, der konstruktive Vorschlägen macht, um Typfehler zu beheben. Diese Vorschläge sind immer gleich aufgebaut: Ändere Ausdruck von Typ alter Typ zu Typ neuer Typ. Ein üblicher Fehler ist es, Parameter zu vertauschen. Guided Type Debugging generiert dann diese Ausgabe: Listing 4.1: Vertauschte Parameter test = [] : 1 > What is the expected type of test? [Int] > Potential fixes: > 1. Change (:) from type Int -> [Int] -> [Int] > to type [Int] -> Int -> [Int] > Show 2-step fixes? (y/n) Einige Änderungen erscheinen zunächst sinnlos, z.B. (:) oder not zu ändern. Solche Vorschläge erscheinen beispielsweise, wenn die falsche Funktion verwendet wurde oder die Parameter vertauscht sind. Im Vergleich zum manuellen Aufwand bei Interactive debugging und Explain type conflicts reicht bei Guided Type Debugging eine Zieltypangabe als Eingabe aus. Dann können bereits Änderungen angezeigt werden. Prinzipiell ist das Verfahren für viele typinferierte funktionale Programmiersprachen anwendbar. Stellt man sich noch dazu vor, dass man das Verfahren in einem Werkzeug wie dem GHC Interactive einbaut, könnte das die Produktivität steigern. Obwohl die Autoren von [ErwigGTD] darauf eingehen, dass Typannotationen ggf. fehlerhaft sein können, wäre eine Variante denkbar, bei der die Typannotation einer Funktion als Zieltyp der Funktion für GTD verwendet wird. Dann wäre folgende Situation vorstellbar: Listing 4.2: Beispiel-Session mit Guided Type Debugging 1 rev [] = [] 2 rev (x:xs) = rev xs ++ x 3 withoutLast = rev . tail . rev 19 4 Zusammenfassung 4 test :: [Char] 5 test = withoutLast ['A','B','C '] > Type error: cannot infer type for test. > Potential fixes to achieve test :: [Char]: > 1. Change x from type a to [a]. > 2. Change (++) from type [a] -> [a] -> [a] to type [a] -> a -> [a] > There are no more 1-step fixes. Type :m to display 2-step fixes. In einer Entwicklungsumgebung könnte man direkt x und ++ farbig hervorheben und den Fehler als Tool Tip anzeigen. Solche konstruktiven Vorschläge gibt es momentan nicht einmal für die Entwicklungsumgebung für F#. 20 Literaturverzeichnis [ErwigGTD] Sheng Chen and Martin Erwig: Guided Type Debugging Twelfth International Symposium on Functional and Logic Programming (FLOPS 2014), 35-51, 2014 [ErwigCF] Sheng Chen and Martin Erwig: Counter-Factual Typing for Debugging Type Errors POPL '14 Proceedings of the 41st ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, 583-594, 2014 [ErwigVar] Sheng Chen, Martin Erwig, Eric Walkingshaw: An Error-Tolerant Type System for Variational Lambda Calculus ICFP '12 Proceedings of the 17th ACM SIGPLAN international conference on Functional programming, 29-40, 2012 [HageHeeren] Jurriaan Hage and Bastiaan Heeren: Heuristics for type error discovery and recovery (revised) Implementation of Functional Languages (IFL 2006), volume 4449, 199-216, 2006 [Chitil] Olaf Chitil: Compositional Explanation of Types and Algorithmic Debugging of Type Errors ICFP '01 Proceedings of the sixth ACM SIGPLAN international conference on Functional programming, 193-204, 2001 [HaackWells] C. Haack and J. B. Wells: Type error slicing in implicitly typed higher-order languages European Symposium on Programming, 284-301, 2003 21