• Keine Ergebnisse gefunden

Erweiterung des Standard-Unifikationsalgorithmus zur Verwendung bei der Typisierung von Typklassen in Haskell

N/A
N/A
Protected

Academic year: 2022

Aktie "Erweiterung des Standard-Unifikationsalgorithmus zur Verwendung bei der Typisierung von Typklassen in Haskell"

Copied!
51
0
0

Wird geladen.... (Jetzt Volltext ansehen)

Volltext

(1)

Erweiterung des

Standard-Unifikationsalgorithmus zur Verwendung

bei der Typisierung von Typklassen in Haskell

Bachelorarbeit B.Sc. Informatik

Goethe-Universit¨ at Frankfurt am Main

vorgelegt von:

Bernd von Wahden MN: 5343458

betreuet von:

Prof. Dr. Manfred Schmidt-Schauß Nils Dallmeyer

April 2017

(2)
(3)

Erkl¨ arung

Hiermit erkl¨ are ich, dass ich die Bachelorarbeit selbst¨ andig und oh- ne Benutzung anderer als der angegebenen Quellen und Hilfsmittel verfasst habe.

Frankfurt, den 06.04.2017

(4)
(5)

Zusammenfassung

Im Rahmen dieser Arbeit wird ein Programm zur Unifikation auf Typen unter Ber¨ucksichtigung von Typklassenbedingungen in der funktionalen Programmier- sprache Haskell entwickelt und umgesetzt. Es werden Algorithmen zur Unifika- tion unter Typklassen vorgestellt und implementiert sowie ein Interface erstellt, welches f¨ur Ein- und Ausgaben Haskell-Syntax verwendet.

(6)

Inhaltsverzeichnis

1 Einf¨uhrung 6

1.1 Motivation . . . 6

1.2 Uber diese Arbeit . . . .¨ 6

1.3 Uber das Programm . . . .¨ 7

I Grundlagen 8

2 Unifikationsalgorithmen 9 2.1 Der klassische Unifikationsalgorithmus . . . 9

2.1.1 Typen . . . 9

2.1.2 Unifikation . . . 10

2.1.3 Algorithmus . . . 11

2.2 Erweiterung f¨ur Typklassen . . . 13

2.2.1 Typklassen . . . 13

2.2.2 Entscheiden von Typklassenbedingungen . . . 14

2.2.3 Erweiterte Unifikation . . . 15

2.3 Erweiterung f¨ur Konstruktorklassen . . . 16

2.3.1 Kinds . . . 16

2.3.2 Erweiterte Unifikation . . . 16

3 Lexer und Parser 21 3.1 Lexer . . . 21

3.2 Parser . . . 21

3.2.1 Aufgabe . . . 22

3.2.2 Rekursiv absteigender Parser . . . 23

II Programm 25

4 Unifikation 26 4.1 L¨osen von Unifikationsgleichungen . . . 26

4.1.1 Datentypen . . . 26

4.1.2 Funktionen . . . 27

(7)

4.2 Typklassenbedingungen . . . 28

4.2.1 Axiome . . . 28

4.2.2 Constraints . . . 29

4.3 Erweiterte Unifikation . . . 30

5 Interface 31 5.1 Grundlagen . . . 31

5.2 Befehle . . . 32

5.2.1 Syntax . . . 32

5.2.2 Semantik . . . 32

6 Ein- und Ausgabe 33 6.1 Lexer . . . 33

6.2 Parser . . . 34

6.2.1 Parser-Kombinatoren . . . 34

6.2.2 Grammatiken . . . 35

6.2.3 Beispiel . . . 37

6.3 Show-Instanzen . . . 37

III Anhang 39

A Liste der Befehle 40 A.1 Ubersicht . . . .¨ 40

A.2 Informationsbefehle . . . 40

A.3 Konfigurationsbefehle . . . 41

A.4 Beispiele . . . 42

B Parser-Grammatiken 46 B.1 Gleichungen . . . 46

B.2 Constraints . . . 47

B.3 Gesamt . . . 47

B.4 Axiome . . . 47

(8)

Kapitel 1

Einf¨ uhrung

1.1 Motivation

Typsysteme sind ein integraler Bestandteil der meisten h¨oheren Programmier- sprachen. Sie erh¨ohen das Abstraktionsniveau und vereinfachen somit das Pro- grammieren enorm. In jeder Sprache, die es erlaubt, Typdeklarationen auszulas- sen, ist die Berechnung von Typen eine der Hauptaufgaben des Typ-Checkers.

Ein wichtiger Teil dieser Typberechnung ist die Unifikation auf Typen, das L¨osen von Typgleichungen, um aus einer Menge von vorher bestimmten Bedingungen den gesuchten Typ konstruieren zu k¨onnen.

Ziel dieser Arbeit ist der Entwurf und die Implementierung eines Programms zur Unifikation unter Ber¨ucksichtigung von Typklassenbedingungen in Haskell.

Es soll ein System entstehen, das in der Lage ist, Typgleichungen zu l¨osen, und gleichzeitig Typbedingungen verifizieren kann, die sich durch Haskells Typ- klassensystem ergeben. Als Basis dient dazu die klassische Unifikation zur Be- rechnung einer Typsubstitution auf Typgleichungen. Diese wird durch Algorith- men zur Entscheidung von Klassenconstraints erweitert.

Dieses System soll zu didaktischen und Demonstrationszwecken genutzt wer- den k¨onnen. Es wird ein Interface ben¨otigt, das den Algorithmus auf benut- zerfreundliche Weise anspricht. Dazu sollen Ein- und Ausgabe in der normalen Haskell-Syntax m¨oglich sein.

1.2 Uber diese Arbeit ¨

Gliederung

Die Arbeit besteht aus zwei gr¨oßeren Teilen.

(9)

Im Grundlagen-Teil werden die verwendeten Grundlagen dargelegt, sowohl die umgesetzten Algorithmen als auch Techniken zum Verarbeiten von Eingaben.

Damit sollen die notwendigen Grundlagen zum Verst¨andnis der Implementie- rung gelegt werden.

Im Programm-Teil wird das erstellte Programm dokumentiert. Der Aufbau wird so sein, dass zun¨achst die Implementierung der im Grundlagen-Teil vorgestellten Unifikationsalgorithmen erl¨autert wird. Darauf folgt die Erkl¨arung der Benut- zerschnittstelle. Zum Schluss wird das Einlesen der Eingaben sowie die Ausgabe der Ergebnisse behandelt.

Dieser Teil dient vor allem dazu, die bei der Umsetzung der Anforderungen ver- wendeten Methoden und Konzepte darzustellen. Weitere Informationen zu der Implementierung finden sich oft im Anhang oder im kommentierten Quelltext.

Notation

Bestimmte Elemente sind zur besseren Lesbarkeit hervorgehoben:

Textform Schriftart Quelltext Schreibmaschine Schl¨usselw¨orter serifenlos math. Variablen kursiv

Einfache Zahlen in einer eckigen Klammer [7] beziehen sich auf die im Literatur- verzeichnis unter der jeweiligen Nummer angegebene Quelle. Zusammengesetzte Nummern [2.1.3] beziehen sich auf den zugeh¨origen Abschnitt dieser Arbeit.

1.3 Uber das Programm ¨

Das Programm wurde unter dem Glasgow Haskell Compiler, Version 8.0.2, [4]

entwickelt und getestet. Das Programm wurde vorwiegend in die interaktive Umgebung des GHC (GCHi) geladen und ausgef¨uhrt. Die in Programmteil und Anhang gezeigten Beispielausgaben sind auf diese Weise entstanden.

(10)

Teil I

Grundlagen

(11)

Kapitel 2

Unifikationsalgorithmen

In diesem Kapitel sollen die Unifikationsalgorithmen vorgestellt werden, welche im Programm zum Einsatz kommen.

Die Grundlagen zu Typen und Typklassen werden behandelt sowie ausgehend von der Standard-Unifikation erl¨autert, wie ein Algorithmus aussehen muss, der die gestellten Anforderungen erf¨ullt.

2.1 Der klassische Unifikationsalgorithmus

Der klassische Unifikationsalgorithmus bildet die Grundlage der anderen ein- gesetzten Algorithmen. Er ermittelt aus einer Menge von Typgleichungen eine Typsubstitution. Dieser Abschnitt beschreibt die notwendigen Grundlagen und erl¨autert das Verfahren.

2.1.1 Typen

Ein Typ ist eine Beschreibung f¨ur Objekte mit gleichen Eigenschaften.

Typen helfen dabei, von den Interna der Implementierung zu abstrahieren. Wenn etwas einen bestimmten Typ hat, kommen damit bestimmte Eigenschaften ein- her, ohne dass der Programmierer wissen muss, wie diese umgesetzt sind.

Ein Typsystem kann Fehler erkennen, bei denen auf Daten in einer nicht f¨ur diesen Typ vorgesehenen Weise zugegriffen wird.

In Haskell hat jeder Ausdruck einen Typ. Die Typisierung erfolgt statisch, d.h.

nach Compilierung des Programms sind die Typen aller Ausdr¨ucke bekannt.

Haskell besitzt ein starkes Typsystem, d.h. der Compiler f¨uhrt keine internen Typumwandlungen durch (z.B. kann ein Objekt mit Ganzzahltyp nicht in einem Kontext verwendet werden, in dem ein Gleitkommazahltyp erwartet wird).

Durch diese Eigenschaften k¨onnen Typfehler zur Laufzeit vollends vermieden werden.

(12)

Haskell erlaubt auch Typvariablen. Diese sind Platzhalter f¨ur irgendeinen belie- bigen Typ.

Definition 2.1 (polymorpher Typ). Ein polymorpher Typ T ist ein Typ, der Typvariablen enth¨alt. T ist von der Form

T = TV |TC T1 ... Tn,

wobei TV eine Typvariable, TC ein n-stelliger Typkonstruktor und T1,...,Tn

ebenfalls polymorphe Typen sind.

Wenn T keine Typvariablen enth¨alt, nennt man T

”monomorph“.

