Haskell Typklassen

Werbung
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
Herunterladen