Seminar Programmiersprachen und Programmiersysteme WS 2012/13 Arrows in Haskell Seminarausarbeitung von Jan Meyer 8. März 2013 Dozent: Priv.-Doz. Dr. Frank Huch Institut für Informatik Christian-Albrechts-Universität zu Kiel Programmiersprachen und Übersetzerkonstruktion Inhaltsverzeichnis 1 Einleitung 3 2 Arrows in Haskell 2.1 Die Typklasse Category . . . . . . . . . . . . 2.2 Die Typklasse Arrow . . . . . . . . . . . . . . 2.3 Die Typklassen ArrowZero und ArrowPlus 2.4 Die Typklasse ArrowChoice . . . . . . . . . 2.5 Die Typklasse ArrowLoop . . . . . . . . . . 2.6 Die Typklasse ArrowApply . . . . . . . . . . 2.7 Kleisli Arrows . . . . . . . . . . . . . . . . . . . . . . . . . 3 4 5 9 11 13 14 16 3 Arrows und Monaden 3.1 LL-Parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.2 Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 17 21 4 Zusammenfassung und Bewertung 22 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 Einleitung In der funktionalen Programmierung existieren eine Reihe von verschiedenen Abstraktionsmöglichkeiten. Am verbreitesten sind dabei Funktionen wie map und f old, die es mit Hilfe von Funktionen höherer Ordnung ermöglichen übliche Programmmuster zu vereinheitlichen. Eine andere Abstraktionsmöglichkeit bilden Berechnungen. Zweck einer Berechnungsabstraktion ist die Bildung einheitlicher, übersichtlicherer Programme. In Haskell werden Berechnungen meistens mit Hilfe von Monaden abstrahiert. Monaden bieten eine standardisierte Schnittstelle zur Verknüpfung von Programmteilen, wodurch jede Monade verschiedene Eigenschaften abstrahieren kann, zum Beispiel Fehlerbehandlung, Ein- und Ausgabe oder Nebenläufigkeit. Letztlich kann ein monadischer Wert als Ergebnis einer Berechnung in einem bestimmten Kontext betrachtet werden. Eine alternative Abstraktionsmöglichkeit für Berechnungen bilden Arrows. Sie unterscheiden sich von Monaden durch eine eine andere Sichtweise auf Berechnungen. Während Monaden Operationen bilden, die ein Ergebnis produzieren, bilden Arrows eine Transformation, die aus einer Eingabe ein Ergebnis erzeugen. Sie orientieren sich demnach stärker am Prinzip einer Funktion. Zur Verknüpfung von abstrahierten Programmteilen nutzen Arrows dabei eine Reihe von Kombinatoren, die die Abstraktion verschiedener Eigenschaften ermöglichen. Diese Seminararbeit wird sich in Kapitel 2 zunächst mit den Typdefinitionen und möglichen Implementierungen der einzelnen, zu Arrows gehörenden Typklassen beschäftigen. Dabei soll vor allem ein Verständnis für die Verwendung der jeweiligen Kombinatoren geschaffen werden und letztlich die Verbindung zwischen Arrows und Monaden geklärt werden. In Kapitel 3 werden Arrows und Monaden an Hand einer Parserimplementierung verglichen. Außerdem werden die jeweiligen syntaktischen Spracherweiterungen betrachtet. In Kapitel 4 wird diese Seminararbeit zusammengefasst und das Programmieren mit Arrows bewertet. 2 Arrows in Haskell Arrows betrachten Berechnungen als Übergang zwischen zwei Typen in einem bestimmten Kontext, zum Beispiel einfache Funktionen (a → b), Berechnungen mit optionalem Ergebnis (a → M aybe b) oder Berechnungen mit beliebig vielen Ergebnissen (a → [b]). In Haskell nennt man eine Berechnungsart Arrow, wenn diese die Typklasse Arrow, die die Typklasse Category beerbt, instanziiert. Aus diesem Grund werden zunächst die Typklassen Category und Arrow vorgestellt. Danach werden eine Reihe weiterer Typklassen betrachtet, die speziellere Arrows beschreiben. Die im Folgenden angegebenen Definitionen der Typklassen entstammen den Modulen Control.Category und Control.Arrow des Glasgow Haskell Compilers GHC [1, 2]. 3 2 Arrows in Haskell 2.1 Die Typklasse Category Die Typklasse Category ermöglicht eine einheitliche Komposition von Berechnungen. class Category c where id :: c a a (.) :: c b d -> c a b -> c a d Listing 2.1: Klassendefinition der Typklasse Category Für jede Berechnungsart c bildet die Identitätsberechnung jedes Element eines Typs a auf sich selbst ab. Der Kombinator (○) komponiert zwei Berechnungen der Typen a nach b und b nach d. Die Verkettung dieser Berechnungen ist demnach eine Berechnung des Typs a nach d. Typklassen bieten in Haskell eine Möglichkeit Funktionen strukturiert zu Überlagern. Strukturiert bedeutet in diesem Zusammenhang, dass eine typkorrekte Implementierung der Funktionen der Klasse in der Regel nicht ausreicht. Häufig werden zusätzliche Gesetze angegeben, die eine Instanz der Typklasse erfüllen muss. Erst diese sichern Benutzern der Instanz die gewünschten Eigenschaften der Typklasse zu. Die Typklasse Category bildet die algebraische Struktur eines Monoids bezüglich der Komposition über eine Menge von Berechnungen. Daher muss die Identitätsberechnung id das neutrale Element bezüglich der Komposition für alle Berechnungen darstellen. Weiterhin muss für die Komposition durch (○) das Assoziativgesetz gelten. Jede gültige Instanz der Typklasse Category muss aus diesen Gründen die folgenden Gesetze der Typklasse erfüllen. id ○ f = f f ○ id = f (h ○ g) ○ f = h ○ (g ○ f ) Tabelle 2.1: Gesetze der Typklasse Category Neben der Kompositionsfunktion (○) der Typklassendefinition existieren für Instanzen der Typklasse Category die folgenden, vordefinierten Kombinatoren. Sie umgehen die mathematische Formulierung der Kompositionsfunktion (○) zu Gunsten einer besseren Lesbarkeit von Programmen, da die Anwendungsreihenfolge der Berechnungen deutlicher hervorgehoben wird. ( < < <) :: Category c = > c b d -> c a b -> c a d ( < < <) = (.) ( > > >) :: Category c = > c a b -> c b d -> c a d f >>> g = g . f Listing 2.2: Vordefinierte Kombinatoren für Instanzen der Typklasse Category Der Kombinator (⋘) ist eine Umbenennung der Kompositionsfunktion (○). Der Kombinator (⋙) verkettet ebenfalls Berechnungen, diese werden jedoch in umgekehrter Reihenfolge, also von links nach rechts, angegeben. 4 2 Arrows in Haskell Berechnungen des Typs a → b, also einfache Funktionen, können die Typklasse Category instanziieren. instance Category ( - >) where id = Prelude . id (.) = ( Prelude ..) Listing 2.3: Instanz der Typklasse Category für Funktionen Die Identitätsfunktion id und die Kompositionsfunktion (○) sind für Funktionen in Haskell bereits im Prelude implementiert und können übernommen werden. Durch die Instanziierung ist es möglich, Funktionen mit Hilfe der Kombinatoren (⋙) und (⋘) zu verketten. Die Identitätsfunktion id verhält sich dabei neutral und auf Grund des Assoziativgesetzes kann auf eine Klammerung verzichtet werden. > 3 > 8 > 7 > 8 id 3 ((+ 1) >>> (* 2)) 3 ((+ 1) <<< (* 2)) 3 ((+ 1) >>> id >>> (* 2)) 3 Listing 2.4: Beispiele für die Benutzung der Typklasse Category Zwar ist man mit Hilfe der Typklasse Category in der Lage Kombinationen von Berechnungen anzugeben, die Anwendungsmöglichkeiten sind aber sehr begrenzt. So ist es mit Hilfe der bisherigen Kombinatoren nicht möglich, eine Berechnung anzugeben, die die Ergebnisse zweier anderer Berechnungen kombiniert. liftC2 :: Category c = > ( b -> d -> e ) -> c a b -> c a d -> c a e liftC2 op f g = ... Listing 2.5: Beispiel für Grenzen der Typklasse Category Durch eine einfache Verkettung ginge der ursprüngliche Eingangswert verloren, die zweite Berechnung könnte nicht mehr auf diesen angewendet werden. Es fehlt also eine Möglichkeit Werte durch eine Berechnung unangetastet durchzureichen. Weiterhin fehlen Möglichkeiten mehrstellige Berechnungen zu verketten oder einfache Funktionen zu Berechnungen zu liften. Um auch solche Berechnungen abstrahiert angeben zu können, bedarf es weiterer Hilfsfunktionen und Kombinatoren. Diese sind Teil der Typklasse Arrow. 2.2 Die Typklasse Arrow Instanzen der Typklasse Category sind nicht in der Lage mit Zwischenergebnissen zu arbeiten. Dieses Problem lässt sich jedoch mit Hilfe von Tupeln lösen. Dazu wird eine neue Funktion eingeführt, die eine Berechnung nur auf die erste Komponente eines Tupels anwendet. Diese Funktion heißt f irst und ist Teil der Typklasse Arrow, die die Typklasse Category beerbt. Des Weiteren wird der Typkonstruktor arr eingeführt, der Funktionen des Typs b → c zu Arrows des Typs a b c liftet. 5 2 Arrows in Haskell class Category a = > Arrow a where arr :: ( b -> c ) -> a b c first :: a b c -> a (b , d ) (c , d ) ... Listing 2.6: Klassendefinition der Typklasse Arrow Um eine gültige Instanz der Typklasse Arrow zu werden, muss eine Berechnungsart lediglich diese beiden Funktionen implementieren. Zusätzlich besteht die Typklasse aber aus weiteren Funktionen, die das Arbeiten mit Tupeln weiter vereinfachen, denn sie abstrahieren übliche Programmstrukturen. Deren vordefinierte Implementierungen können wie bei jeder anderen Typklasse bei Bedarf, zum Beispiel zur Optimierung, überschrieben werden. Die Funktion second stellt das passende Gegenstück zu f irst dar und wendet eine Berechnung nur auf die zweite Komponente eines Tupels an. Der Kombinator (∗∗∗) ist eine Kombination aus f irst und second. Er wendet die erste Berechnung auf die erste Komponente und die zweite Berechnung auf die zweite Komponente eines Tupels an. Der Kombinator (&&&) erstellt zunächst ein Tupel aus einer Eingabe, bevor er sich wie (∗∗∗) verhält. ... second :: a b c -> a (d , b ) (d , c ) second f = arr swap >>> first f >>> arr swap where swap :: (x , y ) -> (y , x ) swap ~( x , y ) = (y , x ) (***) :: a b c -> a d e -> a (b , d ) (c , e ) f *** g = first f >>> second g (&&&) :: a b c -> a b d -> a b (c , d ) f &&& g = arr (\ b -> (b , b )) >>> ( f *** g ) Listing 2.7: Fortgesetzte Klassendefinition der Typklasse Arrow Es gibt zwei Gründe, warum es notwendig ist die Funktion f irst zu implementieren und die restlichen Funktionen mit ihrer Hilfe vordefiniert sind. Zum einen ist sie am einfachsten zu Implementieren. Zum anderen ist es wichtig die Auswertungsreihenfolge der Arrows zu beachten, insbesondere um die Reihenfolge von möglichen Seiteneffekten zu bewahren. Um diese einzuhalten, muss der Kombinator (∗∗∗) die Berechnung f vor g, wie vordefiniert, ausführen. Neben den zusätzlichen Kombinatoren innerhalb der Klassendefinition existieren weitere vordefinierte Kombinatoren zur Kombination von Arrows mit einfachen Funktionen [2]. Zusätzlich ist die Identitätsberechnung für jeden Arrow vordefiniert. returnA :: Arrow a = > a b b returnA = arr id Listing 2.8: Vordefinierte Funktion für Instanzen der Typklasse Arrow Auch bei der Typklasse Arrow reicht eine typkorrekte Implementierung ihrer Funktionen nicht aus. Jede gültige Instanz muss zusätzlich die folgenden Gesetze erfüllen. 6 2 Arrows in Haskell arr id arr (f ⋙ g) f irst (arr f ) f irst (f ⋙ g) f irst f ⋙ arr f st f irst f ⋙ arr (id ∗∗∗ g) f irst (f irst f ) ⋙ arr assoc = = = = = = = id arr f ⋙ arr g arr (f irst f ) f irst f ⋙ f irst g arr f st ⋙ f arr (id ∗∗∗ g) ⋙ f irst f arr assoc ⋙ f irst f where assoc ∶∶ ((a, b), c) → (a, (b, c)) assoc ((a, b), c) = (a, (b, c)) Tabelle 2.2: Gesetze der Typklasse Arrow Funktionen des Typs a → b sind nicht nur Instanzen der Typklasse Category, sie können auch die Typklasse Arrow instanziieren und sind somit in der Lage abstrahiert mit Zwischenergebnissen zu arbeiten. instance Arrow ( - >) where arr f = f first f = \( x , y ) -> ( f x , y ) Listing 2.9: Instanz der Typklasse Arrow für Funktionen Einfache Berechnungen müssen nicht mit Hilfe der Funktion arr zu Berechnungen geliftet werden, ihr Typ ist bereits passend. Aus diesem Grund kann bei einfachen Funktionen auf den Typkonstruktor arr verzichtet werden. > ( first (+ 1)) (2 , 2) (3 , 2) > ( second (+ 1)) (2 , 2) (2 , 3) > ((+ 1) *** (* 2)) (2 , 2) (3 , 4) > ((+ 1) &&& (* 2)) 2 (3 , 4) Listing 2.10: Beispiele für die Benutzung der Typklasse Arrow Allein mit den Kombinatoren der Typklasse Category war es bisher nicht möglich Zwischenergebnisse durchzureichen. Erst mit Hilfe von Tupeln und den Kombinatoren der Typklasse Arrow ist dies möglich. So lässt sich zum Beispiel die Funktion lif tA2 implementieren. liftA2 :: Arrow a = > ( c -> d -> e ) -> a b c -> -- liftA2 op f g = arr (\ b -> (b , b )) >>> ->>> arr (\( c , b ) -> (b , c )) >>> ->>> arr (\( d , c ) -> (c , d )) >>> liftA2 op f g = ( f &&& g ) >>> arr ( uncurry op ) a b d -> a b e first f first g arr ( uncurry op ) > liftA2 (+) (+ 1) (* 2) 2 7 Listing 2.11: Lösung für die Grenzen der Typklasse Category mit Hilfe der Typklasse Arrow 7 2 Arrows in Haskell Bis jetzt wurden die Typklassen Category und Arrow lediglich als neue Notation für die Kombination von Funktionen betrachtet. Sie sind aber vor allem dafür geeignet unterschiedliche Arten von Berechnungen zu Verallgemeinern. Eine solche Berechnungsart sind Berechnungen mit optionalem Ergebnis. Dabei ist es möglich, dass eine Berechnung zu einem oder keinem Ergebnis kommt. Das Ergebnis wird daher mit Hilfe der Datenstruktur M aybe gekapselt. Falls die Berechnung zu einem Ergebnis kommt, wird dieses mit Hilfe des Konstruktors Just ausgedrückt. Ansonsten wird der Konstruktor N othing, ohne weitere Informationen, zurückgegeben. Um das Verhalten der Funktionen der Typklassen Category und Arrow an Berechnungen mit optionalem Ergebnis des Typs a → M aybe b anzupassen, muss zunächst ein neuer Datentyp eingeführt werden. Dieser neue Datentyp M F kann zu einer Instanz der Typklassen erklärt werden, wobei der Fehlerfall N othing propagiert wird. newtype MF a b = MF { runMF :: a -> Maybe b } instance Category MF where id = MF (\ a -> Just a ) ( MF g ) . ( MF f ) = MF (\ a -> case f a of Nothing -> Nothing Just b -> g b ) instance Arrow MF where arr f = MF (\ b -> Just ( f b )) first ( MF f ) = MF (\( b , d ) -> case f b of Nothing -> Nothing Just c -> Just (c , d )) Listing 2.12: Instanzen der Typklassen Category und Arrow für Berechnungen mit optionalem Ergebnis Eine weitere Art von Berechnungen simuliert Nichtdeterminismus. Statt genau einem Ergebnis kann diese beliebig viele, auch keine, Ergebnisse haben. In Haskell werden diese Ergebnisse in der Regel in einer Liste angegeben, die Berechnungen haben also den Typ a → [b]. Analog zu Berechnungen mit einem optionalen Ergebnis muss auch hier zunächst der passende Datentyp LF angegeben werden, bevor dieser die Typklassen instanziieren kann. newtype LF a b = LF { runLF :: a -> [ b ] } instance Category LF where id = LF (\ a -> [ a ]) ( LF g ) . ( LF f ) = LF (\ a -> [ c | b <- f a , c <- g b ]) instance Arrow LF where arr f = LF (\ b -> [ f b ]) first ( LF f ) = LF (\( b , d ) -> [( c , d ) | c <- f b ]) Listing 2.13: Instanzen der Typklassen Category und Arrow für Berechnungen mit beliebig vielen Ergebnissen Mit Hilfe der Funktionen und Kombinatoren der Typklassen Category und Arrow lassen sich Programme, obwohl sie zwei verschiedene Berechnungsarten nutzen, völlig analog angeben. 8 2 Arrows in Haskell > let f = arr (+ 1) > let g = arr (* 2) > runMF ( first f ) (2 , 2) Just (3 , 2) > runMF ( second f ) (2 , 2) Just (2 , 3) > runMF ( f >>> g ) 3 Just 8 > runMF ( f *** g ) (2 , 2) Just (3 , 4) > runMF ( f &&& g ) 2 Just (3 , 4) > runMF ( liftA2 (+) f g ) 2 Just 7 > runLF ( first f ) (2 , 2) [(3 , 2)] > runLF ( second f ) (2 , 2) [(2 , 3)] > runLF ( f >>> g ) 3 [8] > runLF ( f *** g ) (2 , 2) [(3 , 4)] > runLF ( f &&& g ) 2 [(3 , 4)] > runLF ( liftA2 (+) f g ) 2 [7] Listing 2.14: Beispiele für die Benutzung der Typklassen Category und Arrow Die Typklasse Arrow bildet die Grundlage für eine Reihe weiterer Typklassen. In diesen werden weitere, speziellere Verknüpfungsarten für Berechnungen abstrahiert. 2.3 Die Typklassen ArrowZero und ArrowPlus Berechnungen mit optionalem Ergebnis und Berechnungen mit beliebig vielen Ergebnissen haben jeweils zwei Fälle: Einen Fehlerfall (N othing, bzw. die leere Liste) und einen Erfolgsfall (Just, bzw. die nicht leere Liste). Bei solchen Arten von Berechnungen ist man unter Umständen an einer Kombination von Ergebnissen mehrerer Berechnungen zu einer Eingabe interessiert, zum Beispiel beim Anwenden mehrerer Parser auf ein Eingabewort. Berechnungsarten, die einen expliziten Fehlerfall besitzen, werden mit Hilfe der Typklasse ArrowZero zusammengefasst. Diese besteht aus der Berechnung zeroArrow, die den jeweiligen Fehlerfall der Berechnungsart erzeugt. class Arrow a = > ArrowZero a where zeroArrow :: a b c Listing 2.15: Klassendefinition der Typklasse ArrowZero Da die beiden oben genannten Berechnungsarten einen expliziten Fehlerfall besitzen, können sie die Typklasse ArrowZero instanziieren. instance ArrowZero MF where zeroArrow = MF ( const Nothing ) instance ArrowZero LF where zeroArrow = LF ( const []) Listing 2.16: Instanzen der Typklasse ArrowZero für Berechnungen mit optionalem Ergebnis und Berechnungen mit beliebig vielen Ergebnissen Um die Ergebnisse von Berechnungen zusammenzufassen, bedarf es eines weiteren Kombinators. Dieser ist Teil der Typklasse ArrowP lus, die die Typklasse ArrowZero beerbt, und heißt (<+>). 9 2 Arrows in Haskell class ArrowZero a = > ArrowPlus a where ( <+ >) :: a b c -> a b c -> a b c Listing 2.17: Klassendefinition der Typklasse ArrowP lus Berechnungen mit beliebig vielen Ergebnissen können die Typklasse ArrowP lus instanziieren. Die beiden Ergebnislisten werden dabei konkateniert, so dass kein Ergebnis verfällt. Bei Berechnungen mit optionalem Ergebnis ist das anders. Sollten beide Berechnungen zu einem Ergebnis kommen, muss ein Ergebnis ausgewählt werden. In der Regel wird daher das Ergebnis der ersten Berechnung verwendet, wodurch die zweiten Berechnung verfällt. instance ArrowPlus MF where ( MF f ) <+ > ( MF g ) = MF (\ b -> case f b of Nothing -> g b Just c -> Just c ) instance ArrowPlus LF where ( LF f ) <+ > ( LF g ) = LF (\ b -> f b ++ g b ) Listing 2.18: Instanzen der Typklasse ArrowP lus für Berechnungen mit optionalem Ergebnis und Berechnungen mit beliebig vielen Ergebnissen Die Typklassen ArrowZero und ArrowP lus bilden zusammen einen Monoid. Die Berechnung zeroArrow verhält sich also bezüglich der Verknüpfung (<+>) neutral, sie darf also weder Ergebnisse hinzufügen noch entfernen. Für den Kombinator (<+>) gilt außerdem das Assoziativgesetz gelten. zeroArrow <+> f = f f <+> zeroArrow = f (f <+> g) <+> h = f <+> (g <+> h) Tabelle 2.3: Gesetze der Typklassen ArrowZero und ArrowP lus Die folgenden Beispiele verdeutlichen vor allem den Unterschied zwischen beiden Berechnungsarten bezüglich der Verwendung des Kombinators (<+>). Bei Berechnungen mit optionalem Ergebnis wird das Ergebnis jeder weiteren erfolgreichen Teilberechnung verworfen, bei Berechnungen mit beliebig vielen Ergebnissen bleiben alle Ergebnisse erhalten. Trotzdem erfüllen beide Berechnungsarten die Gesetze der Typklassen. > let f = arr (+ 1) > let g = arr (* 2) > runMF zeroArrow 3 Nothing > runMF ( f <+ > g ) 3 Just 4 > runMF ( f <+ > zeroArrow <+ > g ) 3 Just 4 > runLF zeroArrow 3 [] > runLF ( f <+ > g ) 3 [4 , 6] > runLF ( f <+ > zeroArrow <+ > g ) 3 [4 , 6] Listing 2.19: Beispiele für die Benutzung der Typklassen ArrowZero und ArrowP lus 10 2 Arrows in Haskell 2.4 Die Typklasse ArrowChoice Die Typklasse Arrow ermöglicht eine vereinheitlichte Komposition von Berechnungen. Sie bietet aber keine Möglichkeit Berechnungen nur unter bestimmten Bedingungen durchzuführen, bisher wird stets jede Berechnung ausgeführt. Eine Funktion, die zwischen zwei unterschiedlichen Berechnungen wählt, modelliert eine bedingte Verzweigung. Zwar ließe sich eine solche Kontrollstruktur mit den bisherigen Kombinatoren angeben, diese wäre aber strikt, beide Berechnungen würden ausgeführt werden. Um dies zu verhindern, benötigt man einen neuen Kombinator. Eine Definition samt möglicher Implementierung einer bedingten Verzweigung, die einen solchen Kombinator nutzt, kann folgendermaßen aussehen. arrowIf :: Arrow a = > ( b -> Bool ) -> a b c -> a b c -> a b c arrowIf p f g = ( arr p &&& returnA ) >>> ( f ||| g ) Listing 2.20: Eine mögliche Implementierung einer bedingten Verzweigungsfunktion für Arrows (arr p &&& returnA) überprüft dabei das Prädikat p und erzeugt ein Tupel aus einem boolschen Wert und dem ursprünglichen Wert. Je nach Prädikatsauswertung wird dann durch den noch undefinierten Kombinator (∣ ∣ ∣) mit (f ∣ ∣ ∣ g) entweder f oder g auf die ursprüngliche Eingabe angewendet. Eine Implementierung von (∣ ∣ ∣), die ein (Bool, a) verarbeitet, funktioniert hier zwar, sie ist allgemein jedoch noch nicht optimal. Ein Tupel vom Typ (Bool, a) kann als Either a a ausgedrückt werden, wobei (T rue, a) auf Lef t a und (F alse, a) auf Right a abgebildet wird. Der Vorteil von Either gegenüber einem Tupel ist die Möglichkeit zwei unterschiedliche Typen anzugeben. Eine entsprechende Definition des Kombinators (∣ ∣ ∣) ist Teil der Typklasse ArrowChoice. class Arrow a = > ArrowChoice a where (|||) :: a b d -> a c d -> a ( Either b c ) d Listing 2.21: Klassendefinition der Typklasse ArrowChoice Die Definition von (∣ ∣ ∣) ist jedoch noch zu eingeschränkt, beide Berechnungsalternativen müssen denselben Ergebnistyp d haben. Ein neuer Kombinator, der wiederum ein Either berechnet, ist allgemeiner. Dieser Kombinator heißt (+++) und kann gleichzeitig genutzt werden, um eine Implementierung für (∣ ∣ ∣) anzugeben. class Arrow a = > ArrowChoice a where (+++) :: a b c -> a d e -> a ( Either b d ) ( Either c e ) (|||) :: a b d -> a c d -> a ( Either b c ) d f ||| g = f +++ g >>> arr untag where untag :: Either a a -> a untag ( Left x ) = x untag ( Right x ) = x Listing 2.22: Erweiterte Klassendefinition der Typklasse ArrowChoice Der Kombinator (+++) lässt sich analog zum Kombinator (∗∗∗) als Kombination zweier einfacherer Funktionen darstellen. Die Funktionen lef t und right wenden eine Berechnung nur auf eine Eingabe an, wenn diese entsprechend in einem Lef t oder einem Right verkapselt wurde. Die Funktion right lässt sich dabei mit Hilfe der Funktion lef t implementieren. Eine Instanz der Typklasse ArrowChoice muss daher nur noch eine Implementierung für lef t angeben. 11 2 Arrows in Haskell class Arrow a = > ArrowChoice a where left :: a b c -> a ( Either b d ) ( Either c d ) right :: a b c -> a ( Either d b ) ( Either d c ) right f = arr mirror >>> left f >>> arr mirror where mirror :: Either x y -> Either y x mirror ( Left x ) = Right x mirror ( Right y ) = Left y (+++) :: a b c -> a d e -> a ( Either b d ) ( Either c e ) f +++ g = left f >>> right g (|||) :: a b d -> a c d -> a ( Either b c ) d f ||| g = f +++ g >>> arr untag where untag :: Either a a -> a untag ( Left x ) = x untag ( Right x ) = x Listing 2.23: Erweiterte Klassendefinition der Typklasse ArrowChoice Funktionen des Typs a → b können die Typklasse ArrowChoice instanziieren. instance ArrowChoice ( - >) where left f = \ e -> case e of Left b -> Left ( f b ) Right d -> Right d Listing 2.24: Instanz der Typklasse ArrowChoice für Funktionen Eine Berechnung wird durch die Nutzung der Funktion lef t nur auf einen Wert angewendet, wenn dieser auch in einem Lef t gekapselt angegeben wurde, ist dies nicht der Fall, wird der Wert unangetastet weitergereicht. Auf Grund der allgemeinen Definition der Kombinatoren (+++) und (∗∗∗) können auch Berechnungen mit unterschiedlichen Ein- und Ausgabetypen kombiniert werden. > ( left (* 2)) ( Left 2) Left 4 > ( left (* 2)) ( Right 2) Right 2 > ((* 2) +++ not ) ( Left 4) Left 8 > ((* 2) +++ not ) ( Right False ) Right True > ((* 2) ||| (+ 1)) ( Left 3) 6 > ((* 2) ||| (+ 1)) ( Right 3) 4 Listing 2.25: Beispiele für die Benutzung der Typklasse ArrowChoice Mit Hilfe der Funktionen der Typklasse ArrowChoice lässt sich jetzt eine nicht strikte bedingte Verzweigung für Arrows, arrowIf , angeben und nutzen. 12 2 Arrows in Haskell arrowIf :: Arrow a = > ( b -> Bool ) -> a b c -> a b c -> a b c arrowIf p f g = arr (\ x -> if p x then Left x else Right x ) >>> ( f ||| g ) > ( arrowIf ( < 5) (* 2) (+ 1)) 2 4 Listing 2.26: Eine bessere Implementierung eines bedingten Verzweigungskombinators für Arrows 2.5 Die Typklasse ArrowLoop In Haskell entsprechen Arrows Daten, sie können daher rekursiv definiert werden, hierbei unterscheiden sie sich nicht von normalen Funktionen. Unter Umständen ist aber eine andere Art von Rekursion gefordert, bei der die Eingabe einer Berechnung von ihrer Ausgabe abhängt. Diese Berechnungen finden zum Beispiel bei der Repräsentation von Flipflops in Schaltnetzen eine Anwendung. Arrows, die Berechnungen mit Feedback unterstützen, werden in der Typklasse ArrowLoop zusammengefasst. Die Typklassendefinition besteht lediglich aus der Funktion loop. Dabei wird ein Arrow des Typs a (b, d) (c, d) berechnet, die zweite Komponente der Ein- und Ausgabe dient dabei aber lediglich als Feedback und ist nach außen nicht sichtbar. class Arrow a = > ArrowLoop a where loop :: a (b , d ) (c , d ) -> a b c Listing 2.27: Klassendefinition der Typklasse ArrowLoop Es ist zu beachten, dass anders als bei einer einfachen Form der Rekursion, mögliche Seiteneffekte bei der Verwendung der Funktion loop nur einmal ausgeführt werden. Funktionen des Typs a → b können gültige Instanzen der Typklasse ArrowLoop definieren und bieten eine Möglichkeit, das Verständnis für diese Typklasse zu verbessern. Die zweite Komponente der Ausgabe wird als zweite Komponente der Eingabe zurückgereicht. instance ArrowLoop ( - >) where loop f = \ b -> let (c , d ) = f (b , d ) in c Listing 2.28: Instanz der Typklasse ArrowLoop für Funktionen Zwar demonstriert diese Instanz das Prinzip der Typklasse, für eine sinnvolle Demonstration ist sie aber ungeeignet. Hierfür bieten sich Berechnungen über Streams von Daten an. Streams werden in Haskell in der Regel durch unendliche Listen repräsentiert, Berechnungen über Streams haben demnach den Typ [a] → [b]. Bevor eine gültige Instanz angegeben werden kann, muss zunächst ein passender Datentyp SF angegeben werden. 13 2 Arrows in Haskell newtype SF a b = SF { runSF :: [ a ] -> [ b ] } instance Category SF where id = SF id ( SF g ) . ( SF f ) = SF ( f >>> g ) instance Arrow SF where arr f = SF (\ bs -> map f bs ) first ( SF f ) = SF ( unzip >>> first f >>> uncurry zip ) instance ArrowLoop SF where loop ( SF f ) = SF (\ bs -> let ( cs , ds ) = unzip ( f ( zip bs ( stream ds ))) in cs ) where stream :: [ a ] -> [ a ] stream ~( x : xs ) = x : stream xs Listing 2.29: Instanzen der Typklassen Category, Arrow und ArrowLoop für Berechnungen über Streams Ein Beispiel, um die Verwendung der Typklasse ArrowLoop zu demonstrieren, ist die Erzeugung des Streams aller Fakultätszahlen aus dem Stream der natürlichen Zahlen. Dazu müssen zunächst zwei Hilfsfunktionen definiert werden. Die erste Hilfsfunktion delay erzeugt eine Berechnung über Streams, die ein gegebenes Element vor einen Stream hängt. Die Berechnung mul multipliziert die beiden Werte eines Tupels miteinander. Diese Implementierung nutzt aus, dass für alle Werte n ∈ N ∖ {0} gilt: n! = n ∗ (n − 1)!. Jedes n des Eingabestreams wird daher mit der Fakultät des Vorgängers multipliziert. Diese Multiplikation kann für jedes n > 0 stattfinden, da der Feedback-Stream um das bekannte Ergebnis 0! = 1 delayed und damit um eine Position verschoben wurde. delay :: a -> SF a a delay x = SF ( x :) mul :: Arrow a = > a ( Integer , Integer ) Integer mul = arr ( uncurry (*)) facSF :: SF Integer Integer facSF = loop ( mul >>> ( returnA &&& delay 1)) facs :: [ Integer ] facs = 1 : runSF facSF [1..] > take 10 facs [1 , 1 , 2 , 6 , 24 , 120 , 720 , 5040 , 40320 , 362880] Listing 2.30: Beispiel für die Benutzung der Typklasse ArrowLoop 2.6 Die Typklasse ArrowApply Einige der wichtigsten Abstraktionsmöglichkeiten funktionaler Sprachen, zum Beispiel map oder f old, sind erst durch Funktionen höherer Ordnung möglich. Arrows höherer Ordnung sowie eine Funktion, die einen Arrow auf eine passende Eingabe anwendet, sind Instanzen der Typklasse ArrowApply. Die entsprechende Applikationsfunktion heißt app. 14 2 Arrows in Haskell class Arrow a = > ArrowApply a where app :: a ( a b c , b ) c Listing 2.31: Klassendefinition der Typklasse ArrowApply Berechnungen mit optionalem Ergebnis, also Funktionen des Typs a → M aybe b, können die Typklasse ArrowApply instanziieren. instance ArrowApply MF where app = MF (\( MF f , x ) -> f x ) Listing 2.32: Instanz der Typklasse ArrowApply für Berechnungen mit optionalem Ergebnis Mit Hilfe der Funktion app ist es unter anderem möglich eine Implementierung der Funktion lef t der Typklasse ArrowChoice anzugeben. Daraus folgt, dass zu jeder Instanz der Typklasse ArrowApply auch eine gültige Instanz der Typklasse ArrowChoice angegeben werden kann [3]. Darüber hinaus ist es mit ihr möglich eine Implementierung der Funktion (≫=), sprich bind, der Typklasse M onad anzugeben. class Monad m where return :: a -> m a ( > >=) :: m a -> ( a -> m b ) -> m b Listing 2.33: Klassendefinition der Typklasse M onad Monadische Werte lassen sich mit Hilfe von Arrows repräsentieren, die unabhängig von ihrer Eingabe diesen monadischen Wert berechnen. Der Typ der Eingabe wird dementsprechend auf (), sprich unit, gesetzt. Um mit diesen Arrows die Typklasse M onad zu instanziieren, muss zunächst der entsprechende neue Datentyp ArrowM onad eingeführt werden. Die Funktion return entspricht einer Berechnung, die unabhängig von einer Eingabe einen monadischen Wert erzeugt. Der Kombinator (≫=) wendet eine Funktion auf den Inhalt eines monadischen Wertes an, um so einen neuen monadischen Wert zu erzeugen. Diese Erzeugung ist erst mit Hilfe der Funktion app möglich. newtype ArrowMonad a b = ArrowMonad ( a () b ) instance ArrowApply a = > Monad ( ArrowMonad a ) where return x = ArrowMonad ( arr ( const x )) ArrowMonad m > >= f = ArrowMonad ( m >>> arr (\ b -> let ArrowMonad g = f b in (g , ())) >>> app ) Listing 2.34: Instanz der Typklasse M onad für Instanzen der Typklasse ArrowApply Programme, die eine Berechnungsart nutzen, die die Typklasse ArrowApply instanziiert, können daher auch mit Hilfe der Funktionen return und (≫=) abstrahiert angegeben werden. Dazu müssen die entsprechenden Arrows allerdings mit Hilfe des Datentyps ArrowM onad gekapselt werden. 15 2 Arrows in Haskell mA :: ArrowMonad MF Int -- mA = ArrowMonad ( MF (\() -> Just 3))} mA = return 3 fA :: Int -> ArrowMonad MF Int -- fA = \ x -> ArrowMonad ( MF (\() -> Just ( x + 1))) fA = \ x -> return ( x + 1) > let ( ArrowMonad nA ) = ( mA > >= fA ) in runMF nA () Just 4 Listing 2.35: Beispiel für die Benutzung der Funktionen return und (≫=) der Typklasse M onad mit Hilfe des Datentyps ArrowM onad Es ist also möglich, für alle Instanzen der Typklasse ArrowApply eine gültige Instanz der Typklasse M onad anzugeben und so die die Funktionen return und (≫=) zur Programmstrukturierung zu nutzen. Um die Verbindung zwischen Monaden und Arrows besser einschätzen zu können ist es daher sinnvoll zu überlegen, ob dies auch in umgekehrter Reihenfolge funktioniert. 2.7 Kleisli Arrows Berechnungen, die einen monadischen Wert erzeugen, sind Funktionen des Typs a → m b. Sie werden auch Kleisli-Arrow einer Monade m genannt [3]. Für jede Monade lässt sich mit Hilfe der Funktionen return und (≫=) eine gültige Instanz der Typklassen Category und Arrow angeben. newtype Kleisli m a b = Kleisli { runKleisli :: a -> m b } instance Monad m = > Category ( Kleisli m ) where id = Kleisli return ( Kleisli g ) . ( Kleisli f ) = Kleisli (\ b -> f b > >= g ) instance Monad m = > Arrow ( Kleisli m ) where arr f = Kleisli ( return . f ) first ( Kleisli f ) = Kleisli (\~( b , d ) -> f b > >= \ c -> return (c , d )) Listing 2.36: Instanzen der Typklassen Category und Arrow für Kleisli-Funktionen Wenn ein Datentyp die Typklasse M onad instanziiert, können Funktionen, die einen solchen monadischen Wert erzeugen, demnach mit Hilfe der Kombinatoren der Typklassen Category und Arrow verknüpft werden. Unter den angegebenen Beschränkungen gilt dies auch für die Typklassen ArrowZero und ArrowP lus (beschränkt auf Instanzen der Typklasse M onadP lus), ArrowChoice und ArrowLoop (beschränkt auf Instanzen der Typklasse M onadF ix) [2]. 16 Der Datentyp M aybe instanziiert die Typklasse M onad. Aus diesem Grund lassen sich Funktionen des Typs a → M aybe b auch ohne neuen Datentyp M F und entsprechender Instanziierungen verknüpfen. divMF :: Kleisli Maybe ( Integer , Integer ) Integer divMF = Kleisli (\( x , y ) -> if y == 0 then Nothing else Just ( x ‘div ‘ y )) > runKleisli ( divMF >>> arr (+ 1)) (6 , 2) Just 4 > runKleisli ( divMF >>> arr (+ 1)) (6 , 0) Nothing Listing 2.37: Beispiele für die Benutzung der Funktionen arr und ⋙ der Typklassen Category und Arrow mit Hilfe von Kleisli-Arrows Kleisli-Arrows können letztlich auch die Typklasse ArrowApply instanziieren. instance Monad m = > ArrowApply ( Kleisli m ) where app = Kleisli (\( Kleisli f , x ) -> f x ) Listing 2.38: Instanzen der Typklasse ArrowApply für Kleisli-Funktionen Daraus folgt zum einen, dass die Typklassen ArrowApply und M onad gleichmächtig sind, denn mit Hilfe der Typklasse M onad ist es möglich eine gültige Instanz der Typklasse ArrowApply anzugeben und mit Hilfe der Typklasse ArrowApply ist es möglich eine gültige Instanz der Typklasse M onad anzugeben. Zum anderen folgt daraus, dass Arrows Monaden generalisieren, denn für jede Monade exisitert ein entsprechender Arrow. Die Frage ist also, welchen Vorteil bringen diese Typklassen, wenn Instanzen der Typklasse M onad deutlich schneller und einfacher zu implementieren sind und letztlich eine mindestens gleiche Mächtigkeit besitzen. 3 Arrows und Monaden Berechnungen, die Instanzen der Typklasse ArrowApply sind, finden immer eine passende Entsprechung mit Hilfe der Typklasse M onad und umgekehrt. Es wird deutlich, dass vor allem die Arrows interessant sind, die die Typklasse ArrowApply gerade nicht implementieren (können). 3.1 LL-Parser Ein Parser (vom engl. to parse, also analysieren) ist ein Programm zur Zerteilung und Umwandlung einer beliebigen Eingabe in ein für die Weiterverarbeitung brauchbares Format. Ein LL-Parser ist ein Top-Down-Parser, der die Eingabe von Links nach rechts abarbeitet, um eine 17 3 Arrows und Monaden Linksableitung der Eingabe zu berechnen. In Haskell bestehen LL-Parser zumeist aus einer Menge mit Kombinatoren verknüpfter Parser, die ein Eingabewort Symbol für Symbol konsumieren, bis das gesamte Eingabewort verarbeitet wurde. Um die Funktionsweise einer Kombinatorbibliothek zu verdeutlichen, wird hier die Beispielgrammatik G ∶∶= a G b ∣ c betrachtet. Ohne zunächst auf die notwendigen Implementierungen der Kombinatoren einzugehen, kann ein entsprechender, in Haskell geschriebener Parser g zur Grammatik G folgendermaßen aussehen. newtype Parser s a = P ([ s ] -> Maybe (a , [ s ])) g :: Parser Char Char g = symbol ’a ’ <* > g <* > symbol ’b ’ <| > symbol ’c ’ Listing 3.1: Möglicher Parser der Grammatik G ∶∶= a G b ∣ c Ein Parser ist also eine Berechnung, die eine Liste von Symbolen auf ein mögliches Ergebnis, bestehend aus Resultat und Restsymbolliste, abbildet. Der Sequenzkombinator (<∗>) verkettet zwei Parser, so dass der zweite Parser auf die Restsymbolliste des ersten Parsers angewendet wird. Der Alternativkombinator (<∣>) wendet beide Parser auf das Eingabewort an und wählt eines der beiden möglichen erfolgreichen Ergebnisse aus. Die Funktion symbol erzeugt einen Parser, der genau ein Symbol erkennt und konsumiert. Er lässt sich folgendermaßen implementieren. symbol :: Eq s = > s -> Parser s s symbol s = P (\ ss -> case ss of [] -> Nothing ( t : ts ) -> if s == t then Just (s , ts ) else Nothing ) Listing 3.2: Mögliche Implementierung der Parser erzeugenden Funktion symbol Sofern die Symbolliste nicht leer ist und das gesuchte Symbol an erster Stelle steht, wird dieses samt Restsymbolliste erfolgreich zurückgegeben, ansonsten ist der Parser gescheitert. Eine solche Kombinatorenbibliothek wird in der Praxis oft mit Hilfe von Monaden implementiert. Es wird daher zunächst versucht, die beiden Parserkombinatoren (<∗>) und (< ∣ >) mit Hilfe monadischer Funktionen anzugeben. Der Alternativkombinator (< ∣ >) entspricht dabei dem Kombinator mplus der Typklasse M onadP lus. class Monad m = > MonadPlus m where mzero :: m a mplus :: m a -> m a -> m a Listing 3.3: Klassendefinition der Typklasse M onadP lus 18 3 Arrows und Monaden instance MonadPlus Parser where mzero = P ( const Nothing ) P f ‘ mplus ‘ P g = P (\ ss -> case f ss of Just (x , ts ) -> Just (x , ts ) Nothing -> g ss ) ( <| >) :: Parser s a -> Parser s a -> Parser s a ( <| >) = mplus Listing 3.4: Instanz der Typklasse M onadP lus für Parser sowie Implementierung des Alternativkombinators (<∣>) Diese Definition eines Alternativkombinators hat allerdings ein space leak Problem [3]. Unter Umständen werden Daten durch den Garbage Collector länger vorgehalten, als auf den ersten Blick vermutet werden kann. Angenommen der erste Parser konsumiert einen Teil des Eingabewortes, dann kann dieser bis zum Schluss nicht durch den Garbage Collector entfernt werden, da im Falle eines Scheiterns des ersten Parsers alle Symbole des Eingabewortes noch für den zweiten Parser zur Verfügung stehen müssen. Scheitert der erste Parser früh stellt dies kein Problem dar. Parst er zunächst erfolgreich eine große Menge des Eingabewortes und scheitert schließlich, wird der zweite Parser auf den bereits ausgewerteten Teil des Eingabewortes angewendet. In der Praxis ist es aber unüblich, dass zwei Parser zu Anfang eine große Übereinstimmung haben. Er wird daher mit hoher Wahrscheinlichkeit sofort scheitern, so dass völlig unnötig Speicher blockiert wurde. Es werden also scheinbar genau dann viele Daten im Speicher vorgehalten, wenn diese eigentlich unnötig sind! Dieses Problem einer solchen Parserdefinition wurde bereits durch Wadler erkannt, es wurde damals aber nur ungenügend gelöst [4]. Swierstra und Duponcheel entwickelten jedoch eine Lösung für dieses space leak Problem [5]. Sie beschränkten sich dabei auf LL(1)-Parser, also einem speziellen LL(k)-Parser. Die Entscheidung, welcher Parser zum Einsatz kommt, wird an Hand der ersten k Symbole des Eingabewortes, dem Lookahead, getroffen. Im Falle eines LL(1)-Parsers wird die Entscheidung also nur mit Hilfe des ersten Symbols gefällt. Ihre Implementierung eines Alternativkombinators kann sofort entscheiden, welcher Parser angewendet werden soll. Daher muss kein Teil des konsumierten Eingabewortes für einen möglichen zweiten Parser weiter vorgehalten werden. Das space leak Problem ist damit gelöst. Sie lösten das Problem mit Hilfe von statischen Informationen zu jedem Parser. Diese werden zusätzlich zur eigentlichen Parsingfunktion, dem DynamicP arser, angegeben. Sie sind Teil des StaticP arsers und geben an, ob er das leere Wort als Eingabe akzeptiert und welche Symbole als erstes Symbol des Eingabewortes akzeptiert werden. data StaticParser s = SP Bool [ s ] newtype DynamicParser s a = DP ([ s ] -> Maybe (a , [ s ])) data Parser s a = P ( StaticParser s ) ( DynamicParser s a ) Listing 3.5: Eine Parserdefinition mit statischen Informationen Die Funktion symbol, die einen solchen Parser erzeugt, muss entsprechend erweitert werden. 19 3 Arrows und Monaden symbol :: s -> Parser s s symbol s = P ( SP False [ s ]) ( DP (\( _ : ts ) -> Just (s , ts ))) Listing 3.6: Mögliche Implementierung der Parser mit statischen Informationen erzeugenden Funktion symbol Der durch die Funktion symbol erzeugte Parser kann also nur auf ein nicht leeres Eingabewort, welches zudem mit dem Symbol s beginnt, angewendet werden. Wird das Eingabewort zunächst auf diese Eigenschaft hin geprüft und die eigentliche Parsingfunktion erst dann eingesetzt, können diese Eigenschaften als gegeben angenommen werden. Auch für den Parser mit statischen Informationen lässt sich eine gültige Instanz der Typklasse M onadP lus angeben. instance ( Eq s ) = > MonadPlus ( Parser s ) where mzero = P ( SP True []) ( DP ( const Nothing )) P ( SP e1 ss1 ) ( DP p1 ) ‘ mplus ‘ P ( SP e2 ss2 ) ( DP p2 ) = P ( SP ( e1 && e2 ) ( ss1 ‘ union ‘ ss2 )) ( DP (\ ss -> case ss of [] -> if e1 then p1 [] else if e2 then p2 [] else Nothing ( t : ts ) -> if t ‘ elem ‘ ss1 then p1 ( t : ts ) else if t ‘ elem ‘ ss2 then p2 ( t : ts ) else if e1 then p1 ( t : ts ) else if e2 then p2 ( t : ts ) else Nothing )) Listing 3.7: Instanz der Typklasse M onadP lus für Parser mit statischen Informationen Durch die Überprüfung der statischen Informationen wird maximal ein Parser ausgewählt. Der Sequenzkombinator (<∗>), der zwei Parser hintereinander ausführen soll, stellt aber ein Problem dar. Die Typklasse M onad stellt dazu den Kombinator (≫=) zur Verfügung. Es ist aber unmöglich, für einen Parser mit statischen Informationen eine gültige Implementierung anzugeben. ( > >=) :: m a -> ( a -> m b ) -> m b Listing 3.8: Typsignatur des Kombinators (≫=) der Typklasse M onad Betrachtet man erneut den Typ der Funktion (≫=) so erkennt man das Problem: Der zweite Parser wird mit Hilfe des Ergebnisses des ersten Parsers erzeugt. Es ist demnach unmöglich die statischen Informationen der Parser zu kombinieren, denn die statischen Informationen des zweiten Parsers stehen erst nach Ausführung des ersten Parsers zur Verfügung. Somit können die statischen Informationen des kombinierten Parsers nicht vor seiner Ausführung überprüft werden. Aus diesem Grund entschieden sich Swierstra und Duponcheel zwangsweise dazu auf eine monadische Implementierung ihres Parsers zu verzichten. Obwohl ihr Parser auch ohne eine monadische Implementierung funktioniert, ist dies doch ein Nachteil. Hughes erkannte dahinter kein vereinzeltes, sondern ein grundsätzliches Problem: Es existieren Berechnungsformen, 20 3 Arrows und Monaden die zwar eine allgemeine Struktur aufweisen, aber keine Monade bilden [3]. Er generalisierte Monaden und entwickelte so die Grundlagen der Typklasse Arrow, die in Kapitel 2 bereits vorgestellt wurde. Mit Hilfe der Typklasse Category lässt sich der Sequenzkombinator (<∗>) implementieren. Dazu muss allerdings die Definition des Parser um einen Eingabeparameter erweitert werden. data StaticParser s = SP Bool [ s ] newtype DynamicParser s a b = DP (( a , [ s ]) -> Maybe (b , [ s ])) data Parser s a b = P ( StaticParser s ) ( DynamicParser s a b ) instance Category ( Parser s ) where id = P ( SP False []) ( DP Just ) P ( SP e2 ss2 ) ( DP p2 ) . P ( SP e1 ss1 ) ( DP p1 ) = P ( SP ( e1 && e2 ) ( ss1 ++ if e1 then ss2 else [])) ( DP (\( a , ss ) -> case p1 (a , ss ) of Just (c , ts ) -> p2 (c , ts ) Nothing -> Nothing )) ( <* >) :: Parser s a b -> Parser s b c -> Parser s a c ( <* >) = ( > > >) Listing 3.9: Instanz der Typklasse Category für Parser mit statischen Informationen sowie Implementierung des Sequenzkombinators (<∗>) Die Implementierung der Parser erzeugenden Funktion symbol muss dementsprechend ebenfalls angepasst werden. Eine Instanziierung der Typklassen Arrow, ArrowZero und ArrowP lus, sowie die Angabe eines entsprechenden Alternativkombinators ist ebenfalls möglich [3]. Mit Hilfe von Arrows kann also eine allgemeine Berechnungsabstraktion genutzt werden, um einen Parser mit statischen Informationen zu implementieren, dessen Alternativkombinator kein space leak Problem hat. Dieser Parser hat aber einen entscheidenden Nachteil gegenüber einem monadischen Parser ohne statische Informationen, er ist auf LL(1)-Grammatiken beschränkt. Ein monadischer Parser kann hingegen nicht nur LL(k)-Grammatiken sondern sogar kontext-sensitive Grammatiken parsen. Dies erkennt man erneut an der Typsignatur der Funktion (≫=). Mit Hilfe des Ergebnisses des ersten Parsers lässt sich der zweite Parser dynamisch während des Parsens erzeugen. Dies ist mit Arrows unmöglich. 3.2 Syntax Die Einführung von Monaden klärte ein bis dahin nur mangelhaft gelöstes Problem der puren Sprache Haskell auf elegante Weise: Input/Output, kurz I/O [6]. Bereits vorher wurden jedoch die vielen verschiedenen Möglichkeiten dieser Art funktionale Programme zu strukturieren erkannt [7]. Die rasche Verbreitung der Typklasse M onad und ihrer Kombinatoren wurde dabei von einem weiteren Faktor gefördert, es wurde eine spezielle Syntax für die Nutzung von Monaden geschaffen: die do-Notation. Diese führt zwar das Schlüsselwort do ein, sie ist jedoch reiner syntaktischer Zucker und lässt sich eindeutig in Funktionen, die aus monadischen Kombinatoren bestehen, umwandeln. Der Vorteil der do-Notation besteht in ihrer einfacheren Lesbarkeit, sie erinnert dabei an eine imperative Schreibweise. Ein Beispiel zur Verdeutlichung der einfacheren Lesbarkeit der do-Notation ist eine Implementierung der Funktion lif tM 2. 21 Diese wendet die Funktion op auf die Inhalte der beiden monadischen Werte m und n an und erzeugt einen neuen monadischen Wert. Obwohl beide Implementierungen logisch identisch sind, ist die zweite Implementierung intuitiver lesbar. liftM2 :: Monad m = > ( a -> b -> c ) -> m a -> m b -> m c liftM2 op m n = m > >= \ a -> n > >= \ b -> return ( a ‘op ‘ b ) liftM2 op m n = do a <- m b <- n return ( a ‘op ‘ b ) Listing 3.10: Zwei gültige Implementierungen der Funktion lif tM 2 zur Veranschaulichung der do-Notation Die Notwendigkeit, Werte mit Hilfe von Tupeln explizit durch Berechnungen durchzureichen, ermöglicht es, beim Programmieren mit Arrows direkt über die Lebendigkeit von Variablen zu bestimmen. Zusammen mit der point-free Definitionsweise von Arrows kann dies aber zu unübersichtlichen Programmen führen. Aus diesen Gründen wurde an einer Spracherweiterung für eine einfachere Syntax für Arrows gearbeitet, der proc-Notation, welche ebenfalls reiner syntaktischer Zucker ist [8]. Bei dieser Arrow-Abstraktion übernimmt das Schlüsselwort proc die Rolle des λ bei der Lambda-Abstraktion, wobei statt einer Funktion ein Arrow definiert wird. Das Schlüsselwort * steckt einen Wert in einen Arrow. Zur Verdeutlichung wird erneut das passende Pendant der Funktion lif tM 2 betrachtet, lif tA2. Dabei werden ausschließlich die beiden nicht vordefinierten Funktionen arr und f irst verwendet. Obwohl sich mit Hilfe des Kombinators (&&&) in diesem Fall eine einfachere Implementierung angeben ließe, lässt sich so das Problem der expliziten Weitergabe von Werten besser veranschaulichen. liftA2 :: Arrow a = > ( c -> d -> e ) -> a b c -> a b d -> a b e liftA2 op f g = >>> >>> >>> >>> >>> arr (\ b -> (b , b )) first f arr (\( c , b ) -> (b , c )) first g arr (\( d , c ) -> (c , d )) arr ( uncurry op ) liftA2 op f g = proc b -> do c <- f -< b d <- g -< b returnA -< ( c ‘op ‘ d ) Listing 3.11: Zwei gültige Implementierungen der Funktion lif tA2 zur Veranschaulichung der proc-Notation Mit Hilfe der proc-Notation wird zum einen die explizite Weiterleitung der Variablen b, c und d mit Hilfe von Tupeln verborgen. Zum anderen wird die point-free Definitionsweise umgangen, die Arrows f und g können beide auf die Variable b angewendet werden. 22 4 Zusammenfassung und Bewertung 4 Zusammenfassung und Bewertung In Kapitel 2 wurden die Typklassen der Module Control.Category und Control.Arrow betrachtet. Dabei wurden sowohl die verschiedenen Möglichkeiten dieser Berechnungsabstraktion beleuchtet, als auch die Verbindung der Typklasse ArrowApply zur Typklasse M onad geklärt. In Kapitel 3 wurden die Kombinatoren eines LL(1)-Parsers, die sich mit Hilfe von Monaden nicht realisieren ließen, durch Arrows implementiert. Daneben gibt es eine Reihe weiterer Beispiele von Berechnungen, die sich nicht mit Hilfe von Monaden implementieren oder sich mit Hilfe von Arrows intuitiver abstrahieren lassen. Zu Nennen sind hier Berechnungen über Streams, zum Beispiel als Repräsentation von Schaltnetzen [9]. Für diese Berechnungen lässt sich keine gültige Instanz der Typklasse ArrowApply und damit der Typklasse M onad finden, gleichzeitig lassen sie sich auf natürliche Weise als Arrows darstellen. Sie profitieren somit von der stärkeren Generalität der Typklasse Arrow im Vergleich zur Typklasse M onad. Weiterhin wurde die einfachere Lesbarkeit der für Arrows eingeführten proc-Notation verdeutlicht. Arrows generalisieren Monaden, damit sind sie in der Lage eine breitere Anwendung zu finden. Sie sind jedoch aufwändiger zu implementieren und zu nutzen. Dies liegt vor allem an der expliziten Angabe von Zwischenwerten, die es allerdings auch ermöglicht, genauer über die Lebensdauer von Variablen zu bestimmen. Da es aber zu jeder Monade einen entsprechenden Arrow gibt, sollte für die meisten Berechungen zunächst die Implementierung einer Monade versucht werden. Erst wenn dies nicht möglich ist, werden Arrows interessant. The real flexibility with arrows comes with the ones that aren’t monads, otherwise it’s just a clunkier syntax. – Philippa Cowderoy 23 Literaturverzeichnis [1] Haskell.org, “Control.category,” 2007. http://www.haskell.org/ghc/docs/latest/ html/libraries/base/Control-Category.html [12.02.2013]. [2] Haskell.org, “Control.arrow,” 2002. http://www.haskell.org/ghc/docs/latest/html/ libraries/base/Control-Arrow.html [12.02.2013]. [3] J. Hughes, “Generalising monads to arrows,” Sci. Comput. Program., vol. 37, pp. 67–111, May 2000. [4] P. Wadler, “How to replace failure by a list of successes,” in Proc. of a conference on Functional programming languages and computer architecture, (New York, NY, USA), pp. 113– 128, Springer-Verlag New York, Inc., 1985. [5] S. D. Swierstra and L. Duponcheel, “Deterministic, error-correcting combinator parsers,” in Advanced Functional Programming, pp. 184–207, 1996. [6] S. L. Peyton Jones and P. Wadler, “Imperative functional programming,” in Proceedings of the 20th ACM SIGPLAN-SIGACT symposium on Principles of programming languages, POPL ’93, (New York, NY, USA), pp. 71–84, ACM, 1993. [7] P. Wadler, “The essence of functional programming,” in Proceedings of the 19th ACM SIGPLAN-SIGACT symposium on Principles of programming languages, POPL ’92, (New York, NY, USA), pp. 1–14, ACM, 1992. [8] R. Paterson, “A new notation for arrows,” in ICFP, pp. 229–240, 2001. [9] R. Paterson, “Arrows and computation,” in The Fun of Programming (J. Gibbons and O. de Moor, eds.), pp. 201–222, Palgrave, 2003. 24