Polymorphe Typen erh¨ohen die Flexibilit¨at des Typsystems enorm, da die selbe Funktion auf verschiedenen Typen verwendet werden kann.

Eine solche Wiederverwendbarkeit nennt man Polymorphismus. Mit diesem wer- den wir uns bei der Behandlung von Typklassen noch genauer besch¨aftigen.

2.1.2 Unifikation

Der Begriff Unifikation wird in vielen verschiedenen Kontexten verwendet und beschreibt allgemein das Gleichmachen von Termen durch Ersetzung. Ein Unifi- kationsalgorithmus hat die Aufgabe, f¨ur jede Variable eine Ersetzung zu finden, sodass die Terme syntaktisch1 gleich sind.

Nachfolgend einige Definitionen, die die Aufgabe eines Unifikationsalgorithmus formalisieren:

Definition 2.2 (Typsubstitution). Eine Typsubstitutionσist eine Abbildung {α17→τ1, ..., αn7→τn} einer endlichen Menge von Typvariablen auf Typen.

τ σbeschreibt die Anwendung der Substitutionσauf den Typτ. Dabei werden alle Vorkommen vonαi durchτi ersetzt.

Definition 2.3(Unifikationsproblem). Eine Unifikation auf Typen hat als Ein- gabe eine Menge von Gleichungen der Formτ1=. τ2, wobeiτ1undτ2polymorphe Typen sind. Ausgabe ist eine Substitution (Unifikator)σ, sodass (τ1)σ= (τ2)σ f¨ur alle Gleichungen erf¨ullt ist.

Es gibt nicht nur einen Unifikator. Allgemein interessieren uns vor allem die Unifikatoren, welche m¨oglichst wenig Annahmen ¨uber die Typen der Variablen machen:

Definition 2.4 (allgemeinster Unifikator). Ein allgemeinster Unifikator einer Menge Γ von Typgleichungen ist ein Unifikatorσ, sodass es f¨ur jeden anderen Unifikatorρeine Substitutionγ mitρ(x) = (σ(x))γ f¨ur alle Typvariablenxin Γ gibt.

Allgemeinste Unifikatoren einer Gleichungsmenge sind bis auf Umbenennung der Typvariablen eindeutig.

1Es gibt auch semantische Unifikationsverfahren. F¨ur unsere Zwecke werden diese aber nicht ben¨otigt.

(13)

2.1.3 Algorithmus

Der Unifikationsalgorithmus verwendet einen regelbasierten Ansatz, d.h. es gibt eine Menge von Regeln, die auf die zu unifizierenden Gleichungen angewandt werden, bis keine Regel mehr angewendet werden kann oder ein Fail auftritt.

Regeln

Die Regeln bestehen aus einer Voraussetzung und einer Folgerung, welche un- tereinander notiert sind. Γ bezeichnet die Rest(multi)menge von Gleichungen und Γσdie Anwendung der Substitution σ auf beide Seiten aller Gleichungen in Γ.

DECOMPOSE Γ∪ {T C τ1 ...τn

=. T C τ10 ...τn0} Γ∪ {τ1

=. τ10, ..., τn=. τn0} (2.1) ORIENT Γ∪ {τ=. α}

Γ∪ {α=. τ}, (2.2)

wennαeine Typvariable undτ keine Typvariable ist

ELIM Γ∪ {α=. α}

Γ , (2.3)

wennαeine Typvariable ist

SOLVE Γ∪ {α=. τ}

(Γ{α7→τ})∪ {α=. τ}, (2.4) wenn die Typvariableαnicht inτ, aber in Γ vorkommt

Folgende Situationen f¨uhren zu einem Abbruch der Berechnung:

SYMBOL CLASH Γ∪ {T C1τ1... τn =. T C2 τ10 ...τm0 }

Fail , (2.5)

wennT C16=T C2

OCCURS CHECK Γ∪ {α=. τ}

Fail , (2.6)

wennτ 6=αund Typvariableαkommt inτ vor

(14)

Eigenschaften

Der Algorithmus terminiert f¨ur jedes Unifikationsproblem auf Typen.

Ein Fail tritt genau dann auf, wenn es keinen Unifikator gibt.

Andernfalls hat das resultierende Gleichungssystem Γ die Form {α1

=. τ1, ..., αn

=. τn},

wobei αi paarweise verschiedene Typvariablen sind und kein αi in einem τj

vorkommt. Der gefundene Unifikator ist dann {α17→τ1, ..., αn 7→τn}.

Dies ist stets ein allgemeinster Unifikator. Alle m¨oglichen Reihenfolgen von Re- gelanwendungen liefern bis auf Umbenennung der Variablen das gleiche Ergeb- nis.

Die Gr¨oße des Unifikators kann exponentiell in der Gr¨oße der Eingabemenge werden. Aus diesem Grund kann auch die Ausgabe des Programms exponentiell groß werden.

Literatur

F¨ur allgemeine Informationen zu Typen und Typsystemen siehe [8]. Die Defi- nitionen stammen aus [11] und die Grundlagen zu Polymorphismus aus [7] und [13].

Zur Unifikationstheorie siehe [10] und [1]. Die Komponenten des Algorithmus finden sich in [11], [10] und [14].

(15)

2.2 Erweiterung f¨ ur Typklassen

Kernthema dieser Arbeit ist die Erweiterung der Unifikation, sodass sie bei der Typsierung von Haskell-Programmen mit Typklassen verwendet werden kann.

Dieser Abschnitt soll die dazu n¨otigen Grundlagen erl¨autern. Konstruktorklas- sen klammern wir dabei zun¨achst aus, diese werden im n¨achsten Abschnitt be- trachtet.

2.2.1 Typklassen

Typklassen sind ein Sprachkonstrukt in Haskell zur Umsetzung von Polymor- phismus, genauer ad hoc Polymorphismus.

Ad hoc Polymorphismus, auch ¨Uberladung genannt, liegt vor, wenn eine Funk- tion f¨ur verschiedene Typen definiert ist und die Implementierung f¨ur jeden Typ verschieden sein kann.

F¨ur diese Form des Polymorphismus reichen Typvariablen allein nicht aus, da sie nur die gleiche Implementierung f¨ur jeden Typ zulassen.

Die Umsetzung von ¨Uberladung in Haskell besteht aus zwei Teilen:

Die Typklassendefinition (Schl¨usselwort class) listet die Funktionen inklusive Typdeklarationen auf, welche die Typklasse bereitstellt.

Die Instanzdefinition (Schl¨usselwortinstance) gibt die Implementierung der Funk- tionen f¨ur einen bestimmten Typ an. F¨ur jeden Typ, auf dem die Funktionen definiert sein sollen, muss eine eigene Instanz erstellt werden. Eine Instanzdefi- nition h¨angt daher von Typklasse und Typ ab.

Definition 2.5 (Instanz). Wenn die Typklasse Cl eine Instanzdefinition f¨ur den TypT besitzt, sagt man

”T hat eine Instanz vonCl“ und schreibtT ∈Cl.

Wenn von einem Typ bekannt ist, dass er eine Instanz einer bestimmten Typ- klasse hat, k¨onnen alle Funktionen, die die Klasse bereitstellt, auf diesem Typ verwendet werden.

Definition 2.6 (Typklassenconstraint2). Ein (Typklassen)constraint ist ein Ausdruck der Form Cl τ, wobei Cl ein Typklassenname und τ ein polymor- pher Typ ist. Er beschreibt die Bedingungτ ∈Cl.

Constraints der FormCl ak¨onnen in die Typsignatur von anderen Funktionen aufgenommen werden. Damit garantiert der Aufrufer, dass der Typ, welcher durch die Typvariablearepr¨asentiert wird, eine Instanz von Clhat.

Das Konzept von Typklassen ist damit ¨ahnlich dem von Interfaces in imperati- ven Sprachen wie Java.

Genau wie Interfaces k¨onnen auch Typklassen vererbt werden. Die Oberklassen einer Typklasse werden bei der Klassendefinition angegeben. Ein Typ, der eine

2Dieser Begriff wird in der Literatur uneinheitlich verwendet.

(16)

Instanz dieser Typklasse haben soll, muss auch eine Instanz aller Oberklassen haben. Wenn ein Typ eine Instanz dieser Klasse hat, dann d¨urfen auch die Funktionen aller Oberklassen auf diesem Typ verwendet werden.

Definition 2.7((direkte) Ober-/Unterklasse). T C1ist eine direkte Oberklasse von T C2, wenn T C1 in der Definition von T C2 angegeben ist.3

”Oberklasse“

bezeichnet die transitive H¨ulle der Relation

”direkte Oberklasse“.

T C1ist eine (direkte) Unterklasse vonT C2genau dann, wennT C2eine (direkte) Oberklasse vonT C1 ist.

2.2.2 Entscheiden von Typklassenbedingungen

In diesem Abschnitt wollen wir uns mit einem Verfahren besch¨aftigen, das fest- stellt, ob ein Typ eine Instanz einer Typklasse hat.

Definition 2.8 (Substitutionen auf Constraints). Die Anwendung einer Sub- stitutionσauf ein ConstraintCl τ, geschrieben (Cl τ)σ, entsprichtCl(τ σ).

Um entscheiden zu k¨onnen, ob ein Typ eine Instanz hat, ben¨otigt man Wissen

¨uber die vorhandenen Instanzen. Die Tr¨ager dieser Informationen nennt man Axiome.

Folgende 3 Arten von Axiomen lassen sich aus Haskells Typklassensystem relativ leicht erzeugen4:

1. Basisaxiome der FormCl τ, wobeiCleine Typklasse undτein 0-stelliger Typkonstruktor ist. Diese besagen, dassτ ∈Cl.

2. Implikationen der Form (C1, ..., Cm) => Cl (T C a1 ... an), wobei Cl eine Typklasse, T C ein Typkonstruktor, a1..n Typvariablen und C1..m

Constraints der Form Cli ai sind. Die Interpretation ist, dass (T C a1

...an)∈Cl, wenn alle Constraints C1, ..., Cm gelten.

