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$EVFKOLHHQGH%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