Techniken in der Softwareentwicklung: Partial Function Application

Partial Function Application ist eine Technik, bei der eine Funktion einige ihrer Argumente bindet und eine Funktion zurückgibt, die weniger Argumente benötigt.

In Pocket speichern vorlesen Druckansicht 2 Kommentare lesen
Gleisanlagen in Maschen im Gegenlicht

Gleisanlagen in Maschen

(Bild: MediaPortal der Deutschen Bahn)

Lesezeit: 6 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Partial Function Application oder auch Partial Application ist eine Technik in der Softwareentwicklung, die dem Currying ähnelt. Letzteres Verfahren wird vor allem gerne in funktionalen Sprachen verwendet.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Vor ein paar Wochen hatte ich eine Diskussion mit ein einigen meiner Blog-Leser. Ein Leser meinte, dass ich über Partial Function Application schreiben sollte. Ein anderer Leser meinte, dass C++ keine Partial Function Application unterstützt. Das ist falsch. C++ unterstützt Partial Function Application in vielen Varianten. Die Varianten basieren auf std::function, std::bind, std::bind_front, lambdas, auto und currying.

Beginnen möchte ich mit ein wenig Theorie.

Partial Function Application ist einer Technik namens Currying sehr ähnlich. Currying ist ein bekanntes Idiom, das in funktionalen Sprachen wie Haskell gerne verwendet wird. Es steht für eine Technik, bei der eine Funktion, die mehr als ein Argument annimmt, nacheinander in eine Reihe von Funktionen umgewandelt wird, die nur ein Argument annehmen. Deshalb gibt es in einer Programmiersprache wie Haskell nur Funktionen mit einem Argument. Ich höre deine Frage: Wie ist es möglich, eine Funktion wie add zu implementieren, die zwei Argumente braucht? Die Magie geschieht implizit. Eine Funktion, die n Argumente benötigt, wird in Funktionen umgewandelt, die eine Funktion zurückgibt, die nur n -1 Argumente benötigt. Das erste Element wird bei dieser Umwandlung ausgewertet.

Der Name Currying wurde von dem Mathematiker Haskell Curry und Moses Schönfinkel geprägt. Currying ist nach dem Familiennamen von Haskell Curry benannt; Haskell nach seinem Vornamen. Manchmal wird Currying auch Schönfinkeln genannt.

Partial Function Application ist mächtiger als Currying, weil man beliebige Funktionsargumente auswerten kannst. Seit C++11 unterstützt C++ std::function und std::bind.

std::bind ermöglicht es, Callables (aufrufbare Einheiten) auf verschiedene Arten zu erstellen. Callables sind alle Entitäten, die sich wie eine Funktion verhalten. Das sind insbesondere Lambda-Ausdrücke, Funktionsobjekte oder Funktionen selbst.

Man kann

  • Funktionsargumente an beliebige Positionen binden,
  • die Reihenfolge der Funktionsargumente neu ordnen,
  • Platzhalter für Funktionsargumente einführen,
  • Funktionen teilweise auswerten.

Außerdem

  • das neue Callable direkt aufrufen,
  • das Callable in einem Algorithmus der Standard Template Library (STL) verwenden,
  • das Callable in std::function speichern.

Bevor ich ein Beispiel zeige, muss ich noch kurz std::function vorstellen:

  • std::function ist ein polymorpher Funktions-Wrapper. Er kann beliebige Callables annehmen und ihnen einen Namen geben. std::function ist notwendig, wenn der Typ eines Callable angegeben werden muss.

Nun möchte ich ein erstes Beispiel für Partial Function Application vorstellen:

// bindAndFunction.cpp

#include <functional>
#include <iostream>

double divMe(double a, double b){
  return double(a/b);
}

using namespace std::placeholders;                      // (1)