3. Implikationen der Form Cl1 a => Cl2 a, wobei Cl1,2 Typklassen sind.

Diese besagen, dassCl1eine (direkte) Oberklasse vonCl2 ist.

Mithilfe dieser Axiome kann ein entschieden werden, ob ein ConstraintCl τgilt.

Wir beschr¨anken uns zun¨achst auf monomorphe Typen, d.h.τ enth¨alt keine Typvariablen. Daf¨ur kann folgender Algorithmus verwendet werden:

Die Menge MA der Axiome sei gegeben. Der Algorithmus arbeitet auf einer MengeC von Constraints. Diese wird initialisiert mit{Cl τ}, also dem Cons- traint, das ¨uberpr¨uft werden soll. Nachfolgende Schritte werden solange aufC angewandt, bis keine mehr angewendet werden k¨onnen:

3Syntax:class C1 a => C2 a, wennC1direkte Oberklasse vonC2sein soll

4Die ersten beiden Axiomarten k¨onnen aus den Instanzdefinitionen, die dritte aus den Klassendefinitionen abgelesen werden

(17)

1. F¨ur ein ConstraintCl T CausC, wobeiT Cein 0-stelliger Typkonstruktor ist: WennCl T C∈MAoderCl0T C ∈MA, wobeiCleine Oberklasse von Cl0 ist (¨uberpr¨ufbar mit dem 3. Axiomtyp), dann entferneCl T C ausC.

2. F¨ur ein ConstraintCl (T C τ1 ... τn) ausC: Wenn es ein Axiom (C1, ..., Cm) => Cl(T C a1...an) inMAgibt, dann ersetzeCl(T C τ1...τn) inC durch die Constraints (C1)σ, ..., (Cm)σ, wobeiσ={a17→τ1, ..., an7→τn}.

WennC leer ist, trifft das Constraint zu. Wenn inC Elemente verbleiben, die mit obigen Regeln nicht reduziert werden k¨onnen, dann ist das Constraint falsch.

Auf polymorphen Typen ist obiger Algorithmus auch erfolgreich, wenn nur Constraints vom Typ Cl a ¨ubrigbleiben, wobei a eine Typvariable ist. Diese Constraints k¨onnen nicht entschieden werden und m¨ussen als Bedingung stehen bleiben.

2.2.3 Erweiterte Unifikation

Mit den vorgestellten Algorithmen k¨onnen wir nun die Unifikation unter Ber¨uck- sichtigung von Typklassenbedingungen durchf¨uhren:

Eingabe ist eine Menge Γ von Typgleichungen und eine Menge C von Cons- traints.

Wir ignorieren zun¨achstCund wenden den klassischen Unifikationsalgorithmus [2.1.3] auf Γ an. Wenn ein Fail auftritt, ist die Unifikation fehlgeschlagen. An- dernfalls ergibt sich ein Unifikatorσ.

Dieser wird auf alle Constraints inC angewandt, woraus sich eine neue Cons- traintmengeC0 ergibt.

Obiger Algorithmus zur Entscheidung von Constraints [2.2.2] wird dann mit der ConstraintmengeC0 initialisiert und ausgef¨uhrt. Ergebnis des Algorithmus ist eine Constraintmenge C00. Befinden sich darin Constraints, die nicht von der FormCl asind, dann ist die Unifikation fehlgeschlagen. Ansonsten ist die Uni- fikation erfolgreich.

Ausgabe sindC00undσ.

Literatur

Informationen zur Theorie hinter Typklassen und den beschriebenen Algorith- men finden sich in [13], [14] und [11].

Die Umsetzung von Typklassen in Haskell wird beschrieben in [5], [2], [8] und [7].

(18)

2.3 Erweiterung f¨ ur Konstruktorklassen

Der erweiterte Unifikationsalgorithmus aus dem letzten Kapitel funktioniert nur mit Typklassen, deren Instanzen konkrete Typen sind. Haskell erlaubt aber auch Typklassen, die Typkonstruktoren als Instanzen haben, sogenannte Konstruk- torklassen. Diese ben¨otigen eine gesonderte Betrachtung. Die soll in diesem Ab- schnitt vorgenommen werden.

2.3.1 Kinds

Ein zentraler Begriff beim Umgang mit Konstruktorklassen ist der des Kinds.

Kinds sind f¨ur Typen, was Typen f¨ur Daten sind, Beschreibungen f¨ur gleiche Eigenschaften.

So wie in Haskell jeder g¨ultige (Daten-)Ausdruck einen Typ hat, hat jeder g¨ultige Typ einen Kind. Im Standard-Haskell setzen sich alle Kinds zusammen aus

• dem Basis-Kind * und

• dem Kindkonstruktork1→k2, wobei k1 undk2ihrerseits Kinds sind.

* steht f¨ur einen konkreten Typ, z.B. Integer.k1→k2steht f¨ur einen Typkon- struktor, der einen Typ mit Kindk1entgegennimmt und einen Typ mit Kindk2 zur¨uckgibt. Ein Typ mit Kind *→* ist z.B. Maybe, da dieser Typkonstruktor einen konkreten Typ erwartet und einen konkreten Typ zur¨uckgibt.

In Haskell kann jede Typklasse nur Typ(konstruktor)en eines bestimmten Kinds als Instanz haben. Die meisten eingebauten Typklassen erwarten den Kind * als Instanz, also einen konkreten Typ. Es gibt aber auch wichtige Standard- Typklassen, die Typkonstruktoren mit Kind *→* instanzieren. Dar¨uber hinaus kann man eigene Typklassen definieren, die Typen irgendeines Kinds als Instanz haben.

Um diese Typklassen mit abzudecken, muss der Unifikationsalgorithmus ein letztes Mal erweitert werden.

2.3.2 Erweiterte Unifikation

Durch das Erlauben von Konstruktorklassen entstehen 3 grundlegende Proble- me:

1. Jede Typklasse instanziert nur Typen bestimmten Kinds. Um diese Bedin- gung ¨uberpr¨ufen zu k¨onnen, m¨ussen die Kinds aller Typen und Typklassen bekannt sein.

2. Beide Seiten einer Unifikationsgleichung m¨ussen den gleichen Kind haben, um g¨ultig zu sein. Hierf¨ur m¨ussen die Kinds aller Gleichungen berechnet werden k¨onnen.

(19)

3. Typvariablen k¨onnen nun auch f¨ur Typkonstruktoren stehen und somit selbst Argumente haben.

Die L¨osung von 1. und 2. w¨urde die Implementierung eines Kind-Checkers n¨otig machen, der auch in der Lage ist, Kinds (insbesondere von Variablen) selbst zu berechnen. Das w¨are ein interessantes Thema f¨ur eine andere Arbeit, geht aber

¨

uber die Ziele und Inhalte dieser Arbeit ein wenig hinaus.

Die L¨osung von 3. f¨uhrt ebenfalls auf ein interessantes Thema, n¨amlich die Unifikation h¨oherer Ordnung.

Der bisher verwendete Algorithmus [2.1.3] zum L¨osen eines Gleichungssystems l¨ost Unifikationsprobleme 1. Ordnung, da er Variablen nur f¨ur konkrete Typen ohne Argumente zul¨asst. F¨ur Konstruktorklassen ben¨otigen wir nun einen Uni- fikationsalgorithmus, der auch mit Variablen h¨oherer Ordnung umgehen kann.

Unifikation h¨oherer Ordnung ist i.A. unentscheidbar und selbst, wenn man L¨osungen finden kann, sind diese nicht mehr eindeutig bis auf Umbenennung.

Aus diesen Gr¨unden ist es im gegebenen Rahmen unm¨oglich, eine nur ann¨ahernd vollst¨andige L¨osung zu implementieren.

Da sich am Entscheiden von Klassenconstraints [2.2.2] mit Konstruktorklassen wenig ¨andert, wenn wir den Kind-Check ignorieren, lohnt es sich jedoch, zumin- dest eine Teill¨osung des Problems zu finden, die die potenziellen Einsatzbereiche des Programms vergr¨oßert.5

L¨osen von Gleichungen

Die Neuerung im Vergleich zu [2.1.3] sind Typvariablen mit Argumenten. Die Syntax eines polymorphen Typs T [2.1] muss deshalb erweitert werden zu

T = TV T1 ... Tn|TC T1... Tn. Wie geht der Algorithmus mit diesen Variablen um?

Wir versuchen, den Grundalgorithmus so wenig wie m¨oglich zu ver¨andern und gleichzeitig die n¨otigen ¨Anderungen im gleichen Stil zu halten.

Da der Algorithmus [2.1.3] ein regelbasierter Algorithmus ist, liegt es nahe, neue Regeln zu definieren.

Die bisherigen Regeln bleiben g¨ultig, wenn man an jeder Stelle, an der vorher einfach eine Typvariable stand, jetzt ausschließlich eine Typvariable annimmt, die keine Argumente besitzt.6

Eine Gleichung, die eine Typvariable mit Argumenten beinhaltet, wird (sofern kein Fail auftritt) mit den klassischen Regeln solange vereinfacht, bis

1. die Typvariable von SOLVE durch eine Nicht-Konstruktorvariable ersetzt wird

5Nachfolgende ¨Uberlegungen besitzen keine Quelle, sondern entsprechen meinen Ideen zur osung dieses Problems.

6Das muss nicht zwingend eine Typvariable sein, die f¨ur einen konkreten Typ steht. Das werden wir gleich sehen.

(20)

2. auf der linken Seite nur eine Variable ohne Argumente steht

3. die Typvariable auf der Hauptebene steht und auf der anderen Seite keine Variable ohne Argumente steht

Im 1. Fall hat sich das Problem erledigt, da die Variable ersetzt wurde und wir ohne Weiteres fortfahren k¨onnen.

Bei 2. haben wir ebenfalls keine Schwierigkeiten, da die Umformung der Glei- chung bereits beendet ist.

Bei 3. hingegen kommen wir mit den bisherigen Regeln nicht weiter, da die Typvariable auf etwas trifft, das ebenfalls noch umgeformt werden muss. Hier gibt es zwei m¨ogliche F¨alle:

1. Die Variable steht einer anderen Variable mit Argumenten gegen¨uber 2. Die Variable steht einem Typkonstruktor gegen¨uber

Diese beiden F¨alle m¨ussen unterschiedlich behandelt werden, was auf zwei neue Regeln f¨uhrt.

Wie m¨ussen diese Regeln aussehen?

Beginnen wir mit 1., d.h. auf beiden Seiten der Gleichung steht eine Variable mit Argumenten.

Das erinnert an die DECOMPOSE-Regel, bei der zwei Typkonstruktoren mit Argumenten aufgel¨ost werden k¨onnen. Der Fall mit Variablen ist analog bis auf zwei Unterschiede:

• Die beiden Variablen k¨onnen nicht wegfallen wie die Typkonstruktoren, sondern ben¨otigen eine eigene Gleichung

• Die Anzahl an Argumenten der beiden Variablen kann verschieden sein, ohne dass das einen Fehler darstellt

F¨ur den 2. Fall, also Variable mit Argumenten gleichgesetzt mit einem Typ- konstruktor, gilt das Gleiche, nur muss der Fall, in dem sich die Anzahl der Argumente unterscheiden, anders behandelt werden.

Wenn eine Variable auf eine Variable mit einer anderen Anzahl an Argumenten trifft, beh¨alt eine der beiden Variablen Argumente. Da die Anwendung links- assoziativ ist, werden die Argumente von hinten nach vorne zu neuen Glei- chungen zusammengesetzt und die ¨ubigen Argumente beh¨alt die Variable, die urspr¨unglich mehr Argumente hatte.

Wenn eine Variable auf einen Konstruktor trifft, gilt dies nur, wenn der Kon- struktor mindestens so viele Argumente hat wie die Variable.

Wenn wir das ber¨ucksichtigen, ergeben sich die folgenden Regeln:

(21)

DECOMPOSE2.1 Γ∪ {T V1τ1 ...τn=. T V2τ10 ... τm0 } Γ∪ {T V2=. T V1τ1... τn−m, τn−m+1=. τ10, ..., τn

=. τm0 }, wennn≥m.

DECOMPOSE2.2 Γ∪ {T V2τ10 ...τm0 =. T V1τ1... τn} Γ∪ {T V2=. T V1τ1... τn−m, τn−m+1

=. τ10, ..., τn

=. τm0 }, wennn≥m.

DECOMPOSE3.1 Γ∪ {T C τ1 ...τn

=. T V τ10 ...τm0 } Γ∪ {T V =. T C τ1 ...τn−m, τn−m+1

=. τ10, ..., τn

=. τm0 }, wennn≥m.

DECOMPOSE3.2 Γ∪ {T V τ10 ...τm0 =. T C τ1 ...τn}

Γ∪ {T V =. T C τ1 ...τn−m, τn−m+1=. τ10, ..., τn=. τm0 }, wennn≥m.

Nach Anwendung dieser Regeln wurde mindestens eine der beteiligten Typva- riablen zu einer Variable ohne Argumente. Die zugeh¨orige Gleichung kann nun mit den anderen Regeln weiter umgeformt werden.

Bleibt noch die Frage, was passiert, wenn eine Variable auf einen Konstruktor trifft und die Variable mehr Argumente hat. Dann kann die Variable mit obigen Regeln nicht auf eine Weise umgeformt werden, welche die Variable argumentfrei macht. Somit kann die Gleichung nicht aufgel¨ost werden und wir haben einen Fehler.

Um den Algorithmus zu komplettieren, werden deshalb noch folgende Fail- Regeln ben¨otigt:

MISSING ARGUMENTS1 Γ∪ {T C τ1... τn =. T V τ10 ... τm0 }

Fail , (2.7)

wennn < m.

MISSING ARGUMENTS2 Γ∪ {T V τ10 ...τm0 =. T C τ1 ...τn}

Fail , (2.8)

wennn < m.

(22)

Noch eine Anmerkung: Durch die Anwendung von DECOMPOSE3.x verliert nicht nur die Variable Argumente, sondern nat¨urlich auch der Typkonstruktor.

Durch Haskells Darstellung der partiellen Anwendung ohne explizite Funktions- notation sind die Regeln korrekt, wenn wir bei DECOMPOSE(1) darauf achten, dass die Konstruktoren gleiche Argumentzahl haben.

Damit haben wir einen Unifikationsalgorithmus h¨oherer Ordnung, der mit Kon- struktorgleichungen zumindest umgehen kann.

Entscheiden von Constraints

Am Entscheiden von Constraints [2.2.2] ¨andert sich - wie erw¨ahnt - nicht viel.

Wie bei DECOMPOSE muss auch hier genau die Anzahl an Argumenten der Typen in Constraints und Axiomen beachtet werden, um konkrete Typen von Typkonstruktoren zu unterscheiden.

Da nat¨urlich Variablen mit Argumenten in Constraints vorkommen k¨onnen, m¨ussen wir zuletzt die Erfolgsbedingung des Algorithmus aus [2.2.2] ein we- nig erweitern: In Kombination mit obigem Algorithmus zum L¨osen eines Glei- chungssystems mit Konstruktorvariablen gilt das Entscheiden eines Constraints als erfolgreich, wenn am Ende alle Constraints die Form Cl (a τ1 ... τn) ha- ben, wobeiaeine Typvariable undτ1, ...,τn eine beliebige Anzahl polymorpher Typen sind.

Literatur

Die Grundlagen von Kinds sind beschrieben in [7] und [15]. Grundlegende In- formationen zu Unifikation h¨oherer Ordnung finden sich in [10] und [3].

(23)

Kapitel 3

Lexer und Parser

Das Programm verwendet bestimmte Datenstrukturen, auf denen obige Algo- rithmen angewandt werden (mehr dazu im Programm-Teil). Der Nutzer soll die Daten aber in der Standard Haskell-Syntax eingeben k¨onnen. Damit das m¨oglich ist, muss diese in die interne Repr¨asentation umgewandelt werden. In diesem Kapitel werden wir die daf¨ur n¨otigen Schritte sehen.

3.1 Lexer

Zun¨achst findet eine lexikalische Analyse statt. Dabei wird die Eingabe in so- genannte Token unterteilt, welche die eingegebenen Zeichen in Symbolklassen (Bezeichner, Operationen, Trenner, etc.) vorkategorisieren.

Der Lexer kann auch eine geringe Anzahl von Fehlern erkennen, z.B. Zeichen(fol- gen), die nicht zur Syntax geh¨oren, oder Teile der Eingabe filtern, die f¨ur die Semantik nicht von Belang sind, z.B. Kommentare oder Leerzeichen, denen kein Token zugeordnet wird.

Der Lexer bearbeitet die Eingabe linear von links nach rechts. Die Ausgabe ist eine Liste von Token, auch Tokenstrom genannt.

Eine lexikalische Analyse ist zum Einlesen einer Eingabe nicht unbedingt not- wendig, vereinfacht jedoch das Parsen, da die Token das Abstraktionsniveau erh¨ohen und der Parser so nur noch zwischen verschiedenen Symbolklassen un- terscheiden muss, anstatt einzelnen Symbolen.

3.2 Parser

Der Parser ¨ubernimmt die eigentliche Umwandlung. Er ¨ubersetzt eine Folge von Token in einen Syntaxbaum, der dann weiterverarbeitet werden kann.

(24)

3.2.1 Aufgabe

Um die Aufgabe des Parsers formal zu definieren, benutzen wir das Modell einer kontextfreien Grammatik.

Definition 3.1 (Kontextfreie Grammatik). Eine kontextfreie Grammatik ist ein 4-Tupel, bestehend aus

1. N, einer endlichen Menge von Nichtterminalsymbolen

2. T, einer endlichen Menge von Terminalsymbolen mitN∩T =∅ 3. P ⊆N×(N∪T), einer endlichen Menge von Produktionen 4. S∈N, dem Startsymbol.

F¨ur (A, α1), ...,(A, αn)∈P schreibt man auchA→α1|...|αn.

Definition 3.2(Herleitung). SeiG= (N, T, P, S) eine kontextfreie Grammatik w0 kann auswdirekt hergeleitet werden (w→w0) genau dann, wennw=αγβ, w0=αδβ und (γ, δ)∈P mitα, β∈(N∪T).

wnkann ausw1hergeleitet werden (w1wn) genau dann, wenn esw1, ..., wn∈ (N∪T) gibt, sodasswi−1→wi f¨ur allei≤2≤n.

Definition 3.3(Sprache). Die SpracheL(G) einer GrammatikG= (N, T, P, S) ist definiert als

L(G) ={w∈T|S →w} (3.1) Die Frage, ob ein Wortw∈TzuL(G) geh¨ort, wird auch Wortproblem genannt.

Der Formalismus der kontextfreien Grammatiken ist m¨achtig genug, um Struk- turen wie verschachtelte Ausdr¨ucke beliebiger Tiefe zu erkennen, aber einfach genug, um das Wortproblem effizient entscheiden zu k¨onnen.

Da es i.A. mehrere Herleitungen f¨ur ein Wort gibt, macht es Sinn, diese zu kategorisieren. Folgende Ans¨atze sind f¨ur uns relevant:

Definition 3.4(Links-/Rechtsherleitung). Wird in einer Herleitung bei jedem Schritt immer ...

• das linkeste Nichtterminal ersetzt, ist sie eine Linksherleitung.

• das rechteste Nichtterminal ersetzt, ist sie eine Rechtsherleitung.

Eine Aufgabe des Parsers ist, das Wortproblem auf einer vorgegebenen Gram- matik zu l¨osen.

Das gen¨ugt aber nicht, da am Ende des Parsens eine Datenstruktur entstehen soll. Deshalb muss der Parser zus¨atzlich die Struktur der Eingabe erkennen.

Diese Struktur l¨asst sich allgemein beschreiben durch Syntaxb¨aume.

(25)

Definition 3.5(Syntaxbaum). SeiG= (N, T, P, S) eine kontextfreie Gramma- tik. Ein SyntaxbaumB eines Worteswist ein markierter Baum mit folgenden Eigenschaften:

• Die Wurzel ist mitS markiert

• innere Knoten sind mit Nichtterminalen markiert

• wenn ein Knotenv mit dem NichtterminalAmarkiert ist und seine Kin- der (von links nach rechts) die MarkierungenX1, ..., Xn tragen, dann gilt (A, X1...Xn)∈P

• durch Konkatenation der Blattmarkierungen von links nach rechts ergibt sichw

Der Parser muss also erkennen, ob die Eingabe aus einer vorgegebenen Gramma- tik hergeleitet werden kann und, wenn m¨oglich, einen Syntaxbaum konstruieren.

Eine Sache m¨ussen wir bei der Grammatik noch beachten: Da der Syntaxbaum i.A. auch die Semantik einer Eingabe vorgibt, sollte es f¨ur jedes Wort nur einen Syntaxbaum geben. Andernfalls kann es passieren, dass eine Eingabe je nach gew¨ahlter Herleitung eine andere Bedeutung hat. Formal heißt das: Die Gram- matik, nach der geparst wird, sollte eindeutig sein.

Definition 3.6(Eindeutigkeit). Eine kontextfreie GrammatikGheißt eindeu- tig genau dann, wenn es f¨ur jedes Wort w ∈ L(G) genau einen Syntaxbaum gibt.

3.2.2 Rekursiv absteigender Parser

Im Programm wird ein rekursiv absteigender Parser verwendet. Dies ist ein Top- Down Parser, d.h. ausgehend vom Startsymbol wird versucht, eine Herleitung vorw¨arts, zum Wort, zu finden. Die erstellte Herleitung ist eine Linksherleitung.

Verfahren

SeiG= (N, T, P, S) die Parser-Grammatik. F¨ur jedes Nichtterminal A gibt es eine Prozedur, die die SpracheL(N, T, P, A) erkennt. Diese geht jede Produktion vonAdurch und pr¨uft, ob diese anwendbar ist. Sowohl die Eingabe als auch die Produktion werden dabei von links nach rechts abgearbeitet.

St¨oßt der Parser auf ein Nichtterminal, wird dessen Prozedur aufgerufen. Ter- minalsymbole k¨onnen direkt gepr¨uft werden: Stimmt das Symbol mit dem ak- tuellen Zeichen des Inputs ¨uberein, wird in Produktion und Eingabe das jeweils n¨achste Symbol betrachtet. Andernfalls kann diese Produktion nicht angewandt werden, der Parser wird zur¨uckgesetzt und die n¨achste Produktion ¨uberpr¨uft.

Ist keine Produktion erfolgreich, kann die Eingabe nicht hergeleitet werden. Der Parser ist erfolgreich, wenn sowohl Eingabe als auch die aktuelle Produktion das (rechte) Ende erreicht haben.

(26)

Die Konstruktion des Syntaxbaums beginnt bei der Wurzel. Wenn eine Produk- tion ausgew¨ahlt wurde, wird der Syntaxbaum an der aktuellen Stelle (Nichtter- minal) um diese Produktion erweitert. Ist die Produktion anwendbar, wird sie behalten, sonst muss der Baum zur¨uckgesetzt werden.

Linksrekursive Grammatiken

Rekursiv absteigende Parser sind einfach zu implementieren, haben aber den Nachteil, dass sie nicht f¨ur jede kontextfreie Grammatik verwendet werden k¨onnen.

Da Nichtterminale als Prozedur realisiert sind, kann es passieren, dass eine dieser Prozeduren sich selbst aufruft, ohne die Eingabe zu verarbeiten, was zu einer endlosen Rekursion f¨uhrt.

Grammatiken, bei denen dieses Problem auftritt, nennt man linksrekursiv.

Definition 3.7(Linksrekursion). Eine kontextfreie GrammatikG= (N, T, P, S) ist linksrekursiv, wenn es ein NichtterminalA∈N gibt, sodassA →Aα mit α∈(N∪T).

Solche Grammatiken d¨urfen beim Parsen mit einem rekursiv absteigenden Par- ser nicht verwendet werden.

Literatur

[12] gibt einen ¨Uberblick ¨uber die genannten Schritte. Lexer sind in [14] und [1]

beschrieben, Grundlagen von Parsern finden sich in [5], [14], [1] und [6].

(27)

Teil II

Programm

(28)

Kapitel 4

Unifikation

Zu Beginn des Programm-Teils wollen wir uns mit der Umsetzung der Unifi- kationsalgorithmen besch¨aftigen. Wir werden sehen, welche Datentypen daf¨ur geeignet sind, und wie die zugeh¨orige Funktionalit¨at aussieht.

4.1 L¨ osen von Unifikationsgleichungen

Wir beginnen mit dem L¨osen von Unifikationsgleichungen gem¨aß [2.1.3] mit der Erweiterung aus [2.3.2]. S¨amtlicher Code nachfolgenden Abschnitts befindet sich im ModulUnification.

4.1.1 Datentypen

Zuerst m¨ussen wir uns Gedanken um sinnvolle Datentypen machen, auf denen wir dann die Unifikation ausf¨uhren.

Grundlegend, weil immer wieder ben¨otigt, ist die Darstellung polymorpher Ty- pen nach [2.3.2]. Dies geschieht durch den Datentyp

data UniExpr = Const String [UniExpr] | Var String [UniExpr],

welcher sich stark an der Definition orientiert.Stringspeichert den Namen der Variable (Var) bzw. des Konstruktors (Const).

Eine Unifikationsgleichung besteht aus 2 polymorphen Typen, also liegt es nahe, einen Datentyp mit zwei solchen Feldern zu verwenden:

data UniEq = UniEq UniExpr UniExpr

Eine Gleichungsmenge ist somit eine Liste von Gleichungen:1 type UniSet = [UniEq]

1Im Folgenden werden die Begriffe

Liste“ und

Menge“ in Bezug auf Mengen, die als Liste umgesetzt sind, synonym verwendet.

(29)

4.1.2 Funktionen

Nun kann die Funktionalt¨at umgesetzt werden.

Der gew¨ahlte Algorithmus besteht aus einer Menge von Regeln, die immer wie- der auf die zu unifizierenden Gleichungen angewandt werden. Deshalb liegt es nahe, f¨ur jede Regel eine Funktion zu erstellen, welche Umformungen gem¨aß dieser Regel durchf¨uhrt.

Diese Funktionen nehmen eine Gleichungsmenge entgegen, formen alle Glei- chungen nacheinander um und geben die neue Menge zur¨uck.

Ihr Typ istUniSet -> UniSet. Dies hat den Vorteil, dass die Funktionen konka- teniert werden k¨onnen zu einer großen Funktion selben Typs, welche alle Regeln der Reihe nach anwendet. Einziges Problem an diesem Vorgehen ist die SOLVE- Regel, da sie nicht die aktuelle Gleichung, sondern alle anderen Gleichungen in der Menge ver¨andert. Wenn man die Liste nach dem typischen Rekursionssche- ma durchl¨auft, kann die Ersetzung nur in den Gleichungen erfolgen, die nach der

”einsetzbaren“ Gleichung folgen, da die Funktion auf die restlichen Gleichungen keinen Zugriff mehr hat.

Um das zu umgehen, wird die SOLVE-Funktion zweimal angewandt, einmal auf die Liste selbst und nochmals auf ihre Umkehrung. Diese Doppelanwendung entspricht dem SOLVE-Schritt.

Die Funktion, welche alle Regeln ineinander vereint, ist somit

reverse . orient . elim .... . solve . reverse . solve, wobei ... f¨ur die ¨ubrigen Regelfunktionen steht. Das letzte reverse dient da- zu, die Menge mit ihrer vorherigen Form vergleichen zu k¨onnen. Damit kann festgestellt werden, ob sich durch Anwenden der Regeln noch eine Ver¨anderung ergibt oder der Algorithmus fertig ist.

Was noch fehlt, sind die FAIL-Regeln. Diese werden mithilfe einer Funktion check :: UniSet -> Maybe UniError

umgesetzt, die alle Gleichungen auf eine FAIL-Situation pr¨uft und gegebenen- falls einen Fehler (der TypUniErrorist eine Kapselung der Fehlerart) zur¨uckgibt.

Die Funktion

unificate :: UniSet -> Either UniError UniSet

setzt alle Teile zusammen. Sie nimmt eine Gleichungsmenge entgegen und ¨uber- pr¨uft mit check, ob ein Fehler vorliegt. Wenn ja, wird dieser zur¨uckgegeben, die Unifikation ist fehlgeschlagen. Andernfalls wird die Menge mit obiger kom- ponierter Funktion umgeformt. Wenn sich dadurch im Vergleich zur Ursprungs- menge keine Ver¨anderungen ergeben, dann kann keine Regel mehr angewandt werden, die Unifikation ist beendet, Ausgabe ist die Gleichungsmenge. Anson- sten ruft sich die Funktion mit der umgeformten Menge selbst auf. Somit setzt sich dieses Vorgehen fort, bis ein Fehler auftritt oder keine Umformungen mehr m¨oglich sind.

(30)

4.2 Typklassenbedingungen

Neben dem L¨osen von Gleichungen ist auch die Verarbeitung von Typklassenbe- dingungen nach [2.2.2], erweitert durch [2.3.2] ein wesentlicher Bestandteil des Programms. Darum soll es in diesem Abschnitt gehen.

4.2.1 Axiome

Der Algorithmus arbeitet mit Klassenconstraints und Axiomen. Wir begin- nen mit den Axiomen. Folgender Unterabschnitt beschreibt Inhalte des Moduls Axioms.

Datentypen

Wieder machen wir uns zun¨achst Gedanken ¨uber die Datentypen.

Algorithmus [2.2.2] verwendet 3 verschiedene Arten von Axiomen. Der Axiom- typ kennt eben diese Konstruktoren:

data Axiom = Basic String String

| Impl1 String String

| Impl2 [(String,String)] String String [String]

