Jenseits von Fakultät und Fibonacci: Architektur Funktionaler Programme Dr. Klaus Alfert Dr. Bernd Löchner Folie 1 Januar 2011 Dr. Klaus Alfert Dr. Bernd Löchner © Zühlke 2011 Warum ist Funktionale Programmierung in vieler Munde? Folie 3 Januar 2011 Dr. Klaus Alfert Dr. Bernd Löchner © Zühlke 2011 Sun T 2 »Niagara 2« 2007, 8 Cores × 8 Threads Intel i7 »Nehalem« 2008, 4 Cores × 2 Threads IBM POWER 7, 2010, 8 Cores × 4 Threads Oracle T 3 »Rainbow Falls« 2010, 16 Cores × 8 Threads Intel Cloud Computing on a Chip 2009, 48 Cores (Prototyp) Warum ist Nebenläufigkeit mit Objekten schwierig? Folie 9 Januar 2011 Dr. Klaus Alfert Dr. Bernd Löchner © Zühlke 2011 Objekte haben eine Identität :2 :3 :1 Objekte haben einen (lokalen) Zustand :2 :3 :1 Objekte haben Verhalten :2 :3 :1 Multithreading erfordert Locks :2 :3 :1 Ein hoher Grad an Nebenläufigkeit wird leicht unübersichtlich Ein hoher Grad an Nebenläufigkeit wird leicht unübersichtlich Wie kann funktionale Programmierung hier helfen? Was unterscheidet FP von OOP? Folie 17 Januar 2011 Dr. Klaus Alfert Dr. Bernd Löchner © Zühlke 2011 Zu den Programmbeispielen Beispiele in Haskell & Erlang • Haskell • Erlang F# ist puristisch elegant hat Industrial-Strength Die Beispiele sind aber alle übertragbar in andere funktionale Programmiersprachen. © Zühlke 2011 A LISP programmer knows the value of everything, but the cost of nothing. –Alan J. Perlis Epigrams of Programming © Zühlke 2011 Werte, Variablen, Veränderliche Was sind imperative Variablen? • Namen für Adressen von Speicherzellen • Abstraktionsniveau: Assembler x 42 • Objekte haben eine Identität: Entspricht einer Adresse von Speicherzellen Was sind funktionale Variablen? • Namen für Werte: „Sei x beliebig, aber fest“ • Variablen werden an Werte „gebunden“ • Abstraktionsniveau: • Notwendig: x Mathematik Effiziente Abbildung auf Speicherzellen durch Laufzeitsystem 42 © Zühlke 2011 Objekte versus Werte :1 42 42 Copy Gleich, aber nicht identisch :2 :1 42 Gleich und identisch 42 Funktionen erzeugen aus alten Werte neue! © Zühlke 2011 Rekursion statt Iteration: Hyper-Fakultät (Sloane: A Handbook of Integer Sequences) Fakultät ist für Kinder – Hyper-Fakultät ist für Erwachsene n H (n ) = ∏ k k k =1 H(n) = 1, 1, 4, 108, 27648, 86400000, 4031078400000, 3319766398771200000, 55696437941726556979200000, 21577941222941856209168026828800000, 215779412229418562091680268288000000000000000, 61564384586635053951550731889313964883968000000000000000, … © Zühlke 2011 Rekursion statt Iteration: Hyper-Fakultät (Sloane: A Handbook of Integer Sequences) Imperativ: Zuweisungsorientiert Rekursiv: Werteorientiert hfac(n): r := 1; while n > 0 do r := r * n^n; n := n – 1; od return r; hfac(n)-> if n == 0 then 1 else n^n * hfac(n-1) © Zühlke 2011 Rekursion statt Iteration: Hyper-Fakultät (Sloane: A Handbook of Integer Sequences) Imperativ: Zuweisungsorientiert hfac(n): r := 1; while n > 0 do r := r * n^n; n := n – 1; od return r; Berechnung: hfac(3) n 1 0 3 2 r ? 108 27 1 © Zühlke 2011 Rekursion statt Iteration: Hyper-Fakultät (Sloane: A Handbook of Integer Sequences) Imperativ: Zuweisungsorientiert Rekursiv: Werteorientiert hfac(n): r := 1; while n > 0 do r := r * n^n; n := n – 1; od return r; hfac(n)-> if n == 0 then 1 else n^n * hfac(n-1) © Zühlke 2011 Rekursion statt Iteration: Hyper-Fakultät (Sloane: A Handbook of Integer Sequences) Berechnung: hfac(3) Rekursiv: Werteorientiert 108 hfac(n)-> if n == 0 then 1 else n^n * hfac(n-1) 3 fun n if n == 0 then 1 else n^n * hfac(n-1) hfac(2) 2 fun 4 n if n == 0 then 1 else n^n * hfac(1) hfac(n-1) 1 0 1 fun fun n if n == 0 then 1 else n^n * hfac(n-1) hfac(0) 1 n if n == 0 then 1 else n^n * hfac(n-1) © Zühlke 2011 Komplexe Werte sind oft baumartig { A B vorname : „Martin“, nachname : „Mustermann“, anschrift : { C1 strasse : „Hauptstrasse 23“, ort : { plz : „76541“, stadt : „Neustadt“ } }, C21 kurse : [ { id : „Mo1“, title: „Tutorial C#“ }, { id : „Ndo4“, title: „Architektur“ } ] } C D D1 C2 C22 D111 D11 D112 D12 D121 D122 Wie ändert man komplexe Werte? 24 15 28 5 3 26 20 8 18 21 25 35 27 33 update (Tree, Value) Tree 41 Update: Ersetze 41 durch 42 Alter Baum 24 Neuer Baum 24 15 5 3 18 35 26 20 8 28 28 21 25 27 33 Beide Bäume existieren gleichzeitig! 35 41 42 Pattern Matching sum ([]) -> 0; sum ([X | Xs]) -> X + sum (Xs). abs (N) when N >= 0 -> N; abs (N) -> -N. length [] = 0 length (x:xs) = 1 + length xs zip [] _ = [] zip _ [] = [] zip (x:xs) (y:ys) = (x,y) : zip xs ys © Zühlke 2011 Higher-order Funktionen, Polymorphie und Lambda-Ausdrücke map :: (a -> b) -> [a] -> [b] map f [] = [] map f (x:xs) = f x : map f xs map abs [-2 .. 2] >> [2, 1, 0, 1, 2] map (\x -> x*x) [-2 .. 2] >> [4, 1, 0, 1, 4] © Zühlke 2011 Algebraische Datenstrukturen data Tree a = Empty | Node (Tree a) a (Tree a) mapTree :: (a -> b) -> Tree a -> Tree b mapTree f Empty = Empty mapTree f (Node l x r) = Node (mapTree f l) (f x) (mapTree f r) data Color = Green | Red | ... autumn t = mapTree change t where change Green = Red change c = c © Zühlke 2011 Erlang hat Closures – und Clojure auch (und Scala, F#, …) mk_mul_abs (M) -> fun (N) when N > 0 -> M * N; (N) -> M * (-N) end. mk_mul_abs fun Ma3 Ma3 = mk_mul_abs (3). lists:map(Ma3, [-2, -1, 0, 1, 2]). 3 M N when N > 0 -> M * N; M * (-N) >> [6, 3, 0, 3, 6] © Zühlke 2011 Haskell erlaubt Partial Application parabola a b c x = a*x^2 + b*x + c fun a b c fun a x a*x^2 + b*x + c p1 4.2 fun b fun -2.0 c fun x p1 :: Double -> Double 3.5 a*x^2 + b*x + c p1 = parabola 4.2 -2.0 3.5 y = p1 1.0 linear = parabola 0.0 l1 = linear 3.0 2.1 © Zühlke 2011 Programmierung im Großen Verantwortlichkeiten bündeln • Module, APIs und Interfaces • Kontrakte: Daten, Funktionen, Eigenschaften Werteorientierung und Higher-order Funktionen ermöglichen andere Schnittstellen und Kontrakte Information Hiding • Sichtbarkeiten, Namespaces • Implementierungsinterna verbergen Wie in OOP: Benötigt Augenmaß und Disziplin Beispiel: Pattern Matching © Zühlke 2011 Funktionale Programmierung = Werteorientierung & Higher-order Funktionen Welche Auswirkungen hat Funktionale Programmierung auf Architektur und Design? Eoin Wood: Software architecture is the set of design decisions which, if made incorrectly, may cause your project to be canceled. Grady Booch: Architecture represents the significant design decisions that shape a system, where significant is measured by cost of change. ANSI/IEEE 1471-2000 The fundamental organization of a system, embodied in its components, their relationships to each other and the environment, and the principles governing its design and evolution. ANSI/IEEE 1471-2000 The fundamental organization of a system, embodied in its components, their relationships to each other and the environment, and the principles governing its design and evolution. High-Level Architektur Architektur auf Marketing-Niveau: Ein typisches „Marchitecture“-Diagramm CRM Server System im Fokus Pricing Logistik DWH Auftragsverwaltung Kundenverwaltung DB Enterprise Architektur: Funktionalität bleibt lokal, Kommunikation über Werte System im Fokus Pricing HTML JSON/REST Logistik Auftragsverwaltung Portal Server Kundenverwaltung SOAP SOAP Service Bus MQ Host SOAP CRM Server AMQP QuerySet DB DWH High Level Architektur: Grobzerlegung bleibt bestehen Treiber: • Primär fachliche Zerlegung in Aufgaben- und Verantwortlichkeitsgebiete • Reflektiert auch die Organisation (Conway’s Law) Konsequenzen • Architektur auf dieser Ebene ist unabhängig von der Implementierungstechnologie • Interna der Komponenten/Subsysteme sind irrelevant • Kommunikation erfolgt über Werte, Funktionen bleiben lokal und werden nicht transportiert © Zühlke 2011 Mittlere Architektur Mittlere Architektur: Geprägt von nicht-funktionalen Systemanforderungen Funktionalität Zuverlässigkeit Bedienbarkeit Dokumentation Effizienz Machbarkeit Wartbarkeit Portabilität Gliederung gemäß ISO/IEC 9126 Enterprise Java Ein Blick auf Enterprise Java Historisch: • Transaktionsmonitor • Anbindung für verteilte Objekte an (Legacy)-Datenbanken Ausgelegt für • Informationssysteme • Hohen Durchsatz • Skalierbarkeit Funktionalität Bedienbarkeit DokuZuverläs- mentation sigkeit Machbarkeit Wartbarkeit Effizienz Portabilität © Zühlke 2011 Struktur: N-Tier-Schichtenarchitektur http://download.oracle.com/javaee/6/tutorial/doc/bnaay.html © Zühlke 2011 Einfluss von Java Everything is an Object • Auch für Dinge, die eigentlich keine Objekte sind (Stateless Session Beans, MDB, Backing Beans, Servlets, Transaktionen) Transparente Remote-Aufrufe (RMI) Thread-Management nur im Container • Das Lock-Model von Java erlaubt keine Komposition von Concurrent Libraries Interfaces anstatt abstrakter Klassen © Zühlke 2011 Erlang/OTP To Iterate is Human, to Recurse Divine – L. Peter Deutsch Ein Blick auf Erlang/OTP Historisch: • Soft-Realtime Systeme, PBX, Telco Ausgelegt für • (sehr hohe) Zuverlässigkeit und Fehlertoleranz • Skalierbarkeit mit massiver Parallelität • Kommunikationssysteme Funktionalität Bedienbarkeit Anwendungsbeispiele • Diverse Dokumentation Zuverlässigkeit Machbarkeit Effizienz Telco-Systeme von Ericsson • Ejabberd, RabbitMQ, CouchDB Wartbarkeit Portabilität © Zühlke 2011 OTP besteht aus lose gekoppelten „Anwendungen“ … Anwendung Web Server crypto odbc gen_tcp uses … und aus Supervisor-Prozesshierarchien Anwendung Supervisor Worker überwacht Supervisor Worker Worker Einfluss von Erlang auf OTP Erlang stellt die Basismechanismen bereit • Pure Funktionen, Datenstrukturen sind immer Werte • Nebenläufigkeit durch Prozesse – Prozesse sind referenzierbar Entitäten im Sinne von Domain Driven Design – Halten lokalen State, teilen sich aber keinen State! • Massive Parallelität durch leichtgewichtige Prozesse und asynchrones Messaging • Monitoring • Hot-Code von Prozessen über VM-Grenzen hinaus Swapping: Updates im laufenden System Nachbau in Scala: Akka (www.akka.io) © Zühlke 2011 Mittlere Architektur: Domäne der Blue-Print-Architekturen Standardarchitektur für eine bestimmte Systemklasse • Anwendungsdomäne und -typ sind relevant • Abgestimmt auf Hardware und Infrastruktur Geprägt von den nicht-funktionalen Anforderungen • Priorisierung und Balancierung der Qualitätsattribute Funktionalität Bedienbarkeit Das technisch Machbare bestimmt den Lösungsraum • Programmiersprachen • Laufzeitsysteme Zuverlässigkeit Dokumentation Wartbarkeit • Effizienz Machbarkeit Portabilität Bibliotheken © Zühlke 2011 Kleinteilige Architektur © Zühlke 2011 Peter Norvig: Design Patterns in Dynamic Languages (1998) Analyse der Design Patterns aus Lisp/Dylan-Sicht • GoF verwendet C++ und Smalltalk Unterscheidung von Implementierungsebenen • Unsichtbar: • Informell: • Formell: Wird von der Sprache abgedeckt Wird jedes mal neu implementiert Implementierung mit Template oder Macro Norvigs Fazit für die 23 GoF Patterns: • 16 werden deutlich einfacher oder unsichtbar © Zühlke 2011 http://en.wikipedia.org/wiki/Command_pattern GoF Command Pattern © Zühlke 2011 Ein Command ist eine Closure shutdown_command(Receiver) -> fun() -> Receiver ! {shutdown, now} end. @Client: MyMachine = …, Cmd = shutdown_command(MyMachine), add_action(Cmd, Target), MyMachine @Target: ... Cmd(). fun Receiver fun () Receiver ! {shutdown, now} Cmd © Zühlke 2011 Commands mit Undo sind Closures mit Pattern Matching start_command(Receiver) -> fun(do) -> Receiver ! {start, now}; (undo) -> Receiver ! {shutdown, now} end. @Client MyMachine = …, Cmd = start_command(MyMachine), MyMachine fun Receiver fun (do) @Target ... Cmd(do), ... Cmd(undo). Receiver ! {start, now} Cmd (undo) Receiver ! {shutdown, now} © Zühlke 2011 GoF Visitor Pattern “Languages that support double- or multiple dispatch lessen the need for the Visitor pattern. (CLOS actually supports multiple dispatch).” [GoF: p.339] http://en.wikipedia.org/wiki/Visitor_pattern Ein Composite Composite = Algebraischer Datentyp Visitor = Durchlauf der Struktur data CarElement = | | | Car [CarElement] Wheel String Body Engine Die Java Implementierung bei Wikipedia braucht dafür ca. 100 Zeilen toString Body toString Engine toString (Wheel s) toString (Car es) = = = = ["Visit Body"] ["Visit Engine"] ["Visit Wheel " ++ s] concat (map toString es) ++ ["Visit Car"] doCar doCar doCar doCar = = = = ["Moving my Body"] ["Starting my Engine"] ["Kicking Wheel " ++ s] concat (map doCar es) ++ ["Starting my Car"] Body Engine (Wheel s) (Car es) Charakteristik der GoF Patterns (1) Creational Patterns • Factories und Builder lösen allgemeine Probleme, unabhängig von OO, auch in C, Modula2, Haskell, Erlang • Prototypen und Singletons verschwinden Structural Patterns • Interface-Probleme und -Lösungen sind sprachunabhängig (Facade, Bridge, Adapter, Proxy) • Composite und Decorator werden unsichtbar © Zühlke 2011 Charakteristik der GoF Patterns (2) Behavioral Patterns • Können durch Lambdas, Pattern Matching, … anders realisiert werden. • Einige werden unsichtbar, andere sind nicht immer offensichtlich umzusetzen. Generelle Beobachtung: • Patterns, die auf Objektidentitäten basieren, müssen ganz anders umgesetzt werden. © Zühlke 2011 Funktionale Patterns Folie 69 Januar 2011 Dr. Klaus Alfert Dr. Bernd Löchner http://www.flickr.com/photos/robbie73/ © Zühlke 2011 Standardisierte Rekursion Listenfunktionen in Haskell: sum sum [] = 0 (x:xs) = x + sum xs sum = foldList 0 (+) product [] = 1 product (x:xs) = x * product xs product = foldList 1 (*) allTrue [] = True allTrue (x:xs) = x && allTrue xs allTrue = foldList True (&&) Extrahiere das Rekursionsschema foldList startVal combFct [] = startVal foldList startVal combFct (x:xs) = combFct x (foldList startVal combFct xs) © Zühlke 2011 Standardisierte Rekursion Mit foldList können viele Listenfunktionen implementiert werden length [] = 0 length (x:xs) = 1 + length xs length = foldList 0 cFct where cFct x len = 1 + len map f [] map f (x:xs) = [] = f x : map f xs map f = foldList [] cFct where cFct x ys = f x : ys © Zühlke 2011 Standardisierte Rekursion für Algebraische Datentypen Generalisiere für andere Datenstrukturen data Tree a = Empty | Node (Tree a) a (Tree a) foldTree eVal cFct Empty = eVal foldTree eVal cFct (Node l x r) = cFct (foldTree eVal cFct l) x (foldTree eVal cFct r) sumTree = foldTree 0 cFct where cFct sl x sr = sl + x + sr mapTree f = foldTree Empty cFct where cFct ml x mr = Node ml (f x) mr heightTree = foldTree 0 cFct where cFct hl x hr = 1 + max hl hr © Zühlke 2011 Auch im Visitor findet sich strukturierte Rekursion data CarElement = | | | Car [CarElement] Wheel String Body Engine visit :: (CarElement -> a) -> CarElement -> [a] visit f (Car es) = concat (map (visit f) es) ++ [f (Car es)] visit f x = [f x] printCar car = visit toString car where toString Body = "Visit toString Engine = "Visit toString Wheel s = "Visit toString Car es = "Visit Body" Engine" Wheel " ++ s Car" Separation of Concerns © Zühlke 2011 Recursion is the goto of functional programming. –Erik Meijer © Zühlke 2011 Leichtgewichtige Threads Erlang Prozesse sind nebenläufige, meist rekursive Funktionen CalcProc = spawn(fun() -> calc_loop(0)) calc_loop(Acc) -> NewAcc = receive {mul, X} -> {add, X} -> clear -> {get, Pid} -> Acc * X; Auswahl der Nachrichten Acc + X; durch Pattern Matching 0; Pid ! {result, Acc}, Acc; _IgnoredMsg -> Acc end, calc_loop(NewAcc). Funktioniert dank Tail Call Optimization © Zühlke 2011 Nebenläufige Endliche Automaten durch Prozesse, Funktionen und Messaging statt State-Pattern idle() -> receive {Number, incoming} -> start_ringing(), ringing(Number); off_hook -> start_tone(), dial() end. ringing(Number) -> receive {Number, off_hook} -> stop_ringing(), connected(Number); {Number, other_on_hook} -> stop_ringing(), idle() end. incoming off_hook Idle Dial Ringing other_ on_hook on_hook off_hook Connected Cesarini/Thompson: Erlang Programming 2009 Lazy Evaluation und unendliche Datenstrukturen Lazy Evaluation: (Teil-)Ausdrücke werden nur ausgewertet, wenn sie benötigt werden head (x:xs) = x Mit Lazy Evaluation gilt: head [42.0, 1.0/0] == 42.0 Essentiell, um eigene Kontrollstrukturen zu definieren unless cond x y = if !cond then x else y Ideal für DSLs © Zühlke 2011 Lazy Evaluation und unendliche Datenstrukturen Mit Lazy Evaluation werden (potentiell) unendliche Datenstrukturen möglich nats = [0 ..] take 5 nats == [0, 1, 2, 3, 4] Wurzel-Berechnung nach Newton-Raphson: iterate f n = [n,f(n),f2(n),f3(n), …] http://www.flickr.com/photos/robbie73/ sqrtSequence n = iterate (next n) n where next n x = (x + n/x)/2.0 sqrtSequence 2 >> [2.0, 1.5, 1.4166666666666665, 1.4142156862745097, 1.4142135623746899, 1.414213562373095, 1.414213562373095, 1.414213562373095, 1.414213562373095, … © Zühlke 2011 Pipes and Filters Die Mächtigkeit der Unix-Shell kommt durch Pipes&Filters Konstruktionen cat *.txt | tr –sc A-Za-z '\n' | tr a-z A-Z | sort | uniq –c | sort –nr | head Allgemein prg1 –p1 <arg | prg2 –p2 | prg3 –p3 | prg4 –p4 Dies geht auch mit lazy Lists und Funktionskomposition fct4 p4 . fct3 p3 . fct2 p2 . fct1 p1 $ arg Partial Application Listen als TransferContainer Funktionskomposition enspricht Pipe Funktionsapplikation enspricht < Idiomatisch in Haskell © Zühlke 2011 Map/Reduce y = f(x1) ⊗ … ⊗ f(xn) reduce ⊗ Zusammenfassung zum Endergebnis Inspiration für f(x1) x1 f(x2) x2 . map f . . . . . f(xn) xn Berechnung der (unabhängigen) Zwischenergebnisse s Map-Reduce-Framework (C++) Was es sonst noch gibt Eine Reihe weiterer Themen müssen wir auslassen • Memoization • Kombinatoren • Monaden • Macros • Typeful Programming • Continuations • Co-Algebraische Konstruktionen •… © Zühlke 2011 Fazit Funktionale Programmierung Andersartiger Programmierstil • Neue Abstraktionen und Kombinationsmöglichkeiten • Andere Zerlegung von Problemen • Andere Denkweisen und Idiome Einfachere Strukturen machen das Leben leichter • Keine • Pure (kaum) Seiteneffekte Functions • Testen und Parallelität werden sehr viel einfacher © Zühlke 2011 Auswirkungen auf Architektur Vieles bleibt gleich • Architekturtreiber sind weiterhin die Qualitätsattribute • Patterns werden zur Kommunikation für die Entwickler benötigt Kreativität und Augenmaß bleiben zwei wichtige Eigenschaften der Architekten • Gutes • Klare Design Verantwortlichkeiten © Zühlke 2011