3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Begriffsklärung: 3.1 Grundkonzepte funktionaler Programmierung Weitere Operationen auf neu deklarierten Typen: Konstruktorfunktionen oder Konstruktoren liefern Werte des neu definierten Datentyps. Sie können in Mustern verwendet werden (z.B.: Student, Intc). Konstruktoren und Selektoren erlauben das Aufbauen und Zerlegen der Werte neu deklarierter Typen. Durch den Zusatz: deriving (Eq ,Show) Diskriminatorfunktionen oder Diskriminatoren prüfen, ob der Wert eines benutzerdefinierten Datentyps zu einer bestimmten Alternative gehört (Beispiel: isInt). liefert Haskell auch eine standardmäßige Gleichheit und die Möglichkeit, Werte des neuen Typs mittels print auszugeben. Zum Beipiel: Selektorfunktionen oder Selektoren liefern Komponenten von Werten des definierten Datentyps (z.B.: vorname, name, . . . ). data MyNumber = Intc Int | Floatc Float deriving (Eq ,Show) Bemerkung: In funktionalen Sprachen kann man meist auf Selektorfunktionen verzichten. Man verwendet stattdessen Muster/Pattern. ©Arnd Poetzsch-Heffter TU Kaiserslautern 3. Funktionales Programmieren Bemerkung: Haskell ermöglich es dem Benutzer auch, die Gleichheit oder andere Operationen auf neu definierten Typen selbst zu definieren. 281 ©Arnd Poetzsch-Heffter 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Weitere Anwendungen der datatype-Deklaration: 3.1 Grundkonzepte funktionaler Programmierung data Wochentag = Montag | Dienstag | Mittwoch | Donnerstag | Freitag | Samstag | Sonntag deriving (Eq ,Show) istMittwoch :: Wochentag -> Bool istMittwoch Mittwoch = True istMittwoch _ = False Die Wertemenge eines Aufzählungstyps ist eine endliche Menge (von Namen). Oder knapper: istMittwoch w TU Kaiserslautern 282 Beispiel: (Aufzählungstypen) Die datatype-Deklaration kann auch verwendet werden, um Aufzählungstypen zu definieren, indem nur null-stellige Konstruktoren benutzt werden. ©Arnd Poetzsch-Heffter TU Kaiserslautern 283 ©Arnd Poetzsch-Heffter = (w== Mittwoch ) TU Kaiserslautern 284 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Konstruktoren mit beliebiger Stelligkeit Rekursive Datentypen In einer Datentypdeklaration können Konstruktoren mit beliebiger Stelligkeit kombiniert werden; z.B.: data MaybeInt = | Von großer Bedeutung in allen Paradigmen der Programmierung sind rekursive Datentypen. Sie erlauben es insbesondere: Nothing Some Int • Listen beliebiger Länge • Bäume beliebiger Höhe Haskell sieht dafür im Prelude den folgenden parametrisierten Typ vor (vgl. 3.3): data Maybe a ©Arnd Poetzsch-Heffter = | behandeln zu können. Nothing Some a TU Kaiserslautern 3. Funktionales Programmieren 285 ©Arnd Poetzsch-Heffter 3.1 Grundkonzepte funktionaler Programmierung 286 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Listendatentypen) Eine Datentypdeklaration heißt direkt rekursiv, wenn der neu definierte Typ in einer der Alternativen der Datentypdeklaration vorkommt. 1. Ein Datentyp für Integer-Listen: data Wie bei Funktionen gibt es auch verschränkt rekursive Datentypdeklarationen. Intlist = nil | Cons Int Intlist 2. Ein Datentyp für homogene Listen mit Elementen von beliebigem Typ: Eine Datentypdeklaration heißt rekursiv, wenn sie direkt rekursiv ist oder Element einer Menge verschränkt rekursiver Datentypdeklarationen ist. data List a Ein Datentyp heißt rekursiv, wenn er mit einer rekursiven Datentypdeklaration definiert wurde. TU Kaiserslautern TU Kaiserslautern 3. Funktionales Programmieren Definition: (rekursive Datentypen) ©Arnd Poetzsch-Heffter 3.1 Grundkonzepte funktionaler Programmierung | 287 ©Arnd Poetzsch-Heffter = nil Cons a (List a) TU Kaiserslautern 288 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Baumartige Datenstrukturen: 3.1 Grundkonzepte funktionaler Programmierung Begriffsklärungen: (zu Bäumen) • In einem endlich verzweigten Baum hat jeder Knoten endlich Ottmann, Widmayer: „Bäume gehören zu den wichtigsten in der Informatik auftretenden Datenstrukturen“. viele Kinder. • Üblicherweise sagt man, die Kinder sind von links nach rechts geordnet. • Einen Knoten ohne Kinder nennt man ein Blatt, einen Knoten mit Kindern einen inneren Knoten oder Zweig. • Den Knoten ohne Elter nennt man Wurzel. • Ein Baum heißt markiert, wenn jeder Knoten k eine Markierung m(k ) besitzt. • In einem Binärbaum hat jeder Knoten maximal zwei Kinder. • Zu jedem Knoten k gehört ein Unterbaum, nämlich der Baum der k als Wurzel hat. ©Arnd Poetzsch-Heffter TU Kaiserslautern 3. Funktionales Programmieren 289 3.1 Grundkonzepte funktionaler Programmierung 290 3.1 Grundkonzepte funktionaler Programmierung Definition: (Sortiertheit markierter Binärbäume) data IntBBaum = Blatt Int | Zweig Int IntBBaum IntBBaum deriving (Eq ,Show) Ein mit ganzen Zahlen markierter Binärbaum heißt sortiert, wenn für alle Knoten k gilt: • Alle Markierungen der linken Nachkommen von k sind kleiner als einbaum = Zweig 7 ( Zweig 3 ( Blatt 2) (Blatt 4)) (Blatt 5) m(k ). • Alle Markierungen der rechten Nachkommen von k sind größer mark :: IntBBaum -> Int mark ( Blatt n) = n mark ( Zweig n lk rk) = n TU Kaiserslautern TU Kaiserslautern 3. Funktionales Programmieren Datentyp für markierte Binärbäume: ©Arnd Poetzsch-Heffter ©Arnd Poetzsch-Heffter als m(k ). 291 ©Arnd Poetzsch-Heffter TU Kaiserslautern 292 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Prüfung von Sortiertheit Prüfung von Sortiertheit (2) istsortiert :: IntBBaum istsortiert (Blatt n) istsortiert (Zweig n lk istsortiert lk && ( maxmark lk)<n && Aufgabe: Prüfe, ob ein Baum vom Typ IntBBaum sortiert ist. Idee: Berechne zu jedem Unterbaum die minimale und maximale Markierung und prüfe rekursiv die Sortiertheitseigenschaft für alle Knoten/Unterbäume. Wenig effiziente Lösung! Besser ist es, die Berechnung von Minima und Maxima mit der Sortiertheitsprüfung zu verschränken. Idee: Entwickle eine Funktion istsortiert3 mit drei Ergebniswerten: • Angabe, ob Baum sortiert minmark ( Blatt n) = n minmark ( Zweig n lk rk) = n `min` ( minmark lk `min` minmark rk) TU Kaiserslautern 3. Funktionales Programmieren -> Bool = True rk) = istsortiert rk && n<( minmark rk) result = istsortiert einbaum maxmark , minmark :: IntBBaum -> Int maxmark ( Blatt n) = n maxmark ( Zweig n lk rk) = n `max` ( maxmark lk `max` maxmark rk) ©Arnd Poetzsch-Heffter 3.1 Grundkonzepte funktionaler Programmierung • minimale Markierung des Baums • aximale Markierung des Baums 293 ©Arnd Poetzsch-Heffter 3.1 Grundkonzepte funktionaler Programmierung TU Kaiserslautern 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Prüfung von Sortiertheit (3) Begriffsklärung: (Weitere Begriffe zu Bäumen) istsortiert3 :: IntBBaum -> (Bool ,Int ,Int) -- Sei istsortiert3 b == (srt ,mn ,mx) ; dann ist -srt das Ergebnis der Sortiertheitspruefung von b -mn die minimale Markierung von b -mx die maximale Markierung von b Der leere Baum ist ein Baum ohne Knoten. Die Tiefe eines Knotens in einem Baum ist sein Abstand zur Wurzel. Der Wurzelknoten hat die Tiefe 0. Für alle anderen Knoten k gilt: tiefe(k ) = tiefe(elternknoten(k )) + 1 istsortiert3 (Blatt n) = (True , n, n) istsortiert3 ( Zweig n lk rk) = let (lsrt , lmn , lmx) = istsortiert3 lk (rsrt , rmn , rmx) = istsortiert3 rk in (( lsrt && rsrt && lmx <n && n<rmn), lmn , rmx) Die Knoten gleicher Tiefe t nennt man das Niveau t. Die Höhe des leeren Baumes ist 0. Die Höhe eines nicht-leeren Baumes b ist die maximale Knotentiefe plus 1: höhe( b ) = max { tiefe(k ) | k Knoten von b } + 1. istsortiert b = let (srtflag ,_ ,_) = ( istsortiert3 b) in srtflag ©Arnd Poetzsch-Heffter TU Kaiserslautern 294 Die Größe eines Baums ist die Anzahl seiner Knoten. 295 ©Arnd Poetzsch-Heffter TU Kaiserslautern 296 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Datentyp möglicherweise leerer Binärbäume: 3.1 Grundkonzepte funktionaler Programmierung Bäume mit variabler Kinderzahl: Bäume mit variabler Kinderzahl lassen sich realisieren: • durch mehrere Alternativen für Zweige (begrenzte Anzahl von Kindern) data IntBBaum2 = Leer | Knoten Int IntBBaum2 IntBBaum2 • durch Listen von Unterbäumen: data VBaum = Kn Int [VBaum] deriving (Eq ,Show) Der Rekursionsanfang ergibt sich durch Knoten mit leerer Unterbaumliste. ©Arnd Poetzsch-Heffter TU Kaiserslautern 3. Funktionales Programmieren 297 3.1 Grundkonzepte funktionaler Programmierung TU Kaiserslautern 3. Funktionales Programmieren Bäume mit variabler Kinderzahl: (2) 298 3.1 Grundkonzepte funktionaler Programmierung Bemerkungen: • Bäume mit variabler Kinderzahl lassen sich z.B. zur zaehleKnVBaum :: VBaum -> Int zaehleKnVBaumLst :: [ VBaum ] -> Int zaehleKnVBaum (Kn _ xs) = ©Arnd Poetzsch-Heffter Repräsentation von Syntaxbäumen verwenden, indem das Terminal- bzw. Nichtterminalsymbol als Markierung verwendet wird. 1 + ( zaehleKnVBaumLst xs) Besser ist es allerdings, die Information über die Symbole mittels Konstruktoren auszudrücken (vgl. nächstes Beispiel). zaehleKnVBaumLst [] = 0 zaehleKnVBaumLst (x:xs) = ( zaehleKnVBaum x) + ( zaehleKnVBaumLst xs) • Bäume mit variabler Kinderzahl werden auch zur Repräsentation von strukturierten oder semi-strukturierter Daten verwendet (z.B. XML, ...) ©Arnd Poetzsch-Heffter TU Kaiserslautern 299 ©Arnd Poetzsch-Heffter TU Kaiserslautern 300 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (abstrakte Syntaxbäume) Beispiel: (abstrakte Syntaxbäume) (2) Der abstrakte Syntaxbaum eines Programms repräsentiert die Programmstruktur unter Verzicht auf Schlüsselworte und Trennzeichen. Das Femto-Programm Rekursive Datentypen eignen sich sehr gut zur Beschreibung von abstrakter Syntax. a = 73; main = print ( a + 12 ) Als Beispiel betrachten wir die abstrakte Syntax von Femto: Wird durch folgenden Baum repräsentiert: data Programm deriving data Wertdekl deriving data Ausdruck Prog = Prog [ Wertdekl ] Ausdruck (Eq ,Show) = Dekl String Ausdruck (Eq ,Show) = Bzn String | Zahl Int | Add Ausdruck Ausdruck | Mul Ausdruck Ausdruck deriving (Eq ,Show) ©Arnd Poetzsch-Heffter TU Kaiserslautern 3. Funktionales Programmieren [Dekl "a" (Zahl 73)] Add (Bzn "a") (Zahl 12) Die Baumrepräsentation eignet sich besser als die Zeichenreihenrepräsentation zur weiteren Verarbeitung von Programmen. 301 ©Arnd Poetzsch-Heffter 3.1 Grundkonzepte funktionaler Programmierung TU Kaiserslautern 3. Funktionales Programmieren Verschränkte Datentypdeklarationen: 302 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (verschränkte Datentypen) Als Beispiel betrachten wir die abstrakte Syntax einer Erweiterung von Femto um let-Ausdrücke: data Programm deriving data Wertdekl deriving data Ausdruck Haskell unterstützt verschränkt rekursive Datentypdeklarationen. Die Datentypdeklaration werden einfach hintereinander geschreiben (wie verschränkt rekursiven Funktionsdeklarationen). Bei abstrakten Syntaxbäumen wird häufig verschränkte Rekursion der Datentypen benötigt. = Prog [ Wertdekl ] Ausdruck (Eq ,Show) = Dekl String Ausdruck (Eq ,Show) = Bzn String | Zahl Int | Add Ausdruck Ausdruck | Mul Ausdruck Ausdruck | Let Wertdekl Ausdruck deriving (Eq ,Show) Die Deklaration von Wertdekl benutzt Ausdruck; die Deklaration von Ausdruck benutzt Wertdekl. ©Arnd Poetzsch-Heffter TU Kaiserslautern 303 ©Arnd Poetzsch-Heffter TU Kaiserslautern 304 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Unendliche Datenobjekte: Begriffsklärung: (Strom) Zu einer nicht-leeren endlichen Liste kann man sich das erste Element und eine Liste als Rest geben lassen. Hat die endliche Liste xl die Länge n > 0, dann hat (tail xl) die Länge n − 1. 3. Funktionales Programmieren In Haskell kann man den Listendatentyp zur Realisierung von Strömen verwenden. • Lesen des ersten Elements (head) • Entfernen des ersten Elements (tail) • Prüfen, ob noch Elemente im Strom vorhanden sind Haskell unterstützt unendliche Liste und andere unendliche Datenobjekte. TU Kaiserslautern Potenziell unendliche Listen werden meist als Ströme bezeichnet. Typische Operationen: Eine unendliche Liste besitzt keine natürlichzahlige Länge und wird durch Anwendung von tail nicht kürzer. ©Arnd Poetzsch-Heffter 3.1 Grundkonzepte funktionaler Programmierung Beispiel: Strom von Eingabedaten 305 ©Arnd Poetzsch-Heffter 3.1 Grundkonzepte funktionaler Programmierung TU Kaiserslautern 3. Funktionales Programmieren Beispiel: (unendliche Liste) 306 3.1 Grundkonzepte funktionaler Programmierung Unterabschnitt 3.1.5 Eine vorstellbare unendliche Liste ist die Liste der natürlichen Zahlen [0,1,2,3,4,5,...]. In Haskell lässt sich diese Liste wie folgt definieren: incr :: [ Integer ] -> [ Integer ] incr [] = [] incr (x:xs) = (x+1):(incr xs) Ein- und Ausgabe natlist = 0:(incr natlist ) natlist2 = [0 ..] ©Arnd Poetzsch-Heffter TU Kaiserslautern 307 ©Arnd Poetzsch-Heffter TU Kaiserslautern 308 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Ein- und Ausgabe 3.1 Grundkonzepte funktionaler Programmierung Der Typ IO a Der Haskell Prelude stellt Funktionen zur Verfügung, um • Werte auf der Standardausgabe auszugeben und Um Funktionen mit Ein- und Ausgabe von Funktionen ohne Ein- und Ausgabe zu trennen und die richtige Reihenfolge zu garantieren, benutzt Haskell den vordefinierten parametrischen Typ • aus Dateien zu lesen und in Dateien zu schreiben. IO a • Werte von der Standardeingabe zu lesen, Problem: In einer rein funktionalen Sprache wie Haskell haben Funktionen keine Seiteneffekte. Deshalb spielt auch die Reihenfolge des Aufrufs eine weniger wichtige Rolle. Zum Beispiel hat die Funktion putStr zur Ausgabe einer Zeichenreihe die Signatur: Ein- und Ausgabe erzeugen einen Seiteneffekt auf die Programmumgebung. Sie müssen in der richtigen Reihenfolge ausgeführt werden. Zeichenreihe in die Standardausgabe (erzeugt also einen IO-Seiteneffekt) und liefert die Einheit als Ergebnis. ©Arnd Poetzsch-Heffter TU Kaiserslautern 3. Funktionales Programmieren 309 putStr :: String -> IO () putStr nimmt eine Zeichenreihe als Argument, schreibt die ©Arnd Poetzsch-Heffter 3.1 Grundkonzepte funktionaler Programmierung TU Kaiserslautern 3. Funktionales Programmieren Der Typ IO a (2) 310 3.1 Grundkonzepte funktionaler Programmierung Sequentielle Ausführung von IO-Aktionen Für die sequentielle Ausführung von IO-Aktionen stellt Haskell die do-Notation zur Verfügung: Wir nennen Funktionen und Konstanten mit Ergebnistyp IO a IO-Aktionen. do { Anw1 ; ... AnwN ; IO - Aktion } Zum Beispiel ist die Konstante/null-stellige Funktion getLine eine IO-Aktion: getLine :: IO String Ausführung der Aktion getLine liest von der Standardeingabe (erzeugt also einen IO-Seiteneffekt) und liefert eine Zeichereihe. do Anw1 ... AnwN IO - Aktion Eine Anweisung Anw hat die Form Bezeichner <- IO - Aktion wobei der Bezeichner und Zuweisungpfeil “<-” entfallen können, wenn das Ergebnis der IO-Aktion nicht gebraucht wird. ©Arnd Poetzsch-Heffter TU Kaiserslautern 311 ©Arnd Poetzsch-Heffter TU Kaiserslautern 312 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Beispiel: (Anweisungssequenz) 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (Anweisungssequenz) (2) main = do { hSetBuffering stdout NoBuffering ; -- import System .IO putStr "Gib eine Zeichenreihe ein: "; s <- getLine ; putStr " Revertierte Zeichereihe : "; putStrLn ( reverse s) } Ausgabe aller Elemente einer String-Liste: printStringList :: [ String ] -> IO () printStringList [] = putStr "" printStringList (s:xs) = do putStr s printStringList xs Erläuterung: • Die Anweisung s <- getLine bindet den gelesenen Wert an s. • Da die letzte IO-Aktion den Typ IO () hat, hat auch main den Typ IO (). ©Arnd Poetzsch-Heffter TU Kaiserslautern 3. Funktionales Programmieren 313 3.1 Grundkonzepte funktionaler Programmierung 314 3.1 Grundkonzepte funktionaler Programmierung Dateiein- und -ausgabe Bisher haben wir nur IO-Aktion für Zeichenreihen kennen gelernt. Zum Lesen aus und Schreiben in Dateien stellt Haskell u. a. die IO-Aktionen: Haskell unterstützt im Prelude und Bibliotheken viele weitere IO-Aktionen. Z. B.: readFile :: FilePath -> IO String writeFile :: FilePath -> String -> IO () getChar :: IO Char putChar :: Char -> IO () Dabei ist FilePath eine Zeichenreihe, die einen Dateinamen bezeichnet: Und für einen lesbaren und anzeigbaren Typ a die parametrischen IO-Aktionen: type FilePath = String readLn :: IO a print :: a -> IO () TU Kaiserslautern TU Kaiserslautern 3. Funktionales Programmieren Parametrisierte IO-Aktionen ©Arnd Poetzsch-Heffter ©Arnd Poetzsch-Heffter 315 ©Arnd Poetzsch-Heffter TU Kaiserslautern 316