Typklassen und Typen werden der Einfachheit halber nur mit ihrem Namen re- pr¨asentiert.Basic nimmt einen Klassennamen und einen Typnamen und stellt die Basisaxiome dar. Impl1 nimmt zwei Klassennamen und repr¨asentiert die Oberklassenbeziehung, d.h. die 3. Axiomart.Impl2 setzt den 2. Axiomtyp um, bestehend aus der Voraussetzung, einer Liste von Constraints (Paare aus Typ- klassen und Variablen), und der Folgerung, einer Typklasse, einem Typ und einer Liste von Argumenten (Variablen).

Der Typ einer Axiommenge ist erneut festgelegt als type AxiomSet = [Axiom]

Standard-Axiome

Das Programm liefert eine Liste von standardm¨aßig benutzten Axiomen mit.

Dadurch m¨ussen die zu verwendenden Axiome nicht zus¨atzlich angegeben wer- den, wenn sie in den Standard-Axiomen enthalten sind.

Diese Standard-Axiome beziehen sich auf wichtige von Haskells eingebauten Typklassen. Sie sind dem GHCi (Version 8.0.2) entnommen und im Modul AxiomListzu finden.

Funktionen

Zur Verbindung zwischen Axiomen und Constraints kommen wir im n¨achsten Unterabschnitt.

(31)

F¨ur die Axiome allein gibt es nicht viel zu tun. Die FunktioncheckAx¨uberpr¨uft die Axiommenge auf Fehler, z.B. ob f¨ur jedes Paar aus Typklasse und Typ nur ein Axiom vorliegt (andernfalls w¨are das Ergebnis der Unifikation von der Reihenfolge der Axiome abh¨angig) oder ob die Typklassenaxiome (3. Art) nicht zu einer Rekursion f¨uhren, sodass eine Typklasse ihre eigene Oberklasse ist.

Die FunktionengetSubStep,getSubCls,getSuperStep,getSuperClsund getTypesFromCldienen dem Durchsuchen der Axiome nach bestimmten Infor- mationen. Sie werden vom zugeh¨origen Interface-Befehl aufgerufen (dazu sp¨ater mehr). getSubClsund getSuperCls werden zus¨atzlich beim Entscheiden von Constraints aufgerufen.

4.2.2 Constraints

Nun wenden wir uns den Constraints zu. Nachfolgender Code findet sich im ModulReduction.

Datentypen

Ein Constraint besteht aus einem Typklassennamen und einem polymorphen Typ. Dementsprechend hat der Typ

data ClConstr = ClConstr String UniExpr genau diese beiden Felder.

Der TypClSetist analog zuUniSetdefiniert als type ClSet = [ClConstr]

Funktionen

Der Aufbau des Programmteils zur Reduktion der Constraintmenge ist sehr

¨ahnlich zur eigentlichen Unifikation: Auch Algorithmus [2.2.2] hat eine Menge von Regeln, die nacheinander angewandt werden, bis keine Ver¨anderung mehr eintritt.

deleteAx :: AxiomSet -> ClSet -> ClSet

l¨oscht aus der Constraintmenge alle Constraints, die sich aus den Basisaxiomen (evtl. in Kombination mit den Oberklassenaxiomen) herleiten lassen (1. Regel).

R¨uckgabe ist die neue Constraintmenge.

replaceAxhat den gleichen Typ und verwendet die 2. Regel, d.h. die Funktion ersetzt alle Constraints, auf die ein Axiom 2. Art anwendbar ist, durch die Constraints in der Voraussetzung.

Diese Regelfunktionen werden erneut zu einer Funktion zusammengesetzt. Die Hauptfunktion des Moduls,

reduce :: AxiomSet -> ClSet -> Either ClError ClSet,

(32)

wendet diese auf die Constraintmenge an, bis keine Ver¨anderung mehr auftritt.

Dann pr¨uft die Funktion checkCl, ob die ¨ubrigen Constrainttypen alle Varia- blen sind. Findet diese einen Konstruktor, ist die Reduktion fehlgeschlagen, R¨uckgabe vonreduceist der Fehler. Andernfalls wird die reduzierte Constraint- menge zur¨uckgegeben, noch bereinigt von den Constraints, die sich aus anderen durch Oberklassenbeziehungen direkt herleiten lassen.

4.3 Erweiterte Unifikation

Wir haben das L¨osen der Unifikationsgleichungen und das Entscheiden von Constraints behandelt. Was noch fehlt, ist die Kombination beider zur erwei- terten Unifikation nach [2.2.3]. Diese befindet sich im ModulAlgo.

Die Funktion

algo :: AxiomSet -> ClSet -> UniSet -> Either Error (ClSet, UniSet)

setzt alle bisher dargestellten Komponenten gem¨aß Algorithmus [2.2.3] zusam- men.Errorist dabei ein Sammeltyp der Form

data Error = AtAx AxiomError | AtUni UniError | AtCl ClError.

Zun¨achst wirdunificateauf die Gleichungsmenge angewandt. Das resultieren- de Gleichungssystem wird mithilfe der Funktion

insertUni :: UniSet -> ClSet -> ClSet

weiterverarbeitet, die den Unifikator aus den Gleichungen auf die ¨ubergebenen Constraints anwendet. Zuvor werden die Axiome mitcheckAxauf Fehler ¨uberpr¨uft.

Die neuen Constraints werden nun durchreducebearbeitet, bis sich die endg¨ultigen Constraints ergeben. Ausgabe der Funktion sind die gel¨oste Gleichungsmenge und diese Constraints. Tritt bei einem der Schritte ein Fehler auf, wird der Fehler durchErrorgekapselt und zur¨uckgegeben.

(33)

Kapitel 5

Interface

In den folgenden Kapiteln wollen wir die Benutzerschnittstelle des Programms betrachten. Wir werden uns mit dem Aufbau, den verwendeten Techniken und den m¨oglichen Eingabeformen besch¨aftigen.

5.1 Grundlagen

Das Interface ist im Stil eines Interpreters aufgebaut, d.h. es besteht im Wesentli- chen aus einer Endlosschleife, in der immer wieder Eingaben entgegengenommen und sofort auswertet werden.

Es gibt einen Prompt, der die aktuelle Zeile anzeigt, in der Eingaben get¨atigt werden k¨onnen. Das Resultat wird in der darauf folgenden Zeile ausgegeben.

Es existieren 2 grundlegende Arten von Eingaben: Befehle, die sich auf das Interface beziehen, und Unifikationsgleichungen, die gel¨ost werden sollen.

(34)

5.2 Befehle

Wie bei den meisten Interpretern gibt es eine Menge von Befehlen, die den Interpreter selbst betreffen und bestimmte Eigenschaften ausgeben oder Ein- stellungen ver¨andern.

5.2.1 Syntax

In Anlehnung an die GHCi-Syntax beginnen die Befehle mit einem Doppel- punkt. Danach folgt der Name des Befehls und je nach Befehl noch weitere Argumente.

:name arg1 arg2 ... argn

F¨ur jeden Befehlsname existiert eine Kurzschreibweise in Form eines einzel- nen Symbols. Diese Alternativen geben dem Nutzer die Wahl: Die Langformen der Befehle sind oft deutlich aussagekr¨aftiger hinsichtlich ihrer Wirkung, die Abk¨urzungen sparen Schreibarbeit.

Die Befehle werden durch ein einfaches Aufteilen der Eingabe in W¨orter (ge- trennt durch Leerzeichen) erkannt und voneinander unterschieden. Aus der Wortliste werden dann die n¨otigen Argumente per Index extrahiert. ¨Uberz¨ahlige Argumente werden ignoriert.

Verwendet der Nutzer einen undefinierten Befehl oder gibt nicht gen¨ugend Ar- gumente an, gibt das Programm eine Fehlermeldung zur¨uck.

5.2.2 Semantik

Die verf¨ugbaren Befehle lassen sich in 2 gr¨oßere Kategorien unterteilen.

1. Informationsbefehle helfen dem Nutzer bei der Bedienung, indem Infor- mationen zu einem bestimmten Sachverhalt ausgegeben werden.

2. Konfigurationsbefehle ¨andern die Konfiguration des Programms, um ein anderes Verhalten zu erreichen.

Konfigurationsbefehle beziehen sich in diesem Programm auf die Axiome.

Constraints und Gleichungen werden zusammen eingegeben (dazu sp¨ater mehr), aber das Ergebnis von Algorithmus [2.2.2] h¨angt zus¨atzlich von den verwende- ten Axiomen ab. Jene Axiome m¨ussen ebenfalls definiert werden k¨onnen. Diese Aufgabe ¨ubernehmen Konfigurationsbefehle. Details zur Eingabe der Axiome werden wir ebenfalls sp¨ater behandeln.

Die Informationsbefehle haben verschiedene Aufgaben. Manche geben die m¨og- lichen Eingaben an, andere dienen allgemein der Orientierung bei Nutzung des Programms. Es gibt auch Befehle zum Durchsuchen des momentan aktiven Axiome nach bestimmten Informationen.

Eine Liste aller Befehle inklusive Beispiele findet sich im Anhang.

(35)

Kapitel 6

Ein- und Ausgabe

Jede Eingabe, die kein Befehl ist, gilt als Eingabe f¨ur die Unifikation. Diese soll Haskell-Syntax verwenden k¨onnen und ist daher deutlich schwieriger umzuset- zen als die Befehle. Gleiches gilt f¨ur die Eingabe benutzerdefinierter Axiome.

Dieses Kapitel soll die L¨osung aufzeigen.

6.1 Lexer

Nachdem festgestellt wurde, dass es sich nicht um einen Befehl handelt, oder der Befehl Axiome enth¨alt, wird die Eingabe bzw. die Axiommenge zun¨achst von einem Lexer bearbeitet. Dieser besteht aus einer Funktion1, die den String lexertypisch von links nach rechts in eine Liste von Token umwandelt.

Der f¨ur diesen Lexer ben¨otigte Tokentyp ist

data Token = Variable String | Name String

| Bracket BrType BrState | Separator SepType

| Constraint | Function | Equates

| Unknown String

mit den Hilfstypen

