Ein Interpreter für die funktional

Ein Interpreter für die funktional
Ein Interpreter für die
funktional-reaktive Sprache Tempus
Diplomarbeit
Matthias Reisner
Matrikel-Nr.: 2309487
31. August 2011
Gutachter:
Betreuer:
Prof. Dr. rer. nat. habil. Petra Hofstedt
Dipl.-Inf. Wolfgang Jeltsch
Institut für Informatik, Informations- und Medientechnik
Lehrstuhl Programmiersprachen und Compilerbau
Eidesstattliche Erklärung
Der Verfasser erklärt an Eides statt, dass er die vorliegende Arbeit selbständig, ohne
fremde Hilfe und ohne Benutzung anderer als der angegebenen Hilfsmittel angefertigt
hat. Die aus fremden Quellen (einschließlich elektronischer Quellen) direkt oder indirekt
übernommenen Gedanken sind ausnahmslos als solche kenntlich gemacht. Die Arbeit ist
in gleicher oder ähnlicher Form oder auszugsweise im Rahmen einer anderen Prüfung
noch nicht vorgelegt worden.
Cottbus, 31. August 2011
Unterschrift des Verfassers
iii
Danksagung
An dieser Stelle möchte ich mich bei allen Personen bedanken, die mich während meines
Studiums und bei der Bearbeitung dieser Diplomarbeit unterstützt und motiviert haben.
Darunter sind die im Folgenden genannten besonders hervorzuheben.
Ich danke Frau Prof. Hofstedt für die Hilfestellung zum erfolgreichen Fertigstellen
dieser Arbeit, vor allem in den letzten Monaten der Niederschrift.
Weiterhin danke ich Wolfgang Jeltsch für die Bereitstellung dieses interessanten und
herausfordernden Diplomarbeitsthemas, für die zahlreichen Anregungen und Verbesserungsvorschläge, sowie für die unzähligen fachlichen und oft erheiternden Diskussionen,
auch weit über das Thema dieser Arbeit hinaus.
Zudem bin ich auch meinem ehemaligen Lehrer Uwe Kuhmann zu Dank verpflichtet,
ohne den ich mich möglicherweise nie zu einem Informatik-Studium entschlossen hätte.
Nicht zuletzt möchte ich mich bei meiner Familie bedanken, vor allem bei meinen Eltern,
die mir dieses Studium überhaupt erst ermöglicht haben. Um so bedauerlicher finde ich
es, dass mein Vater die Fertigstellung dieser Arbeit nicht mehr miterleben durfte.
Cottbus, 31. August 2011
Matthias Reisner
v
Inhaltsverzeichnis
1
Einleitung
2
Einführung in Tempus
7
2.1 Einführendes Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.2 Startzeitkonsistenz . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.3 Temporallogik und Curry-Howard-Isomorphismus . . . . . . . . . . . 12
3
Die Sprache
3.1 Syntax . . . . . . . . . . . . . . . . . . .
3.2 Typen . . . . . . . . . . . . . . . . . . .
3.2.1 Einfache Datentypen . . . . . . .
3.2.2 Ereignis- und Verhaltenstypen . .
3.2.3 Rekursive Datentypen . . . . . .
3.2.4 Algebraische Datentypen . . . . .
3.3 Ausdrücke . . . . . . . . . . . . . . . . .
3.3.1 Ausdrücke einfacher Datentypen .
3.3.2 Verhalten und Ereignisse . . . . .
3.3.3 Ausdrücke rekursiver Datentypen
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
17
17
18
19
20
20
22
23
24
25
27
Semantische Analyse
4.1 Notation . . . . . . . . . . . . . . . . .
4.2 Allgemeiner Algorithmus . . . . . . . .
4.3 Typinferenz . . . . . . . . . . . . . . .
4.4 Varianzprüfung und Funktor-Instanzen .
4.4.1 Varianzprüfung . . . . . . . . .
4.4.2 Allgemeines co- und contramap
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
33
33
34
37
39
39
41
Implementierung des Interpreters
5.1 Überblick über die Systemarchitektur . . . . .
5.2 Syntaktische Analyse . . . . . . . . . . . . . .
5.2.1 Datentypen des abstrakten Syntaxbaums
5.2.2 Lexer . . . . . . . . . . . . . . . . . .
5.2.3 Parser . . . . . . . . . . . . . . . . . .
5.3 Semantische Analyse . . . . . . . . . . . . . .
5.4 Auswertung . . . . . . . . . . . . . . . . . . .
5.5 Interpreter . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
45
45
47
47
49
49
52
55
58
4
5
1
.
.
.
.
.
.
vii
Inhaltsverzeichnis
6
Zusammenfassung und Ausblick
63
A Quellcode des Tempus-Preludes
67
B Quellcode des Einführungsbeispiels
69
C Installation und Beispielsitzung
71
Literaturverzeichnis
75
viii
1 Einleitung
Obwohl imperative Programmierung noch immer das am häufigsten verwendete Programmierparadigma ist, gewinnt funktionale Programmierung (FP) auch im industriellen
Umfeld mehr und mehr an Bedeutung. Das Fehlen von Seiteneffekten in den rein funktionalen Teilen von FP-Programmen ist nur ein Vorteil, der eine Quelle von fehlerhaften
Implementierungen von vornherein ausschließt. Zudem sind funktionale Programmierer
produktiver als imperative, da funktionale Programme oft durch wesentlich kürzeren und
prägnanteren Quelltext ausgedrückt werden können. Konzepte wie Funktionen höherer
Ordnung1 ermöglichen eine feingranulare Modularität, was zu wiederverwendbarem
Quelltext führt, dessen Komponenten separat getestet werden können. Dazu unterstützen
viele FP-Implementierungen verzögerte Auswertung (engl. lazy evaluation), wodurch
sich neue Möglichkeiten zur Strukturierung von Programmen ergeben [10, 12].
Während in imperativen Sprachen eher beschrieben wird, wie ein Ergebnis aus einer
festgelegten Befehlsabfolge ermittelt werden soll, steht in funktionalen Sprachen der
deklarative Charakter im Vordergrund. Dabei wird beschreiben, was berechnet werden
soll, die genaue Auswertungsreihenfolge wird der zugrunde liegenden Sprachimplementierung überlassen. Für die Interaktion mit dem Betriebssystem oder die Anbindung an
externe Bibliotheken sind jedoch auch in funktionalen Sprachen imperative Sprachbestandteile notwendig, die bspw. in Form von monadischen I/O-Aktionen2 beschrieben
werden können. Im Gegensatz zu diesem imperativen Ansatz existiert die funktionale
reaktive Programmierung, kurz FRP, die es erlaubt, den deklarativen Charakter der
Sprache zu wahren und zudem imperative Bestandteile hinter funktionaleren Konzepten
zu verbergen.
Funktionale reaktive Programmierung
FRP ist ein Programmierparadigma, das es erlaubt reaktive Systeme auf einem höheren Abstraktionsniveau deklarativ zu beschreiben. Reaktive Systeme interagieren mit
ihrer Umgebung, indem auf Eingaben innerhalb fester Zeitschranken reagiert wird und
Ausgaben entsprechend in die Umgebung zurückgeführt werden. Statt Reaktionen auf
1
Funktionen höherer Ordnung (engl. higher-order functions) können selbst wieder Funktionen als Parameter erhalten oder als Funktionsergebnis zurückliefern. Man spricht auch von Funktionen als „first-class“
Objekte.
2
I/O-Aktionen stellen Ablaufpläne dar, die beschreiben, wie ein Ergebnis aus einer Folge von Komponenten berechnet werden kann, wobei diese Komponenten selbst wieder I/O-Aktionen sind. I/O-Aktionen
sind first-class Objekte, können jedoch nicht in rein funktionalen Teilen verwendet werden [17].
1
1 Einleitung
Ereignisse im System explizit, bspw. durch Eventhandler-Registrationen, anzugeben,
erfolgt bei FRP eine Beschreibung des Systems durch sein zeitliches Verhalten in Abhängigkeit von dessen Teilsystemen, wobei üblicherweise ein kontinuierliches Zeitmodell
zugrunde liegt. Implementierungsdetails, wie die Ereignisbehandlung oder die technisch
bedingte Diskretisierung der Zeit, sollen durch abstrakte, funktionale Beschreibungen
verborgen werden. Der modulare Charakter drückt sich auch bei FRP-Systemen durch
ihren hierarchischen Aufbau aus kleineren Komponenten aus, die selbst wieder reaktive
Systeme beschreiben und über Verknüpfungsfunktionen miteinander verbunden werden.
Eine zentrale Rolle in FRP spielen Signale. Diese sind parametrisiert über einem Datentyp und können zu verschiedenen Zeitpunkten verschiedene Werte dieses Datentyps
liefern, sind also zeitabhängige Werte. Dabei wird zwischen den folgenden Signaltypen
unterschieden:
Kontinuierliche Signale können als Funktionen aufgefasst werden, die jeden Zeitpunkt
einer kontinuierlichen Zeit auf Werte des Parametertyps abbilden.
Segmentierte Signale liefern wie kontinuierliche zu jedem Zeitpunkt einen Wert, erlauben Änderungen des Wertes jedoch nur zu diskreten Zeitpunkten.
Diskrete Signale liefern nur zu diskreten Zeitpunkten einen Wert und können so als
Liste von Zeitpunkten mit zugehörigem Wert aufgefasst werden.
Kontinuierliche Signale werden häufig auch als Verhalten (engl.: behaviors) bezeichnet.
Zudem existieren Ereignisse (engl.: events), die jeweils ein Paar aus Zeitpunkt und
zugehörigem Wert darstellen. Da Ereignisse nur einmalig einen Wert liefern, werden
stattdessen auch Ereignisströme – teilweise synonym zu Ereignissen – verwendet. Diese sind eine Folge von Zeit-Wert-Paaren und entsprechen damit diskreten Signalen.
Außerdem von Bedeutung sind Signalfunktionen als Abbildungen von Signalen auf
Signale, die in einigen FRP-Implementierungen anstatt der Signale die grundlegenden
reaktiven Abstraktionen sind. In dynamischen Systemen, in denen zur Laufzeit ein
Wechsel zwischen verschiedenen Signalen erfolgen kann, benötigt man zur Vermeidung
von Speicher- und Zeitineffizienzen zudem eine Startzeit für Signale. Funktionen die
Signale aus einer Startzeit erzeugen werden Signalgeneratoren genannt.3
Die Anwendungsgebiete für FRP sind vielfältig und reichen von der Beschreibung
von Animationen und graphischen Benutzeroberflächen, über die Entwicklung von 3DSpielen bis hin zu visuellen Trackingverfahren oder Robotersteuerungen [27, 25, 21].
Vielen FRP-Implementierungen gemeinsam sind die – zumindest konzeptionellen –
Prinzipien, dass Reaktionen auf Ereignisse unverzüglich eintreten, Abläufe im System
echt parallel erfolgen, sowie die Modellierung von hybriden Systemen, d.h. der Unterstützung von kontinuierlichen und diskreten Signalen. Es besteht daher eine enge
Verwandschaft zu synchronen Datenflusssprachen wie Signal [7], Esterel [1], Lustre [9]
3
2
Eine ausführlichere Erklärung der Semantik von Verhalten und Ereignissen sowie die Erweiterung um
das Startzeitkonzept findet sich in Abschnitt 2.1 und Abschnitt 2.2.
oder Lucid Synchrone [23], sowie Sprachen für hybride Modellierung und Simulation
wie Simulink [26].
FRP erlaubt darüber hinaus die Beschreibung strukturell dynamischer Systeme, also von
Systemen, deren Struktur sich während des Programmablaufs ändern kann, wobei diese
Änderungen in einigen Varianten vor dem Programmstart nicht bekannt sein müssen. Zudem werden reaktive Konstrukte als first-class Objekte behandelt. FRP-Sprachen werden
daher oft in Form von Bibliotheken als eingebettete domänenspezifische Sprachen (engl.:
embedded domain-specific languages, EDSLs) in herkömmlichen Programmiersprachen
umgesetzt. Vorteile wie schnellere und einfachere Umsetzung unter Ausnutzung der
vollen Funktionalität und Optimierungsmöglichkeiten der Trägersprache sowie deren
Bibliotheken überwiegen meist die Nachteile wie mangelnder Spielraum für sprachspezifische Optimierungen oder Einschränkungen durch das Typsystem. Nicht zuletzt
aufgrund der zahlreichen Syntax- und Typsystem-Erweiterungen ist die funktionale
Programmiersprache Haskell die am häufigsten verwendete Trägersprache für EDSLs
[25]. Im Folgenden sollen die wichtigsten Vertreter von FRP-Implementierungen in
Haskell kurz vorgestellt werden.
FRP-Implementierungen in Haskell
Fran („Functional Reactive Animation“) [5] ist eine Haskell-Bibliothek zur Beschreibung von interaktiven und multimedialen Animationen. Hauptaugenmerk wurde auf die
Trennung von Datenmodell und Präsentation der Animationen gelegt. Animationen werden deklarativ durch primitive Verhalten und Ereignisse sowie Kombinatorfunktionen
beschrieben, wobei auch rekursive Beschreibungen erlaubt sind. Animationen werden
als zeitveränderliche Bilder, d.h. als Verhalten aufgefasst. Den Animationen liegt ein
kontinuierliches Zeitmodell zugrunde, das für die Präsentation beliebig diskretisiert
werden kann.
Yampa [21] führt die Ideen früherer FRP-Ansätze weiter. Nicht Signale, sondern Signalfunktionen werden als first-class Objekte betrachtet und Ereignisse zu Ereignisströmen
verallgemeinert. Eine wesentliche Neuheit an Yampa ist die Verwendung der ArrowNotation4 für die Beschreibung von reaktiven Systemen. Arrows zeichnen sich aus durch
höhere Modularität, Vermeidung von Speicherineffizienzen und bieten neue Ansatzpunkte für Korrektheitsbeweise oder Optimierungen [18].
Eine zweite FRP-Implementierung des Hauptautors von Fran ist Reactive [6]. Reactive
verwendet ebenfalls Verhalten und Ereignisse und nutzt dabei unter anderem die drei
klassischen Signaltypen. Zudem ist das Zeitmodell allgemeiner gehalten. Die Implementierung nutzt zahlreiche Haskell-Standardklassen und kombiniert Vorteile von Pull- und
Push-Systemen unter Ausnutzung von nebenläufigen Berechnungen.
4
Arrows sind eine Verallgemeinerung von Monaden. Es existiert eine syntaktische Erweiterung für
Haskell, durch die sich Arrows einfacher beschreiben lassen [13, 22].
3
1 Einleitung
Die durch Jeltsch in [15] vorgestellte FRP-Bibliothek Grapefruit nutzt ebenfalls die
Arrow-Syntax für die Beschreibung von ereignisgesteuerten reaktiven Systemen. Die
Verwendung von Signalgeneratoren bringt den Nachteil von wiederholten Berechnungen
und unerwünschten Effekten beim Wechsel zwischen verschiedenen Signalen mit sich.
Grapefruit vermeidet diese Probleme durch die Verwendung von first-class Signalen
statt Signalgeneratoren, sowie durch Vorausschau auf zukünftige Signalwerte unter
Verwendung von Haskells verzögerter Auswertung. Zudem wird insbesondere der
Startzeitpunkt eines Signals unter Ausnutzung verschiedener Typsystemerweiterungen
statisch im Typ des Signals kodiert, wodurch identische Startzeiten für alle abhängigen
Signalkonsumenten erzwungen werden.
Mit der Entwicklung der Sprache Tempus wurde ein anderer Weg beschritten: Tempus
ist keine FRP-Implementierung in Form einer EDSL in einer Trägersprache, sondern
eine eigenständige Programmiersprache. Die grundlegenden reaktiven Elemente sind
um Startzeiten erweiterte Verhalten und Ereignisse. Das Sichern der Übereinstimmung
von zueinander passenden Startzeiten in Ausdrücken ist ein zentraler Bestandteil der
Sprache und wird durch das Typsystem gesichert, so dass die entsprechenden Typfehler
bereits bei der Kompilierung erkannt werden.
Ziel und Aufbau der Arbeit
In dieser Arbeit soll gezeigt werden, wie ein Interpreter für die Sprache Tempus entwickelt wurde, der Tempus-Module laden und Tempus-Ausdrücke auswerten kann.
Syntaktisch fehlerhafte und typinkorrekte Ausdrücke bzw. Definitionen werden erkannt
und für korrekte Ausdrücke zudem ein allgemeinster Typ bestimmt.
Kapitel 2 gibt zunächst anhand eines einführenden Beispiels einen Einblick in die
Sprache Tempus. Die wichtigsten syntaktischen Konstrukte werden kurz beschrieben
sowie ein Einblick in die zeitabhängigen Typen der Sprache gegeben. Zudem wird
das Konzept der Konsitenz von Startzeiten diskutiert und es wird beschrieben, wie
sich die daraus ergebenen Konsequenzen in der Sprache Tempus widerspiegeln. Des
weiteren wird der Zusammenhang zwischen Tempus und der linearen Temporallogik
näher erklärt.
Eine detailliertere Beschreibung der Sprache Tempus erfolgt in Kapitel 3. Dabei wird
zunächst auf den syntaktischen Aufbau der Sprache eingegangen. Anschließend erfolgt
eine Beschreibung der in Tempus verfügbaren einfachen, zeitabhängigen und rekursiven
Datentypen. Außerdem wird eine Anleitung gegeben, wie algebraische Datentypen in
Tempus nachgebildet werden können. Aufbauend auf der Beschreibung der Typen folgt
eine ausführliche Diskussion aller primitiven Ausdrücke und Operatoren der Sprache
sowie deren Bedeutung.
In Kapitel 4 werden dann die theoretischen Grundlagen für die Implementierung des
Interpreters diskutiert. Nach dem Festlegen der verwendeten Notation erfolgt die ausführlichere Beschreibung des allgemeinen Algorithmus für die semantische Analyse
und des Typinferenzalgorithmus mit einer zusätzlichen Varianzprüfung.
4
Die Implementierung des Interpreters wird in Kapitel 5 beschrieben. Nach einem Überblick über die Systemarchitektur werden die Module für die syntaktische und semantische Analyse einer Tempus-Datei, sowie für die Auswertung von Ausdrücken erläutert.
Des weiteren wird ein Überblick über die Funktionen des Interpreters gegeben.
Abschließend erfolgt eine Zusammenfassung der vorhergehenden Kapitel mit einem
Ausblick auf mögliche Erweiterungen der Sprache und des Interpreters, die im Rahmen
dieser Arbeit nicht umgesetzt werden konnten.
In dieser Arbeit werden die folgenden Notationen zur Kennzeichnung besonderer Konzepte verwendet: Bezeichner für Variablen oder Funktionen werden kursiv, Schlüsselwörter der Sprache Tempus sowie syntaktische Konstrukte in Algorithmenbeschreibungen
werden fett gesetzt. Tempus-Operatoren werden zudem in der Unicode-Darstellung
verwendet (siehe Tabelle 3.1).
5
1 Einleitung
6
2 Einführung in Tempus
Dieses Kapitel soll einen Einblick in die Sprache Tempus geben. Es wird zunächst anhand eines kurzen Beispiels der Aufbau eines Tempus-Programms erklärt. Dabei werden
die wichtigsten syntaktischen und semantischen Konzepte vorgestellt und anschließend
eine Eigenschaft zeitabhängiger Typen, die Konsistenz von Startzeiten, erläutert. Daran
anschließend wird eine aus der Logik entstammende Motivation zur Sprache Tempus
näher ausgeführt.
2.1 Einführendes Beispiel
In diesem Abschnitt soll eine kurze Einführung in die Syntax und die Semantik der
Sprache Tempus gegeben werden. Als Beispiel dient ein reaktives System mit zwei
Tastern und einer Glühbirne, deren Verhalten durch button1 bzw. button2 und bulb
dargestellt werden. Die Glühbirne soll zu Beginn ausgeschaltet sein. Wird einer der
Taster gedrückt, so soll sich der Zustand der Glühbirne entsprechend ändern, d.h. jeweils
zwischen „an“ und „aus“ wechseln. Abbildung 2.1 zeigt einen Auszug aus dem TempusQuelltext für dieses Beispiel.
Das Beispiel enthält eine Reihe von Definitionen, wobei diese entweder Typdefinitionen
oder Wertedefinitionen sein können. Mit „– –“ eingeleitete Zeilen stellen dabei Quelltextkommentare dar. Typen, die durch eine Typdefinition eingeführt wurden, sollen
zur Unterscheidung von anderen Typen im Folgenden Typsynonyme genannt werden.
Typdefinitionen, eingeleitet durch das Schlüsselwort type, definieren neue Typen, die
synonym zu bereits bestehenden verwendet werden können. Das Beispiel nutzt die zwei
elementaren FRP-Datentypen in Tempus, Verhalten und Ereignisse. Verhalten sind in
Tempus zeitabhängige Werte, die als Abbildungen von Zeiten auf Werte aufgefasst
werden können. Ereignisse hingegen stellen Zeitpunkte mit einem zugehörigen Wert
dar. Als vorläufige Semantik für Verhalten und Ereignisse können wir also die folgende
annehmen
~behavior τ = Time → ~τ
~event τ = Time × ~τ .
Im Beispiel werden ein Wahrheitstyp Bool, ein zusätzlicher Verhaltenstyp Behavior und
ein Typ EventStream für Ereignisströme definiert.1 Wahrheitswerte können durch eine
1
Diese Definitionen sind in einem Modul des Interpreters – dem Tempus-Prelude – bereits vordefiniert,
siehe dazu Abschnitt 5.1.
7
2 Einführung in Tempus
– – im Tempus-Prelude vordefinierte Typen
type Bool = 1 + 1
type Behavior α = α × behavior α
type EventStream α = ν σ . event (α × σ)
– – Steuerung
– – control : Behavior Bool → EventStream 1 → Behavior Bool
value control = λ b . λ s . Behavior (head b) (ultraswitch (prepareUltraswitch b s))
– – init : Bool
value init = false
– – one : EventStream 1 → Behavior Bool
value one = control (Behavior init (const init))
– – two : EventStream 1 → EventStream 1 → Behavior Bool
value two = λ s1 . λ s2 . allXor (one s1 ) (one s2 )
– – Testfälle
– – button1 : EventStream 1
value button1 = pack [EventStream 1] (event 2 (hi,
pack [EventStream 1] (const ? never)))
– – button2 : EventStream 1
value button2 = pack [EventStream 1] (event 4 (hi,
pack [EventStream 1] (event 1 (hi,
pack [EventStream 1] (const ? never)))))
– – bulb : Behavior Bool
value bulb = two button1 button2
Abbildung 2.1: Tempus-Beispielprogramm für die Steuerung einer Glühbirne durch
zwei Taster. Der vollständige Quelltext des Programms findet sich in
Anhang B.
8
2.1 Einführendes Beispiel
Alternative über zwei Einheitstypen definiert werden, wobei die linke Alternative dem
Wert „falsch“ und die rechte dem Wert „wahr“ entspricht. Der mit einem Typparameter α
ausgestattete Typ Behavior wurde eingeführt, da Verhalten in Tempus keinen Startwert
haben. Der Typ Behavior bildet solche Verhalten mit Startwerten nach, indem dessen
Werte Paare von Werten des Typs α – den Startwerten – und von Verhalten über dem
Typ α sind. Zuletzt wird der Typ EventStream als ein ν-Typ definiert, mit dessen Hilfe
sich potentiell unendliche Datenstrukturen ausdrücken lassen. Ein Ereignisstrom über
einem Typ α entspricht einer Folge von Ereignissen, die jeweils Werte vom Typ α
liefern. Eine solche Folge wird durch ein Ereignis ausgedrückt, das ein Paar aus einem
Wert vom Typ α und einen Folgestrom liefert. Der Folgestrom ist wiederum vom Typ
EventStream α.
Des weiteren werden eine Reihe von Werten definiert, wobei jede Wertedefinition
durch das Schlüsselwort value eingeleitet wird. Zunächst wird eine Variable control
definiert, die eine Steuerung für einen Taster in Form eines Verhaltens über Wahrheitswerten beschreiben soll. Als Eingabe dienen ein Initialwert vom Typ Behavior Bool
und ein Ereignisstrom über Werte des Typs 1. Der Initialwert beschreibt den Ausgangszustand. Mit jedem Ereigniswert des Ereignisstroms, der einem Drücken des Tasters
entspricht, soll sich der aktuelle Wert des Steuerverhaltens umkehren. Als Ergebnis
wird ein Wert des Typs Behavior Bool zurückgegeben, wobei dem Startwert dieses
Verhaltens der Initalwert entspricht. Für die Beschreibung des Restverhaltens wurde der
Operator ultraswitch genutzt, der aus einer Folge von Verhaltenssegmenten ein einzelnes Verhalten konstruiert. Ähnlich wie Ereignisströme verwendet ultraswitch ν-Typen,
wobei ein Wert des ν-Typs hier als erster Parameter erwartet wird. Die Hilfsfunktion
prepareUltraswitch transformiert den übergebenen Initalwert und den Ereignisstrom in
einen Wert eines solchen ν-Typs. Auf die genaue Definition von prepareUltraswitch soll
an dieser Stelle nicht eingegangen werden, sie findet sich mit den Definitionen weiterer
Hilfsfunktionen im vollständigen Beispielcode in Anhang B.
Mit Hilfe der Funktion control lassen sich nun die einzelnen Tasterverhalten beschreiben.
Die Funktion one beschreibt ein einzelnes Tasterverhalten und erwartet als Eingabe
die Tastendrücke in Form eines Ereignisstroms. Definiert wurde one mittels control,
angewendet auf einen Behavior-Wert, der zu jedem Zeitpunkt den Initialwert init zurückliefert. Da der Ausgangszustand für die Schalter „aus“ sein soll, wurde für den Initialwert
init „falsch“ gewählt. Wie zuvor angedeutet, handelt es sich dabei nicht um einen einfachen Wahrheitswert, sondern um ein Verhalten eines konstanten Wahrheitswertes. Der
Grund hierfür ist die Einhaltung der Konsistenz von Startzeiten, eine Eigenschaft von
Verhalten und Ereignissen, die im nächsten Abschnitt erläutert wird. Der Initialwert wird
in prepareUltraswitch für die Transformation von Verhalten benötigt. Das Verwenden
lokaler Variablen, also von Variablen die durch λ-Ausdrücke gebundenen sind, ist für
die Konstruktion und Transformation von Verhalten (und auch von Ereignissen) nicht
ohne weiteres möglich, da dadurch die erwähnte Konsistenz von Startzeiten zerstört
werden würde. Dieses Problem wird hier durch Einführung eines konstanten Verhaltens
(statt der Verwendung eines einfachen Wahrheitswertes) behoben.
9
2 Einführung in Tempus
Zeitschritt
0
1
2
3
4
5
...
one button1
one button2
bulb
⊥
⊥
⊥
⊥
⊥
⊥
>
⊥
>
>
⊥
>
>
>
⊥
>
⊥
>
>
⊥
>
Tabelle 2.1: Zustände der Verhalten aus dem Beispiel. Zeitschritt 0 stellt den Ausgangszustand des Systems dar. Dabei bedeuten ⊥ den Zustand „aus“ und > den
Zustand „an“.
Zwei dieser in one initialisierten Tasterverhalten werden nun durch die Funktion two
miteinander verbunden. Die nach Anwendung auf die Ereignisströme für die Tastendrücke entstehenden Verhalten werden durch die Hilfsfunktion allXor punktweise mit
einem exklusiven Oder zu einem neuen Verhalten „verschmolzen“. Abschließend werden für die Taster zwei Beispieleingabeströme button1 und button2 definiert, die mit
Hilfe von two zum Verhalten der Glühbirne bulb kombiniert wurden. Der Ereignisstrom
button1 feuert nach zwei Zeitschritten und danach nie wieder. Ein niemals feuerndes
Ereignis wird durch die Primitive never dargestellt und mittels der Operatoren ? und in den passenden EventStream-Typ transformiert. Durch die Primitive pack lässt sich
ein Paar aus dem Einheitswert hi und dem Restereignisstrom dann – unter expliziter
Angabe des Ergebnistyps EventStream 1 – in den entsprechenden ν-Typen „verpacken“.
Analog wurde button2 als ein Ereignisstrom definiert, der nach vier Zeitschritten, dann
nach einem weiteren Zeitschritt und danach nie wieder feuert. Der Wert bulb stellt nun
das Verhalten der Glühbirne in Abhängigkeit der beiden Tastendruckströme button1
und button2 in Form eines Verhaltens über Wahrheitswerten dar. Tabelle 2.1 zeigt die
Verhalten one button1 bzw. one button2 für die Taster und das Verhalten der Glühbirne
bulb im Laufe der Zeit.
2.2 Startzeitkonsistenz
Im vorherigen Abschnitt wurden Verhalten zunächst als Werte aufgefasst, die zu jeder
Zeit einen Wert liefern. Dieses Konzept wird nun um sogenannte Startzeiten erweitert.
Die Startzeit t0 sei der Zeitpunkt, zu dem ein Verhalten erstellt wird und ab dessen Folgezeitpunkt es Werte liefern kann. Analog gilt für Ereignisse die Startzeit als der Zeitpunkt,
zu dessen Folgezeitpunkt das Ereignis frühestens feuern darf. Werte unmittelbar zur
Startzeit t0 werden also ausgeschlossen. Verhalten liefern demnach nur zu Zeiten t > t0
Werte; Ereignisse dürfen nicht zum Zeitpunkt t0 feuern.2 Entsprechend erhalten wir nun
2
Dass das Verbot von Werten zur Startzeit keine Einschränkung der Ausdruckskraft darstellt, wurde bereits
in Abschnitt 2.1 gezeigt: Durch die Definition von Behavior wurde ein Verhalten inklusive Startwert
nachgebildet. Ebenso lässt sich ein Ereignistyp Event α = α + event α definieren, der ein Feuern zum
Startzeitpunkt erlaubt. Die erste Alternative entspricht dabei einem sofort feuernden Ereignis, die zweite
einem später feuernden.
10
2.2 Startzeitkonsistenz
als neue Semantik von Verhalten und Ereignissen die folgende:
~behavior τ = (t0 , ∞) → ~τ
~event τ = (t0 , ∞) × ~τ .
Für gewöhnlich sind die Wertemengen eines Typs zu jedem Zeitpunkt gleich. Wir lassen
nun aber zu, dass sich die Wertemengen des zugehörigen Typs im Laufe der Zeit ändern
können. Der gleiche Typ kann also zu verschiedenen Zeitpunkten unterschiedliche
Wertemengen haben. Für einfache und rekursive Datentypen die keine Verhalten und
Ereignisse enthalten soll diese Eigenschaft jedoch nicht gelten.3 Sie sollen wie gewohnt
zu jedem Zeitpunkt dieselbe Wertemenge besitzten. Für den Typ behavior τ hingegen
soll die Wertemenge zu einem Zeitpunkt t alle die Verhalten umfassen, für die t der
Startzeitpunkt ist. Ebenso sollen Werte des Typs event τ zu einem Zeitpunkt t nur
Ereignisse mit diesem Startzeitpunkt enthalten.
Desweiteren soll gelten, dass ein Verhalten vom Typ behavior τ zu einem Zeitpunkt t
nur Werte liefern kann, die in der zugehörigen Wertemenge des Typs τ zum Zeitpunkt t
sind. Analog soll ein Ereignis vom Typ event τ, welches zum Zeitpunkt t feuert, zu
diesem Zeitpunkt nur Werte aus der Wertemgenge von τ zum Zeitpunkt t liefern. Ein
Verhalten vom Typ behavior N+ , also ein Verhalten über positiven Zahlen, kann bspw.
zu jedem Zeitpunkt einen beliebigen N+ -Wert liefern, da N+ ein einfacher Datentyp ist
und demnach zu jedem Zeitpunkt dieselbe Wertemenge hat. Interessanter ist der Fall
eines Verhaltens vom Typ behavior (event N+ ). Da die Werte des Verhaltens zu jedem
Zeitpunkt t aus der entsprechenden Wertemenge des Typs event N+ zum Zeitpunkt t
stammen müssen, kommen als Werte zum Zeitpunkt t nur diejenigen Ereignisse in Frage,
die t als Startzeitpunkt haben. Diese Einschränkungen bezüglich der Verschiebung von
Werten in der Zeit für Verhalten und Ereignisse wird Startzeitkonsistenz genannt.
Einige FRP-Implementierungen verwenden statt einer Startzeit für jeden zeitabhängigen
Wert, einen globalen Startzeitpunkt, der für alle Verhalten und Ereignisse im System
gilt. Soll in einem dynamischen System nun bspw. ein Wechsel eines Verhaltens b1 zu
einem Verhalten b2 stattfinden, so kann der Wert von b2 von dem Wert des Ereignisses
abhängen, das diesen Wechsel auslöst. Bis zum Zeitpunkt des Wechsels war nicht
bekannt, wann der Wert von b2 benötigt wird, oder ob er überhaupt jemals benötigt
werden wird. Um diesen Wert nun zu berechnen, besteht zum einen die Möglichkeit,
den gesamten Verlauf der Werte von b2 aufzuakkumulieren und zum Zeitpunkt des
Wechsels den aufakkumulieren Wert auszuwerten, oder zum anderen, b2 erst zum
Zeitpunkt des Wechsels Werte liefern zu lassen. Die erste Alternative führt jedoch
zu den bereits erwähnten Speicher- und Zeitineffizienzen, die sich bei lang laufenden
Systemen deutlich bemerkbar machen können, die zweite zu falschen Werten für b2 .
Indem identische Startzeiten für Signale die voneinander abhängen strikt eingehalten
werden, treten diese Probleme von vornherein nicht auf.
3
Die in Tempus verfügbaren Datentypen werden in Abschnitt 3.2 näher erklärt.
11
2 Einführung in Tempus
Ein wesentlicher Bestandteil der Sprache Tempus ist, diese Startzeitkonsistenz statisch,
d.h. durch Restriktionen im Typsystem, abzusichern. Werte können somit nicht in
der Zeit verschoben werden und gehören immer zur Wertemenge des entsprechenen
Zeitpunkts. Zeitverschobenen Werte würden in Ausdrücken nur in Form von lokalen
Variablen vorkommen. Ein Ausdruck der keine solchen Variablen enthält ist daher
zeitunabhängig. Er kann nur zu einem Zeitpunkt t gehörende Werte liefern, wenn auch
alle verwendeten Variablen zu diesem Zeitpunkt t gehörende Werte darstellen. Dies ist
bspw. für globale Variablen, also durch Wertedefinitionen eingeführte Variablen, immer
der Fall.
Im folgenden Beispiel soll die Startzeitkonsistenz anhand einer Funktion evil deutlich
gemacht werden. Dazu setzen wir eine Funktion
() : behavior (τ1 → τ2 ) → behavior τ1 → behavior τ2
const : τ → behavior τ
sowie
als gegeben voraus. Die Funktion () soll ein Verhalten bf über einer Funktion f sowie
ein Verhalten b als Parameter erwarten und f dabei auf jeden Wert von b anwenden.
Das resultierende Verhalten entspricht also dem durch f transformierten Verhalten b.
Die Funktion const soll aus dem übergebenen Wert x ein Verhalten konstruieren, das zu
jedem Zeitpunkt den Wert x hat. Wir konstruieren die Funktion evil nun wie folgt:
evil : behavior α → behavior (behavior α)
evil = λ b . const (λ _ . b) b .
Als Eingabe erhält evil ein Verhalten b und transformiert dieses Verhalten mittels () so,
dass das Ergebnisverhalten zu jedem Zeitpunkt den Wert b selbst hat, denn const (λ _ . b)
als Transformationsverhalten ignoriert den aktuellen Wert und ersetzt ihn durch b. Die
Startzeit der Werte des Ergebnisverhaltens ist nun aber zu jedem Zeitpunkt die des
Verhaltens b und nicht der Zeitpunkt selbst, was dem oben beschriebenen Konzept der
Startzeitkonsistenz widerspricht. Als Lösung des Problems wird die Funktion so definiert, dass das Transformationsverhalten als zusätzlicher, zeitunabhängiger Parameter
übergeben werden muss:
good : behavior α → behavior (α → behavior α) → behavior (behavior α)
good = λ b . λ f . f b .
Die oben erwähnte Funktion () existert in Tempus bereits in Form eines Operators.
Ein Pendant für Ereignisse () sowie const sind ebenfalls Operatoren der Sprache,
die für die Sicherung der Startzeitkonsistenz genutzt werden. Dass diese Operatoren
nicht nur „zufällig“ gewählt wurden, sondern sich durch Aufgreifen von Ideen aus einer
intuitionistischen Logik motivieren lassen, soll im nächsten Abschnitt gezeigt werden.
2.3 Temporallogik und Curry-Howard-Isomorphismus
Die grundlegende Idee zu Tempus stammt aus der Erweiterung einer intuitionistischen
Modallogik zu einer intuitionistischen linearen Temporallogik. Im Unterschied zu klas-
12
2.3 Temporallogik und Curry-Howard-Isomorphismus
sischen Logiken wird bei intuitionistischen eine Aussage nur dann als wahr angesehen,
wenn es einen konstruktiven Beweis für sie gibt. Daher gilt der Satz vom ausgeschlossenen Dritten A ∨ ¬A in intuitionistischen Logiken nicht.
Über den Curry-Howard-Isomorphismus lässt sich eine direkte Beziehung zwischen
formalen Logiken und Programmiersprachen herstellen: Logische Aussagen entsprechen Typen in der Programmiersprache und Beweise dieser Aussagen entsprechen
Ausdrücken (bzw. Programmen) mit dem entsprechenden Typ. Für aussagenlogische
Verknüfungen lassen sich die folgenden Typentsprechungen finden:
Konjunktion A ∧ B
Disjunktion A ∨ B
Implikation A → B
Negation ¬A
=ˆ
=ˆ
=ˆ
=ˆ
Produkttyp τ1 × τ2
Summentyp τ1 + τ2
Funktionstyp τ1 → τ2
τ→0
Nach den oben angegebenen Definitionen für Verhalten und Ereignisse liegt es nahe,
für diese Typen Entsprechungen in einer Logik in Betracht zu ziehen, der ein Zeitkonzept zugrunde liegt. Eben solch ein Zeitkonzept ist Bestandteil von Modal- bzw.
Temporallogiken.4 In allgemeinen Temporallogiken entsprächen Verhalten Aussagen,
die ab einem gewissen Zeitpunkt immer gelten müssten. Ereignisse hingegen würden
Aussagen entsprechen, die zu einem zukünftigen Zeitpunkt einen Wert liefern können.
Tatsächlich entsprechen die temporallogischen Operatoren und ^ genau diesen Anforderungen, wie Jeltsch in [16] darstellt. Eine Aussage A muss in der temporallogischen
Aussage A auf allen Pfaden zu jedem zukünftigen Zeitpunkt gelten. Hingegen muss
eine Aussage A in ^A nur im aktuellen Pfad irgendwann gelten. Wir erhalten also zu
den oben angegebenen Entsprechungen zwei zusätzliche, die sich auf die genannten
temporallogischen Operatoren beziehen:
Notwendigkeit A
Möglichkeit ^A
=ˆ Verhalten behavior τ
=ˆ Ereignis event τ
Durch den Curry-Howard-Isomorphismus lässt sich also eine direkte Beziehung zwischen Temporallogiken und der funktionalen reaktiven Programmierung herleiten.
In [2] geben Bierman und de Paiva unter anderem eine intuitionistische Modallogik IS4
an, die im sogenannten Hilbert-Stil definiert wird. Diese Definition besteht aus einer
Reihe von Axiomen und den folgenden vier Deduktionsregeln:
Γ, A ` A
Id
Γ`A→B
Γ`A
Mp
Γ`B
A ist Axiom
Axiom
Γ`A
∅`A
Box
Γ ` A
4
Das Zeitkonzept im Sinne einer Ordnung über Zeitpunkten ist tatsächlich nur in Temporallogiken
vorhanden. Das in Modallogiken gewöhnlich verwendete Modell einer Menge miteinander verbundener
„Welten“ lässt sich allerdings auch als Menge von Zeitpunkten auffassen.
13
2 Einführung in Tempus
Darin bedeutet Γ ` A, dass aus einer Menge von Annahmen Γ die Aussage A folgt und
A1 . . . An
, dass aus den Voraussetzungen Ai die Aussage B abgeleitet oder geschlossen
B
werden kann. Eine solche Aussage B wird Theorem genannt. Die Aussagen Ai müssen
dabei entweder Axiome oder selbst abgeleitete Theoreme sein. Da das Zeitkonzept von
Temporallogiken eher unser zuvor angegebenen Semantik von Verhalten und Ereignissen entspricht, verallgemeinern wir die Modallogik IS4 zu einer intuitionistischen
Temporallogik. Die angegebenen Axiome und Regeln gelten dabei völlig analog. Im
folgenden werden die angegebenen Regeln nun sowohl auf Logikebene, als auch auf
Ebene der FRP beschrieben.
Die Regel Id entspricht der Identität: Wird A vorausgesetzt, so kann auch A gefolgert
werden. Auf der Programmebene entspricht diese Regel der Eigenschaft, dass wenn eine
zuvor definierte Variable einen Typ τ hat, der Ausdruck der dieser Variable entspricht
auch den Typ τ hat. Mp steht für den Modus ponens, nach dem aus den Aussagen A → B
und A auf B geschlossen werden kann. Übertragen auf die FRP-Typebene entspricht
diese Regel der Funktionsanwendung: Wird eine Funktion des Typs τ1 → τ2 auf einen
Ausdruck des Typs τ1 angewendet, so ist das Ergebnis ein Ausdruck des Typs τ2 .
Axiom lässt eine Folgerung von A ohne Voraussetzungen zu, sofern A ein Axiom
ist. Bierman und de Paiva geben insgesamt 14 Axiome an, von denen hier nur zwei
beispielhaft beschrieben werden:
(A → C) → ((B → C) → (A ∨ B → C))
(A → B) → (A → B)
(∨E)
(I)
Durch das Axiom ∨E wird die Elimination einer Disjunktion beschrieben. Lässt sich
aus zwei Ausagen A und B jeweils die gleiche Aussage C schlussfolgern, so kann eine
Disjunktion A ∨ B direkt durch diese Folgerung C ersetzt werden. Per Curry-HowardIsomorphismus lässt sich eine Funktion case vom Typ (τ1 → σ) → (τ2 → σ) →
τ1 + τ2 → σ finden, die einer Fallunterscheidung über einem Wert e des Typs τ1 + τ2
entspricht. Sind zwei Funktionen f 1 bzw. f 2 mit den zugehörigen Typen τ1 → σ bzw.
τ2 → σ gegeben, so wird in Abhängigkeit des Wertes e entweder f 1 für die linke
Alternative oder f 2 für die rechte Alternative verwendet, um den Ausdruck des Zieltyps
σ zu bestimmen.
Axiom I kann als Darstellung eines Schlusses auf globaler Ebene aufgefasst werden. Wenn, bezogen auf einen festen Startzeitpunkt, die Bedingung A → B zu jedem
zukünftigen Zeitpunkt erfüllt ist und A auch tatsächlich global gilt, so folgt daraus,
dass dann auch B zu jedem zukünftigen Zeitpunkt gelten muss. Wie bereits erwähnt
entspricht dem Operator der Typ behavior, daher entspricht dem Axiom I der
Typ behavior (τ1 → τ2 ) → (behavior τ1 → behavior τ2 ). Genau dieser Typ ist
der des Tempus-Operators , der bereits im Beispiel zur Startzeitkonsitenz verwendet
wurde: Wenn zu jedem Zeitpunkt eine Funktion des Typs τ1 → τ2 angegeben werden
14
2.3 Temporallogik und Curry-Howard-Isomorphismus
kann und zu jedem Zeitpunkt ein Wert vom Typ τ1 vorhanden ist, dann kann durch
Funktionsanwendung auch zu jedem Zeitpunkt ein Wert des Typs τ2 bestimmt werden.5
Die letzte Regel Box beschreibt die Einführungsregel für den Operator und gibt die
Motivation für die Realisierung der im vorherigen Abschnitt beschriebenen Startzeitkonsistenz. Ein Ausdruck A kann als neues Theorem abgeleitet werden, wenn A selbst
ein Theorem darstellt. Da Theoreme zeitunabhängigen Werten entsprechen, sollten auf
FRP-Ebene auch nur Werte vom Typ behavior τ erzeugt werden können, wenn der
Wert vom Typ τ zeitunabhängig ist. Eben aus diesem Grund existiert der Operator const,
mit dem neue Verhalten nur aus Ausdrücken erzeugt werden können, die keine freien
Variablen enthalten.
Fügt man der aus IS4 abgeleiteten Temporallogik ein weiteres Axiom
^A ∧ ^B → ^(A ∧ B ∨ A ∧ ^B ∨ B ∧ ^A)
hinzu, so erhält man eine intuitionistische lineare Temporallogik. Dieses Axiom erzwingt
die Linearität der Zeit, d.h. die Totalität der Ordnung aller Zeitpunkte. Werden zwei
Aussagen A und B irgendwann gelten, so folgt daraus, dass A und B entweder zu
einem Zeitpunkt gemeinsam gelten müssen oder aber zunächst A und zu einem späteren
Zeitpunkt B oder aber zunächst B und später A. Dieses Axiom verbietet insbesondere
eine Verzweigung des Zeitstrahls in zwei Teilstränge, wobei A in dem einen und B in
dem anderen Strang gilt. Daher existiert in Tempus eine Primitive race, welche dem
genannten Axiom entspricht.6
5
Wie anhand der in Abschnitt 3.3 vorgestellten Typregeln zu erkennen ist, entspricht die zum Operator gehörende Typregel LiftAppBeh nicht exakt dem hier angegebenen Typ. Dies ist der Fall, da keine
gewöhnliche Funktion mit zwei Argumenten ist, sondern ein Infixoperator, für den keine Typregel mit
einem geschlossenen Typ angegeben werden kann.
6
Tatsächlich unterscheidet sich die zu race gehörende Typregel Race zum besagtem Axiom dadurch, dass
Race eine gecurryte Version dieses Axioms darstellt, die allerdings isomorph zur „echten“ Regel ist.
15
2 Einführung in Tempus
16
3 Die Sprache
Dieses Kapitel gibt einen tieferen Einblick in die Sprache Tempus. Es wird in Abschnitt 3.1 zunächst kurz auf syntaktischen Besonderheiten der Sprache eingegangen
und dann in Abschnitt 3.2 eine ausführliche Erklärung aller in Tempus verfügbaren
Datentypen gegeben. Darauf aufbauend werden in Abschnitt 3.3 die nativ unterstützten
Primitiven und Operatoren anhand der der Sprache zugrunde liegenden Typisierungsregeln erläutert.
3.1 Syntax
Wie bereits erwähnt, besteht ein Tempus-Programm aus einer Reihe von Typ- und
Wertedefinitionen. Die kontextfreie Grammatik dieser Definitionen ist in Abbildung 3.1
in erweiterter Backus-Naur-Form angegeben. Metasymbole wurden dabei durch Kursivund Großschreibung kenntlich gemacht; bei Terminalsymbolen wurde auf die üblichen Anführungszeichen verzichtet. Insbesondere runde und eckige Klammern werden
entgegen der üblichen Konvention nicht zur Gruppierung bzw. für optionale Konstrukte verwendet, sondern stellen Terminalsymbole dar. Zudem sind Schlüsselwörter der
Sprache fett dargestellt.
Zeilenkommentare werden durch die Sequenz „– –“ eingeleitet und erstrecken sich bis
zum Ende der Zeile, d.h bis zum nächsten Zeilenumbruch. Einzelne Token werden in
Tempus durch expliziten Whitespace voneinander getrennt. So wird bspw. der Ausdruck
λx.x als ein Token, das für einen Bezeichner steht, interpretiert, wohingegen λx.x
als Folge von vier Token interpretiert wird, die für einen λ-Ausdruck steht. Eine Ausnahme von dieser Regelung bilden Klammern, genauer alle Unicode-Zeichen aus den
allgemeinen Kategorien Ps [Punctuation, Open] und Pe [Punctuation, Close], sowie
Kommata. Die Angabe von explizitem Whitespace vor bzw. nach einer Klammer oder
einem Komma ist hierbei nicht erforderlich.
Die Angabe der Klammern für Paare ist in Tempus optional. Der Ausdruck (e1 , e2 )
entspricht damit dem Ausdruck e1 , e2 . Der Grund hierfür ist eine einfachere Notation
für n-Tupel zu ermöglichen, die statt (e1 , (e2 , (. . . , en ) . . .)) kurz als (e1 , e2 , . . . , en )
notiert werden können. Es ist allerdings zu beachten, dass fehlende äußere Klammern
um n-Tupel innerhalb von Ausdrücken die Bedeutung des Ausdruck aufgrund von
Operatorprioritäten verändern können.
Tempus unterstützt die Verwendung von ASCII- als auch von Unicode-Darstellungen
in Quelltextdateien. Daher existieren viele Operatoren in beiden Darstellungsformen.
17
3 Die Sprache
::= ε | Decl Prog
Prog
::= TypeDecl | ValDecl
Decl
TypeDecl ::= type Var FormalArgs = Type
FormalArgs ::= ε | Var FormalArgs
::= value Var = Expr
ValDecl
::= Type0
Type
::= MuType | NuType | Type1
Type0
::= Type2 → Type1 | Type2
Type1
::= Type3 + Type2 | Type3
Type2
::= Type4 × Type3 | Type4
Type3
::= Var ActualArgs | behavior Type5 | event Type5 | Type5
Type4
::= Var | N+ | (Type0 ) | 0 | 1
Type5
::= µ Var . Type0
MuType
::= ν Var . Type0
NuType
ActualArgs ::= Type5 | Type5 ActualArgs
::= Expr0
Expr
::= Expr1 , Expr0 | Expr1
Expr0
::= λ Var . Expr1 | Expr2
Expr1
::= Expr2 Expr3 | Expr2 Expr3 | Expr3
Expr2
::= Expr3 Expr4 | const Expr4 | behavior Expr4 | event Expr4 Expr4
Expr3
| FoldExpr [Type] Expr4 | Expr4
::= Var | PositiveLit | (Expr0 ) | ? | hi | PackExpr [Type] | BaseVal
Expr4
FoldExpr ::= fold | unfold
PackExpr ::= pack | unpack
::= left | right | case | first | second | expand | never | race | reflect
BaseVal
| ultraswitch | ultrajump
Abbildung 3.1: Syntax eines Tempus-Programms
Tabelle 3.1 zeigt die Entsprechungen der in Tempus erlaubten ASCII- und UnicodeZeichensequenzen.
3.2 Typen
Bevor die in Tempus verfügbaren Operatoren und primitiven Funktionen vorgestellt
werden, soll zunächst näher auf die in Tempus verfügbaren Datentypen eingegangen
werden, denn die Bedeutung vieler dieser Funktionen kann unmittelbar aus deren Typ
geschlossen werden. Zu diesen Datentypen zählen einfache Datentypen, außerdem die
in Kapitel 2 bereits beschriebenen Ereignisse und Verhalten, sowie rekursive µ- und
ν-Typen. Auf diese drei Gruppen von Typen wird in den nächsten Unterabschnitten
genauer eingegangen. Abschließend soll in Unterabschnitt 3.2.4 an einem Beispiel
die Rekonstruktion von algebraischen Datentypen durch Tempus-Datentypen gezeigt
18
3.2 Typen
ASCIIDarstellung
UnicodeDarstellung
∗
\
mu
nu
positive
×
λ
µ
ν
N+
−>
<∗>
<.>
()
→
hi
Unicode-Codepoint
U+00D7 Multiplication Sign
U+03BB Greek Small Letter Lamda
U+03BC Greek Small Letter Mu
U+03BD Greek Small Letter Nu
U+2115 Double-Struck Capital N +
U+208A Subscript Plus Sign
U+2192 Rightwards Arrow
U+229B Circled Asterisk Operator
U+2299 Circled Dot Operator
U+27E8 Mathematical Left Angle Bracket +
U+27E9 Mathematical Right Angle Bracket
Tabelle 3.1: Unicode- und alternative ASCII-Darstellung von Schüsselworten und Operatoren in Tempus
werden.
3.2.1 Einfache Datentypen
Tempus unterstützt die folgenden einfachen Datentypen:
Funktionen Der Funktionstyp wird als τ1 → τ2 notiert und beschreibt eine Abbildung von Werten des Typs τ1 auf Werte des Typs τ2 . Im Gegensatz zu anderen
Programmiersprachen erlaubt Tempus nur die Definition von totalen Funktionen.
Leerer Typ 0 wird für den leeren Typ (engl. empty, zero oder bottom type) verwendet,
der keine Werte enthält.
Einheitstyp 1 steht für den Einheitstyp (engl. unit type), der nur einen einzigen Wert
enthält.
Produkttyp τ1 × τ2 bezeichnet den Produkttyp (engl. product type) über zwei Typen τ1
und τ2 . Werte dieses Typs bestehen aus jeweils einem Wert der Typen τ1 und τ2 .
Summentyp τ1 +τ2 steht für den Summentyp (engl. sum type), auch Koprodukt genannt,
über zwei Typen τ1 und τ2 . Er stellt eine Alternative zwischen zwei Ausdrücken
dar, d.h. die Werte dieses Typ enthalten entweder einen Wert vom Typ τ1 oder
einen Wert des Typs τ2 .
Numerischer Typ Der einzige von Tempus nativ unterstützte numerische Datentyp ist
N+ , der alle positiven ganzen Zahlen umfasst. Wird im Folgenden von positiven
Zahlen gesprochen, so sind damit immer die positiven ganzen Zahlen gemeint.
19
3 Die Sprache
3.2.2 Ereignis- und Verhaltenstypen
Ereignis- und Verhaltenstypen wurden bereits in Kapitel 2 beschrieben. Zusammenfassend werden hier noch einmal die wesentlichen Eigenschaften genannt.
Verhalten behavior τ sind über einem Typ τ parametrisiert und stellen zeitabhängige
Werte dieses Typs dar. Jedes Verhalten besitzt einen festen Startzeitpunkt t0 , der dem
Zeitpunkt seiner Erstellung entspricht. Zu jedem Zeitpunkt t > t0 liefert ein Verhalten
einen Wert des Typs τ, wobei dem Startzeitpunkt kein Wert zugeordnet ist.
Ereignisse event τ besitzen wie Verhalten einen Typparameter τ. Sie stellen Zeitpunkte
mit einem zugehörigen Wert vom Typ τ dar. Jedes Ereignis kann maximal zu einem
Zeitpunkt t > t0 einen Wert liefern („feuern“), oder aber niemals. Dabei bezeichnet t0
wieder den Startzeitpunkt, vor und zu dem ein Ereignis keine Werte liefern kann.
3.2.3 Rekursive Datentypen
Tempus erlaubt keine Definition von Werten über implizite Rekursion, also durch
Verwendung der zu definierenden Variable innerhalb ihrer Definition. Dennoch lassen
sich rekursive Datentypen und deren Werte in Tempus beschreiben.
Sogenannte µ-Typen erlauben die Definiton von induktiven Datentypen und werden mit
µ α . τ notiert. Dabei ist α eine Typvariable die durch den µ-Typ gebunden wird und τ
ein Typ in dem α vorkommen darf.1 µ α . τ ist definiert als der bzgl. Mengeninklusion
kleinste Datentyp α, für den α = τ gilt. Betrachtet man τ als Funktion in Abhängigkeit
von α, also τ(α), so wird deutlich, dass es sich somit bei α = τ(α) um einen Fixpunkt
von τ handelt. Da es sich bei µ-Typen um kleinste Fixpunkte handelt, müssen die
entsprechenden Datenstrukturen endlich sein.
Neben endlichen Datenstrukturen können über ν-Typen auch unendliche Datenstrukturen
definiert werden. Analog zu µ-Typen werden ν-Typen notiert als ν α . τ. Sie entsprechen
dem größten Fixpunkt für den gilt α = τ(α), dürfen also unendliche Datenstrukturen enthalten. Weil ν-Typen in der Kategorientheorie das Duale Konzept zu µ-Typen darstellen,
werden ihre Werte auch als koinduktive Datenstrukturen bezeichnet.
Werte von µ- bzw. ν-Typen sollen im Folgenden kurz µ- bzw. ν-Werte genannt werden.
Zudem verwenden wir die Notation µν α . τ, wenn µ- bzw. ν-Typen gleichermaßen gemeint sind. Da µ-Werte endlichen Datenstrukturen entsprechen, liegt die Vermutung
nahe, dass auch die Auswertung solcher Werte immer terminiert. ν-Werte hingegen
stellen ggf. unendliche Datenstrukturen dar, was bei der Auswertung zu Problemen
in Form von Endlosrekursionen führen könnte. Dass die Auswertung von µ-Werten
tatsächlich immer terminiert und jeder ν-Wert bis zu einer beliebigen Tiefe terminierend
ausgewertet werden kann, soll in Unterabschnitt 3.3.3 diskutiert werden.
1
Die Typvariable α darf in τ allerdings nicht an beliebigen Stellen verwendet werden. An welchen
Stellen α genau vorkommen darf wird in Unterabschnitt 4.4.1 beschrieben.
20
3.2 Typen
Varianzen
Rekursive Datentypen werden mittels sogenannten Faltungsoperatoren2 in andere Werte transformiert. Damit diese Operatoren aber angewendet werden können, muss bei
rekursiven Datentypen µν α . τ für den jeweiligen Typ τ eine Funktor-Instanz ableitbar
sein. Das Konzept der Funktoren stammt aus der Kategorientheorie und bezeichnet dort
strukturerhaltene Abbildungen zwischen zwei Kategorien. Dabei wird unterschieden
zwischen kovarianten und kontravarianten Funktoren. Ist im Folgenden nur von Funktoren die Rede, so sind damit immer die kovarianten Funktoren gemeint; für kontravariante
Funktoren verwenden wir den Begriff Kontrafunktor. Funktorwerte lassen sich mittels
der Funktion comap, Kontrafunktorwerte durch die Funktion contramap transformieren.
Diese Funktionen haben für einen Funktor F und einen Kontrafunktor F die folgenden
Signaturen:
comap
: (α → β) → F α → F β
contramap : ( β → α) → F α → F β
Da Werte von Funktortypen Werte produzierende Objekte sind, entspricht die Funktionsanwendung comap f x den Werten, die durch den Funktorwert x produziert werden
und auf die dann die Funktion f angewendet wird. Werte von Kontrafunktortypen sind
Werte konsumierende Objekte. Daher wird im Ausdruck contramap f x zunächst die
Funktion f auf die durch x zu konsumierenden Werte angewendet und diese anschließend
durch x konsumiert.
Um solche Funktor- bzw. Kontrafunktor-Instanzen für einen rekursiven Datentyp %
automatisch ableiten zu können, ist eine zusätzliche Eigenschaft eines solchen Typs
notwendig. Eine durch % gebundenen Typvariable α muss innerhalb von % mit der zum
jeweiligen Funktor passenden Varianz vorkommen, d.h. – vereinfacht gesagt – sie muss
bezüglich der Typkonstruktoren an der „richtgen“ Stelle stehen.
An welchen Stellen eine Variable α stehen darf, lässt sich durch Varianzregeln festlegen.
Die für Tempus-Typen geltenden Varianzregeln zeigt Abbildung 3.2. Wir definieren
dabei einen Typ τ als bezüglich der Varianzprüfung korrekt, wenn unter einem Kontext ∆
gilt ∆ ` Φ(τ). Der Kontext ∆ enthält eine Menge von Typvariablen, wobei jeder dieser
Variablen eine Information angeheftet ist, mit welcher Varianz diese auftritt. Tritt eine
→
←
Variable α kovariant auf, wird dies mit α gekennzeichnet, tritt sie kontravariant auf mit α.
∆ bezeichnet dabei die Umkehrung der Varianzen für alle in ∆ enthalten Variablen:
←
→

a, falls α = a
def
def 
∆ = {α | α ∈ ∆} mit α = 
← .

→
a, falls α = a
Soll eine Typvariable α mit beliebiger Varianz aus einem Kontext ∆ entfernt werden,
→ ←
so notieren wir ∆ \ { α, α} kurz als ∆ \ {α}. Eine genauere Beschreibung der comapund contramap-Funktionen sowie die Herleitung der entsprechendne Funktor-Instanzen
wird in Unterabschnitt 4.4.2 gegeben.
2
Die in Tempus verwendeten Faltungsoperatoren fold und unfold werden in Unterabschnitt 3.3.3 erläutert.
21
3 Die Sprache
∆ ` Φ(τ1 ) ∆ ` Φ(τ2 )
Pair∆
∆ ` Φ(τ1 × τ2 )
∆ ` Φ(τ1 ) ∆ ` Φ(τ2 )
Sum∆
∆ ` Φ(τ1 + τ2 )
∆ \ {α} ∪ { α} ` Φ(τ)
Mu∆ /Nu∆
∆ ` Φ( µν α . τ)
→
∆ ` Φ(τ1 ) ∆ ` Φ(τ2 )
Fun∆
∆ ` Φ(τ1 → τ2 )
∆ ` Φ(τ)
Beh∆
∆ ` Φ(behavior τ)
∆ \ {α} ∪ { α} ` Φ(α)
→
Var∆
∆ ` Φ(τ)
Ev∆
∆ ` Φ(event τ)
∆ ` Φ(0)
Zero∆
∆ ` Φ(1)
Unit∆
Abbildung 3.2: Die Varianzregeln für die Sprache Tempus
3.2.4 Algebraische Datentypen
Einige Programmiersprachen erlauben die Definition von sogenannten algebraischen
Datentypen (ADTs). Dabei handelt es sich um möglicherweise rekursive Datentypen,
die über einer Reihe von Alternativen definiert sind, wobei jede Alternative aus einem
Bezeichner und Parametertypen besteht und einen Datenkonstruktor definiert. Tempus
unterstützt diese Datentypen in der aktuellen Spezifikation nicht. Dennoch lassen sich
mit Hilfe der zuvor beschriebenen Datentypen einen Vielzahl von ADTs nachbilden,3
wenngleich dabei auf die repräsentativeren Bezeichner verzichtet werden muss.
Anhand eines Listentyps List soll nun beispielhaft gezeigt werden, wie ein ADT in einen
äquivalenten Tempus-Datentyp transformiert werden kann. Eine Typdefinition für List
könnte in Haskell wie folgt aussehen:
type List α = Nil | Cons α (List α)
.
Der Typkonstruktor List erhält einen Typparameter α, der dem Elementtyp der Liste
entspricht. Des weiteren werden zwei Datenkonstruktoren eingeführt, wobei Nil die
leere Liste bezeichnet und Cons c l das Anfügen eines Elements c vor eine Liste l.
ADT-Alternativen stellen jeweils Produkte über den Parametertypen der einzelnen
Konstruktoren dar, d.h. wir können jeden Konstruktor durch ein n-stelliges Produkt von
Typen darstellen. Nil entspricht dabei einem 0-stelligen Produkt, also dem Einheitstyp 1.
Cons besitzt zwei Parameter, entspricht also einem Paartyp α × (List α). Zudem kann
ein Wert vom Typ List α immer nur eine der Alternativen Nil oder Cons sein. In
Tempus existiert genau solch ein Datentyp für Alternativen, nämlich der Summentyp +.
McBride bezeichnet diese Datentypen in [20] als reguläre Datentypen, wobei ν-Typen dort nicht
verwendet werden. Diese Typen umfassen nur rekursive Typen, bei denen sich die Typparameter
an den Rekursionsstellen der rechten Seite nicht verändern. Ein unendlicher Binärbaum der Form
Tree ε = Branch (Tree ε) ε (Tree ε) lässt sich mit Hilfe eines ν-Typs durch Tree ε = ν τ . τ × ε × τ
ausdrücken. Dagegen kann ein zu Tree isomorpher Typ Tree0 ε = Branch0 ε (Tree0 (ε, ε)) nicht durch
Tempus-Datentypen definiert werden.
3
22
3.3 Ausdrücke
Alternativen im ADT können daher direkt in Alternativen in Tempus transformiert
werden. Durch die Überführung von Datenkonstruktoren in Produkte und Alternativen
in Summen erhalten wir also den (vorläufigen) Typ
type List α = 1 + α × (List α)
.
Diese Typdefinition ist allerdings noch rekursiv über dem Typ List selbst definiert, was
in Tempus nicht gestattet ist. Diese rekursive Definition kann durch die Verwendung
eines rekursiven Tempus-Datentyps aufgelöst werden. Wir wollen nur endliche Listen
zulassen und verwenden daher einen µ-Typ:
type List α = µ % . 1 + α × %
.
Die rekursive Verwendung von List α wurde also durch die im µ-Typ gebundene Variable % ersetzt. Der so entstandene Tempus-Datentyp entspricht nun dem zuvor definierten
Haskell-Datentyp. Durch das Entfernen der Konstruktorbezeichner können semantische Informationen über den Datentyp verloren gehen. Diese Konstruktoren können in
Tempus jedoch nachträglich in Form von Wertedefinitionen eingeführt werden:
– – Nil
value Nil
: List α
= pack [List α] (left hi)
– – Cons : α → List α → List α
value Cons = λ e . λ l . pack [List α] (right (e, l))
3.3 Ausdrücke
Dieser Abschnitt beschreibt die Primitiven und Operatoren mit deren Hilfe sich TempusAusdrücke und darauf aufbauend Wertedefinitionen beschreiben lassen. Dabei wird für
Typangaben die folgende Notation verwendet: Sei e ein Tempus-Ausdruck und τ ein
Typ, dann bezeichnet e : τ den zu e passenden Typ τ.
Der einfachste Tempus-Ausdruck ist eine Variable x. Dabei kann x eine global oder lokal
definierte Variable sein. Für global definierte Variablen muss vor ihrer Verwendung eine
entsprechende Wertedefinition angegeben worden sein. Lokale Variablen werden durch
λ-Ausdrücke eingeführt, die im nächsten Unterabschnitt beschrieben werden.
Entsprechend den Unterabschnitten zu den verschiedenen Tempus-Datentypen werden
im Folgenden die Ausdrücke erläutert, mit deren Hilfe sich zu diesen Datentypen passende Ausdrücke beschreiben lassen. Abbildung 3.3 zeigt die Typisierungsregeln, die der
Sprache Tempus zu Grunde liegen. Ein Großteil dieser Regeln lässt sich direkt aus den
Axiomen der Temporallogik herleiten, wie bereits in Abschnitt 2.3 an Beispielen gezeigt
Γ ` e 1 : τ1
wurde. Die Regeln sind dabei wie folgt zu lesen: Lässt sich für eine Regel 1
Γ 2 ` e 2 : τ2
aus einem Kontext Γ1 für den Ausdruck e1 der Typ τ1 bestimmen, so kann unter dem
Kontext Γ2 für den Ausdruck e2 der Typ τ2 abgeleitet werden. Ist Γ1 = ∅, so entspricht
23
3 Die Sprache
dies Regeln mit einem zeitunabhängigen Wert als Voraussetzung. Regeln ohne Voraussetzungen entsprechen Axiomen. Zudem drückt die Relation τ1 < τ2 aus, dass τ1
der gleiche oder ein speziellerer Typ als τ2 ist. Bspw. gilt N+ → N+ < a → a, aber
a × b ≮ a × a.
3.3.1 Ausdrücke einfacher Datentypen
Entsprechend den in Unterabschnitt 3.2.1 definierten einfachen Datentypen werden in
diesem Abschnitt die zugehörigen einfachen Ausdrücke beschrieben.
Funktionen Die Anwendung einer Funktion f : τ1 → τ2 auf einen Ausdruck e : τ1
wird notiert als f e und hat den Typ τ2 . Funktionsanwendung ist linksassoziativ,
d.h. ( f e1 ) e2 = f e1 e2 . Mittels λ-Ausdrücken lassen sich neue Funktionen
erzeugen. Ein λ-Ausdruck der Form λ x . e erzeugt einen Ausdruck des Typs
τ1 → τ2 , wobei x : τ1 , e : τ2 und x als lokale Variable in e vorkommen darf. Eine
Funktionsanwendung eines λ-Ausdrucks auf einen Wert (λ x . e1 ) e2 führt dazu,
dass jedes Vorkommen von x in e1 durch den Ausdruck e2 ersetzt wird.
Werte des leeren Typs Da keine Werte des leeren Typs existieren, gibt es in Tempus
auch keinen entsprechenden Wert „Null“. Wie im Einführungsbeispiel beschrieben, existiert jedoch die Primitive never, die ein niemals eintretendes Ereignis
darstellt. never hat den Typ event 0, um deutlich zu machen, dass dieses Ereignis
auch tatsächlich niemals eintreten kann, denn andernfalls müsste ein Wert vom
Typ 0 als Ereigniswert auftreten. Mit Hilfe der Funktion ? könnten Werte vom
Typ 0 in Werte beliebigen Typs konvertiert werden. Zu beachten ist, dass diese
Funktion nur für die Inferenz eines passenden Typs nötig ist. Da Werte vom Typ 0
nie auftreten, können mittels ? auch keine Werte beliebigen Typs erzeugt werden.
Einheitswert Der einzige Wert des Einheitstyps 1 wird mit hi bezeichnet. Er trägt keine
zusätzliche Information und kann wie im Einführungsbeispiel dazu verwendet
werden, das Eintreten eines Ereignisses ohne Ereigniswert kenntlich zu machen.
Paare Die Werte des Produkttyps werden Paare oder 2-Tupel über Ausdrücken e1 : τ1 ,
e2 : τ2 genannt und als (e1 , e2 ) notiert. Um die Teilausdrücke eines Paares zu
ermitteln, werden die Funktionen first und second – genannt Projektionen –
verwendet. Dabei gilt
first (e1 , e2 ) = e1 und
second (e1 , e2 ) = e2 .
Alternativen Eine Alternative zwischen zwei Ausdrücken e1 : τ1 und e2 : τ2 kann mit
Hilfe des Summentyps beschrieben werden. Die Werte dieses Typs können über
die Injektionen left für die linke Alternative und right für die rechte Alternative
24
3.3 Ausdrücke
erzeugt werden. Für einen Ausdruck e : τ1 + τ2 lässt sich mittels der Funktion
case eine Fallunterscheidung, abhängig von der Alternative von e, durchführen:



 f 1 e1 , falls e = left e1
case f 1 f 2 e = 
.

 f 2 e2 , falls e = right e2
Die Funktionen f 1 und f 2 müssen dabei die Typen τ1 → τ bzw. τ2 → τ haben,
insbesondere also den selben Zieltyp τ.
Zahlen Alle Zahlen n ∈ N \ {0} können in Tempus als Zahlenliteral vom Typ N+
notiert werden. Für arithmetische Berechnungen stehen die in Abschnitt 5.1
beschriebenen Funktionen add und mult zur Verfügung, die allerdings nicht Teil
der Tempus-Spezifikation sind. Zudem beschreibt Unterabschnitt 3.3.3, wie sich
Funktionen induktiv über positiven Zahlen definieren lassen.
3.3.2 Verhalten und Ereignisse
Die in diesem Abschnitt beschriebenen Primitiven und Operatoren dienen im Wesentlichen der Konstruktion und Transformation von Ereignissen und Verhalten. Zur
Beschreibung der Bedeutung von Verhalten und Ereignissen verwendet wir die folgenden Notationen: Behavior f ist ein Verhalten, das in t Zeitschritten den Wert f t liefert;
Event t e ist ein Ereignis, das in t Zeitschritten mit dem Wert e feuert.
Verhalten
Ein konstantes Verhalten lässt sich durch den Operator const erzeugen. Der Ausdruck
const e stellt ein Verhalten dar, das zu jedem Zeitpunkt den gleichen Ausdruck e als
Wert liefert. Wie in Abschnitt 2.2 bereits angesprochen, muss e einen zeitunabhängigen
Ausdruck darstellen. Daher dürfen in e keine lokalen Variablen enthalten sein, was in der
Typregel Const durch die Voraussetzung ∅ ` e : τ deutlich wird. Der Typ behavior τ für
const e lässt sich also nur herleiten, wenn sich ohne Verwendung von lokalen Variablen
der Typ τ für den Ausdruck e bestimmen lässt.
Nicht konstante Verhalten lassen sich mit dem Operator behavior erzeugen. Ein Ausdruck behavior f mit einer Funktion f : N+ → τ beschreibt dabei ein Verhalten vom
Typ behavior τ. Das Argument für die Funktion f ist eine positive Zahl n, die einen
Zeitpunkt beschreibt, zu dem der zugehörtige Wert auftreten soll.4 Die Zeit wird dabei
relativ zum Startzeitpunkt angegeben, wobei der Startzeitpunkt nicht enthalten ist. Ein
Verhalten behavior (λ x . x) liefert bspw. ab dem nächsten Zeitschritt seines Erschaffens
den Zeitschritt selbst als Werte, d.h. 1, 2, 3, . . . Analog zu konstanten Verhalten gilt
4
Für die Darstellung der Zeit ist nicht zwingend ein diskretes Modell erforderlich, es würde jedes Modell
mit einer totalen Ordnung aller Zeitpunkte genügen. Zur einfacheren Beschreibung der Ausdrücke und
der Umsetzung im Interpreter wurden jedoch die positiven Zahlen als Zeitmodell gewählt.
25
3 Die Sprache
für f die Beschränkung, dass der Ausdruck der f beschreibt zeitunabhängig sein muss.
Der Operator behavior ist nicht Teil der Tempus-Spezifikation, sondern wurde dem
Interpreter lediglich hinzugefügt, um zu Simulationszwecken Verhalten erzeugen zu
können.
Durch die in Abschnitt 2.2 beschriebenen Restriktionen zur Absicherung der Startzeitkonsistenz begründet sich unter anderem der Operator für die Transformation von
Verhalten. Dieser wird angewendet auf zwei Verhalten, also e = bf be , wobei bf ein Verhalten vom Typ behavior (τ1 → τ2 ) ist und be ein Verhalten vom Typ behavior τ1 . Das
resultierende Verhalten entsteht durch punktweise Anwendung der von bf gelieferten
Funktionen auf die zugehörigen Werte von be zu den entsprechenden Zeitpunkten:
Behavior f τ1 →τ2 Behavior f τ1 = Behavior (λ t . f τ1 →τ2 t ( f τ1 t))
Mit Hilfe der Funktion expand lassen sich aus einem Verhalten die zu späteren Zeitpunkten entstehenden Restverhalten konstruieren. Der Ausdruck expand b liefert für
ein Verhalten b zu jedem Zeitpunkt t ein Paar (v, bt ). Für jeden Zeitpunkt t entspricht v
dabei dem Wert von b bei t. Weiterhin ist bt das zu t gehörige Restverhalten, das zum
Zeitpunkt t startet und die gleichen Werte wie b ab diesem Zeitpunkt liefert. Semantisch
entspricht expand b damit
expand (Behavior f ) = Behavior (λ t . ( f t, Behavior (λ t0 . f (t + t0 )))) .
ultraswitch stellt eine allgemeine switch-Funktion5 dar und konstruiert aus einer Folge
von Verhaltenssegmenten ein einzelnes Gesamtverhalten br . Es erhält als Argument
einen Wert (b, e) vom Typ ν σ . behavior τ × event (τ × σ). Das Verhalten b entspricht
dem Anfangsverhalten von br . Zu einem Zeitpunkt t kann das zugehörige Ereignis e
wiederum einen Wert (v, (b0 , e0 )) liefern. Ab diesem Zeitpunkt t spielen die Werte von b
für die Konstruktion von br nun keine Rolle mehr. Stattdessen gilt ab t das Verhalten b0
für das Gesamtverhalten br . Da b0 zum Zeitpunkt t noch keinen Wert liefert, hat br zu
diesem Zeitpunkt den Wert v. Das neue Verhalten b0 gilt nun solange, bis e0 feuert und
wiederum zu einem neuen Verhalten wechselt.
Ereignisse
Für die Konstruktion von Ereignissen existiert der Operator event. Dabei entspricht
event t e einem Ereignis, das in t Zeitschritten nach seiner Erschaffung mit dem Wert e
feuert. Für den Wert t gelten dabei, außer dass es sich um einen Wert vom Typ N+ handeln
muss, keine weiteren Beschränkungen. Der Ausdruck e muss jedoch zeitunabhängig
sein. Wie auch behavior dient der Operator event nur zu Simulationszwecken und ist
nicht Teil der Tempus-Spezifikation.
5
Eine Funktion switch bzw. ein entsprechendes Konstrukt ist Bestandteil vieler FRP-Implementierungen.
Damit lassen sich Signale beschreiben, die im Laufe der Zeit zwischen verschiedenen Eingangssignalen
hin- und herwechseln können.
26
3.3 Ausdrücke
Analog zum Operator existiert für Ereignisse ein Operator , mit dessen Hilfe
Ereigniswerte transformiert werden können. Der Ausdruck bf e liefert ein neues
Ereignis e0 , das zum gleichen Zeitpunkt feuert wie e. Der Wert von e0 zum Zeitpunkt t
entsteht jedoch durch Anwendung der bei t vom Verhalten bf gelieferten Funktion auf
den durch e gelieferten Wert:
Behavior f Event t e = Event t ( f t e)
Wie bereits beschrieben, stellt die Primitive never des Typs event 0 ein nie eintretendes
Ereignis dar. Da durch dieses Ereignis somit auch nie ein Wert zurückgeliefert wird,
wurde für den Ereignistyp ein Parametertyp verwendet, der keinen Wert besitzt – der
leere Typ.
Die Funktion race, die der Linearität der Zeit in der Temporallogik entspricht, erhält als
Eingabe zwei Ereignisse e1 : event τ1 und e2 : event τ2 . Das Ergebnis der Anwendung
von race e1 e2 ist ein Ereignis e über einer Alternative von drei Möglichkeiten, deren
Wert davon abhängt, in welcher Reihenfolge die Ereignisse e1 und e2 feuern:



