Funktionen

Funktionen sind ein Hauptmechanismus, um Programme zu modularisieren. Sie werden durch eine Folge von einer oder mehr Instruktionen definiert. Die Instruktionen werden nacheinander ausgeführt, wenn die Funktion aufgerufen wird. Anstelle der Instruktionsabfolge kann der eine Aufrufbefehl treten. Dadurch lassen sich Wiederholungen vermeiden, der Code wird übersichtlicher und sprechende Funktionsnamen kommunizieren, was durch sie passiert.

Funktionen sind durch die Instruktionsfolge eindeutig definiert. Dennoch lässt sich der tatsächliche Programmablauf dadurch variieren, dass beim Aufruf Werte für vordefinierte Parameter festgelegt werden. Dazu werden Werte als sogenannte Argumente übergeben.1 Argumente fungieren als tatsächliche Parameter (actual parameters), die formalen Parametern (formal parameters) entsprechen, die bei der Funktionsdefinition spezifiziert werden.2

Man kann zwischen zwei Arten von Argumenten unterscheiden. Im einen Fall legen die Argumente das allgemeine Verhalten der Funktion fest. Beispielsweise könnte man einstellen, ob die Funktion eine json- oder eine toml-Datei erstellen soll. Im anderen Fall werden die Daten bereitgestellt, auf denen die Funktion operieren soll. Das passiert entweder dadurch, dass der übergebene Wert kopiert wird (Skalarwerte); oder dass auf Datenstrukturen verwiesen wird. Die Rede von “Parametern” passt vielleicht besser auf den ersten Fall. Wenn ich mich nicht irre, dann werden Argumente nur im zweiten Fall als Referenzen übergeben. Der Funktion wird durch das Argument gezeigt, mit welchen Daten sie arbeiten soll.

Mir scheint als gibt es auch bei der Verwendung vom Ausdruck “Argument” feine Untrschiede. Werte werden in Form von Variablen, Literalen oder komplexeren Ausdrücken übergeben. Doch es sind nicht Ausdrücke die Argumente, sondern die Werte, zu denen sie evaluieren. Darüber hinaus werden entweder Skalarwerte oder Referenzen übergeben. Im ersten Fall sind die Skalarwerte das Argument. Im letzteren Fall stellt sich aber die Frage, ob die Referenz oder ihr Referent (der referenzierte Wert) als Argument betrachtet wird. Da es der referenzierte Wert ist, aus dem sich die Arbeitsweise der Funktion ableitet, sollte wohl dieser auch ihr Argument genannt werden.

Diese Verwendung entspricht auch der verbreiteten Verwendungsweise vom Wort “Argument” im Kontext der Unterscheidung zwischen Call by Reference und Call by Value. Ein Beispiel: “The C language professes to pass arguments exclusively by value, yet many a programmer has experienced (intentionally or unintentionally) a function call that somehow modified the array passed to it, as if it were passed by reference.”3 Die Unterscheidung zwischen zwei Weisen, das Argument zu übergeben, ist hier syntaktisch definiert. Die Funktion verhält sich wie ein Call by Reference, da tatsächlich eine Referenz auf das Array übergeben wird. Es muss keine Zeiger-Variable erstellt werden, da der Variablenwert im Falle von Arrays bereits eine Referenz ist. Deshalb muss in diesem Fall nicht der &-Operator vorangestellt werden. Das “Argument” im gegebenen Zitat ist das Array, nicht die Referenz auf das Array.

Funktionen sind nur für Argumente bestimmter Typen definiert. Bei vielen Programmiersprachen werden diese semantischen Anforderungen in der Funktionsdefinition über die formalen Parameter expliziert. Einige Sprachen (darunter C++, Java, Haskell und Rust) erlauben es darüber hinaus, Typanforderungen in generischer Form anzugeben. Durch sogenannte generische Typparameter (generic type parameters) wird es möglich, dass der Funktion für einen formalen Parameter Argumente verwandter Typen übergeben werden können. Dadurch werden Wiederholungen weiter vermieden und die zugrundeliegende Logik wird verdeutlicht. Ein Beispiel ist eine Funktion, die das größte Element einer Liste zurückgibt.4 Statt eigene Funktionen für alle Typen zu definieren, die sich auf natürliche Weise anordnen lassen, kann eine Funktionsdefinition für verschiedene Fälle geschrieben und genutzt werden. Generische Programmierung erzeugt einen sogenannten Polymorphismus.5

