2 Guided Type Debugging

Werbung
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
Herunterladen