Event t1 (left (v1 , v2 )),
falls t1 = t2


race (Event t1 v1 ) 

= Event t1 (right (left (v1 , Event (t2 − t1 ) v2 ))),
falls t1 < t2

(Event t2 v2 ) 


Event t2 (right (right (v2 , Event (t1 − t2 ) v1 ))), falls t1 > t2
Die erste Alternative beschreibt das Feuern von e1 und e2 zum selben Zeitpunkt, der Wert
von e ist daher ein Paar aus den Werten der alten Ereignisse. Die zwei verbleibenden
Alternativen beschreiben jeweils das Auftreten eines Ereignisses vor dem anderen. Der
Wert von e ergibt sich daher aus dem Wert des zuerst auftretenden Ereignisses und dem
verkürzten, später feuernden Ereignis.
Die Funktion ultrajump erhält als Argument einen Wert vom Typ ν σ . event (τ + σ),
der eine Folge von Ereignissen darstellt. Diese Folge kann dabei entweder mit einem
Wert e : τ abschließen oder auch unendlich lange Folgeereignisse liefern. ultrajump
verschmilzt all diese Ereignisse zu einem einzigen, das den Wert e als Ergebnis hat. Im
Extremfall kann auch ein nie feuerndes Ereignis entstehen.
3.3.3 Ausdrücke rekursiver Datentypen
Für die Konstruktion und Dekonstruktion von µ- bzw. ν-Typen gibt es die Funktionen
pack [%] und unpack [%] die beide einen rekursiven Datentyp % als Parameter erwarten.6 Diese zusätzliche Typangabe soll im Folgenden Typannotation genannt werden. Die
Beschreibung der beiden Funktionen wird nun anhand von µ-Typen beschrieben; völlig
analog können jedoch auch ν-Typen an deren Stelle verwendet werden. pack [µ α . τ]
erhält einen Wert vom Typ τ, in dem alle Vorkommen von α durch einen Wert vom Typ
6
Die explizite Angabe des Typs % ist notwendig, um die Typprüfung in Anwesenheit von rekursiven
Datentypen zu vereinfachen bzw. zu ermöglichen, siehe dazu auch Kapitel 6.
27
3 Die Sprache
µ α . τ ersetzt wurden. Dieser Wert wird dann in einen Wert vom Typ µ α . τ „verpackt“.
Entsprechend lässt sich diese Transformation mit der Funktion unpack [µ α . τ] umkehren, indem aus einem Wert vom rekursiven Typ µ α . τ der innere Wert vom Typ τ
„ausgepackt“ wird, in dem wiederum alle Vorkommen von α durch einen Wert vom Typ
µ α . τ ersetzt wurden.
Für die Transformation von Werten der rekursiven Datentypen existieren die Faltungsoperatoren fold für µ-Typen und unfold für ν-Typen. fold [µ α . τ1 ] erhält als ersten
Parameter eine Faltungsfunktion f des Typs τ1 [τ2 /α] → τ2 . Zu beachten ist, dass f
ein zeitunabhängiger Wert sein muss, in dem also keine lokalen Variablen vorkommen
dürfen. Der zweite Parameter ist ein Ausdruck e des Typs µ α . τ1 . Damit entspricht
fold [ µ α . τ1 ] f e einer Faltung von e mittels f in einen Wert e0 : τ2 . Die Funktion f
erwartet ihrerseits einen Wert des Typs τ1 [τ2 /α], also einen Wert vom Typ τ1 in dem
alle Vorkommen der Typvariable α durch den Zieltyp τ2 ersetzt wurden. Für die Faltung
von e muss f nun rekursiv auf alle Teilausdrücke angewendet werden, die in e an den
Stellen vorkommen, die der Variablen α im zugehörigen Typ τ1 entsprechen. Anhand
der Struktur des Typs τ1 können diese „Löcher“ ermittelt werden. Der so entstandene
Ausdruck hat nun den Typ τ1 [τ2 /α]. Auf ihn kann wiederum die Funktion f angewendet
werden, um letztlich den Ergebnisausdruck e0 zu erhalten.
Der Operator unfold arbeitet ähnlich wie fold, allerdings für ν-Typen und – wie der
Name bereits andeutet – in umgekehrer Richtung. unfold [ν α . τ1 ] erhält als ersten
Parameter eine Entfaltungsfunktion f : τ2 → τ1 [τ2 /α], die ebenfalls zeitunabhängig
sein muss, sowie als zweiten Parameter einen Wert e vom Typ τ2 . Die Funktion f wird
nun auf e angewendet, um einen Ausdruck des Typs τ1 [τ2 /α] zu erhalten. In diesem
Ausdruck müssen wiederum Teilausdrücke an den Stellen, an denen α in τ1 vorkommt,
rekursiv mittels f bearbeitet werden. Das Ergebnis dieser Entfaltung ist ein Ausdruck
des Typs ν α . τ1 .
Die Anwendung einer Operation fold [ %] f e auf den µ-Wert e stellt eine Induktion über
diesem Wert dar. Dabei ist das Resultat von f für alle Konstruktoren des zugehörigen
Typs % definiert, d.h. für die Werte erzeugenden Funktionen. Der Wert e wird in jedem
Rekursionsschritt abgebaut und die entsprechende Alternative von f für die Berechnung
des Ergebniswertes verwendet. Im Gegensatz dazu beschreibt unfold [%0 ] f 0 e0 eine
Koinduktion über e0 . Die Funktion f 0 ist dabei für jedes Ergebnis von f 0 definiert über
die Destruktoren von %0 , also die Werte abbauenden Funktionen. Dies entspricht einem
Aufbau einer (unendlichen) Datenstruktur [14].
Um die Definition einer Funktion mittels Induktion über positiven Zahlen zu erreichen,
existiert in Tempus die Funktion reflect. Damit lassen sich positive Zahlen in einen Ausdruck vom Typ µ n . 1 + n transformieren, über diesem dann mittels des fold-Operators
induziert werden kann. Dabei ist reflect wie folgt definiert:
reflect 1 = pack [µ n . 1 + n] (left hi)
reflect n = pack [µ n . 1 + n] (right (reflect (n − 1)))
28
.
3.3 Ausdrücke
Eine linke Alternative entspricht also der Zahl 1, eine rechte Alternative einer Zahl
n > 1, wobei der Wert der rechten Alternative wiederum die Zahl n − 1 repräsentiert.
Die Funktionsweise von fold und unfold soll nun noch einmal anhand zweier Beispiele
verdeutlicht werden. Die Funktionen add und mult für die Addition und Multiplikation
von positiven Zahlen sollen dabei als gegeben vorausgesetzt werden. Ebenso nehmen
wir die Funktion even als gegeben an, die für eine positive Zahl n genau dann den
Wahrheitswert true zurückgibt, wenn n eine gerade Zahl ist.
Beispiel für die Verwendung von unfold
Als einfaches Beispiel für die Verwendung des Operators unfold soll die Konstruktion
eines unendlichen Baumes dienen. Wir definieren mit Hilfe eines ν-Typs zunächst einen
Binärbaumtyp BinTree, der parametrisiert über dem Elementtyp α ist. Jeder Knoten
besteht aus einem Tupel vom Typ τ × α × τ, wobei τ den beiden Unterbäumen entspricht.
Damit ergibt sich:
type BinTree α = ν τ . τ × α × τ
.
Des weiteren konstruieren wir nun einen solchen Binärbaum natsTree über positiven
Zahlen. Dabei ist die Wurzel des Baums mit 1 markiert und alle Zahlen n > 1 sind
im Baum so angeordnet, dass das Ergebnis einer Breitentraversierung über dem Baum
gerade alle positiven Zahlen in aufsteigender Reihenfolge liefert. Anders als beim
fold-Operator erhält unfold den Typ des Faltungsergebnisses als Typannotation. Da ein
Binärbaum über positiven Zahlen konstruiert werden soll, verwenden wir BinTree N+ .
Zudem ist der zu entfaltende Ausdruck der Wert der Wurzelmarkierung, also 1 : N+ .
Aus der Typregel Unfold folgt unmittelbar, dass der Typ unserer Faltungsfunktion
N+ → N+ × N+ × N+ sein muss. Dies entspricht einer Funktion n 7→ (l(n), elem(n), r(n)).
Dabei ist n der aktuelle zu entfaltende Wert, elem(n) der Wert der Knotenmarkierung im
aktuellen Schritt, sowie l(n) und r(n) die zu entfaltenden Ausdrücke für die Unteräume
im nächsten Schritt. Wir erreichen die gewünschte Anordnung der Zahlen im Baum,
indem wir für die jeweilige Knotenmarkierung gerade den Entfaltungswert n selbst
wählen, d.h. elem(n) = n, sowie die Entfaltungswerte für den nächsten Schritt definieren
als l(n) = 2n und r(n) = 2n + 1. Der Baum natsTree kann nun einfach über den Operator
unfold mit der eben beschriebenen Entfaltungsfunktion und dem Startwert 1 definiert
werden:
– – natsTree : BinTree N+
value natsTree = unfold [BinTree N+ ]
(λ n . (mult 2 n, n, add 1 (mult 2 n))) 1 .
Beispiel für die Verwendung von fold
Für ein Beispiel für die Verwendung von fold greifen wir den in Unterabschnitt 3.2.4
beschriebenen Datentyp List für die Darstellung von Listen auf, der wie folgt definiert
29
3 Die Sprache
wurde:
type List α = µ % . 1 + α × %
.
Wir wollen eine Funktion mapEven definieren, die eine Liste vom Typ List N+ in eine
Liste vom Typ List Bool transformiert. Die Funktion soll dabei die Listenstruktur beibehalten und jedes Element mittels n 7→ even(n) abbilden. Zunächst soll eine Beispielliste
list konstruiert werden. list soll eine Liste vom Typ List N+ sein und die Elemente 2, 3
und 5 in dieser Reihenfolge beinhalten. Für die Definition von list werden die Elemente
jeweils mit der zugehörigen Restliste gepaart, als rechte Alternative des Listentyps
markiert und mit pack selbst in eine Liste konvertiert. Das Listenende wird durch left hi
gekennzeichnet:
– – list : List N+
value list = pack [List N+ ] (right (2,
pack [List N+ ] (right (3,
pack [List N+ ] (right (5,
pack [List N+ ] (left hi))))))) .
Für die Konstruktion von mapEven verwenden wir den fold-Operator, da es sich sowohl
bei der zu faltenden Liste als auch bei der Ergebnisliste um µ-Werte, also um endliche
Datenstrukturen handelt. Als Typannotation für fold verwenden wir den zu faltenden Typ
List N+ . Da der Ergebnistyp mit List Bool ebenfalls feststeht, ergibt sich aus Typregel
Fold der Typ 1 + N+ × List Bool → List Bool für die Faltungsfunktion. Diese muss also
über eine Fallunterscheidung konstruiert werden und bei leerer Eingabeliste, wie auch
bei einem Paar aus einem Element samt rekursiv bearbeiteter Restliste die passende
Ergebnisliste konstruieren. Bei der linken Alternative, also der leeren Restliste, wird
pack [List N+ ] (left hi) zurückgegeben, d.h. selbst wieder die leere Liste. Ist die Liste
nicht leer, so transformieren wir das aktuelle Element mit even, paaren es mit der rekursiv
bearbeitenden Restliste und geben dieses Paar als rechte Alternative der Ergebnisliste
zurück. Unabhängig davon welche Alternative aufgetreten ist, muss die entstandene
Alternative nun mit Hilfe von pack [List Bool] in den passenden Ergebnistyp konvertiert
werden. Damit ergibt sich für mapEven die folgende Definition:
– – mapEven : List N+ → List Bool
value mapEven = fold [List N+ ] (λ l . pack [List Bool]
(case (λ _ . left hi)
(λ p . right (even (first p), second p)) l)) .
Die Auswertung von mapEven list geht dabei wie folgt vor sich: Die Datenstruktur für
list wird um das erste Element, d.h. den Teil pack [List N+ ] (right (2, l)) abgebaut und
das aktuelle Listenelement n = 2 transformiert zu n0 = even 2 = true. Zudem wird die
Restliste l analog durch Rekursion zur Restergebnisliste l0 transformiert. Der Basisfall
pack [List N+ ] (left hi) wird dabei durch pack [List Bool] (left hi) ersetzt. Schließlich
wird die Ergebnisliste aus n0 und l0 durch pack [List Bool] (right (n0 , l0 )) aufgebaut.
30
3.3 Ausdrücke
Termination
Wie in Unterabschnitt 3.2.3 bereits angedeutet, verbietet Tempus die Definition von
beliebigen rekursiv definierten Datentypen und Funktionen. Sollen Funktionen rekursiv
definiert werden, müssen dazu explizit die Funktionen fold und unfold samt zugehörigen µ- bzw. ν-Typen verwendet werden. Der Grund hierfür liegt in der Sicherung
von Termination durch die Sprache. Die Werte von µ-Typen entsprechen endlichen
Datenstrukturen. Durch eine Konvertierung eines solchen Wertes mittels fold wird
in jedem Rekursionsschritt ein Teil der Datenstruktur abgebaut und rekursiv auf den
verbleibenden Teilstrukturen weitergearbeitet. Da unendlich große Typbeschreibungen
nicht möglich sind, ist auch die Anzahl dieser Teilstrukturen endlich. Eine Berechnung
mit fold auf einem µ-Wert muss somit nach endlich vielen Schritten terminieren.
Etwas anders sieht es bei Werten von ν-Typen aus, da diese durch unfold erzeugten
Strukturen potentiell unendlich groß werden können. Dennoch kann die teilweise Auswertung solcher Werte immer auf eine endliche Anzahl von Schritten begrenzt werden.
Jeder ν-Wert e kann an endlich vielen Stellen weitere Teilausdrücke haben, die wiederum
dem Typ von e entsprechen. Jeder dieser Teilausdrücke wurde dabei durch den Operator
pack in den zugehörigen ν-Typ konvertiert. Entsprechend findet sich bei der Auswertung
jedes ν-Wertes nach endlich vielen Schritten eine „Grenze“ von pack-Operatoren, an
denen die Auswertung ggf. abgebrochen werden könnte. Es ist somit nicht möglich,
dass die Auswertung eines Teilausdrucks von e in einer Endlosrekursion endet, ohne
jemals auf einen dieser pack-Operatoren zu stoßen.
31
3 Die Sprache
τ < Γ(x)
Var
Γ`x:τ
Γ \ {x} ∪ {x : τ1 } ` e : τ2
Lam
Γ ` λ x . e : τ1 → τ2
Γ ` f : τ1 → τ2
Γ ` e : τ1
App
Γ ` f e : τ2
Γ ` e1 : behavior (τ1 → τ2 )
Γ ` e2 : behavior τ1
LiftAppBeh
Γ ` e1 e2 : behavior τ2
Γ ` e1 : behavior (τ1 → τ2 )
Γ ` e2 : event τ1
LiftAppEv
Γ ` e1 e2 : event τ2
∅`e:τ
Const
Γ ` const e : behavior τ
Γ ` t : N+
∅`e:τ
Ev
Γ ` event t e : event τ
Γ`?:0→τ
Null
Γ ` hi : 1
Γ ` first : (τ1 , τ2 ) → τ1
Γ ` left : τ1 → τ1 + τ2
First
Left
∅ ` f : N+ → τ
Beh
Γ ` behavior f : behavior τ
Never
Γ ` never : event 0
Unit
Γ ` x : τ1
Γ ` y : τ2
Pair
Γ ` (x, y) : τ1 × τ2
Γ ` second : (τ1 , τ2 ) → τ2
Right
Γ ` right : τ2 → τ1 + τ2
Γ ` case : (τ1 → σ) → (τ2 → σ) → τ1 + τ2 → σ
Second
Case
Γ ` expand : behavior τ → behavior (τ × behavior τ)
Expand
Γ ` race : event τ1 → event τ2 → event (τ1 × τ2 + τ1 × event τ2 + τ2 × event τ1 )
∀ n ∈ N \ {0} : Γ ` n : N+
Positive
∅ ` f : τ1 [τ2 /α] → τ2
Fold
Γ ` fold [ µ α . τ1 ] f : µ α . τ1 → τ2
Γ ` reflect : N+ → µ n . 1 + n
Race
Reflect
∅ ` f : τ2 → τ1 [τ2 /α]
Unfold
Γ ` unfold [ν α . τ1 ] f : τ2 → ν α . τ1
Packµ/ν
h
i
h
. i
Γ ` pack µν α . τ : τ µν α . τ α → µν α . τ
h
. i Unpackµ/ν
i
h
Γ ` unpack µν α . τ : µν α . τ → τ µν α . τ α
Γ ` ultraswitch : (ν α . behavior τ × event (τ × α)) → behavior τ
Γ ` ultrajump : (ν σ . event (τ + σ)) → event τ
UltraSwitch
UltraJump
Abbildung 3.3: Typisierungsregeln der Sprache Tempus
32
4 Semantische Analyse
Nachdem im vorherigen Kapitel der syntaktische Aufbau und die Bedeutung von Tempus-Typen und -Ausdrücken ausführlich beschrieben wurde, soll in diesem Kaptiel
auf die Algorithmen eingegangen werden, mit denen die Korrektheit von TempusProgrammen geprüft werden kann. In Abschnitt 4.1 werden zunächst grundlegende
Notationen festgelegt, die für die folgenden Abschnitte benötigt werden. Abschnitt 4.2
beschreibt den allgemeinen Algorithmus für die semantische Analyse, worauf der
Typinferenz-Algorithmus in Abschnitt 4.3 und die Varianzprüfung in Abschnitt 4.4
folgen.
4.1 Notation
Wir nennen 0, 1 und N+ nullstellige, behavior und event einstellige, sowie ×, + und →
zweistellige Typkonstruktoren. Allgemein bezeichne ar(T) die Stelligkeit eines Typkonstruktors T. Sei • ein zweistelliger Typkonstruktor, dann verwenden wir die Präfixschreibweise (•) τ1 τ2 synonym zur gewohnten Infixschreibweise τ1 • τ2 . Zudem gelten
für die Anwendung von Typen auf Typkonstruktoren die gleichen Assoziativitäten wie
für Funktionen, also T τ1 . . . τn = (T τ1 . . . τn−1 ) τn .
Eine Substitution S = {x1 7→ τ1 , . . . , xn 7→ τn } wird definiert als Abbildung von
Typvariablen xi auf Typen τi mit i ∈ {1, . . . , n} und


