3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Beispiele: (Funktionen auf Listen) (3) 3.1 Grundkonzepte funktionaler Programmierung Bemerkungen: 5. Zusammenhängen der Elemente einer Liste von Listen: concat :: [[a]] -> [a] concat xl = if null xl then [] else append (head xl) ( concat (tail xl)) • Rekursive Funktionsdeklaration sind bei Listen angemessen, weil Listen rekursive Datenstrukturen sind. 6. Wende eine Liste von Funktionen vom Typ Int -> Int nacheinander auf eine ganze Zahl an: • Mit Mustern lassen sich die obigen Deklaration noch eleganter fassen (s. unten). seqappl :: [( Int -> Int)] -> Int -> Int seqappl xl i = if null xl then i else seqappl (tail xl) (( head xl) i) ©Arnd Poetzsch-Heffter TU Kaiserslautern 3. Funktionales Programmieren 241 3.1 Grundkonzepte funktionaler Programmierung 242 3.1 Grundkonzepte funktionaler Programmierung Die Datenstrukturen der Paare (2) Wir betrachten zunächst Paare und verallgemeinern dann auf n-Tupel: Paare oder 2-Tupel sind die Elemente des kartesischen Produktes zweier ggf. verschiedener Mengen oder Typen. Der Typ der Paare ist also ein Produkttyp. 2 Typ: (a,b) , a, b sind Typparameter Funktionen: (==), (/=) :: (a, b) → (a, b) → Bool wenn (==) auf a und b definiert (_,_) :: a → b → (a, b) fst :: (a, b) → a snd :: (a, b) → b Als Typkonstruktor wird (a,b) in Mixfix-Schreibweise benutzt. Konstanten: Haskell stellt standardmäßig eine Datenstruktur für Paare bereit, die bzgl. der Elementtypen parametrisiert ist. TU Kaiserslautern TU Kaiserslautern 3. Funktionales Programmieren Die Datenstrukturen der Paare ©Arnd Poetzsch-Heffter ©Arnd Poetzsch-Heffter keine Dem Typ (a,b) ist die Menge der geordneten Paare mit Elementen vom Typ a und b zugeordnet. 243 ©Arnd Poetzsch-Heffter TU Kaiserslautern 244 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Beispiel: (Funktionen auf Paaren) 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der n-Tupel Haskell unterstützt n-Tupel für alle n ≥ 3: Transformiere eine Liste von Paaren in ein Paar von Listen: Typ: unzip :: [(a, b)] -> ([a], [b]) unzip xl = if null xl then ([] , []) else ( (fst (head xl)):(fst ( unzip (tail xl))), (snd (head xl)):(snd ( unzip (tail xl))) ) Funktionen: (==), (/=) :: (a, b, ...) → (a, b, ...) → Bool wenn (==) auf a, b, ... definiert (_,_,...) :: a → b → ... → (a, b, ...) Konstanten: it = unzip [(1 , 2), (3, 4) , (9, 10)] (auch das geht erheblich schöner mit Mustern) ©Arnd Poetzsch-Heffter TU Kaiserslautern 3. Funktionales Programmieren (a,b,...) , a, b, ... sind Typparameter keine Seien n ≥ 3 und a1 , . . . , an Typen mit Wertemenge w(a1 ), . . . , w(an ); dann ist dem Tupeltyp (a1 , . . . , an ) das kartesische Produkt w(a1 ) × · · · × w(an ) als Wertemengen zugorndet; also eine Menge geordneter n-Tupel, wobei das i-te Element vom Typ ai ist. 245 ©Arnd Poetzsch-Heffter 3.1 Grundkonzepte funktionaler Programmierung TU Kaiserslautern 3. Funktionales Programmieren Bemerkungen: 246 3.1 Grundkonzepte funktionaler Programmierung Die Datenstruktur der Einheit Haskell unterstützt eine Datenstruktur mit einem definierten Wert: Typ: • Es gibt keine Funkionen, um Elemente aus einem Tupel zu () Funktionen: selektieren. Dafür benötigt man Muster (siehe unten). (==), (/=) :: () → () → Bool • Paare sind wie 2-Tupel, auf ihnen sind aber die Selektorfunktionen Konstanten: fst und snd definiert. () • Es gibt keine 1-Tupel: Klammern um Ausdrücke dienen nur der :: () Dem Typbezeichner () ist eine einelementige Wertemenge zugeordnet. Der Wert wird als Einheit (engl. unity) bezeichnet. Strukturierung und haben darüber hinaus keine Bedeutung; d.h. wenn e ein Ausdruck ist, ist ( e ) gleichbedeutend mit e. Bemerkung: Die Einheit wird oft als Ergebnis verwendet, wenn es keine relevanten Ergebniswerte gibt. ©Arnd Poetzsch-Heffter TU Kaiserslautern 247 ©Arnd Poetzsch-Heffter TU Kaiserslautern 248 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Beispiel: (Geschachtelte Tupel) 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (Funktionen auf n-Tupeln) 1. Flache ein Paar von Paaren in ein 4-Tupel aus: Mit der Tupelbildung lassen sich „baumstrukturierte“Werte, sogenannte Tupelterme, aufbauen. So entspricht der Tupelterm: ausflachen :: ((a, b) ,(c, d)) -> (a, b, c, d) -- nimmt ein Paar von Paaren und liefert 4-Tupel ausflachen pp = ( fst (fst pp), snd (fst pp), fst (snd pp), snd (snd pp) ) ( (8,True), (("Tupel", "sind", "toll"), "aha")) dem Baum: it = ausflachen ( (True ,7) , (’x’ ,5.6) ) Alternative Deklaration mit Mustern: True 8 "Tupel" ©Arnd Poetzsch-Heffter ausflachen ((a, b) ,(c, d)) = (a, b, c, d) "aha" "sind" TU Kaiserslautern 3. Funktionales Programmieren 2. Funktion zur Paarbildung: "toll" paarung :: a -> b -> (a,b) paarung lk rk = (lk ,rk) 249 ©Arnd Poetzsch-Heffter 3.1 Grundkonzepte funktionaler Programmierung TU Kaiserslautern 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Funktionstypen: Beispiel: Eine zweistellige Funktion f mit Argumenten vom Typ a und Typ b und Ergebnistyp c kann in Haskell auf zwei Arten typisiert werden: Die Additionsoperation (+) auf Typ Int hat in Haskell den Typ 1. Gecurryte Form: (+) :: Int -> Int f :: a -> b -> c In der Mathematik typisiert man die Additionsoperation plus üblichweise mit: Nach den Präzedenzregeln für -> ist das a -> (b ->c), also eine Funktion, die ein Wert vom Typ a nimmt und eine Funktion vom Typ b -> c liefert. plus :: (Int ,Int) -> Int Diese Variante kann man in Haskell wie folgt definieren: Ist x::a und y::b, dann sind (f x) y oder gleichbedeutend f x y korrekte Anwendungen. plus ip = (fst ip) + (snd ip) 2. Tupel-Form: Oder eleganter mit Mustern: f :: (a,b) -> c plus (m,n) = m + n In diesem Fall ist für x::a und y::b, f (x,y) eine korrekte Anwendungen. ©Arnd Poetzsch-Heffter 250 TU Kaiserslautern 251 ©Arnd Poetzsch-Heffter TU Kaiserslautern 252 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung Muster in Deklarationen und Ausdrücken Begriffsklärung: (Muster in Haskell) Muster sind ein Sprachkonstrukt um strukturierte Werte einfacher handhaben zu können (siehe Funktion ausflachen). Muster (engl. Pattern) in Haskell sind Ausdrücke gebildet über Bezeichnern, Konstanten und Konstruktoren. Ein Wert heißt hier strukturiert, wenn er mittels Konstruktoren aus anderen Werten zusammengebaut wurde. Alle Bezeichner in einem Muster müssen verschieden sein. Ein Muster M mit Bezeichnern b1 , . . . , bk passt auf einen strukturierten Wert w (engl.: a pattern matches a value w), wenn es eine Substitution der Bezeichner bj in M durch Werte vj gibt, in Zeichen M[v1 /b1 , . . . , vk /bk ], so dass Konstruktoren sind spezielle Haskell-Funktionen. Bisher behandelte Konstruktoren: • der Listkonstruktor (:) (daher der Name “cons”) • die Tupelbildung durch (_,...,_) M[v1 /b1 , . . . , vk /bk ] = w In 3.1.4 werden wir benutzerdefinierte Konstruktoren kennen lernen. ©Arnd Poetzsch-Heffter TU Kaiserslautern 3. Funktionales Programmieren 253 ©Arnd Poetzsch-Heffter TU Kaiserslautern 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Beispiel: (ML-Muster, Passen) 254 3.1 Grundkonzepte funktionaler Programmierung Beispiel: (ML-Muster, Passen) (2) 5. first:rest passt auf ["computo","ergo","sum] mit first ="computo" und rest =["ergo", "sum"] . 1. (x,y) passt auf (4,5) mit Substitution x=4, y=5. 6. ((8,x), (y,"aha")) passt auf ((8,True), (("Tupel","sind","toll"), "aha")) mit x = True und y = ("Tupel","sind","toll"). 2. (erstesElem,zweitesElem) passt auf (-47,(True,"dada")) mit erstesElem =-47, zweitesElem =(True,"dada") . 3. x:xs passt auf 7:8:9:[] mit x = 7 und xs = 8:9:[] , d.h. xs = [8,9]. 4. x1:x2:xs passt auf 7:8:9:[] mit x1 = 7, x2 = 8, xs = [9] . 8 ©Arnd Poetzsch-Heffter TU Kaiserslautern 255 ©Arnd Poetzsch-Heffter True y TU Kaiserslautern "aha" 256 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Wertvereinbarungen mit Mustern 3.1 Grundkonzepte funktionaler Programmierung Funktionsvereinbarung mit Mustern Muster können in Haskell-Wertvereinbarungen verwendet werden: <Muster > = <Ausdruck > ; Muster können in Haskell-Funktionsdeklarationen verwendet werden (vgl. Folie 222): Wenn das Muster auf den Wert des Ausdrucks passt und σ die zugehörige Substitution ist, werden die Bezeichner im Muster gemäß σ an die zugehörige Werte gebunden. <Funktionsbez > <Parametermuster > ... = ... <Funktionsbez > <Parametermuster > ... = <Ausdruck > <Ausdruck > Wenn das Muster auf den Wert des Ausdrucks nich passt, wird eine Ausnahme erzeugt, sobald auf einen der deklarierten Bezeichner zugegriffen wird. Bei der Funktionsanwendung wird der Reihe nach geprüft, auf welches Parametermuster der aktuelle Parameter passt (vgl. Folie 229). Beispiel: Die Gleichung zum ersten passenden Fall wird verwendet. (x, y) = ©Arnd Poetzsch-Heffter (4, 5); TU Kaiserslautern 3. Funktionales Programmieren 257 ©Arnd Poetzsch-Heffter 3.1 Grundkonzepte funktionaler Programmierung TU Kaiserslautern 3. Funktionales Programmieren Beispiele: (Funktionsdeklaration mit Mustern) 258 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Funktionsdeklaration mit Mustern) (2) 2. Deklaration von ist_sortiert::[Int] ->Bool mit drei Mustern: ist_sortiert [] = True ist_sortiert (x:[]) = True ist_sortiert (x1:x2:xs) = if x1 <= x2 then ist_sortiert (x2:xs ) else False 1. Deklaration von foldplus ohne Muster: foldplus :: [Int] -> Int foldplus xl = if null xl then 0 else (head xl) + foldplus (tail xl) Deklaration von foldplus mit Muster: Deklaration mit drei Mustern und Wächtern: foldplus :: [Int] -> Int foldplus [] = 0 foldplus (x:xl) = x + foldplus xl ©Arnd Poetzsch-Heffter TU Kaiserslautern ist_sortiert [] ist_sortiert (x:[]) ist_sortiert (x1:x2:xs) | x1 <= x2 | otherwise 259 ©Arnd Poetzsch-Heffter = = True True = = ist_sortiert (x2:xs) False TU Kaiserslautern 260 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Beispiele: (Funktionsdeklaration mit Mustern) (3) Beispiele: (Funktionsdeklaration mit Mustern) (4) Deklaration von ist_sortiert::[Int]->Bool mit zwei Mustern und Wächtern: ist_sortiert (x1:x2:xs) | x1 <= x2 = | otherwise = ist_sortiert x = 3.1 Grundkonzepte funktionaler Programmierung 4. Verwendung geschachtelter Muster: unzip :: [(a, b)] -> ([a],[b]) unzip [] = ([], []) unzip ((x,y):ps) = ( (x : (fst (unzip ps))), (y : (snd (unzip ps))) ) ist_sortiert (x2:xs) False True 3. Deklaration von append ::[a]->[a]->[a]: append ([] , l2) = l2 append ((x:xs), l2) = x : ( append (xs , l2)) ©Arnd Poetzsch-Heffter TU Kaiserslautern 3. Funktionales Programmieren 261 ©Arnd Poetzsch-Heffter 3.1 Grundkonzepte funktionaler Programmierung TU Kaiserslautern 3. Funktionales Programmieren let-Ausdruck Beispiele: (let-Ausdruck) Der Mustermechanismus kann auch innerhalb von Ausdrücken eingesetzt werden. a = let a = 2*3 in a*a Syntax des let-Ausdrucks: b = let a = 2*3 in let (b,c) = (a,a+1) in a*b*c let <Liste von Deklarationen > in <Ausdruck > TU Kaiserslautern 3.1 Grundkonzepte funktionaler Programmierung unzip :: [(a, b)] -> ([a], [b]) unzip [] = ([] , []) unzip ((x,y):ps) = let (xs , ys) = unzip ps in ((x:xs), (y:ys)) Die aus den Deklarationen resultierenden Bindungen sind nur im let-Ausdruck gültig. D.h. sie sind sichtbar im let-Ausdruck an den Stellen, an denen sie nicht verdeckt sind. ©Arnd Poetzsch-Heffter 262 263 ©Arnd Poetzsch-Heffter TU Kaiserslautern 264 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren case-Ausdruck 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (case-Ausdruck) Syntax des case-Ausdrucks: case <Ausdruck0 > of <Muster1 > -> <Ausdruck1 > ... <MusterN > -> <AusdruckN > ist_sortiert xl = case xl of [] -> True (x:[]) -> True (x1:x2:xs) -> if x1 <= x2 then ist_sortiert (x2:xs) else False Prüfe der Reihe nach, ob der resultierende Wert von <Ausdruck0> auf eines der Muster passt. Passt er auf ein Muster, nehme die entsprechenden Bindungen vor und werte den zugehörigen Ausdruck aus (die Bindungen sind nur in dem zugehörigen Ausdruck gültig). ©Arnd Poetzsch-Heffter TU Kaiserslautern 3. Funktionales Programmieren 265 ©Arnd Poetzsch-Heffter 3.1 Grundkonzepte funktionaler Programmierung TU Kaiserslautern 3. Funktionales Programmieren Bemerkungen: 266 3.1 Grundkonzepte funktionaler Programmierung Unterabschnitt 3.1.4 • Das Verbot von gleichen Bezeichnern in Mustern hat im Wesentlichen den Grund, dass nicht für alle Werte/Typen die Gleichheitsoperation definiert ist. mal2 = twotimes = (a,a) = Benutzerdefinierte Datentypen \x -> 2*x \x -> x+x (mal2 , twotimes ) • Wenn keines der angegebenen Muster passt, wird eine Ausnahme erzeugt (abrupte Terminierung). ©Arnd Poetzsch-Heffter TU Kaiserslautern 267 ©Arnd Poetzsch-Heffter TU Kaiserslautern 268 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Benutzerdefinierte Datentypen 3.1 Grundkonzepte funktionaler Programmierung Vereinbarung von Typbezeichnern Haskell erlaubt es, Bezeichner für Typen zu deklarieren (vgl. F. 209): Fast alle modernen Spezifikations- und Programmiersprachen gestatten es dem Benutzer, „neue“ Typen zu definieren. type IntPaar = (Int ,Int) ; type CharList = [Char] ; type Telefonbuch = [(( String ,String ,String ,Int) ,[ String ])] ; Übersicht: • Vereinbarung von Typbezeichnern type IntegerNachInteger • Deklaration neuer Typen = Integer -> Integer ; fakultaet :: IntegerNachInteger ; -- Argument muss >= 0 sein fakultaet = fac ; • Summentypen • Rekursive Datentypen Dabei wird kein neuer Typ definiert, sondern nur ein “neuer” Bezeichner an einen bekannten Typ gebunden. ©Arnd Poetzsch-Heffter TU Kaiserslautern 3. Funktionales Programmieren 269 3.1 Grundkonzepte funktionaler Programmierung 3.1 Grundkonzepte funktionaler Programmierung Neue Typen werden in Haskell mit der datatype-Deklaration definiert, die im Folgenden schrittweise erläutert wird. Verdeutlichung benutzt werden (siehe Typ Telefonbuch). Definition eines neuen Typs und Konstruktors: • Zwei unterschiedliche Bezeichner können den gleichen Typ data <NeuerTyp > = bezeichnen; z.B.: <Konstruktor > <Typ1 > ... <TypN > Die obige Datatypdeklaration definiert: (Int ,Int ,Int) (Int ,Int ,Int) • einen neuen Typ und bindet ihn an <NeuerTyp> • eine Konstruktorfunktion mit Signatur kalenderwoche :: Date -> Int -- Parameter muss existierenden Kalendertag sein kalenderwoche (tag ,monat ,jahr) = ... <Konstruktor >:: <Typ1 > -> ... -> <TypN > -> <NeuerTyp > Die Konstruktorfunktion ist injektiv. kalenderwoche (11 ,12 ,2003) TU Kaiserslautern 270 Deklaration neuer Typen • Typvereinbarungen können zur Abkürzung oder zur ©Arnd Poetzsch-Heffter TU Kaiserslautern 3. Funktionales Programmieren Bemerkungen: (Typvereinbarungen) type IntTriple = type Date = ©Arnd Poetzsch-Heffter Typ- und Konstruktorbezeichner müssen mit einem Großbuchstaben beginnen. 271 ©Arnd Poetzsch-Heffter TU Kaiserslautern 272 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Beispiel: (Definition von Typ, Konstruktor, Selektoren) 3.1 Grundkonzepte funktionaler Programmierung Bemerkungen: data Person = Student String String Int Int definiert den neuen Typ Person und den Konstruktor Student :: String -> String -> Int -> Int -> Person Jede Datentypdeklaration definiert einen neuen Typ, d.h. insbesondere: Wir definieren dazu folgende Selektorfunktionen: vorname :: Person -> String vorname ( Student v n g m) • die Werte des neuen Typs sind inkompatibel mit allen anderen = v Typen; • auch Werte strukturgleicher benutzerdefinierter Typen sind name :: Person -> String name ( Student v n g m) inkompatibel. = n geburtsdatum :: Person -> Int geburtsdatum ( Student v n g m) = g matriknr :: Person -> Int matriknr ( Student v n g m) ©Arnd Poetzsch-Heffter = m TU Kaiserslautern 3. Funktionales Programmieren 273 ©Arnd Poetzsch-Heffter 3.1 Grundkonzepte funktionaler Programmierung TU Kaiserslautern 3. Funktionales Programmieren Beispiele: (Typkompatibilität) 274 3.1 Grundkonzepte funktionaler Programmierung Bemerkung: 1. Der Typ Person ist inkompatibel mit dem Tupeltyp type Person2 = (String ,String ,Int ,Int) Den Konstruktor kann man sich als eine Markierung der Werte seines Argumentbereichs vorstellen. Insbesondere ist vorname ("Niels","Bohr",18851007,221) nicht typkorrekt. Dabei werden Werte mit unterschiedlicher Markierung als verschieden betrachtet. 2. Person ist inkompatibel mit dem strukturgleichen Typ Adresse: data Adresse = Wohnung String String Int Int Konstruktoren erlauben es in gewisser Weise neue Produkttypen zu definieren. Insbesondere ist name ( Wohnung " Casimirring " " Lautern " 27 67663 ) nicht typkorrekt. ©Arnd Poetzsch-Heffter TU Kaiserslautern 275 ©Arnd Poetzsch-Heffter TU Kaiserslautern 276 3. Funktionales Programmieren 3.1 Grundkonzepte funktionaler Programmierung 3. Funktionales Programmieren Summentypen 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Summentypen) Ein Summentyp stellt die disjunkte Vereinigung der Elemente anderen Typen zu einem neuen Typ dar. Die meisten modernen Programmiersprachen unterstützen die Deklaration von Summentypen. 1. Ein anderer Datentyp zur Behandlung von Personen: In Haskell definiert man Summentypen durch Angabe von Alternativen bei der datatype-Deklaration: data <NeuerTyp > = <Konstruktor1 > | <Konstruktor2 > ... | <KonstruktorM > data Person2 = Student String String Int Int | Mitarbeiter String String Int Int | Professor String String Int Int String <Typ1_1 > ... <Typ1_N1 > <Typ2_1 > ... <Typ2_N2 > <TypM_1 > ... <TypM_NM > ©Arnd Poetzsch-Heffter TU Kaiserslautern 3. Funktionales Programmieren 277 ©Arnd Poetzsch-Heffter 3.1 Grundkonzepte funktionaler Programmierung TU Kaiserslautern 3. Funktionales Programmieren Beispiele: (Summentypen) (2) 278 3.1 Grundkonzepte funktionaler Programmierung Beispiele: (Summentypen) (3) 2. Eine benutzerdefinierte Datenstruktur für Zahlen: data MyNumber = Intc | Floatc isInt (Intc m) isInt ( Floatc r) isFloat (Intc m) isFloat ( Floatc r) = = = = Int Float plus :: MyNumber -> MyNumber -> MyNumber plus (Intc m) (Intc n) = Intc (m+n) plus (Intc m) ( Floatc r) = Floatc (( fromInteger ( toInteger m))+r) plus ( Floatc r) (Intc m) = Floatc (r+( fromInteger ( toInteger m))) plus ( Floatc r) ( Floatc q) = Floatc (r+q) True False False True neg :: MyNumber -> MyNumber neg (Intc m) = Intc (-m) neg ( Floatc r) = Floatc (-r) ©Arnd Poetzsch-Heffter TU Kaiserslautern 279 ©Arnd Poetzsch-Heffter TU Kaiserslautern 280