int main(){

  std::cout << '\n';

  // invoking the function object directly
  std::cout << "1/2.0= " << std::bind(divMe, 1, 2.0)() 
    << '\n';                                            // (2)

  // placeholders for both arguments                    // (3)
  std::function<double(double, double)> 
    myDivBindPlaceholder= std::bind(divMe, _1, _2);
  std::cout << "1/2.0= " << myDivBindPlaceholder(1, 2.0) << '\n';

  // placeholders for both arguments, swap the arguments   (4)
  std::function<double(double, double)> 
    myDivBindPlaceholderSwap= std::bind(divMe, _2, _1);
  std::cout << "1/2.0= " << myDivBindPlaceholderSwap(2.0, 1)
    << '\n';

  // placeholder for the first argument                    (5)
  std::function<double(double)> 
    myDivBind1St= std::bind(divMe, _1, 2.0);
  std::cout<< "1/2.0= " << myDivBind1St(1) << '\n';

  // placeholder for the second argument                   (6)
  std::function<double(double)> 
    myDivBind2Nd= std::bind(divMe, 1.0, _1);
  std::cout << "1/2.0= " << myDivBind2Nd(2.0) << '\n';

  std::cout << '\n';

}

Um die einfache Schreibweise _1, _2 für die Platzhalter std::placeholders::_1, std::placeholders::_2 im Quellcode zu verwenden, muss ich den Namensraum std::placeholders in Zeile 1 einführen.

Ich binde in Zeile 2 im Ausdruck std::bind(divMe, 1, 2.0) die Argumente 1 und 2.0 an die Funktion divMe und rufe sie an Ort und Stelle auf. (3, 4, 5 und 6) folgen einer ähnlichen Strategie, aber ich gebe den erstellten Callables mit std::function einen Namen und rufe sie schließlich auf. Eine Template-Signatur wie double(double, double) (4) oder double(double) (5 und 6) steht für die Art der Callables, die std::function akzeptiert. double(double, double) ist ein Callable, das zwei double annimmt und ein double zurückgibt.

Insbesondere die letzten beiden Beispiele (5 und 6), in denen std::function eine Funktion der Arität zwei annimmt und eine Funktion der Arität eins zurückgibt, sind ziemlich erstaunlich. Die Arität einer Funktion ist die Anzahl der Argumente, die sie erhält.std::bind wertet in beiden Aufrufen nur ein Argument aus und verwendet für das nicht ausgewertete einen Platzhalter. Diese Technik wird Partial Function Application genannt.

Zum Abschluss ist hier die Ausgabe des Programms:

In C++11 gibt es eine weitere Möglichkeit, Partial Function Application einzusetzen: Lambda-Ausdrücke.

std::bind und std::function sind in C++11 nahezu überflüssig. Man kann Lambda-Ausdrücke anstelle von std::bind und auto fast immer anstelle von std::function verwenden. Hier ist das entsprechende Programm, das auf auto und Lambda-Ausdrücken basiert.

// lambdaAndAuto.cpp

#include <functional>
#include <iostream>

double divMe(double a, double b){
  return double(a/b);
}

using namespace std::placeholders;

int main(){

  std::cout << '\n';

  // invoking the function object directly
  std::cout << "1/2.0= " << [](int a, int b)
    { return divMe(a, b); }(1, 2.0) << '\n';

  // placeholders for both arguments
  auto myDivBindPlaceholder= [](int a, int b)
    { return divMe(a, b); };
  std::cout << "1/2.0= " << myDivBindPlaceholder(1, 2.0) << '\n';

  // placeholders for both arguments, swap the arguments
  auto myDivBindPlaceholderSwap= [](int a, int b)
    { return divMe(b, a); };
  std::cout << "1/2.0= " << myDivBindPlaceholderSwap(2.0, 1) 
    << '\n';

  // placeholder for the first argument
  auto myDivBind1St= [](int a){ return divMe(a, 2.0); };
  std::cout<< "1/2.0= " << myDivBind1St(1) << '\n';

  // placeholder for the second argument
  auto myDivBind2Nd= [](int b){ return divMe(1, b); };
  std::cout << "1/2.0= " << myDivBind2Nd(2.0) << '\n';

  std::cout << '\n';

}