τ, falls x 7→ τ ∈ S
def 
Sx = 

 x, sonst .
Als leere Substitution ι wird die Substitution bezeichnet, die jede Typvariable durch sich
selbst ersetzt und sie entspricht der Identitätsfunktion. Als Anwendung einer Substitution
S auf einen Typ τ wird Sτ rekursiv über der Struktur von τ definiert:


falls τ = x und x eine Typvariable ist
Sx,
def 
Sτ = 

T (Sτ1 ) . . . (Sτn ), falls τ = T τ1 . . . τn .
Dabei bezeichnet T den Typkonstruktor von τ, τi sind die Unterterme. Mit S2 ◦ S1 wird
die Verkettung zweier Substitutionen S1 und S2 bezeichnet. Die Anwendung zweier
verketteter Substitutionen auf einen Typ entspricht der Anwendung beider Substitutionen
def
hintereinander: (S2 ◦ S1 )τ = S2 (S1 τ). Zudem führen wir die Notation τ1 [τ2 /α] ein, die
eine Ersetzung aller Vorkommen der Variable α im Typ τ1 durch den Typ τ2 bezeichnet.
33
4 Semantische Analyse
Für einen Typ τ bezeichnet ftv(τ) die Menge der freien Typvariablen in τ und ist definiert
durch
ftv(x)
= {x}
ftv(T τ1 . . . τn ) = ftv(τ1 ) ∪ . . . ∪ ftv(τn )
ftv µν α . τ
= ftv(τ) \ {α} .
Bei Typen unterscheiden wir zudem zwischen monomorphen Typen und polymorphen
Typschemata. Ein Typschema ∀ α . τ ist ein polymorpher Typ τ, in dem Typvariablen
α = α1 , . . . , αn universell quantifiziert vorkommen können [11]. Diese Unterscheidung
ist hilfreich, da während der Typinferenz gleiche Typvariablen in Typen von Teilausdrücken auch durch den gleichen Typ repräsentiert werden sollen. Diese Typvariablen
entsprechen dabei Typen, die im weiteren Verlauf der Typinferenz noch spezialisiert
werden können. Dagegen stellen allquantifizierte Typvariablen in Typschemata echt polymorphe Typen dar. Wir definieren als Instanziierung eines Typschemas die Ersetzung
aller quantifizierten Variablen durch neue mittels
def
instantiate(∀ α1 . . . αn . τ) = {α1 7→ β1 , . . . , αn 7→ βn }τ mit β∗1 , . . . , β∗n ,
wobei {αi 7→ βi } mit i ∈ {1, . . . , n} eine Substitution ist und die Notation β∗ für die
Erzeugung einer neuen, zuvor nicht verwendeten Typvariablen β verwendet wird. Analog
ist die Generalisierung
def
generalize(τ) = ∀ α . τ mit α = ftv(τ)
die Verallgemeinerung eines Typs τ zu einem Typschema. Weiterhin wird als Typumgebung Γ = {xi 7→ σi } eine Abbildung von Typvariablen auf Typschemata und dazu der
Wertebereich dom(Γ) = {x | x 7→ σ ∈ Γ} definiert.
4.2 Allgemeiner Algorithmus
Das Ergebnis der syntaktischen Analyse ist ein Tempus-Programm in Form einer Liste
von Werte- und Typdefinitionen. Diese Definitionen sind Grundlage für die anschließende semantische Analyse. Diese umfasst im Wesentlichen die Inferenz des allgemeinsten
Typs für jede Variablendefinition, sowie eine zusätzliche Varianzprüfung für Typdefinitionen und die inferierten Typen. Abbildung 4.1 zeigt den allgemeinen Algorithmus für
die semantische Analyse eines Tempus-Moduls.
Ziel des Algorithmus ist die Bestimmung der Menge Σ, die eine Zuordnung von Typsynonymen zu deren konkreteren Typen beinhaltet, sowie der Menge Γ, die eine Zuordnung
der Bezeichner von Wertedefinitionen auf ihren jeweiligen Typ enthält. Beide Mengen
sind beim Start des Algorithmus leer. Die Liste von n Typsynonym- und Wertedefinitionen decli , mit i ∈ {1 . . . n} wird nun in der Reihenfolge ihrer Definition im Quellcode
durchlaufen. Da Tempus keine syntaktische Unterscheidung zwischen Typvariablen und
Typsynonymbezeichnern innerhalb von Typausdrücken erlaubt, werden beide bei der
34
4.2 Allgemeiner Algorithmus
semantischen Analyse weitestgehend gleich behandelt. Für jedes Typsynonym wird
zunächst eine Varianzprüfung mit dem in Unterabschnitt 4.4.1 vorgestellten Algorithmus V ausgeführt, wobei diese für den Typ τ erfolgt, in dem zuvor alle Typsynonyme
durch ihren konkreteren Typ ersetzt wurden. Diese Ersetzung wird durch die Funktion
expandType vorgenommen:
expandType(Σ, t τ1 . . . τn ) = expandType({x1 7→ τ1 , . . . , xn 7→ τn }Σ(t))
mit t ∈ Σ ∧ ar(t) = n
expandType(Σ, T τ1 . . . τn ) = T (expandType(Σ, τ1 )) . . . (expandType(Σ, τn ))
mit T ∈ {0, 1, N+ , behavior, event, ×, +, →}
expandType(Σ, µν α . τ)
= µν α . expandType(Σ \ {α}, τ)
expandType(_, _)
= Abbruch mit Fehler .
Jedes Typsynonym wird anschließend in der Menge Σ gespeichert, sofern die folgenden
Bedingungen erfüllt sind, andernsfalls bricht der Algorithmus mit einem Fehler ab:
• Nachdem ein Typsynonym definiert wurde, darf kein weiterer Typ mit demselben
Namen definiert werden. Dies wird durch die Bedingung t < Σ gesichert.
• Typsynonyme und Typvariablen müssen vor ihrer Verwendung definiert worden
sein. Bei der Verwendung von Typvariablen müssen diese entweder durch µ- oder
ν-Typen lokal gebunden sein, oder, bei Verwendung innerhalb der rechten Seite
einer Typdefinition, als Typparameter auf der linken Seite auftauchen. Insbesondere ist damit auch die Verwendung der zu definierenden Variable selbst innerhalb
ihrer Definition verboten, um Typen mit impliziter Rekursion auszuschließen. Der
Algorithmus sichert dies durch die Bedingung ftv(τ) ⊆ {x1 , . . . , xn } ∪ Σ.
• Für alle Typsynonyme muss bei deren Verwendung die Anzahl der übergebenen
Typparameter mit der Anzahl der Typparameter in ihrer Definition übereinstimmen. Partielle Anwendung ist bei Typsynonymen im Gegensatz zu Funktionen also nicht gestattet. Diese Bedingung wird bereits beim Anwenden von expandType
gesichert.
• Die Varianzprüfung muss erfolgreich gewesen sein.
Bei Wertedefinitionen wird zunächst geprüft, ob die zu definierende Variable v bereits
in der Menge Γ enthalten ist. Ist dies der Fall, wird mit einem Fehler abgebrochen, da
auch bei Werten keine doppelen Variablendefinitionen erlaubt sind. Andernfalls wird
für den Ausdruck e ein allgemeinster Typ τ bestimmt. Der für die Typinferenz verwendete Algorithmus W wird in Abschnitt 4.3 detailliert beschrieben. Anschließend wird
auch für τ eine Varianzprüfung durchgeführt. Schlägt diese fehl, bricht der SemantikAlgorithmus mit einem Fehler ab. Sofern bei der Analyse für die Wertedefinition keine
Fehler aufgetreten sind, wird Γ um die Zuordnung des expandierten und generalisierten
Typs τ zu v erweitert. Die Sicherung der Bedingung, dass innerhalb von Ausdrücken nur
zuvor global oder lokal eingeführte Variablen verwendet werden dürfen, wird während
der Typinferenz gesichert, die im folgenden Abschnitt beschrieben wird.
35
4 Semantische Analyse
Σ=Γ=∅
for i = 1 to n do
if decli ist Typdefinition der Form t x1 . . . xn = τ then
varOk ← V(∅, expandType(Σ, τ))
if t < Σ ∧ ftv(τ) ⊆ {x1 , . . . , xn } ∪ Σ ∧ varOk then
Σ = Σ ∪ {t x1 . . . xn 7→ τ}
else
Abbruch mit Fehler
else if decli ist Wertedefinition der Form v = e then
if v < Γ then
(τ, _) ← W(Γ, ∅, e)
varOk ← V(∅, expandType(Σ, τ))
if ¬varOk then
Abbruch mit Fehler
Γ ← Γ ∪ {v 7→ generalize(expandType(Σ, τ))}
else
Abbruch mit Fehler
Abbildung 4.1: Algorithmus zur semantischen Analyse eines Tempus-Moduls. Σ ist
die Menge der Typsynonyme, Γ die Menge der inferierten Typen der
definierten Werte.
36
4.3 Typinferenz
4.3 Typinferenz
Für die Typinferenz wird der in Abbildung 4.2 angegebene Algorithmus W verwendet,
der auf dem von Damas und Milner [4] beschriebenen Typzuweisungsalgorithmus W
basiert. W bestimmt für jeden Ausdruck einen allgemeinsten Typ. Der Algorithmus
arbeitet auf Grundlage der in Abbildung 3.3 aufgeführten Typisierungsregeln. Die
Herleitung eines allgemeinsten Typs erfolgt für einen Ausdruck rekursiv über seinen
Teilausdrücken. Bei rekursiven Typen in Typannotationen wird im dargestellten Algorithmus davon ausgegengen, dass ggf. verwendete Typsynonyme bereits durch ihre
spezielleren Typen ersetzt worden sind.
Der Algorithmus W erwartet als Parameter zwei Typumgebungen Γ und Λ, sowie
den Ausdruck e, für den der allgemeinste Typ zu bestimmen ist. Λ ist zu Beginn leer
und enthält bei der rekursiven Auswertung der Unterterme von e die lokal durch λAusdrücke eingeführten Variablen mit ihren zugehörigen Typen. Γ stellt die globale
Typumgebung dar, in der alle zuvor definierten globalen Variablen mit dem jeweiligen
allgemeinsten Typ enthalten sind. Das Ergebnis des Algorithmus ist ein Paar aus einer
Substitution S und dem allgemeinsten Typ τ für e. S enthält dabei die Ersetzungen aller
in τ aufgetretenen Typvariablen durch ihre konkreteren Typen. Die Substitution wird
nach der Typinferenz für e nicht mehr benötigt, ist aber für die Zwischenschritte von W
zur Herleitung des Typs eines Ausdrucks aus den Typen seiner Teilausdrücke notwendig.
Der einfachste Fall für die Typinferenz ist W(Γ, Λ, x), wobei x eine Variable ist. In
diesem Fall wird der der Variablen x zugeordnete Typ aus Γ ∪ Λ zusammen mit der
leeren Substitution ι zurückgegeben. Wurde x zuvor nicht lokal oder global definiert,
also x < Γ ∪ Λ, so liegt ein Fehler vor und der Algorithmus bricht ab. Für die Typinferenz eines λ-Ausdrucks λ x . e muss W für e aufgerufen werden. Da hier lokal
eine neue Variable x eingeführt wird, muss beim rekursiven Aufruf x aus der globalen
Typumgebung Γ entfernt und der lokalen Typumgebung Λ hinzugefügt werden, da
lokale Variablen globale überdecken. Dabei werden eventuell vorhandene Vorkommen
von x in Λ durch x 7→ β∗ ersetzt, d.h. durch eine neue Variable allgemeinsten Typs.
Die Herleitung des Typs eines Ausdrucks e1 e2 kann direkt auf einen äquivalenten
Ausdruck () e1 e2 zurückgeführt werden, da es sich bei dem Infixoperator nur um
eine spezielle Syntax für reguläre Funktionsanwendung handelt. Analog kann auch die
Inferenz von Ausdrücken der Form e1 e2 behandelt werden.
Für die Sicherung der in Abschnitt 2.2 beschriebenen Startzeitkonsistenz existiert eine
Hand voll Typregeln, die voraussetzen, dass ein gewisser Typ für einen zeitunabhängigen
Ausdruck herleitbar ist. Die Typregel Const bspw. hat als Voraussetzung ∅ ` e : τ, d.h.
für den Ausdruck e muss ohne Verwendung von lokalen Variablen der Typ τ herleitbar
sein. Ist dies der Fall, kann der Typ für const e mit behavior τ abgeleitet werden. Für
die Typinferenz eines Ausdrucks const e muss also sichergestellt sein, dass bei der
Typherleitung von e lokale Variablen nicht betrachtet werden. Dies geschieht einfach
indem beim rekursiven Aufruf von W für die Berechnung des Typs von e die Menge der
lokalen Variablen Λ = ∅ gesetzt wird. Der Ergebnistyp von const e ergibt sich somit
37
4 Semantische Analyse
direkt aus dem Typ von e. Ist e vom Typ τ dann ist const e vom Typ behavior τ. Die
Substitution ist in diesem Fall ι.
Bei Ausdrücken, die einen Operator mit Typannotationen verwenden, gibt es zudem
eine weitere Einschränkung: Der hier beschriebene Typinferenzalgorithmus ist nicht
in der Lage, den Typ solcher Ausdrücke abzuleiten, bei denen freie Typvariablen im
rekursiven Datentyp vorkommen. Der Typ eines Ausdrucks fold [ µ α . β + α] e wäre
bspw. nicht ableitbar, da β im Typ der Annotation als freie Variable vorkommt.1
Unter anderem für die Typherleitung einer Funktionsanwendung e1 e2 ist eine Unifizierung zweier Typen τ1 und τ2 notwendig. Dabei werden die Termrepräsentationen
beider Typen durch Ersetzung der darin enthaltenen Typvariablen durch konkretere
Typen soweit aneinander angeglichen, bis beide Terme gleich sind. Ist eine solche Ersetzung nicht möglich, bricht die Unifizierung mit einem Fehler ab. Der verwendete
Unifizierungsalgoritmus mgu ist in Abbildung 4.3 dargestellt und basiert auf dem Standardunifizierungsalgorithmus von Robinson [24], für den Heeren in [11] eine rekursiv
definierte Variante angibt. Ein Aufruf von mgu(τ1 , τ2 ) liefert eine Substitution S für die
Typvariablen in τ1 und τ2 , die nötig ist, um diese beiden Typen anzugleichen.
Bei der Unifizierung mgu(τ1 , τ2 ) treten im Wesentlichen vier Fälle auf:
1. Bei beiden Typen τ1 und τ2 handelt es sich um dieselbe Typvariable oder denselben Typkonstruktor. Dann ist keine Ersetzung nötig und die leere Substitution ι
wird zurückgegeben.
2. Der Typ τ1 ist eine Typvariable x. In diesem Fall wird eine Substitution zurückgegeben, die die Ersetzung x 7→ τ2 enthält, sofern x nicht als freie Variable in τ2
vorkommt.2 Analog wird verfahren, falls τ2 eine Typvariable ist.
3. Beide Typen τ1 und τ2 stellen eine Anwendung von Typen %1 bzw. %2 auf Typargumente σ1 bzw. σ2 dar. Dann werden die Typen %1 und %2 rekursiv unifiziert
und die entstehende Substitution S auf σ1 und σ2 angewendet, bevor diese selbst
mit dem Ergebnis S0 unifiziert werden. Die Ergebnissubstitution entsteht dann aus
der Verknüpfung S0 ◦ S.
4. Es handelt sich bei den beiden Typen um die gleiche Art von rekursiven Datentypen. In dem Fall werden die gebundenen Typvariablen α1 bzw. α2 in beiden Typen
durch einen neuen, nullstelligen Typkonstruktor Θ ersetzt und die Unifizierung auf
den Untertypen der rekursiven Typen fortgesetzt. Dieses Verfahren stellt sicher,
dass die gebundenen Tyvariablen in beiden Typen nicht beim rekursiven Aufruf
von mgu durch konkretere Typen substituiert werden.
In allen sonstigen Fällen schlägt die Unifizierung fehl, was zu einem Abbruch des
Unifizierungsalgorithmus führt.
1
2
Auf dieses Problem wird in Kapitel 6 noch einmal ausführlicher eingegangen.
Die letztgenannte Bedingung ist erforderlich, da durch x = τ2 (x) sonst ein unendlicher Typ definiert
werden würde; das Prüfen dieser Bedingung ist als Occurs-Check bekannt.
38
4.4 Varianzprüfung und Funktor-Instanzen
Wie bereits erwähnt, stellt Algorithmus W eine Implementierung der in Abbildung 3.3
dargestellten Typregeln dar, die wiederum auf den von Damas und Milner angegebenen
Typregeln basieren. Für diese wird in [4] bewiesen, dass für einen Ausdruck unter einer
Typumgebung immer der allgemeinste Typ inferiert wird. Dies beruht im Wesentlichen
auf der Eigenschaft des Unifizierungsalgorithmus mgu, immer den allgemeinsten Unifikator für zwei Terme zu finden, wofür Robinson in [24] einen Beweis angibt. Zudem
wird bei der Typinferenz für Variablen immer der allgemeinste Typ als Ausgangstyp
gewählt und dieser nach und nach spezialisiert. Die Eigenschaft der Inferenz des allgemeinsten Typs wird durch die in dieser Arbeit vorgestellten Erweiterung der Typregeln
nicht zerstört. Ein überwiegender Teil dieser Erweiterungen umfasst Regeln ohne Voraussetzungen. Diese besitzen entsprechend ihrer Bedeutung bereits den allgemeinsten
Typ und könnten auch durch Erweiterung der Typumgebung um zusätzliche globale
Variablen ausgedrückt werden. Entsprechend wurde auch die Typinferenz gemäß der
Regeln LiftAppBeh und LiftAppEv durch Umformung auf eine Funktionsanwendung
zurückgeführt. Es bleiben letztlich noch die Typregeln, die leere Kontexte in ihren
Voraussetzungen haben. Auch hierbei wird immer der allgemeinste Typ ermittelt, denn
die Typinferenz für die Teilausdrücke, auf die sich der leere Kontext bezieht, erfolgt
nach dem gleichen Prinzip wie für die übrigen Teilausdrücke, lediglich mit einer leeren
lokalen Typumgebung.
4.4 Varianzprüfung und Funktor-Instanzen
Wie bereits in Unterabschnitt 3.2.3 erwähnt wurde, muss für alle Typdefinitionen und die
inferierten Typen der Wertedefinitionen eine zusätzliche Prüfung der rekursiven Typen
vorgenommen werden. Für jeden Typ der Form µν α . τ muss sichergestellt sein, dass α
kovariant in τ auftritt. Für die Sicherstellung dieser Eigenschaft wurden bereits die in
Abbildung 3.2 angegebenen Varianzregeln vorgestellt. Diese Varianzprüfung ist notwendig, um festzustellen, ob für einen rekursiven Datentyp eine Funktor-Instanz ableitbar
ist. Typen ohne eine solche Instanz dürfen nicht verwendet werden. In den folgenden
Unterabschnitten werden der Algorithmus zur Varianzprüfung und die Herleitung eines
allgemeinen comap und contramap erklärt.
4.4.1 Varianzprüfung
Ist τ ein Typsynoynm oder der bei der Typinferenz ermittelte Typ für einen Ausdruck e,
so wird mit dem in Abbildung 4.4 angegebenen Algorithmus V geprüft, ob τ bezüglich
der Varianz der Variablen korrekt ist. Dies geschieht durch Rekursion über der Struktur
von τ, wobei vorausgesetzt wird, dass in τ bereits alle Typsynonyme ersetzt wurden. Der
Algorithmus V erhält als Eingabe zwei Parameter, eine Varianzenumgebung ∆ und den
Typ τ selbst. Die Umgebung ∆ enthält die Information, ob eine durch µ- oder ν-Typen
gebundene Variable α in den jeweiligen Teiltypen ko- oder kontravariant auftreten muss.
Zu Beginn der Varianzprüfung ist ∆ = ∅. Liefert V(∅, τ) = True, so ist der Typ τ
39
4 Semantische Analyse
W(Γ, Λ, x)
W(Γ, Λ,
W(Γ, Λ,
W(Γ, Λ,
W(Γ, Λ,
W(Γ, Λ,
W(Γ, Λ,
W(Γ, Λ,
W(Γ, Λ,
W(Γ, Λ,
W(Γ, Λ,
W(Γ, Λ,
W(Γ, Λ,
W(Γ, Λ,
= if x ∈ dom(∆) then
(ι, instantiate(∆(x)))
mit ∆ = Γ ∪ Λ
else
Abbruch mit Fehler
λ x . e)
= let (S, τ) = W(Γ \ {x}, Λ \ {x} ∪ {x : β}, e)
in (S, Sβ → τ)
mit β∗
e1 e2 )
= let (S1 , τ1 ) = W(Γ, Λ, e1 )
(S2 , τ2 ) = W(Γ, S1 Λ, e2 )
S3
= mgu(S2 τ1 , τ2 → β)
mit β∗
in (S3 ◦ S2 ◦ S1 , S3 β)
(e1 , e2 ))
= let (S1 , τ1 ) = W(Γ, Λ, e1 )
(S2 , τ2 ) = W(Γ, S1 Λ, e2 )
in (S2 ◦ S1 , S2 τ1 × τ2 )
e1 e2 )
= W(Γ ∪ {() 7→ behavior (γ → δ)
→ behavior γ → behavior δ},
Λ, () e1 e2 )
mit γ∗ , δ∗
e1 e2 )
= W(Γ ∪ {() 7→ behavior (γ → δ)
→ event γ → event δ},
Λ, () e1 e2 )
mit γ∗ , δ∗
const e)
= let (_, τ) = W(Γ, ∅, e)
in (ι, behavior τ)
behavior f )
= let (_, τ) = W(Γ, ∅, f )
S
= mgu( N+ → α, τ)
mit α∗
in (S, S(behavior α))
event t e)
= let (S1 , τ1 ) = W(Γ, Λ, t)
S2
= mgu( N+ , τ1 )
(_, τ2 ) = W(Γ, ∅, e)
in (S2 ◦ S1 , event τ2 )
fold [µ α . τ1 ] f ) = let (_, τ2 ) = W(Γ, ∅, f )
S
= mgu(τ1 β/α → β, τ2 )
mit β∗
in (S, S(µ α . τ1 → β))
unfold [ν α . τ1 ] f ) = let (_, τ2 ) = W(Γ, ∅, f )
S
= mgu( β → τ1 β/α , τ2 ) mit β∗
in
h
i
(S, hS( β →.ν iα . τ1 ))
µ
pack ν α . τ )
= ∅, τ µν α . τ α → µν α . τ
h
i
h
. i
unpack µν α . τ ) = ∅, µν α . τ → τ µν α . τ α
(
Typ der Primitive e entsprechend der
e)
= let τ =
zugehörigen Typregel
in (∅, τ)
Abbildung 4.2: Algorithmus W zur Typinferenz. Die Typen der hier nicht aufgeführten, „einfachen“ Ausdrücke lassen sich direkt durch die Typen der
zugehörigen Typregeln aus Abbildung 3.3 angeben.
40
4.4 Varianzprüfung und Funktor-Instanzen
=ι
=ι
= {x 7→ τ}
mit x < ftv(τ)
= {x 7→ τ}
mit x < ftv(τ)
= let S = mgu(τ1 , τ2 )
in mgu(Sσ1 , Sσ2 ) ◦ S
mgu( µ α1 . τ1 , µ α2 . τ2 ) = mgu(τ1 [Θ/α1 ] , τ2 [Θ/α2 ]) mit Θ∗
mgu(ν α1 . τ1 , ν α2 . τ2 ) = mgu(τ1 [Θ/α1 ] , τ2 [Θ/α2 ]) mit Θ∗
mgu(_, _)
= Abbruch mit Fehler
mgu(x, x)
mgu(T, T)
mgu(x, τ)
mgu(τ, x)
mgu(τ1 σ1 , τ2 σ2 )
Abbildung 4.3: Unifizierungsalgorithmus mgu. Typvariablen werden durch x dargestellt; T entspricht einem n-stelligen Typkonstruktor.
V(∆,
V(∆,
V(∆,
V(∆,
V(∆,
V(∆,
x)
=x∈∆
T)
= True
T τ)
= V(∆, τ)
τ1 • τ2 ) = V(∆, τ1 ) ∧ V(∆, τ2 )
τ1 → τ2 ) = V(∆, τ1 ) ∧ V(∆, τ2 )
→
µ α . τ) = V(∆ \ {α} ∪ { α},
τ)
ν
→
T ∈ {0, 1, N+ }
T ∈ {behavior, event}
• ∈ {×, +}
Abbildung 4.4: Algorithmus V zur Varianzprüfung
bezüglich der Varianzen korrekt, andernfalls liegt ein Typfehler vor. Der Algorithmus
arbeitet auf Grundlage der Varianzregeln aus Abbildung 3.2. Bei Typkonstruktoren
ist die Varianz korrekt, wenn sie für alle Teiltypen korrekt ist. Eine Ausnahme stellt
ein Funktionstyp τ1 → τ2 dar. Hierbei müssen die Varianzen im Teiltyp τ2 mit ∆
korrekt sein, im Teiltyp τ1 werden jedoch alle Varianzen der in ∆ vorhandenen Variablen
umgekehrt, so dass τ1 bzgl. ∆ korrekt sein muss.
4.4.2 Allgemeines co- und contramap
Um rekursive Typen für die Operatoren fold bzw. unfold verwenden zu können, muss für
die entsprechenden Typen eine Funktorinstanz ableitbar sein. Der Grund dafür ist, dass
für die Auswertung von Ausdrücken mit den Faltungsoperatoren die Funktionen comap
bzw. contramap notwendig sind. Wie in Unterabschnitt 3.3.3 beschrieben, werden die
Faltungsoperatoren rekursiv auf Teilausdrücke des µ- oder ν-Wertes angewendet. Die
Anwendung erfolgt dabei an genau den Stellen im Ausdruck, die der Position der
gebundenen Typvariablen α im zugehörigen Typ entsprechen. Der Typ des Ausdrucks
kann als eine Art „Schablone“ verstanden werden, in der alle Vorkommen von α markiert
sind. Da die Struktur eines Ausdrucks der seines zugehörigen Typs entspricht, lässt sich
diese Schablone auf den Ausdruck übertragen. Nach dem Übertragen dieser Schablone
entsprechen die markierten Stellen genau den Teilausdrücken, an denen der rekursive
Aufruf stattfinden muss.
41
4 Semantische Analyse
Was eben bildhaft durch das Übertragen einer Schablone beschrieben wurde, entspricht
tatsächlich der Anwendung einer passenden comap-Funktion für den zugehörigen Typ.
Wenn die Typvariable α für einen rekursiven Typ in diesem nur an kovarianter Position
vorkommt, kann ein solches comap generisch für diesen Typ abgeleitet werden. Die
Varianzprüfung sichert also, dass diese Ableitung überhaupt möglich ist. Die Herleitung
eines solchen comap erfolgt rekursiv über der Struktur des entsprechenden Datentyps.
Da Typvariablen in beliebigen Typen auch an kontravarianter Stelle vorkommen können, muss neben comap auch eine Funktion contramap für Kontrafunktoren abgeleitet
werden.3
Die Definition dieser beiden Funktionen auf Grundlage einer allgemeinen genmapFunktion zeigt Abbildung 4.5. Dabei bezeichnet genmapα [τ] die genmap-Funktion
bezüglich der Typvariablen α für den Typ τ, wobei α zusätzlich die Varianz angeheftet
ist. Entsprechend sind comap und contramap als Spezialfälle von genmap definiert. Für
die Typen 0, 1 und N+ entspricht genmap der Identitätsfuntion, da α in diesen Typen gar
nicht vorkommt. Bei Produkten wird genmap rekursiv für beide Teilausdrücke hergeleitet, bei Koprodukten entsprechend für die vorhandene Alternative. Eine Besonderheit
stellt der Funktionstyp τ1 → τ2 dar. Im Fall comap tritt α in τ1 in kontravarianter
Position auf, während es in τ2 in kovarianter Position steht. Umgekehrt steht es bei
contramap in τ1 in kovarianter und in τ2 in kontravarianter Position. Bei Funktionen
muss also für die rekursive Bearbeitung von τ1 die Varianz von α umgekehrt werden,
während für τ2 die aktuelle Varianz beibehalten wird.
Etwas komplizierter ist die Funktionsweise von genmap bei rekursiven Datentypen.
Wird durch den rekursiven Datentyp die gleiche Typvariable α gebunden, bezüglich
der das genmap abgeleitet wird, so entspricht genmap der Identitätsfunktion, da sich
kein Vorkommen von α in diesem Typ auf das ursprüngliche α bezieht. Wird eine
andere Typvariable β durch einen Typ µν β . τ gebunden, so muss die anzuwendene Funktion f zunächst in allen Untertermen der Rekursionsstellen in τ bzgl.h β angewendet
i werden. Diese Anwendung geschieht durch genmapβ [τ] genmapα µν β . τ f . Die
Anwendung genmapβ [τ] g e sorgt dafür, dass g in e an allen Rekursionsstellen bzgl.
β aufgerufen
h wird.
i Die Funktion g ist dabei selbst wieder ein rekursiver Aufruf von
genmapα µν β . τ f . Die zu α gehörenden Terme sind nach diesem rekursiven Aufruf also in allen Untertermen an den Rekursionsstellen bzgl. β mittels der Funktion f
transformiert worden. Nun muss f noch auf die übrigen Terme der Vorkommen von
α in τ angewendet werden. Dies geschieht durch genmapα [τ0 ] f , wobei τ0 dem Typ
τ entspricht, in dem aber bereits die Rekursionsstellen bzgl. β transformiert wurden.
Der neue Typ τ0 ergibt sich somit aus τ durch Ersetzung aller Vorkommen von β durch
µ β . τ, wobei dort in τ bereits alle Vorkommen von α durch α0 , den Zieltyp der Transν
h
. i
formationsfunktion f , ersetzt wurden. Insgesamt ergibt sich τ0 = τ µν β . τ [α0 /α] β .
3
Neben der Unterscheidung kovariantes oder kontravariantes map wird bei rekursiven Datentypen außerdem unterschieden, für welche Art von rekursiven Datentypen die Herleitung erfolgt. Für µ-Typen wird
dabei die Bezeichnung induktives map und für ν-Typen die Bezeichnung koinduktives map verwendet.
Da die Konstruktion beider Funktionen allerdings dieselbe ist, wird diese zusätzliche Unterscheidung
bei der Definition der map-Funktionen hier nicht getroffen.
42
4.4 Varianzprüfung und Funktor-Instanzen
comapα [τ]
= genmap →α [τ]
contramapα [τ] = genmap ←α [τ]
[0]
[1]
[N+ ]
[τ1 × τ2 ]
f
f
f
f
genmapα [τ1 + τ2 ]
f
[τ1 → τ2 ]
[event τ]
i τ]
h[behavior
µα.τ
i
hν
genmapα µν β . τ
f
f
f
f
β
f
genmapα
genmapα
genmapα
genmapα
genmapα
genmapα
genmapα
genmapα
genmapα
f
=⊥
= hi
=n
= genmapα [τ1 ] f e1 , genmapα [τ2 ] f e2


falls e = left e1
 left (genmapα [τ1 ] f e1 ),
e
= 
 right (genmap [τ ] f e ), falls e = right e
2
2
2
α
g
= genmapα [τ2 ] f ◦ g ◦ genmap α [τ1 ] f
(Event t e) = Event t (genmapα [τ] f e)
(Behavior g) = Behavior (λ t . genmapα [τ] f (g t))
e
=e
h
i
e
= pack µν β . τ [α0 /α]
h h
. ii
◦ genmapα τ µν β . τ [α0 /α] β f
h
i ◦ genmapβ [τ] genmapα µν β . τ f
h
i
◦ unpack µν β . τ e
wobei f : α → α0 und α , β


 f e falls α = β
e
= 
e
falls α , β
_
hi
n
(e1 , e2 )
Abbildung 4.5: Definition von comap und contramap auf Grundlage eines allgemeinen
genmap
43
4 Semantische Analyse
44
5 Implementierung des Interpreters
Dieses Kapitel befasst sich mit der Implementierung des Tempus-Interpreters. Es wird
zunächst ein allgemeiner Überblick über die Architektur des Interpreters gegegeben. In
den anschließenden Abschnitten erfolgt eine genauere Erklärung der einzelnen Module,
unterteilt nach syntaktischer Analyse und semantischer Analyse des Eingabequelltextes,
sowie der Auswertung von Ausdrücken. Abschließend wird beschrieben, wie aus diesen
Modulen der Interpreter in Form einer Konsolenanwendung erstellt wurde und dessen
Kommandos erklärt.
5.1 Überblick über die Systemarchitektur
Der Tempus-Interpreter wurde in der funktionalen Programmiersprache Haskell implementiert und die zugehörigen Module wurden zu einem Cabal-Paket1 zusammengefasst.2
Durch dieses Paket wird ein ausführbares Programm tempus bereitgestellt, welches
dem Tempus-Interpreter entspricht.
Die Implementierung des Interpreters enthält zwei zusätzliche Funktionen add und mult
für die Addition und Multiplikation ganzer Zahlen:
add
: N+ → N+ → N+
~add x y = x + y
mult
: N+ → N+ → N+
~mult x y = x ∗ y
Abbildung 5.1 zeigt den Aufbau des Tempus-Interpreters mit den einzelnen Modulen.
Dabei sind die Module entsprechend den folgenden Abschnitten gruppiert. Der Interpreter läd beim Start eine Datei Prelude.tp, die das Tempus-Prelude enthält. Dieses
besteht aus einer Reihe von vordefinierten Typ- und Wertedefinitionen, die beim Start
des Tempus-Interpreters geladen werden und in anderen Modulen verwendet werden
können. Der vollständige Quellcode des Tempus-Preludes ist in Anhang A abgedruckt.
Nach dem Start des Interpreters kann jeweils ein Tempus-Modul aus einer Datei h filei
geladen werden.3 Das Modul Prelude.tp und die Eingabedatei h filei werden in der
1
Cabal ist ein Paketsystem, das das Erstellen von Haskell-Bibliotheken und -Programmen ermöglicht und
das Erzeugen von plattformunabhängigen Anwendungen erleichtern soll [3].
2
Das entsprechende Paket kann unter http://hackage.haskell.org/package/tempus heruntergeladen werden.
3
Um in h filei Unicode-Zeichen verwenden zu können, muss die Zeichenkodierung der Datei UTF-8 sein.
45
5 Implementierung des Interpreters
Interpreter
Tempus.
Interpreter
Prelude
.tp
h filei
Tempus.
Main
Syntaktische
Analyse
Semantische
Analyse
Tempus.
Lexer
Tempus.
TypeCheck
AST
Auswertung
Σ, Γ
Tempus.
Evaluation
Tempus.
Parser
Hilfsmodule
Tempus.
Syntax
Wert vom
Typ Value
+
Ausgabe
Tempus.
Loc
Abbildung 5.1: Übersicht über die Architektur und die einzelnen Module des TempusInterpreters
ersten Phase einer syntaktischen Analyse unterzogen, deren Ergebnis ein abstrakter
Syntaxbaum (engl. abstract syntax tree, AST) für jeden definierten Typ und Wert ist.
In der zweiten Phase erfolgt die Typinferenz für die definierten Werte sowie die Varianzprüfung. Das Ergebnis dieser Phase sind die Mengen der Typsynonyme Σ sowie der
Werte mit den inferierten Typen Γ. Diese Mengen werden dann in der dritten Phase im
Interpreter je nach Bedarf für die Auswertung verwendet. Der auszuwertende Ausdruck
wird dabei in einen Wert vom Typ Value konvertiert, der zur Darstellung von TempusAusdrücken bei der Auswertung dient und für die Ausgabe auf der Konsole genutzt
wird.
46
5.2 Syntaktische Analyse
5.2 Syntaktische Analyse
Die für die syntaktische Analyse notwendigen Datentypen und Funktionen wurden auf
die Module Tempus.Lexer und Tempus.Parser aufgeteilt. Zudem existiert das Hilfsmodul
Tempus.Loc sowie das Modul Tempus.Syntax, in dem die Datentypen für den AST
definiert sind.
5.2.1 Datentypen des abstrakten Syntaxbaums
Die Datentypen für den abstrakten Sytaxbaum sind in Abbildung 5.2 dargestellt und
umfassen:
• Den Typ Program, der ein Tempus-Modul in Form einer Liste von Typ- oder
Wertedefinitionen darstellt,
• den Typ Decl für Definitionen, wobei der Konstruktor DeclType für eine Typdefinition und DeclVal für eine Wertedefinition steht,
• einen Typ Type für die Darstellung eines Tempus-Datentyps,
• MuType und NuType für einen µ- bzw. ν-Typ,
• den Typ Expr für die Darstellung eines Tempus-Ausdrucks,
• einen Typ Var für die Darstellung eines Variablenbezeichners sowie
• einen Typ ScrCode α für die Ausgabe von Tempus-Quellcode.
Die Datentypen spiegeln im Wesentlichen den syntaktischen Aufbau der jeweiligen
Tempus-Strukturen wider. Beim Datentyp Type sind zudem einige Besonderheiten zu
beachten: Durch µ- oder ν-Typen gebundene Typvariablen sowie die Anwendung von
Typsynonymen ohne Parameter werden im AST durch TyApp v [] dargestellt, wobei v
der entsprechende Variablenbezeichner ist. Dies ist notwendig, da beide während der
syntaktischen Analyse nicht ohne Weiteres unterscheidbar sind. Außerdem werden
die Datenkonstruktoren TyVar für Typvariablen und TyCon für Variablen rekursiver
Datentypen nicht während der syntaktischen Analyse verwendet, sondern erst während
der Typinferenz.
Für die Ausgabe von Quellcode im Interpreter wurde außerdem der Datentyp SrcCode α
mit zugehörigen Instanzen der Klasse Show eingeführt, die in Haskell für die Umwandlung von Werten verschiedener Datentypen in Werte des Typs String genutzt wird. Für
die zuvor genannten Datentypen existieren vom Compiler abgeleitete Show-Instanzen,
die zu Debug-Zwecken genutzt werden, sowie die manuell erstellten Show-Instanzen
für die jeweils in den Typ SrcCode eingeschlossenen Typen. Die Instanzen wurden so
definiert, dass für einen Ausdruck e die Anwendung der zum Typ gehörenden parseFunktion auf das Ergebnis von show (SrcCode e) wieder der Ausdruck e selbst entsteht.
47
5 Implementierung des Interpreters
type Program = [Decl]
data Decl
= DeclType SrcLoc Var [Var] Type
| DeclVal SrcLoc Var Expr
data Type
= TyMu MuType | TyNu NuType
| TyFun Type Type | TyPlus Type Type
| TyPair Type Type | TyApp Var [Type]
| TyBehav Type
| TyEvent Type
| TyNat
| TyZero
| TyUnit
| TyVar Integer
| TyCon Integer
data MuType = MuType Var Type
data NuType = NuType Var Type
data Expr
= ExPair Expr Expr
| ExLiftAppB Expr Expr
| ExApp Expr Expr
| ExVar Var
| ExBehav Expr
| ExNull
| ExLeft
| ExCase
| ExFst
| ExNever
| ExReflect
| ExUnfold Type Expr
| ExUnpack Type
| ExUSwitch
| ExLam Var Expr
| ExLiftAppE Expr Expr
| ExConst Expr
| ExNatLit Integer
| ExEvent Expr Expr
| ExUnit
| ExRight
| ExExpand
| ExSnd
| ExRace
| ExFold Type Expr
| ExPack Type
| ExUJump
newtype Var = Var String
newtype SrcCode a = SrcCode a
Abbildung 5.2: Definitionen der Datentypen für den abstrakten Syntaxbaum im Modul
Tempus.Syntax
48
5.2 Syntaktische Analyse
5.2.2 Lexer
Das Modul Tempus.Loc definiert Hilfstypen und -funktionen für die Kennzeichnung von
Quelltextpositionen. Der Typ SrcLoc beschreibt eine Position (row, col) durch den Wert
der Zeile row und der Spalte col im Quelltext. Mit Hilfe der Funktion showSrcLoc wird
eine solche Position in eine Zeichenkette umgewandelt, um bei Ausgaben im Interpreter
eine einheitliche Formatierung zu erreichen. Zudem wird der parametrisierte Datentyp
Loc α definiert, durch den einem Typ α eine Positionsinformation vom Typ SrcLoc
angeheftet werden kann. Mit der Funktion unLoc kann diese Positionsangabe entfernt
werden.
type SrcLoc = (Int, Int)
data Loc α = Loc SrcLoc α
showSrcLoc :: SrcLoc → String
unLoc
:: Loc α → α
Im Modul Tempus.Lexer sind Datenstrukturen und Funktionen definiert, mit deren Hilfe
eine Zeichenkette, bspw. die Eingabezeichenkette eines Tempus-Moduls, in eine Folge
von Token zerlegt werden kann. Abbildung 5.3 zeigt die entsprechenden Datentypen.
Der Typ Token definiert Datenkonstruktoren für die einzelnen Morpheme des TempusQuelltextes. Zu diesen gehören Variablennamen, Zahlenliterale, Operatorsymbole für
Typen und Ausdrücke, sowie die einzelnen Schlüsselwörter. Ein zusätzliches Token ist
EOF, welches das Ende des Eingabe- bzw. Tokenstroms markiert.
Für die Beschreibung des Lexers wurde eine Zustandsmonade Lexer eingeführt, die
einen Wert vom Typ LexerState als Zustand speichert. Ein solcher Zustand besteht aus
dem restlichen Eingabestring input und der aktuellen Quelltextposition loc, wobei loc
die Position des ersten Zeichens von input im ursprünglichen Eingabestring beschreibt.
Da Lexer eine Monade ist, lassen sich über die Monadenfunktionen return und (=)
aus primitiven Lexern komplexere kombinieren. Erstere umfassen dabei bspw. Lexer für
das Lesen von einzelnen Zeichen oder über einem Prädikat definierten Zeichenketten.
Durch solche primitiven Lexer wurde der Lexer lexInput erstellt, der ein einzelnes Token
gefolgt von Whitespace einliest und den Eingabestring kürzt bzw. die Quelltextposition
entsprechend verändert. Wurde ein Token erfolgreich gelesen, so ist das Ergebnis
Just tok mit dem Token tok, andernfalls Nothing.
5.2.3 Parser
Für den Parser wurde der Parser-Generator Happy [19] verwendet. Ähnlich dem CWerkzeug yacc erhält Happy eine angereicherte BNF-Grammatik als Eingabe und
erzeugt daraus ein Haskell-Modul, das einen LALR-Parser für die definierte Grammatik enthält. Die Eingabedatei Tempus/Parser.y enthält die Grammatik, aus der
mit Happy das Modul Tempus.Parser generiert wird. Diese Grammatik entspricht der
in Abbildung 3.1 dargestellten Form, die um zusätzliche Typinformationen für die
49
5 Implementierung des Interpreters
data Token = Variable String | KWBehavior
| NatLit Integer | KWCase
| Equals
| KWConst
...
| EOF
data LexerState = LexerState {
input :: String,
loc :: SrcLoc
}
type Lexer α
= State LexerState α
lexInput :: Lexer (Maybe Token)
Abbildung 5.3: Datentypen des Lexers mit der Signatur der Funktion lexInput
einzelnen Metasymbole und die jeweiligen Rückgabewerte der verschiedenen Alternativen ergänzt wurde. Neben einigen Hilfsfunktionen definiert dieses Modul die vier
Funktionen parseProgram, parseDecl, parseType und parseExpr, welche Parsern für
die Metasymbole Prog, Decl, Type bzw. Expr der Grammatik entsprechen:
parseProgram :: String → ParseResult Program
parseDecl
:: String → ParseResult Decl
parseType
:: String → ParseResult Type
parseExpr
:: String → ParseResult Expr .
Happy erlaubt es, monadische Parser zu erzeugen, indem die zu verwendende Monade
in der Eingabedatei kenntlich gemacht wird. Parserfehler können so einfacher behandelt
und Quellcodepositionen für die Fehlerausgabe verwendet werden. Daher existiert neben
der Lexer-Monade eine Parser-Monade Parser. Um die für den Lexer verwendeten
Quelltextpositionen auch bei der Fehlerausgabe im Parser nutzen zu können, gestattet es
Happy außerdem, die im Parser zu verwendende Lexer-Funktion anzugeben. Zu diesem
Zweck existiert die Funktion lex. Für die Verwendung einer gesonderten Lexer-Funktion
muss außerdem explizit ein Token angegeben werden, das das Ende des Eingabestroms
markiert. Hierzu wurde das zuvor erwähnte Token EOF genutzt. Die verwendete ParserMonade sowie die für das Parsen notwendigen Funktionen werden im Rest dieses
Abschnitts beschrieben.
Für das Ergebnis des Parsens wurde der Typ ParseResult α genutzt. Im Fehlerfall wird
eine Fehlermeldung mit zugehöriger Quelltextposition zurückgegeben, an der der Fehler
im Eingabequelltext aufgetreten ist, ansonsten ein Wert vom Typ α. Ein Wert vom
Typ Parser α besteht aus einer Funktion runParser die als Eingaben die zu parsende
Zeichenkette und die aktuelle Quelltextposition erhält und als Ergebnis ein Wert vom Typ
ParseResult α liefert. Für Parser wurde eine entsprechende Monaden-Instanz definiert,
die in Abbildung 5.4 dargestellt ist.
50
5.2 Syntaktische Analyse
type ParseResult α = Either (Loc String) α
newtype Parser α = Parser {
runParser :: String → SrcLoc → ParseResult α
}
instance Monad Parser where
return
:: α → Parser α
return a = Parser $ λ _ _ → Right a
(=)
:: Parser α → (α → Parser β) → Parser β
pα = fpβ = Parser $ λ s loc → case runParser pα s loc of
Left e → Left e
Right a → runParser ( fpβ a) s loc
fail
fail s
:: String → Parser α
= Parser $ λ _ loc → Left $ Loc loc s
Abbildung 5.4: Implementierung der Parser-Monade Parser
Die Funktion return e erzeugt einen immer erfolgreichen Parser der keine Eingabezeichen liest, sondern nur den Wert e als Ergebnis zurückgibt. Durch pα = fpβ werden
zwei Parser pα und pβ zu einem neuen Parser p0 zusammengesetzt. Dieser neue Parser
entsteht, indem auf pα auf eine Eingabezeichenkette s und Quelltextposition loc angewendet wird. Ist das Parsen erfolgreich, entsteht ein Ergebniswert a : α, andernfalls
scheitert der gesamte Parser p0 mit der durch pα entstandenen Fehlermeldung. Beim
erfolgreichem Parsen wird zudem der Parser pβ = fpβ a ausgeführt und dessen Ergebnis
als Ergebnis von p0 zurückgegeben.
Die Funktion initParser :: Parser α → String → ParseResult α initialisiert einen Parser
parser auf einer Zeichenkette str und wird für die vier genannten parse-Funktionen
benötigt. Beginnend bei Position (1, 1), die dem ersten Zeichen von str entspricht,
wird sämtlicher Whitespace aus str entfernt bis das erste Token erreicht wurde. Auf
den dort beginnenden Reststring und die Position des ersten Zeichens dieses Tokens
wird im Anschluss der Parser parser mittels runParser angewendet und das Ergebnis
zurückgegeben.
Als Bindeglied zwischen Lexer und Parser kann die Funktion lex angesehen werden.
Happy erfordert es, diese Funktion im Continuation-Passing Style4 zu definieren. Daher
erhält lex auch als Parameter eine Funktion cont, die auf das nächste gelesene Token
angewendet wird. Tritt beim Lesen des Token ein Fehler auf, so wird mit einer Fehlermeldung abgebrochen. Andernfalls ergibt sich der entstehende Parser aus dem Lesen
eines Tokens tok und dem Parser, der bei Anwendung von cont auf tok mit der neuen
Eingabezeichenkette und Quelltextposition entsteht. Die Implementierung von lex zeigt
Abbildung 5.5.
4
Beim Continuation-Passing Style wird der Kontrollfluss einer Funktion durch sogenannte Continuations
gesteuert. Diese sind selbst wieder Funktionen, die der aufrufenden Funktion als Parameter mitgereicht
51
5 Implementierung des Interpreters
lex
:: (Loc Token → Parser α) → Parser α
lex cont = Parser $ λ s loc →
case runState lexInput $ LexerState s loc of
(Nothing, _)
→ Left $ Loc loc $ "lexer error at "
++ showSrcLoc loc
0
0
(Just t, LexerState s loc ) → runParser (cont (Loc loc t)) s0 loc0
Abbildung 5.5: Implementierung der Funktion lex
type T a = ErrorT ContextError (State TState) a
data ContextError = UndefinedVariable Var | DuplicateVariable Var
| DuplicateType Var
| OccursCheck Type Type
| SymbolClash Type Type | IncorrectVariances Type
| UndefinedType Var
| TypeArgsMismatch Var
| NoMuType Type
| NoNuType Type
| NoRecType Type
data TState = TState {
varID :: Integer,
conID :: Integer
}
Abbildung 5.6: Implementierung der Monade T für die semantische Analyse
5.3 Semantische Analyse
Für die semantische Analyse wurde wie bereits bei der syntaktischen Analyse das Konzept der Monaden genutzt. Dazu wurde eine Fehlermonade mit einer Zustandsmonade
zu einer Monade T α kombiniert, die es erlaubt eine monadische Aktion mit einem
Fehler abzubrechen und Hilfswerte für die Analyse zu speichern. Im Fehlerfall wird
ein Wert vom Typ ContextError zurückgeliefert, der Auskunft über den aufgetretenen
Fehler gibt. Die Fehlerfälle umfassen dabei bei der Typinferenz oder Varianzprüfung
aufgetretene Fehler, sowie allgemeine Fehler wie undefinierte oder doppelt definierte
Bezeichner. Der Zustand vom Typ TState enthält Integer-Zähler varID und conID. Diese
Werte werden für die Erzeugung von neuen Typvariablen benötigt. Die Verwendung
von Integer-Werten vereinfacht dabei das Erzeugen von neuen Variablen im Gegensatz zum Finden von noch nicht benutzen Bezeichnern. Dabei entspricht varID dem
aktuellen Zähler für allgemein verwendete Typvariablen und conID dem Zähler für die
bei der Unifizierung erzeugten, in rekursiven Datentypen gebundenen Typvariablen.
Abbildung 5.6 zeigt die Implementierung der genannten Typen.
Für die Darstellung von Typumgebungen wurde der Typ TypeEnv als Liste von Variablen
mit ihrem zugehörtigen Typschema eingeführt. Typschemata sind vom Typ TypeScheme
werden und von dieser für die verbleibenden Berechnungen aufgerufen werden.
52
5.3 Semantische Analyse
und bestehen aus einer Liste von Typvariablennummern und dem jeweiligen Typ. Zudem
existiert der Typ TypeSynEnv für die Typsynonymumgebung. Sie entspricht einer Liste,
wobei jedes Element aus dem Bezeichner des Typs, einer Liste von Variablennamen für
die Parameter des Typs und dem Typ der rechten Seite der entsprechenden Typdefinition
besteht.
Der in Abbildung 4.1 beschrieben Algorithmus für die semantische Analyse eines
Tempus-Moduls wurde durch die Funktion checkProgram implementiert. Die Funktion
erwartet als Eingabe eine Typ- und eine Typsynonymumgebung mit bereits definierten
globalen Variablen und Typsynonymen, sowie die Liste der zu prüfenden Wert- bzw.
Typdefinitionen decli . Für jede Definition wird geprüft, ob die oben beschriebenen
Restriktionen eingehalten werden. Für Wertedefinitionen wird eine Typinferenz ausgeführt und Typen zudem einer Varianzprüfung unterzogen. Anders als im Algorithmus
beschrieben, wird in checkProgram die Varianzprüfung für inferierte Typen nur dann
tatsächlich ausgeführt, wenn ein Ausdruck mit einer Typannotationen bearbeitet wird.
Tritt dabei ein Fehler auf, so bricht checkProgram die Analyse der verbleibenden Definitionen ab und gibt den aufgetretenen Fehler als Wert vom Typ ContexError zurück.
War die Analyse aller Definitionen erfolgreich, wird ein Paar aus einer Typ- und einer
Typsynonymumgebung zurückgegeben, wobei dieses nur die durch die Analyse der
Definitionen decli entstandenen Ergebnisse enthält.
Abbildung 5.7 zeigt die Typen und Typsignaturen der für die Typinferenz verwendeten
Funktionen. Die Funktion checkProgram greift für die Berechnung der Ergebnisse auf
die Monade T zurück. Der in Abbildung 4.2 angegebene Algorithmus W kann so relativ
einfach in Haskell übertragen werden. Die Funktion typeExpr implementiert den Algorithmus W. Eingaben sind die globalen und lokalen Typumgebungen Γ und Λ, sowie
die Typsynonymumgebung Σ und der Ausdruck e für den der allgemeinste Typ inferiert
werden soll. Für den Fall e = ExApp e1 e2 , also einer Funktionsanwendung e1 e2 , ist
beispielhaft die Implementierung angegeben. Die Hilfsfunktion freshVar verwendet den
angesprochenen Monaden-Zustand TState um noch nicht verwendete Typvariablen zu
erzeugen. Zudem behandelt typeExpr nur die in Algorithmus W angegebenen Fälle
für e. Der Typ einfacher, nativer Funktionen wird über die Funktion typeSimple ermittelt,
für die die einzelnen Typen fest implementiert sind.
Weiterhin wurde der Algorithmus für die Unifizierung durch die Funktion mgu umgesetzt, die ebenfalls die Monade T verwendet und so auch eine einfachere Fehlerbehandlung erlaubt. Bei dieser Funktion unterscheidet sich die Implementierung jedoch
teilweise von dem in Abbildung 4.3 angegebenen Algorithmus. Der Grund hierfür liegt
in der Darstellung von Typausdrücken des Typs Type, denn jeder Typkonstruktor muss
auf all seine Typargumente angewendet werden, um einen Wert vom Typ Type zu erhalten. Eine gemeinsame Behandlung des Falls mgu(T, T) für alle Typkonstruktoren T
ist somit nicht ohne Weiteres möglich. Stattdessen werden alle Typargumente jeweils
paarweise durchlaufen und unifiziert. Die dabei entstehenden Substitutionen werden
zu einer Ergebnissubstitution verknüpft, wobei die jeweils aktuelle Substitution als
Akkumulatorargument der Funktion mgu übergeben wird. Daher erhält mgu neben der
53
5 Implementierung des Interpreters
type TypeScheme = ([Integer], Type)
type TypeEnv
= [(Var, TypeScheme)]
type TypeSynEnv = [(Var, ([Var], Type))]
checkProgram :: TypeEnv → TypeSynEnv → Program
→ Either (Loc ContextError) (TypeEnv, TypeSynEnv)
typeExpr :: TypeEnv → TypeEnv → TypeSynEnv → Expr → T (Subst, Type)
typeExpr Γ Λ Σ e = case e of
...
ExApp e1 e2 → do
β
← freshVar
(s1 , τ1 ) ← typeExpr Γ Λ Σ e1
(s2 , τ2 ) ← typeExpr Γ (substEnv s1 Λ) Σ e2
s3
← mgu Σ (substType s2 τ1 ) (TyFun τ2 β) []
return (compSubst s3 (compSubst s2 s1 ), substType s3 β)
...
freshVar :: T Type
typeSimple :: Expr → T Type
mgu
:: TypeSynEnv → Type → Type → Subst → T Subst
substEnv :: Subst → TypeEnv → TypeEnv
compSubst :: Subst → Subst → Subst
substType :: Subst → Type → Type
Abbildung 5.7: Typen und Signaturen der für die Typinferenz verwendeten Funktionen. Die Funktion typeExpr enthält zudem einen Auszug aus der
Implementierung.
Liste der Typsynonyme und den zu unifizierenden Typen eine Substitution als Argument,
die zu Beginn leer ist.
Die Funktion correctVariances setzt den in Abbildung 4.4 angegebenen Algorithmus V
um. Sie liefert für einen Typ unter einer bestimmten Typsynonymumgebung genau dann
einen Wahrheitswert True, wenn die Varianzprüfung erfolgreich war. Auch in dieser
Funktion wurde die T-Monade verwendet. Der im Algorithmus angegebene Kontext ∆
wurde als eine Liste von Paaren (x, var) umgesetzt, wobei x eine Typvariable und var
die zugehörige Varianz als Wert vom Typ Variance ist:
data Variance = CoVariant | ContraVariant
correctVariances :: TypeSynEnv → Type → T Bool
.
Viele der im Modul Tempus.TypeCheck verwendeten Funktionen arbeiten rekursiv über
der Struktur eines Ausdrucks. Einige Hilfsfunktionen behandeln dabei nur wenige Fälle
gesondert, für die restlichen wird die entsprechende Funktion nur rekursiv auf alle Teilausdrücke des Ausdrucks angewendet. Für solche Funktionen wurde das Haskell-Paket
uniplate genutzt, das dabei helfen soll, redundanten Quellcode (sogenannten „boilerplate
54
5.4 Auswertung
code“) zu beseitigen. Ein solches Beispiel ist die Hilfsfunktion occursIn, die für eine
Typvariable x (repräsentiert durch einen Integer-Wert) und einen Typ τ entscheidet,
ob x als Variable in τ vorkommt. Der einzige interessante Fall ist hierbei τ = TyVar x,
für alle anderen Fälle würde die Berechnung nur rekursiv auf die Teilstrukturen von τ
verschoben werden. Hat ein Haskell-Wert e den Typ einer Instanz der Klasse Uniplate,
so kann mit der Funktion universe eine Liste von Teilausdrücken von e generiert werden,
die alle denselben Typ wie e haben. Eine Uniplate-Instanz ist für einen Typ ableitbar,
wenn Instanzen der Klassen Data und Typeable für diesen Typ existieren. Letztere können vom Compiler allerdings automatisch generiert werden. Somit lässt sich occursIn
einfach durch eine List-Comprehension formulieren:
universe
:: Uniplate on ⇒ on → [on]
occursIn
:: Integer → Type → Bool
occursIn x τ = x ∈ [ y | TyVar y ← universe τ]
.
5.4 Auswertung
Die für die Auswertung von Tempus-Ausdrücken notwendigen Datentypen und Funktionen befinden sich im Modul Tempus.Evaluation. Der Ergebnistyp eines ausgewerteten
Ausdrucks ist Value:
data Value = Natural Integer
| Unit
| Function (Value → Value)
| Pair Value Value
| ChoiceLeft Value
| ChoiceRight Value
| Behavior (RelTime → Value)
| Event RelTime Value .
Debei existieren Konstruktoren für die Werte einfacher Datentypen sowie für Verhalten
und Ereignisse. Funktionen werden durch Haskell-Funktionen des Typs Value → Value
repräsentiert. Verhalten und Ereignisse werden analog zur Konstruktion in Tempus
durch eine Funktion RelTime → Value für Verhalten bzw. durch ein Paar von RelTime
und Value für Ereignisse dargestellt, wobei RelTime der Zeittyp ist. Anders als bei
Tempus-Ausdrücken werden Werte rekursiver Datentypen nicht durch ihre gepackte
Form, sondern durch den inneren Wert selbst dargestellt. Ein Ausdruck pack [ µ α . 1 +
N+ × α] (left hi) wird bspw. durch den Wert ChoiceLeft Unit repräsentiert.
Zeitdarstellung
Für die Darstellung von Zeiten existiert der Datentyp RelTime. Sämtliche Zeitwerte
stellen dabei relative Zeiten dar, also die Zahl der Zeitschritte vom aktuellen Zeitpunkt
55
5 Implementierung des Interpreters
in die Zukunft. Der kleinste Wert ist daher One und entspricht einem Zeitschritt. Succ n
entspricht der Zeit n + 1. Für die Darstellung der Primitive never wird eine Zeitspanne
von unendlich vielen Zeitschritten benötigt, die durch die in Abbildung 5.8 abgebildete
Funktion infinite erzeugt werden kann. Um das Vereinigen einer unendlichen Folge von
Ereignissen mittels ultrajump zu ermöglichen, wird ein Zeitmodell benötigt, das eine
verzögerte Addition von Zeitwerten zulässt. Die Addition für Integer-Werte wäre für
diesen Zweck bspw. zu strikt. Es wurde daher die Darstellung in Form von RelTime
gewählt, die die Implementierung einer entsprechend verzögerten Addition erlaubt.
Eine Besonderheit gibt es dabei für die Instanz der Klasse Ord5 des Typs RelTime. Für
die Implementierung der Auswertung eines Ausdrucks race (Event t1 e1 ) (Event t2 e2 )
muss entschieden werden, welches der beiden Ereignisse zuerst feuert. Dies lässt sich
über min t1 t2 ermitteln, wobei min als Klassenfunktion von Ord automatisch vom Compiler generiert werden kann. Da Werte des Typs RelTime auch unendlich groß werden
können, kann die in Abbildung 5.8 abgebildete Standardimplementierung der Funktion
min auf Basis der Funktion (6) allerdings nicht verwendet werden. Das Problem soll
durch das folgende Beispiel verdeutlicht werden:
Gegeben seien die Ausdrücke e1 = event 3 hi, e2 = never und e3 = never mit den
zugehörigen Ereigniszeiten t1 = Succ (Succ One) und t2 = t3 = infinite. Ein Ausdruck
race e1 e2 könnte erfolgreich ausgewertet werden, da die Berechnung min t1 t2 mit
Succ (Succ One) terminiert. Ist jedoch ein drittes Ereignis beteiligt, kann bei der Standardimplementierung von min eine nicht terminierende Berechnung auftreten. So muss
für race e1 (race e2 e3 ) die Ergebnisereigniszeit min t1 (min t2 t3 ) ermittelt werden.
Für die Berechnung von min t2 t3 mit infinite für beide Argumente ergibt sich durch
min t2 t3 = if t2 6 t3 then t2 else t3
(Definition von min)
= if infinite 6 infinite then t2 else t3
(Ersetzen der Argumente)
= if Succ infinite 6 Succ infinite then t2 else t3 (Definition von infinite)
= if infinite 6 infinite then t2 else t3
(3. Fall der Definition von (6))
...
eine Endlosrekursion und die Berechung der Ergebniszeit schlägt fehl. Eine Lösung
des Problems bietet die Definition der Minimum-Funktion durch min? . Hierbei wird
Haskells verzögerte Auswertung ausgenutzt: Die Berechung von t = min? t2 t3 ist
zwar noch immer nicht terminierend, in jedem Auswertungsschritt wird aber ein SuccKonstruktor erzeugt, der wiederum bei der Berechung von min? t1 t genutzt werden
kann. Der Ausdruck min? t1 (min? t2 t3 ) liefert nun wie gewünscht die minimale Zeit
t1 = Succ (Succ One) als Ergebnis.
Ein ähnliches Problem tritt bei der Berechnung der Summe zweier RelTime-Werte
auf, die bspw. zur Bestimmung von Ereigniszeitpunkten für Ausdrücke der Form
ultrajump e verwendet wird. Auch die Funktion (+) als Teil der Klasse Num für
numerische Berechnungen lässt sich vom Compiler generieren, die erzeugte Funktion
5
Die Haskell-Standardklasse Ord enthält Funktionen für den Umgang mit Datentypen, für die eine totale
Ordnung über deren Werten exisiert.
56
5.4 Auswertung
data RelTime = One | Succ RelTime
infinite :: RelTime
infinite = Succ infinite
(6) :: RelTime → RelTime → Bool
One 6 _
= True
_
6 One = False
Succ t1 6 Succ t2 = t1 6 t2
min :: RelTime → RelTime → RelTime
min t1
t2
= if t1 6 t2 then t1 else t2
min? :: RelTime → RelTime → RelTime
min? One
_
= One
min? _
One
= One
?
min (Succ t1 ) (Succ t2 ) = Succ (min t1 t2 )
Abbildung 5.8: Typ des verwendeteten Zeitmodells und die Funktion infinite, sowie
die Standardimplementierung der Minimum-Funktion min mit der korrekten Version min?
kann in Anwesenheit von unendlichen RelTime-Werte aber auch hier zu einer Endlosrekursion führen. Bei der Implementierung wurde ein ähnlicher Ansatz wie bei min?
verfolgt, nämlich bei jedem rekursiven Aufruf einen Succ-Konstruktor zu erzeugen,
weshalb auf die genaue Implementierung an dieser Stelle nicht näher eingegangen wird.
Auswertung von Ausdrücken
Die Auswertung eines Ausdrucks erfolgt rekursiv über seinen Teilausdrücken. Bedingt
durch die zugrundeliegende Haskell-Implementierung mit verzögerter Auswertung werden auch die Tempus-Ausdrücke verzögert ausgewertet, d.h. nur die Teilausdrücke die
für das Ergebnis auch tatsächlich ausgewertert werden müssen. Für die Auswertung
eines Ausdrucks existiert die Funktion eval. Sie erhält als Parameter eine Liste der
für den aktuellen Ausdruck sichtbaren durch λ-Ausdrücke eingeführten Variablen mit
deren Werten, die Liste der aktuell sichtbaren Typsynonyme, Listen für die inferierten
Typen und Definitionen aller globalen Werte, sowie den auszuwertenden Ausdruck
selbst. Es erfolgt eine Fallunterscheidung über allen Konstruktoren des Typs Expr, die
im Wesentlichen aus der Auswertung aller Teilausdrücke und anschließender Konstruktion des Ergebniswertes besteht. Da die Funktionen pack und unpack in der internen
Darstellung von Value-Werten nicht berücksichtigt werden, entspricht eval für diese
Fälle der Identitätsfunktion.
Die Funktion eval verwendet für die Generierung der comap- und contramap-Funktionen
die Funktion genmap. Abbildung 5.9 zeigt die Signaturen dieser Funktionen. Die Funktion genmap erhält als Parameter die entsprechende Varianz bzgl. der das map abgeleitet werden soll, die Liste der sichtbaren Typsynonyme, die Typvariable nach der
57
5 Implementierung des Interpreters
abgeleitet werden soll, den Typ für den abgeleitet werden soll, sowie die anzuwendene Funktion. Als Ergebnis wird wiederum eine Funktion zurückgegeben, die die
entsprechende Transformation vornimmt. Die Implementierung erfolgte nach der in Abbildung 4.5 angegebenen Definitionen, wobei genmap CoVariant der Funktion comap
und genmap ContraVariant der Funktion contramap entspricht.
h
i
Hervorzuheben bei der genmap-Implementierung ist der Fall genmapα µν β . τ mit
β , α. Die in Unterabschnitt 4.4.2 diskutierte Definition für diesen Fall lautet
h
i
h
i
genmapα µν β . τ f e = pack µν β . τ [α0 /α]
◦g
h
i ◦ genmapβ [τ] genmapα µν β . τ f
h
i
◦ unpack µν β . τ e
h h
. ii
mit g = genmapα τ µν β . τ [α0 /α] β f und f : α → α0
und ist semantisch sowie bzgl. der Typisierung korrekt. Bei strenger Umsetzung dieser
Definition ergeben sich jedoch drastische Performanzeinbußen bei der Auswertung von
Ausdrücken. Nach dem Entpacken von e und dem rekursiven Anwenden
von genmap
h
. i
bzgl. β in allen Teilstrukturen entsteht ein Ausdruck des Typs τ0 = τ µν β . τ [α0 /α] β .
In diesem sollen nun mittels g alle verbleibenden Teilausdrücke des Typs α durch f
transformiert werden. Würde diese Transformation durch g = genmapα [τ0 ] f erfolgen, so würde bei der Auswertung ein exponentieller Aufwand entstehen, da für jede
Teilstruktur des Typs µν β . τ [α0 /α] wiederum ein entsprechendes genmap hergeleitet
und die Teilstrukturen rekursiv bearbeitet werden würden. Durch das vorangehende
genmap bzgl. β ist aber sichergestellt, dass in diesen Teilstrukturen kein Ausdruck des
Typs α mehr vorkommt, weshalb zwar eine rekursive Bearbeitung stattfinden würde,
diese aber nie tatsächlich zu einer Ersetzung führen würde. Deshalb kann g für die
Implementierung durch die wesentlich effizientere Transformation
g0 = genmapα [τ] f
ersetzt werden, ohne fehlerhafte Ergebnisse zu erhalten. Die rekursive genmap-Herleitung würde bei jedem Vorkommen von Teilausdrücken des Typs β die Rekursion
abbrechen und gemäß der Definition genmapα β f e0 = e0 den unveränderten Teilausdruck e0 zurückliefern. Die Implementierung von genmap wurde entsprechend dieser
optimierten Berechnung angepasst.
5.5 Interpreter
Das Modul Tempus.Interpreter enthält den eigentlichen Interpreter. Es handelt sich
dabei um eine Konsolen-REPL6 . Für die Eingabe von Kommands auf der Konsole
wurde die Haskell-Bibliothek haskeline genutzt, die unter anderem die automatische
6
read-eval-print loop, auf Deutsch etwa „Eingabe-Auswertung-Ausgabe-Schleife“
58
5.5 Interpreter
type EvalEnv = [(Var, Expr)]
eval
:: [(Var, Value)] → TypeSynEnv → TypeEnv
→ EvalEnv → Expr → Value
genmap :: Variance → TypeSynEnv → Var → Type
→ (Value → Value) → Value → Value
Abbildung 5.9: Signaturen der Funktionen eval für die Auswertung von Ausdrücken
und genmap für die Herleitung allgemeiner comap- bzw. contramapFunktionen
Vervollständigung von Bezeichnern oder Dateinamen erlaubt. Für die syntaktische und
semantische Analyse sowie die Auswertung von Ausdrücken werden in der REPL die in
den vorhergehenden Abschnitten beschriebenen Module genutzt.
Die Funktion repl stellt den Interpreter dar. Diese I/O-Aktion liest in einer Schleife ein
Kommando von der Eingabe, wertet dieses aus und gibt das Ergebnis wiederum auf der
Konsole aus. Der einzige Parameter von repl ist ein optionaler Dateipfad, der den Ort
der Prelude-Datei enthält. Sofern ein solcher Pfad angegeben wurde, wird beim Start
des Interpreters das Prelude-Modul aus der entsprechenden Datei geladen, andernfalls
wird kein Prelude-Modul verwendet. Innerhalb der Hauptschleife des Interpreters wird
der aktuelle Interpreter-Zustand als Wert des Typs IState gespeichert und entsprechend
des eingegebenen Kommandos geändert. Abbildung 5.10 zeigt den Typ IState und die
Signatur der Funktion repl. Der Interpreter-Zustand enthält den Dateipfad des aktuell
geladenen Tempus-Moduls modulePath, die Liste der aktuell geladenen Typsynonyme synEnv, die Liste der inferierten Typen aller aktuell geladenen Wertedefinitionen
typeEnv, die Liste der aktuell geladenen Wertedefinitionen evalEnv, sowie die drei
zuletzt genannten Listen nur für das geladene Prelude-Modul im Tupel preludeEnvs.
Die gesonderte Speicherung von preludeEnvs ist notwendig, da nach dem Laden eines
neuen Moduls die Definitionen des zuvor geladenen Moduls entfernt und durch die des
neuen Moduls ersetzt werden. Alle Definitionen des Prelude-Moduls sollen dabei jedoch
wieder verfügbar sein, ohne das Prelude-Modul erneut zu analysieren. Zudem werden
die Definitionen des Preludes bei der semantischen Analyse eines zu ladenden Moduls
berücksichtigt, so dass Prelude-Typen und -Werte innerhalb von beliebigen Modulen
verwendet werden dürfen.
Entsprechend der eingegebenen Kommandos wird auf die Funktionen für das Parsen,
die semantische Analyse und die Auswertung aus den zuvor diskutierten Modulen
zurückgegriffen. Der Interpreter unterstützt dabei die folgenden Kommandos:
:? gibt eine Liste aller verfügbaren Kommandos in der Konsole aus.
:l entfernt die Definitionen des aktuell geladenen Moduls aus dem Speicher. Dabei
werden auch alle während der Sitzung eingeführten Definitionen aus dem Speicher
entfernt.
:l h filei läd ein Tempus-Modul aus der Datei h filei. Es wird eine syntaktische und
59
5 Implementierung des Interpreters
data IState = IState {
modulePath :: Maybe FilePath,
typeEnv
:: TypeEnv,
synEnv
:: TypeSynEnv,
evalEnv
:: EvalEnv,
preludeEnvs :: (TypeEnv, TypeSynEnv, EvalEnv)
}
repl :: Maybe FilePath → IO ()
Abbildung 5.10: Zustandstyp IState der REPL und Signatur der Funktion repl
semantische Analyse für alle Definitionen im Modul durchgeführt. Ist diese für
alle Definitionen erfolgreich, werden die entsprechenen Typen bzw. Ausdrücke in
den Speicher geladen. Alle Definitionen eines zuvor geladenen Moduls und der
aktuellen Sitzung werden entfernt.
:v gibt eine Liste aller aktuell geladenen Definitionen aus. Bei Wertedefinitionen wird
der Bezeichner mit zugehörigem Typ ausgegeben, bei Typsynonymen die gesamte
Definition. Es werden alle Definitionen ausgegeben, die interaktiv oder, sofern
ein Modul geladen wurde, in diesem definiert wurden. Die Definitionen aus dem
Prelude-Modul werden nicht angezeigt.
:p gibt eine Liste der Prelude-Definitionen aus. Dieses Kommando arbeitet analog zu
:v, zeigt jedoch nur die Definitionen des Prelude-Moduls an.
:t hexpri führt eine Typinferenz für den Ausdruck hexpri aus und gibt den ermittelten
Typ in der Konsole aus. Tritt ein Fehler auf, so wird stattdessen dieser ausgegeben.
hdecli führt eine neue Definition ein, wobei hdecl i eine Typ- oder Wertedefinition sein
kann. Es wird eine syntaktische und semantische Analyse durchgeführt und bei Erfolg die Definition der aktuellen Werte- bzw. Typsynonymliste hinzugefügt, sonst
ein Fehler ausgegeben. Ein Überschreiben einer bereits vorhandenen Definition
ist nicht möglich.
hexpri prüft den Ausdruck hexpri und gibt das ausgewertete Ergebnis in der Konsole
aus. Im Fehlerfall wird auch hier stattdessen eine Fehlermeldung angezeigt.
:q beendet die aktuelle Tempus-Sitzung.
Ausgewertete Ausdrücke vom Typ Value werden im Wesentlichen analog zur ASCIIForm bei der Eingabe ausgegeben. Die vollständige Ausgabe von Funktionen ist jedoch
nicht möglich, stattdessen wird lediglich <function> angezeigt. Eine Besonderheit
stellen Verhalten und Ereignisse dar. Ein Ereignis Event t e wird in der Konsole durch
@t: e dargestellt, wobei t als Zahl für den Zeitpunkt des Feuerns und e entsprechend
dem jeweiligen Ausdruck dargestellt wird. Handelt es sich um ein nie eintretendes
Ereignis, so wird dieses durch die Ausgabe von ? kennlich gemacht. Genauer, tritt
60
5.5 Interpreter
ein Ereignis nach eventShowLookAhead Zeitschritten nicht ein, so wird ? ausgegeben. Der Standardwert liegt bei 100 Zeitschritten. Ein Ereignis Behavior f wird als
(@1: f 1; @2: f 2; . . .; @n: f n) mit n = behaviorShowFuture und dem Standardwert 7 für behaviorShowFuture angezeigt. Werte rekursiver Datentypen werden wie bei
der internen Darstellung als Werte ihres inneren Typs ausgegeben, insbesondere also
ohne Kennzeichnung von pack- bzw. unpack-Operationen. Zudem ist zu beachten, dass
die in Unterabschnitt 3.3.3 diskutierte, terminierende Auswertung von ν-Werten für die
Ausgabe in der aktuellen Interpreter-Version nicht umgesetzt ist, diese bei unendlichen
ν-Strukturen also nicht terminiert.
Im Modul Tempus.Main ist die Funktion main enthalten, die zum Erzeugen der ausführbaren Datei tempus verwendet wird. Diese ermittelt den Dateipfad des Prelude-Moduls
und startet anschließend die REPL mit diesem Pfad. Im Cabal-Paket des Interpreters
ist bereits eine Version des Prelude-Moduls enthalten, die standardmäßig nach der Installation des Paketes verwendet wird. Bei der Suche nach dem Prelude-Modul werden
die folgenden Pfade in dieser Reihenfolge durchsucht: 1.) Das aktuelle Verzeichnis, in
dem der Interpreter gestartet wurde, 2.) das Verzeichnis, in der die ausführbare Datei
tempus gespeichert ist, 3.) das Cabal-Verzeichnis für zusätzliche Programmdateien.
Sollte in keinem dieser Verzeichnisse eine Datei Prelude.tp vorhanden sein, so wird
der Interpreter gestartet ohne ein Prelude-Modul zu laden.
Für einen schnellen Einstieg in den Umgang mit dem Interpreter wird in Anhang C eine
Installationsanleitung sowie eine vollständige Interpreter-Sitzung gegeben. Anhand von
kurzen Beispielen soll dort die Verwendung der wichtigsten Interpreter-Kommandos
demonstriert werden.
61
5 Implementierung des Interpreters
62
6 Zusammenfassung und Ausblick
In der vorliegenden Arbeit wurde beschrieben, wie für die funktionale reaktive Programmiersprache Tempus eine Interpreter-Implementierung in der funktionalen Sprache
Haskell entstanden ist.
Es wurde gezeigt, wie bei beliebiger Verwendung von lokalen Variablen Inkonsistenzen
in der Startzeit von zeitabhängigen Werten entstehen können und wie Tempus diese durch
das Verbot von lokalen Variablen bei der Erzeugung zeitabhängiger Werte vermeidet.
Die Sicherung der Konsistenz von Startzeiten wird also statisch in der Sprache verankert,
wodurch das Verschieben von Werten in der Zeit verhindert wird. Zudem wurde eine
Motivation der Sprache Tempus durch Anwendung des Curry-Howard-Isomorphismus
auf eine intuitionistische lineare Temporallogik gegeben. Tempus’ Typsystem mit den
Regeln zur Sicherung der Startzeitkonsistenz wurde in Form von Inferenzregeln angegeben, die die Axiome der zugrunde liegenden Temporallogik widerspiegeln. Der Sprache
Tempus liegt somit ein starkes theoretisches Fundament zugrunde.
Des weiteren wurde eine kontextfreie Grammatik für die Sprache Tempus angegeben
und es erfolgte eine umfangreiche Beschreibung von Syntax und Semantik der Primitiven und Operatoren. Dabei wurde auch ausführlich auf das Typsystem mit dessen
Besonderheiten eingegangen. Wie trotz des Verbotes von impliziter Rekursion durch die
Operatoren fold und unfold rekursive Ausdrücke beschrieben werden können, wurde
unter anderem an Beispielen deutlich gemacht. Dabei wurde ebenfalls gezeigt, wie
für µ-Werte Termination bei der Auswertung gesichert wird und wie sich unendliche
ν-Werte trotzdem immer bis zu einer beliebigen Tiefe terminierend berechnen lassen.
Auch eine Möglichkeit zur Nachbildung einer Teilmenge von algebraischen Datentypen
in Tempus wurde aufgezeigt.
Es wurden weiterhin etablierte Algorithmen für die Inferenz des allgemeinsten Typs
eines Ausdrucks so erweitert, dass diese für die Typinferenz von Tempus-Ausdrücken
anwendbar sind. Zudem wurde beschrieben, wie sich anhand der Struktur eines Typs automatisch Funktor-Instanzen mit den entsprechenden Funktionen comap und contramap
ableiten lassen, die für die Auswertung von Faltungs- und Entfaltungsoperatoren notwendig sind. Dazu wurde auf Grundlage von Varianzregeln eine zusätzliche semantische
Prüfung eingeführt, die ermittelt, ob für einen Typ eine solche Funktor-Instanz existiert.
Schließlich wurden die beschriebenen Algorithmen in Haskell umgesetzt und in eine
neue, interaktive Konsolenanwendung integriert. Der so entstandene Tempus-Interpreter
ist in der Lage, Module mit Tempus-Definitionen zu laden, diese auf syntaktische und
63
6 Zusammenfassung und Ausblick
semantische Korrektheit zu prüfen und interaktiv um zusätzliche Typ- oder Wertedefinitionen zu ergänzen. Unter Verwendung dieser Definitionen eingegebene TempusAusdrücke können einer Typinferenz unterzogen oder ausgewertet werden. Einige häufig
verwendete, allgemeine Datentypen und Funktionen wurden in einem Prelude-Modul
zusammengefasst, das beim Start des Interpreter automatisch geladen wird. Zudem
wurde gezeigt, dass durch die für die Implementierung gewählte Zeitdarstellung Ereigniszeitpunkte korrekt und terminierend geordnet werden können, auch wenn dabei
niemals feuernde Ereignisse beteiligt sind. Zuletzt wurde gezeigt, wie sich durch kleine
Änderungen der formalen Algorithmusbeschreibungen drastische Performanzeinbußen
in der Implementierung vermeiden lassen.
Erweiterungsmöglichkeiten
Beim Schreiben dieser Arbeit und der Implementierung des Interpreters stellten sich die
im Folgenden genannten Punkte als, für den Rahmen dieser Arbeit, zu komplex oder
zeitintensiv heraus. Sie könnten als Ansatzpunkte für Erweiterungen an der Sprache
Tempus oder dem Interpreter dienen:
• Die aktuelle Version des Interpreters bietet zwar die Möglichkeit einzelne TempusModule zu laden, verfügt aber über kein eigenständiges Modulsystem. Eine Erweiterung der Sprache könnte die Auszeichnung von Modulnamen sowie der
importierten und exportierten Definitionen beinhalten. Dabei könnte auch das
aktuelle Prelude-Modul erweitert und ggf. auf mehrere Module einer Standardbibliothek aufgeteilt werden.
• Weiterhin bietet die Sprachdefinition eine Reihe von Möglichkeiten zur Erweiterung. Diese könnten umfassen:
– Die Einführung von Musteranpassungen1 in λ-Ausdrücken oder in Kombination mit einer Parameterliste bei der Definition von Werten. Für eine
Wertedefinition
– – onFirst : (α → β) → (α × γ) → ( β × γ)
value onFirst = λ f . λ p . ( f (first p), second p)
wären z.B. die äquivalenten Formen
value onFirst0 = λ f . λ ( p1 , p2 ) . ( f p1 , p2 )
value onFirst00 f ( p1 , p2 ) = ( f p1 , p2 )
oder
denkbar. Dabei wäre darauf zu achten, dass bei λ-Ausdrücken keine Musteranpassungen erlaubt sein dürften, die teilweise undefinierte Funktionen
1
Bei einer Musteranpassung wird versucht ein Muster p mit einem Ausdruck e anzugleichen, wobei p
eine Variable oder ein vollständig angewendeter Datenkonstruktor ist, dessen Parameter selbst wieder
Muster sind. In λ-Ausdrücken wie (λ p . e0 ) e können bei erfolgreicher Musteranpassung so alle in p
definierten Variablen als lokale Variablen in e0 eingeführt werden.
64
entstehen lassen. Eine Musteranpassung λ (left e1 ) . e würde bspw. nicht
den Fall right e2 abdecken. Gleiches gilt auch für Musteranpassungen in
Parameterlisten von Wertedefinitionen, jedoch ließe sich dieses Problem hier
beseitigen, wenn für denselben Wert eine Reihe von Definitionen angegeben
werden dürften, die zusammen jede mögliche Alternative abdecken.
– Eine Unterstützung von expliziten Typangaben für Wertedefinitionen. Die
angegebenen Typen müssten mit dem Ergebnis der Typinferenz unifiziert
werden, um festzustellen, ob der angegebene Typ eine zulässige Spezialisierung des allgemeinsten Typs ist. Explizit spezialisierte Typen würden bei der
Ausgabe im Interpreter die Bedeutung der zugehörigen Ausdrücke mitunter
wesentlich besser widerspiegeln als ein allgemeinster Typ.
– Die Einführung der in Unterabschnitt 3.2.4 beschriebenen algebraischen
Datentypen. Die dort am Beispiel beschriebene Umwandlung eines ADTs
in einen äquivalenten Tempus-Datentyp mit zugehörigen Wertedefinitionen
für die Konstruktoren könnte formalisiert werden, um eine solche Transformation automatisch generieren zu lassen.
• Einen Ansatzpunkt für eher theoretische Betrachtungen bietet die Sicherung der
Startzeitkonsistenzen zeitabhängiger Werte. Es könnte geklärt werden, wie genau
sich das Verbot der Verwendung lokaler Variablen in verschiedenen Primitiven auf
die Ausdruckskraft der Sprache auswirkt. Gibt es bspw. Verhalten oder Ereignisse,
die sich aufgrund dieser Restriktionen in Tempus nicht beschreiben lassen? Falls
ja, wie lassen sich diese klassifizieren? Ähnliche Einschränkungen existieren für
die Faltungs- und Entfaltungsoperatoren. Auch hier wäre zu klären, welche Klasse
von Ausdrücken sich nicht beschreiben lässt.
• In Unterabschnitt 3.3.3 wurde gezeigt, wie µ-Werte vollständig und ν-Werte
beliebig tief terminierend ausgewertet werden können. In der aktuellen InterpreterVersion können ν-Ausdrücken jedoch noch nicht terminierend angezeigt werden.
Die Implementierung könnte so erweitert werden, dass die Ausgabe von rekursiven Datentypen nur bis zu einer gewissen Tiefe erfolgt. Dies könnte auf Basis der
Anzahl von ausgewerteten pack-Funktionen pro Teilausdruck geschehen.
• Ein wesentlicher Schwachpunkt der aktuellen Implementierung ist das Verbot von
freien Typvariablen innerhalb von Typannotationen, was das Erstellen polymorpher Funktionen unter Verwendung rekursiver Ausdrücke einschränkt. Es wäre
zu untersuchen, wie die Typinferenz erweitert werden müsste, um polymorphe
Typen in Typannotationen zuzulassen. Dabei wäre unter anderem eine syntaktische Unterscheidung von Typvariablen und Typsynonymen erforderlich. Unter
Verwendung des List-Typs aus Unterabschnitt 3.2.4 würde eine Definition
value Cons = λ elem . λ list . pack [List element] (right (elem, list))
bspw. durch Hinzufügen einer zusätzlichen Typdefinition wie
type element = elementName × atomicNumber
65
6 Zusammenfassung und Ausblick
die Semantik des Wertes Cons verändern; aus einem polymorphen Elementtyp
würde ein monomorpher werden. Unter Verwendung eines Modulsystems könnte
so das Hinzufügen von Typsynonymen zu einem Modul A die zuvor erfolgreiche
Analyse eines Moduls B zerstören. Weiterhin könnte untersucht werden, ob,
und wenn ja, wie durch Anwendung erweiterter Verfahren zur Typinferenz (z.B.
Higher-Order-Unifizierung) auf Typannotationen vollständig verzichtet werden
kann.
66
Anhang A
Quellcode des Tempus-Preludes
Das folgende Tempus-Modul stellt die aktuelle Version des Tempus-Preludes dar. Die
darin definierten Typen und Werte werden vor jeder Interpreter-Sitzung geladen und
können sowohl interaktiv, als auch in Tempus-Modulen verwendet werden. Das PreludeModul ist in der Datei Prelude.tp im Basisverzeichnis des Cabal-Paketes enthalten.
––
Functions
– – id : α → α
value id = λ x . x
– – curry : (α × β → c) → α → β → c
value curry = λ f . λ x . λ y . f (x, y)
– – uncurry : (α → β → c) → α × β → c
value uncurry = λ f . λ p . f (first p) (second p)
––
Tuples
– – pair : α → β → α × β
value pair = λ x . λ y . (x, y)
– – normalize : (α × β) × c → α × β × c
value normalize = λ p . (first (first p), second (first p), second p)
– – onFirst : (α → β) → (α × c) → (β × c)
value onFirst = λ f . λ p . ( f (first p), second p)
– – onSecond : (α → β) → (c × α) → (c × β)
value onSecond = λ f . λ p . (first p, f (second p))
––
Booleans
type Bool = 1 + 1
– – false : Bool
value false = left hi
– – true : Bool
value true = right hi
67
Anhang A Quellcode des Tempus-Preludes
– – branch : α → α → Bool → α
value branch = λ x . λ y . case (λ _ . x) (λ _ . y)
– – not : Bool → Bool
value not = branch true false
– – and : Bool → Bool → Bool
value and = branch (λ _ . false) id
– – or : Bool → Bool → Bool
value or = branch id (λ _ . true)
– – xor : Bool → Bool → Bool
value xor = branch id not
––
Extended behaviors
type Behavior α = α × behavior α
– – Behavior : α → behavior α → Behavior α
value Behavior = pair
– – head : Behavior α → α
value head = first
– – tail : Behavior α → behavior α
value tail = second
– – cut : Behavior α → event β → event (Behavior α × β)
value cut = λ b . λ e . const pair expand (tail b) e
––
Extended events
type Event α = α + event α
– – now : α → Event α
value now = left
– – later : event α → Event α
value later = right
––
Event streams
type EventStream α = ν σ . event (α × σ)
68
Anhang B
Quellcode des Einführungsbeispiels
Im Folgenden ist der vollständige Quelltext des in Abschnitt 2.1 diskutierten Einführungsbeispiels abgedruckt. Dieses Modul ist auch dem Cabal-Paket des Interpreters in
der Datei Tempus/Examples/lightbulb.tp als Beispiel beigefügt.
––
Preparation
– – allNot : Behavior Bool → Behavior Bool
value allNot = λ b . Behavior (not (head b)) (const not tail b)
– – allXor : Behavior Bool → Behavior Bool → Behavior Bool
value allXor = λ b1 . λ b2 . Behavior (xor (head b1 ) (head b2 ))
(const xor tail b1 tail b2 )
type Ticks = ν σ . event σ
– – dropDummies : EventStream 1 → Ticks
value dropDummies = unfold [Ticks]
(λ s . (const second) unpack [EventStream 1] s)
type Segments = ν σ . Behavior Bool × event σ
– – flip : Behavior Bool × event α → event (Behavior Bool × α)
value flip = λ p . const (onFirst allNot) cut (first p) (second p)
– – step : Behavior Bool × Ticks
→ Behavior Bool × event (Behavior Bool × Ticks)
value step = λ p . (first p, flip (onSecond (unpack [Ticks]) p))
– – alternate : Behavior Bool → Ticks → Segments
value alternate = curry (unfold [Segments] step)
type UltraswitchArg = ν σ . behavior Bool × event (Bool × σ)
– – shiftRecPoints : behavior Bool × event Segments → UltraswitchArg
value shiftRecPoints = unfold [UltraswitchArg]
(onSecond (λ e . const (λ s . normalize (unpack [Segments] s)) e))
– – ultraswitchArg : Segments → UltraswitchArg
value ultraswitchArg = λ s . shiftRecPoints (
tail (first (unpack [Segments] s)), second (unpack [Segments] s))
69
Anhang B Quellcode des Einführungsbeispiels
– – prepareUltraswitch : Behavior Bool → EventStream 1
→ ν σ . behavior Bool × event (Bool × σ)
value prepareUltraswitch =
λ b . λ s . ultraswitchArg (alternate b (dropDummies s))
––
Actual example
– – control : Behavior Bool → EventStream 1 → Behavior Bool
value control = λ b . λ s .
Behavior (head b)
(ultraswitch (prepareUltraswitch b s))
– – init : Bool
value init = false
– – one : EventStream 1 → Behavior Bool
value one = control (Behavior init (const init))
– – two : EventStream 1 → EventStream 1 → Behavior Bool
value two = λ s1 . λ s2 . allXor (one s1 ) (one s2 )
––
Test cases
– – s1 : EventStream 1
value s1 = pack [EventStream 1] (event 2 (hi,
pack [EventStream 1] (const ? never)))
– – s2 : EventStream 1
value s2 = pack [EventStream 1] (event 4 (hi,
pack [EventStream 1] (event 1 (hi,
pack [EventStream 1] (const ? never)))))
– – test : Behavior Bool
value test = two s1 s2
70
Anhang C
Installation und Beispielsitzung
Für die Installation des Interpreters ist ein Haskell-Compiler erforderlich, der die Installation von Paketen des Cabal-Paketsystems unterstützt. Zum Erzeugen des Parsers
ist zudem das Werkzeug happy nötig, das ebenfalls als Cabal-Paket installiert werden
kann. Des weiteren sind die folgenden Pakete zur Installation erforderlich: mtl, uniplate,
array, utf8-string, filepath, directory, haskeline und executable-path.1 Die hier beschriebene Installation des Interpreters setzt außerdem das Paket cabal-install voraus, das die
Installation von cabal-Paketen erleichtert.
Für die Installation des Interpreters gibt es die Möglichkeit das Tempus-Paket2 direkt
von der Haskell-Paketdatenbank Hackage [8] zu installieren oder eine Kompilierung
des Paketes in Quelltextform vorzunehmen. Die Installation über Hackage kann einfach
durch
$ cabal install tempus
erreicht werden. Die Installation über den Quelltext erfolgt durch
$ tar -xzf tempus-0.1.0.tar.gz (sofern das Paket als gepackte Version vorliegt,
andernfalls werden die Quelltextdateien im Verzeichnis tempus-0.1.0 angenommen)
$ cd tempus-0.1.0
$ cabal install
Eine Konsolenanwendung tempus wird nun im Cabal-Verzeichnis für ausführbare Dateien installiert. Zudem wird das Prelude-Modul in das Cabal-Verzeichnis für zusätzliche
Paketdateien kopiert.3 Sofern das entsprechende Cabal-Verzeichnis für ausführbare Dateien über die aktuell gesetzte Pfad-Variable erreichbar ist, kann der Tempus-Interpreter
nun gestartet werden.
1
Die genauen Anforderungen an die Paketversionen sowie zusätzliche Anforderungen, wie vom Compiler
zu unterstützende Spracherweiterungen, können der Datei tempus.cabal entnommen werden.
2
Die Cabal-Version des Interpreters findet sich unter http://hackage.haskell.org/package/
tempus.
3
Diese Verzeichnisse können in der zum Cabal-Paketsystem gehörenden Konfigurationsdatei geändert
werden, siehe dazu [3].
71
Anhang C Installation und Beispielsitzung
$ tempus
This is tempus version 0.1.0
Loaded Prelude module from file ‘./Prelude.tp’.
>
Eine Interpreter-Sitzung ist nun gestartet und der Interpreter ist bereit Kommandos
auszuwerten. Sofern bei der Installation des Prelude-Moduls kein Fehler aufgetreten ist,
wird der Nutzer über den Pfadnamen des aktuell geladenen Preludes informiert. Es soll
nun eine Funktion zip definiert werden, die zwei Verhalten b1 und b2 punktweise mit
einer Funktion f zu einem neuen Verhalten b verknüpft.
> value zip = \ f . \b1 . \ b2 . const f <*> b1 <*> b2
*** parse error: (1,25): error parsing token . at (1,23)
Der Syntaxfehler weist auf ein unerwartetes Token „.“ nach „\b1“ hin. Wie in Abschnitt 3.1 beschrieben, werden Token durch expliziten Whitespace voneinander getrennt.
Eine Korrektur von „\b1“ zu „\ b1“ behebt den Syntaxfehler.
> value zip = \ f . \ b1 . \ b2 . const f <*> b1 <*> b2
*** type error: (1,1): undefined variable ‘f’
Ein neuer Fehler deutet auf eine undefinierte Variable f hin. Die Ursache liegt in der Verwendung der lokalen Variable f im Ausdruck const f , was aufgrund der Beschränkungen
zur Sicherung der Startzeitkonsistenz verboten ist. Eine Lösung ist, die Funktion f als
Verhalten bf über einer Funktion zu übergeben.
> value zip = \ bf . \ b1 . \ b2 . bf <*> b1 <*> b2
zip : behavior (_f -> _j -> _k) -> behavior _f -> behavior _j
-> behavior _k
Die Definition ist nun fehlerfrei und der inferierte Typ wird ausgegegben. Ein Beispielausdruck zeigt, dass die Funktion auch das gewünschte Resultat liefert.
> zip (const add) (behavior id) (const 10)
(@1: 11; @2: 12; @3: 13; @4: 14; @5: 15; @6: 16; @7: 17; ...)
Wie erwartet wird ein Verhalten erzeugt, das zu jedem zukünftigen Zeitpunkt den um 10
erhöhten Wert der Anzahl der Zeitschritte zu diesem Zeitpunkt liefert. Die Definition von
zip und der Beispielausdruck sollen nun in eine Datei ausgelagert werden. Dazu wird
eine neue Datei firststeps.tp angelegt, in der die folgenden Definitionen gespeichert
werden:
value zip
value beh1
value beh2
= λ bf . λ b1 . λ b2 . bf b1 b2
= behavior id
= const 10
value sample = zip (const add) beh1 beh2
Dieses Tempus-Modul kann nun im Interpreter geladen werden.
72
> :l firststeps.tp
zip : behavior (_f -> _j -> _k) -> behavior _f -> behavior _j
-> behavior _k
beh1 : behavior positive
beh2 : behavior positive
sample : behavior positive
firststeps>
Die Liste der Definitionen des Moduls wird in der Konsole mit den zugehörigen inferierten Typen ausgegeben. Zudem wird der Name des aktuell geladenen Moduls am Anfang
der Kommandozeile angezeigt. Die Beispieldefinitionen des Moduls können nun im
Interpreter verwendet werden.
firststeps> sample
(@1: 11; @2: 12; @3: 13; @4: 14; @5: 15; @6: 16; @7: 17; ...)
Ein Aufruf des Wertes sample liefert also das gleiche Ergebnis wie der zuvor verwendete
Beispielausdruck. Es wurden nun die wichtigsten Interpreter-Funktionen vorgestellt, so
dass die Beispielsitzung beendet werden kann.
firststeps> :q
Leaving tempus.
73
Anhang C Installation und Beispielsitzung
74
Literaturverzeichnis
[1] Gérard Berry and Georges Gonthier. The ESTEREL synchronous programming
language: design, semantics, implementation. Sci. Comput. Program., 19:87–152,
November 1992.
[2] Gavin Bierman and Valeria de Paiva. On an Intuitionistic Modal Logic. Studia
Logica, 65(3):383–416, August 2000.
[3] The Haskell Cabal. Webseite, 2011. http://www.haskell.org/cabal/.
[4] Luis Damas and Robin Milner. Principal type-schemes for functional programs.
In Proceedings of the 9th ACM SIGPLAN-SIGACT symposium on Principles of
programming languages, POPL ’82, pages 207–212, New York, NY, USA, 1982.
ACM.
[5] Conal Elliott and Paul Hudak. Functional Reactive Animation. In Proceedings of
the second ACM SIGPLAN international conference on Functional programming,
ICFP ’97, pages 263–273, New York, NY, USA, 1997. ACM.
[6] Conal M. Elliott. Push-pull functional reactive programming. In Proceedings of
the 2nd ACM SIGPLAN symposium on Haskell, Haskell ’09, pages 25–36, New
York, NY, USA, 2009. ACM.
[7] Thierry Gautier, Paul Le Guernic, and Löic Besnard. SIGNAL: A declarative
language for synchronous programming of real-time systems. In Proc. of a conference on Functional programming languages and computer architecture, pages
257–277, London, UK, 1987. Springer-Verlag.
[8] HackageDB – The Hackage package database. Webseite, 2011. http://hackage.
haskell.org.
[9] N. Halbwachs, P. Caspi, P. Raymond, and D. Pilaud. The synchronous dataflow
programming language LUSTRE. In Proceedings of the IEEE, pages 1305–1320,
1991.
[10] Haskell in industry.
Webseite, 2011.
haskellwiki/Haskell_in_industry.
http://www.haskell.org/
[11] Bastiaan J. Heeren. Top Quality Type Error Messages. PhD thesis, Universiteit
Utrecht, The Netherlands, September 2005. http://www.cs.uu.nl/people/
bastiaan/phdthesis.
75
Literaturverzeichnis
[12] J. Hughes. Why Functional Programming Matters. Computer Journal, 32:98–107,
April 1989.
[13] John Hughes. Generalising Monads to Arrows. Science of Computer Programming,
37:67–111, Mai 2000.
[14] Bart Jacobs and Jan Rutten. A Tutorial on (Co)Algebras and (Co)Induction. EATCS
Bulletin, 62:62–222, 1997.
[15] Wolfgang Jeltsch. Signals, Not Generators! In Trends in Functional Programming, volume 10 of Trends in Functional Programming, pages 145–160. Intellect,
UK/The University of Chicago Press, USA, June 2–4 2009.
[16] Wolfgang Jeltsch. The Curry–Howard Correspondence between Temporal Logic
and Functional Reactive Programming. Webseite, 2011. http://www.cs.ut.
ee/~varmo/tday-nelijarve/jeltsch-slides.pdf.
[17] Simon Peyton Jones.
Tackling the Awkward Squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell. In Engineering theories of software construction, pages 47–96. Press,
2002.
http://research.microsoft.com/en-us/um/people/simonpj/
Papers/marktoberdorf/mark.pdf.
[18] Hai Liu, Eric Cheng, and Paul Hudak. Causal commutative arrows and their
optimization. In Proceedings of the 14th ACM SIGPLAN international conference
on Functional programming, ICFP ’09, pages 35–46, New York, NY, USA, 2009.
ACM.
[19] Simon Marlow and Andy Gill. Happy User Guide, 2001. http://www.haskell.
org/happy/.
[20] Conor McBride. The Derivative of a Regular Type is its Type of One-Hole Contexts
(Extended Abstract), 2001. http://strictlypositive.org/diff.pdf.
[21] Henrik Nilsson, Antony Courtney, and John Peterson. Functional Reactive Programming, Continued. In Proceedings of the 2002 ACM SIGPLAN workshop on
Haskell, Haskell ’02, pages 51–64, New York, NY, USA, 2002. ACM.
[22] Ross Paterson. A new notation for arrows. In Proceedings of the sixth ACM
SIGPLAN international conference on Functional programming, ICFP ’01, pages
229–240, New York, NY, USA, 2001. ACM.
[23] Marc Pouzet. Lucid Synchrone Release, version 3.0 – Tutorial and Reference Manual, 2006. http://www.di.ens.fr/~pouzet/lucid-synchrone/
lucid-synchrone-3.0-manual.pdf.
[24] J. A. Robinson. A Machine-Oriented Logic Based on the Resolution Principle. J.
ACM, 12:23–41, Januar 1965.
[25] Neil Sculthorpe. Towards Safe and Efficient Functional Reactive Programming.
PhD thesis, School of Computer Science, University of Nottingham, Juli 2011.
76
Literaturverzeichnis
[26] Simulink User’s Guide. Webseite, 2011. http://www.mathworks.de/help/
toolbox/simulink/.
[27] Zhanyong Wan and Paul Hudak. Functional Reactive Programming from First
Principles. In Proceedings of the ACM SIGPLAN 2000 conference on Programming
language design and implementation, PLDI ’00, pages 242–252, New York, NY,
USA, 2000. ACM.
77
Was this manual useful for you? yes no
Thank you for your participation!

* Your assessment is very important for improving the work of artificial intelligence, which forms the content of this project

Download PDF

advertisement