First-Class Funktionen

Inhaltsverzeichnis[Anzeigen]

Funktionale Programmiersprachen zeichnen sich durch First-Class Funktionen aus. First-Class Funktionen verhalten sich wie Daten und werden gerne in C++ in der Standard Template Library eingesetzt.

 

First-Class Funktionen

FirstClassFunctions

First-Class Funktionen verhalten sich wie Daten. Was heißt das eigentlich?

First-Class Funktionen können

  • als Argument einer Funktion verwendet werden.

    std::accumulate(vec.begin(), vec.end(), []{ int a, int b}{ return a+b; })

  • von einer Funktion zurückgegeben werden.
       std::function<int(int, int)> makeAdd(){
         return [](int a, int b){ return a + b; };
       }
       std::function<int(int, int)> myAdd= makeAdd();
       myAdd(2000, 11);  // 2011
  • einer Funktion zugewiesen oder in einer Variable gespeichert werden.

Noch ein paar Worte zu dem zweiten Punkt. Die Funktion makeAdd gibt die Lambda-Funktion [](int a, int b){ return a + b; } zurück. Diese benötigt zwei int-Argumente und liefert einen int-Wert: std::function<int(int,int)>. Der Rückgabetyp der Funktion makeAdd lässt sich an den generischen Funktions-Wrapper myAdd binden und ausführen.

Welche Mächtigkeit in First-Class Funktionen in C++ steckt, zeigt sehr eindrucksvoll das dispatch table in dem gleichnamigen Artikel. 

Funktionszeiger sind die First-Class Funktionen des einfachen Mannes. Funktionszeiger kennt bereits C. Hier geht C++ deutlich weiter. Insbesondere an der Evolution des Funktionskonzepts lässt sich sehr schön die Evolution von C++ festmachen.

Die Evolution des Funktionskonzepts

Vereinfachend gesprochen besteht die Evolution aus 4 Stufen.

FunctionEvolution

C++ kennt Funktionen. C++ erweiterte seine Portfolio um Funktionsobjekte. Dies sind Objekte, die sich Funktionen verhalten, da ihr Klammeroperator überladen ist. Seit C++11 kennt C++ Lambda-Funktionen, mit C++14 sogar generische Lambda-Funktionen. Durch jede dieser Evolutionsstufen werden Funktionen in C++ mächtiger und komfortabler.

  • Funktionen => Funktionsobjekte: Funktionsobjekte können im Gegensatz zu Funktionen Zustand besitzen. Das erlaubt ihnen, ein Gedächtnis aufbauen oder auch ihr Verhalten weitergehend anzupassen.
  • Funktionsobjekte => Lambda-Funktionen: Lambda-Funktionen werden in der Regel am Ort ihres Einsatzes implementiert. Das erhört die Lesbarkeit des Codes, reduziert den notwendigen Schreibaufwand auf ein Minimum um gibt dem Optimierer maximale Einsicht in den zu erzeugenden Code.
  • Lambda-Funktionen => Generische Lambda-Funktionen: Generische Lambda-Funktionen sind Funktions-Templates sehr ähnlich, lassen sich aber deutlich einfacher implementieren. Sie können sehr universell eingesetzt werden, da sie einerseits als Lambda-Funktionen Zustand (Closure) besitzen können, da sie andererseits über ihren Typ parametrisiert sind.

Zugegeben, das ganze war jetzt schon recht theorielastig. Daher kommt jetzt ein Beispiel, dass die vier Stufen des Funktionskonzepts nochmals genauer vorstellt.

 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// evolutionOfFunctions.cpp

#include <iostream>
#include <numeric>
#include <string>
#include <vector>

std::string addMe(std::string fir, std::string sec){
  return fir + " " + sec;
};

struct AddMe{
  
  AddMe()= default;
  AddMe(std::string gl): glue(gl) {}
  
  std::string operator()(std::string fir, std::string sec) const {
    return fir + glue + sec;
  }
  
  std::string glue= " ";
  
};

