Eine Einführung in die Grundprinzipien der funktionalen Programmierung

Nachdem ich lange Zeit mit objektorientierter Programmierung gelernt und gearbeitet hatte, trat ich einen Schritt zurück, um über die Systemkomplexität nachzudenken.

" Complexity is anything that makes software hard to understand or to modify." - John Outerhout

Bei einigen Recherchen fand ich funktionale Programmierkonzepte wie Unveränderlichkeit und reine Funktion. Diese Konzepte sind große Vorteile beim Aufbau von Funktionen ohne Nebenwirkungen, sodass die Wartung von Systemen einfacher ist - mit einigen anderen Vorteilen.

In diesem Beitrag werde ich Ihnen anhand vieler Codebeispiele mehr über die funktionale Programmierung und einige wichtige Konzepte erzählen.

In diesem Artikel wird Clojure als Beispiel für eine Programmiersprache verwendet, um die funktionale Programmierung zu erläutern. Wenn Sie mit einer LISP-Sprache nicht vertraut sind, habe ich denselben Beitrag auch in JavaScript veröffentlicht. Schauen Sie sich die funktionalen Programmierprinzipien in Javascript an

Was ist funktionale Programmierung?

Funktionale Programmierung ist ein Programmierparadigma - ein Stil zum Aufbau der Struktur und der Elemente von Computerprogrammen -, der Berechnungen als Bewertung mathematischer Funktionen behandelt und Zustandsänderungen und veränderbare Daten vermeidet - Wikipedia

Reine Funktionen

Das erste grundlegende Konzept, das wir lernen, wenn wir funktionale Programmierung verstehen wollen, sind reine Funktionen . Aber was bedeutet das wirklich? Was macht eine Funktion rein?

Woher wissen wir also, ob eine Funktion vorhanden ist pureoder nicht? Hier ist eine sehr strenge Definition der Reinheit:

  • Es gibt das gleiche Ergebnis zurück, wenn die gleichen Argumente angegeben werden (es wird auch als bezeichnet deterministic).
  • Es verursacht keine beobachtbaren Nebenwirkungen

Es gibt das gleiche Ergebnis zurück, wenn die gleichen Argumente angegeben werden

Stellen Sie sich vor, wir möchten eine Funktion implementieren, die die Fläche eines Kreises berechnet. Eine unreine Funktion würde radiusals Parameter empfangen und dann berechnen radius * radius * PI. In Clojure steht der Bediener an erster Stelle und radius * radius * PIwird (* radius radius PI):

Warum ist das eine unreine Funktion? Einfach, weil es ein globales Objekt verwendet, das nicht als Parameter an die Funktion übergeben wurde.

Stellen Sie sich nun vor, einige Mathematiker argumentieren, dass der PIWert tatsächlich ist, 42und ändern den Wert des globalen Objekts.

Unsere unreine Funktion ergibt nun 10 * 10 * 42= 4200. Für denselben Parameter ( radius = 10) haben wir ein anderes Ergebnis. Lass es uns reparieren!

TA-DA ?! Jetzt übergeben wir immer den P- I Wert als Parameter an die Funktion. Jetzt greifen wir nur noch auf Parameter zu, die an die Funktion übergeben wurden. Kein External object.

  • Für die Parameter radius = 10& PI = 3.14haben wir immer das gleiche Ergebnis:314.0
  • Für die Parameter radius = 10& PI = 42haben wir immer das gleiche Ergebnis:4200

Dateien lesen

Wenn unsere Funktion externe Dateien liest, ist dies keine reine Funktion - der Inhalt der Datei kann sich ändern.

Zufallszahlengenerierung

Jede Funktion, die auf einem Zufallszahlengenerator basiert, kann nicht rein sein.

Es verursacht keine beobachtbaren Nebenwirkungen

Beispiele für beobachtbare Nebenwirkungen sind das Ändern eines globalen Objekts oder eines als Referenz übergebenen Parameters.

Jetzt wollen wir eine Funktion implementieren, um einen ganzzahligen Wert zu empfangen und den um 1 erhöhten Wert zurückzugeben.

Wir haben den counterWert. Unsere unreine Funktion empfängt diesen Wert und weist den Zähler mit dem um 1 erhöhten Wert neu zu.

Beobachtung : Von der Veränderbarkeit wird bei der funktionalen Programmierung abgeraten.

Wir modifizieren das globale Objekt. Aber wie würden wir es schaffen pure? Geben Sie einfach den um 1 erhöhten Wert zurück. So einfach ist das.