Andere Programmiermechanismen haben einen ähnlichen Effekt.6 Aus praktischen Gründen erlauben Programmiersprachen Mehrdeutigkeit (ambiguity) von Operatoren und Bezeichnern (identifiers). In vielen Fällen liegen die Vorteile dieser Praxis klar auf der Hand. So wäre es absurd für die Addition zwischen verschiedenen Zahlentypen ein eigenes Symbol einzuführen. Man kann aber auch Funktionsnamen überladen (overloading). Dann wird ein Name genutzt, um verschiedene Funktionen aufzurufen. Wenn sich die verschiedenen Funktionen nur in den Typen der Argumente unterscheiden, dann haben wir genau die Situation, die durch die Abstraktion von generischer Programmierung vermieden werden sollte.

Einige Autoren sprechen auch bei Overloading von einem Polymorphismus.7 Aus der Verwendung von generischen Typparametern folgt parametrischer Polymorphismus (parametric polymorphism), aus der Verwendung von Overloading folgt Ad-hoc-Polymorphismus (ad hoc polymorphism). Die negative Konnotation vom letztgenannten Ausdruck hebt die fehlende Generalisierung hervor.8

Durch Funktionen wird von Details der konkreten Implementierung abstrahiert.9 Die Anwenderin der Funktion muss verstehen, was sie macht; sie muss aber nicht verstehen, wie genau die Funktion ihre Aufgabe erledigt. Damit eine Funktion aber wirklich das macht, was erwartet wird, müssen die notwendigen Informationen bereitgesetellt werden. Dazu kann die Signatur der Funktion konsultiert werden. Formale Parameter sind ein Bestandteil der Signatur: Sie sagen der Anwenderin, in welcher Reihenfolge (positionale Parameter) oder mit welchem Namen (Schlüsselwortparameter) Argumente übergeben werden und von welchem Typ sie sein müssen. Dadurch wird die Schnittstelle (interface) der Funktion nach außen offengelegt.

Der dynamische Funktionsbegriff der Informatik, nach dem Funktionen Folgen von Anweisungen sind, die zusammen aufgerufen und parametrisiert werden können, hat seinen Ursprung in der reinen Mathematik. In der mengentheoretischen Definition gemäß sind Funktionen Relationen, bei denen jedes Element des sogenannten Definitionsbereiches (genau) einem Element des Wertebereiches zugeordnet wird. Als Menge aufgefasst enthält beispielsweise die zweite Potenzfunktion die Paare: {<1, 1>, <2, 4>, <3, 9>, <4, 16>,...}. Die Zuweisung folgt einer Regel, die sich im gegebenen Beispiel in der Formel f(x) = x * x explizieren lässt.

Für die mathematische Auffassung von Funktionen ist der Umstand wesentlich, dass Kombinationen von Argumenten auf einen (und nur einen) Wert abgebildet werden. Auch in der Programmierung kann die Ausführung einer Funktion in einem Rückgabewert (return value) münden. Das funktionale Paradigma der Programmierung zeichnet sich dadurch aus, dass Funktionen primär oder sogar einzig für ihren Rückgabewert ausgeführt werden. In anderen Programmierstilen sind auch oder insbesondere Nebeneffekte – die Modifikation von Werten und Strukturen – von Interesse. Außerdem soll – erneut wie in der Mathematik – (weitgehend) gewährleistet sein, dass gleiche Inputs zum gleichen Resultat führen. Die Umgebung, in der eine Funktion aufgerufen wird, soll keinen Einfluss auf ihren Rückgabewert haben.

Eine terminologische Unterscheidung trägt diesen verschiedenen Ansätzen Rechnung.10 Was bisher “Funktion” genannt wurde, kann allgemeiner Subroutine genannt werden. Eine Subroutine ohne Rückgabewert ist eine Prozedur. Nur eine Subroutine, die wie in der Mathematik einen Rückgabewert hat, wird Funktion genannt. Ob nur Subroutinen ohne Nebeneffekte “Funktion” genannt werden sollten, hängt sicherlich vom Kontext ab. Eine funktionale Programmiersprache wird in dieser Frage strenger sein.

