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