Sehen Sie, dass unsere reine Funktion increase-counter2 zurückgibt, aber der counterWert immer noch der gleiche ist. Die Funktion gibt den inkrementierten Wert zurück, ohne den Wert der Variablen zu ändern.

Wenn wir diese beiden einfachen Regeln befolgen, wird es einfacher, unsere Programme zu verstehen. Jetzt ist jede Funktion isoliert und kann keine Auswirkungen auf andere Teile unseres Systems haben.

Reine Funktionen sind stabil, konsistent und vorhersehbar. Bei gleichen Parametern geben reine Funktionen immer das gleiche Ergebnis zurück. Wir müssen nicht an Situationen denken, in denen derselbe Parameter unterschiedliche Ergebnisse hat - weil dies niemals passieren wird.

Vorteile für reine Funktionen

Der Code ist definitiv einfacher zu testen. Wir müssen uns über nichts lustig machen. So können wir reine Funktionen mit unterschiedlichen Kontexten testen:

  • Bei gegebenem Parameter A→ erwarten Sie, dass die Funktion einen Wert zurückgibtB
  • Bei gegebenem Parameter C→ erwarten Sie, dass die Funktion einen Wert zurückgibtD

Ein einfaches Beispiel wäre eine Funktion zum Empfangen einer Sammlung von Zahlen und zum Erwarten, dass jedes Element dieser Sammlung erhöht wird.

Wir erhalten die numbersSammlung, verwenden sie mapmit der incFunktion, um jede Zahl zu erhöhen, und geben eine neue Liste mit inkrementierten Zahlen zurück.

Für die wäre input[1 2 3 4 5]das zu erwarten .output[2 3 4 5 6]

Unveränderlichkeit

Im Laufe der Zeit unverändert oder nicht änderbar.

Wenn Daten unveränderlich sind, kann sich ihr Status nicht ändernnachdem es erstellt wurde. Wenn Sie ein unveränderliches Objekt ändern möchten, können Sie dies nicht. Stattdessen erstellen Sie ein neues Objekt mit dem neuen Wert.

In Javascript verwenden wir üblicherweise die forSchleife. Diese nächste forAnweisung enthält einige veränderbare Variablen.

Für jede Iteration ändern wir den iund den sumOfValueStatus . Aber wie gehen wir mit Veränderlichkeit in der Iteration um? Rekursion! Zurück zu Clojure!

Hier haben wir also die sumFunktion, die einen Vektor numerischer Werte empfängt. Das recurspringt zurück in die, loopbis wir den Vektor leer bekommen (unsere Rekursion base case). Für jede "Iteration" addieren wir den Wert zum totalAkkumulator.

Bei der Rekursion behalten wir unsere Variablen beiunveränderlich.

Beobachtung : Ja! Wir können reducediese Funktion implementieren. Wir werden dies im Higher Order FunctionsThema sehen.

Es ist auch sehr häufig den letzten aufzubauen Zustand eines Objekts. Stellen Sie sich vor, wir haben eine Zeichenfolge und möchten diese Zeichenfolge in eine Zeichenfolge umwandeln url slug.

In OOP in Ruby würden wir beispielsweise eine Klasse erstellen UrlSlugify. Und diese Klasse wird eine slugify!Methode haben, um die Zeichenfolgeneingabe in eine umzuwandeln url slug.

Wunderschönen! Es ist implementiert! Hier haben wir eine zwingende Programmierung, die genau sagt, was wir in jedem slugifyProzess tun möchten - zuerst Kleinbuchstaben, dann unbrauchbare Leerzeichen entfernen und schließlich verbleibende Leerzeichen durch Bindestriche ersetzen.

Aber wir mutieren den Eingabestatus in diesem Prozess.

Wir können mit dieser Mutation umgehen, indem wir eine Funktionszusammensetzung oder Funktionsverkettung durchführen. Mit anderen Worten, das Ergebnis einer Funktion wird als Eingabe für die nächste Funktion verwendet, ohne die ursprüngliche Eingabezeichenfolge zu ändern.

Hier haben wir:

  • trim: Entfernt Leerzeichen an beiden Enden einer Zeichenfolge
  • lower-case: konvertiert die Zeichenfolge in Kleinbuchstaben
  • replace: Ersetzt alle Übereinstimmungsinstanzen durch Ersetzen in einer bestimmten Zeichenfolge

Wir kombinieren alle drei Funktionen und können "slugify"unseren String.

Apropos Funktionen kombinieren , wir können die compFunktion verwenden, um alle drei Funktionen zusammenzusetzen. Lass uns einen Blick darauf werfen:

Referentielle Transparenz

Lassen Sie uns Folgendes implementieren square function:

Diese (reine) Funktion hat bei gleichem Eingang immer den gleichen Ausgang.

Wenn Sie "2" als Parameter des square functionTestaments übergeben, wird immer 4 zurückgegeben. Jetzt können wir das (square 2)durch 4 ersetzen. Das war's! Unsere Funktion ist referentially transparent.

Wenn eine Funktion konsistent dasselbe Ergebnis für dieselbe Eingabe liefert, ist sie grundsätzlich referenziell transparent.

reine Funktionen + unveränderliche Daten = referenzielle Transparenz

Mit diesem Konzept können wir uns die Funktion auswendig lernen. Stellen Sie sich vor, wir haben diese Funktion:

Das ist (+ 5 8)gleich 13. Diese Funktion führt immer zu 13. Also können wir das tun:

Und dieser Ausdruck wird immer dazu führen 16. Wir können den gesamten Ausdruck durch eine numerische Konstante ersetzen und auswendig lernen.

Funktioniert als erstklassige Entitäten

Die Idee von Funktionen als erstklassige Entitäten ist, dass Funktionen auch als Werte behandelt und als Daten verwendet werden.

In Clojure ist es üblich defn, Funktionen zu definieren, aber dies ist nur syntaktischer Zucker für (def foo (fn ...)). fngibt die Funktion selbst zurück. defnGibt a zurück, vardas auf ein Funktionsobjekt zeigt.

Funktionen als erstklassige Entitäten können:

  • beziehen sich darauf aus Konstanten und Variablen
  • Übergeben Sie es als Parameter an andere Funktionen
  • Geben Sie es als Ergebnis anderer Funktionen zurück

Die Idee ist, Funktionen als Werte zu behandeln und Funktionen wie Daten zu übergeben. Auf diese Weise können wir verschiedene Funktionen kombinieren, um neue Funktionen mit neuem Verhalten zu erstellen.

Stellen Sie sich vor, wir haben eine Funktion, die zwei Werte summiert und dann den Wert verdoppelt. Etwas wie das:

Nun eine Funktion, die Werte subtrahiert und das Doppelte zurückgibt:

Diese Funktionen haben eine ähnliche Logik, aber der Unterschied sind die Operatorfunktionen. Wenn wir Funktionen als Werte behandeln und diese als Argumente übergeben können, können wir eine Funktion erstellen, die die Operatorfunktion empfängt, und sie in unserer Funktion verwenden. Lass es uns bauen!

Erledigt! Jetzt haben wir ein fArgument und verwenden es, um aund zu verarbeiten b. Wir haben die Funktionen +und übergeben -, um mit der Funktion zu komponieren double-operatorund ein neues Verhalten zu erstellen.

Funktionen höherer Ordnung

Wenn wir über Funktionen höherer Ordnung sprechen, meinen wir eine Funktion, die entweder:

  • nimmt eine oder mehrere Funktionen als Argumente oder
  • gibt eine Funktion als Ergebnis zurück

Die double-operatoroben implementierte Funktion ist eine Funktion höherer Ordnung, da sie eine Operatorfunktion als Argument verwendet.

Sie haben wahrscheinlich schon davon gehört filter, mapund reduce. Schauen wir uns diese an.

Filter

Bei einer bestimmten Sammlung möchten wir nach einem Attribut filtern. Die Filterfunktion erwartet, dass ein trueoder ein falseWert bestimmt, ob das Element in die Ergebnissammlung aufgenommen werden soll oder nicht . Wenn der Rückrufausdruck lautet true, wird das Element von der Filterfunktion in die Ergebnissammlung aufgenommen. Andernfalls wird es nicht.

Ein einfaches Beispiel ist, wenn wir eine Sammlung von ganzen Zahlen haben und nur die geraden Zahlen wollen.

Imperativer Ansatz

Ein zwingender Weg, dies mit Javascript zu tun, ist:

  • Erstellen Sie einen leeren Vektor evenNumbers
  • iteriere über den numbersVektor
  • Schieben Sie die geraden Zahlen auf den evenNumbersVektor

Wir können die filterFunktion höherer Ordnung verwenden, um die Funktion zu empfangen even?und eine Liste mit geraden Zahlen zurückzugeben:

Ein interessantes Problem, das ich auf dem Hacker Rank FP Path gelöst habe, war das Filter Array-Problem . Die Problemidee besteht darin, ein bestimmtes Array von Ganzzahlen zu filtern und nur die Werte auszugeben, die kleiner als ein angegebener Wert sind X.

