diff --git a/lec-cpp-advanced-fp.html b/lec-cpp-advanced-fp.html new file mode 100644 index 0000000..acaa920 --- /dev/null +++ b/lec-cpp-advanced-fp.html @@ -0,0 +1,952 @@ + + + + + + + + + Funktionale Programmierung (FP) in C++ + + + + + +
+

Funktionale Programmierung (FP) in C++

+

+Martin Sulzmann +

+
+
+

Übersicht

+ +

Funktionaler +Programmierstil

+

Ist das was neues?

+

Nein! Siehe Lisp aus den 50er/60er Jahren.

+

Funktionale Programmierung (FP) war immer da. War aber nie “main +stream”. Findet sich aber jetzt in den meisten Programmiersprachen (z.B. +Java, Python, …).

+
+
+

Motivation

+

Mapper int Array

+
void mapInt(int* p, int len, int f(int)) {
+ for(auto i=0; i<len; i++)
+   p[i] = f(p[i]);
+}
+
+int inc(int x) { return x+1; }
+int square(int x) { return x*x; }
+
+void beispiel1() {
+   int x[5] = { 1,2,3,4,5 };
+
+   mapInt(x,5,inc);
+   mapInt(x,5,square);
+}
+

Mapper float Array

+
void mapFloat(float* p, int len, float f(float)) {
+ for(auto i=0; i<len; i++)
+   p[i] = f(p[i]);
+}
+
+float incF(float x) { return x+1; }
+float squareF(float x) { return x*x; }
+
+void beispiel2() {
+   float y[5] = { 1.0,2.0,3.0,4.0,5.0 };
+
+   mapFloat(y,5,incF);
+   mapFloat(y,5,squareF);
+}
+

(Funktionale) Abstraktion über die “map” Funktion.

+

Effektiv identisch zur “int” Version.

+

Generische Variante

+
template<typename T>
+void map(T* p, int len, T f(T)) {
+ for(auto i=0; i<len; i++)
+   p[i] = f(p[i]);
+}
+
+void beispiel3() {
+   int x[5] = { 1,2,3,4,5 };
+   float y[5] = { 1.0,2.0,3.0,4.0,5.0 };
+
+   // Instanziierung der generischen Funktion.
+   map<int>(x,5,inc);
+   map<float>(y,5,squareF);
+
+
+   // Mit anonymen Funktionen (aka "lambdas")
+   map<int>(x,5,[](int i) -> int { return i+1; });
+   map<float>(y,5,[](float f) -> float { return f*f; });
+}
+

Funktionale Abstraktionen kommen oft in Kombination mit +“generics”.

+

“map” Funktionen werden als anonyme Funktion (aka “lambda” Notation) +dargestellt.

+
+
+

Lambda notation

+

Bevor wir uns FP in C++ anschauen, zuerst ein paar Beispiele in +Lambda Notation.

+

Betrachte

+
f = \x -> x + 1
+

Wir verwenden hier die Lambda Notation zur Darstellung von +anonymen Funktionen.

+

Betrachte folgende Lambda Funktion zur Inkrementierung einer +beliebigen Zahl.

+
\x -> x + 1
+

Beschreibung der Syntax

+
\x
+ +
->
+ +
x + 1
+ +

Zusammengefasst.

+
f = \x -> x + 1
+
+    ^^^^^^^^^^^^
+
+    Lambda Funktion = anonyme Funktion (Funktion ohne Namen)
+

Die Lambda Funktion ist gebunden an die Variable f.

+

Funktionsauswertung

+

Auswertung von f(1).

+

Wir schreiben “=>” fuer einen Auswertungsschritt.

+
    f(1)
+=> (\x -> x + 1)   (1)
+   ^^^^^^^^^^^^    ^^^^^
+    Funktion       Argument
+
+=> (Ersetze Parameter x durch das Argument 1)
+
+   1 + 1
+
+=> 2
+

Weiteres Beispiel.

