Strafunski - Fachbereich Mathematik und Informatik

Werbung
Fachbereich 12
Mathematik und Informatik
Philipps-Universität Marburg
Autor: Jan Eric Schmidt
Matr.-Nr.: 1195603
Rosenstr. 7
35037 Marburg
E-Mail: [email protected]
Tel.: 06421/23538
6WUDIXQVNL
Strategische Programmierung mit Haskell
Seminararbeit von Jan Eric Schmidt
Im Rahmen des SeminarsÄ.RQ]HSWHYRQ3URJUDPPLHUVSUDFKHQ³
Ä*HQHULVFKHIXQNWLRQDOH3URJUDPPLHUXQJ7HLO³
unter der Leitung von Prof. Dr. Gumm und Prof. Dr. Loogen
WS 2003/2004
1
,QKDOWVYHU]HLFKQLV
Inhaltsverzeichnis: ......................................................................................................................2
I. Einleitung ................................................................................................................................2
II. Inkrementelle Herleitung der Grundzüge der strategischen Programmierung ......................3
Beispiel ...................................................................................................................................3
Typerweiterung.......................................................................................................................4
Der strategische Traversierungskombinator everywhere.................................................5
Rekursive Traversierung.........................................................................................................6
Erweiterung auf monadische Transformationen.....................................................................7
Generische Abfragen ..............................................................................................................9
Typ-Synonyme .....................................................................................................................10
Die Klasse Typeable und der cast Operator..................................................................11
Zusammenfassung ................................................................................................................12
III. Strafunski Software Paket ..................................................................................................13
Bibliothek .............................................................................................................................13
StrategyLib................................................................................................................13
StrategyPrimitives ...............................................................................................13
Themes............................................................................................................................15
Term.................................................................................................................................15
Präcompiler...........................................................................................................................15
IV. Abschließende Betrachtung................................................................................................16
Vorteile .................................................................................................................................16
Nachteile ...............................................................................................................................16
Ausblick................................................................................................................................17
Literaturverzeichnis ..................................................................................................................17
,(LQOHLWXQJ
Diese Arbeit beruht hauptsächlich auf den Arbeiten von Ralf Lämmel and Joost Visser, den
Entwicklern des Strafunski Sofware Pakets (siehe Literaturverzeichnis).
Strafunski basiert auf Haskell und wurde für die generische Programmierung und zur
Verarbeitung von Sprachen entwickelt. Es bietet dem Programmierer funktionale Strategien
an, die eine einfache generische Traversierung auf heterogenen Datenstrukturen ermöglichen
(sinnvolle Anwendungen: in der Programmanalyse und als Transformationskomponenten bei
der Sprachverarbeitung.)
Funktionale Strategien sind generische Funktionen, die durch Terme beliebigen Typs
traversieren können und dazu typspezifische sowie typübergreifende Aufgaben ausführen
können.
Im nächsten Kapitel sollen die Grundzüge für das Verständnis der strategischen
Programmierung und der funktionalen Strategien inkrementell anhand eines Beispiels
entwickelt werden. In Kapitel III wird das Strafunski Software Paket genauer vorgestellt. In
Kapitel IV wird noch einmal auf die Vor- und Nachteile eingegangen und ein theoretischer
Ausblick auf die Idee der strategischen Programmierung geworfen.
2
,,,QNUHPHQWHOOH+HUOHLWXQJGHU*UXQG]JHGHU
VWUDWHJLVFKHQ3URJUDPPLHUXQJ
%HLVSLHO
Das Problem soll an einem Beispiel verdeutlicht werden. Gegeben sei folgende heterogene
Datenstruktur, die eine Grundstruktur für ein Unternehmen darstellt. Ein Unternehmen
(Company) besteht aus Abteilungen (Dept), die jeweils einen Manager (Manager) und
optionale Untereinheiten (SubUnit) beinhalten. Eine Untereinheit kann entweder ein
Arbeitnehmer (Employee) oder wiederum eine Abteilung sein. Manager und Arbeitnehmer
sind Personen (Person), die ein Gehalt (Salary) beziehen.
data
data
data
data
data
data
type
type
type
Company
Dept
SubUnit
Employee
Person
Salary
Manager
Name
Address
=
=
=
=
=
=
=
=
=
C [Dept]
D Name Manager [SubUnit]
PU Employee | DU Dept
E Person Salary
P Name Address
S Float
Employee
String
String
Passend zu dieser Datenstruktur ist dies ein Beispielunternehmen:
genCom :: Company
genCom = C [D "Research" ralf [PU joost, PU marlow],
D "Strategy" blair [] ]
ralf , joost, marlow, blair :: Employee
ralf
= E (P "Ralf"
"Amsterdam")
joost
= E (P "Joost"
"Amsterdam")
marlow
= E (P "Marlow"
"Cambridge")
blair
= E (P "Blair"
"London")
(S
(S
(S
(S
8000)
1000)
2000)
10000)
In diesem Beispielunternehmen soll nun das Gehalt jedes Beschäftigten (Arbeitnehmer und
Manager) um 10% erhöht werden. Dazu entwickeln wir eine Funktion:
increase :: Float -> Company -> Company
Diese soll mit dem Aufruf (increase 0.1 genCom) jedes Gehalt (Salary) in unserem
Unternehmen genCom mit dem Faktor 0,1 multiplizieren.
Hier ein Beispielcode für diese Funktion:
increase :: Float -> Company -> Company
icrease k (C ds) = C (map (incD k) ds)
incD :: Float -> Dept -> Dept
incD k (D nm mgr us) = D nm (incE k mgr) (map (incU k) us)
incU :: Float -> SubUnit -> SubUnit
incU k (PU e) = PU (incE k e)
3
incU k (DU d) = DU (incD k d)
incE :: Float -> Employee -> Employee
incE k (E p s) = E p (incS k s)
incS :: Float -> Salary -> Salary
incS k (S s) = S (s * 1+k))
Diese Implementierung der Funktion increase benutzt vier Hilfsfunktionen, von denen
drei (incD, incU und incE) nur für die Traversierung der vorgegebenen Datenstruktur
nötig sind. Erst die Hilfsfunktion incS beinhaltet den „problemrelevanten“ Code (die
Erhöhung des Gehalts). Falls diese Datenstruktur erweitert oder verändert wird, muss der
„Traversierungscode“ der Funktion increase entsprechend angepasst werden.
Mit Hilfe der strategischen Programmierung soll diese Traversierung vereinfacht werden, so
dass sich der Programmierer auf den „problemrelevanten“ Code beschränken kann und sich
nicht noch zusätzlich mit der Datenstruktur befassen muss.
Im folgenden wird ein Ansatz für die Strategische Programmierung anhand dieses Beispiels
vorgestellt. Ziel soll eine Funktion sein, die sich nicht mehr mit der Traversierung des
Datenstrukturbaums befassen muss:
increase :: Float -> Company -> Company
increase k = everywhere (mkT (incS k))
Die Hilfsfunktionen everywhere und mkT werden im folgenden eingeführt. Die
„problemrelevante“ Funktion incS aus dem obigen Beispiel bleibt erhalten.
Die Funktion mkT („make a transformation“) soll die Funktion incS „anheben“, so dass sie
auf jeden Typ angewendet werden kann und nicht nur auf Salary-Knoten. Bei Knoten vom
Typ Salary wird die Funktion incS angewendet, bei allen anderen Typen die
Identitätsfunktion. Die Funktion everywhere ist ein Beispiel für einen generischen
Traversierungskombinator, die später noch ausführlicher vorgestellt werden(s.u.). Sie wendet
ihr Argument (in diesem Beispiel die „angehobene“ Funktion incS) auf alle Knoten des
Datenstrukturbaums an. Die beiden Funktionen everywhere und mkT benutzen
Hilfsfunktionen, die mit Hilfe von zwei Klassen (Typeable und Term, näheres siehe unten)
überladen definiert werden. Für die vorliegende Datenstruktur müssen daher Instanzen dieser
beiden Klassen abgeleitet werden, damit die Funktionen everywhere und mkT eingesetzt
werden können. Theoretisch müssten diese Instanzen vom Anwendungsprogrammierer für die
Datenstruktur manuell erstellt werden, so dass er sich wieder mit der Datenstruktur
auseinandersetzen müsste, allerdings sind diese Instanzen sehr einfach aufgebaut, so dass sie
automatisch erstellt werden können.
7\SHUZHLWHUXQJ
Wenn wir eine „problemrelevante“ Funktion vom Typ t haben, so muss diese erweitert
werden, damit sie auf alle Typen anwendbar ist. Dabei soll sie angewandt auf den Typ t
unverändert arbeiten, und im Falle eines anderen Typs die Identitätsfunktion ausführen.
Voraussetzung für eine solche Anhebung ist ein Typ-sicherer cast Operator, der zur
Laufzeit den zugrunde liegenden Typ überprüft und in unserem Fall die Klasse Typeable
benötigt.
4
-- An abstract class
class Typeable
-- A type-safe cast operator
cast :: (Typeable a, Typeable b) => a -> Maybe b
Die cast Funktion nimmt ihr Argument x vom Typ a und vergleicht diesen mit dem Typ b
und liefert im Fall der Gleichheit Just x , in allen anderen Fällen Nothing zurück. Der
Ergebniskontext ist dabei von der Typsignatur (also dem Typ b) abhängig (näheres siehe „ Die
Klasse Typeable und der cast Operator“ , Kapitel II).
Mit Hilfe des cast Operators können wir die Funktion mkT implementieren:
mkT :: (Typeable a,
mkT f = case cast f
Just g
Nothing
Typeable b) => (b -> b) -> a -> a
of
-> g
-> id
Das Argument f ist eine Funktion, die eine Transformation von Typ b nach Typ b vornimmt
(in unserem Beispiel der Typ Salary). Falls mkT nun auf einen Knoten (in unserer
heterogenen Datenstruktur) angewendet wird, der den gleichen Typ wie die
Transformationsfunktion hat, wird diese darauf ausgeführt, in allen anderen Fällen wird die
Identität zurückgeliefert und somit der Knoten unverändert gelassen.
Somit haben wir nun unsere „ problemrelevante“ Funktion incS auf alle Typen a ausgeweitet
(für die Instanzen der Klasse Typeable vorhanden sind):
inc :: Typeable a => Float -> a -> a
inc k = mkT (incS k)
Diese Funktion ist auf jeden Knoten unserer Datenstruktur anwendbar, das Ziel ist allerdings
eine Funktion, die sich selbst auf alle Knoten anwendet, also die Datenstruktur selbständig
traversiert.
'HUVWUDWHJLVFKH7UDYHUVLHUXQJVNRPELQDWRUHYHU\ZKHUH
Der Kombinator everywhere soll sein Argument (eine Funktion) auf jeden Knoten des
zugrunde liegenden Datenstrukturbaums anwenden. Um diesen zu implementieren sind
weitere Schritte notwendig. Dazu wird die Klasse Term eingeführt, die eine Funktion gmapT
zur Verfügung stellt, die eine generische Transformation auf alle Kindknoten anwendet.
Class Typeable a => Term a where
gmapT :: (forall b. Term b => b -> b) -> a -> a
Das bedingt, dass für jeden Typ der Datenstruktur eine Instanz der Klasse Term definiert
wird.
instance Term Employee where
gmapT f (E per sal) = E (f per) (f sal)
5
Am Beispiel des Arbeitnehmers sehen wir, dass das Argument f einfach auf beide
Kindsknoten (Person und Salary) angewendet wird und daraus ein Employee
konstruiert wird.
Des Weiteren wird an diesem Beispiel deutlich, warum wir eine Sprache benötigen, die Rang
2-Polymorphie unterstützt. Das erste Argument hat einen polymorphen Typ (forall b.
Term b => b -> b), denn es wird sowohl auf Person als auch auf Salary angewandt.
(Anmerkung: Rang 1-Polymorphie erlaubt die Anwendung von Quantoren nur auf die ganze
Funktion. In diesem Fall wird ein „forall b“ eingesetzt um das erste Argument
zusätzlich zu quantifizieren. Erst die Nachfolger von Haskell 98 unterstützen Rang 2
Polymorphie.)
Falls ein Knoten der Datenstruktur keine Kindknoten besitzt, hat die Funktion gmapT keinen
Effekt:
instance Term Bool where
gmapT f x = x
Eine Haupteigenschaft der Traversierungskombinatoren ist die Möglichkeit, verschiedene
Basiskombinatoren (siehe auch „ Bibliothek“ , Kapitel III) zu verbinden und daraus
komplexere Kombinatoren zu bilden. Aus diesem Grund soll die Funktion gmapT nur auf die
direkten Kindesknoten angewendet werden, so dass nicht zuviel „ Traversierungslogik“ in der
Funktion enthalten ist, man nennt dies auch Ein-Schicht-Traversierung (one-layer traversal).
Dies soll anhand eines Beispiels einer Instanz der Klasse Term für Listen verdeutlicht werden:
instance Term a => Term [a] where
gmapT f []
= []
gmapT f (x:xs) = f x : f xs
Hierbei wird auf eine Rekursion innerhalb der Funktion gmapT verzichtet (Anmerkung: Mit
Rekursion würde die letzte Zeile der Funktionsdefinition folgendermaßen aussehen:
gmapT f (x:xs) = f x : gmapT f xs
). Dies unterscheidet sie von einer gewöhnlichen map Funktion, die rekursiv aufgebaut ist und
eine komplette Liste durchläuft.
5HNXUVLYH7UDYHUVLHUXQJ
Der rekursive Traversierungskombinator everywhere sieht folgendermaßen aus:
--Apply a transformation everywhere, bottom-up
everywhere :: Term a => (forall b. Term b => b -> b) -> a -> a
everywhere f x = f (gmapT (everywhere f) x)
Er wendet eine Transformation auf alle Knoten der Datenstruktur von “ unten nach oben”
(bottom-up) an.
Mit Hilfe der einschichtigen Traversierungsfunktion gmapT können verschiedene
Traversierungen erreicht werden, so z.B. auch eine Anwendung auf alle Knoten von „ oben
nach unten“ (top-down):
--Apply a transformation everywhere, top-down
6
everywhere’ :: Term a
=>(forall b. Term b => b -> b) -> a -> a
everywhere’ f x = gmapT (everywhere’ f) (f x)
(Im Kapitel III werden verschiedene Basiskombinatoren und daraus zusammengesetzte
Kombinatoren vorgestellt.)
(UZHLWHUXQJDXIPRQDGLVFKH7UDQVIRUPDWLRQHQ
In diesem Abschnitt soll das Konzept der Transformation auf Monaden erweitert werden.
Dies wird anhand eines weiteren Beispiels geschehen und wir werden sehen, dass die oben
eingeführten Transformationen ein Spezialfall der monadischen Transformationen sind.
In dem einführenden Beispiel wurde jedes Gehalt eines Arbeitnehmers mit einem Faktor
multipliziert. Nun soll statt dessen für jeden Arbeitnehmer das Gehalt aus einer externen
Datenbank ausgelesen werden und in unserem Beispielunternehmen eingefügt werden. Dies
erfordert Ein- und Ausgabe Operationen, die in Haskell mit Hilfe einer IO-Monade realisiert
werden können. Es soll also eine Funktion
lookupSalaries :: Company -> IO Company
implementiert werden, die jedes Gehalt eines Arbeitnehmers mit Hilfe einer externen
Datenbank modifiziert. Man beachte, dass das Resultat in eine IO Monade eingebettet ist.
Dazu benötigen wir eine neue Funktion, die die Typerweiterung auf Monaden ausweitet. Die
ursprünglich Funktion haben wir mkT für „ make a transformation“ genannt. Die Erweiterung
soll mkM für „ make a monadic transformation“ heißen:
mkM :: (Typeable a, Typeable b, Typeable (m a),
Typeable (m b), Monad m)
=> (b -> m b) -> a -> m a
mkM f = case cast f of
Just g
-> g
Nothing
-> return
Die Typbeschreibung dieser Funktion ist wesentlich umfangreicher als bei der Funktion mkT,
beinhaltet aber nichts wesentlich Neues. Es muss nur garantiert werden, dass der cast
Operator für alle auftretenden Typen verfügbar ist. Ein weiterer Unterschied der beiden
Funktionen besteht darin, dass hier das Ergebnis in eine Monade eingebettet wird.
Des Weiteren muss die Klasse Term durch eine neue Funktion gmapM erweitert werden, die
der Funktion gmapT sehr ähnlich ist und deren Funktionalität auf Monaden ausweitet:
class Typeable a => Term a where
gmapT :: (forall b. Term b => b -> b)
-> a -> a
gmapM :: (forall b. Term b => b -> m b) -> a -> m a
Analog müssen für alle verwendeten Datentypen Instanzen der Klasse Term definiert werden.
Die dazugehörigen Beispielinstanzen nutzen das in Haskell übliche do-Konstrukt für
Monaden (die Implementierung der Funktion gmapT wurde der Übersicht halber
weggelassen, s.o.):
7
instance Term Employee where
-gmapT ...
gmapM f (E p s) = do
p’ <- f p
s’ <- f s
return (E p’ s’)
Auch in diesem Fall wird in dieser Funktion auf Rekursion verzichtet, um die Ein-Schicht
Funktionalität zu gewährleisten. Als Beispiel zeigen wir die Instanz für Listen:
instance Term a => Term [a] where
-gmapT ...
gmapM f []
= return []
gmapM f (x:xs) = do
x’ <- f x
xs’ <- f xs
--keine Rekursion
return (x’:xs’)
Mit Hilfe dieser neuen Funktionen kann nun ein monadischer Traversierungskombinator
definiert werden (analog zu dem everywhere ein bottom-up Kombinator):
everywhereM :: (Monad m, Term a)
=> (forall b. Term b => b -> m b) -> a -> m a
everywhereM f x = do
x’ <- gmapM (everywhereM f) x
f x’
Damit haben wir alle nötigen Vorkehrungen getroffen, um die angestrebte Funktion
lookupSalaries nach dem Prinzip der „ strategischen Programmierung“ zu
implementieren:
lookupSalaries :: Company -> IO Company
lookupSalaries = everywhereM (mkM lookupE)
lookupE :: Employee -> IO Employee
lookupE (E p@(P n _) _) = do s <- dbLookup n
return (E p s)
dbLookup :: Name -> IO Salary
-- Abfrage des Gehalts zu einer Person aus einer externen
-- Datenbank
(Anmerkung: Die Art der Implementierung der Funktion dbLookup ist für den Ansatz der
strategischen Programmierung nicht von Bedeutung, daher wird hier darauf verzichtet.)
Mit der hier vorgestellten Implementierung für monadische Transformationen kann auch die
weiter oben eingeführte Transformation ausgedrückt werden. Die Funktion gmapT kann
durch die Funktion gmapM mit der Identitätsmonade ausgedrückt werden. Somit ist eine
Transformation nur ein Spezialfall von monadischen Transformationen. In der Strafunski
Bibliothek (siehe Kapitel III) sind daher auch nur Kombinatoren mit monadischem Typ
enthalten.
8
*HQHULVFKH$EIUDJHQ
Zusätzlich zu den generischen Transformationen, die keine Typveränderung vornehmen
(type-preserving), ist eine weitere Art von generischen Programmen in Bezug auf die
strategische Programmierung interessant: die generischen Abfragen (generic queries). Bei den
generischen Abfragen hat das Ergebnis einen festgelegten Typ, d.h. es wird auf einen Typ
abgebildet (type-unifying).
-- Transformation: forall a. Term a => a -> a
-- Abfrage:
forall a. Term a => a -> R
Passend zu unserem Beispielunternehmen soll eine Gesamtrechnung für alle Gehälter erstellt
werden. Es soll also eine Funktion konstruiert werden, die alle Gehälter in der Datenstruktur
ausliest und aufsummiert:
salaryBill :: Company -> Float
Somit wird die ursprüngliche Unternehmensstruktur nicht in das Ergebnis übernommen,
sondern auf den festgelegten Typ Float abgebildet (Faltung).
Auch in diesem Fall wird eine Funktion definiert, die eine Typerweiterung vornimmt und so
den „ problemrelevanten“ Code in eine polymorphe Funktion umwandelt (mkQ). Dies wird
ähnlich wie im vorigen Beispiel mit einer Traversierungsfunktion (everything)
kombiniert.
salaryBill :: Company -> Float
salaryBill = everything (+) (0 ’mkQ’ billS)
billS :: Salary -> Float
billS (S f) = f
In diesem Beispiel ist billS die “ problemrelevante” Funktion. Sie wird auf ein
Gehalt(Salary) angewendet und liefert den dazugehörigen Float Wert. In dieser
Konstruktion wird dem Traversierungskombinator everything (s.u.) ein Operator (+) als
Parameter mit übergeben. Die Funktion für die Typerweiterung benötigt einen zusätzlichen
Parameter(default parameter). Dieser wird in die Berechung (in diesem Fall die Addition) mit
einbezogen, falls der Typ des gerade traversierten Knotens nicht mit dem Typ der
„ problemrelevanten“ Funktion übereinstimmt. In diesem Beispiel bedeutet das, dass bei der
Traversierung des Beispielunternehmens im Falle eines Gehaltknotens, dessen Float Wert
zu der Berechungssumme dazuaddiert wird und in jedem andern Fall 0 (default-Wert) zu der
Berechnungssumme hinzuaddiert wird.
-- make a query
mkQ :: (Typeable a, Typeable b) => r -> (b -> r) -> a -> r
(r ’mkQ’ q) a = case cast a of
Just b
-> q b
Nothing
-> r
(Anmerkung: Die Infixschreibweise (r ’mkQ’ q) wird hier bevorzugt, um
hervorzuheben, dass entweder der default-Wert oder der „ problemrelevante“ Ergebniswert
mit in die Berechnung einfließt.)
9
Als nächster Schritt muss ein weiteres mal die Klasse Term erweitert werden. Hinzu kommt
eine Funktion gmapQ (Q für query) , die auf eine Liste vom Typ r abbildet.
class Typeable a => Term
gmapT :: (forall b.
gmapM :: (forall b.
gmapQ :: (forall b.
a where
Term b => b -> b)
-> a -> a
Term b => b -> m b) -> a -> m a
Term b => b -> r)
-> a -> [r]
Hierzu ein paar Beispielinstanzen (die Implementierungen für gmapT und gmapM werden der
Übersicht halber nicht mit aufgelistet, s.o.):
instance Term Employee where
-gmapT ...
-gmapM ...
gmapQ f (E p s) = [f p, f s]
instance Term a => Term [a] where
-gmapT ...
-gmapM ...
gmapQ f []
= []
gmapQ f (x:xs) = [f x, f xs]
-- keine Rekursion
instance Term Bool where
-gmapT ...
-gmapM ...
gmapQ f x = []
Der everyhting Kombinator benutzt zusätzlich zu der schon bekannten
Typerweiterungsfunktion und Rekursion die Listenfaltung(foldl), um alle Knoten von
„ oben nach unten“ (top-down) zu durchlaufen und mit Hilfe des übergebenen 2stelligen
Operators auf den Resultattyp abzubilden.
everything :: Term a
=> (r -> r -> r)
-> (forall a. Term a => a -> r)
-> a -> r
everything k f x = foldl k (f x) (gmapQ (everything k f) x)
7\S6\QRQ\PH
Der Typ des everywhere Kombinators enthält ein implizites „ forall a“ , das sich auf
den außenstehenden Term a bezieht.
everywhere :: forall a. Term a
=> (forall b. Term b => b -> b)
-> a -> a
Dies kann äquivalent umgeschrieben werden.
everywhere ::
(forall b. Term b => b -> b)
-> (forall a. Term a => a -> a)
10
In Haskell ist es legitim einen neuen Typ zu definieren, der die generische Eigenschaft dieser
Funktionen hervorhebt.
type GenericT = forall a. Term a => a -> a
Somit kann der Typ von everywhere auch folgendermaßen ausgedrückt werden:
everywhere :: GenericT -> GenericT
Analog kann dies auch für generische monadische Transformationen und generische
Abfragen definiert werden.
type GenericM m
type GenericQ r
= forall a. Term a => a -> m a
= forall a. Term a => a -> r
Dadurch erhalten wir übersichtlichere Typdefinitionen für unsere bereits definierten
Traversierungskombinatoren:
everywhereM :: Monad m => GenericM m -> GenericM m
everything :: (r -> r -> r) -> GenericQ r -> GenericQ r
'LH.ODVVH7\SHDEOHXQGGHUFDVW2SHUDWRU
Die Klasse Typeable wurde bisher nicht näher erläutert. Wir benötigen sie um
Typüberprüfungen mit Hilfe des cast Operators durchführen zu können. Der hier
vorgestellte Ansatz ist leider keine optimale Lösung (näheres bei Nachteilen, Kapitel IV), für
akademische Zwecke allerdings anschaulich, da er sich mit Standard Haskell realisieren lässt
und keine spezielle Compilerunterstützung benötigt.
In der Klasse Typeable wird eine Hilfsfunktion typeOf überladen definiert, die für jeden
benutzten Datentyp eine Repräsentation (TypeRep) definiert.
class Typeable a where
typeOf :: a -> TypeRep
data TypeRep = TR String [TypeRep]
Dazu gehörende Beispielinstanzen könnten folgendermaßen aussehen:
instance Typeable Int where
typeOf x = TR "Prelude.Int" []
instance Typeable Bool where
typeOf x = TR "Prelude.Bool" []
instance Typeable a => Typeable [a] where
typeOf x = TR "Prelude.List" [typeOf (get x)]
where
get :: [a] -> a
get = undefined
11
Dabei wertet die Funktion typeOf ihr Argument x niemals aus. Im Falle der Instanz für
Listen bedient sie sich des in Haskell vorgegebenen Werts undefined (mit dem Typ
forall a.a), um eine Auswertung zu vermeiden aber dennoch den Typ bestimmen zu
können.
Der cast Operator kann nun mit Hilfe der überladenen typeOf Funktion implementiert
werden:
cast :: (Typeable a, Typeable b) => a -> Maybe b
cast x = r
where
r = if typeOf x == typeOf (get r)
then Just (unsafeCoerce x)
else Nothing
get :: Maybe a -> a
get x = undefined
Ein Laufzeitvergleich der Typrepräsentationen von dem Argument x und dem Resultat r
bestimmt, ob eine Umwandlung mit Hilfe der unsafeCoerce Funktion durchgeführt wird
oder Nothing zurückgeliefert wird. Die unsafeCoerce Funktion hat den Typ:
unsafeCoerce :: a -> b
Die unsafeCoerce Funktion arbeitet wie die Identitätsfunktion, allerdings wird dabei der
Typ verändert. Dies ist zwar unsicher, aber in diesem Fall stimmen die in der Klasse
Typeable definierten Typrepräsentationen überein, und somit ist die Typumwandlung
möglich. Um die Funktionsweise des cast Operators besser verstehen zu können, soll der
folgende Ausschnitt aus einer interaktiven GHCi Session dienen:
Prelude> (cast ’a’) :: Maybe Char
Just ’a’
Prelude> (cast ’a’) :: Maybe Bool
Nothing
Prelude> (cast True) :: Maybe Bool
Just True
Der zur Laufzeit vorherrschende Typsignaturkontext entscheidet also über das Ergebnis des
cast Operators.
=XVDPPHQIDVVXQJ
Wir haben anhand des Beispielunternehmens die zwei wichtigen Arten der strategischen
Programmierung kennen gelernt: die generischen monadischen Transformationen (und die
„ normale“ Transformation als deren Speziallfall) und generische Abfragen. In der Strafunski
Bibliothek werden wir diese beiden Arten wiedererkennen (in Form der type-preserving und
type-unifying funktionalen Strategien). Mit Hilfe der beiden Klassen Typeable und Term
ist es möglich, verschiedene Kombinatoren zu definieren, die verschiedene Traversierungen
auf heterogenen Datenstrukturen ermöglichen, ohne dass sich der Programmierer mit dem
Aufbau der Datenstruktur auseinandersetzen muss. Allerdings müssen die Instanzen für die
12
beiden Klassen Typeable und Term passend zu der vorliegenden Datenstruktur abgeleitet
werden. Da diese aber sehr simpel aufgebaut sind, ist es möglich, dass diese Instanzen
automatisch generiert werden (siehe „ Präcompiler“ , Kapitel III).
Somit benötigt man drei Teile, um den gewünschten „ problemrelevanten“ Effekt mit Hilfe der
strategischen Programmierung zu erreichen:
1. Arbeit durch den Programmierer
Der Programmierer muss den „ problemrelevanten“ Code implementieren (z.B. incS,
lookupE, billS) und diesen mit dem passenden typerweiternden Kombinator
verbinden und das gewünschte Traversierungsschema auswählen.
2. „ Mechanisch“ generierter Code
Zu den beiden Klassen Typeable und Term müssen die Instanzen zu allen
Datentypen der vorliegenden Datenstruktur erstellt werden. Dies kann manuell oder
automatisch durchgeführt werden (z.B. mit Hilfe des DrIFT Präcompilers oder
Template Haskell).
3. Bibliothek
Eine Bibliothek enthält eine Auswahl von Kombinatoren. Verschiedene
Kombinationen ermöglichen unterschiedliche Traversierungen, dadurch kann eine
Reihe von verschiedenen Traversierungsschemata(siehe „ Bibliothek“ , Kapitel III)
angeboten werden.
,,,6WUDIXQVNL6RIWZDUH3DNHW
Der Name „ Strafunski“ ist ein zusammengesetztes Kunstwort: „ Stra“ steht für Strategien,
„ fun“ für funktionale Programmierung und deren harmonische Kombination soll an die Musik
von Igor Stravinsky erinnern. Das Strafunski Paket besteht aus zwei Hauptteilen, einer
Bibliothek mit funktionalen Strategien und einem Präcompiler. Um mit dem Strafunski Paket
zu arbeiten, müssen folgende Schritte ausgeführt werden:
1. Anwendung des Präcompilers auf das Datentypsystem, auf dem gearbeitet werden
soll.
2. Import der präcompilierten Datentypen und des StrategyLib Moduls in die
Applikation.
3. Die in der Bibliothek enthaltenen Strategie Kombinatoren müssen nach Bedarf
ausgewählt, kombiniert und spezialisiert werden und auf die zu bearbeitenden Terme
angewendet werden.
%LEOLRWKHN
6WUDWHJ\/LE
Die ganze Bibliothek besteht aus mehreren Haskell Modulen, die über das Hauptmodul
StrategyLib importiert werden können.
6WUDWHJ\3ULPLWLYHV
Dieses Modul enthält eine Grundmenge von Strategie Kombinatoren. Es besteht aus
abstrakten Datentypen und verbirgt somit die interne Implementierung. Es existieren sogar
verschiedene Implementierungen mit unterschiedlichen Eigenschaften in den Bereichen
Performance, Erweiterbarkeit und dem Ausnutzen von Typeigenschaften. In der
Paketkonfiguration können diese verschiedenen Module ausgewählt werden. Die meisten
13
Strategie Kombinatoren treten in zwei Formen auf, type-preserving und type-unifying (siehe
oben).
-- type-preserving, vereinfacht: für alle t, t -> m t
data TP m = abstract
-- type-unifying, vereinfacht: für alle t, t -> m u
data TU u m = abstract
Hierzu eine Auswahl der wichtigsten Basis Strategie Kombinatoren:
--Anwendung einer Strategie, für einen Term Typ t überladen:
applyTP :: (Monad m,Term t) => TP m -> t -> m t
applyTU :: (Monad m,Term t) => TU u m -> t -> m u
--Aktualisierung einer Stategie ; Hinzufügen von typspezifischem Verhalten:
adhocTP :: (Monad m,Term t) => TP m -> (t -> m t) -> TP m
adhocTU :: (Monad m,Term t) => TU a m -> (t -> m u) -> TU u m
--Generische monadische Identitätsfunktion :
idTP
:: Monad m => TP m
--Generische monadische konstante Funktion :
constTU :: Monad m => u -> TU u m
--Immer fehlschlagende Strategie; Fehlschlag kann über die Monade abgefangen werden:
failTP :: MonadPlus m => TP m
failTU :: MonadPlus m => TU u m
--Hintereinanderausführung von Strategien, wobei die erste Strategie den Typ beibehält
--(typ-preserving):
seqTP
:: Monad m => TP m -> TP m -> TP m
seqTU
:: Monad m => TP m -> TU u m -> TU u m
--Anwendung von alternativen Strategien, falls die erste fehlschlägt wird die zweite
--ausgeführt:
choiceTP :: MonadPlus m => TP m -> TP m -> TP m
choiceTU :: MonadPlus m => TU u m -> TU u m -> TU u m
--Anwendung des Arguments auf alle Kindsknoten:
allTP
:: Monad m => TP m -> TP m
allTU
:: (Monad m,Monoid u) => TU u m -> TU u m
--Anwendung des Arguments auf einen Kindsknoten; von links nach rechts; im Falle eines
--Fehlschlags wird der nächste probiert:
oneTP
:: MonadPlus m => TP m -> TP m
oneTU
:: MonadPlus m => TU u m -> TU u m
14
7KHPHV
Es existieren mehrere Module, die verschiedene Bereiche der generischen Programmierung
abdecken. Es folgt eine ausgewählte Vorstellung einiger Themes mit Auszügen aus den
Modulen (Es werden nur Strategien vorgestellt, deren Bestandteile in dieser Arbeit definiert
sind):
-- Das FlowTheme enthält Kombinatoren, die den Datenfluss beeinflussen und kontrollieren:
try s = s ‘choiceTP‘ idTP
--Das FixpointTheme befasst sich mit der iterativen Termtransformation; es bricht ab
--falls ein „ Fixpunkt“ erreicht wird:
repeat s
= try (seqTP s (repeat s))
outermost s = repeat (oncetd s)
innermost s = repeat (oncebu s)
--Das TraversalTheme enthält wichtige Traversierungsschemata:
topdown s = s ‘seqTP‘ (allTP (topdown s))
bottomup s = (allTP (bottomup s)) ‘seqTP‘ s
stoptd s
= s ‘choiceTP‘ (allTP (stoptd s))
oncetd s
= s ‘choiceTP‘ (oneTP (oncetd s))
oncebu s
= (oneTP (oncebu s)) ‘choiceTP‘ s
select s
= s ‘choiceTU‘ (oneTU (select s))
Der Vollständigkeit halber eine Auflistung der restlichen Themes:
• NameTheme – abstrakte Algorithmen zur Namensanalyse (wichtig für das Bearbeiten
von Sprachen)
• RefactoringTheme
• MetricsTheme – Kombinatoren für die Berechnung von Software Metriken
• PathTheme – adaptive Programmierung mit Baumpfaden
• KeyholeTheme – Strategien mit versteckten Strategietypen (TP bzw. TU)
• EffectTheme – Die Behandlung von monadischen Effekten für Strategien
• ContainerTheme – Strategien als heterogene Behälter
Des Weiteren wird die Verarbeitung von XML Dokumenten unterstützt.
7HUP
Dieses Modul bietet ein generisches Term Interface in Form einer Typ Klasse Term (siehe
auch Kapitel II). Die interne Umsetzung bleibt dem Benutzer verborgen.
3UlFRPSLOHU
Die Benutzung der Strafunski Bibliothek in einer Applikation erfordert, dass für die in der
Applikation vorhandenen Datentypen Instanzen der Term Klasse zur Verfügung stehen (siehe
oben). Das Strafunski Paket enthält einen Präcompiler, der diese Instanzen automatisch
generiert. Dazu wird eine Erweiterung des DrIFT tools (früher: Derive tool) benutzt.
15
,9$EVFKOLH‰HQGH%HWUDFKWXQJ
9RUWHLOH
• 3UlJQDQ]
Bei der Programmierung auf heterogenen Datenstrukturen können generische strategische
Programme eine Aufgabenstellung wesentlich prägnanter erfassen als nicht generische
Programme. Der generische Zugriff auf die Datenstruktur sorgt dafür, dass sich der
Programmierer nicht mit jedem auftretenden Typ einzeln befassen muss und vermeidet
somit Wiederholungsarbeiten.
• 7UHQQXQJYRQEDVLFDFWLRQVXQGWUDYHUVDOFRQWURO
Die strategischen Kombinatoren ermöglichen das eindeutige Aufspalten unterschiedlicher
Teilaufgaben in abstrakte Funktionen, die normalerweise zusammen in einem
Codefragment untergebracht sind. So erlauben Strategien die Trennung von z.B.
„ problemrelevanten“ Grundfunktionen, Traversierungskontrolle, Voraussetzungen bzw.
Bedingungen für die Anwendbarkeit und Effekten. Diese Teilbereiche können somit
unabhängig voneinander analysiert, entwickelt, getestet und gewartet werden.
• :LHGHUYHUZHQGEDUNHLW
Der Einsatz von Strategien ermöglicht Wiederverwendbarkeit auf zwei verschiedenen
Ebenen. So muss innerhalb einer Applikation eine „ problemrelevante“ Funktion nur
einmal implementiert werden und kann dort auf einfache Weise mehrfach eingesetzt
werden. Applikationsübergreifend können gewisse Strategien wie z.B. allgemeine
Traversierungsschemata oder Anwendungsgebiet-spezifische generische Algorithmen
problemlos wiederverwendet werden.
• $QSDVVXQJVIlKLJNHLW
Die Programmierung mit Strategien verhindert, dass Änderungen der Datenstruktur (z.B.
Typänderungen, algebraische Typdefinitionen, Klassenhierarchien) übermäßige
Veränderungen im Programmcode erfordern. Somit ist die Wartung solcher Systeme
einfach zu handhaben.
1DFKWHLOH
Das Laufzeitverhalten von generischen strategischen funktionalen Programmen kann sich
deutlich von handgeschriebenen unterscheiden. Benchmarkanalysen der in Kapitel II
entwickelten Programme für das Anheben des Gehalts (increase) zeigen, dass das
generische Programm 3,5mal langsamer läuft als das handgeschriebene. Dies liegt
hauptsächlich an der unvorteilhaften Implementierung des cast Operators. Bei jedem
Traversierungsschritt müssen Typüberprüfungen mit Hilfe des cast Operators und der
Klasse Typeable durchgeführt werden und diese „ bremsen“ das Programm aus. Wenn
jedoch eine Typüberprüfung durch Compilerunterstützung vereinfacht wird, wird dies das
Laufzeitverhalten positiv beeinflussen.
Des Weiteren können bei generischen Programmen unnötige Traversierungen vorkommen. So
existiert beispielsweise in Haskell kein eigener Datentyp für Strings, diese werden intern
durch eine Liste über Character ( [Char] ) repräsentiert. Und diese Liste wird auch Element
für Element traversiert. Es können natürlich Abbruchbedingen eingebaut werden, diese
müssen aber manuell erstellt werden (z.B. ist dem Computer nicht wie dem Benutzer
instinktiv bewusst, dass innerhalb eines Strings kein Salary Knoten auftreten kann). Um
solche Abbruchbedingungen effektiv zu implementieren, muss eine Analyse der
Datenstruktur durchgeführt werden, dies kann sich allerdings in einigen Fällen durchaus
lohnen. Auch können einige Optimierungsmethoden, die bei handgeschriebenen Programmen
funktionieren, nicht auf generische Traversierungsschemata angewendet werden. Dies liegt
16
vor allem daran, dass die gmap Funktionen auf der Term Klasse beruhen und Funktionen
höherer Ordnung benutzen.
$XVEOLFN
Die Idee der strategischen Programmierung ist nicht nur auf die Sprache Haskell oder
funktionale Programmierung beschränkt. Es existieren mehrere Arbeiten, die sich mit diesem
Thema befassen und eine rein theoretische Grundlage auf mathematischer Basis hierzu
geschaffen haben. Darüber hinaus gibt es mehrere Inkarnationen der strategischen
Programmierung:
• Stratego (eine Sprache, die auf Grundlage der strategischen Programmierung
geschaffen wurde)
• Java (Visitor Pattern)
• Prolog (Language Processing Toolbox)
• C (Grammar Deployment Kit)
Des Weiteren wurden mehrere „ Design Patterns“ für die strategische Programmierung
entwickelt (siehe Literaturverzeichnis), die dem Programmierer einen Einblick in die
Benutzungsmöglichkeiten sowie Anstöße bzw. Anleitungen für die Benutzung liefern.
/LWHUDWXUYHU]HLFKQLV
R. Lämmel, S. Peyton-Jones. Scrap Your Boilerplate: A Practical Design Pattern for Generic
Programming. In 3URF2I7KH$&06,*3/$1:RUNVKRSRQ7\SHVLQ/DQJXDJH'HVLJQDQG
,PSOHPHQWDWLRQ7/',, ACM Press
R. Lämmel and J. Visser. Design Patterns for Functional Strategic Programming. In 3URF2I
7KLUG$&06,*3/$1:RUNVKRSRQ5XOHGEDVHG3URJUDPPLQJ58/(¶, Pittsburgh, USA,
Oct.5 2002, ACM Press. 14pages.
R. Lämmel, E. Visser and J. Visser. The Essence of Strategic Programming – An inquiry into
trans-paradigmatic genericity. Draft; available from the authors’ web site, 2002.
R. Lämmel and J. Visser. Typed Combinators for Generic Traversal. In 3URF3UDFWLFDO
$VSHFWVRI'HFODUDWLYH3URJUDPPLQJ3$'/, Portland, OR, USA, volume 2257 of
LNCS, pages 137-154. Springer-Verlag, Jan.2002
Strafunski Webseite. http://www.cs.vu.nl/Strafunski/ (Abruf: 2004-01-28)
17
Herunterladen