Eine zwingende Javascript-Lösung für dieses Problem ist etwa:

Wir sagen genau, was unsere Funktion tun muss - durchlaufen Sie die Sammlung, vergleichen Sie das aktuelle Element der Sammlung mit xund verschieben Sie dieses Element auf das, resultArraywenn es die Bedingung erfüllt.

Deklarativer Ansatz

Wir wollen jedoch einen deklarativeren Weg, um dieses Problem zu lösen, und auch die filterFunktion höherer Ordnung verwenden.

Eine deklarative Clojure-Lösung wäre ungefähr so:

Diese Syntax scheint in erster Linie etwas seltsam, ist aber leicht zu verstehen.

#(> x%) ist nur eine anonyme Funktion, die es x empfängt und mit jedem Element in der Sammlung vergleicht n. % repräsentiert den Parameter der anonymen Funktion - in diesem Fall das aktuelle Element innerhalb von t he filter.

Wir können dies auch mit Karten tun. Stellen Sie sich vor, wir haben eine Karte von Menschen mit ihren nameund age. Und wir möchten nur Personen über einen bestimmten Alterswert filtern, in diesem Beispiel Personen, die älter als 21 Jahre sind.

Zusammenfassung des Codes:

  • Wir haben eine Liste von Personen (mit nameund age).
  • wir haben die anonyme Funktion #(< 21 (:age %)). Denken Sie daran, dass t he% das aktuelle Element aus der Sammlung darstellt? Nun, das Element der Sammlung ist eine Personenkarte. Wenn wir do (:age {:name "TK" :age 26}), gibt es e,in diesem Fall den Alterswert 26 zurück.
  • Wir filtern alle Personen basierend auf dieser anonymen Funktion.

Karte

Die Idee der Karte ist es, eine Sammlung zu transformieren.

Die mapMethode transformiert eine Sammlung, indem sie eine Funktion auf alle ihre Elemente anwendet und aus den zurückgegebenen Werten eine neue Sammlung erstellt.

Lassen Sie uns die gleiche peopleSammlung oben bekommen. Wir möchten jetzt nicht nach "über dem Alter" filtern. Wir wollen nur eine Liste von Strings, so etwas wie TK is 26 years old. Die letzte Zeichenfolge kann also sein, :name is :age years oldwo :nameund :agesind Attribute von jedem Element in der peopleSammlung.

In einer zwingenden Javascript-Weise wäre es:

In einer deklarativen Clojure-Weise wäre es:

Die ganze Idee ist, eine bestimmte Sammlung in eine neue Sammlung umzuwandeln.

Ein weiteres interessantes Problem mit dem Hacker-Rang war das Problem mit der Aktualisierungsliste . Wir möchten nur die Werte einer bestimmten Sammlung mit ihren absoluten Werten aktualisieren.

Zum Beispiel muss der Eingang [1 2 3 -4 5]der Ausgang sein [1 2 3 4 5]. Der absolute Wert von -4ist 4.

Eine einfache Lösung wäre eine direkte Aktualisierung für jeden Erfassungswert.

Wir verwenden die Math.absFunktion, um den Wert in seinen absoluten Wert umzuwandeln und die direkte Aktualisierung durchzuführen.

Dies ist keine funktionale Möglichkeit, diese Lösung zu implementieren.

Zuerst haben wir etwas über Unveränderlichkeit gelernt. Wir wissen, wie wichtig Unveränderlichkeit ist, um unsere Funktionen konsistenter und vorhersehbarer zu machen. Die Idee ist, eine neue Sammlung mit allen absoluten Werten aufzubauen.

Zweitens, warum nicht maphier verwenden, um alle Daten zu "transformieren"?

Meine erste Idee war, eine to-absoluteFunktion zu erstellen , die nur einen Wert verarbeitet.

Wenn es negativ ist, wollen wir es in einen positiven Wert (den absoluten Wert) umwandeln. Andernfalls müssen wir es nicht transformieren.

Nachdem wir nun wissen, wie man absolutefür einen Wert vorgeht, können wir diese Funktion verwenden, um sie als Argument an die mapFunktion zu übergeben. Erinnern Sie sich, dass a higher order functioneine Funktion als Argument empfangen und verwenden kann? Ja, Karte kann es!

Beeindruckend. So schön! ?

Reduzieren

Die Idee des Reduzierens besteht darin, eine Funktion und eine Sammlung zu erhalten und einen Wert zurückzugeben, der durch Kombinieren der Elemente erstellt wurde.