data BrState = Open | Close data BrType = Round | Square data SepType = Comma | Semicolon

Die TokenVariableundName erkennen Variablen- und Typnamen. Variablen beginnen mit Kleinbuchstaben, Typkonstruktoren und Typklassen mit Groß- buchstaben, gefolgt von beliebig vielen alphanumerischen Zeichen. Der Lexer erkennt diese Zeichenfolgen und schreibt den Namen in das Tokenargument.

1Alle Komponenten des Lexers finden sich im ModulHelper

(36)

Die ¨ubrigen Token repr¨asentieren bestimmte Symbole:Bracketsteht f¨ur runde und eckige Klammern (jeweils geschlossen oder offen),Separatorf¨ur Kommas und Semikola,Constraintf¨ur=>,Functionf¨ur->undEquatesf¨ur=ohne>.

Außerdem filtert der Lexer Whitespaces, diese werden nicht an den Parser wei- tergeleitet.

Unknownrepr¨asentiert den einzigen Fehlertyp, den der Lexer finden kann, n¨amlich Symbole oder Sequenzen, auf die keines obengenannter Token passt. Mit dem Auftreten dieses Tokens ist das Lexing fehlgeschlagen, die Eingabe enth¨alt ung¨ul- tige Zeichenfolgen. Die fraglichen Zeichen werden zwecks Fehlermeldung mit

¨ubernommen.

6.2 Parser

Die vom Lexer erstellte Tokenliste wird dem Parser ¨ubergeben.

6.2.1 Parser-Kombinatoren

F¨ur den Parser wurden Parser-Kombinatoren2verwendet. Dabei handelt es sich um eine Bibliothek f¨ur einen rekursiv absteigenden Parser.

Grundlagen

F¨ur jedes Nichtterminal muss eine Funktion geschrieben werden, welche die entsprechende Sprache erkennt. Diese Funktionen haben den Typ

type Parser a b = [a] -> [([a],b)],

wobeiader Tokentyp undbder Typ des Syntaxbaums ist. Der R¨uckgabetyp des Parsers ist eine Liste, da die Grammatik mehrdeutig sein kann. Außerdem wird eine fehlgeschlagene Regelanwendung durch eine leere Liste repr¨asentiert. Die Liste enth¨alt Paare aus einer Tokenliste, bei der es sich um die noch einzulesen- den Token handelt, und dem bisher konstruierten Syntaxbaum. Ein gegebenes Wort konnte hergeleitet werden, wenn es in der R¨uckgabeliste des entsprechen- den Parsers Paare gibt, die keine Token mehr enthalten, Resultat sind die zu- geh¨origen Syntaxb¨aume. Andernfalls konnte keine Herleitung gefunden werden, oder die Eingabe enth¨alt nach allen m¨oglichen Herleitungswegen noch Symbole, die nicht verarbeitet wurden.

Die Bibliothek enth¨alt grundlegende Funktionen zum Erkennen von Terminalen, z.B.symbol, sowie Kombinatoren, mit denen mehrere Parser zu einem gr¨oßeren zusammengesetzt werden k¨onnen. Die wichtigsten Kombinatoren sind<|>zum Kennzeichnen von Alternativen und <**>3 zum Konkatenieren von Parsern.

Die Umsetzung dieser Kombinatoren als Operatoren hat den Vorteil, dass die

2siehe [9]

3Das Symbol dieses und einiger anderer Operatoren wurde ver¨andert, um die Kompatibi- lit¨at mit derApplicative-Typklasse zu wahren.

(37)

Grammatiksyntax aus der Theorie nahezu direkt in einen Parser ¨ubersetzt wer- den kann. Die Elementarfunktionen geben als

”Syntaxbaum“ grunds¨atzlich die gefundenen Token zur¨uck. Um den gew¨unschten Syntaxbaum zu konstruieren, steht der Kombinator<@zur Verf¨ugung. Dieser wendet eine spezifizierte Funk- tion auf das Resultat an, um die Token in den Baum zu ¨uberf¨uhren.

Anwendung

In einem Compiler w¨are das Ergebnis des Parsens der pure Syntaxbaum, welcher dann einer semantischen Analyse unterzogen w¨urde. F¨ur unsere Zwecke gen¨ugt es, den Parser direkt die ben¨otigte Datenstruktur erzeugen zu lassen, da jede vom Parser erkannte Eingabe auch eine g¨ultige Eingabe ist.

Im Programm wird<!>statt<|>verwendet, wodurch bei Alternativen nur die erste erfolgreiche behalten wird. Damit entf¨allt das Durchsuchen der Resultatli- ste und bei sorgf¨altiger Konstruktion des Parsers stellt dies keine Einschr¨ankung dar. Dabei ist besonders auf die Reihenfolge der Produktionen zu achten, sodass die erste erfolgreiche Produktion auch die gew¨unschte ist.

Funktionsweise und Verwendung der Parser-Kombinatoren werden beschrieben in [12] und [6].

6.2.2 Grammatiken

Im Folgenden einige Grammatiken, die das Eingabeformat angeben. Der ¨Uber- sichtlichkeit halber sind diese Grammatiken reduziert, d.h. sie erkennen eine Teilsprache, welche syntaktische Besonderheiten z.B. einiger Typkonstruktoren ignoriert. Die vollst¨andigen Grammatiken sowie weitere Hinweise zur Syntax finden sich in Anhang B.

Gleichungen und Constraints

Gleichungen und Constraints werden zusammen eingegeben. Jede Eingabe, die kein Befehl ist, wird als diese Kombination interpretiert. Nachfolgende Gram- matik beschreibt die Eingabe der Unifikationsgleichungsmenge:4

USet → Eq Eqs

Eqs → Sep Eq Eqs |

Sep → , | ;

Eq → Ex=Ex

Exs → Ex Exs |

Ex → (V arExs) | (N ameExs) Das Startsymbol ist jeweils das oberste Nichtterminal.

4Zu beachten ist, dass diese und alle folgenden Grammatiken eigentlich auf Token definert sind. Zur einfacheren Lesbarkeit wurden hier die Token durch die zugeh¨origen Symbole ersetzt.

V arundN amebeziehen sich auf die TokenVariableundName.

(38)

Das Eingabeformat kann verbal folgendermaßen beschrieben werden: Die Uni- fikationsgleichungen werden mit Komma oder Semikolon getrennt eingegeben.

Jede Gleichung besteht aus zwei polymorphen Typen, die durch ein Gleichheits- zeichen miteinander verbunden werden. Ein polymorpher Typ ist entweder ein Variablen- oder ein Konstruktorname, jeweils mit einer beliebigen Anzahl Ty- pen als Argumente. Die Klammerung stellt sicher, dass die Argumente korrekt zugeordnet werden.

Als n¨achstes die Grammatik der Constraintmenge:5 CSet → Cl Cls

Cls → ,Cl Cls |

Cl → N ameEx

Constraints werden also mit Komma getrennt und ein Constraint besteht aus einem Typklassennamen und einem polymorphen Typ.

Zuletzt die Grammatik f¨ur die Gesamteingabe (jeden Nicht-Befehl):

All → (CSet) => USet | USet | CSet

Eine Eingabe besteht somit entweder nur aus einer Constraintmenge oder nur aus einer Gleichungsmenge (in diesen F¨allen wird die andere Menge implizit als leer angenommen) oder aus beiden, wobei die Constraintmenge in Klammern gesetzt und von den Gleichungen durch=>getrennt wird.

Axiome

Axiome sind kein Teil der Standard-Eingabe. Jedoch k¨onnen ¨uber bestimmte Befehle eigene Axiome definiert werden.6Die dabei eingegebenen Axiome folgen einer speziellen Syntax, die sich mit folgender Grammatik beschreiben l¨asst:

ASet → Ax Axs

Axs → Sep Ax Axs |

Nvs → ,N ame V ar Nvs |

Vs → V arVs |

Ax → N ame N ame | N ame V ar=>N ame V ar

| (N ame V ar Nvs) =>N ame(N ame V arVs)

| N ame(N ame V arVs )

Die einzelnen Axiome werden mit Komma oder Semikolon voneinander getrennt.

Die ersten drei Alternativen vonAxentsprechen den 3 verschiedenen Axiomar- ten. Die Syntax entspricht der im Theorieteil vorgestellten. Die letzte Alternati- ve deckt den Fall ab, dass ein Axiom 2. Typs keine Constraints als Voraussetzung hat.

5Alle Nichtterminale, die nicht mehr explizit aufgef¨uhrt sind, beziehen sich auf obenste- hende Grammatik(en).

6siehe Anhang A f¨ur Informationen zu diesen Befehlen

(39)

6.2.3 Beispiel

Nachfolgend ein (vereinfachtes) Beispiel f¨ur die Anwendung der Parser-Kombina- toren im Programm:

parseUniEq = parseEx <**> token [Equates] **> parseEx

<@ (\(e1,e2) -> UniEq e1 e2)

Der Parser f¨ur eine Unifikationsgleichung. Diese Funktion implementiert das NichtterminalEqobenstehender Grammatik.parseExist der Parser zuEx, die Funktiontokenerkennt eine Liste von Terminalen (in diesem Fall das Token f¨ur

=). Die Operatoren <**>und **>konkatenieren die Ergebnisse, d.h. erst wird der linke Parser ausgef¨uhrt, dann der rechte.**>gibt automatisch das Ergebnis des rechten Parsers zur¨uck, w¨ahrend<**>beide Ergebnisse zu einem Paar kom- biniert. Beide Operatoren sind rechtsassoziativ, wodurch die erste Zeile einen Parser erstellt, dessen Ergebnis das Token ignoriert (denn es tr¨agt keine Infor- mation ¨uber die Gleichung, sondern dient nur der Erkennung) und die beiden Seiten der Gleichung (TypUniExpr) zu einem Paar zusammenf¨ugt. Dieses muss nun noch in den Typ einer Unifikationsgleichung umgewandelt werden. Daf¨ur ist der Operator<@ zust¨andig, der eine Funktion auf das Paar anwendet, die dieses in den gew¨unschten Datentyp verpackt. Der Typ der Parse-Funktion ist somitParser Token UniEq.

