Arrows in Haskell - AG Programmiersprachen und

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