+
y = 1
+
+f = \ x -> x + y
+

Beachte. Die Lambda Funktion \ x -> x + y hat +Paramter x und verweist auf die “freie” Variable +y.

+

D.h. im Funktionsrumpf können wir auf Variable verweisen, welche in +einem äusseren Sichtbarkeitsbereicht definiert sind.

+

f(1) liefert 2

+

weil

+
       f(1) =>  x + y wobei gilt x = 1
+            =>  1 + y
+            =>  1 + 1 weil in y der Wert 1 gespeichert ist
+            => 2
+

Partielle Funktionsanwendung

+

Betrachte

+
sum1 = \(x,y) -> x + y
+
+
+ sum1(3,4) => 7
+

Parameter von sum1 ist ein Paar von Werten.

+

Betrachte

+
sum2 = \x -> (\y -> x + y)
+
+(sum2(3))(4)
+

sum2(3) liefert eine Funktion! Dies wird auch partielle +Funktionsanwendung (“partial +application” genannt.

+
sum2(3) => \y -> 3 + y
+

Deshalb

+
(sum2(3))(4) => (\y -> 3 + y)(4) => 3 + 4 => 7
+ +

Anstatt

+
(sum2(3))(4)
+

können wir kürzer schreiben

+
sum2(3)(4)
+

Klammern können “links” weggelassen werden. Die Annhame ist das +Funtionsanwendung links-assoziativ ist.

+ +

Betrachte folgendes Java Beispiel.

+
(obj1.meth1()).meth2()
+ +

In Java ist dies als Methodenverkettung (method +chaining).

+

Methodenverkettung ist links-assoziativ. Deshalb können wir obigen +Programmcode wie folgt schreiben.

+
obj1.meth1().meth2()
+
+
+

Capture by copy versus capture by reference

+

Betrachte

+
y = 1
+
+f = \ x -> x + y
+
+y = 2
+
+z = f(1)
+

Wir definieren zuerst y dann f. Danach +überschreiben wir den Wert gespeichert in y mit +2.

+

Welchen Wert liefert die Auswertung von f(1)? Was ist der Wert von +z?

+

Es kommt darauf an wie wir die “freie” Variable y in der +Definition von f behandeln.

+

Es gibt hier 2 Möglichkeiten.

+ +

Capture by copy

+

capture by copy, bedeutet Werte von freien Variable werden +kopiert.

+

Betrachte

+
    y = 1
+    f = \ x -> x + y   // An der Stelle wir ersetzen effektiv y durch den Wert 1.
+                       // Aus der Sicht des Compilers gilt:
+                       // f = \x -> x + 1
+
+    y = 2
+    z = f(1)
+

f(1) liefert 2 weil

+
           f(1) => x + y wobei x = 1
+                => 1 + y wobei die Annahme capture by copy gilt, der kopierte Wert von y ist hier 1
+                => 1 + 1
+                => 2
+

Frage: Weshalb benutzt man capture by copy, anstatt direkt einen +konstanten Wert einzugtragen? Also warum der Umweg über die Variable +y?

+

Antwort: Siehe Konstanten. Besser Platzhalter (Variablen) zu +verwenden als konkrete Zahlen.

+

Capture by reference

+

capture by reference, bedeutet bei der Auswertung des Lambda +Ausdrucks, holen wir uns immer den aktuellen Wert der freien +Variablen.

+

Betrachte

+
    y = 1
+    f = \ x -> x + y   // An der Stelle merken wir uns die Referenz auf die freie Variable y
+                       // Aus der Sicht des Compilers, mehr Arbeit ist notwendig.
+                       // Wir muessen uns alle freien Variablen merken.
+
+    y = 2
+    z = f(1)
+

f(1) liefert 3 weil

+
             f(1) => x + y wobei x = 1
+                  => 1 + y wegen Annahme (b), gilt y ausgewertet liefert 2
+                  => 1 + 2
+                  => 3
+

C++

+

C++ unterstützt capture by copy und capture by reference

+

Deshalb ist die C++ Lambda Notation “kompliziert”.

+
[capture](parameters) -> return_type { function_body }
+

In den meisten anderen Programmiersprachen wird immer capture by +reference verwendet.

+
+
+

Einfache FP Beispiele in C++

+
// --std=c++14
+
+#include <functional>
+#include <stdio.h>
+#include <iostream>
+using namespace std;
+
+
+
+void mapInt(int* p, int len, int f(int)) {
+ for(auto i=0; i<len; i++)
+   p[i] = f(p[i]);
+}
+
+
+int inc(int x) { return x+1; }
+int square(int x) { return x*x; }
+
+
+int test1() {
+   int x[5] = { 1,2,3,4,5 };
+
+   // Funktionen mit Namen
+   mapInt(x,5,inc);
+   mapInt(x,5,square);
+
+   // Lambdas
+   mapInt(x,5, [](int x) -> int { return x+1; });
+   mapInt(x,5, [](int x) -> int { return x*x; });
+
+   // Kombination mit auto
+   auto f = [](int x) -> int { return x+1; };
+   mapInt(x,5,f);
+
+   printf("%d",x[0]);
+   return 0;
+}
+
+void test2() {
+
+  // Entweder
+  // int x = 1;
+  // oder wir verwenden "auto".
+  auto x = 1;
+  auto f = [x] (int y) -> int { return x + y; };
+
+  cout << f(1) << " ";
+  x = 5;
+  cout << f(1) << " ";
+
+  auto g = [&x] (int y) -> int { return x + y; };
+
+  x = 2;
+  cout << g(1) << " ";
+  x = 3;
+  cout << g(1);
+}
+
+
+
+int sum(int x, int y) { return x + y; }
+
+void sumEx() {
+      cout << sum(5, 2) << endl;
+      cout << sum(10, 5) << endl;
+}
+
+// Mit lambdas.
+void sumEx1() {
+      auto sum = [](int x, int y) -> int { return x + y; };
+
+      cout << sum(5, 2) << endl;
+      cout << sum(10, 5) << endl;
+
+
+      // Beachte. Inferenz von Rueckgabe Typ ist moeglich.
+
+      auto sum2 = [](int x, int y) /* -> int */ { return x + y; };
+
+      cout << sum2(5, 2) << endl;
+      cout << sum2(10, 5) << endl;
+
+}
+
+// Partial function application.
+// Uncurried form. See https://wiki.haskell.org/Currying
+// The lambda bound variable x appears in the capture list of the inner lambda.
+void sumEx2() {
+
+      // Funktion welche eine Funktion hat Rueckgabewerthat!
+
+      // Betrachte std::function<int(int).
+      // Beschreibt eine Funktion mit Argumenttyp int und Rueckgabetyp int.
+      // In der Mathematik geschrieben als "int -> int".
+      // In C++, ist "function" ein Template zur Beschreibung solcher Funktionsraeume.
+      // Das ganze ist definiert im Name space "std".
+
+      function<function<int(int)>(int)> sum;
+      //         int -> int
+      //  int -> (int -> int) ist der Typ von sum !!!
+      // Im allgemeinen gilt:
+      //  function<B(A)>  beschreibt den Funktionstyp "A -> B".
+
+      sum = [](int x) -> function<int(int)> { return [x](int y) -> int { return x + y; }; };
+      // 1.   Auesserer Lambda-Ausdruck, hat Parameter x vom Typ int
+      // 2.                     Rueckgabetyp ist ein Lambda Funktionstyp!
+      // 3.                                                    Innerer Lambda-Ausdruck!
+      // 4.                                                    Parameter y vom Typ int.
+
+
+
+
+      // Liefert Fehler!
+      // Weil sum, d.h. der aeussere Lambda-Ausdruck erwartet "int x" als Parameter!
+      // cout << sum(5,2) << endl;
+
+      // cout << sum(5) << endl;
+      // Beachte:
+      //  Welchen Wert liefert sum(5) ???
+      // sum(5) liefert eine Funktion zurueck!
+      // Deshalb liefert "cout << sum(5);" einen Compilerfehler.
+
+      // cout << (sum(5))(2) << endl;
+
+      // Wieso erhalten wir 7?
+      // 1. sum(5) liefert effektiv den Lambda-Ausdruck "\y -> 5 + y".
+      // 2. sum(5) ist eine Funktion, wir fuettern diese Funktion mit einem weiteren Argument.
+      // 3.     (sum(5))      (2)
+      //        ^^^^^^^^      ^^^^^
+      //        Funktion      Argument
+      // 4. Deshalb erhalten wir 7 als Ergebnis!
+
+      // Beispiel einer partiellen Funktionsanwendung.
+
+      // ZWISCHENDURCH: In C++/Java gilt folgendes.
+      // Im Fall von Methodenaufrufen, betrachte z.B.
+      //  (obj.m1()).m2()
+      //   obj unterstuert die Methode m1 und liefert ein Objekt zurueck
+      //   auf welches wir Methode m2 anwenden.
+      //  Abkuerzendende Schreibweise: obj.m1().m2()
+      //  Annahme ist, dass Methodenaufrufe sind links-assoziativ.
+
+      //  Obige Annahme fuer Methodenaufrufe wird auch angewandt
+      //  fuer Funktionsanwendungen.
+      //  Annahme ist, dass Funktionsanwendung sind links-assoziativ.
+      //  Deshalb anstatt (sum(5))(2) koennen wir kuerzer schreiben:
+
+      cout << sum(5)(2) << endl;
+
+}
+
+// Spielereien.
+void sumEx3() {
+
+  // Maximale Typinferenz!!!
+  auto sum = [](int x) /* -> std::function<int(int)> */ { return [x](int y) { return x + y; }; };
+
+  auto inc = sum(1);
+
+  cout << inc(2) << endl;
+  // Effektiv das gleiche wie
+  // cout << sum(1)(2) << endl;
+
+}
+
+void test3() {
+  // sumEx();
+  // sumEx1();
+  // sumEx2();
+  // sumEx3();
+
+}
+
+
+int main() {
+  // test1();
+  // test2();
+  // test3();
+
+}
+
+
+

FP Version von count

+
#include <functional>
+#include <stdio.h>
+#include <iostream>
+using namespace std;
+
+////////////////////////////////////
+// Zaehlen von Woertern.
+// Wort = Sequenz von Zeichen welche nicht ' ' (Leerzeichen/Blank) entaehlt.
+
+
+
+// C Version
+
+
+char* skipBlanks(char* input) {
+  while(input[0] == ' ')
+    input++;
+  return input;
+}
+
+char* scanWord(char* input) {
+  while(*input != '\0' && *input != ' ')
+    input++;
+  return input;
+}
+
+
+int count(char* input) {
+  int cnt = 0;
+
+  while(*input != '\0') {
+    input = skipBlanks(input);
+    if(*input != '\0') {
+      cnt++;
+      input = scanWord(input);
+    }
+  }
+  return cnt;
+}
+
+// C++ strings
+
+int countString(string s) {
+ char cString[s.size() + 1];
+
+ s.copy(cString, s.size() + 1);
+ cString[s.size()] = '\0';
+
+ return count(cString);
+}
+
+
+////////////////////////////////////
+// Funktionale Version
+
+
+std::function<string(string)> skip(std::function<bool(char)> p)  {
+  return [p](string s) -> string  {
+    if (s.length() == 0) {
+      return s;
+    } else if(p(s[0])) {
+    string s2 = s.substr(1);
+    return skip(p)(s2);
+    } else {
+    return s;
+    }
+  };
+}
+
+
+int countFP(string input) {
+  int cnt = 0;
+
+  auto skipBlanks = skip([](char c) -> bool { return c == ' '; });
+
+  auto scanWord = skip([](char c) -> bool { return c != ' ';});
+
+  while(input.length() != 0) {
+    input = skipBlanks(input);
+    if(input.length() != 0) {
+      cnt++;
+      input = scanWord(input);
+    }
+  }
+  return cnt;
+}
+
+void testCount(std::function<int(string)> countFunc) {
+  string tests[3] = { "", "dfd ", " dfjkd .s22?? " };
+
+  for(int i=0; i <3; i++) {
+    cout << "\n" << countFunc(tests[i]);
+  }
+}
+
+int main() {
+  testCount(countString);
+  testCount(countFP);
+}
+

FP Version und C Strings

+
std::function<char*(char*)> skip(std::function<bool(char)> p)  {
+  return [p](char* s) -> char*  {
+    if (*s == '\0') {
+      return s;
+    } else if(p(s[0])) {
+    return skip(p)(s+1);
+    } else {
+    return s;
+    }
+  };
+}
+
+
+int countFP(char* input) {
+  int cnt = 0;
+
+  auto skipBlanks = skip([](char c) -> bool { return c == ' '; });
+
+  auto scanWord = skip([](char c) -> bool { return c != ' ';});
+
+  while(*input != '\0') {
+    input = skipBlanks(input);
+    if(*input != '\0') {
+      cnt++;
+      input = scanWord(input);
+    }
+  }
+  return cnt;
+}
+
+
+

Nochmal Stack Machines

+ +
// Simple stack-based VM.
+
+#include <vector>
+#include <stack>
+#include <iostream>
+#include <string>
+using namespace std;
+
+// VM code
+typedef enum {
+  PLUS,
+  MULT,
+  ONE,
+  TWO
+} Code_t;
+
+string show(Code_t c) {
+  switch(c) {
+  case ONE: return "1";
+  case TWO: return "2";
+  case PLUS: return "+";
+  case MULT: return "*";
+  }
+}
+
+string show(vector<Code_t> cs) {
+  string s;
+  for(int i=0; i < cs.size(); i++) {
+    s = s + show(cs[i]);
+    if(i < cs.size() - 1) {
+      s = s + " ";
+    }
+  }
+  return s;
+}
+
+class VM {
+    vector<Code_t> code;
+  public:
+    VM(vector<Code_t> c) : code(c) {}
+
+  void showAndRun() {
+    cout << "\n Exp: " << exp();
+    cout << "\n VM code: " << show(code);
+    cout << "\n => " << run();
+  }
+
+  void showAndRun2() {
+    cout << "\n Exp: "
+     << exec<string>(
+         [](Code_t x) -> string {
+           return show(x);
+         },
+         [](string x, Code_t op, string y) -> string {
+           switch(op) {
+               case PLUS: return ("(" + x + " + " + y + ")");
+               case MULT: return ("(" + x + " * " + y + ")");
+           default: return ""; // should never apply
+           }
+             });
+    cout << "\n VM code: " << show(code);
+    cout << "\n => "
+     << exec<int>(
+         [](Code_t x) -> int {
+               switch(x) {
+               case ONE: return 1;
+           case TWO: return 2;
+           default: return -1; // should never apply
+           }
+         },
+         [](int x, Code_t op, int y) -> int {
+           switch(op) {
+               case PLUS: return (x+y);
+               case MULT: return (x*y);
+           default: return -1; // should never apply
+           }
+             });
+  }
+
+/*
+
+Methoden run sind exp sind von der Struktur sehr aehnlich.
+
+Wir abstrahieren den gemeinsamen Kern in der exec Methode
+welche in einer apply Funktion parameterisiert ist.
+
+Durch geeignet Wahl der apply Funktion erhalten wir run und exp.
+
+*/
+
+  int run() {
+    stack<int> s;
+
+    for(int i = 0; i < code.size(); i++) {
+    switch(code[i]) {
+    case ONE:
+      s.push(1);
+        break;
+    case TWO:
+      s.push(2);
+        break;
+    case MULT: {
+      auto right = s.top();
+      s.pop();
+      auto left = s.top();
+      s.pop();
+      s.push(left * right);
+      break;
+    }
+    case PLUS: {
+      auto right = s.top();
+      s.pop();
+      auto left = s.top();
+      s.pop();
+      s.push(left + right);
+      break;
+    }
+    } // switch
+      } // for
+
+      return s.top();
+  } // run
+
+  string exp() {
+    stack<string> s;
+
+      for(int i = 0; i < code.size(); i++) {
+    switch(code[i]) {
+    case ONE:
+      s.push("1");
+        break;
+    case TWO:
+      s.push("2");
+        break;
+    case MULT: {
+      auto right = s.top();
+      s.pop();
+      auto left = s.top();
+      s.pop();
+      s.push("(" + left + " * " + right + ")");
+      break;
+    }
+    case PLUS: {
+      auto right = s.top();
+      s.pop();
+      auto left = s.top();
+      s.pop();
+      s.push("(" + left + " + " + right + ")");
+      break;
+    }
+    } // switch
+      } // for
+
+      return s.top();
+  } // run
+
+  template<typename T>
+  T exec(std::function<T(Code_t)> convert, std::function<T(T,Code_t,T)> apply) {
+    stack<T> s;
+
+      for(int i = 0; i < code.size(); i++) {
+    switch(code[i]) {
+    case ONE:
+      s.push(convert(ONE));
+        break;
+    case TWO:
+      s.push(convert(TWO));
+        break;
+    case MULT: {
+      auto right = s.top();
+      s.pop();
+      auto left = s.top();
+      s.pop();
+      s.push(apply(left,MULT,right));
+      break;
+    }
+    case PLUS: {
+      auto right = s.top();
+      s.pop();
+      auto left = s.top();
+      s.pop();
+      s.push(apply(left,PLUS,right));
+      break;
+    }
+    } // switch
+      } // for
+
+      return s.top();
+  } // run
+
+};
+
+
+// Examples
+
+void testVM() {
+
+  {
+    vector<Code_t> cs{
+      ONE, TWO, TWO, PLUS, MULT
+    };
+
+
+    VM(cs).showAndRun();
+  }
+
+  {
+    vector<Code_t> cs{
+      ONE, TWO, PLUS, TWO, MULT
+    };
+
+
+    VM(cs).showAndRun();
+  }
+
+
+}
+
+
+
+int main() {
+
+  testVM();
+}
+
+
+

C-Funktionen versus C++ closures (function template)

+

Online version

+
#include <iostream>
+#include <functional>
+using namespace std;
+
+// C-style functions versus C++ closures (function template)
+// int f(int)  versus  function<int(int)> f
+
+
+int inc(int x) { return x+1; }
+
+typedef int (*intToint)(int); // A bit of a Hack to define local variables that have a C-style function type.
+
+int main()
+{
+
+    intToint f1;
+
+    function<int(int)> f2;
+
+    // OK
+    // C-style functions can be converted to C++ closures ("function").
+    f1 = inc;
+    f2 = inc;
+
+    // OK
+    int y = 1;
+    f2 = [y](int x) -> int { return x+y; };
+
+    // Not OK
+    // C++ closures cannot be converted C-style functions.
+    //f1 = [y](int x) -> int { return x+y; };
+
+    // OK
+    // Special case.
+    // If the capture list is empty, C++ closures can be converted to C-style functions.
+    f1 = [](int x) -> int { return x+1; };
+
+    function<int(int)> g;
+    g = [](int x) -> int { return x+1; };
+
+    // Not OK
+    // The capture list of g is empty.
+    // But based on g's type, function<int(int)>, the compiler assumes the assigment could be to C++ closure
+    // with a non-empty capture list. Hence, the assignment is rejected.
+    // f1 = g;
+
+    return 0;
+}
+
+ +