Der Parser f¨ur eine Datenstruktur findet sich meistens in der N¨ahe ihrer Defini- tion. Grundlegende Parser, z.B. f¨ur die TokenVariable undName wurden ins ModulHelperausgelagert.

6.3 Show-Instanzen

Als Letztes wollen wir die Ausgabe des Programms betrachten.

Die Ausgabe des vorgestellten und implementierten Algorithmus sind eine Cons- traintmenge und eine Gleichungsmenge. Diese sollen in Haskell-Syntax ausgege- ben werden. Außerdem gibt es Befehle, die Axiome ausgeben, was ebenfalls in der gleichen Syntax wie die Eingabe erfolgen soll.

Um das zu erreichen, wird die eingebaute Typklasse Show verwendet. Diese stellt die Funktionshowzur Verf¨ugung, welche einen Datentyp in eineString- Repr¨asentierung umwandelt, die dann mit einer IO-Aktion ausgegeben werden kann.

Die Struktur ist ¨ahnlich zu den Parsern, d.h. es gibt f¨ur jeden Datentyp eine show-Funktion7und diese rufen sich untereinander auf, wenn ein Datentyp Teil eines anderen Datentyps ist. Beispielsweise ruft die show-Funktion von UniEq

7Bei Datentypen, die mit demtype-Schl¨usselwort definiert wurden, wird f¨ur die Ausgabe einnewtype-Alias erstellt.

(40)

dieshow-Funktion vonUniExprin folgender Weise auf, um eine Gleichung aus- zugeben:8

show (UniEq ex1 ex2) = show ex1 ++ " = " ++ show ex2

Gleiches gilt f¨ur Fehlertypen, d.h. wenn bei der Auswertung der Eingabe ein Fehler auftritt, wird dieshow-Funktion des zugeh¨origen Fehlers aufgerufen, die die Fehlerinformationen in eine Fehlermeldung umwandelt.

Wie bei den Parsern findet sich auch dieshow-Funktion eines Datentyps in der Regel im gleichen Modul wie die Typdefinition selbst.

8Auch das ist eine vereinfachte Fassung. Im Programm wird an der Stelle noch ber¨ucksichtigt, dass die ¨außersten Klammern von Typen oft ausgelassen werden k¨onnen.

(41)

Teil III

Anhang

(42)

Anhang A

Liste der Befehle

Eine Liste aller Interface-Befehle mit Beispielen

A.1 Ubersicht ¨

Nachfolgend eine tabellarische ¨Ubersicht ¨uber die verf¨ugbaren Befehle:

Name Abk¨urzung Kurzbeschreibung

help ? Hilfe

commands > Liste aller Befehle print $ Informationen zu Axiomen axioms ! aktive Axiome ¨andern

A.2 Informationsbefehle

:help

:helpgibt eine interne Hilfe zur Verwendung des Programms aus, insbesondere zur Eingabe von Gleichungen. Keine Parameter.

:commands

Befehl zur Auflistung aller Kommandos mit Hinweisen zur Syntax. Kurzform dieses Anhangs. Keine Parameter.

:print

Dieser Befehl gibt Infos zu den im Moment aktiven Axiomen. Er hat 2 Parame- ter:

1. Informationsart

(43)

2. Typklasse

G¨ultige Argumente f¨ur die Informationsart (jeweils bezogen auf die darauffol- gende Typklasse):

Argument Ausgabe

subclasses alle Unterklassen

directsubclasses alle direkten Unterklassen superclasses alle Oberklassen

directsuperclasses alle direkten Oberklassen

typesin alle Instanzen

Die Ausgabe erfolgt als Axiommenge, d.h. die gefundenen Typklassen bzw. Ty- pen werden in Axiome nach [2.2.2] verpackt.

A.3 Konfigurationsbefehle

:axioms

Dieser Befehl ¨andert die zur Unifikation verwendeten Axiome.

Die Parameter sind:

1. Art der ¨Anderung 2. (Liste von Axiomen)

Ob Axiome mit ¨ubergeben werden, h¨angt von der ¨Anderungsart ab.

G¨ultige Argumente daf¨ur sind:

Argument 2 Parameter Funktionalit¨at

add ja angegebene Axiome hinzuf¨ugen remove ja angegebene Axiome entfernen clear nein alle Axiome entfernen

default nein R¨ucksetzen auf Standard-Axiome

Jene Befehle, die eine Axiommenge entgegennehmen, sind die einzigen, welche von der in Abschnitt 5.2.1 beschriebenen Syntax abweichen. Alle Zeichen hinter der ¨Anderungsart werden als Axiommenge interpretiert und durch Lexer und Parser geschickt, unabh¨angig davon, ob und wie viele Leerzeichen sich darin befinden.

Die konfigurierten Axiome gelten nur, solange das Programm l¨auft. Bei Neustart werden sie automatisch auf Standard zur¨uckgesetzt.

(44)

A.4 Beispiele

Nachfolgend eine Reihe von Beispielaufrufen f¨ur die genannten Befehle

:help

Ausgabe wurde gek¨urzt

:commands

:print

F¨ur alle Beispiele wurden die Standardaxiome verwendet

(45)

(direct)subclasses

(direct)superclasses

typesin

(46)

:axioms

add

remove

(47)

clear

default

(48)

Anhang B

Parser-Grammatiken

Nachfolgend Parser-Grammatiken, die alle vom Programm erkannten Tokenfol- gen angeben (mit Erkl¨arung). F¨ur die Notation gilt das Gleiche wie im Pro- grammteil.

B.1 Gleichungen

USet → Eq Eqs

Eqs → Sep Eq Eqs |

Sep → , | ;

Eq → ExExRed=ExExRed

ExExRed → Ex | ExRed

Exs → Ex Exs |

Ex → V ar | (V arExs) | Const | (Const Exs )

| (Ex->Ex Funcs) | [ExExRed]

| (ExExRed,ExExRed Tups)

ExRed → Ex->Ex Funcs | Const Ex Exs | V arEx Exs Funcs → ->Ex Funcs |

Tups → ,ExExRed Tups |

Const → N ame | (->) | [] | () | (,Coms)

Coms → ,Coms |

Im Vergleich zur minimalen Fassung gibt es 2 wesentliche ¨Anderungen:

1. Im Typ ist nun Sondersyntax f¨ur Listen, Tupel und Funktionen m¨oglich 2. An bestimmten Stellen k¨onnen (runde) Klammern weggelassen werden Die spezielle Syntax ist auf 2 Arten umgesetzt: Einerseits wurde das neue Nicht- terminalConsteingef¨uhrt, welches neben Konstruktornamen auch Symbolkom- binationen als Konstruktor erlaubt, und andererseits wurden weitere Produk- tionen f¨ur den polymorphen Typ eingef¨uhrt, mit denen die Konstruktorsyntax

(49)

auch infix verwendet werden kann. Diese Konstruktoren werden intern in Namen umgewandelt (Function f¨ur ->, List f¨ur [] und TupleN f¨ur (,...,)), sodass sie theoretisch auch ohne spezielle Syntax verwendet werden k¨onnen.

Damit nicht jeder Ausdruck vollst¨andig geklammert sein muss, gibt es das Sym- bolExExRed, welches an den Stellen verwendet wird, an denen Klammern aus- gelassen werden k¨onnen. Hinzu kommen Spezialf¨alle wie nullstellige Konstruk- toren, die nie geklammert sein m¨ussen.

B.2 Constraints

An dieser Grammatik ¨andert sich nichts, alle ¨Anderungen stecken implizit im Nichtterminal Ex. Der Vollst¨andigkeit halber sei sie hier trotzdem nochmals angegeben.

CSet → Cl Cls

Cls → ,Cl Cls |

Cl → N ameEx

B.3 Gesamt

All → (CSet) =>USet | Cl=>USet | USet | CSet Neu ist hier, dass ein einzelnes Constraint nicht geklammert werden muss.

B.4 Axiome

ASet → Ax Axs

Axs → Sep Ax Axs |

Ax → N ameConst | N ame V ar=>N ame V ar

| N ameTyp | (N ame V arNvs) =>N ameTyp Typ → (Const V arVs) | [V ar] | (V ar->V ar)

| (V ar,V arCVs) CVs → ,V ar CVs | Nvs → ,N ame V ar Nvs |

Vs → V arVs |

In diesem Fall besteht die Erweiterung nur aus der Typsyntax. Analog zu den Gleichungen wurde daf¨ur der Typname durchConstersetzt und ein neues Nicht- terminalTyperstellt, das die Infix-Syntax erkennt.

Referenzen

Outline

ÄHNLICHE DOKUMENTE

Schritte beim Entwurf geometrischer Algorithmen1. maschinelle Darstellung der Ein- und

Eine Conditorwaare, welche aus jtart verfüßten Sruchtfähten, die zum Theile auch mit den fogenannten Frichtenäthern parfiimirt find, beteht und durch Ginjegen der Gefäße in ein

context Subject::hasChanged() post: observer ^ update (self). context Subject ::

Wir halten diesen Typ für so allgemein bekannt, daß er nicht noch weiter zergliedert werden muß, obwohl dies ohne weiteres möglich wäre, z.B.: Zahl ist eine Folge von Ziffern,

Die Definition sollte so nahe wie möglich am Euklid’schen Algorithmus sein: man zieht solange die kleinere von der größeren Zahl ab, bis eine der beiden Zahlen 0 ist; dann ist

Im Rumpf der Funktion elem wird zum einen der überladene Gleichheitsoperator verwen- det, zum anderen ruft sich die Funktion selbst rekursiv auf. Für beide Funktionsaufrufe gilt,

Als Workaround werden stattdessen bei einer potenziellen Mehrdeutigkeit in dem inferierten Typ einer Funktion, wenn eine explizite Typsignatur für diese Funkti- on angegeben ist,

Unter Einsatz der Halteklaue und der Sperrklinke kann die Hobelkette mit einer geeigneten Kettenspannvorrichtung zusammengefahren und gespannt werden. Es ist darauf