Der Ausdruck [](int a, int b){ return divMe(a, b); }(1, 2.0) definiert einen Lambda-Ausdruck, der divMe ausführt. Die nachgestellten geschweiften Klammern rufen den Lambda-Ausdruck nur mit den Argumenten 1 und 2.0 auf. Die übrigen Lambda-Ausdrücke werden dagegen in den nachfolgenden Zeilen aufgerufen. Dank eines Lambda-Ausdrucks kann jedes Argument der zugrunde liegenden Funktion gebunden werden.

Bis jetzt habe ich Partial Function Application mit std::bind und Lambda-Ausdrücken angewendet. In C++20 gibt es eine neue Variante von std::bind:

std::bind_front erzeugt ein Callable. std::bind_front kann eine beliebige Anzahl von Argumenten haben und bindet seine Argumente an der Front. Das wirft die Frage auf, warum wir std::bind_front haben, denn seit C++11 gibt es std::bind, das auch am Anfang binden kann. Der Grund ist einfach: Erstens ist std::bind_front einfacher zu benutzen, weil es keine Platzhalter braucht, und zweitens propagiert std::bind_front eine Ausnahmespezifikation des zugrunde liegenden Callables.

Das folgende Programm veranschaulicht, dass sich std::bind_front durch std::bind oder Lambda-Ausdrücke ersetzen lässt.

// bindFront.cpp

#include <functional>
#include <iostream>

int plusFunction(int a, int b) {
    return a + b;
}

auto plusLambda = [](int a, int b) {
    return a + b;
};

int main() {
    
    std::cout << '\n';
    
    auto twoThousandPlus1 = 
      std::bind_front(plusFunction, 2000);            // (1)
    std::cout << "twoThousandPlus1(20): " 
      << twoThousandPlus1(20) << '\n';
    
    auto twoThousandPlus2 = 
      std::bind_front(plusLambda, 2000);              // (2)
    std::cout << "twoThousandPlus2(20): " 
      << twoThousandPlus2(20) << '\n';
    
    auto twoThousandPlus3 = 
      std::bind_front(std::plus<int>(), 2000);        // (3)
    std::cout << "twoThousandPlus3(20): " 
      << twoThousandPlus3(20) << '\n';
    
    std::cout << "\n\n";
    
    using namespace std::placeholders;
    
    auto twoThousandPlus4 = 
      std::bind(plusFunction, 2000, _1);              // (4)
    std::cout << "twoThousandPlus4(20): " 
      << twoThousandPlus4(20) << '\n';
    
    auto twoThousandPlus5 =  [](int b) 
      { return plusLambda(2000, b); };                // (5)
    std::cout << "twoThousandPlus5(20): " 
      << twoThousandPlus5(20) << '\n';
       
    std::cout << '\n';
    
}

Jeder Aufruf (1 - 5) erhält eine Callable mit zwei Argumenten und gibt eine Callable mit nur einem Argument zurück, weil das erste Argument an 2000 gebunden ist. Die Callable ist eine Funktion (1), ein Lambda-Ausdruck (2) und ein vordefiniertes Funktionsobjekt (3). _1 steht für das fehlende Argument. Mit dem Lambda-Ausdruck (5) lässt sich ein Argument direkt anwenden und ein Argument b für den fehlenden Parameter angeben. Aus Lesbarkeits-Sicht ist std::bind_front deutlich angenehmer als std::bind oder der Lambda-Ausdruck.

Argument-Dependent Lookup (ADL), auch bekannt als Koening Lookup, steht für eine Reihe von "magischen" Regeln für das Auflösen von unqualifizierten Funktionen auf der Grundlage ihrer Funktionsargumente. (rme)