int main(){
  
  std::vector<std::string> myStrings={"The", "evolution", "of", "the", "function", "concept", "in", "C++."};
  
  std::string fromFunc= std::accumulate(myStrings.begin(), myStrings.end(), std::string{}, addMe);
  
  std::cout << fromFunc << std::endl;
  
  std::cout << std::endl;
  
  std::string fromFuncObj= std::accumulate(myStrings.begin(), myStrings.end(), std::string{}, AddMe());
  
  std::cout << fromFuncObj << std::endl;
  
  std::string fromFuncObj2= std::accumulate(myStrings.begin(), myStrings.end(), std::string{}, AddMe(":"));
  
  std::cout << fromFuncObj2 << std::endl;
  
  std::cout << std::endl;
  
  std::string fromLambdaFunc= std::accumulate(myStrings.begin(), myStrings.end(), std::string{}, [](std::string fir, std::string sec){ return fir + " " + sec; });
  
  std::cout << fromLambdaFunc << std::endl;
  
  std::string glue=":";
  
  auto lambdaFunc= [glue](std::string fir, std::string sec){ return fir + glue + sec; };
  
  std::string fromLambdaFunc2=  std::accumulate(myStrings.begin(), myStrings.end(), std::string{}, lambdaFunc);
  
  std::cout << fromLambdaFunc2 << std::endl;
  
  std::cout << std::endl;
  
  std::string fromLambdaFuncGeneric= std::accumulate(myStrings.begin(), myStrings.end(), std::string{}, [glue](auto fir, auto sec){ return fir + glue + sec; });
  
  std::cout << fromLambdaFuncGeneric << std::endl;
  
  auto lambdaFuncGeneric= [](auto fir, auto sec){ return fir + sec; };
  
  auto fromLambdaFuncGeneric1= std::accumulate(myStrings.begin(), myStrings.end(), std::string{}, lambdaFuncGeneric);
  
  std::cout << fromLambdaFuncGeneric1 << std::endl;
  
  std::vector<int> myInts={1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  
  auto fromLambdaFuncGeneric2= std::accumulate(myInts.begin(), myInts.end(), int{}, lambdaFuncGeneric);
  
  std::cout << fromLambdaFuncGeneric2 << std::endl;
  
}

 

Los geht es in dem Programm mit der Funktion addMe (Zeile 8 - 10), dem Funktionsobjekt AddMe (Zeile 12 - 23), den Lambda-Funktionen (Zeile 45 und 51) und den generischen Lambda-Funktionen in Zeile 59,63 und 64. Ziel des Programms ist es, die Strings des Vektors myStrings in der Zeile 27 mit Hilfe des Algorithmus std::accumulate auf verschieden Art zusammenzuaddieren. Da trifft es sich gut, dass std::accumulate als Funktion höherer Ordnung (dazu mehr im nächsten Artikel) First-Class Funktionen annehmen kann. Als First-Class Funktionen verwende ich Funktionszeiger, Funktionsobjekte und (generische) Lambda-Funktionen. 

In Zeile 29 kommt der Funktionszeiger addMe zum Einsatz. Das geht mit dem Funktionsobjekt AddMe() bzw AddMe(":") (Zeile 35 und 39) schon deutlich komfortabler, da die Funktionsobjekte das parametrisieren des Bindeglieds zwischen den Strings erlauben. Die gleich Komfortabilität bieten auch Lambda-Funktionen (Zeile 45 und 51). Insbesondere die Lambda-Funktion lambdaFunc verwendet eine Kopie der Variablen glue (Zeile 49). Damit ist sie genau genommen ein Closure. Besonders interessant ist die generische Lambda-Funktion [glue](auto fir, auto sec){ return fir + glue + sec; }) in Zeile 59. Sie verwendet als Closure ebenfalls die Variable glue. Die generische Lambda-Funktion lambdaFuncGeneric kann sowohl für std::vector<std::string>, als auch für std::vector<int> (Zeile 69) angewandt werden. Im Falle des std::vector<int> ist das Ergebnis 55.  

Zum Abschluss die Ausgabe des Programms.

 evolutionOfFunctions

Es wäre auch einfacher gegangen

Aus didaktischen Gründen habe ich std::accumulate angewandt, um Paare von Elementen zu addieren. Das wäre auch bei der generischen Lambda-Funktion lambdaFunctionGeneric (Zeile 63) deutlich einfacher gegangen, den std::accumulate besitzt eine einfachere Version, die keine aufrufbare Einheit benötigt. Diese einfachere Version addiert ihr Elemente, beginnend mit Startwert, zusammen.  Damit lässt sich zum Beispiel Zeile 65 deutlich einfacher schreiben: auto fromLambdaFuncGeneric1= std::accumulate(myStrings.begin(), myStrings.end(), std::string{}). Die gleiche Aussage trifft auch auf fromLambdaFuncGeneric2 zu.

Wie geht's weiter?

Ich habe es ja bereits im Text angedeutet. Das klassische Gegenstück zu First-Class Funktionen sind Funktionen höherer Ordnung, den diese nehmen typischerweise First-Class Funktionen an. Im nächsten Artikel dreht sich daher alles zum Funktionen höherer Ordnung, einem weiterem Charakteristikum funktionaler Programmierung.

 

 

 

 

 

 

 

 

title page smalltitle page small Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library".   Hole dir dein E-Book. Unterstütze meinen Blog.

 

Kommentar schreiben


Modernes C++

Abonniere den Newsletter

Inklusive zwei Artikel meines Buchs
Introduction und Multithreading

Beiträge-Archiv

Sourcecode

Neuste Kommentare