Der Aufruf (call/invocation) einer Subroutine wird in vielen Programmiersprachen als Ausdruck aufgefasst.11 Diese Implementierung liegt insbesondere bei Funktionen nahe, da sie wie Ausdrücke zu Werten evaluieren. Wenn Aufrufe einer Subroutine alleine stehen, fungieren sie als Anweisungen. Ich vermute, dass es in dieser Frage zwischen verschidenen Sprachen Detailunterschiede gibt.12

Die Verwendung von Prozeduren als Ausdrücke kann eine Fehlerquelle sein. Das Textbook-Beispiel hierzu ist die Verwendung von Short-Circuit-Operatoren. Dabei handelt es sich um Versionen der Booleschen Operatoren, bei denen der zweite Operand nur dann evaluiert wird, wenn der Wahrheitswert des Gesamtausdrucks durch den Wert des ersten Operand nicht bereits feststeht. Eine Konjunktion (UND) ist wahr, wenn beide Operanden wahr sind; ist der erste bereits falsch, braucht der zweite nicht mehr berücksichtigt werden. Eine Disjunktion (ODER) ist wahr, wenn ein Operand wahr ist; ist der erste bereits wahr, braucht der zweite nicht mehr evaluiert werden. Enthält der zweite Operand eine Prozedur, dann würden ihre Nebeneffekte bei einem Kurzschluss nicht mehr berechnet werden.

Während eines Aufrufs kann eine Subroutine mehrfach aktiviert werden.13 Dies ist dann der Fall, wenn sich eine Subroutine während ihrer Ausführung selbst aufruft. Bei diesem Vorgang spricht man von Rekursion.

Zusammenfassung

Literatur

Franěk, Frantisek. 2004. Memory as a Programming Concept in C and C++. Cambridge: Cambridge University Press. Scott, Michael Lee. 2016. Programming Language Pragmatics. 4. Aufl. Waltham, MA: Morgan Kaufmann. Strachey, Christopher. 2000. „Fundamental Concepts in Programming Languages“. Higher-Order and Symbolic Computation 13 (1/2): 11–49. https://doi.org/10.1023/A:1010000313106.

Fußnoten

  1. Bei Argumenten denkt man an etwas, das in Diskussionen oder ähnlichen Auseinandersetzungen vorgebracht wird, um eine Äußerung oder Annahme zu untermauern. Natürlich hat die Verwendung dieses Wortes in der Informatik historische Gründe. Wie das Wort in der Alltagssprache so hat auch das Wort in der Informatik seinen Ursprung in der Rede über logische Zusammenhänge. In einem Thread auf StackExchange werden einige plausible Spekulationen und interessante Quellen angeführt. 

  2. Die Ausdrücke actual parameter und formal parameter gehören zum Standardvokabular bei der Rede über Funktionen. Vgl. beispielsweise Scott 2016, 411. 

  3. Franěk 2004, 84. 

  4. https://doc.rust-lang.org/book/ch10-01-syntax.html. 

  5. Generische Typenparameter werden nicht nur bei Funktionen und Methoden, sondern insbesondere auch bei Klassen und Structures verwendet. 

  6. Vgl. Scott 2016, 322. 

  7. Die erste ausdrückliche Unterscheidung dieser zwei Formen ist vielleicht Strachey 2000, 36-37. 

  8. Subtyppolymorphismus (subtype polymorphism) ist eine weitere Form von Polymorphismus, die bei Klassen auftreten kann. In Sprachen mit Vererbung können Objekte einer Unterklasse in Kontexten verwendet werden, die für die Oberklasse definiert sind. 

  9. Vgl. Scott 2016, 411. 

  10. Vgl. bspw. Franěk, 59: “Let us observe that computer scientists prefer to speak of procedures (if no value is returned) and functions (if a value is returned and the execution has no “side effects”, meaning that it does not modify any data not local to the function).” Die Anforderung, dass keine Nebeneffekte bestehen, ist vielleicht unnötig streng. 

  11. Für C siehe https://learn.microsoft.com/en-us/cpp/c-language/function-call-c?view=msvc-170. Für Python siehe https://docs.python.org/2/reference/expressions.html#calls. 

  12. Leider weiß ich nichts Näheres über die genaue Implementierung in verschiedenen Sprachen. In Sprachen, in denen Anweisungen durch das Semikolon abgeschlossen werden, ist die syntaktische Form von f(x); vielleicht als Anweisung definiert, während eine Verwendung ohne Semikolon einen Ausdruck auszeichnet. 

  13. Vgl. Franěk 2004, 77.