Haskell Typklassen Seminararbeit Studiengang Informatik (M) an der Hochschule Karlsruhe von Yves Fischer Sommersemester 2013 Matrikelnummer Betreut durch 39645 Prof. Dr. Martin Sulzmann Zusammenfassung Diese Ausarbeitung soll dem Leser eine Einführung in Typklassen und deren Funktionsweise in Haskell geben. Neben der grundsätzlichen Funktionsweise werden die verschiedenen Ausprägungen und die damit verbundenen Möglichkeiten erörtert. Abschliessend wird ein Vergleich mit C++ und den ursprünglich für C++11 geplanten Concepts angestellt. Inhaltsverzeichnis 1. Motivation 1 2. Das Konzept der Typklassen 2.1. Einführung in Typklassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2. Übersetzung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 4 5 3. Zoo der Typklassen 3.1. Single parameter Typklassen 3.2. Multi parameter Typklassen . 3.3. Constructor Classes . . . . . 3.4. Functional Dependencies . . . . . . 15 15 17 18 19 4. Vergleich mit C++ 4.1. C++ Objektorientierung und Haskell Typklassen . . . . . . . . . . . . . . . . 4.2. Funktionsüberladung in C++ als Gegensatz zu Typklassen . . . . . . . . . . . 4.3. C++ Concepts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20 21 21 22 5. Fazit 28 A. Literatur 29 B. Quellcodes B.1. Temperatureinheiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . B.2. EXP Interpreter mit Einheiten . . . . . . . . . . . . . . . . . . . . . . . . . 31 31 32 C. Abbildungsverzeichnis 34 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1. Motivation 1. Motivation Die meisten Programmiersprachen kennen das Konzept von Typen. Der Typ bestimmt ob es sich bei Daten um eine Ganzzahl, eine Fließkommazahl usw. oder auch eine Funktion handelt. Statisch getypte Sprachen erzwingen die Einhaltung ihrer Typregeln bei der Übersetzung. Dynamische Sprachen erlauben mehr Freiheiten und verlagern die Typprüfung in die Laufzeit des Programms. Der Compiler einer statisch getypten Sprache kann über Typregeln Fehler schon während der Übersetzung erkennen. Dies könnte zum Beispiel das Dividieren einer Funktion (math.sin / 40) oder die falsche Anwendung einer arithmetische Operation (a="Foo"; b=a+1) sein. Trotz der Striktheit einer statisch getypten Programmiersprache erwartet der Programmierer dennoch Flexibilität, sodass es beispielsweise möglich sein soll eine Addition mit Ganzzahlen 42 + 32 genauso zu schreiben wie Addition mit Fliesskommazahlen 42.32 + 32.23. Es muss also möglich sein einen Operator (oder allgemein eine Funktion) mehrdeutig zu verwenden. Hierzu existieren verschiedene Ansätze, im Allgemeinen spricht man von (Typ)Polymorphismus. Polymorphismus ist eine Voraussetzung für die Generische Programmierung. Generische Programmierung (lat. genus, die Art, die Klasse) beschreibt eine Methode der Programmierung, bei der ein Programm durch Parametrisierung flexibel gestaltet wird ohne jedoch die Typisierung zu beeinträchtigen. Polymorphismus wird unterteilt in parametrischen Polymorphismus und Adhoc Polymorphismus. Die Identitätsfunktion id mit der Signatur (α) ⇒ α ist parametrisch polymorph, da für das Argument α ein beliebiger Typ eingesetzt werden kann. Ebenso die Funktion zur Anwendung einer anderen Funktion auf die elemente einer Liste: map ( (α) ⇒ β ) → [α] ⇒ [β ] Hier wird als erstes Argument eine Funktion übergeben die einen Wert von Typ α in einen Wert von Typ β verwandelt. Das zweite Argument ist eine Liste über welche die Funktion angewendet werden soll. Das Ergebnis ist eine Liste mit Elementen vom Typ β . Dieser Algorithmus kennt keine Eigenschaften der Daten vom Typ α oder β . Die einzige Bedingung an die Argumente ist, dass wenn für das erste α bspw. Int gilt, es für weitere α ebenfalls gilt. Nicht nur auf Funktionenstypen, auch auf Daten selbst ist parametrischer Polymorphismus anwendbar. Die Darstellung einer Liste mit Elementen vom Typ α als Liste von α ([α]) kann in Haskell auch mit einem Datentyp als einfach verkettete Liste realisiert werden: Listing 1: parametrischer Listen Typ und map()-Funktion -- Eng: cons-ecutive: aneinandergrenzend; Nil: Null data List a = Cons a (List a) | Nil deriving (Show) 1 1. Motivation mapL :: (a -> b) -> List a -> List b mapL _ Nil = Nil mapL f (Cons x xs) = Cons (f x) (mapL f xs) -- Anwendung: Main> mapL (\x -> x*2) (Cons 1 (Cons 2 (Cons 4 Nil))) Cons 2 (Cons 4 (Cons 8 Ende)) Main> mapL (\x -> x++"z") (Cons "a" (Cons "b" Nil)) Cons "az" (Cons "bz" Nil) In der Anwendung zu diesem Beispiel kommt der Ausdruck x*2 vor. Der Multiplikationsoperator (oder die infix Funktion) * wird hier als Funktion vom Typ Int → Int ⇒ Int verwendet (Nimmt zwei Int und gibt ein Int zurück). Im Allgemeinen sollte natürlich auch möglich sein * mit Fliesskommazahlen zu verwenden (wie 2.5*3.0). Auf den ersten Blick ist es daher naheliegend * parametrisch polymorph und so für jeden Datentyp einsetzbar zu machen. mul :: a -> a -> a mul x y = x ∗ y Wie bereits erwähnt wurde verliert man mit der Verwendung von parametrisch polymorphen Typen die Information über den tatsächlich vorliegenden Typ (dieser steht ja nicht fest). Somit ist der Typ von x unbekannt. Wenn der Typ unbekannt ist, dann können keine typ-spezifischen Funktionen aufgerufen werden, also nur ebenfalls parametrisch polymorphe Funktionen. Darum ist eine Implementierung der Multiplikation mit parametrischem Polymorphismus nicht möglich. Gesucht ist daher eine Möglichkeit, dass für mehrere bestimmten Typen (z.B. Int,Float) eine Methode (z.B. Operator *) unterschiedlich definiert werden kann. Dies ist das Merkmal für die Unterstützung von sogenanntem Adhoc Polymorphismus. Eine Möglichkeit wäre es den Operator für die Verwendung mit verschiedenen Typen zu überladen. Es gäbe dann eine Implementierung Operator* für Int → Int ⇒ Int wie für Float → Float ⇒ Float und weitere. In Haskell ohne Typklassen ist es nicht möglich eine Funktion direkt für verschiedene Typen zu überladen. Darum hierzu das folgende Beispiel in C++. Listing 2: Adhoc Polymorphismus mit Überladung in C++ #include <iostream> struct C { int x,y; }; int mul(int a, int b) { return a * b; } double mul(double a, double b) { return a * b; } C mul(C a, C b) { return C({a.x*b.x, a.y*b.y}); } template<typename A, typename B> auto foo(A a, B b) -> decltype(mul(a,b)) { 2 2. Das Konzept der Typklassen return mul(a,b); } int main() { std::cout << mul( C({3,2}), C({2,3}) ).x << "\t" << mul(2,3) << "\t" << mul(2.5,2.4) << std::endl; return 0; } In der Funktion foo(a,b) sind die Typparameter A und B unabhängig und damit existieren theoretisch nm = 23 (zwei Parameter mit drei möglichen Typen) mögliche Versionen dieser Funktion[18, S. 2]. Ein weiterer Grund warum dieses Vorgehen in Haskell nicht genutzt wird ist, dass dieser Polymorphismus alleine keine Klassenbildung zulässt und damit keine Beschreibung von Datentypen zulässt. Daher wird in C++ decltype benötigt um den Rückgabetyp zu beschreiben. Ohne decltype() wäre das Beispiel in C++ nicht umsetzbar da mul wiederum überladen ist und somit die Rückgabe entweder ein struct C oder ein double ist — bzw. irgendetwas unbestimmt anderes sein könnte. Mittels Überladung allein ist es also nicht möglich „Meta-Typen”(Klassen) zu bilden und diese als Prädikat für die Typisierung der Argumente/Rückgabe zu verwenden. Eine Klasse könnte sein: jeder Typ welcher die Multiplikation implementiert. Neben der Überladung von Funktionen ist mit Haskell Typklassen die Bildung solcher Klassen möglich. Die syntaktischen und semantischen Regeln werden im folgenden Abschnitt vorgestellt. Weiter wird auf die verschiedenen Ausprägungen bei der Verwendung von Typklassen eingegangen. Abschließend soll ein Vergleich mit dem C++0x Concepts Entwurf die Gemeinsamkeiten zwischen Typklassen und Concepts hervorheben. 2. Das Konzept der Typklassen Haskell verwendet ein statisch typisiertes polymorphes Typsystem (Hindley/Milner). Wadler und Blott [18] beschreiben eine Möglichkeit Adhoc Polymorphismus mittels eines Preprozessor umzusetzen, sodass überladene Funktionen mittels parametrischen Polymorphismus darstellbar und damit konform zum Hindler/Milner Typsystem sind. Jedes Programm mit Typklassen lässt sich also zur Kompilierzeit in ein Programm ohne Typklassen übersetzen. 3 2. Das Konzept der Typklassen 2.1. Einführung in Typklassen In der Einleitung wurden die Einschränkungen von statisch typisierten Sprachen am Beispiel von Operatoren auf Zahlen beschrieben. Diese Einschränkungen lassen sich mit Adhoc Polymorphismus über die Haskell Typklassen beheben. Es wurde das Problem eines für mehrere Typen verwendbaren (polymorphen) Multiplikationsoperator * gezeigt und warum parametrischer Polymorphismus keine Lösung bietet. Mit einer Typklasse kann die Multiplizierbarkeit eines Typs beschrieben werden: {-# LANGUAGE NoImplicitPrelude #-} import Prelude(Int) -- imports fuer implementierung import GHC.Base(timesInt) -- von (*) fuer Int class Multipliable a where (*) :: a -> a -> a NoImplicitPrelude verhindert das automatischen Einbinden der Prelude, eine Standardbibliothek für Haskell. Dort wäre der Operator ∗ bereits definiert. Die Typklasse sagt erst einmal nicht mehr aus, als das es Typen geben kann, für die eine infix Funktion * mit zwei Argumenten und einem Rückgabewert von genau diesem Typ definiert ist. Um die Mitgliedschaft eines Typs in der Klasse Multipliable festzulegen, wird eine Instanz der Klasse angelegt. Für den bereits im Haskell Prelude definierten Typ Int würde dies, unter zuhilfenahme der Funktion timesInt (eine primitive, von Haskell Compiler GHC definierte Funktion), dann wie folgt aussehen: instance Multipliable Int where (*) x y = timesInt x y Genauso kann die Multiplikation jetzt aber auch für einen selbst definierten Vektoren Datentyp durchgeführt werden: data Vector = Vector Int Int instance Multipliable Vector where (*) (Vector ax ay) (Vector bx by) = (Vector (timesInt ax bx) (timesInt ay by)) Die Multipliable Klasse ermöglicht es so die Funktion * als Operator für alle Typen zu verwenden für die eine Implementierung, d.h. instance, der Klasse vorliegt. 2.1.1. Eingeschränkte Typen Möchte man eine polymorphe Funktion schreiben die nicht parametrisch polymorph ist, sondern nur für die Typen einer bestimmten Typklasse anwendbar ist, dann lässt sich dies mit mit eingeschränkten Typen ausdrücken. 4 2. Das Konzept der Typklassen mysum :: Num a => [a] -> a -- Num ist eine Typklasse aus der Haskell -- Prelude mit arithmetischen Funktionen mysum [] = 0 -- 0 ist das neutrale Element fuer Num mysum (x:xs) = x + (mysum xs) Num a => ... ist ein Prädikat auf die freie Typvariable a und wird Kontext genannt. Durch die Einschränkung können nur die in der Typklasse definierten Funktionen verwendet werden, in diesem Fall +. 2.1.2. Hierarchie von Typklassen Typklassen können miteinander in Beziehung gesetzt werden, sodass es möglich ist Hierarchien (azyklische Graphen) abzubilden. Dazu wird die Typvariable (hier a) einer Klasse mit einem Prädikat beschränkt. So wurde ausgedrückt, dass Num die Typklasse Eq beinhalten muss. Num ist damit eine Subklasse von Eq. {-# LANGUAGE NoImplicitPrelude #-} import Prelude(Bool,Int) import GHC.Base(plusInt,minusInt,eqInt) class Eq a where (==) :: a -> a -> Bool class Eq a => Num a where (+) :: a -> a -> a (-) :: a -> a -> a Die Instanzierung von Num mit Int wird daher verweigert: instance Num Int where ←- No instance for (Eq Int) arising from the superclasses of an instance declaration Possible fix: add an instance declaration for (Eq Int) In the instance declaration for ‘Num Int’ (+) x y = x ‘plusInt‘ y (-) x y = x ‘minusInt‘ y Solange für Int keine Eq Instanz existiert: instance Eq Int where (==) x y = x ‘eqInt‘ y 2.2. Übersetzung Die Übersetzung verfolgt das Ziel das Haskell Programm ohne die Typklassen Konstrukte class und instance darstellen zu können. 5 2. Das Konzept der Typklassen Im Folgenden soll anhand eines Beispiels gezeigt werden wie Adhoc Polymorphismus möglich ist und wie darauf aufbauend der Übersetzungsalgorithmus von Wadler und Blott funktioniert. Dazu wird eine show Funktion, d.h. die Konvertierung des Werts zu einer lesbaren Zeichenkette (String), implementiert werden. In diesem Beispielprogramm gibt es zwei Typen die vollkommen unabhängig in ihrer Struktur und ihrem Einsatzzweck sind: TrafficLight und Vector. data TrafficLight = Red | Yellow | Green data Vector = Vector Int Int Beide können mit den folgenden zwei Funktionen unabhängig in ein String konvertiert werden: trafficLightString :: TrafficLight -> String trafficLightString t = case t of Red -> "Red" Yellow -> "Yellow" Green -> "Green" vectorString :: Vector -> String vectorString (Vector x1 x2) = "("++(show x1)++","++(show x2)++")" Um zu erreichen, dass beide Typen mit ein und derselben Funktion in eine Zeichenkette konvertiert werden können, wird diese Funktion parametrisch polymorph gestaltet. Das heißt anstatt einen konkreten Typ anzugeben (Typen beginnen in Haskell immer mit einem großen Buchstaben) wird ein universeller Typparameter (z.B. a) eingesetzt. Zusätzlich wird eine Funktion übergeben die einen Wert vom Typ a in einen String übersetzt. In diesem Fall trafficLightString oder vectorString. -- "overloaded" function show’ :: (a -> String) -> a -> String show’ func d = func d Offensichtlich kann diese Funktion jetzt für alle Typen für die eine passende „a -> String” Funktion existiert angewendet werden: *Main> show’ trafficLightString Red "Red" *Main> show’ vectorString (Vector 23.4 45.6) "(23.4,45.6)" Nicht jedoch mit einer für den jeweiligen Typ unpassenden Funktion: *Main> show’ vectorString Red←vectorString ist nicht anwendbar für Red aus TrafficLight *Main> show’ trafficLightString (Vector 23.4 45.6)←trafficLightString ist nicht anwendbar für Vector 6 2. Das Konzept der Typklassen 2.2.1. Method Dictionaries Eine Typklasse besteht aus einer Menge von Funktionen. Sie lässt sich auch als Datentyp darstellen. Die Datenstruktur wird Dictionary genannt, die Position im Datentyp ist dabei der Schlüssel, die Funktion ist der Wert. Als Typklasse: class Num a where (+) :: a -> a -> a (-) :: a -> a -> a Als Method Dictionary: data NumD a = NumD (a -> a -> a) (a -> a -> a) Um Typklassen abzubilden werden den Funktionen, welche den polymorphen Typ verwenden, nicht nur der jeweilige Wert übergebene sondern dazu jeweils noch das passende „Method Dictionary”. Dieser Schritt ist vom Compiler einzufügen. 2.2.2. Beispiel: Method Dictionary Das Beispiel aus Wadler und Blott [18, S. 4] veranschaulicht (Abbildung 1) den Übersetzungsvorgang. Die Zahlen 1 8 markieren die Entsprechungen im Quell- bzw. Zielcode. Im folgenden werde diese Markierungen im einzelnen erläutert. 1 Die Typklasse Num wird als Dictionary im Datentyp NumD gespeichert. Die Funktionen der Typklasse , 2 3 und 4 werden in gewöhnliche toplevel Funktionen überführt, die jedoch nur den Zweck haben aus dem Dictionary die passende Funktion auszuwählen. Funktion 5 und 6 definieren die Dictionaries für die instance Num Int bzw. instance Num Float. Der Typeinschränkung Num a von square 7 wird so übersetzt, dass square’ als ersten Parameter ein Dictionary NumD a erwartet, sodass z.B. für Int ein NumD Int übergeben werden muss. Somit ist sichergestellt, dass kein Typ übergeben werden kann für den die Klasse Num nicht implementiert ist. In den Aufruf von square in test 8 wird das passende NumD Int durch Referenzierung von numDInt 5 vom Compiler eingefügt. 2.2.3. Übersetzungsregeln Für die Umsetzung in einem Haskell Compiler wurden die Übersetzungsregeln weiter formalisiert. An einem kleinen Beispiel können die formalen Übersetzungsregeln aus Hall u. a. [5] einfach dargestellt werden. Für einen Haskell Ausdruck: eq (1::Int)(2::Int):: Bool sei die Typklasse Eq mit einer Instanz für Int definiert: 7 2. Das Konzept der Typklassen class Num a where 1 data NumD a = NumDict (a -> a -> a) 1 (+) :: a -> a -> a 2 (a -> a -> a) (*) :: a -> a -> a 3 (a -> a) negate :: a -> a 4 add (NumDict a m n) = a 2 instance Num Int where 5 mul (NumDict a m n) = m 3 neg (NumDict a m n) = n 4 (+) = addlnt (*) = mullnt numDInt :: NumD Int 5 negate = negInt numDInt = NumDict addlnt mullnt negInt instance Num Float where 6 (+) = addFloat numDFloat :: NumD Float 6 (*) = mulFloat numDFloat = NumDict addFloat mulFloat negFloat negate = negFloat square :: Num a => a -> a 7 square’ :: NumD a -> a -> a 7 square x = x * x square’ numDa x = mul numDa x x test :: Int 8 test :: Int 8 test = square 2 test = square’ numDInt 2 Abbildung 1: Übersetzung nach Wadler und Blott [18] class Eq a where eq :: a -> a -> Bool instance Eq Int where eq = primEqInt Die Funktion primEqInt sei als primitive Funktion vordefiniert. Desweiteren sind die Typen Bool und Int mit Kind ? (d.h. ohne weitere Parameter) verfügbar. Damit existiert eine initiale Umgebung E0 (Abbildung 3). Die (PROG) Regel (Abbildung 2) schreibt vor, dass zuerst Klassendefinitionen, dann Instanzen und dann Ausdrücke ausgewertet werden. Daher wird in Abbildung 4 zuerst die (CLASS) Regel angewendet. Die Klasse Eq: E ` classdecl Die (CLASS) Regel wird angewendet (Abbildung 4). (1),(2) Für die Typvariable a der Klasse Eq wurde keine Einschränkung (Kontext) definiert. (3) Die Typvariable a wird in AE aufgenommen. 8 2. Das Konzept der Typklassen classdecls ` (1) E classdecls : DE ; bindsetC instdecls (2) E ⊕ DE ⊕ IE ` exp (PROG) instdecls : IE ; bindsetI (3) E ⊕ DE ⊕ IE ` exp : τ ; exp program E ` classdecls ; instdecls ; exp : (DE ⊕ IE, τ) ; letrec bindsetC ; bindsetI in exp Abbildung 2: Die (PROG) Regel AE = {} TE = {Bool : 0, Int : 0} CE = {} IE = {} E0 = LIE = {} V E = {primEqInt : (Int → Int → Bool)} LV E = {} Type variable environment Type constructor environment Type class environment Instance environment Local instance environment Variable environment Local variable environment Abbildung 3: Die Initiale Umgebung E0 (4) Die Typen der Funktionen in Eq sind gültig. Da a in AE aufgenommen wurde ist (a → type a → Bool) gültig. Die E ` Regel wird hier nicht tiefer verfolgt. (5) siehe (1). (6) Für eq wird ein Eintrag für die Variable Environment (V E) erstellt. Da die Klasse Eq ohne Kontext für a und mit nur einer Funktion definiert wurde, gestaltet sich die Anwendung der Regel sehr einfach. Die resultierende Funktion eq wählt aus einem Method Dictionary lediglich den ersten (von insgesamt nur einem) Eintrag aus. Die (CLASS) Regel fügt in die Umgebung CE einen Eintrag für die Klasse ein und in die V E ein Eintrag für die Funktion eq. Daraus ergibt sich dann die Umgebung E1 (Abbildung 5). Die Klasseninstanz: E ` instdecls Nach dem die class Deklaration abgearbeitet wurde wird die instance Deklaration angewendet (Abbildung 6) woraus die Umgebung E2 resultiert (Abbildung 7). Die Bedingungen für (INST) werden wie folgt angewendet: (1) Stellt fest, dass der Typ auf den in der Instanz parametriert wird (τ) die Form χα1 ...αk hat. Der Typ ist Int und somit ist der Typkonstruktor χ = Int. Int hat Kind ∗ daher ist k = 0 und α1 ...αk = (). 9 2. Das Konzept der Typklassen Das Symbol ⇓ leitet das Ergebnis der praktischen Anwendung der Regel ein (1) θ = hκ1 α, ..., κl αi (2) (CE of E) κi (1 ≤ i ≤ l) (3) AE = {α} ⇓ () = hi ⇓ 0/ ⇓ AE = {a} type type (4) E ⊕ AE ` τi (1 ≤ i ≤ n) (5) LIE = {dvar1 : κ1 α, ..., dvarn : κn α} (6) LVE = {var1 : τ1 , ..., varn : τn } ⇓ E ⊕ AE ` (a → a → Bool) ⇓ LIE = {} ⇓ LVE = {eq : a → a → Bool} classdecl ` E class θ ⇒ κ α where var1 : τ1 , ..., varn : τn ⇓ classdecl ` class () ⇒ Eq a where eq : a → a → Bool : CE {κ : ∀α.θ ⇒ LVE}, {Eq : ∀a.() ⇒ {eq : (a → a → Bool}} IE = ∀α.hκαi ⇒ LIE, ⇓ {} VE ∀α.hκαi ⇒ LVE {eq : ∀a.Eq a(a → a → a → Bool)}} ; dvar1 = λ dvar : κα . projectm+n dvar; ... dvarm = λ dvar : κα . projectm+n dvar; m 1 m+n m+n var1 = λ dvar : κα . projectm+1 dvar; ... varn = λ dvar : κα . projectm+n dvar; ⇓ eq = λ dvar : Eq a . project11 dvar; E0 Abbildung 4: CLASS Regel auf Beispiel angewendet AE = {} TE = {Bool : 0, Int : 0} CE = {Eq : ∀a.() ⇒ {eq : (a → a → Bool}} IE = {} E1 = LIE = {} eq : ∀a.Eq a(a → a → a → Bool), VE = primEqInt : (Int → Int → Bool) LV E = {} Abbildung 5: Umgebung E1 (2) Daher muss für den Typ τ in der Type constructor Environment (T E) ein Eintrag mit dem Wert k vorhanden sein. In diesem Fall existiert ein Eintrag {Int:0}. (4),(5),(6) Nicht relevant da der Kontext θ im Beispiel leer ist. (7) Überprüft ob die Klasse κ/Eq in der Type Class Environment(CE) existiert. (8),(9) Nicht relevant da der Kontext θ im Beispiel leer ist. 10 2. Das Konzept der Typklassen (1) (2) (3) (4) (5) (6) (7) (8) τ = χα1 ...αk (TE of E) χ =k AE = {α1 , ..., αk } θ 0 = hκ10 τ10 , ..., κl0 τl0 i (CE of E) κi (1 ≤ i ≤ l) AE τi0 (1 ≤ i ≤ l) (CE of E) κ = ∀α.hκ1 α, ..., κm αi ⇒ {var1 : τ1 , ..., varn : τn } LIE ={dvar1 : κ10 τ10 , ..., dvarl : κl0 τl0 } dict (9) dict E ⊕ AE ⊕ LIE ` κi τ ; dexpi (1 ≤ i ≤ m) ⇓ E ⊕ AE ⊕ LIE ` () ; () exp exp (10) E ⊕ AE ⊕ LIE ` expi : τi [τ/α] ; expi (1 ≤ i ≤ n) ⇓ E ⊕ AE ⊕ LIE ` primEqInt : (α → α → Bool)[Int/α] ; primEqInt instdecl E ⇓ τ = Int() ⇓ Int = 0 ∈ {Bool : 0, Int : 0} ⇓ AE = {} ⇓ θ 0 = hi ⇓ 0/ ⇓ 0/ ⇓ (CE of E) Eq = ∀α.hi ⇒ {eq : α → α → Bool} ⇓ LIE = {} ` instance θ ⇒ κτ where var1 = exp1 ...varm = expm ⇓ instdecl E1 ` instance () ⇒ Eq Int where eq = primEqInt : IE = {dvar : ∀α1 ...αk .θ ⇒ κτ} ⇓ {dictEqInt : Eq Int} ; dvar = Λα1 ...αk . λ dvar1 : k10 τ10 .λ dvarl : kl0 τl0 . h1 , ..., dexpm , exp1 , ..., expn i ⇓ dictEqInt = hprimEqInti Abbildung 6: INST Regel auf Beispiel angewendet (10) Überprüft ob die für eq eingesetzte Funktion gültig ist. Im von der Klasse vorgegebene Typ (τi = α → α → Bool) wird dazu α durch das τ der Instanz (hier Int) ersetzt. Der Beispielausdruck: E ` exp Der Ausdruck eq (1::Int)(2::Int):: Bool wird in Abbildung 8 übersetzt. Der Einfachheit halber wird in der ersten Regel (COMB) so getan, als ob eq den Typ (Int, Int) → Bool hätte. Dies verkürzt die Auflösung und ändert nichts am Ergebnis. Über (REL)→(SPEC)→(TAUT) wird aus V E die Bindung zur parametrisch polymorphen eq-Funktion ermittelt. Auf zweitem Weg ermittel (REL) über (DICT-REL)→(DICT-SPEC)→(DICT-TAUT-IE) das passende Dictionary für Int. 11 2. Das Konzept der Typklassen AE = {} TE = {Bool : 0, Int : 0} CE = {Eq : ∀a.() ⇒ {eq : (a → a → Bool}} IE = {dictEqInt : Eq Int} E2 = LIE = {} eq : ∀a.Eq a(a → a → a → Bool), VE = primEqInt : (Int → Int → Bool) Abbildung 7: Umgebung E2 Das Ergebnis: E ` program Die vom Beispiel abgeleitete Übersetzung ergibt ausgehend von (PROG) (Abbildung 2 S.9) das folgende Programm in der Zielsprache[5, §3.2] mit der Umgebung E2 (Abbildung 7): letrec eq = λ dvar : Eq a.project11 dvar; dictEqInt = <primEqInt>; in eq dictEqInt 1 2 12 2. Das Konzept der Typklassen n Die Nummer n in ; gibt die Regel an in der die Bedingung aufgelöst wird exp exp E ` exp : τ 0 → τ (2) ; exp exp (COMB) ⇓ E2 ` eq : (Int, Int) → Bool exp ; (eq dictEqInt) (TAUT) E ` exp0 : τ 0 ; exp0 ⇓ E2 ` (1, 2) : (Int, Int) ; (1, 1) exp (1) E ` (exp exp0 ) : τ ; (exp exp0 ) ⇓ exp E2 ` (eq (1, 2)) : Bool ; (eq dictEqInt (1, 2)) over−exp ` E ; exp dict (REL) over−exp var : hκ1 τ1 , κm τm i ⇒ τ ⇓ E2 E ` κi τi ; dexpi exp ` eq : hEq Inti ⇒ Bool (3) dict ; eq (4) (1 ≤ i ≤ m) ⇓ E2 ` Eq Int ; dictEqInt (2) E ` var : τ ; exp dexp1 ...dexpm ⇓ exp E2 ` eq : (Int → Int → Bool) ; eq dictEqInt poly−exp poly−exp ` E var : ∀α1 ...αk . θ ⇒ τ ⇓ E2 ; var type (SPEC) E ` τi ` eq : Eq Int (TAUT) (1 ≤ i ≤ k) ⇓ 0/ ; eq over−exp E ` var : θ ⇒ τ[τ1 /α1 , ..., τk /αk ] ; var τ1 ...τk ⇓ over−exp E2 ` eq : Eq Int ; eq exp Abbildung 8: Die E ` und E over−exp ` 13 Regeln auf Beispiel angewendet (3) 2. Das Konzept der Typklassen over−dict ` E over−dict hκ1 τ1 , ..., κn τn i ⇒ κτ ⇓ E2 ; dexp dict (DICT-REL) ` hi ⇒ Eq Int (5) E ` κi τi ; dexpi (1 ≤ i ≤ n) ⇓ 0/ ; dictEqInt (4) dict E ` κτ ; dexp dexp1 ...dexpn ⇓ dict E2 ` Eq Int ; dictEqInt poly−dict ` E (DICT-SPEC) poly−dict ∀α1 ...αm . θ ⇒ κ τ ⇓ E2 ; dexp ` ∀ .hi ⇒ Eq Int ; dictEqInt over−dict E ` (6) (θ ⇒ κτ)[τ1 /α1 , ..., τm /αm ] ; dexp τ1 ...τm ⇓ (5) over−dict E2 ` (hi ⇒ Eq Int)[] ; dictEqInt (IE of E) dvar = ∀α1 ...αn .θ ⇒ κτ ⇓ (IE of E2 ) dvar = Eq Int dict (DICT-TAUT-IE) E ` κτ ; dvar ⇓ dict E2 ` Eq Int ; dictEqInt dict Abbildung 9: Die E ` Regeln für Abbildung 8 14 (6) 3. Zoo der Typklassen 2.2.4. Mehrdeutige Typen Bei der Verwendung von überladenen Funktionen können Mehrdeutigkeiten entstehen, in denen Typen nicht ohne Hinweise durch den Programmierer bestimmt werden können. Ein Beispiel dafür ist der Ausdruck [10, §4.3.4]: -- show :: a -> String -- read :: Read a => String -> a show (read "1") Die Kombination von show und read ergibt den eindeutigen Typ String. Es lässt sich daraus auch eine syntaktisch korrekte Funktion bilden: readAndShow x :: String -> String readAndShow x = show (read x) Die Anwendung ist allerdings nicht möglich, da die „innere”Typvariable a niemals bestimmt werden kann. Damit die Funktion angewendet werden kann muss die Rückgabe von read explizit typisiert werden: readAndShow x :: String -> String readAndShow x = show (read x :: Int) 3. Zoo der Typklassen In diesem Abschnitt werden die verschiedenen Arten der Typklassen vorgestellt. Ausgehend von der ürsprünglichen Typklassenart mit nur einem Typparameter werden darauf aufbauende Anwendungsmöglichkeiten und Erweiterungen vorgestellt. Die bisherigen Beispiele von Typklassen verwendeten nur single parameter Typklassen. Auch die Beschreibung in Wadler und Blott [18] betrachtet ausschließlich single parameter Typklassen. 3.1. Single parameter Typklassen Ende der 80’er Jahre löste man mit Typklassen das Problem der Typisierung von Operatoren wie ==. Dazu wurden Klassen gebildet wie: class Eq a where (==) :: a -> a -> a class Num a where (+),(-),(*) :: a -> a -> a Für die Überladung von Operatoren wie dem Vergleich oder den mathematischen Operatoren sind single parameter Typklassen gut geeignet. Wenn aber Operationen mit Operanden von 15 3. Zoo der Typklassen gemischten Typ wie: (4::Int)+ (5::Double) anzuwenden sind sollte (+) auf den ersten Blick den Typ Int -> Double -> Double haben. Andererseits möchte man nicht, auch wenn es möglich wäre (mit multi parameter Typklassen), jede mögliche und sinnvolle Kombination von Typen für einen Operator implementieren. Eine Alternative wäre es, in diesem Fall das erste Argument der + Operation in ein Double Datentypen zu konvertieren: (fromIntegral (4::Int)) + (5::Double) fromIntegral ist in Haskell definiert. Es konvertiert die 4::Int über die Klasse Integral(Int).toInteger in den Datentyp Integer. Mit Num(Double).fromInteger wird der Integer Typ in einen Double Typ konvertiert. -- GHC/Real.hs -- | general coercion from integral types fromIntegral :: (Integral a, Num b) => a -> b fromIntegral = fromInteger . toInteger 3.1.1. Anwendungsbeispiel Temperatureinheiten Um die Anwendung von Typklassen anschaulich zu gestalten werden einige Beispiele die Darstellung physikalischer Einheiten behandeln. Am Beispiel der Konvertierung von Ganzzahlen in Fließkommazahlen ist zu sehen, wie AdhocPolymorphismus die Verwendung von verschiedenen Typen durch die einfache Konvertierung mit einer einheitlichen Funktion vereinfacht. Das Gleiche ist auch für benutzerdefinierte Datentypen, zum Beispiel Temperaturwerte möglich. Diese können auch unterschiedliche Typen, sprich Einheitensysteme haben, sind dabei jedoch vollständig untereinander konvertierbar. Ein möglicher Ansatz diese Konvertierung für Temperatureinheiten Typen zu implementieren ist für jede Einheit die Implementierung in die universelle SI-Einheit Kelvin zu fordern. In Form einer Typklasse: class CoerceKelvin a where toKelvin :: a -> Kelvin fromKelvin :: Kelvin -> a Eine Temperatureinheit, die als Datentyp in Haskell dargestellt wird, kann diese Funktionen jetzt unterstützen: data Celsius = Celsius Double deriving (Show) instance CoerceKelvin Celsius where toKelvin (Celsius c) = Kelvin (c + 273.15) fromKelvin (Kelvin k) = Celsius (k - 273.15) und vergleichbar mit fromIntegral ist die Implementierung einer polymorphen Konvertierungsfunktion möglich: 16 3. Zoo der Typklassen 250 200 150 100 50 0 -50 -25 0 Celsius Réaumur Delisle Newton 25 Fahrenheit 50 75 100 Rømer Abbildung 10: Temperaturskalen relativ zu Celsius (-10◦ − 100◦ ) coerce :: (CoerceKelvin a, CoerceKelvin b) => a -> b coerce = fromKelvin . toKelvin Grafisch dargestellt zeigt die Abbildung 10 auf der X-Achse einen Celsius Wert und auf der Y-Achse den entsprechenden Zahlenwert in einer anderen Temperaturskala. Der vollständige Quelltext des Beispiels findet sich in Unterabschnitt B.1 ab S. 31. 3.2. Multi parameter Typklassen Im vorangegangenen Beispiel für single parameter Typklassen wurde eine Konvertierungsfunktion erstellt. Mit dieser wurde es möglich zwischen Temperatureinheiten zu konvertieren. Ein erweiterter Ansatz würde die Mischung von Einheiten in den Argumenten für einen Operator (z.B. +) erlauben. Angelehnt an den EXP Interpreter [16] beherrscht eine eingebettete Sprache den typsicheren Umgang mit physikalischen Einheiten. Ein valider Ausdruck ist: (kilometer 4) .+. (meter 1) :: EXP Meter EXP Meter gibt die Zieleinheit an. Die Operanden müssen in diese konvertierbar sein. class Plus a b c where (.+.) :: EXP a -> EXP b -> EXP c 17 3. Zoo der Typklassen -- (.+.) mit Ergebnis als Meter instance (CoerceEXP a Meter, CoerceEXP b Meter) => Plus a b Meter where (.+.) = ... Bei dem Typ EXP werden Phantom Types benutzt um die Einheit auf den Ausdruck zu kodieren. Die Typvariable a kommt nur auf der linken Seite der Deklaration im Typkonstruktor vor, nicht aber im Datenkonstruktor EXP. Der Instanz-Context bestimmt mit (CoerceEXP a Meter, ...): Der erste und zweite Operand in die Ergebniseinheit Meter konvertierbar sein. Da es mehrere instance-Deklarationen, für verschiedenen Ergebniseinheiten gibt, ist die Typklasse Plus nicht eindeutig (sondern mehrdeutig, ambiguous). Es muss daher wie im Beispielausdruck zu sehen der Ergebnisdatentyp explizit angegeben werden, damit die passende Instanz ausgewählt werden kann. Der vollständige Quelltext des Beispiels findet sich im Unterabschnitt B.2 ab S. 32. 3.3. Constructor Classes Ein Standardbeispiel für Konstruktor Klassen ist die Überladung von map[7] (Deutsch: Abbildung). Ein gewöhnliches map sei definiert als: data List a = Cons a (List a) | Nil deriving (Show) mapL :: (a -> b) -> List a -> List b mapL _ Nil = Nil mapL f (Cons x xs) = Cons (f x) (mapL f xs) -- Anwendungsbeispiel -- *Main> mapL (\x -> x + 10) (Cons 1 $ Cons 2 $ Cons 3 Nil) -- Cons 11 (Cons 12 (Cons 13 Nil)) ist ein Typkonstruktor (Kind: * -> *). Er wird mit einem anderen Typ als Argument aufgerufen um den eigentlich Typ zu formen. List Das beschriebene map kann nur auf Listen angewendet werden. Durch die Verwendung des Typkonstruktors List auf Listen mit beliebigen Elementdatentyp a, aber nicht auf andere Datenstrukturen. Um die map Funktion zu überladen erstellt man eine Typklasse. Diese wird, abgeleitet aus der Kategorientheorie oft Functor genannt. class Functor f where fun :: (a -> b) -> f a -> f b Vergleicht man den Typ von fun mit dem obenstehenden map, so ist leicht nachzuvollziehen, dass lediglich der Datentyp List mit dem Typparameter f der Klasse ausgetauscht wurde. Darum ist auch die Implementierung für Listen identisch zu map: 18 3. Zoo der Typklassen instance Functor List where fun _ Nil = Nil fun f (Cons x xs) = Cons (f x) (fun f xs) Mit der Functor Klasse ist jetzt auch möglich die map Funktion gleichermaßen für andere Datentypen zu definieren: data Maybe a = Just a | Nothing instance Functor Maybe where fun _ Nothing = Nothing fun f (Just x) = Just (f x) instance Functor [] where fun = map Eine Funktion lässt mit dieser überladenen Definition von fun gleichermaßen auf einen Maybe Typ wie eine List anwenden: mul3 :: Num a => a -> a mul3 x = x * 3 -- Anwendung Main> fun show . fun mul3 $ [1,2,3,4] ["3","6","9","12"] Main> fun show . fun mul3 $ Just 1 Just "3" Auch das vorangegange Beispiel zu multi parameter Typklassen verwendete bereits eine Klasse mit Typkonstruktor: -- Definition von EXP: data EXP a = EXP Exp -- Die Plus Operation class Plus a b c where (.+.) :: EXP a -> EXP b -> EXP c Die Klasse Plus a b c wird direkt für entsprechenden Einheiten parametrisiert. Der Operandentyp wird mithilfe des Typkonstruktors EXP gebildet. Einen weiteren wichtigen Anwendungsfall finden Constructor Classes bei der Definition von Monaden. 3.4. Functional Dependencies Das Beispiel zu Multi parameter Typklassen hat gezeigt wie leicht bei der Verwendung mehrerer Typparameter Mehrdeutigkeiten entstehen können. Lässt sich das Problem dort zwar durch explizite Typisierung beheben, lassen sich aber auch Fälle konstruieren in denen die Mehrdeutigkeit nicht behoben werden kann. Ein Standardbeispiel hierzu ist eine Collection-API[8, S.4]: 19 4. Vergleich mit C++ class Collects e ce where empty :: ce -- Die leere Liste insert :: e -> ce -> ce -- Einfuegen eines Elements member :: e -> ce -> Bool -- Ist Ein Element Teil der Liste? instance Eq e => Collects e [e] where ... Wird diese Typklasse verwendet könnte dies wie folgt aussehen: -- Initialisieren einer leeren Liste mit zwei Elementen x und y neueListe x y = liste2 where liste2 = insert y liste1 -- Typ: Collects e ce => ce liste1 = (insert x (empty)) -- Typ: Collects e ce => ce Um die Typen einzeln beschreiben zu können wurde der Vorgang aufgeteilt. liste1 enthält das erste Element, liste2 beide Elemente. Für das insert in die leere Liste steht fest: Es handelt sich um eine Liste vom Typ ce mit Elementen vom Typ e. Der Typ von e steht aber dem zweiten insert nicht mehr zur Verfügung da es lediglich den Collection Datentyp sieht. Fatalerweise sieht der naive Compiler nicht den Zusammenhang zwischen Elementdatentyp und Collectiondatentyp. Für die Funktion neueListe würde daher der Typ: neueListe x :: (Collects e1 ce, Collects e2 ce) => e1 -> e2 -> ce hergeleitet werden. Es wäre also theoretisch möglich in eine Liste Elemente mit verschiedenen Typen einzufügen. Functional Dependencies lösen dieses Problem indem die Beziehung zwischen der Typvariable e und ce ausgedrückt wird. ce ; e bedeutet: Der Typ ce bestimmt eindeutig den Typ von e. class Collects ce e | ce -> e where empty :: ce insert :: e -> ce -> ce member :: e -> ce -> Bool Für weitere Informationen sei auf Jones [8] verwiesen, dort werden Functional Dependencies tiefergehend und vollständig vorgestellt. 4. Vergleich mit C++ Der Vergleich mit C++ ist auf mehreren Ebenen möglich. Hier soll als erstes die mögliche Konfusion zwischen OO-Klassen in C++ und Haskell Typklassen angesprochen werden. C++ besitzt mehrere Möglichkeiten Typpolymorphie einzusetzen. Hier wird die klassische Funktionsüberladung mit Typklassen verglichen. Die für C++ 11 geplanten Concepts besitzen viele interessante Parallelen zu Haskell Typklassen. Ihre Verwandschaft wird ausführlich dargestellt. 20 4. Vergleich mit C++ 4.1. C++ Objektorientierung und Haskell Typklassen Die Verwendung des Begriffs class/Typklasse in Haskell deutet eine Verwandschaft mit Klassen in der objektorientierten Programmierung an. Es gibt jedoch einige deutliche Unterschiede zwischen Klassen im objektorientierten Sinn und Typklassen in Haskell. Eine Typklasse kann eher mit einem Interface in Java verglichen werden. Sie definiert ein Protokoll mit dem der Typ verwendet werden kann, nicht die tatsächliche Implementierung des Verhaltens. Wie eine Java-Klasse das Verhalten für einen Typ bei dem implementieren eines Interfaces bestimmt, definiert eine Haskell Instanz die Typklasse für einen bestimmten Typ. Während in C++ und Java üblicherweise Daten und Memberfunktionen gemeinsam in Klassen definiert werden, sind diese in Haskell getrennt. Auch gibt es in Haskell keine Sichtbarkeiten (private,public etc.) wie in objektorientierten Sprachen. Dagegen unterstützt Haskell nativ das Bilden von Modulen, wobei Funktionen und Typen explizit exportiert werden[6, S. 26]. Bei Bildung von Analogien darf nicht ausser Acht gelassen werden, dass Haskell eine funktionale Programmiersprache ist und C++ eine imperative Programmiersprache. Haskell verwendet Lazy-evaluation, verbietet Seiteneffekte und hat keine Altlasten unmittelbarer Vorgängersprachen. Der Vergleich hinkt auch schon darum, weil in der Praxis bei der Programmierung in Haskell kein Wert auf die Einhaltung der Analogie zum objektorientierten Paradigma gelegt wird. 4.2. Funktionsüberladung in C++ als Gegensatz zu Typklassen Adhoc Polymorphismus wird in Haskell genauso wie in C++ verwendet um eine generische Übersetzungsmethode von Daten in eine lesbare Zeichendarstellung zu realisieren. Diese kann dann verwendet werden um Programmdaten für Logging, Debugging oder Kommunikationszwecken in eine Zeichendarstellunge zu transformieren. Haskell Die Haskell Prelude definiert die Typklasse Show: class Show a where -- | Convert a value to a readable ’String’. show :: a -> String -- Anwendungsbeispiel intToString :: Int -> String intToString i = show i 21 4. Vergleich mit C++ C++ verwendet für Ausgaben Output-Streams (std::basic_ostream bzw. die Spezialisierung basic_ostream<char>: std::ostream). Für die Verwendung zur Ausgabe von Strings wurde der Shiftoperator wie folgt definiert (in Header <string>): template <class CharT, class Traits, class Allocator> std::basic_ostream<CharT, Traits>& operator<<(std::basic_ostream<CharT, Traits>& os, const std::basic_string<CharT, Traits, Allocator>& str); vereinfacht: std::ostream& operator<<(std::ostream& os, const std::string& str); Die Programmstandardausgabe std::cout ist eine Instanz von std::ostream. Ausgaben auf den Stream erfolgen mit dem Shiftoperator <<. Erweitern in C++ C++ unterstützt die Überladung von Funktionen. Damit wird für einen neuen Datentyp eine neue Überladung des Shiftoperators mit ostream angelegt: struct Vector { float x,y; }; std::ostream& operator<<(std::ostream& os, const Vector& v) { os << v.x << "," << v.y << std::endl; } Für Klassen deren Daten nicht sichtbar (private) sind muss die << Funktion als friend deklariert werden. Bei der Auflösung von Funktionsnamen führt C++ mit Argument dependent name lookup eine besondere Art der Namensauflösung ein. Der Namensraum der << Funktion muss nicht explizit angegeben werden, da ein Namensraum zur Auflösung des Funktionsnamens um den Namensraum der Argumente erweitert wird. Erweitern in Haskell Typklassen: Die Überladung von show folgt dem gewöhnlichen Vorgehen für date Vector = Vector Float Float instance Show Vector where show (Vector x y) = (show a) ++ "," ++ (show b) 4.3. C++ Concepts Bjarne Stroustrup erklärte 1993 in einem Interview die Typsicherheit als ein Ziel von C++[14, S. 743]: 22 4. Vergleich mit C++ Much stronger static type checking–in the end approximating total safety, is the ideal. [..] Dies wirft die Frage auf, warum die Typsicherheit von Templates nicht schon früher bzw. beim Entwurf der Templates für C++ in Erwägung gezogen wurde. Gibbons und Paterson [3, S. 9] beschreiben C++ Templates aus Sicht des Typsystems als Rückschritt gegenüber früher entwickelten Sprachen: The C++ template mechanism provides no means to define a concept explicitly; it is merely an informal artifact rather than a formal construct. In that sense, it is a retrograde step from earlier languages supporting data abstraction. Hierzu muss jedoch beachtet werden, dass die genannten früheren Sprachen gegenüber C++ Templates keine Metaprogrammierung unterstützten. Bjarne Stroustrup erklärte 2012 [9]: The reason we don’t already have something like concepts, is that in 1988 I did not know a solution that gave flexibility/generality, performance, and good early checking. As far as I know, nobody else knew either. I chose flexibility/generality and performance at the cost of delayed checking and appalling error messages. C++ Templates wurden nach dem Model des C Preprozessors entworfen. Die Typüberprüfung geschieht erst nach der Instanzierung eines Templates. Sie werden zur Kompilierzeit vollständig aufgelöst und haben keinen Runtime Overhead. Die Entwicklung der Typklassen in Haskell fand zeitlich in etwa parallel zu der Entwicklung der Templates in C++ statt. Einerseits hat die Übersetzung von Typklassen in Haskell gegenüber C++ Templates durch das Übergeben des Method Dictionary einen Runtime Overhead[12, S. 35], andererseits war zu dieser Zeit die Spaltung zwischen der Forschung an imperativen und funktionalen Sprachen recht groß [12, S. 10]. Auch zu dieser Zeit wurde die Grundlage für bounded polymorphism gelegt, welches die Grundlage für Generics in Java bildet[12, S. 9]. 4.3.1. Motivation für C++ Concepts In C++ existiert bereits der Begriff des Concepts für eine algebraische Struktur und wird im Zusammenhang mit der Standard Template Library (STL) verwendet. Die STL bietet dem C++ Programmierer viele generische Algorithmen und einige Container Klassen. Listing 3 zeigt die Anwendung der STL sort() Funktion zum sortieren eines Set. Die Iteratoren (.begin() und .end()) bieten jedoch nicht das von sort() geforderte STLConcept RandomAccessIterator. Das std::set stellt Iteratoren mit dem Concept BidirectionalIterator bereit. Wie aus der Iterator Refinement Hierarchie (Abbildung 11) zu sehen ist, sind diese eine Superklasse von RandomAccessIterator und damit nicht ausreichend. 23 4. Vergleich mit C++ Default Constructible Assignable Equality Comparable Trivial Iterator Input Iterator Output Iterator Forward Iterator Less Than Comparable Bidirectional Iterator Random Access Iterator Abbildung 11: Hierarchie der C++ STL Iterator Concepts Rein syntaktisch existiert jedoch in Listing 3 kein Fehler. Die Missachtung der nicht im C++ Typsystem verankerten STL-Concepts wird erst nach der Template Substitution erkannt. Das heißt der Fehler tritt innerhalb des std::sort<T> Template auf. Die Fehlermeldungen des Compilers stehen somit hinsichtlich von Variablennamen, Aufrufen und eingesetzten Typen nicht mehr im Zusammenhang mit dem vom Programmierer verfassten Code. Durch den Umfang des durch das Template bestimmten Codes sind die Fehlermeldungen lang und letztendlich für den Programmier extrem unverständlich. Listing 3: Beispiel falscher Verwendung von std::sort #include <algorithm> #include <set> int main(int argc, char **argv) { std::set<int> s{7,6,5,1,2,3}; std::sort(s.begin(), s.end()); return 0; } Versuchen wir das Programm aus Listing 3 zu übersetzen, informiert uns gcc-4.7.2 mit einer 46 Zeilen langen Fehlermeldung: In file included from /usr/include/c++/4.7/algorithm:63:0, from stl_sort_1.cpp:1: /.../bits/stl_algo.h: In instantiation of ’void std::sort(_RAIter, _RAIter) [with _RAIter = std::_Rb_tree_const_iterator<int>]’: stl_sort_1.cpp:10:28: required from here /../bits/stl_algo.h:5476:4: error: no match for ’operator-’ in ’__last - __first’ ... Dabei handelt es sich um eine Fehlermeldung mitten aus der Anwendung des Algorithmus. An diesem Beispiel ist zu sehen, dass der Compiler das Template mit einem nicht erlaubten 24 4. Vergleich mit C++ Iterator instanziert und erst beim Übersetzen des daraus resultierenden Codes auf syntaktische Fehler stößt. Angenommen der übergebene Iterator implementiert Operatoren/Methoden, ist aber trotzdem kein RandomAccessIterator, müsste der Fehler nicht weiter auffallen (entspricht damit Duck-Typing[15, S. 6]). Die GCC libstdc++ bietet die Möglichkeit Concepts mithilfe der Boost Concepts-Check Library (BCCL)[11] (Eine Kopie basierend auf Version 1.12.0 ist in libstdc++ enthalten) zu prüfen (-D_GLIBCXX_CONCEPT_CHECKS). Es finden sich dann in den Fehlermeldungen neben den Symptomen (wie Typ unterstützt eine Operation nicht) auch konkrete Hinweise auf die Ursache des Problems. ConceptGCC[4] als Testimplementierung des Indiana C++ Concepts Proposals erzeugt eine weitaus kürzere, leider noch nicht sehr einfach zu verstehende, Fehlermeldung: stl_sort_1.cpp: In function ’int main(int, char**)’: stl_sort_1.cpp:10: error: no matching function for call to ’sort(std::_Rb_tree_const_iterator<int>, std::_Rb_tree_const_iterator<int>)’ /.../stl_algo.h:2673: note: candidats are: void std::sort(_Iter, _Iter) [with _Iter = std::_Rb_tree_const_iterator<int>] <requirements> /.../iterator_concepts.h:174: note: no concept map for requirement ’std::RandomAccessIterator<std::_Rb_tree_const_iterator<int> >’ /.../iterator_concepts.h:174: note: no concept map for requirement ’std::OutputIterator<std::_Rb_tree_const_iterator<int>, const int&>’ /.../iterator_concepts.h:174: note: no concept map for requirement ’std::OutputIterator<std::_Rb_tree_const_iterator<int>, const int&>’ /.../concepts.h:803: note: no concept map for requirement ’std::ShuffleIterator<std::_Rb_tree_const_iterator<int> >’ Im Gegensatz zur Fehlermeldung des Compilers ohne C++-Concepts beschränkt sich hier die Fehlermeldung auf den Hinweis, dass versucht wurde eine Template-Funktion ungültig zu instanzieren. Der Compiler versucht nicht diese ungültige Instanzierung zu übersetzen. 4.3.2. Template Instanzierung in C++ Während Haskell zur Übersetzung der Typklassen auf einen in der Sprache bereits vorhandenen Polymorphismus zurückgreift, werden in C++ Templates durch Spezialisierung der Algorithmen zur Kompilierzeit aufgelöst. Die Spezialisierung eines Templates auf einen Templateparameter heißt Template Instanzierung. Jede Templateinstanz muss seperat typgeprüft und übersetzt werden[12, S. 18]. Der C++ Compiler erzeugt bei der Parametrierung der Templates eine neue Instanz des Typs/Funktion und substituiert dabei den Typparameter. Zur Unterscheidung wird der C++ Compiler die Instanzierung eines Templates beim Name-Mangling der instanzierten Klasse/Funktion umbennenen. Listing 4-5 zeigen eine Greedy-Instantiation[17, §10.4.1] eines simplen Datentyp mit vereinfachtem Name-Mangling. Es ist jedoch zu beachten dass die Darstellung in Listing 5 während des Übersetzungsvorgangs nicht auftritt, da der Code zu diesem Zeitpunkt bereits in die Zielsprache übersetzt wurde. Wird der instanzierte Datentyp in verschiedenen Translation-Units (Objectcode Dateien bzw. die dazugehörigen Quellcodedateien) angefordert, dann wird er mehrfach instanziert und es ist Aufgabe des Linkers mehrfach vorkommende und identische Instanzierungen zu vereinen. Zum Vergleich mit Haskell sei auf Abbildung 1 (S. 8) verwiesen. 25 4. Vergleich mit C++ Listing 4: Parametrisierter Typ (C++) Listing 5: Template instanziert mit int template <typename T> struct Box { T v; }; struct Box__INT_ { int v; }; int main(void) { return (Box_INT_{0}).v; } int main(void) { return (Box<int>{0}).v; } 4.3.3. C++ Concepts und Haskell Typklassen Unabhängig von den Unterscheiden in der technischen Umsetzung von Templates und Typklassen existieren große Ähnlichkeiten zwischen Haskell Typklassen und C++ Concepts. Bei den Überlegungen zur Typisierung von Templates wurden jedoch auch andere Konzepte in Erwägung gezogen. Eine mögliche Alternative wäre der bounded polymorphism Ansatz gewesen, wie er auch in Java 1.5 mit Generics implementiert wurde. Stroustrup [13] lehnt ihn aus mehreren praktischen Gründen ab[13, S. 5, The base-class approach]. Vereinfacht zusammengefasst sind die Gründe: • Ein Entwickler eines Templates müsste Anforderungen an einen Template-Parameter als Interface-Klasse definieren. • Zwei unabhängige Entwickler könnten für die gleiche Anforderung zwei Interface-Klassen entwerfen. Es kann dann für den Anwender kompliziert werden, mit seinem Datentyp beide Interface-Klassen zu unterstützen. • Ein Templateargument ist beschränkt auf Klassen, es könnten keine primitiven Typen mehr verwendet werden. • Alle Requirements müssen als Member der Klasse formuliert werden. Builtin oder freie Funktionen müsste in einer Klasse durchgereicht werden. Er sieht die Gefahr, dass diese Lösung zu komplexen Klassenhierarchien führen wird womit auch durch den Einsatz von virtuellen Funktionen ein Performanceproblem entstehen wird. Das von Stroustrup [13] im Oktober 2003 im C++ Standards Committee Meeting vorgestellte Design für Concepts ähnelt mit dem usage-pattern approach[13, S. 13] Haskell Typklassen[12, S. 25]. In einer Vergleichsstudie (Abbildung 12) implementierten Garcia u. a. [2] Problemstellungen in mehreren Programmiersprachen jeweils unter Verwendung der dort vorhandenen Möglichkeiten der generischen Programmierung. 26 4. Vergleich mit C++ 4 Garcia et al. H I H I # # # # # # # Cecil H I I H # # #† H I # ? # H I H I I H # ∗ C# H I I H † H I ? # H I I H ∗ - SML OCaml Haskell Eiffel H I Multi-type concepts Multiple constraints Associated type access Constraints on assoc. types Retroactive modeling Type aliases Separate compilation Implicit arg. deduction Java H I I H C++ Using the multi-parameter type class extension to Haskell (Peyton Jones et al., 1997). Using the functional dependencies extension to Haskell (Jones, 2000). Planned language additions. Table 1: The level of support for important properties for generic programming in the evaluated languages. A black circle indicates full support, a white circle indicates poor support, and a half-filled circle indicates partial support. The rating of “-” in the C++ column indicates that C++ does not explicitly support the feature, but one can still program as if the feature were supported due to the permissiveness of C++ templates. Abbildung 12: Vergleich von Generischer Programmierung in Programmiersprachen[2] Haskell faired particularly well in this study, with Standard ML not too far Section 3 describes the design of the generic graph library that forms the basis for comparisons. Sections through present of behind, whileourEiffel, Java, and 4C# did 11not do the as individual well. [. implementations ..] the graph library in the selected languages. By explaining how the graph library was The take-away point for us what wasfeatures that ofa each design forfulfill concepts C++ should be implemented, we illustrate language the roles of in generic These sections also evaluate the level of supportML, each language pro- F- bounded based on theprogramming. best features of Haskell and Standard and not vides for generic programming. Section 12 discusses in detail the most important polymorphism[12, 23]. during the course of this study. Section 13 concludes the issues we S. encountered paper. Nach Uneinigkeiten zwischen dem Texas A&M Team (Stroustrup, Dos Reis u.a.) und dem 2 Generic Indiana University Team (Garcia, Siek, GregorProgramming u.a.) wurde erst im September 2006 ein gemeinDefinitions of generic programming vary. generic programming involves sames Concepts Proposal erarbeitet welches imTypically, C++Standards Comittee positiv aufgenommen type parameters for data types and functions. Although type parameters are rewurde[12, S. 39]. quired for generic programming, there is much more to generic programming than just type parameters. Inspired by the STL, we take a broader view of generic programming and use the definition from (Jazayeri et al., 1998) reproduced in Figure 1. The next section discusses the terminology and techniques that have emerged to ++ support generic programming. 4.3.4. Vergleich der Anwendung von C Concepts und Typklassen Concept Terminology C++ Haskell The notion of abstraction is fundamental to realizing generic algorithms: generic Concept Typklasse algorithms are specified in terms of abstract properties of types, not in terms of particular types. Following and Austern, adopt the ... concept C<T> {}; the terminology of Stepanovclass C awe where term concept to mean the formalization of an abstraction as a set of syntactic Modeling Concept-Map/auto Instanz concept_map C<int> instance C Int Constraints requires Kontext requires C<T> C a => ... Algorithm Template Funktion polymorphe Funktion template <typename T> T func(T x){ ... }; func :: C a => a -> a func x = ... Abbildung 13: Begriffe in C++ Concepts und Haskell (nach [1]) C++ und Haskell verwenden für gleiche Funktionen verschiedene Schlüsselwörter und Namen. Eine tabellarische Übersicht in Abbildung 13 zeigt die Entsprechungen. 27 5. Fazit Auf Basis der Kategorien Concept,Modeling,Constraints,Algorithm haben Bernardy u. a. [1] viele Vergleiskriterien erarbeitet und diese in Haskell und C++ Concepts untersucht. Nur ein bis zwei Kriterien konnten nicht in beiden Sprachen umgesetzt werden. 5. Fazit Nach einer Einführung in Haskell Typklassen wurden die technische Umsetzung eines simplen Beispiels in Haskell näher betrachtet. Es zeigt sich, dass Typklassen in Haskell eine klare und im Grunde einfache Semantik zugrundeliegt. Neben der single parameter Typklasse wurden die Erweiterungen Constructor-Class und multi parameter Typklasse vorgestellt. Beispiele veranschaulichen die praktische Anwendung dieser Erweiterungen. Der Vergleich mit C++ zeigt, dass dort mit den Concepts ein Typsystem für Templates nachgerüstet wird das deutlich beinflusst ist: C++concepts and Haskell type classes are very similar[1]. Einfache Programme können 1 : 1 übertragen werden, wobei die Darstellung in Haskell dann tendenziell klarer und durchsichtiger möglich ist. 28 A. Literatur A. Literatur [1] Jean-Philippe Bernardy u. a. „A comparison of c++ concepts and haskell type classes“. In: Proceedings of the ACM SIGPLAN workshop on Generic programming. WGP ’08. Victoria, BC, Canada: ACM, 2008, S. 37–48. ISBN: 978-1-60558-060-9. DOI: 10 . 1145/1411318.1411324. URL: http://doi.acm.org/10.1145/1411318. 1411324. [2] Ronald Garcia u. a. „An Extended Comparative Study of Language Support for Generic Programming“. In: Journal of Functional Programming 17 (2 2007). [3] Jeremy Gibbons und Ross Paterson. „Parametric Datatype-Genericity“. In: Workshop on Generic Programming. Edinburgh, 2009. URL: http://www.comlab.ox.ac.uk/ jeremy.gibbons/publications/parametric.pdf. [4] Douglas Gregor. ConceptGCC. 2011. URL: http://www.generic-programming. org/software/ConceptGCC/. [5] Cordelia V. Hall u. a. „Type classes in Haskell“. In: Bd. 18. 2. New York, NY, USA: ACM, März 1996, S. 109–138. DOI: 10.1145/227699.227700. URL: http:// doi.acm.org/10.1145/227699.227700. [6] Paul Hudak und Joseph H. Fasel. „A gentle introduction to Haskell“. In: SIGPLAN Not. 27.5 (Mai 1992), S. 1–52. ISSN: 0362-1340. DOI: 10.1145/130697.130698. URL: http://doi.acm.org/10.1145/130697.130698. [7] Mark P. Jones. „Functional Programming with Overloading and Higher-Order Polymorphism“. In: Advanced Functional Programming, First International Spring School on Advanced Functional Programming Techniques-Tutorial Text. London, UK, UK: Springer-Verlag, 1995, S. 97–136. ISBN: 3-540-59451-5. URL: http://dl.acm.org/ citation.cfm?id=647698.734150. [8] Mark P. Jones. „Type Classes with Functional Dependencies“. In: Proceedings of the 9th European Symposium on Programming Languages and Systems. ESOP ’00. London, UK, UK: Springer-Verlag, 2000, S. 230–244. ISBN: 3-540-67262-1. URL: http:// dl.acm.org/citation.cfm?id=645394.651909. [9] Danny Kalev. Bjarne Stroustrup Expounds on Concepts and the Future of C++. 2009. URL : http://www.devx.com/cplus/Article/42448/0/. [10] Simon Peyton Jones u. a. „The Haskell 98 Language and Libraries: The Revised Report“. In: Journal of Functional Programming 13.1 (2003). http://www.haskell.org/ definition/, S. 0–255. [11] Jeremy Siek, Andrew Lumsdaine und David Abrahams. The Boost Concept Check Library (BCCL). URL: http ://www . boost . org/doc/libs/1 53 0/libs/ concept check/concept check.htm. 29 A. Literatur [12] Jeremy G. Siek. „The C++0x “Concepts” Effort“. In: Generic and Indexed Programming. Hrsg. von Jeremy Gibbons. Bd. 7470. Lecture Notes in Computer Science. Springer Berlin Heidelberg, 2012, S. 175–216. ISBN: 978-3-642-32201-3. DOI: 10.1007/9783 - 642 - 32202 - 0 4. URL: http ://dx . doi . org/10 . 1007/978 - 3 - 642 32202-0 4. [13] Bjarne Stroustrup. Concept checking – N1510=03-0093. 2003. [14] Bjarne Stroustrup. „History of programming languages—II“. In: (1996). Hrsg. von Thomas J. Bergin Jr. und Richard G. Gibson Jr., S. 699–769. DOI: 10.1145/234286. 1057836. URL: http://doi.acm.org/10.1145/234286.1057836. [15] Bjarne Stroustrup und Andrew Sutton. A Concept Design for C++ (Slides to n3351). 2012. [16] Martin Sulzmann. „Funktionale Programmierung in Haskell - Ad-hoc Polymorphismus/Überladung“. In: Vorlesung IM 221 - Konzepte von Programmiersprachen. Hochschule Karlsruhe, 2012. [17] D.A. Vandevoorde und N.M.A. Josuttis. C++ Templates: The Complete Guide. Prentice Hall, 2003. ISBN: 9780201734843. [18] P. Wadler und S. Blott. How to make ad-hoc polymorphism less ad hoc. Austin, Texas, USA, 1989. DOI: 10 . 1145/75277 . 75283. URL: http ://doi . acm . org/ 10.1145/75277.75283. 30 B. Quellcodes B. Quellcodes B.1. Temperatureinheiten 1 2 import Graphics.Rendering.Chart.Simple import Graphics.Rendering.Chart.Simple.Internal(PlotPDFType) 3 4 5 6 7 8 9 10 data data data data data data data Kelvin = Kelvin Double deriving (Show) Celsius = Celsius Double deriving (Show) Réaumur = Réaumur Double deriving (Show) Fahrenheit = Fahrenheit Double deriving (Show) Rømer = Rømer Double deriving (Show) Delisle = Delisle Double deriving (Show) Newton = Newton Double deriving (Show) 11 12 13 14 class CoerceKelvin a where toKelvin :: a -> Kelvin fromKelvin :: Kelvin -> a 15 16 17 18 instance CoerceKelvin Kelvin where toKelvin k = k fromKelvin k = k 19 20 21 22 instance CoerceKelvin Celsius where toKelvin (Celsius c) = Kelvin (c + 273.15) fromKelvin (Kelvin k) = Celsius (k - 273.15) 23 24 25 26 27 instance CoerceKelvin Réaumur where toKelvin (Réaumur r) = toKelvin (Celsius (r*1.25)) fromKelvin k = Réaumur (c*0.8) where (Celsius c) = fromKelvin k 28 29 30 31 instance CoerceKelvin Fahrenheit where toKelvin (Fahrenheit f) = Kelvin ((f+459.67)*(5/9)) fromKelvin (Kelvin k) = Fahrenheit ((k*1.8)-459.67) where 32 33 34 35 instance CoerceKelvin Rømer where toKelvin (Rømer r) = Kelvin (((r-7.5)*40)/21.0 + 273.15) fromKelvin (Kelvin k) = Rømer (((k-273.15)*21)/40.0 + 7.5) 36 37 38 39 40 instance CoerceKelvin Delisle where toKelvin (Delisle d) = toKelvin (Celsius (100-(d*0.66))) fromKelvin k = Delisle ((100-c)*1.5) where (Celsius c) = fromKelvin k 41 42 43 44 45 instance CoerceKelvin Newton where toKelvin (Newton n) = toKelvin (Celsius ((100*n)/33.0)) fromKelvin k = Newton (c*0.33) where (Celsius c) = fromKelvin k 46 47 48 coerce :: (CoerceKelvin a, CoerceKelvin b) => a -> b coerce = fromKelvin . toKelvin 49 50 51 52 53 54 55 56 57 plotUnits :: PlotPDFType t => String -> t plotUnits f = plotPDF f ([-20.0,-10.0..100.0] :: [Double]) ((\(Celsius c)->c) . coerce . Celsius) "o" "Celsius" ((\(Réaumur r)->r) . coerce . Celsius) "-" "Réaumur" ((\(Fahrenheit c)->c) . coerce . Celsius) "+" "Fahrenheit" ((\(Rømer r)->r) . coerce . Celsius) "~" "Rømer" ((\(Delisle r)->r) . coerce . Celsius) "*" "Delisle" ((\(Newton x)->x) . coerce . Celsius) "x" "Newton" 31 B. Quellcodes B.2. EXP Interpreter mit Einheiten 1 2 3 {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleContexts #-} 4 5 6 7 8 9 10 -- Dieses Programm ist ein simpler Interpreter fuer -- eine eingebetette Sprache mit data Exp = Val Int -- Ganzahlen | Plus Exp Exp -- eine Plus Operation | Convert Exp (Exp -> Exp) -- Wertekonvertierungen deriving (Show) 11 12 13 instance Show (Exp -> Exp) where show _ = "Exp -> EXP a" 14 15 16 17 18 19 -- Wertetypen sind: data Kilometer data Meter data Centimeter data Celsius 20 21 22 23 -- Die Ausdruecke werden mit Phantom Types typisiert -- Jeder Ausdrueck ergibt eine Ganzzahl mit einem der Wertetypen data EXP a = EXP Exp deriving (Show) 24 25 26 unwrap :: EXP a -> Exp unwrap (EXP x) = x 27 28 29 30 -- Die Klasse CoerceEXP bietet Wertekonvertierungen an class CoerceEXP a b where coerceEXP :: EXP a -> EXP b 31 32 33 instance CoerceEXP a a where coerceEXP = id 34 35 36 37 -- Konvertierung von Kilometer in Meter instance CoerceEXP Kilometer Meter where coerceEXP e = EXP ( Convert (unwrap e) (\ee -> Val ((evalExp ee) * 1000))) 38 39 40 41 -- Konvertierung von Meter in Centimeter instance CoerceEXP Meter Centimeter where coerceEXP e = EXP ( Convert (unwrap e) (\ee -> Val ((evalExp ee) * 100))) 42 43 44 45 46 -- eval startet die Evaluierung eines EXP Ausdrucks im Einheitensystem a -- zu einem Ergebnis als Ganzzahl eval :: EXP a -> Int eval e = evalExp (unwrap e) 47 48 49 50 51 evalExp evalExp evalExp evalExp :: Exp -> Int (Val i) = i (Plus e1 e2) = (evalExp e1) + (evalExp e2) (Convert e f) = evalExp $ f e 52 53 54 55 -- Die Plus (.+.) Operation class Plus a b c where (.+.) :: EXP a -> EXP b -> EXP c 56 57 58 59 60 -- (.+.) mit Ergebnis als Meter instance (CoerceEXP a Meter, CoerceEXP b Meter) => Plus a b Meter where (.+.) e1 e2 = EXP (Plus (unwrap (coerceEXP e1 :: EXP Meter)) (unwrap (coerceEXP e2 :: EXP Meter))) 61 62 63 64 -- (.+.) mit Ergebnis als Centimeter instance (CoerceEXP a Centimeter, CoerceEXP b Centimeter) => Plus a b Centimeter where (.+.) e1 e2 = EXP (Plus (unwrap (coerceEXP e1 :: EXP Centimeter)) 32 B. Quellcodes (unwrap (coerceEXP e2 :: EXP Centimeter))) 65 66 67 68 69 70 -- (.+.) mit Ergebnis als Celsius instance (CoerceEXP a Celsius, CoerceEXP b Celsius) => Plus a b Celsius where (.+.) e1 e2 = EXP (Plus (unwrap (coerceEXP e1 :: EXP Celsius)) (unwrap (coerceEXP e2 :: EXP Celsius))) 71 72 73 74 -- Vereinfachte Konstruktoren fuer Werte meter :: Int -> EXP Meter meter x = EXP ( Val x ) 75 76 77 kilometer :: Int -> EXP Kilometer kilometer x = EXP (Val x) 78 79 80 centimeter :: Int -> EXP Centimeter centimeter x = EXP (Val x) 81 82 83 celsius :: Int -> EXP Celsius celsius x = EXP (Val x) 84 85 86 87 88 89 -- Tests test0 :: Int test0 = eval expr -- 10cm + 1m = 110cm where expr :: EXP Centimeter expr = (centimeter 10) .+. (meter 1) 90 91 92 93 94 test1 :: Int test1 = eval expr -- 1m + 4km = 4001m where expr :: EXP Meter expr = (meter 1) .+. (kilometer 4) 95 96 97 98 99 test2 :: Int test2 = eval expr -- 1C + 10C = 11C where expr :: EXP Celsius expr = (celsius 1) .+. (celsius 10) 100 101 102 103 104 test3 :: Int test3 = eval expr -- 20km + 10m = 20010m where expr :: EXP Meter expr = (kilometer 20) .+. (meter 10) 105 106 107 108 109 110 tests = and $ [test0 ,test1 ,test2 ,test3 ] == == == == 110 4001 11 20010 111 112 113 114 115 116 -- -- Illegal, No instance for (CoerceEXP Centimeter Meter) -- testFail0 :: Int -- testFail0 = eval expr -- 1cm + 1m = ?m => loss of precision -where expr :: EXP Meter -expr = (centimeter 1) .+. (meter 1) 117 118 119 120 121 122 -- -- Illegal, No instance for (Plus Kilometer Meter Kilometer) -- testFail1 :: Int -- testFail1 = eval expr -- 1km + 1m = ?km => loss of precision -where expr :: EXP Kilometer -expr = (kilometer 20) .+. (meter 10) 33 C. Abbildungsverzeichnis C. Abbildungsverzeichnis 1. 2. 3. 4. 5. 6. 7. Übersetzung nach Wadler und Blott [18] Die (PROG) Regel . . . . . . . . . . . Die Initiale Umgebung E0 . . . . . . . CLASS Regel auf Beispiel angewendet Umgebung E1 . . . . . . . . . . . . . . INST Regel auf Beispiel angewendet . Umgebung E2 . . . . . . . . . . . . . . exp over−exp 8. Die E ` und E ` . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 9 9 10 10 11 12 Regeln auf Beispiel angewendet . . . . . . . . . . . 13 dict 9. 10. 11. 12. 13. Die E ` Regeln für Abbildung 8 . . . . . . . . . . . . . . . . . . . . Temperaturskalen relativ zu Celsius (-10◦ − 100◦ ) . . . . . . . . . . . . Hierarchie der C++ STL Iterator Concepts . . . . . . . . . . . . . . . . Vergleich von Generischer Programmierung in Programmiersprachen[2] Begriffe in C++ Concepts und Haskell (nach [1]) . . . . . . . . . . . . . 34 . . . . . . . . . . . . . . . 14 17 24 27 27