# Programmierparadigmen ## Was ist ein Paradigma? - Paradigma – aus dem Altgriechischen Beispiel, Muster; Erzählung mit beispielhaftem Charakter (laut Duden) - Programmierparadigmen beschreiben grundsätzliche Arten wie Computer-Programme formuliert werden können - Programmiersprachen können einzelne oder viele Konzepte aufgreifen - Keine verbreitete Sprache greift alle behandelten Konzepte auf - Betrachtung unterschiedlicher Sprachen - Ziel der Veranstaltung: Weiten der in Algorithmen und Programmierung eingeführten Sichten hin zu einem Werkzeugkoffer zur Lösung realer Probleme... ## Warum unterschiedliche Paradigmen? Komplexität von Software schlecht beherrschbar ## Was bedeutet das? - Programmierer schreiben, testen und dokumentieren zwischen 325 und 750 Codezeilen pro Monat - maximal 360.000 Zeilen in 40 Jahren! - Komplexität muss verborgen werden, z.B. durch - Kapselung - Spezifische Spachkonstrukte, Domain Specific Languages - Ausdrucksstärkere Sprachen - Entwicklung neuer Programmierparadigmen hilft Grenzen (ein wenig) zu verschieben - Theoretische Rahmenbedingungen (Turing-Mächtigkeit, Satz von Rice) behalten Gültigkeit! ## Welche Paradigmen existieren? - Aus Vorlesung AuP: - Imperative Algorithmen - Applikative Algorithmen - Deduktive Algorithmen - Aber Vielzahl weiterer Formen - teilweise ergänzend, unterschiedliche Kategorisierung möglich - Bsp: prozedural, deklarativ, objekt-orientiert, datenstromorientiert, parallele & verteilte Programmierung... - Teilweise unterschiedliche Bezeichnungen - Applikativ bzw. Funktional - Deduktiv bzw. Logisch - Aktueller Trend: Multiparadigmen-Sprachen - Umsetzung unterschiedlichster Paradigmen in einer Sprache - Beispiele: Scala, neuere C++-Standards, ... # Objektorientierung und weiterführende Konzepte am Beispiel Java - Bekannt: - Grundlegendes Verständnis von Java - Kapselung durch Klassen und Vererbung - Ziele: - Verständnis der Probleme bei Vererbung und Typersetzbarkeit in objektorientierten Programmiersprachen - Kennenlernen der Grundideen generischer und abstrahierender Konzepte in Objekt-orientierter Programmierung (OOP) - Praktische Erfahrungen anhand von Java & C++ - Ausdrucksstärke erhöhen, Komplexität verbergen ## Unit Testing ### Motivation - Große Software-Systeme entwickeln sich über lange Zeiträume - Wie können Änderungen an komplexen Code-Basen beherrscht werden? - Veränderung über Zeit + Komplexität der Software - Änderungen führen mglw. zu Auswirkungen, die für Einzelne nicht immer überschaubar sind - Software muss nach Änderung von Grund auf durchgetestet werden - Verbreitetes Vorgehen: zusätzlichen Code schreiben, der eigentlichen Code automatisch “überprüft” - Nicht vollständig möglich (z.B. Halteproblem) - Eher Heuristik - Test-Code wird bei Ereignissen oder periodisch ausgeführt - Vor Releases, nach Commit in Repository, während der Entwicklung ... ### Eigenschaften von Unit-Tests - Software schlecht als Ganzes testbar -> Zergliederung von Software in sinnvolle Einheiten - Individuelle Tests dieser Einheiten - Dabei: reproduzierbar & vollautomatisierbar - Ziel: Wann immer Änderungen in komplexen Programmen vorgenommen werden, möglichst vollständiger Test, da Programmierer nicht mehr alles überblicken - Messung der Vollständigkeit der Tests schwierig - Üblich: Messung von Überdeckung (Coverage) in Bezug auf Anzahl Funktionen, Code-Zeilen oder Verzweigungen - Gute Praxis: Wenn ein Bug beim Testen oder Live-Betrieb auftritt -> Schreiben eines zusätzlichen Tests, um Wiederauftreten zu erkennen ### Unit-Testing in Java - De facto Standard: JUnit Framework - „Best Practice” für einfachen Einsatz: - Java Code in ein oder mehrere Klassen im Ordner src speichern - Im Ordner tests jeweils eine Klasse anlegen, die Funktionen einer Implementierungsklasse prüft - Konvention: Testklasse einer Klasse Name heißt NameTest - Eigentliche Tests werden in Methoden implementiert, die als Tests annotiert sind - Typischer Ansatz: für bekannte Werte ausführen und Ergebnis mit Grundwahrheit (erwartetes Verhalten) vergleichen, bspw. mit assertEquals-Funktion - Viele weitere Features, z.B. Deaktivieren von Tests, Timeouts, GUI Coverage, Mocks ### Unit Testing – Richtiges Abstraktionsniveau - Um die Tests auszuführen, müssen jeweils entsprechende Hauptprogramme generiert werden („Test Suites“) - Hauptschwierigkeiten von Unit-Tests: - Richtiges Abstraktionsniveau - „Herauslösen“ von zu testendem Code aus Umgebung - Zwei wesentliche Möglichkeiten: 1. Individuelles Testen von Klassen: - Vernachlässigt Zusammenspiel zwischen Klassen - Oft sehr aufwändig, da andere Klassen für Unit-Tests nachgebildet werden müssen (Mocks) - Was bei zyklischen Abhängigkeiten? 2. Gemeinsames Testen von Klassen: - Erfordert Eingreifen in gekapselte Funktionalitäten - Private & Protected Member-Variablen & Methoden! - Eigentlich nicht möglich?! ## Reflections - Normaler Ablauf: Programm schreiben, compilieren, ausführen - Aber was wenn ich ein Programm zur Laufzeit inspizieren oder verändern möchte? - Unterschiedliche Gründe - Testen (um Fehler zu injizieren!) - Fehlersuche („Debugging“) - Nachladen von Plugins zur Modularisierung von Programmen - Serialisierung/Deserialisierung von Code - „Patchen“ zur Laufzeit - Erkunden der Ablaufumgebung (z.B. OS-/Shared-Library Version) - Benötigt die Fähigkeit, im Programm Codestruktur zu analysieren und ggf. zu verändern: - Typisch: Abruf Klassenhierarchie, Auflisten von Methoden und Parametern, Austausch von Klassen und Methoden - Teil von Java, Python, ... API verstreut über verschiedene Packages, z.B. java.lang.Class, java.lang.instrument, java.lang.reflect ```java Class cls = "test".getClass(); System.out.println("Die Klasse heisst " + cls.getName()); // Die Klasse heisst java.lang.String ``` ```java // import java.lang.reflect.Method; Method[] methods = cls.getMethods(); for (Method m : methods) System.out.println(m.getName()); ``` ### Annotationen - Annotationen erlauben Anmerkungen an Klassen & Methoden - Beginnen mit @ - Einige wenige vordefinierte z.B. @Override - Aber auch eigene; u.a. durch Reflections abrufbar - Häufig genutzt, wenn zusätzlicher Code geladen wird (Java EE) - Oder um Unit-Tests zu markieren... ```java class MultiTest { @org.junit.jupiter.api.Test void mul() { ... ``` ### Reflektionen über Reflections - Reflections sind ein sehr mächtiges Werkzeug, aber Einsatz sollte wohldosiert erfolgen - Nachteile: - Geringe Geschwindigkeit weil Zugriff über Programmcode erfolgt - Kapselung kann umgangen werden - private, protected und final können entfernt werden - Aufruf/Veränderung interner Methoden & Auslesen/Veränderung interner Variablen - Synchronisation zwischen externen und internen Komponenten bei Weiterentwicklung? - Debugging veränderter Programme? - Sicherheit?! - Verwandte Techniken: - Monkey Patching (JavaScript-Umfeld) - Method Swizzling (Swift/Objective-C-Umfeld) ## Assertions - Kann man interne Zustände testen, ohne invasive Techniken wie Reflections? - Einfache Möglichkeit: An sinnvollen Stellen im Programmcode testen, ob Annahmen/Zusicherungen (Assertions) stimmen... - Tests, die nie falsch sein sollten - Erlauben gezielten Programmabbruch, um Folgefehler zu vermeiden - Erlauben gezieltes Beheben von Fehlern - Gemeinsames Entwickeln von Annahmen und Code ```java class Stack { public void push(Object o) { ... if(empty() == true) // es sollte ein Objekt da sein System.exit(-1); } ... } ``` Aber: Ausführungsgeschwindigkeit niedriger - Zeitverlust stark abhängig von Programm/Programmiersprache - Verbreitetes Vorgehen: - Aktivieren der Tests in UnitTests und Debug-Versionen - Deaktivieren in Releases - Benötigt spezielle „if“-Bedingung: assert - Aktivierung der Tests über Start mit java -ea ```java class Stack { public void push(Object o) { ... assert empty() == false } ``` ### Welche braucht man? - Woran erkennt man beim Programmieren bzw. (erneutem) Lesen von Code, dass man eine Assertion hinzufügen sollte? - Eine einfache Heuristik – Die „Eigentlich“-Regel: - Wenn einem beim Lesen von Programmcode ein Gedanke der Art „Eigentlich müsste an dieser Stelle XY gelten“ durch den Kopf geht, - dann sofort eine entsprechende Assertion formulieren! ### Spezielle Assertions: Pre- & Postconditions - An welchen Stellen ist es sinnvoll, Annahmen zu prüfen? - Einfache Antwort: an so vielen Stellen wie möglich - Komplexere Antwort: Design by contract, ursprünglich Eiffel - Methoden/Programmabschnitte testen Bedingung vor und nach Ausführung - Einige Sprachen bieten spezialisierte Befehle: requires und ensures -> Ziel mancher Sprachen: Formale Aussagen über Korrektheit ```java class Stack { public void push(Object o) { assert o != null // precondition ... assert empty() == false // postcondition } ... } ``` ### Klasseninvarianten - Bei OO-Programmierung sind Vor- und Nachbedingungen nur eingeschränkt sinnvoll - Bedingungen oft besser auf Objekt-Ebene -> interner Zustand - Invarianten spezifizieren Prüfbedingungen - In Java nicht nativ unterstützt: - Erweiterungen, wie Java Modeling Language - Simulation: ```java class Stack { void isValid() { for(Object o : _objs) // Achtung: O(n) Aufwand! assert o != null } public void push(Object o) { isValid() // always call invariant ... isValid() // always call invariant } ``` ## Exeptions Signifikantes Element vieler Sprachen: Wie wird mit Fehlern umgegangen? Fehler können unterschiedliche Gründe haben Besser für Code-Komplexität: Fehlerprüfungen an zentralerer Stelle - Abbrechen und Programm-Stack „abbauen“ bis (zentrale) Fehlerbehandlung greift - Dabei Fehler sinnvoll gruppieren - Java (und viele mehr): try/catch/throw-Konstrukt ```java private void readFile(String f) { try { Path file = Paths.get("/tmp/file"); if(Files.exists(file) == false) throw new IOException("No such dir"); array = Files.readAllBytes(file); } catch(IOException e) { // do something about it } } ``` throw übergibt ein Objekt vom Typ Throwable an Handler, dabei zwei Unterarten: - Error: Sollte nicht abgefangen werden z.B. Fehler im Byte-Code, Fehlgeschlagene Assertions - Exceptions: - Checked Exception: Programm muss Exception fangen oder in Methode vermerken - Runtime Exceptions: Müssen nicht (aber sollten) explizit behandelt werden, bspw. ArithmeticException oder IndexOutOfBoundsException ### Checked Exceptions Deklaration einer überprüften Exception: ```java void dangerousFunction() throws IOException { ... if(onFire) throw IOException("Already burns"); ... } ``` Die Deklaration mit "throws IOException" lässt beim build mögliche Fehler durch IOExceptions dieser Funktion zu, diese müssen durch die aufrufende Methode abgefangen werden. Aufrufe ohne try-catch-Block schlagen fehl! Sollte man checked oder unchecked Exceptions verwenden? - Checked sind potenziell sicherer - Unchecked machen Methoden lesbarer - Faustregel unchecked, wenn immer auftreten können (zu wenig Speicher, Division durch 0) Abfangen mehrerer unterschiedlicher Exceptions ```java try { dangerousFunction(); } catch(IOException i) { // handle that nasty error } catch(Exception e) { // handle all other exceptions } ``` Aufräumen nach einem try-catch-Block: Anweisungen im finally-Block werden immer ausgeführt, d.h. auch bei return in try- oder catch-Block (oder fehlerloser Ausführung) ```java try { dangerousFunction(); } catch(Exception e) { // handle exceptions return; } finally { // release locks etc.. } ``` ## Generizät von Datentypen (Typ-)Generizität: - Anwendung einer Implementierung auf verschiedene Datentypen - Parametrisierung eines Software-Elementes (Methode, Datenstruktur, Klasse, ...) durch einen oder mehrere Typen Beispiel: ```java int min(int a, int b) { return a < b ? a : b; } float min(float a, float b) { return a < b ? a : b; } String min(String a, String b) { // lexikographisch return a.compareTo(b) < 0 ? a : b; } ``` ### Grenzen von Typsubstitution Problem: Für jeden Typ? Wie kann sort implementiert werden? Möglicher Ausweg: Klassenhierarchie mit zentraler Basisklasse ```java void sort(Object[] feld) { ... } //z.B. java.lang.Object void sort(java.util.Vector feld) { ... } //alternativ (nutzt intern Object) ``` Möglicher Ausweg 2: Nutzung primitiver Datentypen nicht direkt möglich ```java Object[] feld = new Object[10]; //Object[] ≠ int[] feld[0] = new Integer(42); int i = ((Integer) feld[0]).intValue(); //erfordert Wrapper-Klassen wie java.lang.Integer ``` Weiteres Problem: Typsicherheit
Typ-Substituierbarkeit: Kann ein Objekt einer Oberklasse (eines Typs) durch ein Objekt seiner Unterklasse (Subtyps) ersetzt werden? Beispiel (isSubtyp): short $\rightarrow$ int $\rightarrow$ long Viele Programmiersprachen ersetzen Typen automatisch, d.h. diese wird auch für shorts und ints verwendet ```java long min(long a, long b) { return a < b ? a : b; } ``` Kreis-Ellipse-Problem: Modellierung von Vererbungsbeziehungen - „Ist ein Kreis eine Ellipse?“ „Oder eine Ellipse ein Kreis?“ - Annahme: Kreis := Ellipse mit Höhe = Breite ```java Circle c = new Circle(); c.skaliereX(2.0); //skalieren aus Klasse Circle c.skaliereY(.5); //is das noch ein Kreis? ``` evtl. Reihenfolge in der Klassenhierarchie tauschen (nutzung von Radius)? Was bedeutet das für Ellipse? Verwandte Probleme: Rechteck-Quadrat, Set-Bag ### Ko- und Kontravarianz Geg.: Ordnung von Datentypen von spezifisch $\rightarrow$ allgemeiner - Gleichzeitige Betrachtung einer Klassenhierarchie, die Datentypen verwendet - Kovarianz: Erhaltung der Ordnung der Typen - Kontravarianz: Umkehrung der Ordnung - Invarianz: keines von beiden - Anwendung für - Parameter - Rückgabetypen - Ausnahmetypen - Generische Datenstrukturen Beispiel: Basierend auf Meyer‘s SKIER-Szenario ```java class Student { String name; Student mate; void setRoomMate(Student s) { ... } } ``` Wie überschreibt man in einer Unterklasse Girl oder Boy die Methode „setRoomMate“ in elternfreundlicher Weise? Von Eltern sicher gewollt - Kovarianz: ```java class Boy extends Student { void setRoomMate(Boy b) { ... } } class Girl extends Student { void setRoomMate(Girl g) { ... } } ``` Was passiert mit folgendem Code? ```java Boy kevin = new Boy("Kevin"); Girl vivian = new Girl("Vivian"); kevin.setRoomMate(vivian); ``` - Verwendet setRoomMate der Basisklasse - setRoomMate Methoden der abgeleiteten Klassen überladen nur Spezialfälle $\rightarrow$ gültig - In C++ und Java keine Einschränkung der Typen zur Compile-Zeit - Kovarianz so nur in wenigen Sprachen implementiert (z.B. Eiffel über redefine); Überprüfung auch nicht immer statisch! - Auch bekannt als catcall-Problem (cat = changed availablility type) Ausweg: Laufzeitüberprüfung ```java class Girl extends Student { ... public void setRoomMate(Student s) { //student wird aufgerufen! nicht boy oder girl, dadurch können die methoden der klasse verwendet werden if (s instanceof Girl) super.setRoomMate(s); else throw new ParentException("Oh Oh!"); } } ``` Nachteil: Nur zur Laufzeit überprüfung #### Ko- und Kontravarianz für Rückgabewerte Kovarianz (gängig): ```java public class KlasseA { KlasseA ich() { return this; } } public class KlasseB extends KlasseA { KlasseB ich() { return this; } } ``` Kontravarianz macht wenig Sinn und kommt (gängig) nicht vor In objektorientierten Programmiersprachen im Allgemeinen - Kontravarianz: für Eingabeparameter - Kovarianz: für Rückgabewerte und Ausnahmen - Invarianz: für Ein- und Ausgabeparameter ### Liskovsches Substitutionsprinzip (LSP) Barbara Liskov, 1988 bzw. 1993, definiert stärkere Form der Subtyp-Relation, berücksichtigt Verhalten: > Wenn es für jedes Objekt $o_1$ eines Typs S ein Objekt $o_2$ des Typs T gibt, so dass für alle Programme P, die mit Operationen von T definiert sind, das Verhalten von P unverändert bleibt, wenn $o_2$ durch $o_1$ ersetzt wird, dann ist S ein Subtyp von T.' Subtyp darf Funktionalität eines Basistyps nur erweitern, aber nicht einschränken.
Beispiel: Kreis-Ellipse $\rightarrow$ Kreis als Unterklasse schränkt Funktionalität ein und verletzt damit LSP ### Generics in Java (Typsicherheit) Motivation: Parametrisierung von Kollektionen mit Typen ```java LinkedList liste = new LinkedList(); liste.add("Generics"); String s = liste.get(0); ``` auch für Iteratoren nutzbar ```java Iterator iter = liste.iterator(); while(iter.hasNext()) { String s = iter.next(); ... } ``` oder mit erweiterter for-Schleife ```java for(String s : liste) { System.out.println(s); } ``` Deklaration: Definition mit Typparameter ```java class GMethod { static T thisOrThat(T first, T second) { return Math.random() > 0.5 ? first : second; } } ``` - T = Typparameter (oder auch Typvariable) wird wie Typ verwendet, stellt jedoch nur einen Platzhalter dar - wird bei Instanziierung (Parametrisierung) durch konkreten Typ „ersetzt“ - nur Referenzdatentypen (Klassennamen), keine primitiven Datentypen Anwendung: - explizite Angabe des Typparameters ```java String s = GMethod.thisOrThat("Java", "C++"); Integer>thisOrThat(new Integer(42), new Integer(23)); ``` - automatische Typinferenz durch Compiler ```java String s = GMethod.thisOrThat("Java", "C++"); Integer i = GMethod.thisOrThat(new Integer(42), new Integer(23)); ``` #### Eingrenzung von Typparametern Festlegung einer Mindestfunktionalität der einzusetzenden Klasse, z.B. durch Angabe einer Basisklasse - Instanziierung von T muss von Comparable abgeleitet werden (hier ein Interface, dass wiederum generisch ist, daher Comparable) - Verletzung wird vom Compiler erkannt ```java static> T min(T first, T second) { return first.compareTo(second) < 0 ? first : second; } ``` Angabe des Typparameters bei der Klassendefinition: ```java class GArray { T[] data; int size = 0; public GArray(int capacity) { ... } public T get(int idx) { return data[idx]; } public void add(T obj) { ... } } ``` Achtung: new T[n] ist unzulässig! Grund liegt in der Implementierung von Generics: Es gibt zwei Möglichkeiten der internen Umsetzung generischen Codes: - Code-Spezialisierung: jede neue Instanziierung generiert neuen Code - Array → ArrayString, Array → ArrayInteger - Problem: Codegröße - Code-Sharing: gemeinsamer Code für alle Instanziierungen - Array → Array, Array → Array - Probleme: keine Unterstützung primitiver Datentypen & keine Anpassung von Algorithmen an Typ Java: Code-Sharing durch Typlöschung (Type Erasure) Typen beim Übersetzen geprüft, aber keinen Einfluss auf Code sichert auch Kompatibilität zu nicht generischem Code (Java-Version < 1.5) Bsp.: ArrayList vs. ArrayList Beispiel: Reflektion (Metaklassen) zur Erzeugung nutzen; danach Konstruktionsaufruf ```java public GArray(Class clazz, int capacity) { data = (T[]) Array.newInstance(clazz, capacity); } GArray array = new GArray(String.class, 10); ``` #### Kovarianz generischer Typen einfache Felder in Java sind kovariant ```java Object[] feld = new Object[10]; feld[0] = "String"; feld[1] = new Integer(42); ``` Instanziierungen mit unterschiedliche Typen sind jedoch inkompatibel ```java GArray anArray = new GArray(); GArray anotherArray = (GArray) anArray; ``` #### Wildcards Wildcard „?“ als Typparameter und abstrakter Supertyp für alle Instanziierungen ```java GArray aRef; aRef = new GArray(); aRef = new GArray(); ``` aber nicht: ```java GArray aRef = new GArray(); ``` hilfreich insbesondere für generische Methoden ```java // dieser Methode ist der genaue Typ egal static void pO(GArray ia) { for(Object o : ia) { System.out.print(o); } } // floats wollen wir mit Genauigkeit 2 haben static void pF(GArray ia) { for(Float f : ia) { System.out.printf("%5.2f\n", f); } } ``` Beschränkte Wildcards - nach „unten“ in der Klassenhierarchie $\rightarrow$ Kovarianz ```java ? extends Supertyp ``` Anwendungsbeispiel: Sortieren eines generischen Feldes erfordert Unterstützung der Comparable-Schnittstelle ```java void sortArray(GArray array) { ... } ``` - nach „oben“ in der Klassenhierarchie $\rightarrow$ Kontravarianz ```java ? super Subtyp ``` Anwendungsbeispiel: Feld mit ganzen Zahlen und Objekten ```java GArray array; // Zuweisungskompatibel zu ... array = new GArray(); array = new GArray(); array = new GArray(); // aber nur erlaubt: Object obj = array.get(0); ``` PECS = Producer extends, Consumer super $\rightarrow$ Producer liest nur sachen, Consumer legt daten/Objekte/... ab # Objectorientierung am Beispiel C++ - Ziel von C++: volle Kontrolle über Speicher & Ausführungsreihenfolgen sowie skalierbarere Projekt-Größe - Kompiliert zu nativem Maschinencode und erlaubt genauere Aussagen über Speicher-, Cache- und Echtzeitverhalten - Viele Hochsprachenelemente (Wie Java objektorientiert; sogar ähnliche Syntax an viele Stellen (weil Java ursprünglich an C++ angelehnt)) - Jedoch kompromissloser Fokus Ausführungsgeschwindigkeit, d.h. - Keine automatische Speicherverwaltung - Keine Initialisierung von Variablen (im Allgemeinen) - Kein Speicherschutz! - Dinge, die Zeit kosten, müssen im Allgemeinen erst durch Schlüsselworte aktiviert werden - C++ ist zu sehr großen Teilen eine Obermenge von C - Fügt Objektorientierung hinzu - Versucht fehleranfällige Konstrukte zu kapseln - Führt (viele) weitere Sprachkonstrukte ein, die Code kompakter werden lassen ## Vergleich mit Java ```java [Hello.java] package hello; // say that we are part of a package public class Hello { // declare a class called Hello: // declare the function main that takes an array of Strings: public static void main(String args[]) { // call the static method, println on class System.out with parameter "Hi Welt!": System.out.println("Hi Welt!"); } } // end of class Hello ``` ```cpp [Hello.cpp] // include declarations for I/O library where cout object is specified in namespace std:: #include // declare the function main that takes an int and array of strings and returns an int as the exit code int main(int argc, char* argv[]) { // stream string to cout object flush line with endl std::cout << "Hello world!" << std::endl; return 0; } // end of main() ``` - Unterschiede im Aufbau: - C++ hat globale Funktionen, also außerhalb von Klassen, wie main - #include gibt Dateien mit Klassen- und Funktionsdefinitionen an, die der Compiler einlesen soll - Java-Programme werden in packages gegliedert, in C++ gibt es mit modules ein ähnliches Konzept, welches aber (noch) nicht verbreitet ist - C++-Programme können (ohne Bezug zu Dateien) in namespaces untergliedert werden, hier std - Programmargumente: - In Java bekommt main ein String-Array übergeben, die Länge kann über .length abgefragt werden - C/C++-Programme erhalten ein Array von char* (Details zu Pointern folgen) - In C/C++ sind Arrays keine Pseudoobjekte, sondern Speicherbereiche in denen die Daten konsekutiv abgelegt sind $\rightarrow$ argc wird benötigt die Anzahl an Elementen zu kodieren - Rückgabewerte: - In Java keine Rückgabe in der main-Methode - In C++ Rückgabe eines exit code - 0 gibt an: Programmausführung erfolgreich - Andere Werte geben einen Programm-spezifischen Fehlercode zurück - Primitive Datentypen: - Wie in Java einfache Datentypen, die „Zahlen“ enthalten - char, short, int, long sind auf 64-bit Maschinen 8 bit, 16 bit, 32 bit und 64 bit breit (char braucht in Java 16 Bit!) - long ist auf 32 bit Maschinen 32 Bit breit, long long [sic!] ist immer 64 Bit - bool speichert Boolsche Werte (Breite hängt vom Compiler ab!) - Ein unsigned vor Ganzahltypen gibt an, dass keine negativen Zahlen in der Variable gespeichert werden (Beispiel: unsigned int) $\rightarrow$ Kann größere Zahlen speichern & zu viel Unsinn führen (beim Vergleich mit vorzeichenbehafteten Zahlen) ## C++ Klassen Header Foo.hpp deklariert Struktur und Schnittstelle ```cpp public: // Block ohne Zugriffsbeschränkung Foo(); // Konstruktor ~Foo(); // Destruktor protected: // Block von Dingen, auf die auch abgeleitete Klassen zugreifen dürfen int num; // Member-Variable }; // WICHTIGES Semikolon! ``` Implementierung in getrennter Datei Foo.cpp ```cpp #include "Foo.hpp" // Klassen Deklaration einbinden #include // Einbinden von Funktionen der stdlib Foo::Foo() : // Implementierung des Konstuktors von Foo num(5) { // Statische Initialisierung von num, Code in Klammern {} kann auch initialisieren std::cout << "c" << std::endl; } Foo::~Foo() { std::cout << "d" << std::endl; } ``` - Reine Implementierung auch im Header möglich, aber Trennung von Implementierung und Deklaration erlaubt schnelleres Kompilieren - Trennung nicht immer möglich (später mehr Details), aber im Allgemeinen zu bevorzugen - Der scope-Operator :: wird zum Zugriff auf namespaces und zur Beschreibung der Klassenzugehörigkeit von Methoden verwendet - Initialisierung von Variablen vor Funktionsrumpf etwas „merkwürdig“ zu lesen, aber erlaubt schnelle Implementierungen... - Syntax: nach Konstruktor : dann jeweils Variable(Wert) - Variablen durch , getrennt - Wichtig: Reihenfolge der Variablen wie in Deklaration der Klasse! - Schlüsselworte private, protected und public vergleichbar zu Java, werden aber vor ganze Blöcke geschrieben - Kapselung nur auf Ebene von Klassen ➞ Klassen sind immer public - protected erlaubt nur der Klasse selber und Unterklassen den Zugriff - Zugriffe außerhalb der Klassenstruktur können durch friend- Deklaration erlaubt werden (teilweise verrufen!) - Auch *final* ähnlich zu Java $\righarrow$ Verhindert weiteres Ableiten von Klassen - Schlüsselwort const markiert Methoden, die Objekte nicht verändern $\rightarrow$ Erlauben die Übergabe von Nur-Lesen-Referenzen - Größere Unterschiede zu Java: - Klassen können Destruktoren besitzen - Werden aufgerufen wenn Objekt zerstört wird - Kann bspw. dafür verwendet werden, um von dem Objekt allozierte Speicherbereiche freizugeben (Achtung: anschließend darf auf diese nicht mehr zugegriffen werden – problematisch wenn anderen Objekte diese Speicherbereiche bekannt gegeben wurden!) - Destruktor kann Zerstören eines Objekts aber nicht verhindern - Methodensignatur ~Klassenname() – kein Rückgabetyp! - Warum gibt es das nicht in Java? - Neben dem Standardkonstruktor oder einem expliziten Konstruktor existiert ein Copy-Constructor - Methodensignatur Klassenname(const Klassenname& c) - Wird aufgerufen wenn Objekt kopiert werden soll - Vergleichbar zu Object.clone() in Java - Überladen von Methoden vergleichbar zu Java - Parametertypen (oder const-Markierung) müssen sich unterscheiden! - Nur Veränderung des Rückgabewertes nicht ausreichend ```cpp class Foo { public: void doMagic(int i); void doMagic(std::string s); }; ``` ## C++ Präprozessor C/C++-Code kann vor dem Übersetzen durch einen Präprozessor verändert werden - Alle Präprozessor-Makros beginnen mit # - (Haupt-)gründe: - Importieren anderer Dateien - An- und Ausschalten von Features je nach Compile-Optionen - Kapselung von Plattform-spezifischem Code - Vermeiden von Redundanzen - Makros sollten vermieden werden - Schwierig zu lesen - Keine Namespaces - Keine Typsicherheit - Manchmal jedoch einzige Möglichkeit Beispiele: ```cpp #include "X.hpp" // Datei X.hpp aus Projekt-Ordner #include // Datei cstdio aus System-Includes #ifdef DEBUG // falls Konstante DEBUG definiert ist std::cout << "Wichtige Debugausgabe" << std::endl; #endif #define DEBUG // Konstante setzen #define VERSION 3.1415 // Konstante auf einen Wert setzen #define DPRINT(X) std::cout << X << std::endl; // Macro-Fkt. #undef DEBUG // Konstante löschen, good practice! #ifndef __linux__ // falls nicht für Linux übersetzt playMinesweeper(); #endif ``` ### Einschub: Include Guards Eine (oft hässliche) Eigenschaft des #include-Befehls: kein Überprüfen ob eine Datei vorher bereits eingebunden wurde. Problematisches Beispiel: ```cpp #include "Bar.hpp" //in "Bar.hpp" ist "Foo.hpp" bereits inkludiert worden #include "Foo.hpp" //Fehler weil kallse Foo bereits deklariert wurde ``` Common Practice: Include-Guards um alle Header-Dateien ```cpp #ifndef FOO_HPP #define FOO_HPP ... #endif ``` ## Speichermanagement - Programmspeicher enthält Code und Daten, vom Betriebssystem i.A. auf virtuelle Adressbereiche abgebildet - Unterschiedliche Varianten von Datenspeicher: - Stack hält alle Variablen einer Methode, aller aufrufenden Methoden, Parameter, Rückgabewerte und einige Management-Daten - Heap hält Variablen und Objekte, die nicht direkt über Methodenaufrufe übergeben werden - Speicher für globale und statische Objekte und Variablen - Java legt primitive Datentypen im Stack ab und Objekte im Heap - C++ kann sowohl primitive Datentypen als auch Objekte in Stack und Heap abbilden - Für den Stack bieten Java und C++ automatisches Speicher-Mgmt. - Für den Heap bietet nur Java automatisches Speicher-Mgmt. ### Eigenschaften des Stack-Speichers: - Variablen/Objekte haben klare Lebensdauer $\rightarrow$ Werden immer gelöscht wenn Funktion verlassen wird $\rightarrow$ Man kann Speicher nicht „aufheben“ - In der Regel sehr schnell, weil im Prozessor-Cache - In der Größe begrenzt, z.B. 8MB bei aktuellen Linux-Systemen - Für flexiblere Speicherung brauchen wir anders organisierten Speicher... ### Heap: Keine klare Struktur - Anlegen: in C++ & Java mit new - Um angelegten Speicher anzusprechen: Zeiger und Referenzen - In Java automatisch Zeiger - In C++ Zeiger durch * hinter Typ ```cpp int main() { int* i = new int[3]; int* j = new int; delete [] i; delete j; return 0; } ``` - Löschen von Heap-Speicher: - Java automatisch - In C++ nur manuell - durch genau einen Aufruf von delete - Programmierer ist dafür verantwortlich, dass danach kein Zeiger auf diesen Speicher mehr benutzt wird - Warum der Unterschied? - Nicht einfach festzustellen, wann letzter Zeiger auf Objekt gelöscht wurde - Zeiger können selbst auch im Heap gespeichert sein - Zyklische Referenzen! - Relativ aufwändiges Scannen, in Java durch regelmäßige Garbage Collection gelöst - Führt zu Jitter (Schwankung der Zeitdauer, die bestimmte Programmabschnitte zur Bearbeitung benötigen) & Speicher-Overhead, ... Beispiele - Anlegen eines Objects auf dem Heap: ```cpp std::string* s = new std::string("wiz!"); delete s; ``` - Allokation von Feldern: ```cpp int* i = new int[29]; // gültige Indicies 0-28 i[0] = 23; delete [] i; // nicht mit delete i; verwechseln! ``` - Zeiger können durch & auf beliebige Variablen ermittelt werden ```cpp int i = 0; int* j = &i; // &-Operator erzeugt Zeiger; j darf nicht gelöscht werden ``` - Zeiger können durch * dereferenziert werden ```cpp int i = 0; int* j = &i; // &-Operator erzeugt Zeiger *j = 1; // Zugriff auf Variableninhalt ``` - Zugriff auf Methoden/Member Variablen ```cpp std::string* s = new std::string("wiz"); (*s).push_back('?'); // manuelles Derefenzieren s->push_back('?'); // -> Operator delete s; ``` - C++ übergibt alles als Kopie ```cpp void set(std::string s) { s = "foo"; } int main() { std::string s = "bar"; set(s); std::cout << s; // gibt bar aus return 0; } ``` - Zeiger können verwendet werden, um schreibend zuzugreifen ```cpp void set(std::string* s) { *s = "foo"; } int main() { std::string s = "bar"; set(&s); std::cout << s; // gibt foo aus return 0; } ``` - Zeiger erlauben syntaktisch sehr viele Dinge mit unvorhersehbaren Nebenwirkungen ```cpp std::string* magicStr() { std::string s("wiz!"); return &s; // gibt Speicher auf Stack weiter; Tun Sie das nie! } int main() { std::string* s = magicStr(); std::cout << *s; // Stack ist bereits überschrieben! return 0; } ``` Warum wirken sich Speicherfehler so unvorhersehbar aus? - Speicherfehler entstehen sehr häufig durch Zugriff auf Speicherbereiche nachdem diese freigegeben worden sind - Ob hierdurch später ein Fehler auftritt, hängt davon ab wie der freigegebene Speicher nach der Freigabe wieder genutzt wird - Die insgesamte Speichernutzung wird durch die Gesamtheit aller Speicherallokationen und -freigaben beeinflusst - Das kann dazu führen, dass ein Speicherfehler in Modul X erst lange nach seinem Entstehen Auswirkungen zeigt, nachdem in einem anderen Modul Y eine Änderung eingeführt wurde - Auch eingebundene dynamische Bibliotheken haben Einfluss - Das macht es so schwierig, solche Fehler schwierig zu finden ### Bessere Alternative: Referenzen - Zeigen ebenfalls auf Speicher, Compiler stellt aber sicher, dass Speicher gültig ist (wenn man nicht in Zeiger wandelt etc.)! - Markiert durch Suffix & - Beispiel: ```cpp void set(std::string& s) { s = "foo"; } int main() { std::string s = "bar"; set(s); std::cout << s; // gibt foo aus return 0; } ``` - Dereferenzierung durch * und -> nicht notwendig - Referenzen sind toll, haben aber eine Einschränkung: ```cpp std::string& magicStr() { std::string s("wiz!"); return s; //< FEHLER } ``` ```cpp std::string& magicStr() { static std::string s("wiz!"); return s; // klappt prima } ``` - Per Referenz übergebene Rückgabewerte müssen im Speicher noch existieren, wenn Methodenaufruf abgeschlossen ist... - OK für globale Variablen, Member-Variablen, statische Variablen... - Nicht-OK für Speicher der wirklich dynamisch alloziert werden muss - Allgemein bleiben nur Zeiger und Heap: ```cpp std::string* magicStr() { std::string* s = new std::string("wiz!"); return s; // klappt prima, aber: aufpassen wann s gelöscht // werden kann und vollständig vergessen wurde! } ``` - Konvertierung von Zeigern zu Referenzen mit „*“-Operator: ```cpp std::string& s = *magicStr(); // Konvertieren in Referenz; Delete nicht mehr möglich std::string s2 = *magicStr(); // Konvertieren in Referenz & Kopie! Delete nicht direkt möglich ``` - Konvertierung von Referenzen zu Zeigern mit „&“-Operator: ```cpp std::string s("bla"); std::string* sStar = &s; // Konvertieren in Zeiger ``` - Abschließende Bemerkungen zum Speicher - Niemals Speicher doppelt löschen – Niemals Löschen vergessen! - Häufige Praxis: Zeiger auf NULL setzen nach dem Löschen (Aber: gibt es danach wirklich keinen anderen Zeiger mehr?) - Nur Speicher löschen, der mit „new“ allokiert wurde - Speicher der mit „new“ allokiert wurde in jedem möglichen Programmablauf löschen (selbst wenn Exceptions auftreten)... - Nie über Feldgrenzen hinweg lesen/schreiben (auch negative Indizes!) - Programme ausgiebig testen (dabei Address Sanitizer aktivieren!) - Statische Code Analyse nutzen: z.B. http://cppcheck.sourceforge.net - malloc/free sind Äquivalente in Sprache C und nicht typsicher! - Verbreitetes Vorgehen in C++ (Pattern): Resource Acquisition Is Initialization (RAII) - Speicher (oder Ressourcen im Allgemeinen) wird nur im Konstruktor einer Klasse reserviert - Destruktor gibt Speicher frei - Sicheres (Exceptions!), nachvollziehbares Konstrukt - Beispiel: (Funktioniert leider noch nicht immer) ```cpp class MagicString { std::string* s; public: MagicString() : s(new std::string("wiz!")) {} std::string* magicStr() { return s; } ~MagicString() { delete s; } }; ``` ## Vererbung - Vermeiden von Mehrfachimplementierungen - Vermeiden von Dopplung interner Daten - Vererbung syntaktisch ebenfalls ähnlich zu Java: ```java class Foo { public: int magic() const { return 23; } int enchanting() const { return 0xbeef; } }; class FooBar : public Foo { public: int magic() const { return 42; } }; ``` - Unterschied zu Java: Methoden „liegen“ bei C++ statisch im Speicher - D.h. f.magic(); ruft statisch magic-Methode in Klasse Foo auf, weil f eine Referenz vom Typ Foo ist - Vermeidet Mehrfachimplementierungen, realisiert aber keine einheitliche Schnittstelle! - Nach Überschreiben einer Methode wollen wir meist, dass genutzte Methode nicht vom Referenztyp abhängt, sondern vom Objekttyp - Idee zu jedem Objekt speichern wir Zeiger auf zu nutzende Methoden - Tabelle wird *vtable* bezeichnet - Markierung von Methoden, für die ein Zeiger vorgehalten wird, mit Schlüsselwort virtual - Funktionierendes Beispiel: ```cpp class Foo { public: virtual int magic() const { return 23; } }; class FooBar : public Foo { public: int magic() const override { return 42; } }; int r(const Foo& f) { return f.magic(); } int main() { return r(FooBar()); // yeah gibt 42 zurück! } ``` - virtual-Markierung genügt in Oberklasse, alle abgeleiteten Methoden ebenfalls „virtuell“ - override-Markierung optional, aber hätte vor fehlendem virtual gewarnt! ## Mehrfachvererbung - C++ unterstützt keine Interfaces - Aber C++ unterstützt Mehrfachvererbung! Pro Interface eine Basisklasse -> mit abstrakten Methoden erstellen - Gute Praxis: Explizites Überschreiben ```cpp class NiceFooBar : public Foo, public Bar { // erlaube NiceFooBar().magic() int magic() const override { return Bar::magic(); } }; ``` - Wegen Mehrfachvererbung: kein super:: - Stattdessen immer NameDerBasisKlasse:: - Aber: Diamond Problem - Markieren der Ableitung als virtual behebt das Problem Komposition statt Vererbung - Vererbungshierarchien werden trotzdem häufig als zu unflexibel angesehen - Ein möglicher Ausweg: - Klassen flexiblen aus anderen Objekten zusammensetzen - Einzelobjekte modellieren Aspekte des Verhaltens des Gesamtobjekts - Werden beim Anlegen des Gesamtobjekts übergeben - Engl.: Prefer composition over inheritance ```cpp class Automatisierungsmodul { public: void steuere() = 0; }; class Roboter : public Automatisierungsmodul{ public: void steuere() { /* call HAL */ } }; class DumbDevice : public Automatisierungsmodul { public: void steuere() { /* do nothing */ } }; class Geraet { protected: Automatisierungsmodul* _a; Geraet(Automatisierungsmodul* a, Saeuberungsmodul* s): _a(a), _s(s) {} public: void steuere() { _a->steuere(); } }; ``` ## Operator Overloading - In Java: Unterschied zwischen "==" und "equals()" bei String-Vergleich - In C++: "=="-Operator für String-Vergleich - Umsetzung: Hinzufügen einer Methode mit Namen *operatorx* wobei für x unter anderem zulässig: $+ - * / % ^ & | ~ ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= <=> && || ++ -- , ->* -> () []$ - Vereinfacht Nutzung komplexer Datentypen teilweise sehr stark - Aber: Erfordert Disziplin beim Schreiben von Code - Oft erwartet: Freiheit von Exceptions (Wer gibt Speicher frei, wenn eine Zuweisung fehlgeschlagen ist?) - Semantik der Operatoren muss selbsterklärend sein - Ist der Operator auf einem multiplikativen Ring + oder * ? - Was ist, wenn zwei ungleiche Objekte jeweils kleiner als das andere sind? - Ist * bei Vektoren das Skalar- oder das Kreuzprodukt (oder etwas ganz anderes)? ## Templates - Generische Datentypen werden in C++ mit Templates realsiert - Häufig ähnlich eingesetzt wie Generics, aber können neben Typen auch Konstanten enthalten - Zur Compile-Zeit aufgelöst ➞ Deklaration & Implementierung in Header-Dateien - Einfaches Beispiel (mit Typen, ähnl. zu Generics, primitive Typen ok!): ```cpp template // typename keyword -> deklariert T als Typ T max(T a, T b) { return (a > b ? a : b); } ``` ```cpp int i = 10; int j = 2; int k = max(j, i); // explizit int l = max(j, i); // automat. Typinferenz durch Parametertypen ``` - Ein wichtiges Grundkonzept von Templates: Substitution failure is not an error (SFINAE) es -> wird solange nach passenden Templates (in lexikogr. Reihenfolge) gesucht bis Parameter passen (sonst Fehler!) - Sehr häufig verwendetes Konstrukt & mächtiger als es scheint, aber schwer zu beherrschen - Wir können alternativ versuchen, durch SFINAE zu verhindern, dass Funktionen doppelt definiert sind - Trick: Einführen eines Pseudoparameters, der nicht benutzt wird ```cpp template T quadrieren(T i, typename T::Val pseudoParam = 0) { T b(i); b *= i; return b; } ``` - Trick: Einführen eines Hilfstemplates (sogenannter trait): wenn arithmetic::Cond definiert ist, muss T = int sein ```cpp template struct arithmetic {}; template<> struct arithmetic { using Cond = void*; }; ``` - Definition einer Funktion, die nur für int instanziiert werden kann: ```cpp template T quadrieren(T i, typename arithmetic::Cond = nullptr) { return i * i; } ``` ## Container - Templates werden an vielen Stellen der C++ Standard-Bibliothek verwendet - Container implementieren alle gängigen Datenstrukturen - Prominente Beispiele: ```cpp template class vector; // dynamisches Array template class list; // doppelt verkette Liste template class set; // geordnete Menge basiert auf Baum template class map; // Assoziatives Array, geordnet // wie oben aber basierend auf Hash-Datenstruktur template class unordered_set; template class unordered_map; ``` - Alle Templates sind stark vereinfacht dargestellt, weitere Parameter haben Standardwerte, die z.B. Speicherverhalten regeln ### Container Enumerieren - Je nach Struktur unterschiedlicher Zugriff - Oft über Iteratoren vom Typ Container::iterator, bspw. vector::iterator ```cpp std::vector v{ 1, 2, 3 }; // Initialisierung über Liste // “normale” for-Schleife, Beachte: Überladene Operatoren ++ und * for(std::vector::iterator i = v.begin(); i != v.end(); ++i) { std::cout << *i << std::endl; } // auto erlaubt Typinferenz → Code lesbarer, aber fehleranfälliger for(auto i = v.begin(); i != v.end(); ++i) { std::cout << *i << std::endl; } // range loop (nutzt intern Iteratoren), komplexe Datentypen nur mit Ref. “&” sonst werden Kopie erzeugt! for(int i : v) { // hier ohne “&”, da nur int in v gespeichert std::cout << i << std::endl; } ``` ### Container Einfügen - Unterschiedliche Operationen je nach Container-Typ - std::vector::push_back() fügt neues Element am Ende ein - Allokiert ggf. neuen Speicher - Exisitierende Pointer können dadurch invalidiert werden!!! - std::list zusätzlich push_front() fügt Element am Anfang ein - std::set, std::map, … - insert() fügt Element ein, falls es nicht existiert (Optional mit Hinweis wo ungefähr eingefügt werden soll) - operator[] erlaubt Zugriff aber auch Überschreiben alter Elemente - emplace() Einfügen, ohne Kopien zu erzeugen (nicht behandelt) ### Container Löschen - Unterschiedliche Operationen je nach Container-Typ - Allgemein: erase(Container::iterator) (Vorsicht ggf. werden Iterator/Zeiger auf Objekte dadurch ungültig!) - std::vector::resize() löscht implizit letzte Elemente bei Verkleinerung - std::vector::pop_back()entfernt letztes Element - std::list hat zusätzlich pop_front() - std::set, std::map, … löschen nur mit erase() ## Shared Pointer - Synonym: Smart Pointer - Ziel: Sichereres Verwenden von Speicher - Idee: kleine, schlanke Zeiger-Objekte, die Referenzzähler + Zeiger auf komplexere Objekte enthalten, wird letztes Zeiger-Objekt gelöscht, wird auch das komplexe Objekt gelöscht - Realisierung mit RAII, Templates, Operator-Überladung - Beispiel, wie shared_ptr sich verhalten sollten ```cpp using stringP = shared_ptr; stringP hello() { // gibt kopie der referenz zurück return stringP(new std::string("Hello!")); } int main() { stringP x = hello(); stringP y(x); // Erstellen einer weiteren Referenz std::cout << y->length(); return 0; // Original-String wird gelöscht wenn letzte Ref. weg } template class shared_ptr { // Vereinfacht! T* p; // Zeiger auf eigentliches Objekt int* r; // Referenzzähler public: // neue Referenz auf Objekt erzeugen shared_ptr(T* t) : p(t), r(new int) { *r = 1; } // Referenz durch andere Referenz erzeugen shared_ptr(const shared_ptr& sp) : p(sp.p), r(sp.r) { ++(*r); } T* operator->() const { // benutzen wie einen richtigen Zeiger return p; } ~shared_ptr() { if(--(*r) == 0) { // Objekt loeschen, wenn letzte Referenz weg delete r; delete p; } } }; // TODO operator= implementieren! ``` # Einführung in Funktionale Programmierung ## Applikaive Algorithmen Grundidee: - Definition zusammengesetzter Funktionen durch Terme: $f(x) = 5x + 1$ - Unbestimmte: - x, y, z, . . . vom Typ int - q, p, r , . . . vom Typ bool - Terme mit Unbestimmten (z.B. Terme vom Typ int: $x, x - 2, 2x + 1, (x + 1)(y - 1)$) - Terme vom Typ bool $p, p Λ true, (p V true) ⇒ (q V false)$ > Sind $v_1, ..., v_n$ Unbestimmte vom Typ $\tau_1,...,\tau_n$ (bool oder int) und ist $t(v_1, ..., v_n)$ ein Term, so heißt $f(v_1, ..., v_n) = t(v_1, ..., v_n)$ eine Funktionsdefinition vom Typ $\tau$ . $\tau$ ist dabei der Typ des Terms. - Erweiterung der Definition von Termen - Neu: Aufrufe definierter Funktionen sind Terme > Ein applikativer Algorithmus ist eine Menge von Funktionsdefinitionen. Die erste Funktion $f_1$ wird wie beschrieben ausgewertet und ist die Bedeutung (Semantik) des Algorithmus. - Kategorisierung nach unterschiedlichen Kriterien - Ordnung der Sprache - Erster Ordnung: - Funktionen können (nur) definiert und aufgerufen werden - Höherer Ordnung: - Funktionen können außerdem als Parameter an Funktionen übergeben werden und/oder Ergebnisse von Funktionen sein. - Funktionen sind hier auch Werte! -- erstklassige Werte; - Erstklassig: Es gibt keine Einschränkungen. - Umgekehrt: Wert ist eine Funktion ohne Parameter - Auswertungsstrategie: - Strikte Auswertung: - Synonyme: strict evaluation, eager evaluation, call by value, applikative Reduktion - Die Argumente einer Funktion werden vor Eintritt in die Funktion berechnet (ausgewertet) – wie z.B. in Pascal oder C. - Bedarfsauswertung: - Synonyme: Lazy evaluation, call by need - Funktionsargumente werden unausgewertet an die Funktion übergeben - Erst wenn die Funktion (in ihrem Körper) die Argumente benötigt, werden die eingesetzten Argumentausdrücke berechnet, und dann nur einmal. - Realisiert „Sharing“ (im Unterschied zur Normalform-Reduktion – dort werden gleiche Ausdrücke immer wieder erneut berechnet). - Typisierung: - Stark typisiert: Die verbreiteten funktionalen Programmiersprachen sind stark typisiert, d.h. alle Typfehler werden erkannt. - Statisch typisiert: Typprüfung wird zur Übersetzungszeit ausgeführt. - Dynamisch typisiert: Typprüfung wird zur Laufzeit ausgeführt - Untypisiert: Reiner Lambda-Kalkül (später) ## Die funktionale Programmiersprache Erlang - Entwickelt ab der zweiten Hälfte der 1980er Jahre im Ericsson Computer Science Laboratory (CSLab, Schweden) - Ziel war, eine einfache, effiziente und nicht zu umfangreiche Sprache, die sich gut zur Programmierung robuster, großer und nebenläufiger Anwendungen für den industriellen Einsatz eignet. - Erste Version einer Erlang-Umgebung entstand 1987 auf der Grundlage von Prolog. Später wurden Bytecode-Übersetzer und abstrakte Maschinen geschaffen. ### Arbeiten mit Erlang - Erlang-Programme werden durch Definition der entsprechenden Funktionen in Modulen erstellt - Module können in den Erlang-Interpreter geladen und von diesem in Zwischencode übersetzt werden - Anschließend können Anfragen im Interpreter gestellt werden Modul "fakultaet.erl": ```erlang -module(fakultaet). -export([fak/1]). fak(0) -> 1; fak(N) when N > 0 -> (N) * fak(N-1). ``` Laden in Interpreter mittels: ```c(fakultaet)``` Testen der Funktion, z.B. mit: ```fakultaet:fak(5)``` ### Elemente von Erlang - Ganzzahlen (Integer): - 10 - -234 - 16#AB10F - 2#110111010 - $A - B#Val erlaubt Zahlendarstellung mit Basis B (mit B ≤ 36). - $Char ermöglicht Angabe von Ascii-Zeichen ($A für 65). - Gleitkommazahlen (Floats): - 17.368 - -56.654 - 12.34E-10. - Atome (Atoms): - abcef - start_with_a_lower_case_letter - 'Blanks can be quoted' - 'Anything inside quotes \n\012' - Erläuterungen: - Atome sind Konstanten, die Ihren eigenen Namen als Wert haben - Atome beliebiger Länge sind zulässig - Jedes Zeichen ist innerhalb eines Atoms erlaubt - Einige Atome sind reservierte Schlüsselwörter und können nur in der von den Sprachentwicklern gewünschen Weise verwendet werden als Funktionsbezeichner, Operatoren, Ausdrücke etc. - Reserviert sind: *after and andalso band begin bnot bor bsl bsr bxor case catch cond div end fun if let not of or orelse query receive rem try when xor* - Tupel (Tuples): - {123, bcd} % Ein Tupel aus Ganzzahl und Atom - {123, def, abc} - {person, 'Joe', 'Armstrong'} - {abc, {def, 123}, jkl} - {} - Erläuterungen: - Können eine feste Anzahl von “Dingen” speichern - Tupel beliebiger Größe sind zulässig - Kommentare werden in Erlang mit % eingeleitet und erstrecken sich dann bis zum Zeilenende - Listen: - [123, xyz] - [123, def, abc] - [{person, 'Joe', 'Armstrong'}, {person, 'Robert', 'Virding'}, {person, 'Mike', 'Williams'}] - "abcdefgh" wird zu [97,98,99,100,101,102,103,104] - "" wird zu [] - Erläuterungen: - Listen können eine variable Anzahl von Dingen speichern - Die Größe von Listen wird dynamisch bestimmt - "..." ist eine Kurzform für die Liste der Ganzzahlen, die die ASCIICodes der Zeichen innerhalb der Anführungszeichen repräsentieren - Variablen: - Abc - A_long_variable_name - AnObjectOrientatedVariableName - Erläuterungen: - Fangen grundsätzlich mit einem Großbuchstaben an - Keine „Funny Characters" - Variablen werden zu Speicherung von Werten von Datenstrukturen verwendet - Variablen können nur einmal gebunden werden! - Der Wert einer Variablen kann also nicht mehr verändert werden, nachdem er einmal gesetzt wurde: *N = N + 1 VERBOTEN!* - Einzige Ausnahmen: Die anonyme Variable "_" (kein Lesen möglich) und das Löschen einer Variable im Interpreter mit f(N). - Komplexe Datenstrukturen: - [{{person,'Joe', 'Armstrong'}, {telephoneNumber, [3,5,9,7]}, {shoeSize, 42}, {pets, [{cat, tubby},{cat, tiger}]}, {children,[{thomas, 5},{claire,1}]}}, {{person,'Mike','Williams'}, {shoeSize,41}, {likes,[boats, beer]}, ... }] - Erläuterungen: - Beliebig komplexe Strukturen können erzeugt werden - Datenstrukturen können durch einfaches Hinschreiben erzeugt werden (keine explizite Speicherbelegung oder -freigabe) - Datenstrukturen können gebundene Variablen enthalten - Pattern Matching: - $A = 10$ erfolgreich, bindet A zu 10 - ${B, C, D} = {10, foo, bar}$ erfolgreich, bindet B zu 10, C zu foo and D zu bar - ${A, A, B} = {abc, abc, foo}$ erfolgreich, bindet A zu abc, B zu foo - ${A, A, B} = {abc, def, 123}$ schlägt fehl (“fails”) - $[A,B,C] = [1,2,3]$ erfolgreich, bindet A zu 1, B zu 2, C zu 3 - $[A,B,C,D] = [1,2,3]$ schlägt fehl - $[A,B|C] = [1,2,3,4,5,6,7]$ erfolgreich bindet A zu 1, B zu 2, C zu [3,4,5,6,7] - $[H|T] = [1,2,3,4]$ erfolgreich, bindet H zu 1, T zu [2,3,4] - $[H|T] = [abc]$ erfolgreich, bindet H zu abc, T zu [] - $[H|T] = []$ schlägt fehl - ${A,_, [B|_],{B}} = {abc,23,[22,x],{22}}$ erfolgreich, bindet A zu abc, B zu 22 - Erläuterungen: - „Pattern Matching“, zu Deutsch „Mustervergleich“, spielt eine zentrale Rolle bei der Auswahl der „richtigen“ Anweisungsfolge für einen konkreten Funktionsaufruf und dem Binden der Variablen für die Funktionsparameter (siehe spätere Erklärungen) - Beachte die Verwendung von "_", der anonymen (“don't care”) Variable (diese Variable kann beliebig oft gebunden, jedoch nie ausgelesen werden, da ihr Inhalt keine Rolle spielt). - Im letzten Beispiel wird die Variable B nur einmal an den Wert 22 gebunden (das klappt, da der letzte Wert genau {22} ist) - Funktionsaufrufe: - module:func(Arg1, Arg2, ... Argn) - func(Arg1, Arg2, .. Argn) - Erläuterungen: - Arg1 .. Argn sind beliebige Erlang-Datenstrukturen - Die Funktion und die Modulnamen müssen Atome sein (im obigen Beispiel module und func) - Eine Funktion darf auch ohne Parameter (Argumente) sein (z.B. date() – gibt das aktuelle Datum zurück) - Funktionen werden innerhalb von Modulen definiert - Funktionen müssen exportiert werden, bevor sie außerhalb des Moduls, in dem sie definiert sind, verwendet werden - Innerhalb ihres Moduls können Funktionen ohne den vorangestellten Modulnamen aufgerufen werden (sonst nur nach einer vorherigen Import-Anweisung) - Modul-Deklaration: ```erlang -module(demo). -export([double/1]). double(X) -> times(X, 2). times(X, N) -> X * N. ``` - Erläuterungen: - Die Funktion double kann auch außerhalb des Moduls verwendet werden, times ist nur lokal in dem Modul verwendbar - Die Bezeichnung double/1 deklariert die Funktion double mit einem Argument - Beachte: double/1 und double/2 bezeichnen zwei unterschiedliche Funktionen - Eingebaute Funktionen (Built In Functions, BIFs) - date() - time() - length([1,2,3,4,5]) - size({a,b,c}) - atom_to_list(an_atom) - list_to_tuple([1,2,3,4]) - integer_to_list(2234) - tuple_to_list({}) - Erläuterungen: - Eingebaute Funktionen sind im Modul erlang deklariert - Für Aufgaben, die mit normalen Funktionen nicht oder nur sehr schwierig in Erlang realisiert werden können - Verändern das Verhalten des Systems - Beschrieben im Erlang BIFs Handbuch - Definition von Funktionen: ```erlang func(Pattern1, Pattern2, ...) -> ... ; % Vor dem ; steht der Rumpf func(Pattern1, Pattern2, ...) -> ... ; % Das ; kündigt weitere Alternativen an ... % Beliebig viele Alternativen möglich func(Pattern1, Pattern2, ...) -> ... . % Am Ende muss ein Punkt stehen! ``` - Erläuterungen: - Funktionen werden als Sequenz von Klauseln definiert - Sequentielles Testen der Klauseln bis das erste Muster erkannt wird (Pattern Matching) - Das Pattern Matching bindet alle Variablen im Kopf der Klausel - Variablen sind lokal zu jeder Klausel (automatische Speicherverw.) - Der entsprechende Anweisungsrumpf wird sequentiell ausgeführt Was passiert wenn wir mathstuff:factorial() mit einem negativen Argument aufrufen? Der Interpreter reagiert nicht mehr? - Erste Reaktion: rette das Laufzeitsystem durch Eingabe von CTRL-G - User switch command 01. --> h 02. c [nn] - connect to job 03. i [nn] - interrupt job 04. k [nn] - kill job 05. j - list all jobs 06. s [shell] - start local shell 07. r [node [shell]] - start remote shell 08. q - quit erlang 09. ? | h - this message 10. --> - Liste durch Eingabe von j alle Jobnummern auf - Beende den entsprechenden Shell-Job durch k - Starte eine neue Shell durch Eingabe von s - Liste durch erneute Eingabe von j die neuen Jobnummern auf - Verbinde durch Eingabe von c mit neuer Shell - Zweite Reaktion: Ergänze factorial() um zusätzliche Bedingung: - „Beschütze“ die Funktion vor Endlosrekursion durch Ergänzung eines sogenannten Wächters (Guards) bei dem entsprechenden Fallmuster (Pattern) - Erläuterungen: - Der Guard wird durch das Atom when und eine Bedingung vor dem Pfeil -> formuliert - Vollständig „beschützte“ Klauseln können in beliebiger Reihenfolge angeordnet werden - Achtung: Ohne Guard führt diese Reihenfolge zu Endlosschleifen - Beispiele für Guards: - number(X) % X is a number - integer(X) % X is an integer - float(X) % X is a float - atom(X) % X is an atom - tuple(X) % X is a tuple - list(X) % X is a list - length(X) == 3 % X is a list of length 3 - size(X) == 2 % X is a tuple of size 2. - X > Y + Z % X is > Y + Z - X == Y % X is equal to Y - X =:= Y % X is exactly equal to Y (i.e. 1 == 1.0 succeeds but 1 =:= 1.0 fails) - Alle Variablen in einem Wächter müssen zuvor gebunden werden - Traversieren (“Ablaufen”) von Listen: ``` average(X) -> sum(X) / len(X). sum([H|T]) -> H + sum(T); % summiert alle Werte auf sum([]) -> 0. len([_|T]) -> 1 + len(T); % Wert des Elements len([]) -> 0. % interessiert nicht ``` - Die Funktionen sum und len verwenden das gleiche Rekursionsmuster - Zwei weitere gebräuchliche Rekursionsmuster: ``` double([H|T]) -> [2*H|double(T)]; % verdoppelt alle double([]) -> []. % Listenelemente member(H, [H|_]) -> true; % prüft auf member(H, [_|T]) -> member(H, T); % Enthaltensein member(_, []) -> false. % in Liste ``` - Listen und Akkumulatoren: ``` average(X) -> average(X, 0, 0). average([H|T], Length, Sum) -> average(T, Length + 1, Sum + H); average([], Length, Sum) -> Sum / Length. ``` - Interessant sind an diesem Beispiel: - Die Liste wird nur einmal traversiert - Der Speicheraufwand bei der Ausführung ist konstant, da die Funktion “endrekursiv” ist (nach Rekursion steht Ergebnis fest) - Die Variablen Length und Sum spielen die Rolle von Akkumulatoren - Bemerkung: average([]) ist nicht definiert, da man nicht den Durchschnitt von 0 Werten berechnen kann (führt zu Laufzeitfehler) - „Identisch“ benannte Funktionen mit unterschiedlicher Parameterzahl: ``` sum(L) -> sum(L, 0). sum([], N) -> N; sum([H|T], N) -> sum(T, H+N). ``` - Erläuterungen: - Die Funktion sum/1 summiert die Elemente einer als Parameter übergebenen Liste - Sie verwendet eine Hilfsfunktion, die mit sum/2 benannt ist - Die Hilfsfunktion hätte auch irgendeinen anderen Namen haben können - Für Erlang sind sum/1 und sum/2 tatsächlich unterschiedliche Funktionsnamen - Shell-Kommandos: - h() - history . Print the last 20 commands. - b() - bindings. See all variable bindings. - f() - forget. Forget all variable bindings. - f(Var) - forget. Forget the binding of variable X. This can ONLY be used as a command to the shell - NOT in the body of a function! - e(n) - evaluate. Evaluate the n:th command in history. - e(-1) Evaluate the previous command. - Erläuterungen: Die Kommandozeile kann wie mit dem Editor Emacs editiert werden (werl.exe unterstützt zusätzlich Historie mit Cursortasten) - Spezielle Funktionen: ``` apply(Func, Args) apply(Mod, Func, Args) % old style, deprecated ``` - Erläuterungen: - Wendet die Funktion Func (im Modul Mod bei der zweiten Variante) auf die Argumente an, die in der Liste Args enthalten sind - Mod und Func müssen Atome sein bzw. Ausdrücke, die zu Atomen evaluiert werden und die eine Funktion bzw. Modul referenzieren - Jeder Erlang-Ausdruck kann für die Formulierung der an die Funktion zu übergebenden Argumente verwendet werden - Die Stelligkeit der Funktion ist gleich der Länge der Argumentliste - Beispiel: ```` 1> apply( lists1,min_max,[[4,1,7,3,9,10]]).``` -> {1, 10} - Bemerkung: Die Funktion min_max erhält hier ein (!) Argument - Anonyme Funktionen: ``` Double = fun(X) -> 2*X end. > Double(4). > 8 ``` - Erläuterung: - Mittels “fun” können anonyme Funktionen deklariert werden - Diese können auch einer Variablen (im obigen Beispiel Double) zugewiesen werden - Interessant wird diese Art der Funktionsdefinition, da anonyme Funktionen auch als Parameter übergeben bzw. als Ergebniswert zurückgegeben werden können - Die Funktionen, die anonyme Funktionen als Parameter akzeptieren bzw. als Ergebnis zurückgeben nennt man Funktionen höherer Ordnung Kap 3a seite 48-84 ## Lambda Kalkül [comment]: <> (Kapitel 3b) # Multithreading & Parallele Programmierung [comment]: <> (Kapitel 4) ## Grundlagen ## Parallele Programmierung in Erlang ## Parallele Programmierung in C++ ### Threads Thread („Faden“) := leichtgewichtige Ausführungseinheit oder Kontrollfluss (Folge von Anweisungen) innerhalb eines sich in Ausführung befindlichen Programms - Threads teilen sich den Adressraum des ihres Prozesses - in C++: Instanzen der Klasse std::thread - führen eine (initiale) Funktion aus ```cpp #include #include void say_hello() { std::cout << "Hello Concurrent C++\n"; } int main() { std::thread t(say_hello); t.join(); } ``` Alternative Erzeugung von Threads über Lamda Ausdruck: ```cpp std::thread t([]() { do_something(); }); ``` oder mit Instanz einer Klasse - erfordert Überladen von operator() ```cpp struct my_task { void operator()() const { do_something(); } }; my_task tsk; std::thread t1(tsk); // mit Objekt std::thread t2{ my_task() }; // über Konstruktor ``` Parameter-Übergabe bei Thread-Erzeugung über zusätzliche Argumente des thread-Konstruktors. Vorsicht bei Übergabe von Referenzen, wenn Eltern-Thread vor dem erzeugten Thread beendet wird. ```cpp void fun(int n, const std::string& s) { for (auto i = 0; i < n; i++) std::cout << s << " "; std::cout << std::endl; } std::thread t(fun, 2, "Hello"); t.join(); ``` Warten auf Threads - t.join() wartet auf Beendigung des Threads t - blockiert aktuellen Thread - ohne join() keine Garantie, dass t zur Ausführung kommt - Freigabe der Ressourcen des Threads ```cpp std::thread t([]() { do_something(); }); t.join(); ``` Hintergrund Threads - Threads können auch im Hintergrund laufen, ohne dass auf Ende gewartet werden muss - „abkoppeln“ durch detach() - Thread läuft danach unter Kontrolle des C++-Laufzeitsystems, join nicht mehr möglich Thread-Identifikation - Thread-Identifikator vom Typ `std::thread::id` - Ermittlung über Methode get_id() ```cpp void fun() { std::cout << "Hello from " << std::this_thread::get_id() << std::endl; } std::thread t(fun); t.join(); ``` ### Datenparallele Verarbeitung ### Kommunikation zwischen Threads ### Taskparallelität ...