Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Die funktionale Programmierung ist ein Stil der Programmierung, der die Verwendung von Funktionen und unveränderlichen Daten betont. Bei der typierten funktionalen Programmierung wird die funktionale Programmierung mit statischen Typen kombiniert, z. B. mit F#. Im Allgemeinen werden die folgenden Konzepte in der funktionalen Programmierung hervorgehoben:
- Funktionen als primäre Konstrukte, die Sie verwenden
- Ausdrücke anstelle von Anweisungen
- Unveränderliche Werte über Variablen
- Deklarative Programmierung über imperative Programmierung
In dieser Reihe werden Sie Konzepte und Muster in der funktionalen Programmierung mit F# erkunden. Dabei erfahren Sie auch Einiges über F#.
Terminologie
Funktionale Programmierung, wie andere Programmierparadigma, enthält ein Vokabular, das Sie schließlich lernen müssen. Im Folgenden finden Sie einige allgemeine Begriffe, die Sie immer sehen:
- Funktion – Eine Funktion ist ein Konstrukt, das eine Ausgabe erzeugt, wenn eine Eingabe angegeben wird. Formeller gesagt, weist sie ein Element aus einem Satz einem anderen Satz zu. Dieser Formalismus wird in vielerlei Hinsicht ins Konkrete übertragen, insbesondere bei der Verwendung von Funktionen, die mit Datensammlungen arbeiten. Es ist das grundlegende (und wichtigste) Konzept der funktionalen Programmierung.
- Ausdruck – Ein Ausdruck ist ein Konstrukt im Code, der einen Wert erzeugt. In F# muss dieser Wert gebunden oder explizit ignoriert werden. Ein Ausdruck kann durch einen Funktionsaufruf trivial ersetzt werden.
- Reinheit - Reinheit ist eine Eigenschaft einer Funktion, sodass ihr Rückgabewert für dieselben Argumente immer gleich ist und dass die Auswertung keine Nebenwirkungen hat. Eine reine Funktion hängt vollständig von ihren Argumenten ab.
- Referentielle Transparenz – Referentielle Transparenz ist eine Eigenschaft von Ausdrücken, sodass sie durch ihre Ausgabe ersetzt werden können, ohne das Verhalten eines Programms zu beeinträchtigen.
- Unveränderlichkeit – Unveränderlichkeit bedeutet, dass ein Wert nicht an Ort und Stelle geändert werden kann. Dies steht im Gegensatz zu Variablen, die sich direkt ändern können.
Beispiele
Die folgenden Beispiele veranschaulichen diese Kernkonzepte.
Funktionen
Das gängigste und grundlegende Konstrukt in der funktionalen Programmierung ist die Funktion. Hier ist eine einfache Funktion, die einer ganzen Zahl 1 hinzufügt:
let addOne x = x + 1
Die Typsignatur lautet wie folgt:
val addOne: x:int -> int
Die Signatur kann als „addOne
nimmt einen int
-Wert mit dem Namen x
entgegen und erzeugt einen int
-Wert“ gelesen werden. Formeller betrachtet, ist addOne
die Zuordnung eines Wert aus dem Satz ganzer Zahlen zum Satz ganzer Zahlen. Das ->
Token gibt diese Zuordnung an. In F# können Sie in der Regel die Funktionssignatur betrachten, um einen Sinn für die Funktionsweise zu erhalten.
Warum ist die Signatur also wichtig? Bei der typierten Funktionalen Programmierung ist die Implementierung einer Funktion oft weniger wichtig als die eigentliche Typsignatur! Die Tatsache, dass zur Laufzeit der Wert 1 zu einer Ganzzahl durch addOne
hinzugefügt wird, ist interessant, aber wenn Sie ein Programm erstellen, ist entscheidend, dass die Funktion ein int
akzeptiert und zurückgibt, da dies festlegt, wie Sie diese Funktion tatsächlich nutzen werden. Wenn Sie diese Funktion richtig verwenden (im Hinblick auf die Typsignatur), können Probleme nur innerhalb des Funktionskörpers der addOne
Funktion diagnostiziert werden. Dies ist der Impuls hinter der typierten funktionalen Programmierung.
Ausdrücke
Ausdrücke sind Konstrukte, die zu einem Wert ausgewertet werden. Im Gegensatz zu Anweisungen, die eine Aktion ausführen, werden Ausdrücke als solche betrachtet, die eine Aktion ausführen und dabei einen Wert liefern. Ausdrücke werden fast immer in der funktionalen Programmierung anstelle von Anweisungen verwendet.
Betrachten Sie die vorherige Funktion, addOne
. Der Text von addOne
ist ein Ausdruck:
// 'x + 1' is an expression!
let addOne x = x + 1
Es ist das Ergebnis dieses Ausdrucks, der den Ergebnistyp der addOne
Funktion definiert. Beispielsweise könnte der Ausdruck, aus dem diese Funktion besteht, in einen anderen Typ geändert werden, z. B. ein string
:
let addOne x = x.ToString() + "1"
Die Signatur der Funktion lautet jetzt:
val addOne: x:'a -> string
Da für jeden Typ in F# ToString()
aufgerufen werden kann, wurde der Typ von x
verallgemeinert (automatische Verallgemeinerung genannt), und der resultierende Typ ist ein string
.
Ausdrücke sind nicht nur Teile von Funktionen. Sie können Ausdrücke haben, die einen Wert erzeugen, den Sie an anderer Stelle verwenden. Eine häufige ist if
:
// Checks if 'x' is odd by using the mod operator
let isOdd x = x % 2 <> 0
let addOneIfOdd input =
let result =
if isOdd input then
input + 1
else
input
result
Der if
Ausdruck erzeugt einen Wert namens result
. Beachten Sie, dass Sie result
vollständig weglassen könnten, wodurch der Ausdruck if
zum Rumpf der Funktion addOneIfOdd
wird. Das Wichtigste, das man über Ausdrücke wissen muss, ist, dass sie einen Wert erzeugen.
Es gibt einen speziellen Typ, der verwendet wird, unit
wenn nichts zurückgegeben werden kann. Betrachten Sie beispielsweise diese einfache Funktion:
let printString (str: string) =
printfn $"String is: {str}"
Die Signatur sieht wie folgt aus:
val printString: str:string -> unit
Der unit
Typ gibt an, dass kein tatsächlicher Wert zurückgegeben wird. Dies ist nützlich, wenn Sie über eine Routine verfügen, die "Arbeit ausführen" muss, obwohl kein Wert für diese Arbeit zurückgegeben werden muss.
Dies steht im scharfen Gegensatz zur imperativen Programmierung, bei der das entsprechende if
-Konstrukt eine Anweisung ist, und die Erstellung von Werten erfolgt häufig durch das Ändern von Variablen. In C# kann der Code z. B. wie folgt geschrieben werden:
bool IsOdd(int x) => x % 2 != 0;
int AddOneIfOdd(int input)
{
var result = input;
if (IsOdd(input))
{
result = input + 1;
}
return result;
}
Es ist erwähnenswert, dass C# und andere Sprachen im C-Stil den ternären Ausdruck unterstützen, der die ausdrucksbasierte bedingte Programmierung ermöglicht.
Bei der funktionalen Programmierung ist es selten, Werte durch Ausdrücke zu ändern. Obwohl einige funktionale Sprachen Anweisungen und Mutationen unterstützen, ist es nicht üblich, diese Konzepte in der funktionalen Programmierung zu verwenden.
Reine Funktionen
Wie bereits erwähnt, sind reine Funktionen Funktionen, die:
- Immer denselben Wert für dieselbe Eingabe auswerten.
- Keine Nebenwirkungen.
Es ist hilfreich, mathematische Funktionen in diesem Kontext zu betrachten. In der Mathematik hängen Funktionen nur von ihren Argumenten ab und haben keine Nebenwirkungen. In der mathematischen Funktion f(x) = x + 1
hängt der Wert von f(x)
nur vom Wert von x
. Reine Funktionen in der funktionalen Programmierung funktionieren auf die gleiche Art und Weise.
Beim Schreiben einer reinen Funktion muss die Funktion nur von ihren Argumenten abhängen und keine Aktion ausführen, die zu einem Nebeneffekt führt.
Hier ist ein Beispiel für eine nicht reine Funktion, da sie vom globalen, änderbaren Zustand abhängt:
let mutable value = 1
let addOneToValue x = x + value
Die addOneToValue
Funktion ist eindeutig unrein, da value
sie jederzeit geändert werden kann, um einen anderen Wert als 1 zu haben. Dieses Muster, das von einem globalen Wert abhängt, ist in der funktionalen Programmierung zu vermeiden.
Nachfolgend sehen Sie ein weiteres Beispiel für eine nicht reine Funktion, da sie einen Nebeneffekt ausführt:
let addOneToValue x =
printfn $"x is %d{x}"
x + 1
Obwohl diese Funktion nicht von einem globalen Wert abhängt, schreibt sie den Wert von x
in die Ausgabe des Programms. Obwohl es nichts inhärent falsch gibt, dies zu tun, bedeutet dies, dass die Funktion nicht rein ist. Wenn ein anderer Teil Ihres Programms von einem externen Teil des Programms abhängt, z. B. vom Ausgabepuffer, kann sich das Aufrufen dieser Funktion auf diesen anderen Teil Ihres Programms auswirken.
Durch Entfernen der printfn
-Anweisung wird die Funktion rein:
let addOneToValue x = x + 1
Obwohl diese Funktion inhärent nicht besser ist als die vorherige Version mit der printfn
Anweisung, stellt sie sicher, dass alles, was diese Funktion tut, ist, einen Wert zurückzugeben. Wenn Sie diese Funktion beliebig oft aufrufen, wird dasselbe Ergebnis erzeugt: Sie erzeugt lediglich einen Wert. Die vorhersagbarkeit, die durch Reinheit gegeben wird, ist etwas, das viele funktionale Programmierer anstreben.
Unveränderlichkeit
Schließlich ist eines der grundlegendsten Konzepte der typierten funktionalen Programmierung unveränderlichkeit. In F# sind alle Werte standardmäßig unveränderlich. Dies bedeutet, dass sie nicht direkt vor Ort verändert werden können, es sei denn, Sie markieren sie ausdrücklich als änderbar.
In der Praxis bedeutet das Arbeiten mit unveränderlichen Werten, dass Sie Ihren Ansatz für die Programmierung von "Ich muss etwas ändern" in "Ich muss einen neuen Wert erzeugen".
Beispielsweise bedeutet das Hinzufügen von 1 zu einem Wert, einen neuen Wert zu erzeugen, ohne den vorhandenen Wert zu verändern.
let value = 1
let secondValue = value + 1
In F# verändert der folgende Code nicht die value
Funktion; stattdessen wird eine Gleichheitsprüfung durchgeführt.
let value = 1
value = value + 1 // Produces a 'bool' value!
Einige funktionale Programmiersprachen unterstützen überhaupt keine Mutation. In F# wird sie unterstützt, ist aber nicht das Standardverhalten für Werte.
Dieses Konzept erstreckt sich noch weiter auf Datenstrukturen. Bei der funktionalen Programmierung weisen unveränderliche Datenstrukturen wie Sets (und viele mehr) eine andere Implementierung auf, als sie ursprünglich erwartet werden. Konzeptionell führt das Hinzufügen eines Elements zu einer Menge nicht zu einer Änderung der Menge, sondern erzeugt eine neue Menge mit dem hinzugefügten Wert. Unter den Deckeln wird dies häufig durch eine andere Datenstruktur erreicht, die eine effiziente Nachverfolgung eines Werts ermöglicht, sodass die entsprechende Darstellung der Daten als Ergebnis gegeben werden kann.
Diese Art der Arbeit mit Werten und Datenstrukturen ist wichtig, da sie sie zwingt, alle Vorgänge zu behandeln, die etwas ändern, als ob eine neue Version dieses Elements erstellt wird. Dies ermöglicht es, dass die Gleichheit und Vergleichbarkeit in Ihren Programmen konsistent ist.
Nächste Schritte
Im nächsten Abschnitt werden die Funktionen gründlich behandelt, um verschiedene Möglichkeiten zu untersuchen, wie Sie sie in der funktionalen Programmierung verwenden können.
Die Verwendung von Funktionen in F# untersucht Funktionen tief und zeigt, wie Sie sie in verschiedenen Kontexten verwenden können.
Weiterführende Lektüre
Die Thinking Functionally-Serie ist eine weitere großartige Ressource, um die funktionale Programmierung mit F# zu erfahren. Sie behandelt grundlagen der funktionalen Programmierung pragmatisch und leicht lesbar, indem sie F#-Features verwendet, um die Konzepte zu veranschaulichen.