Ein häufiges Beispiel, über das gesprochen wird, ist der Gesamtbetrag einer Bestellung. Stellen Sie sich vor, Sie wären auf einer Einkaufswebsite. Sie haben hinzugefügt Product 1, Product 2, Product 3, und Product 4in Ihren Warenkorb (Bestellung). Nun wollen wir den Gesamtbetrag des Warenkorbs berechnen.

In zwingender Weise würden wir die Bestellliste durchlaufen und jeden Produktbetrag zum Gesamtbetrag addieren.

Mit reducekönnen wir eine Funktion erstellen, die das behandelt, amount sumund sie als Argument an die reduceFunktion übergeben.

Hier haben wir shopping-cartdie Funktion sum-amount, die den Strom empfängt total-amount, und das current-productObjekt für sumsie.

Die get-total-amountFunktion wird verwendet , um reducedie shopping-cartdurch die Verwendung sum-amountund ab 0.

Eine andere Möglichkeit, den Gesamtbetrag zu erhalten, besteht darin, mapund zu komponieren reduce. Was meine ich damit? Wir können das verwenden, mapum das shopping-cartin eine Sammlung von amountWerten umzuwandeln , und dann einfach die reduceFunktion mit +Funktion verwenden.

Der get-amountempfängt das Produktobjekt und gibt nur den amountWert zurück. Was wir hier haben, ist [10 30 20 60]. Und dann reducekombiniert das alle Elemente durch Addition. Wunderschönen!

Wir haben uns angesehen, wie jede Funktion höherer Ordnung funktioniert. Ich möchte Ihnen ein Beispiel zeigen, wie wir alle drei Funktionen in einem einfachen Beispiel zusammensetzen können.

Apropos shopping cart, stellen wir diese Liste der Produkte in unserem Auftrag haben:

Wir möchten die Gesamtmenge aller Bücher in unserem Warenkorb. So einfach ist das. Der Algorithmus?

  • Filtern nach Buchtyp
  • Verwandeln Sie den Einkaufswagen mithilfe einer Karte in eine Betragssammlung
  • Kombinieren Sie alle Elemente, indem Sie sie mit Reduzieren addieren

Erledigt! ?

Ressourcen

Ich habe einige Ressourcen organisiert, die ich gelesen und studiert habe. Ich teile diejenigen, die ich wirklich interessant fand. Weitere Ressourcen finden Sie in meinem Github-Repository für funktionale Programmierung .

  • Ruby-spezifische Ressourcen
  • Javascript-spezifische Ressourcen
  • Clojure spezifische Ressourcen

Intros

  • FP in JS lernen
  • Einführung in FP mit Python
  • Übersicht über FP
  • Eine kurze Einführung in das funktionale JS
  • Was ist FP?
  • Jargon für funktionale Programmierung

Reine Funktionen

  • Was ist eine reine Funktion?
  • Reine funktionale Programmierung 1
  • Reine funktionale Programmierung 2

Unveränderliche Daten

  • Unveränderlicher DS für die funktionale Programmierung
  • Warum ein gemeinsamer veränderlicher Zustand die Wurzel allen Übels ist
  • Strukturelles Teilen in Clojure: Teil 1
  • Strukturelles Teilen in Clojure: Teil 2
  • Strukturelles Teilen in Clojure: Teil 3
  • Strukturelles Teilen in Clojure: Letzter Teil

Funktionen höherer Ordnung

  • Eloquent JS: Funktionen höherer Ordnung
  • Spaß Spaß Funktion Filter
  • Spaß Spaß Funktion Karte
  • Fun Fun Funktion Basic Reduce
  • Fun Fun Funktion Advanced Reduce
  • Clojure Funktionen höherer Ordnung
  • Reiner Funktionsfilter
  • Rein funktionale Karte
  • Rein funktionale Reduzierung

Deklarative Programmierung

  • Deklarative Programmierung vs Imperativ

Das ist es!

Hey Leute, ich hoffe du hattest Spaß beim Lesen dieses Beitrags und ich hoffe du hast hier viel gelernt! Dies war mein Versuch zu teilen, was ich lerne.

Hier ist das Repository mit allen Codes aus diesem Artikel.

Komm lerne mit mir. Ich teile Ressourcen und meinen Code in diesem Learning Functional Programming-Repository .

Ich hoffe, Sie haben hier etwas Nützliches gesehen. Und bis zum nächsten Mal! :) :)

Mein Twitter & Github. ☺

TK.