Type Classes in Functional Logic Programming

Werbung
Arbeitsgruppe Programmiersprachen und Übersetzerkonstruktion
Institut für Informatik
Christian-Albrechts-Universität zu Kiel
Seminararbeit
Type Classes in Functional Logic
Programming
Matthias Böhm
WS 2012/2013
Inhaltsverzeichnis
1 Einleitung
1
2 Grundlagen
2.1 Typsysteme . . . . . . . . . . . . . . . . . . . . . . . .
2.1.1 Damas-Milner Typsystem . . . . . . . . . . . .
2.1.2 Polymorphismus . . . . . . . . . . . . . . . . .
2.1.3 Typklassen . . . . . . . . . . . . . . . . . . . .
2.1.4 Liberalisierung des Damas-Milner Typsystems
2.2 Funktional-logische Programmierung . . . . . . . . . .
2.2.1 Call-Time- und Run-Time-Choice . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3 Vorstellung der neuartigen Implementierung von Typklassen
3.1 Syntax der Quellprogramme . . . . . . . . . . . . . . . . .
3.1.1 Annotation der Funktionssymbole . . . . . . . . .
3.2 Transformation der Programme . . . . . . . . . . . . . . .
3.2.1 Erzeugung von Typzeugen . . . . . . . . . . . . . .
3.2.2 Haupttransformation . . . . . . . . . . . . . . . . .
3.3 Wichtige Vorbereitungen . . . . . . . . . . . . . . . . . . .
3.3.1 Annotation der Funktionssymbole . . . . . . . . .
3.3.2 Kontextreduktion . . . . . . . . . . . . . . . . . . .
3.3.3 Markierung der Klassennamen . . . . . . . . . . .
3.4 Fallbeispiel . . . . . . . . . . . . . . . . . . . . . . . . . .
3.5 Optimierungen . . . . . . . . . . . . . . . . . . . . . . . .
4 Diskussion
4.1 Effizienz . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2 Kein Ergebnisverlust . . . . . . . . . . . . . . . . . . . .
4.3 Module und separate Kompilierung . . . . . . . . . . . .
4.3.1 Implementierung der separaten Kompilierung bei
4.4 Probleme mit HO-Patterns . . . . . . . . . . . . . . . .
5 Zusammenfassung
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2
2
2
3
4
8
10
11
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
12
12
13
14
14
15
18
18
18
19
19
20
. . . .
. . . .
. . . .
TOY .
. . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
22
22
22
23
23
24
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
26
ii
1 Einleitung
Im Damas-Milner Typsystem, dem Typsystem, das von den meisten funktionalen und
logisch-funktionalen Programmiersprachen verwendet wird, können keine Funktionen
überladen werden. Möchte man zum Beispiel eine Funktion show für die Umwandlung
von Daten in Zeichenketten definieren, so muss man für jeden Typ eine eigene Funktion
angeben. Es ist nicht erlaubt, den gleichen Funktionsnamen zu verwenden. Um dennoch
Überladung zu ermöglichen, wurde das Damas-Milner Typsystem um das Konzept der
Typklassen erweitert.
Typklassen wurden zuerst in der Programmiersprache Haskell eingeführt. Sie waren
unter anderem dafür gedacht, den Gleichheitsoperator zu überladen: Man wollte gerne
Integer, Zeichen, Zeichenketten, Listen, Boolesche Werte etc. mit demselben Operator
(„==“) vergleichen können. Vor der Einführung von Typklassen wurde die Gleichheitsoperation durch den Compiler definiert, die sich bei algebraischen Datentypen an deren
Struktur orientierte. Mit Typklassen wurde es nun möglich, dass der Programmierer für
die gewünschten Typen eine eigene Gleichheitsfunktion definieren kann.
Während Typklassen in funktionalen Programmiersprachen mittlerweile wohletabliert
sind, sind die Versuche, Typklassen in funktional-logische Sprachen einzubetten, noch im
experimentellen Stadium [MM11]. So unterstützen einige Implementierungen der Sprache Curry 1 Typklassen, aufgrund der Verwendung eines Wörterbuch-basierten Ansatzes
stoßen diese aber auf Probleme [Lux08]. So gehen in gewissen Konstellationen Funktionsergebnisse verloren, die in funktional-logischen Programmen erwünscht sind.
In dieser Ausarbeitung wird eine alternative Implementierung von Typklassen vorgestellt, bei der dies nicht auftritt. Dazu wird erst auf die Grundlagen der verwendeten
Typsysteme und der funktional-logischen Programmierung eingegangen, dann wird die
eigentliche Programmtransformation im Hauptteil erläutert. Zum Schluss folgt eine Diskussion der Vorteile und Nachteile der vorgestellten Implementierung.
1
danae.uni-muenster.de/~lux/curry/, babel.ls.fi.upm.es/research/Sloth/,
zinc-project.sourceforge.net/
1
2 Grundlagen
2.1 Typsysteme
Typsysteme sind ein wichtiger Bestandteil von deklarativen Programmiersprachen wie
Haskell1 und Curry2 . Durch die Typisierung von Ausdrücken können schon zur Compilezeit Programmierfehler entdeckt werden. Deshalb wird oft im Zusammenhang mit
Typsystemen folgendes Zitat erwähnt: „Well-typed programs cannot ,go wrong‘ “ (Robin Milner, [Mil78]).
2.1.1 Damas-Milner Typsystem
Das Damas-Milner Typsystem ([DM82]) ist ein unter funktionalen und funktional-logischen Programmiersprachen weit verbreitetes Typsystem. So verwenden zum Beispiel
Haskell, Standard ML3 und Curry dieses Typsystem als Basis.
Die Syntax der Typsprache ist in Abb. 2.1 gegeben.4
Typvariablen sind Platzhalter für beliebige Typen.
Typkonstruktoren werden in Programmen durch Deklarationen von algebraischen Datentypen eingeführt. In Haskell wird ein Typkonstruktor der Stelligkeit n zum Beispiel
durch data C τn = ... eingeführt.
Einfache Typen bestehen aus Typvariablen, Typkonstruktoren und dem „→“-Symbol.
τ → τ 0 steht hierbei für den Typ einer Funktion von τ nach τ 0 .
Typschemata stehen für eine ganze Familie von Typen. Die durch den Allquantor
gebundenen Typvariablen können durch beliebige Typen ersetzt werden. Typschemata
werden unter anderem bei Funktionen eingesetzt. So ist der Typ der Funktion length in
Haskell gegeben durch ∀α.[α] → Int. Dies bedeutet, dass für α jeder Typ eingesetzt werden kann. Durch Typschemata wird parametrischer Polymorphismus ermöglicht (siehe
Abschnitt 2.1.2).
Typinferenz
Das Damas-Milner Typsystem ermöglicht eine einfache Inferenz der Typen von Ausdrücken. Dabei wird der Typ eines Ausdrucks unter einer gegebenen Menge von Typannahmen bestimmt, geschrieben A ` e : τ . A ist hierbei eine Menge von Typannahmen
der Form „Bezeichner : σ“.
1
http://www.haskell.org
http://www.curry-language.org
3
z.B. http://www.smlnj.org
4
Ein Oberstrich zusammen mit einem Index n bedeute die n-fache Wiederholung des Ausdrucks, z.B.
bedeutet τn τ1 τ2 . . . τn .
2
2
2 Grundlagen
Typvariablen:
Typkonstruktoren:
Einfache Typen:
Typschemata:
α, β, γ, . . .
C
τ
::= α
| C τn
| τ → τ0
σ
::= ∀ αn .τ
n = Stelligkeit von C, n ≥ 0
n≥0
Abbildung 2.1: Basistypsprache des Damas-Milner-Typsystems
Beispiel 1 Sei A = {null : ∀α.[α] → Bool, tail : ∀α.[α] → [α]} und die Funktion length gegeben durch length xs = if null xs then 0 else 1 + length
(tail xs). Dann kann für die Funktion length der Typ ∀α.[α] → Int hergeleitet
werden, geschrieben „A ` length : ∀α.[α] → Int“.
Mit Hilfe von Inferenzregeln kann überprüft werden, ob eine gegebene Typannahme
A ` e : τ korrekt ist ([DM82]). Die Inferenzregeln stellen aber kein konstruktives Verfahren für die Typzuweisung dar, die für einen gegebenen Ausdruck und eine Menge
von Typannahmen den Typ des Ausdrucks bestimmt. Es existiert aber ein Algorithmus,
der in [DM82] beschrieben wird (Algorithmus W), mit dem dieser Typ zum Beispiel
von einem Compiler berechnet werden kann. Genauer wird durch diesen Algorithmus
bei gegebenen A und Ausdruck e eine Substitution π und ein Typ τ berechnet, so dass
π(A) ` e : τ gilt. Dies soll im Folgenden durch die Notation A e : τ |π ausgedrückt
werden.
Wohlgetyptheit
Die Definition der Wohlgetyptheit ist eine wichtige Eigenschaft aller Typsysteme. Im
Damas-Milner Typsystem ist Wohlgetyptheit zum Beispiel wie folgt definiert:
Definition 1 (Wohlgetyptheit) Eine Regel l = r in einem funktionalen oder
logisch-funktionalen Programm ist dann typkorrekt im Damas-Milner Typsystem,
wenn die Typen der linken und der rechten Seite gleich sind. Ein gegebenes Programm, das aus mehreren Regeln besteht, ist dann typkorrekt, wenn alle Regeln
typkorrekt sind.
2.1.2 Polymorphismus
Polymorphismus bedeutet im Allgemeinen, dass ein bestimmter Bezeichner verschiedene Typen annehmen kann. Es werden üblicherweise zwei Arten von Polymorphismen
unterschieden ([WB89]):
Parametrischer Polymorphismus
Parametrischer Polymorphismus tritt immer dann auf, wenn eine Funktion auf verschiedenen Typen genau dasselbe Verhalten zeigt. Ein Beispiel dafür ist die length-Funktion
3
2 Grundlagen
auf Listen:
length [ ] = 0
length ( x : xs ) = 1 + length xs
Diese Funktion hat im Damas-Milner Typsystem den Typ ∀α.List α → Int. Die
Funktion kann also mit Listen beliebigen Typs aufgerufen werden: length [1, 2] ;
2, length [’a’, ’b’, ’c’] ; 3. Parametrischer Polymorphismus wird im DamasMilner Typsystem durch Typschemata abgebildet.
Ad-hoc Polymorphismus
Ad-hoc Polymorphismus (auch Overloading genannt) tritt dann auf, wenn eine Funktion für verschiedene Typen unterschiedliches Verhalten zeigt. Ein Beispiel dafür ist die
Gleichheitsfunktion: Wird diese auf zwei Zahlen angewandt, wird vollkommen anderer
Code ausgeführt als wenn sie zum Beispiel auf Listen angewandt wird.
Ad-hoc Polymorphismus wird im Damas-Milner Typsystem nicht unterstützt. Jede
Funktion kann nur genau einen Typ besitzen. Es ist also nicht möglich, die Gleichheitsfunktion so zu überladen, dass sie gleichzeitig auf Zahlen und auf Listen angewandt
werden kann. Für alle Gleichheitsfunktionen müssten deshalb unterschiedliche Namen
eingeführt werden.
Um dennoch Überladung zu ermöglichen, muss das Damas-Milner Typsystem erweitert werden. In Haskell und in anderen Programmiersprachen wurden dafür Typklassen
eingeführt.
2.1.3 Typklassen
Typklassen ([WB89]) erlauben auf einfache und elegante Weise das Überladen von Funktionen.
Basiselemente von Typklassen
Die einzelnen Elemente von Typklassen können am besten anhand eines Beispiels eingeführt werden.
Für die Ausgabe von Daten und Werten auf der Konsole ist es oft hilfreich, eine
Funktion show zur Verfügung zu haben, die die übergebenen Daten in eine Zeichenkette
umwandelt.
Im Damas-Milner Typsystem müsste man dafür für jeden Datentypen eine eigene
show-Funktion bereitstellen, zum Beispiel showInt für Zahlen oder showChar für Zeichen. Außerdem müsste man für die Ausgabe auf der Konsole ebenfalls für jeden Typ
eine entsprechende Funktion bereitstellen. Wie eine solche Implementierung demnach
aussehen muss, ist in Abbildung 2.2 a) gezeigt5 .
Mit Typklassen ist es möglich, die show-Funktion zu überladen, sodass man für alle
Typen denselben Methodennamen verwenden kann. Außerdem kann man nun auch die
5
Hier wird davon ausgegangen, dass intToString und floatToString schon implementiert sind
4
2 Grundlagen
showInt : : Int −> String
showInt n = i n t T o S t r i n g n
showChar : : Char −> String
showChar c = " ’ " ++ [ c ] ++ " ’ "
showFloat : : Float −> String
showFloat f = f l o a t T o S t r i n g f
p r i n t I n t : : Int −> IO ( )
printInt n =
putStrLn ( showInt n )
p r i n t C h a r : : Char −> IO ( )
printChar c =
putStrLn (showChar c )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
c l a s s Show a where
show : : a −> String
instance Show Int where
show n = i n t T o S t r i n g n
instance Show Char where
show c = " ’ " ++ [ c ] ++ " ’ "
instance Show Float where
show f = f l o a t T o S t r i n g f
print : : Show a => a −> IO ( )
print x = putStrLn (show x )
p r i n t F l o a t : : Float −> IO ( )
printFloat f =
putStrLn ( showFloat f )
a)
b)
Abbildung 2.2: Implementierung der Konsolenausgabe von Daten ohne und mit Verwendung von Typklassen
Ausgabefunktion print überladen (Abb. 2.2 b)). Die Konstrukte, die dafür notwendig
sind, erläutere ich nun im Folgenden.
Will man eine Methode überladen, so muss zuerst eine Typklasse angegeben werden
(Zeilen 1 - 2). In der Typklasse selber stehen die zu überladenden Methoden (hier die
Methode show).
Danach muss für jeden Typ, für den man die Methoden überladen will, eine Instanz
angegeben werden. In der Instanzdeklaration werden die konkreten Implementierungen
der überladenen Methoden angegeben (Zeilen 4 - 11).
Nun kann man mit Hilfe der überladenen Methoden auch andere Funktionen überladen, wie hier die print-Funktion (Zeilen 13 - 14). Dazu muss zusätzlich zur Signatur
der Funktion ein Kontext angegeben werden. Im obigen Beispiel ist der Kontext „(Show
a)“. Im Kontext wird angegeben, dass für bestimmte Typvariablen nur Typen eingesetzt
werden dürfen, die einer bestimmten Typklasse angehören. Im Beispiel wird angegeben,
dass für die Typvariable a ein Typ eingesetzt werden muss, der der Typklasse Show angehört. Die Funktion print kann nun dank des Kontexts auf alle Ausdrücke angewandt
werden, deren Typ eine Instanz der Typklasse Show ist, und es wird jeweils die korrekte
Implementierung von show ausgewählt und aufgerufen.
Kontexte werden in Haskell üblicherweise in der Form (Kontext1 , . . ., Kontextn )
=> Signatur angegeben ([HPJW+ 92]); hier wird dafür die Schreibweise hKontext1 , . . .,
Kontextn i ⇒ Signatur benutzt. Die komplette Erweiterung der Typsyntax aufgrund
5
2 Grundlagen
Klassennamen:
Kontext:
Gesättigter Kontext:
Überladener Typ:
κ
θ
φ
ρ
::= hκn αn i
::= hκn τn i
::= φ ⇒ τ
n≥0
n≥0
Abbildung 2.3: Erweiterung der Typsyntax auf Typklassen
von Typklassen ist in Abb. 2.3 angegeben. Der Begriff „überladener Typ“ wurde hier
noch nicht eingeführt und steht für eine Typsignatur mit einem zusätzlichen Kontext.
Ein einfacher Kontext darf darüber hinaus nur Typvariablen enthalten, während ein
gesättigter Kontext auch beliebige Typen enthalten kann. Tritt in einem Kontext der
Ausdruck κ τ auf, so wird dies als „die Klasse κ schränkt den Typ τ ein“ gelesen.
Weitere Elemente
Kontexte können auch in Klassen- und Instanzdeklarationen auftreten. Kontexte in Instanzdeklarationen werden zum Beispiel dann benötigt, wenn man die show-Funktion
für Paare überladen will. Dies ist im Damas-Milner Typsystem unmöglich:
showPair : : ( a , b ) −> String
−− I m p l e m e n t i e r u n g unmoeglich , da Typen von
−− a und b n i c h t b e k a n n t .
showPair ( a , b ) = ???
Mit Typklassen ist es möglich, eine show-Funktion für Paare anzugeben. Da auch die
Elemente des Paares in Zeichenketten umgewandelt werden sollen, ist es notwendig, dass
die show-Funktion auf die Elemente des Paares angewendet werden kann. Dies ist nur
dann möglich, wenn die Typen der Elemente Instanzen der Show-Klasse sind. Dies wird
durch die Kontextangabe hShow a, Show bi in folgender Instanzdeklaration ausgedrückt:
instance (Show a , Show b ) => Show ( a , b ) where
show ( a , b ) = " ( " ++ show a ++ " , " ++ show b ++ " ) "
Wie bei Funktionsdeklarationen wird im Kontext ausgedrückt, dass für die aufgeführten Typvariablen nur Typen eingesetzt werden können, die die entsprechenden Typklassen implementieren.
Ein Beispiel für die Verwendung von Kontexten in Klassendeklarationen ist folgendes:
c l a s s Eq a where
(==) : : a −> a −> Bool
c l a s s Eq a => Ord a where
(<=) : : a −> a −> Bool
Die Klasse Eq überlädt den Gleichheitsoperator, und die Klasse Ord überlädt den
Kleinergleich-Operator und den Gleichheitsoperator, da im Kontext der Klassendefinition von Ord die Klasse Eq angegeben ist. Die Klasse Ord wird in diesem Fall auch
Subklasse von Eq genannt. Mit den beiden Definitionen des Beispiels soll ausgedrückt
6
2 Grundlagen
data DictShow a = DictShow ( a −> String ) −− W o e rt e r b u c ht y p
selShow ( DictShow f ) = f −− S e l e k t i o n s f u n k t i o n f u e r d i e show−Funktion
−− Woerterbuch f u e r den I n t −Typ
d i c t S h o w I n t : : DictShow Int
d i c t S h o w I n t = DictShow i n t T o S t r i n g
−− Woerterbuch f u e r den Char−Typ
dictShowChar : : DictShow Char
dictShowChar = DictShow ( \ c −> " ’ " ++ [ c ] ++ " ’ " )
−− Woerterbuch f u e r den F l o a t −Typ
d i c t S h o w F l o a t : : DictShow Float
d i c t S h o w F l o a t = DictShow f l o a t T o S t r i n g
print : : DictShow a −> a −> IO ( )
print dictShow x = putStrLn ( selShow dictShow x )
Abbildung 2.4: Übersetzung von Typklassen nach Wörterbüchern (Übersetzung des
show-Beispiels aus Abb. 2.2)
werden, dass, wenn eine Ordnung definiert wird, auch immer die Gleichheit definiert sein
muss.
Implementierung mit Wörterbüchern
Typklassen werden üblicherweise mit Hilfe von Wörterbüchern implementiert. Für jede
Typklasse wird ein algebraischer Datentyp angelegt, der Platzhalter für die Methoden
der Typklasse besitzt. Um auf die Methoden im Wörterbuch zuzugreifen, werden außerdem Selektorfunktionen angelegt. Jede Instanz der Typklasse definiert ein konkretes
Wörterbuch, in dem auf die konkreten Implementierungen der Typklassenfunktionen
für den jeweiligen Typ verwiesen wird. In Abb. 2.4 wird dies anhand eines konkreten
Beispiels gezeigt.
Eine überladene Methode wie die print-Methode aus dem show-Beispiel bekommt
einen oder mehrere zusätzliche Parameter. In diesen Parametern werden die Wörterbücher übergeben, die zu den Typen der Parameter gehören, auf die die Funktion angewandt wird. So würde zum Beispiel der Ausdruck print ’a’ übersetzt werden nach
print dictShowChar ’a’, und der Ausdruck print 1 nach print dictShowInt 1.
Die Übersetzung der Typklassen in Wörterbücher wird zumeist durch eine Transformation auf dem Quellprogramm durchgeführt. Das transformierte Programm enthält keine Typklassen mehr und ist außerdem im Damas-Milner Typsystem gültig.
Diese Transformation kommt in den meisten Implementierungen von Typklassen vor
([Aug93, PJ93, HHPJW96, JJM97]).
7
2 Grundlagen
2.1.4 Liberalisierung des Damas-Milner Typsystems
In [LFMMRH10] wird eine Erweiterung des Damas-Milner Typsystems vorgeschlagen,
die - wie später ersichtlich wird - eine einfache Übersetzung von Typklassen ermöglicht.
Die wesentliche Erweiterung ist die Neudefinition des Begriffs der Wohlgetyptheit (siehe
2.1.1). Es wird davon ausgegangen, dass ein Programm aus Regeln der Form l = r
besteht, wobei die linke und die rechte Seite Datenvariablen, also Parameter, enthalten
können.
Definition 2 Eine Programmregel l = r ist dann wohlgetypt in Hinsicht auf eine
Menge A von Typannahmen, wenn folgendes gilt:
a) A ∪ {xn : αn } l : τL |πL
b) A ∪ {xn : βn } r : τR |πR
c) ∃π.(τL , αn πL ) = (τR , βn πR )π, wobei π eine Substitution ist
wobei {xn } die Menge der Datenvariablen in l ist, und {αn } und {βn } frische Variablen. Die Typen und Substitutionen in Punkt a) und b) werden mit Hilfe des
Algorithmus W berechnet (siehe 2.1.1).
Es werden nun also nicht nur die Typen der linken und der rechten Regelseiten bestimmt, sondern auch die Typen der vorkommenden Datenvariablen. Die ermittelten
Substitutionen bilden dabei die in den Typannahmen eingeführten frischen Typvariablen auf die konkreten Typen der Datenvariablen ab.
Eine Programmregel ist nun also nicht nur dann wohlgetypt, wenn die Typen der
linken und der rechten Seite übereinstimmen, sondern auch, wenn die für die rechte
Seite inferierten Typen (τR , αn πR ) für die rechte Seite und die Variablen allgemeiner
als die für die linke Seite inferierten Typen (τL , αn πL ) sind (dies wird durch Punkt 3
ausgesagt).
Beispiel 2 Sei A = {eq :: ∀α.α → α → Bool, Z :: Nat, S :: Nat → Nat, f ::
∀α.Bool → α}, wobei Nat der Datentyp für Peanozahlen sein soll. Dann ergeben
sich für die folgenden Regeln folgende Tupel (τL , αn πL ) bzw. (τR , αn πR ):
Regel
eq Z Z = True
eq (S x) (S y) = eq x y
eq Z (S x) = False
f True = Z
(τL , αn πL )
(Bool)
(Bool, x :: N at, y :: N at)
(Bool, x :: N at)
(α)
(τR , αn πR )
(Bool)
(Bool, x :: γ, y :: δ)
(Bool, x :: γ)
(N at)
Die ersten drei Regeln sind wohlgetypt, da die Typen der rechten Seite allgemeiner
als die der linken Seite sind; die letzte Regel hingegen ist nicht wohlgetypt, da N at
spezieller ist als α, denn es gibt keine Substitution, durch die N at in eine Typvariable
überführt werden könnte.
8
2 Grundlagen
eq : : ∀α.α → α → Bool
r b o o l : : Repr Bool
r n a t : : Repr Nat
eq : : ∀α.Repr α → α → α → Bool
eq
eq
eq
eq
rbool
rbool
rbool
rbool
eq
eq
eq
eq
rnat
rnat
rnat
rnat
True True −> True
True False −> False
False True −> False
False False −> True
Z Z −> True
Z ( S x ) −> False
( S x ) Z −> False
( S x ) ( S y ) −> eq r n a t x y
a)
eq
eq
eq
eq
True True −> True
True False −> False
False True −> False
False False −> True
eq
eq
eq
eq
Z Z −> True
Z ( S x ) −> False
( S x ) Z −> False
( S x ) ( S y ) −> eq x y
b)
Abbildung 2.5: Typindizierte Funktionen (mit GADTs bzw. der liberalen Typerweiterung)
Typindizierte Funktionen
Das vorgestellte liberalere Typsystem ermöglicht eine einfache Verwendung von typindizierten Funktionen. Typindizierte Funktionen sind Funktionen, deren Typ durch einen
anderen Typ parametrisiert ist ([HL07]). Ein Beispiel dafür, wie solche Funktionen aussehen, ist in Abb. 2.5 a) zu sehen.
Mithilfe von GADTs (generalized algebraic datatypes) kann ein solches Programm
auch in funktionalen Programmiersprachen wie Haskell hingeschrieben werden, wenn die
entsprechende Spracherweiterung eingeschaltet wird. Darauf soll hier aber nicht weiter
eingegangen werden.
Mit der oben beschriebenen Erweiterung des Damas-Milner Typsystems können die
Regeln aus Abb. 2.5 a) sogar noch weiter vereinfacht werden: Das Argument, welches
den Typ angibt, kann weggelassen werden. Dann erhält man das Programm in Abb.
2.5 b). Es ist also möglich, für jeden Typ, für den die Gleichheit definiert werden soll,
die entsprechenden Regeln so anzugeben, wie sie in einer unüberladenen Definition der
Gleichheit hingeschrieben werden würden.
Die Gleichheitsfunktion kann also einfach dadurch definiert werden, dass die einzelnen
Regeln für die unterschiedlichen Typen nacheinander hingeschrieben werden; es müssen
nicht für jeden Typ unterschiedliche Funktionsnamen gewählt werden, wie es im reinen
Damas-Milner Typsystem nötig wäre.
9
2 Grundlagen
2.2 Funktional-logische Programmierung
Funktional-logische Programmierung (kurz FLP) vereint Konzepte der logischen und
der funktionalen Programmierung. Die logische Programmierung basiert auf Prädikaten.
Funktionen werden dadurch ausgedrückt, dass in dem entsprechenden Prädikat noch ein
Feld für den „Rückgabewert“ vorhanden ist. Ein Beispiel ist die append-Funktion für
Listen; der dritte Parameter des Prädikats enthält dabei das Funktionsergebnis:
append ( [ ] , Ys , Ys ) .
append ( [ X| Xs ] , Ys , [ X| Zs ] ) :− append ( Xs , Ys , Zs ) .
Die Verwendung von Prädikaten hat den Vorteil, dass auch sehr leicht Umkehrfunktionen von append berechnet werden können, zum Beispiel:
?− append ( Xs , [ 3 , 4 ] , [ 1 , 2 , 3 , 4 ] ) .
; Xs = [ 1 , 2 ]
In funktionalen Programmen würde man die append-Funktion als eine Liste von Regeln hinschreiben:
append [ ] ys = ys
append ( x : xs ) ys = x : append xs ys
In funktionalen Programmen kann man damit nur das Ergebnis einer Funktion berechnen:
append [ 1 , 2 ] [ 3 , 4 ]
; [1 , 2 , 3 , 4]
Mit der funktional-logischen Programmierung ist nun auch möglich, wie in Prolog auf
die Umkehrfunktionen von append zuzugreifen, indem die Funktion als Prädikat benutzt
wird. Wie dies aussehen kann, zeige ich hier für die funktional-logische Programmiersprache Curry:
append xs [ 3 , 4 ] =:= [ 1 , 2 , 3 , 4 ] where xs f r e e
; xs = [ 1 , 2 ]
Somit können bei FLP Funktionen in der gewohnten Schreibweise der funktionalen
Programmierung hingeschrieben werden, aber auch als Prädikate verwendet werden.
Eine weitere wichtige Eigenschaft der funktional-logischen Programmierung ist der
Nichtdeterminismus, der auch von der Logikprogrammierung übernommen wurde. So
ist es zum Beispiel möglich, folgende Operation zu definieren:
coin = 0
coin = 1
Bei der Auswertung von coin kann nun als Ergebnis sowohl 0 als auch 1 herauskommen.
10
2 Grundlagen
2.2.1 Call-Time- und Run-Time-Choice
Durch den Nichtdeterminismus können subtile Semantikprobleme auftreten, wie zum
Beispiel in folgendem Codeausschnitt:
coin = 0
coin = 1
two x = [ x , x ]
Bei einem Aufruf von two coin ist die Frage, welche Ergebnisse dabei herauskommen.
Man unterscheidet zwei Semantiken:
• Call-time-choice: Der Parameterwert wird beim Aufruf der Funktion gebunden,
mögliche Ergebnisse sind also [0, 0] und [1, 1].
• Run-time-choice: Der Wert wird erst bei der Auswertung ermittelt. Mögliche Ergebnisse sind hier [0, 0], [0, 1], [1, 0] und [1, 1].
Die üblicherweise von funktional-logischen Programmiersprachen verwendete Semantik ist die Call-Time-Choice-Semantik.
Bei der Verwendung von Typklassen mit nullstelligen Methoden können durch die
Übersetzung in Wörterbücher ungewollte Effekte auftreten, wenn Call-Time-Choice verwendet wird. Ein Beispiel dafür ist folgendes Programm (entnommen von [Lux]):
c l a s s Arb a where
arb : : a
instance Arb Bool where
arb = True
arb = False
twoArb : : Arb a => [ a ]
twoArb = [ arb , arb ]
data DictArb a = DictArb a
s e l A r b : : DictArb a −> a
s e l A r b ( DictArb a ) = a
d i c t A r b B o o l = DictArb arbBool
where arbBool = True
arbBool = False
twoArb : : DictArb a −> [ a ]
twoArb d i c t = [ s e l A r b d i c t , s e l A r b d i c t ]
Originalprogramm
Transformiertes Programm
Im Originalprogramm soll der Aufruf von twoArb die Werte [False, False], [False,
True], [True, False] und [True, True] ergeben. Wird im Gegensatz dazu die Funktion twoArb im transformierten Programm durch twoArb dictArbBool aufgerufen, so
werden nur die Werte [False, False] und [True, True] ermittelt.
Der Grund dafür ist, dass im Aufruf twoArb dictArbBool das Wörterbuch dictArbBool an den Parameter dict gebunden wird. Wird nun die Funktion arbBool durch das
linke selArb dict ausgewertet, so wird der ermittelte Wert durch Sharing aufgrund
der Call-Time-Choice-Semantik auch an das rechte selArb dict weitergegeben, womit
insgesamt nur die zwei Ergebnisse berechnet werden.
Mit der im Hauptteil vorgestellten Transformation treten die erläuterten Probleme mit
nullstelligen Funktionen nicht mehr auf, und es werden hier alle vier Lösungen berechnet.
11
3 Vorstellung der neuartigen
Implementierung von Typklassen
Der Hauptgegenstand des vorgestellten Papiers ([MM11]) ist eine Transformation von
Quellprogrammen mit Typklassen in Zielprogramme, die wohlgetypt in dem vorgestellten liberalen Typsystem sind. Durch diese Transformation werden Typklassen auf eine
neuartige Weise implementiert. Die vorgestellte Transformation ist insofern neuartig, als
sie einen anderen Ansatz als die Verwendung von Wörterbüchern verfolgt.
3.1 Syntax der Quellprogramme
In Abb. 3.1 ist die Syntax der Quellprogramme zusammengefasst, die im Folgenden
verwendet wird.
Programme bestehen aus Datentypdeklarationen, Klassendeklarationen, Klasseninstanzen, den Typen für die deklarierten Funktionen und den Funktionen selber. Die
Funktionen werden durch eine Menge von Regeln angegeben.
Datentypdeklarationen, Klassendeklarationen und Typklassen-Instanzdeklarationen
folgen der Syntax von Haskell; das data-Schlüsselwort leitet eine Definition eines algebraischen Datentyps (ADT ) ein, mit dem class-Schlüsselwort wird eine Klassendeklaration mit mehreren Funktionen eingeleitet, und nach dem instance-Schlüsselwort
wird eine Instanzdeklaration für den Typen C α und die Klasse κ angegeben.
Die Typangaben für die deklarierten Funktionen folgen ebenfalls der Syntax von Haskell; es wird immer sowohl der Kontext als auch der Typ angegeben.
Regeln bestehen aus einer linken und einer rechten Seite. Auf der linken Seite wird
zuerst das Funktionssymbol angegeben, das angibt, zu welcher Funktion die Regel gehört. Hier wird auch gleich der überladene Typ der Funktion angegeben. Anschließend
werden Patterns, also Muster angegeben, die aus funktionalen Programmiersprachen
bekannt sind. Die Pattern müssen linear sein, jede Datenvariable darf also pro Regel
nur genau einmal auf der linken Seite vorkommen. Auf der rechten Seite der Regel wird
ein Ausdruck angegeben; dieser wird ausgewertet, wenn die Funktion mit Parametern
aufgerufen wird. Die Annotation des Funktionssymbols auf der linken Seite der Regel ist
für die weiter hinten beschriebene Transformation notwendig.
Pattern können aus Datenvariablen und Datenkonstruktoren bestehen, wie es aus funktionalen Programmiersprachen wie Haskell bekannt ist. In dem vorgestellten Papier werden außerdem noch folgende Konstrukte als Pattern betrachtet: teilweise angewendete
Datenkonstruktoren und teilweise angewandte Funktionen. Diese Pattern werden von
den Autoren HO-Pattern (Higher Order Pattern) genannt, und beispielsweise in der Pro-
12
3 Vorstellung der neuartigen Implementierung von Typklassen
Funktionssymbol:
Datenkonstruktor:
Datenvariable:
program
::=
data
::=
class
::=
inst
::=
type
::=
rule r
::=
pattern t
::=
expression e
::=
f
K
x
data class inst type rule
data C α = K1 τ | . . . | Km τ
class θ ⇒ κ α where f :: τ
instance θ ⇒ κ (C α) where f t → e
f :: θ ⇒ τ
(f :: ρ) t → e
x | K tn
| f tn
x | K | f :: ρ | e e0 | let x = e in e0
t linear
t linear
n ≤ Stelligkeit(K)
n < Stelligkeit(f )
Abbildung 3.1: Syntax der Quellprogramme
grammiersprache TOY1 unterstützt ([SML+ 11]). Ein normales Pattern ist zum Beispiel
Pair 0 ’a’, wenn Pair ein Datenkonstruktor des Typs Int → Char → P air ist, ein
HO-Pattern ist hingegen zum Beispiel Pair 10. Diese Pattern werden im TOY-System
durch einen HO-CRWL (HO Constructor-based conditional ReWriting Logic) genannten
Ansatz unterstützt ([GmHgRA97]). In anderen FL-Programmiersprachen werden HOPattern aber nicht unterstützt. Die vorgestellte Transformation kann aber natürlich auch
ausgeführt werden, wenn HO-Patterns aus der Quellsprache entfernt werden.
Ausdrücke sind ähnlich aufgebaut wie beim λ-Kalkül; dieser wird hier noch um letAusdrücke erweitert. Hier ist wieder anzumerken, dass Funktionen durch ihre überladenen Typen annotiert werden.
Es wird außerdem zugelassen, dass Klassennamen in Kontexten durch ein • markiert
werden. Diese Markierungen sind für die Transformation von Bedeutung. Wie genau die
Markierung durchgeführt wird, wird in Abschnitt 3.3.3 beschrieben.
Die Syntax der Zielprogramme ist ähnlich der Syntax für Quellprogramme, nur dass
keine Klassen- und Instanzdeklarationen mehr vorkommen, Funktionssymbole in Regeln
und Ausdrücken nicht mehr mit Typinformationen annotiert sind, und die Typangaben
in den Typdeklarationen keinen Kontext mehr besitzen.
3.1.1 Annotation der Funktionssymbole
Wie schon vorher erwähnt, müssen, damit die weiter hinten vorgestellte Transformation
funktioniert, alle Funktionssymbole in Regeln und Ausdrücken durch ihren überladenen
Typ annotiert werden. Diese Information muss in einer der Transformation vorhergehenden Typüberprüfungsphase ermittelt werden.
1
http://www.fdi.ucm.es/profesor/fernan/TOY/index.html
13
3 Vorstellung der neuartigen Implementierung von Typklassen
Beispiel 3 Die Funktion g x → eq x [True] wird unter der Annahme, dass der
Typ von eq „hEq ai ⇒ a → a → Bool“ ist, nach der Typprüfung in der vorgestellten Syntax als (g::hi ⇒ (List Bool) → Bool) x → (eq::hEq(List Bool)i ⇒
(List Bool) → (List Bool) → Bool) x [True] geschrieben. Da die Funktion eq
auf Parameter des Typs List Bool angewandt wird, wurde die Typvariable a aus
der Typangabe für eq in diesem konkreten Aufruf der Funktion durch (List Bool)
ersetzt. Der Kontext in der Typannotation von g ist außerdem leer, da er durch
eine Kontextreduktion reduziert wurde, worauf in Abschnitt 3.3.2 näher eingegangen
wird.
3.2 Transformation der Programme
In diesem Abschnitt stelle ich die Transformation vor, die in dem vorgestellten Papier
vorgeschlagen wird, und im nächsten Abschnitt gehe ich darauf ein, welche Vorbereitungen notwendig sind, um die Transformation durchführen zu können.
Die Idee der Transformation ist folgende: Alle überladenen Funktionen werden in typindizierte Funktionen übersetzt, und es werden neue Parameter eingeführt. Diese Parameter nehmen Typrepräsentanten entgegen. Wird eine überladene Funktion aufgerufen,
so wird zusätzlich ein Typrepräsentant übergeben. Dieser bestimmt, welche Variante der
überladenen Funktion ausgeführt wird.
3.2.1 Erzeugung von Typzeugen
Typzeugen sind Datenwerte, die als Repräsentanten von Typen dienen. Damit die Typzeugen denselben Typ haben, den sie repräsentieren, werden alle algebraischen Datentypen um den Typzeugen erweitert.
Beispiel 4 Der algebraische Datentyp data Nat = Z | S Nat wird um den Typzeugen #Nat erweitert: data Nat = Z | S Nat | #Nat, und der algebraische Datentyp data List a = Nil | Cons a (List a) wird durch den Typzeugen #List
a erweitert: data List a = Nil | Cons a (List a) | #List a.
Somit hat zum Beispiel der Typzeuge #List (#List #Nat) genau den Typ, den er
repräsentiert, nämlich List (List N at).
Die Generierung von Typzeugen kann auf einfache Weise formalisiert werden:
Definition 3 (Generierung von Typzeugen) Aus einem gegebenen Typ kann
mit folgender rekursiver Funktion der entsprechende Typzeuge generiert werden:
• testif y(α) = xα
• testif y(C τ1 . . . τn ) = #C testif y(τ1 ) . . . testif y(τn )
Zwei Punkte sind bei der obigen Definition zu beachten:
• Gleiche Typvariablen werden durch gleiche Datenvariablen ersetzt.
14
3 Vorstellung der neuartigen Implementierung von Typklassen
• Für den Funktionstyp τ → τ 0 ist die Funktion testif y nicht definiert. Es wird
davon ausgegangen, dass in der Quellsprache Instanzen über Funktionstypen nicht
möglich sind. Möchte man dies trotzdem zulassen, so kann man einen Konstruktor
Arrow einführen, der den Typ α → β → (α → β) hat und somit den Funktionstyp
repräsentiert.
3.2.2 Haupttransformation
Nachdem nun die Funktion testif y definiert wurde, kann man die gesamte Programmtransformation angeben:
Definition 4 (Transformationsfunktionen)
transprog (data class inst type rule) =
transdata (data) transclass (class) transinst (inst) transtype (type) transrule (rule)
transdata (data C α = K1 τ | . . . | Km τ ) = data C α = K1 τ | . . . | Km τ | #C α
transclass (class θ ⇒ κ α where f :: τ ) = f :: α → τ
transinst (instance θ ⇒ κ (C α) where f t → e) =
f testif y(C α) transexpr (t) → transexpr (e)
transtype (f :: θ ⇒ τ ) = f :: α1 → . . . → αn → τ , wobei α1 . . . αn in θ vorkommen
und durch Klassen eingeschränkt werden, die mit • markiert sind
transrule ((f :: ρ) t → e) = transexpr (f :: ρ) transexpr (t) → transexpr (e)
transexpr (x) = x
transexpr (K) = K
transexpr (f :: ρ) = f testif y(τ1 ) . . . testif y(τn ), wobei ρ ≡ φ ⇒ τ und τ1 . . . τn in φ
vorkommen und durch eine Klasse, die mit • markiert ist, eingeschränkt sind
transexpr (e e0 ) = transexpr (e) transexpr (e0 )
transexpr (let x = e in e0 ) = let x = transexpr (e) in transexpr (e0 )
Die Transformation des gesamten Programmes wird durch die Funktion transprog
durchgeführt; diese wiederum übersetzt die einzelnen Programmteile (Datendeklarationen, Klassen- und Instanzdeklarationen, Typdeklarationen und Regeln).
Im Folgenden werde ich die Transformationen für die einzelnen Programmteile näher
erläutern.
15
3 Vorstellung der neuartigen Implementierung von Typklassen
Transformation der Datentypen
Wie schon in Abschnitt 3.2.1 erläutert, werden alle algebraischen Datentypen um den
jeweiligen Typzeugen erweitert, der als Repräsentant des Datentyps fungiert.
Transformation der Klassendeklarationen
Durch die Transformationsfunktion werden Klassendeklarationen aus dem Quellprogramm entfernt, und es werden im Zielprogramm nur noch Typen für die Klassenmethoden angegeben. Alle Klassenmethoden werden um einen Parameter erweitert, in dem
der Typrepräsentant übergeben wird.
Beispiel 5 Die Typklasse
class Foo a where
foo :: a -> Bool
wird in die folgende Typangabe umgesetzt:
foo :: a -> a -> Bool
Transformation der Instanzdeklarationen
In den Instanzdeklarationen werden die konkreten Implementierungen der überladenen
Funktionen für einen gegebenen Typ angegeben. Jede hier deklarierte überladene Funktion erhält im Zielprogramm einen zusätzlichen Parameter, in dem der Typrepräsentant
übergeben wird.
Jede Regel erhält außerdem als zusätzliches Pattern den Typzeugen für den Typen,
der in der Instanzdeklaration angegeben wurde.
Dadurch werden für eine gegebene Klassenmethode für jeden Typ eine Reihe von
Regeln erzeugt, die als Ganzes eine einzige typindizierte Funktion darstellen. Über das
zusätzlich eingeführte Argument wird beim Aufruf der Funktion ein Pattern Matching
ausgeführt. Der jeweils übergebene Typzeuge bestimmt dabei, welche Regeln ausgeführt
werden.
Beispiel 6 Die Instanzdeklaration
instance Foo (List a) where
foo xs = False
wird in die Funktion
foo (#List xa ) xs = False
umgewandelt. Wird außerdem die Instanzdeklaration
instance Foo (Pair a b) where
foo p = True
angegeben, so wird diese in eine Regel für die gleiche Funktion umgesetzt:
foo (#Pair xa xb ) p = True.
Wird schließlich noch die Instanzdeklaration
instance Foo Int where
foo x = False
angegeben, so wird folgende Regel erzeugt:
16
3 Vorstellung der neuartigen Implementierung von Typklassen
foo #Int x = False.
Insgesamt entsteht also eine typindizierte Funktion mit drei Regeln:
foo (#List xa ) xs = False
foo (#Pair xa xb ) p = True
foo #Int x = False.
Man sieht, dass für eine gegebene Klassenmethode bei jeder Instanzdeklaration für die
Klasse und einen gegebenen Typen die schon vorhandenen Regeln der entsprechenden
Funktion um weitere Regeln ergänzt werden. Die entsprechende Funktion wird dabei
immer mehr erweitert. Man sieht auch, dass über das erste Argument beim Aufruf der
Funktion dann ein Pattern Matching durchgeführt werden kann, wobei der übergebene
Typzeuge gegen die in den Regeln angegebenen Typzeugen gematcht wird.
Transformation der Typdeklarationen
Die Typangaben mit Kontext aus dem Quellprogramm werden in Typangaben ohne
Kontext übersetzt. Dabei werden weitere Parameter erzeugt, die den ursprünglichen Parametern vorangestellt werden. Die Typen der Parameter sind diejenigen Typvariablen,
die im Kontext θ durch Klassen, die mit einem • markiert sind, eingeschränkt sind.
Beispiel 7 Die Typdeklaration f :: hEq • a, Ord a, Eq • bi ⇒ a → b → Bool wird
in folgende Typdeklaration umgewandelt: f :: a → b → a → b → Bool.
Die zusätzlichen Parameter nehmen in den Aufrufen der Funktion die Typzeugen
entgegen.
Transformation der Regeln
Bei Regeln werden einfach alle Komponenten der Regel übersetzt: Bei dem Funktionssymbol, mit dem die Regel beginnt, wird einfach nur die Typannotation entfernt, die
Pattern werden übersetzt und der Ausdruck auf der rechten Seite ebenfalls.
Da in den Pattern nur Funktionen vorkommen, deren Kontext in der Typannotation
leer ist, wird bei diesen Funktionen lediglich der Kontext entfernt.
Transformation der Ausdrücke
Bei der Transformation der Ausdrücke ist lediglich die Transformation von Funktionen
interessant: In einem Ausdruck hat die Funktion einen bestimmten Typ, der durch die
Typinferenz ermittelt wurde, und insbesondere einen gesättigten Kontext besitzt. Im
Kontext steht jetzt also, welchen Typ die Parameter haben müssen, für die die Funktion
überladen wurde. Deshalb müssen die entsprechenden Typzeugen in den zusätzlich generierten Parametern übergeben werden. Wieder werden hier nur die Typen betrachtet,
die im Kontext durch Klassennamen mit einem • eingeschränkt sind.
Ansonsten wird bei der Transformation einfach in den Ausdruck rekursiv hinabgestiegen, und Datenkonstruktoren und -variablen werden unverändert gelassen.
17
3 Vorstellung der neuartigen Implementierung von Typklassen
Beispiel 8 Angenommen, die Funktion f, die schon weiter oben verwendet wurde,
ist in einem Aufruf folgendermaßen annotiert:
f :: hEq • Bool, Ord Bool, Eq • (List Int)i ⇒ Bool → (List Int) → Bool.
Dann wird dieser Ausdruck in folgenden Ausdruck übersetzt:
f #Bool (#List #Int).
3.3 Wichtige Vorbereitungen
Die vorgestellte Programmtransformation wird nach der Typprüfungsphase durchgeführt, im Gegensatz zu der Programmtransformation, die bei dem Wörterbuch-basierten
Ansatz durchgeführt wird; dort ist die Transformation in die Typüberprüfungsphase integriert [HHPJW96, WB89].
Durch die Typüberprüfung muss das Quellprogramm auf die Haupttransformation
vorbereitet werden. Dazu zählt, dass die Typen der Funktionen im Programm ermittelt
werden und im Programm eingefügt werden, dass die Kontexte in diesen Typangaben
soweit wie möglich reduziert werden, und dass in den Kontexten die gewünschten Klasseneinschränkungen durch • -Symbole markiert werden.
3.3.1 Annotation der Funktionssymbole
Wie schon in 3.1.1 erwähnt, muss bei allen Vorkommen von Funktionen der überladene Typ angegeben werden, damit ermittelt werden kann, welche Typzeugen bei einem
konkreten Aufruf übergeben werden müssen.
3.3.2 Kontextreduktion
Häufig ist die Information in den Kontexten redundant, das heißt, es können möglicherweise Klassenbeschränkungen weggelassen werden ([JJM97]). Dieser Kontextreduktion
genannte Vorgang wird durch die folgenden Regeln bestimmt:
• Doppelte Klassenbeschränkungen können weggelassen werden: So wird aus
hEq a, Eq ai hEq ai.
• Instanzdeklarationen mit Kontextangaben können verwendet werden: Gibt es zum
Beispiel eine Instanzdeklaration instance hEq ai => Eq (List a) where ...,
dann kann der Kontext hEq a, Eq (List a)i zu hEq ai reduziert werden.
• Die Subklassenbeziehung kann verwendet werden: Gibt es zum Beispiel eine Klassendeklaration class hEq ai => Ord a where ..., so kann der Kontext
hEq a, Ord ai zu hOrd ai reduziert werden, da jede Instanz von Ord auch eine
Instanz von Eq ist.
18
3 Vorstellung der neuartigen Implementierung von Typklassen
Beispiel 9 Somit wird der Kontext hOrd a, Eq a, Eq (List a)i zu hOrd ai reduziert.
Die Kontextreduktion ist notwendig für die Übersetzung, wie folgendes Beispiel zeigt:
Es sei eine Instanzdeklaration für den Pair-Datentyp gegeben (instance hEq a, Eq bi ⇒
Eq (Pair a, b) where ...) und die folgende Regel: g p1 p2 → ([fst p1, snd p2],
eq p1 p2). Ohne Kontextreduktion wird folgender Typ für g inferiert:
hEq • (P air a a)i ⇒ (P air a a) → (P air a a) → (P air (List a) Bool). Dann würde die übersetzte linke Seite der Regel g (#Pair xα xα ) p1 p2 lauten. Dies ist aber
nicht erlaubt, da für Pattern Linearität gefordert wird; hier jedoch kommt xα zweimal
vor.
Nach der Kontextreduktion (Verwendung der Instanzdeklaration, Entfernung von Duplikaten) lautet der Kontext hEq ai, und die linke Regelseite wird nach g xα p1 p2
übersetzt, was gültig ist.
3.3.3 Markierung der Klassennamen
Nach der Typprüfung und der Kontextreduktion müssen noch die Klassennamen im
Kontext markiert werden. Diese Information bestimmt, wie oben beschrieben wurde, für
welche Typen Typzeugen übergeben werden.
Nach der Kontextreduktion werden im Kontext nur noch Typvariablen durch Klassen
eingeschränkt. Für jede eingeschränkte Typvariable soll genau ein Typzeuge übergeben
werden, auch wenn die Typvariable von mehreren Klassen eingeschränkt wird. Deshalb
wird eines der Vorkommen einer eingeschränkten Typvariable durch • markiert.
Beispiel 10 Angenommen der Kontext lautet hN um a, Ord ai. Dann wird zum
Beispiel das erste Vorkommen von a markiert: hN um• a, Ord ai.
Die Bedeutung der Markierung der Klassennamen ist hier zum Beispiel die folgende:
Da die Typvariable a einmal durch den Kontext N um und einmal durch den Kontext
Ord eingeschränkt wird, kommt sie im Kontext zweimal vor, ihr zugehöriger Typzeuge
muss aber nur einmal übergeben werden.
Die Information, für welche Typen Typzeugen übergeben werden müssen, wird durch
die Markierungen auch in saturierte Kontexte weitergereicht, in denen die Typvariablen
durch andere Typen ersetzt sein können.
3.4 Fallbeispiel
Die vorgestellte Transformation soll jetzt anhand eines größeren konkreten Beispiels
illustriert werden. In dem Beispiel wird die Show-Klasse verwendet, die in Abschnitt
2.1.3 eingeführt wurde.
In Abbildung 3.2 ist das Originalprogramm angegeben. Im Programm werden verschiedene Instanzen der Typklasse Show deklariert, und zwar für die Typen Int, Char,
für Paare und für Listen. Außerdem wird die Funktion print angegeben, die ja schon
aus dem früheren Beispiel bekannt ist, und zwei Beispiele dafür, wie diese Funktion
angewendet werden kann (f1 und f2).
19
3 Vorstellung der neuartigen Implementierung von Typklassen
In Abbildung 3.3 ist das durch die Typannotationen erweiterte Programm angegeben,
und in Abbildung 3.4 ist das transformierte Programm angegeben.
3.5 Optimierungen
Als mögliche Optimierung wurde die Spezialisierung von Funktionen vorgestellt. Dabei
werden aus den typindizierten Funktionen separate Funktionen mit anderem Namen
generiert.
Beispiel 11 Die show Funktion aus dem Beispiel, angewendet auf einen Typzeugen, der eine Liste repräsentiert, kann in folgende spezialisierte Funktion umgesetzt
werden:
s h o w _ l i s t x a ( x : y : ys ) = show x a x ++ " , " ++ s h o w _ l i s t x a ( y : ys )
s h o w _ l i s t x a [ x ] = show x a x
s h o w _ l i s t xa [ ] = " "
Außerdem kann show #Int in show_int, show #Char in show_char, usw. umgesetzt
werden.
Auch überladene Funktionen wie die print-Methode können auf diese Weise spezialisiert werden, wenn sie im Programm mit einem konkreten Typzeugen aufgerufen werden.
c l a s s Show a where
show : : a −> String
instance Show Int where
show x = i n t T o S t r i n g x
instance Show Char where
show c = " ’ " ++ [ c ] ++ " ’ "
instance (Show a , Show b ) => Show ( P a i r a b ) where
show ( a , b ) = " ( " ++ show a ++ " , " ++ show b ++ " ) "
instance (Show a ) => Show ( List a ) where
show ( x : y : ys ) = show x ++ " , " ++ show ( y : ys )
show [ x ] = show x
show [ ] = " "
print : : Show a => a −> IO ( )
print x = putStrLn (show x )
f 1 = print 1
f 2 = print ( 2 , [ ’ c ’ , ’ d ’ ] )
Abbildung 3.2: Originalprogramm
20
3 Vorstellung der neuartigen Implementierung von Typklassen
c l a s s Show a where
show : : a → String
instance Show Int where
show x = ( i n t T o S t r i n g : : hi ⇒ Int → String ) x
instance Show Char where
show c = " ’ " ++ [ c ] ++ " ’ "
instance hShow a, Show bi ⇒ Show (P air a b) where
show ( x , y ) = " ( " ++ (show : : hShow• ai ⇒ a → String ) x
++ " , " ++ (show : : hShow• bi ⇒ b → String ) y ++ " ) "
instance hShow ai ⇒ Show (List a) where
show ( x : y : ys ) = (show : : hShow• ai ⇒ a → String ) x
++ " , " ++ (show : : hShow• (List a)i → List a → String ) ( y : ys )
show [ x ] = (show : : hShow• ai ⇒ a → String ) x
show [ ] = " "
print : : hShow• ai ⇒ a → IO ()
( print : : hShow• ai ⇒ a → IO () ) x =
( putStrLn : : hi ⇒ String → IO () ) ( ( show : : hShow• ai ⇒ a → String ) x )
f 1 = ( print : : hShow• Inti ⇒ Int → String ) 1
f 2 = ( print : : hShow• (P air Int (List Char))i ⇒ (P air Int (List Char)) → String )
(2 , [ ’ c ’ , ’d ’ ] )
Abbildung 3.3: Mit Typinformation annotiertes Originalprogramm
: : a → a → String
#Int x = i n t T o S t r i n g x
#Char c = " ’ " ++ [ c ] ++ " ’ "
(#Pair x a x b ) ( a , b ) =
" ( " ++ show x a a ++ " , " ++ show x b b ++ " ) "
show (#List x a ) ( x : y : ys ) =
show x a x ++ " , " ++ show (#List x a ) ( y : ys )
show (#List x a ) [ x ] = show x a x
show (#List x a ) [ ] = " "
show
show
show
show
print : : a → a → IO ()
print x a x = putStrLn (show x a x )
f 1 = print #Int 1
f 2 = print (#Pair #Int (#List #Char ) ) ( 2 , [ ’ c ’ , ’ d ’ ] )
Abbildung 3.4: Transformiertes Programm
21
4 Diskussion
4.1 Effizienz
Anhand mehrerer Testprogramme wurde im vorgestellten Papier die Effizienz der Programme, die mit der vorgestellten Transformation übersetzt wurden, mit der Effizienz der
Programme, die mit einem Wörterbuch-basierten Ansatz übersetzt wurden, verglichen.1
Das Ergebnis der Untersuchung ist, dass die Programme, die typindizierte Funktionen
benutzen, in allen Fällen mindestens gleich schnell sind wie die Programme mit Wörterbüchern, und in vielen Fällen eine Geschwindigkeitssteigerung festgestellt werden kann;
die beste Geschwindigkeitssteigerung ist eine Steigerung um den Faktor 2,3. Auch nach
der Anwendung von Programmoptimierungen auf beiden Seiten sind die Programme mit
Typzeugen immer noch schneller als die Wörterbuch-basierten.
Der Grund für die Geschwindigkeitssteigerungen ist vor allem, dass beim Wörterbuchbasierten Ansatz die gewünschten Funktionen aus dem Wörterbuch mit Hilfe von
Selektionsfunktionen extrahiert werden müssen, bevor sie angewendet werden können.
Diese Selektion bedeutet einen zusätzlichen Overhead, besonders dann, wenn die Wörterbücher zusätzlich noch verschachtelt sind. So enthält das Wörterbuch einer Subklasse
die Wörterbücher der Oberklassen, und wenn eine Methode einer Oberklasse ermittelt
werden soll, muss zuerst vorher das entsprechende Wörterbuch ausgepackt werden.
4.2 Kein Ergebnisverlust
Der in Abschnitt 2.2.1 beschriebene Verlust von Ergebnissen tritt bei der vorgestellten
Übersetzung nicht mehr auf. Dies liegt daran, wie das in Abschnitt 2.2.1 angegebene
Programm jetzt übersetzt wird:
arb : : a → a
arb #Bool = True
arb #Bool = False
twoArb : : a −> [ a ]
twoArb x a = [ arb x a , arb x a ]
In der Liste, die in twoArb konstruiert wird, stehen nun zwei Methodenaufrufe, die unabhängig voneinander ausgewertet werden. Nun können also beim Aufruf der Funktion
twoArb::List Bool auch die Ergebnisse [False, True] und [True, False] herauskommen.
1
Dabei wurde das Laufzeitsystem TOY benutzt
22
4 Diskussion
4.3 Module und separate Kompilierung
Jede Programmiersprache sollte ein Modulsystem unterstützen, da dies für die Programmierung im Großen benötigt wird. Außerdem ist es wichtig, dass einzelne Module separat
kompiliert werden können. Wenn sich also ein Modul ändert, sollten die anderen nicht
neu kompiliert werden müssen. Dies ist manchmal auch gar nicht möglich, wenn zum
Beispiel der Quellcode eines Moduls nicht vorliegt, und nur das Kompilat des Moduls
vorhanden ist.
Die vorgestellte Transformation unterstützt aber Module und separate Kompilierung
nicht per se. Das Problem ist, dass aus einer Klassenmethode eine typindizierte Funktion
gebildet wird, deren Regeln aber auf mehrere Module verteilt sein können (eine solche
Funktion nennt man offen).
Im Allgemeinen müssen bei jedem Hinzufügen neuer Regeln zu einer offenen Funktion
alle Module neu kompiliert werden, da die neuen Regeln zum Beispiel in das Pattern
Matching mit einbezogen werden müssen. Eine separate Kompilierung ist nur dann möglich, wenn besondere Umstände oder Mechanismen dies unterstützen. Dies ist bei der
Implementierung der Programmiersprache TOY, die die Autoren des vorgestellten Papiers verwenden, der Fall.
4.3.1 Implementierung der separaten Kompilierung bei TOY
Bei der verwendeten Implementierung der Programmiersprache TOY wird nach Prolog
übersetzt. Außerdem ist die Auswertungsstrategie für Funktionen demand-driven, das
heißt, es wird nur das ausgerechnet, was nötig ist, um fortzufahren.
Beispiel 12 Es sei folgende Funktion auf Peano-Zahlen gegeben:
l e q Z y = True
leq (S x) Z = False
leq (S x) (S y) = leq x y
Beim Aufruf der Funktion leq wird zuerst das erste Argument benötigt, um eine Auswahl zwischen der ersten und den beiden anderen Regeln zu treffen. Dafür
wird das Argument bis zur Kopf-Normalform ausgewertet. Wurde das erste Argument nach S x ausgewertet, dann wird zusätzlich das zweite Argument ausgewertet,
um zwischen den letzten beiden Regeln eine Auswahl zu treffen. Wurde hingegen
das erste Argument nach Z ausgewertet, so wird das zweite Argument nicht weiter
ausgewertet, und die erste Regel angewandt.
Diese Auswertung sieht im Kompilat folgendermaßen aus und spiegelt genau dieses
Vorgehen wieder:
l e q (A, B,H) :− hnf (A, HA) , leq_1 (HA, B,H ) .
23
4 Diskussion
leq_1 ( z , B, t r u e ) .
leq_1 ( s (X) ,B,H) :− hnf (B,HB) , leq_1_2 ( s (X) ,HB, H ) .
leq_1_2 ( s (X) , z , f a l s e ) .
leq_1_2 ( s (X) , s (Y) ,H) :− l e q (X, Y,H ) .
Das letzte Argument in den Prädikaten stellt das Ergebnis der Funktion dar; das
Prädikat hnf wandelt das übergebene Argument in Kopf-Normalform um.
Diese Auswertungsstrategie ermöglicht nun die separate Kompilierung: Da alle übersetzten Klassenmethoden den Typzeugen als erstes Argument erhalten, wird bei der
bedarfsgesteuerten Auswertung immer zuerst das erste Argument in Kopf-Normalform
ausgewertet. Dies sieht für die Show-Funktion aus dem Fallbeispiel (Abb. 3.4) folgendermaßen aus:
show (W, A, H) :− hnf (W,HW) , show_1 (HW, A, H ) .
show_1(# i n t , A,H) :− i n t T o S t r i n g (A,H ) .
show_1(#char , A,H) :− . . . .
show_1(# l i s t (WA) ,A,H) :− . . . .
In jedem Modul haben also die show- und show_1-Prädikate dasselbe Schema. Dann
kann aber das Hauptprogramm einfach dadurch gebildet werden, indem alle Kompilate, die ja in der Programmiersprache Prolog vorliegen, aneinandergehängt werden. Es
müssen lediglich die doppelten Regeln für show entfernt werden.
Dadurch wird die separate Kompilierung einzelner Module unterstützt. In anderen
Laufzeitsystemen kann es aber durchaus möglich sein, dass die vorgestellte Transformation die separate Kompilierung einzelner Module nicht unterstützt oder dafür kompliziertere Mechanismen benötigt werden.
4.4 Probleme mit HO-Patterns
Um festzustellen, ob eine gegebene Funktionsapplikation ein HO-Pattern (siehe Abschnitt 3.1) bildet, muss die Stelligkeit der Funktion bekannt sein. Es könnten aber
durchaus in verschiedenen Modulen bei der Implementierung von Klassenmethoden verschiedene Stelligkeiten verwendet werden (z.B. durch η-Reduktion). Deshalb muss explizit festgelegt werden, welche Stelligkeit Klassenmethoden haben. Dies könnte zum
Beispiel in der Klassendeklaration geschehen:
c l a s s Show a where
show/1 : : a → String
Ein weiteres Problem, das schon bereits erwähnt wurde, ist die Verwendung von überladenen Funktionen in HO-Pattern auf der linken Seite von Regeln. Bei der Übersetzung
solcher Konstrukte können nicht-lineare linke Regelseiten entstehen:
24
4 Diskussion
Beispiel 13 Sei f gegeben durch f show = True, wobei show die überladene Funktion aus dem Fallbeispiel sein soll. Dann würde das Programm nach der Typüberprüfung folgendermaßen lauten: f :: hEq • ai ⇒ (a → a → Bool) → bool eq ::
hEq • ai ⇒ a → a → Bool = True, und die übersetzte Regel würde f xa (eq xa )
= True lauten, was aufgrund der Verletzung der Links-Linearität nicht erlaubt ist.
Da keine Lösung für dieses Problem bekannt ist, wird in dem vorliegenden Papier
verboten, dass überladene Funktionen in HO-Pattern auf der linken Seite von Regeln
vorkommen.
25
5 Zusammenfassung
In dem vorgestellten Papier wurde eine Übersetzung von Typklassen vorgestellt, die
eine Alternative zu dem Wörterbuch-basierten Ansatz darstellt. Die vorgestellte Transformation zeigt gewisse Eigenschaften, die unter anderem in der funktional-logischen
Programmierung erwünscht sind. So hat sich gezeigt, dass die Transformation schnellere
Programme generiert. Außerdem wird die Semantik der Call-Time-Choice eingehalten,
was beim Wörterbuch-basierten Ansatz nicht der Fall ist.
Die Transformation setzt eine vorhergehende Typüberprüfung voraus, in der alle vorkommenden Funktionssymbole durch ihren überladenen Typ annotiert werden. Dazu
muss die Typüberprüfung so erweitert werden, dass sie Typklassen berücksichtigt. An
der inferierten Typinformation orientiert sich die Transformation, ohne die Typüberprüfung wäre die Transformation nicht durchführbar.
Die generierten Programme sind im Damas-Milner-Typsystem nicht gültig, sondern
erfordern eine Erweiterung des Damas-Milner Typsystems. Diese Erweiterung ermöglicht
es, überladene Funktionen einfach hinzuschreiben, indem für die unterschiedlichen Typen
die entsprechenden Regeln angegeben werden.
Die Transformation basiert auf der Erstellung von Typzeugen, die einen gewissen Typ
repräsentieren, und im Programm durchgereicht werden. In den überladenen Funktionen
wird anhand der Typzeugen bestimmt, welcher Code ausgeführt wird. Die Typzeugen
werden zu jedem algebraischen Datentyp hinzugefügt.
Für jede Klassenmethode einer Typklasse wird eine typindizierte Funktion generiert.
Für jede Instanziierung der Klassenmethode werden der typindizierten Funktion weitere
Regeln hinzugefügt. Die typindizierte Funktion ist also offen.
Außerdem werden bei der Transformation zusätzliche Parameter in den überladenen
Funktionen eingeführt, in denen die Typzeugen übergeben werden. Welche Parameter
hinzugefügt werden, wird anhand der Typannotation aus der Typüberprüfungsphase bestimmt. Die entsprechende Information wird dabei aus dem Kontext der Typen ermittelt.
Die vorgestellte Transformation unterstützt die separate Kompilierung von Modulen
nicht auf triviale Weise, wie es beim Wörterbuch-basierten Ansatz der Fall ist. In dem
vorgestellten Programmiersystem TOY ist es aufgrund der verwendeten Auswertungsstrategie möglich, Module separat zu kompilieren.
Aufgrund der Einfachheit der Transformation und der erwähnten Vorteile bei funktional-logischer Programmierung stellt diese eine ernstzunehmende Alternative für die Implementierung von Wörterbüchern dar.
26
Literaturverzeichnis
[Aug93]
Augustsson, Lennart: Implementing Haskell overloading. In: Proceedings of the conference on Functional programming languages and computer architecture. New York, NY, USA : ACM, 1993 (FPCA ’93). –
ISBN 0–89791–595–X, 65–73
[DM82]
Damas, Luis ; Milner, Robin: Principal type-schemes for functional
programs. In: Proceedings of the 9th ACM SIGPLAN-SIGACT symposium on Principles of programming languages. New York, NY, USA :
ACM, 1982 (POPL ’82). – ISBN 0–89791–065–6, 207–212
[GmHgRA97] Gonzalez-moreno, J. C. ; Hortala-gonzalez, M. T. ; RodríguezArtalejo, Mario: A Higher Order Rewriting Logic for Functional Logic
Programming. In: ICLP, MIT Press, 1997, S. 153–167
[HHPJW96]
Hall, Cordelia V. ; Hammond, Kevin ; Peyton Jones, Simon L. ;
Wadler, Philip L.: Type classes in Haskell. In: ACM Trans. Program.
Lang. Syst. 18 (1996), März, Nr. 2, 109–138. http://dx.doi.org/10.
1145/227699.227700. – DOI 10.1145/227699.227700. – ISSN 0164–0925
[HL07]
Hinze, Ralf ; Löh, Andres: Generic programming, now! In: Proceedings
of the 2006 international conference on Datatype-generic programming.
Berlin, Heidelberg : Springer-Verlag, 2007 (SSDGP’06). – ISBN 3–540–
76785–1, 978–3–540–76785–5, 150–208
[HPJW+ 92]
Hudak, Paul ; Peyton Jones, Simon ; Wadler, Philip ; Boutel, Brian ; Fairbairn, Jon ; Fasel, Joseph ; Guzmán, María M. ; Hammond,
Kevin ; Hughes, John ; Johnsson, Thomas ; Kieburtz, Dick ; Nikhil,
Rishiyur ; Partain, Will ; Peterson, John: Report on the programming language Haskell: a non-strict, purely functional language version
1.2. In: SIGPLAN Not. 27 (1992), Mai, Nr. 5, 1–164. http://dx.doi.
org/10.1145/130697.130699. – DOI 10.1145/130697.130699. – ISSN
0362–1340
[JJM97]
Jones, Simon P. ; Jones, Mark ; Meijer, Erik: Type Classes: An Exploration of the Design Space. In: In Haskell Workshop, 1997, S. 1 –
16
[LFMMRH10] López-Fraguas, Francisco ; Martin-Martin, Enrique ; RodríguezHortalá, Juan: Liberal typing for functional logic programs. In: Pro-
27
Literaturverzeichnis
ceedings of the 8th Asian conference on Programming languages and systems. Berlin, Heidelberg : Springer-Verlag, 2010 (APLAS’10). – ISBN
3–642–17163–X, 978–3–642–17163–5, 80–96
[Lux]
Lux, W.: Type-classes and call-time choice vs. run-time choice. Post to
the Curry mailing list. http://www.informatik.uni-kiel.de/~curry/
listarchive/0790.html
[Lux08]
Lux, Wolfgang: Adding Haskell-style Overloading to Curry. In: 25.Workshop der GI-Fachgruppe „Programmiersprachen und Rechenkonzepte“,
2008, S. 67–76
[Mil78]
Milner, Robin: A theory of type polymorphism in programming. In:
Journal of Computer and System Sciences 17 (1978), S. 348–375
[MM11]
Martin-Martin, Enrique: Type classes in functional logic programming. In: Proceedings of the 20th ACM SIGPLAN workshop on Partial
evaluation and program manipulation. New York, NY, USA : ACM, 2011
(PEPM ’11). – ISBN 978–1–4503–0485–6, 121–130
[PJ93]
Peterson, John ; Jones, Mark: Implementing type classes. In: Proceedings of the ACM SIGPLAN 1993 conference on Programming language
design and implementation. New York, NY, USA : ACM, 1993 (PLDI
’93). – ISBN 0–89791–598–4, 227–236
[SML+ 11]
Sánchez, Purificación A. ; Martín, Sonia E. ; Leiva, Antonio J. F.
; Luezas, Ana G. ; Fraguas, Francisco J. L. ; Artalejo, Mario R.
; Pérez, Fernando S.: TOY - A Multiparadigm Declarative Language.
2.3.2. Universidad Complutense de Madrid, October 2011. http://www.
fdi.ucm.es/profesor/fernan/TOY/index.html
[WB89]
Wadler, P. ; Blott, S.: How to make ad-hoc polymorphism less ad
hoc. In: Proceedings of the 16th ACM SIGPLAN-SIGACT symposium
on Principles of programming languages. New York, NY, USA : ACM,
1989 (POPL ’89). – ISBN 0–89791–294–2, 60–76
28
Herunterladen