Blogbeitrag:

Einfach, sicher, produktiv: Funktionale Programmierung

Einfach, sicher, produktiv: Funktionale Programmierung

So lesbar könnte und sollte sicherer Code aussehen:

kunden
|> filter(vipKunde?)
|> sort(jahresUmsatz)

Wenn Ihr Code so klar und ausdrucksstark ist, haben Sie diesen Beitrag womöglich nicht mehr nötig. Alle anderen sollten sich von seiner Länge nicht abschrecken lassen, denn: Ich möchte Ihnen anhand konkreter Codebeispiele die Grundprinzipien und immensen Potenziale der sogenannten funktionalen Programmierung (FP) vermitteln. Am Ende werden Sie nicht nur glauben, dass die oben gezeigte Codestrecke funktioniert, sondern auch schon ungefähr verstehen, wie und warum.
Bitte nehmen Sie sich die Zeit zur Lektüre also in Ihrem eigenen Interesse! Der Einsatz von FP kann Ihre Entwicklung auf ein völlig neues Niveau von Qualität und Produktivität heben. Das sollte Entwickler genauso interessieren wie Manager.
Ich werde am Anfang zunächst kurz auf die Motivation und den theoretischen Hintergrund von FP eingehen. Das soll dem Verständnis dienen, ist aber für die weiter unten folgenden Erläuterungen anhand konkreter Codebeispiele nicht unbedingt nötig. Sie können auch direkt zu „Unsere ersten Funktionen“ springen und ggf. bei Fragen auf die Einleitung zurück kommen.

Scheitern scheint vorprogrammiert zu sein

Programmierer stehen in einem eher schlechten Ruf. Fast jeder kennt Symptome wie diese:

  • Hoher Anteil von Debugging und/oder Defekten
  • Regression der Software (d.h. Defekte durch Wartung)
  • Entwickler verstehen nach kurzer Zeit den eigenen Code nicht mehr
  • Entwickler schreiben lieber neuen Code, statt existierenden zu verwenden
  • Abnehmende Wartbarkeit bis hin zum Wartungskollaps
  • „Isolierte Programmsituationen“ (Euphemismus für unerklärliche Fehler)
  • Produktivität und Qualität suboptimal, Ziele werden gerissen

Das alles weist auf handwerkliche Schwächen hin. Wer (ob als Manager oder Entwickler) diese Symptome ernst nimmt, muss daran interessiert sein, die Entwicklung einfacher, sicherer und produktiver zu machen. Man kann und sollte mit sinnvollen Prozessen (Agile, TDD, BDD, u.ä) gute Rahmenbedingungen schaffen. Aber für das Vermeiden handwerklicher Fehler gibt es m.E. keinen größeren Schritt als die Einführung des Paradigma „Funktionale Programmierung“.
Viele halten funktionale Programmierung irrtümlich für komplizierter als „normale“ Programmierung. Eigentlich ist eher das Gegenteil richtig: Guter FP-Code besticht durch seine Einfachheit und Klarheit. Davon können Sie sich in diesem Beitrag überzeugen. Richtig verstanden, kann FP den Softwareentwickler von unnötigem mentalen Ballast befreien und damit erheblich die Produktivität und Qualität erhöhen.
Auch vor der Lektüre des Folgenden sollten Sie intuitiv begreifen können, was die eingangs gezeigte Codestrecke errechnet. Und das ist der Punkt: Jeder, dessen Code deutlich komplizierter und/oder unstabiler ist, sollte sich fragen: Muss das im Jahr 2017 noch sein? Und: Warum ist das überhaupt so? Mit dieser Frage möchte ich beginnen.

Problemverursacher: „Wie“ statt „Was“

Die eingangs genannten (und weitere) Probleme sind typische Symptome der sogenannten „imperativen Programmierung“ (zu dieser Familie gehört übrigens auch das in der Industie am meisten verbreitete Paradigma der objekt-orientierten Programmierung!). Diese Art des Programmierens ist seit Jahrzehnten so allgegenwärtig, dass sie zu selten in Frage gestellt wird, obwohl es bessere Alternativen gäbe (neben FP gibt es z.B. auch logische Programmierung, die für manche Aufgabenstellungen eine interessante Alternative ist).
Die meisten Menschen (Profis wie Amateure) haben von Programmierung folgende Vorstellung: Man beschreibt Programmabläufe(Algorithmen), die schrittweise Daten einlesen, verarbeiten/verändern und ausgeben. Genau dieses schrittweise Vorschreiben (Programm: lat. das Vor-Geschriebene)  nennt man imperativ (lat. „befehlend“).
Im Kleinen funktioniert das auch noch einigermaßen gut, gerade für Anfänger, weil es an Kochrezepte o.ä. erinnert. Aber das Problem ist: Mit zunehmender Programmgröße wird das ganze unübersichtlich, es kommt immer stärker zu den gefürchteten Nebeneffekten. Selbst die einfachsten Dinge wie die Berechnung eines Auftragswerts aus der Summe seiner Positionen erfordern im Verhältnis zum fachlichen Problem zu viel Aufmerksamkeit. Hochgerechnet auf komplexe Anwendungen führt das zu einer mentalen Überlastung der Programmierer. Die Folge: Kleinste Konzentrationsschwächen oder Störungen führen zur Einführung von Fehlern, deren Folgen oft weit über die aktuell bearbeitete Programmstelle hinausgehen.
Diese ohnehin schon problematische Situation ist in den letzten Jahrzehnten noch verschärft worden: Softwaresyteme werden nicht nur immer komplexer, sondern arbeiten zunehmend nebenläufig und verteilt (man denke nur an Multi-Core-Prozessoren und die asynchrone, verteilte Natur des Internets). Durch die Grundschwäche der imperativen Programmierung, ständig Datenzustände zu verändern, kommt es dabei zu den gefürchteten „race conditions“, bei denen Algorithmen sich gegenseitig in die Quere kommen und inkonsistente Zustände erzeugen. Mit steigender Last tauchen plötzlich vermeintlich unerklärliche Fehler auf – auch bekabnnt als „Heisen-Bugs“ (in Anspielung auf Werner Heisenberg, Erfinder der „Unschärferelation“ der Quantentheorie und einer der theoretischen Väter der Atombombe). Die Programme fangen an, sich nondeterministisch (nicht vorhersagbar) zu verhalten – und das ist ja gerade nicht das Ziel imperativer Programmierung.

Back to Church: Transformieren statt Reformieren

Wenn es um die Grundlagen moderner Programmierung geht, fällt meist der Name Alan Turing. Weniger bekannt ist sein Zeitgenosse Alonzo Church. Dieser entwickelte 1935 das sogenannte Lambda(λ)-Kalkül und legte damit nicht nur den Grundstein für Compilerbau, sondern auch für das, was wir heute funktionale Programmierung nennen.
Schon wenn man die theoretische Modelle dieser beiden vergleicht, ahnt man, welches eher zu Problemen führt: Turing arbeitete u.a. mit der nach ihm benannten Maschine, einem Mechanismus, der theoretisch unendliche Bänder vor und zurückspulen und die auf ihm befindlichen Informationen beliebig immer wieder ändern kann. Selbst die einfachsten Lösungen werden auf dieser hypothetischen Infrastruktur schnell extrem unübersichtlich, der kleinste Fehler verursacht das Scheitern des Programms. Zwar ist die Turing-Maschine nur ein theoretisches Modell zum Beweis des sogenannten Entscheidungsproblems, aber ihre Funktionsweise ist eben auch ein Menetekel für das Scheitern der imperativen Programmierung an Komplexität.
Church hingegen hat sich überlegt, was die einfachste und universellste Weise wäre, Funktionalität so darzustellen, dass man sie einer Maschine beibringen kann. Statt auf mechanische Maschinen a la Turing hat er sich an den Grundbausteinen der Mathematik orientiert: Funktionen. Deren Grundprinzip ist es, erhaltene Parameter in Rückgabewerte zu transformieren, ohne jemals die Parameter selbst zu ändern. Diese Einschränkung hat Church zu einem Modell geführt, das so mächtig wie Turings ist, aber viel einfacher und ungleich sicherer (im Sinne von robuster und korrekter).

Ein wenig Theorie, aber keine Angst vor griechischen Buchstaben!

Auch wenn Mathematik nicht Ihr Ding ist, können Sie sorglos weiterlesen: Denn das Lambda-Kalkül von Church besticht durch seine Einfachheit. Es kann nur Funktionen beschreiben, die einen einzigen Parameter erhalten und einen Ausdruck zu dessen Umwandlung besitzen!
Schauen wir, wie die Quadratfunktion bei Church aussieht:

f(x) -> x*x    // mathematische Notation
λx.x*x         // Notation im Lambda-Kalkül

Ist doch einfach, oder? λ definiert eine Funktion, x ist ihr Parameter, der Punkt trennt Parameter und Transformationsausdruck. Es ist einfach nur eine andere Notation für Funktionen.
Die softwaretechnische Umsetzung ist auch einfach: Nehmen wir an, die o.g. Quadratfunktion hätte den symbolischen Namen sqr erhalten. Wenn im Programmcode z.B. sqr(x) auftaucht, dann weiß der Compiler, dass er das durch x*x ersetzen kann. Das gleiche gilt natürlich, wenn statt x konstante Werte verwendet werden (sqr(3) könnte im Compilat sofort als 9 stehen). Nur damit wir das auch erwähnt haben: Diesen Vorgang nennt man im Lambda-Kalkül Beta(β)-Reduktion. Den Begriff werden Sie aber vielleicht nie mehr benutzen. Es sei denn, Sie sind Compilerentwickler – oder Angeber 😉
Nun wissen wir aber, dass die wenigsten Funktionen nur einem Parameter haben. Und hier zeigt sich wieder einmal, dass Beschränkung frei machen kann. Denn auch Church stand natürlich vor diesem Problem – und er hat es genial einfach gelöst. Schauen Sie sich die Definition einer Additionsfunktion in Lambdanotation an:

f(x,y) -> x+y         // übliche mathematische Notation
λx.λy.x+y             // Ein Lambda kann nur einen Parameter haben
f(x) -> (g(y) -> x+y) // erste Fkt liefert zweite Fkt!

Aha, zwei Parameter, zwei Lambdas. Aber was bedeutet das? Das erste Lambda erzeugt ein zweites Lambda, in dessen Transformationsausdruck der Wert des Parameter des ersten auftaucht (man spricht hier auch vom Binden eines Parameters; der Rückgabewert ist eine sogenannte Closure, weil darin der konkrete Wert von x beim Aufruf von f(x) eingeschlossen wird )!
Dieser genial einfache Trick führt implizit zu einer weiteren ganz grundlegenden Eigenschaften funktionaler Programmierung: Funktionen selbst sind Typen, beschrieben über ihre Signatur. Das führt zu vielen extrem mächtigen Möglichkeiten, die es so nur in der FP gibt: Funktionen höherer Ordnung, Partial Application, Currying, Piping etc. Keine Sorge, das ist alles ganz einfach, und Sie werden es bald kennen.
Zum Abschluss des Theorieblocks und als Übergang in die Praxis möchte ich Ihnen zeigen, wie (von der syntaktischen Vereinfachung in Programmiersprachen abgesehen) die Signatur (also der Typ) einer FP-Funktion eigentlich typischerweise aussieht, hier am Beispiel der o.g. add-Funktion, die mit dem Ganzzahltyp int arbeiten soll:

(int, int) -> int        // vereinfachte Signatur
(int) -> (int) -> int    // interne, Lambda-konforme Signatur

Eine FP-Funktion mit vermeintlich zwei Parametern ist also in Wirklichkeit eine mit einem Parameter, deren Rückgabewert eine weitere Funktion mit ebenfalls einem Parameter ist!
Damit sei vorerst Schluss mit der Theorie. Was das Lambda-Kalkül in der Praxis für Möglichkeiten bietet und dass es sich mit der richtigen Programmiersprache überhaupt nicht fremd anfühlen muss, möchte ich Ihnen im Folgenden demonstrieren.

Ein Schluck vom Zaubertrank gefällig? Elixir

logoAls Sprache für unsere Codebeispiele habe ich Elixir verwendet. Zum einen ist sie elegant und auch ohne große Erläuterungen leicht verständlich, zum anderen ist sie als Technologie für Digitalisierung und IoT eine ziemlich gute Wahl. Im Detail erläutere ich das in meinem Beitrag Elixir: Zaubertrank für Digitalisierung?
Wenn Sie gerne hands-on lernen: Ausführliche Infos und gute Start-Tutorials finden Sie unter https://elixir-lang.org/  (die folgenden Codebeispiele können Sie aber auch ohne weiteres beim Lesen nachvollziehen, die Sprache ist einfach genug)

Unsere ersten Funktionen

Dann schreiben wir doch einfach mal Funktionen, wie wir sie oben besprochen haben. In Elixir macht man das in sogenannten Modulen:

defmodule SimpleSample
    def add(x,y), do: x+y
    def times(x,y), do: x*y
    def sqr(x), do: x*x
end

Viel eleganter kann eine Sprache nicht mehr sein, oder? Auf den ersten Blick mögen Sie Datentypen vermissen. Eigentlich bin ich auch ein Verfechter eines starken Typsystems, aber Elixir hat mich mit seiner Typlosigkeit bisher überzeugt, zumal ein message-basiertes Aktorsystem wegen der losen Kopplung in der Regel ohnehin nicht mit Typen arbeiten sollte.
Wenn man Funktionen eines Moduls verwenden möchte, kann man das entweder explizit durch Voranstellen des Modulnamens machen (SimpleSample.add ) oder indem man das Modul in den aktuellen Scope importiert. Um die Codebeispiele übersichtlich zu halten, wählen wir für unser Modul diesen zweiten Weg.
Wie alle modernen Sprachen besitzt Elixir eine interaktive Shell (REPL). Dort können wir unsere Funktionen erproben (der Einfachheit halber lasse ich im Folgenden das vorangestellte Prompt iex(…)> jeweils weg und gebe die Ausgabe als Kommentar an:

import SimpleSample
add(3,4)
# -> 7
sqr(7)
# -> 49

Wie kann ich mir Werte „merken“, um sie in weiteren Schritten zu verwenden? So einfach, wie man sich das wünscht:

result = add(3,4)
sqr(result)
# -> 49

result ist in diesem Beispiel übrigens keine Variable, wie Sie sie aus anderen Programmiersprachen kennen, sondern nur ein Alias für einen unveränderlichen Wert. Aus Convenience-Gründen kann man solche Aliasse in Elixir sogar mehrfach verwenden, wodurch es wie eine Variable aussieht. Aber der Compiler schützt uns vor Ungemach und weist hinter den Kulissen jeweils neue eindeutige Namen zu. Ein Beispiel dafür, wie Elixir uns den Umstieg in die FP einfach macht.

Durch diese Röhre muss er kommen: Die Pipe

Aber in FP-Code ist es ohnehin eher unüblich, mit zugewiesenen Zwischenergebnissen zu arbeiten, vor allem wenn man nur am Endergebnis interessiert ist. In Java u.ä. Sprachen hat man als Notlösung  dieses Problems das implemetierungsintensive Pattern Fluent API erfunden. In der FP gehört diese Problematik der Weiterverarbeitung zum Grundkonzept „Komponierbarkeit“ (darunter versteht man das Zusammenstecken von Funktionen zu größeren Funktionen – funktionales Lego, sozusagen).
Sie werden dafür schnell ein Ausdrucksmittel verinnerlichen und lieben, das Sie vielleicht aus Unix kennen: Piping

# Wir könnten natürlich sqr(add(3,4)) schreiben. Besser ist aber:
3 |> add(4)|> sqr
# -> 49

Das fühlt sich schon fast wie ein Taschenrechner an, oder? (Und gleicht fast imperativem Programmieren durch das Deklarieren einer Transformations-Abfolge, aber auf Basis von Funktionen und ohne “Variablen”). Genau dieses intuitive und mächtige Aneinanderreihen von Funktionen ist ein allgegenwärtiges Konzept von FP, das übrigens ohne zusätzlichen Entwicklungsaufwand auf allen Abstraktionsebenen gleich funktioniert. Es ist Grundlage des mächtigen Architekturmusters „Pipes & Filters“, wie auch der Code zu Beginn des Beitrags zeigt. Der Elixir-Webserver Phoenix besitzt auf Basis dieses Musters eine beeindruckend stabil und einfach erweiterbare Architektur mit sogenannten Plugs.
Falls jemand Pipes nicht kennt, hier die einfache Arbeitsweise: Der Pipe-Operator (in Unix ist das | , in Elixir |> ) nimmt den Rückgabewert der vorigen Funktion und speist ihn als ersten Parameter in die folgende Funktion. Das ist auch der Grund, warum im Beispiel die Funktionsaufrufe  jeweils einen Parameter weniger benötigen, ohne dass Sie irgendwas anders definieren müssen. An dieser Stelle sei an das Lambda-Kalkül erinnert, das mit seinem einparameterigen Aufbau hierfür die Grundlage ist.

Level up: Funktionen höherer Ordnung

Bereits der Verzicht auf veränderliche Zustände bietet inhärente Stabilitätsvorteile, wäre aber mit einiger Disziplin auch in imperativer Programmierung zu erreichen (in Java kann man bei konsequenter Verwenundg von final durchaus thread-sicheren Code schreiben, das macht bloß kaum jemand). Die eigentliche Stärke von FP sind aber eine große Ausdrucksmächtigkeit und ein Grad an Wiederverwendung, den sie in OOP selbst beim besten Design nicht erreichen werden. Und das wird vor allem durch ein bei FP unabdingbares Konzept ermöglicht: Funktionen höherer Ordnung. Das sind ganz einfach Funktionen, deren Parameter und/oder Rückgabewert selbst Funktionen sind.
Eingangs habe ich Ihnen erläutert, dass die Einfachheit des Lambda-Kalküls zu einer wichtigen Eigenschaft von FP-Sprachen führte: dass Funktionen selbst Typen sind. Das entspricht auch mathematischem Usus und Bedarf. Denken Sie z.B. an die Mengenlehre.
Worum geht es bei Funktionen höherer Ordnung? Nicht nur in der Mathematik, sondern auch in Anwendungen des alltäglichen Gebrauchs hat man regelmäßig den Bedarf, Datenmengen umzuwandeln, zu filtern, zu sortieren, zu kumulieren u.ä. In der  imperativen  Programmierung macht man so etwas eher umständlich mit Schleifen, Variablen etc.  Durch diesen Aufwand sind über die Jahrzehnte vermutlich Millionen Personenjahre vergeudet worden. Das Bewusstsein für die unsinnig hohe Redundanz und Fehleranfälligkeit dieses Ansatzes hat z.B. zu OO-Patterns wie „Template Method“, „Strategy“ u.ä. geführt. Aber diese Muster sind Behelfslösungen und kurieren eigentlich nur Symptome. Die Diagnose bleibt: Imperative Programmierung hält sich zu sehr mit dem “Wie” auf. Funktionale Programmierung kümmert sich um das “Was”, ist also eher deklarativ.
Ich erläutere das am klassischen Beispiel der Mengenlehre: Der Abbildung einer Menge auf eine andere. Wer im Mathematikunterricht nicht geschlafen hat, weiß, dass man dazu eine Abbildungsfunktion benötigt, die jeweils ein Element der Quellmenge in ein Element der Zielmenge transformiert.
Schauen wir uns im Code an, wie wir für die Menge der natürlichen Zahlen 1 bis 100 die Menge ihrer Quadratzahlen ermitteln können. Jetzt fangen wir allmählich an, richtig funktional zu programmieren:

1..100 |> Enum.map( &sqr/1 )
# -> [1, 4, 9, 16, …, 10000]

Wenn Sie bisher nur imperativ programmiert haben, werden Sie vermutlich nicht mit einer so einfachen Lösung gerechnet haben. Sie haben dabei zwei neue Elixir-Datentypen und ihre erste Funktion höherer Ordnung kennengelernt:

  • [1, 4, …, 10000] ist eine Darstellung eines sehr grundlegenden FP-Datentyps: Listen (wie man mit Listen arbeitet, schauen wir uns weiter unten bei Rekursion an)
  • 1..100 stellt eine Range dar, die für Enumeration und Streaming (wir kommen in späteren Teilen der Serie zum Streaming; dann können wir die Quadratzahlen aller natürlichen Zahlen ermitteln) verwendet werden kann. Sie entspricht im Wesentlichen einer Liste [1,2,.. 99, 100]
  • map ist eine von vielen Funktion aus dem Modul Enum. Sie bildet eine aufzählbare Menge(daher Enum für Enumeration) auf eine andere ab. Um das machen zu können, benötigt sie als 2. Parameter (der erste ist die Ausgangsmenge und kommt über die Pipe rein) die Abbildungsfunktion für die Elemente. Das macht sie zu einer Funktion höherer Ordnung

Das Besondere hier ist also der Parameter der map-Funktion. Hier sollten Sie unsere selbst geschriebene Quadrat-Funktion sqr wiedererkennen, die für die eigentliche Transformation der Elemente sorgt. Die etwas komische Schreibweise &map/1 wird hier benötigt, um eine eindeutige Referenz auf die Funktion mit einem Parameter zu erzeugen. Wir werden gleich elegantere Notationsmethoden kennen lernen.
Was mache ich, wenn ich die Zahlen mit drei multiplizieren möchte (warum auch immer)? Am liebsten würde ich unsere Multiplikationsfunktion times/2 wiederverwenden. Aber die benötigt im Gegensatz zu sqr zwei Parameter. Nun, ganz einfach, denn den zweiten Parameter kennen wir ja bereits: 3. So können wir ihn mitgeben:

# Ich kann auch Funktionen mit mehr Parametern nehmen, wenn ich diese binde
1..100 |> Enum.map( &times(&1, 3) )
# -> [3, 6, 9, 12, …]

Ist das cool, oder was? &1 ist der Platzhalter des erhaltenen Parameters (map stellt nur einen Parameter, das zu mappende Element, zur Verfügung, aber wir werden auch komplexere Funktionen höherer Ordnung kennenlernen). Wir hätten im oberen Beispiel statt &sqr/1 also auch &sqr(&1) schreiben können. Verwenden Sie einfach die Schreibweise, die Ihnen und Ihren Kollegen lesbarer erscheint.
Spielen wir noch ein wenig mit der Pipe und Funktionen höherer Ordnung rum. Sagen wir, wir wollen nur die ungeraden Vielfachen von drei  haben, müssen also irgendwie filtern. Können Sie raten, wie die Funktion heißt und wie wir sie einbauen? Richtig.

1..100
|> Enum.map( &times(&1,3) )
|> Enum.filter( fn x -> rem(x,2) != 0 end )
# -> [3, 9, 15, 21, …]

Es funktioniert. Übrigens hätte man in diesem Fall die Filter-Funktion auch vor die map-Funktion stellen können, da nur die Multiplikation ungerader Zahlen selbst ungerade ist. Für die meisten Anwendungsfälle spielt die seuqentielle Anordnund der Pipe natürlich schon eine Rolle, die sich aus den Anforderungen und dem sukzessiven Transformationsbedarf ergibt.
Sie haben dabei eine weitere Möglichkeit kennengelernt, nämlich eine anonyme Funktion inline  zu definieren: fn x -> … end. &rem/2 (für remainder) ist eine Kernel-Funktion zur Divisionsrest-Ermittlung (in anderen Sprachen auch als mod bekannt).
Das ist doch alles recht einfach, vor allem im Vergleich zu imperativem Code. Aber abgesehen davon, dass wir die Pipe jetzt in der üblicheren und übersichtlicheren Schreibweise untereinander verwenden, drückt unser Code schon wieder etwas zu viel „Wie“ statt „Was“ aus und fängt syntaktisch an wie Javascript auszusehen (noch nicht wirklich, aber wehret den Anfängen…).
Auf der Abstraktionsebene dieser Codestrecke möchte ich aber eigentlich nur deklarieren, dass ich ungerade Zahlen brauche, mich und den Codeleser nicht damit beschäftigen, wie ich feststelle, ob eine Zahl ungerade ist. Zumal eine solche Funktion womöglich anderweitig verwendbar ist. Bleiben Sie möglichst immer auf einem Abstraktionsniveau. Das können wir also mit mehr Wiederverwendungspotenzial und Klarheit schreiben. Unser erstes funktionales Refactoring:

# wiederverwendbare Funktionen, ergänzt im Modul Simple Sample
def ungerade?(x), do: rem(x,2) != 0
def ungeradeVerdreifachte(zahlen) do
    import Enum
    # lokale Definition einer adhoc-Funktion, macht den Code lesbarer
    verdreifache = &times(3,&1)
    zahlen
    |> map( verdreifache )
    |> filter( ungerade? )
end
# im Applikationscode würde dann nur noch stehen
meineZahlenquelle
|> ungeradeVerdreifachte
|> weitereFuntkion

Bitte sehen Sie für den Moment vom fachlichen Unsinn des Beispiels ab. Sie werden sich sicher bessere Anwendungsmöglichkeiten vorstellen können, sollten aber schon jetzt einen Eindruck davon erhalten haben, wie ausdrucksstark der Code sein könnte, den Sie bereits mit den bisher erlernten Sprachmitteln für Ihre Anwendung schreiben könnten.
Dabei haben wir eines der mächtigsten und unverzichtbarsten Sprachmittel der funktionalen Programmierung noch gar nicht kennen gelernt: Rekursion.

Play it again, Sam! Die Macht der Rekursion und grenzenloser Datentypen

Mancher von Ihnen hat sicher schon rekursive (d.h. sich selbst aufrufende) Funktionen entwickelt oder gesehen. Viele haben Angst vor Rekursion und/oder schlechte Erfahrungen damit gemacht: Stacküberläufe, Datentyp-Überläufe, Endlosschleifen sind typische Effekte bei Rekursion in imperativer Programmierung.
In FP ist Rekursion kein Schreckgespenst, sondern erleichtert das Leben erheblich. Zum einen lernt man bei FP schnell, wie ein Mathematiker zu denken, zum anderen sind FP-Sprachen auf rekursive Funktionen optimiert, weil unfassbar viele Probleme sehr elegant rekursiv lösbar sind. Das sehen wir jetzt.
Wie definiert ein Mathematiker Funktionen (und oft auch Mengen, z.B. die Peano-Zahlen)? Er denkt nicht in Schleifen, Summenvariablen, Ausstiegsbedingungen o.ä. wie ein imperativer Programmierer, sondern er unterscheidet Basisfälle und rekursive Definitionen. Überlegen Sie kurz, wie Sie bisher die Fakultäts-Funktion implementiert hätten (womöglich mit einer Variable, die in einer Schleife hochmultipliziert wird, also imperativ). Und jetzt schauen Sie, wie Sie das in Elixir machen können bzw. sollten:

def fac(0), do: 1
def fac(x) when x > 0, do: x * fac(x-1)

Sie kodieren auf einfachste Weise, wie ein Mathematiker die Funktion definiert: x! = {1 für x = 0; x*(x-1)! für alle natürlichen Zahlen}. Wie das Unterscheiden dieser beiden Fälle technisch funktioniert, schauen wir uns gegen Ende an. Stichworte sind Pattern Matching und Value Extraction. Im Moment würde uns das zu sehr aufhalten.
Dieser deklarative Programmierstil ist vielleicht ungewohnt, aber extrem einfach und sicher, sobald man sich einen entsprechenden Denkstil angeeignet hat. Wir werden später bei der Listenrekursion noch sehen, dass das nicht nur für offensichtlich rekursive mathematische Funktionen hilfreich ist.
Durch ihrer Optimierung auf Rekursion hat die FP außerdem einen weiteren unschlagbaren Vorteil gegenüber den üblichen Verdächtigen: Exaktere Datentypen. Mit „alle natürlichen Zahlen“ meint der Mathematiker nicht nur die, für die das Ergebnis zufällig in 64 Bit passt. Haben Sie schon mal in Java o.ä. Sprachen versucht, die Fakultät von 1000 auszurechnen? „Geht nicht!“ gilt nicht. Was ich schockierend finde: Die meisten Codebeispiele, die man zuhauf im Internet und in Lehrbüchern findet, verwenden ganz selbstverständlich den Datentyp long und gehen oft noch nicht einmal auf die Rahmenbedingung ein, dass damit nur bis zu 20! gerechnet werden kann – geschweige denn, wie man diese sprachentwurfs-bedingte Grenze überwinden könnte, denn es soll ja vorkommen, dass fachliche Anforderungen auch jenseits solcher willkürlicher Grenzen reichen. Leider wird auf diese Weise angehenden Programmierern das Fach vermittelt. Da wundert einen nichts mehr!
Schauen wir einfach, was Elixir von begrenzten Datentypen hält (ähnliches gilt für Haskell, Scala u.a. FP-Sprachen):

fac(1000)
# -> 4023872600770937 … 00000000

Sie können sich vorstellen, dass das ganz schön viele Ziffern sind. Um ein Haar hätte ich sie gezählt, da fiel mir ein, dass ein FP-Entwickler so etwas nicht nötig hat. Das hier sollten Sie mittlerweile problemlos verstehen:

anzahl_stellen = &( &1 |> to_string |> String.length)
1000 |> fac |> anzahl_stellen
# -> 2568

Witzigerweise entspricht die Anzahl der Stellen der Telefonnummer meines Elternhauses (ich komme gebürtig aus einem kleinen Ort); vielleicht ist es ja meine persönliche 42…
Hier haben Sie auch noch mal ein Best Practice: Statt die ggf. unerklärliche Pipe to_string |> String.length direkt zu verwenden, weisen wir sie als adhoc-Funktion einem Alias zu, der ausdrückt was sie macht: Sie ermittelt die Anzahl der Stellen einer Zahl. Die Regeln von “Clean Code” lassen sich auch auf FP anwenden.
In Anlehnung an die an „Casablanca“ angelehnte Kapitelüberschrift sollten Sie jetzt allmählich denken „Ich glaube, das ist der Beginn einer wunderbaren Freundschaft“.

Elegant? Definitiv! Exakt? Verblüffend! Aber auch schnell?

Aber elegante, bei Entwicklern beliebte Programmiersprachen haben oft eine teure Kehrseite, man denke an Ruby. Falls Sie also jetzt im typischen „Java ist aber schneller und C noch mehr“-Reflex denken „Ja, aber die Performance…!“: Bei fac(1000) zuckt die REPL noch nicht mal, das Ergebnis (immerhin das Produkt von 1000 Multiplikationen mit einer Länge von 2568 Ziffern) steht da in Sekundenbruchteilen (und die längste Zeit benötigt vermutlich die Ausgabe der riesigen Zahl auf dem Display). Ähnlich ist es bei fac(10000) (35660 Stellen). Erst bei fac(30000)(121288 Stellen exakt berechnet!) fängt die Berechnung an, in den spürbaren Bereich mehrerer Sekunden zu gehen. Der Prozessor ist auch nur ein Mensch. Aber wir haben ja auch noch nicht über die Verteilung von Arbeitslast auf mehrere Prozessorkerne gesprochen. Das Thema Parallelisierung heben wir uns lieber für weniger triviale Probleme in folgenden Beiträgen auf (z.B. rekursive Ermittlung optimaler Züge bei Brettspielen, für deren kombinatorischen Explosion FP und Nebenläufigkeit a la Aktoren perfekte Lösungsmittel sind). Das wäre hier zwar auch schon möglich, aber bei einem so lächerlich kleinen Berechnungsproblem nun wirklich mit Kanonen auf Spatzen geschossen. Wann berechnet man schon mal solche Fakultäten in diesen Größenordnungen? Der Punkt ist: Man sollte es einfach können, wenn man es braucht.
Natürlich funktionieren auch die normalen Rechenfunktionen in diesen gigantischen Größenordnungen:

fac(20000)-fac(10000) |> anzahl_stellen
# -> 77338

Zur Erinnerung: 77338 ist nicht das Ergebnis von 20000! – 10000!, sondern die Anzahl der Stellen des im ersten Schritt der Pipe korrekt berechneten Ergebnisses!
Ich finde es ziemlich beeindruckend, so etwas im Sekundenbereich ausrechnen zu können, während die vermeintlich auf die Prozessorarchitekturen unserer PCs hochoptimierten Compiler vieler Programmiersprachen jenseits von 2^63 die Hufe hochreißen (hat übrigens gerade mal 19 Stellen!) – und wirklich schnell ist Rekursion in Java auch nicht, weswegen viele ja lieber auf Schleifen zurückgreifen.
Ich weiß nicht, wie die Compilerbauer von Elixir, Erlang, Haskell, Scala und Co. diese Grenzenlosigkeit mit dieser Performanz schaffen, aber das schöne ist ja gerade: Als FP-Programmierer sollte und muss ich mich nicht mit so mondänen Problemen wie Prozessor-Wortbreite oder Stacktiefe aufhalten lassen, sondern kann mich auf die zu lösenden Probleme konzentrieren. Selbst wenn nur dieser Vorteil für FP spräche, wäre er eigentlich schon zwingend.

Aber immer Kopieren statt Ändern ist doch langsam?

Viele argumentieren auch, dass es doch ein Performance-Fresser sein muss, dass in FP ständig neue Datenstrukturen entstehen, weil bestehende ja nicht geändert werden dürfen. Aber das stimmt nicht. Zum einen sollte sich jeder, der so kleinteilig optimieren will, die berühmten Worte von Donald Knuth in Erinnerung rufen: “Premature optimization is the root of all evil”. Das zeigt sich auch drastisch, wenn man bedenkt, das bei Multithreading durch die Synchronisierung gemeinsam benutzten Speichers ein Vielfaches der durch veränderliche Zustände vermeintlich gesparten Zeit vergeudet wird.
Außerdem sind viele Datenstrukturen bei FP überhaupt erst aufgrund der Nur-Lese-Eigenschaft optimierbar: Wenn einer LinkedList ein neues Element vorangestellt wird, besteht die neue Liste nur aus diesem Element und dem Link auf die alte Liste (die sich ja nie ändern kann). Und schließlich kann FP fast ausschließlich mit dem (bekanntlich schnellen) Stack arbeiten und hat kaum vergleichsweise teure Heap-Aufrufe. Natürlich gibt es auch in FP die Möglichkeit, veränderliche Zustände festzuhalten. In Elixir geschieht das u.a. über Aktoren/Prozesse, die wiederum hochoptimiert bzgl. der Nutzung ihres absolut geschützten lokalen Speichers sind.

Rekursive Listenverarbeitung

Vielleicht wird mancher einwenden, dass die Welt der rein zahlenmengen-basierten Mathematik naturgemäß ein Heimspiel für funktionale Programmierung ist, allerdings für typische technische oder Business-Anwendungen nur eine relativ kleine Rolle spielt. D’accord!
Eine typischere Problematik der meisten Anwendungsdomänen ist das Verarbeiten von Datenmengen (Sortieren, Filtern, Summieren, Optimieren etc.). Natürlich geben die existierenden Funktionen  höherer Ordung (z.B. des Enum-Moduls) dafür schon unglaublich viel her. Mittlerweile sollten Sie eine grobe Vorstellung davon haben, was diese anfangs schon gezeigte Codestrecke macht und wie Sie die aufgerufenen fachlichen Funktionen implementieren könnten:

kunden
|> filter(vipKunde?)
|> sort(jahresUmsatz)

Aber ich möchte Ihnen gegen Ende dieses langen Beitrags noch beibringen, wie Sie rekursiv mit Listen arbeiten und dabei selbst beliebige Funktionen höherer Ordnung schreiben können, die Sie dann für verschiedene fachliche Zwecke wiederverwenden können.
Stellen Sie sich vor, Sie sollen den höchsten Jahresumsatz einer gegebenen Menge von Kunden ermitteln und sehen noch weitere ähnliche Aufgaben auf sich zu kommen. Sie wollen daher von vornherein  eine Funktion höherer Ordnung schreiben, die nicht auf Kunden beschränkt ist, sondern beliebige maximale Werte aus beliebigen Mengen mit beliebigen Attributen ermittelt. Überlegen Sie kurz, wie Sie das angehen würden (z.B. mit OOP-Mitteln, gerne aber auch mit den schon bekannten FP-Mitteln).
Ein Tip, den ich auch bei OOP schon immer gegeben habe und den man mit „Test First!“-Ansätzen wie TDD und BDD hervorragend umsetzen kann: Überlegen Sie immer, wie Sie selbst etwas gerne würden benutzen würden, wenn es das schon als API gäbe. Das führt Sie in der Regel zu gut verwendbaren Schnittstellen, in unserem Fall also Funktionen. Wie würden Sie das Problem des maximalen Jahresumsatz also in idiosynkratischer Elixir-Syntax lösen wollen? Ich schlage folgendes vor:

kunden
|> max(jahresumsatz)

Sprechender und dabei wiederverwendbarer kann Code kaum sein. Jeder, der das liest, wüsste sofort, dass hier der maximale Wert unter den Jahresumsätzen der Kunden gesucht wird (kunden steht hier stellvertretend für eine Menge von Kunden, die selbst schon als Funktionsparameter oder aus dem vorderen Teil der Pipe kommen könnte).
Man könnte sich auch schon etwas unter jahresumsatz vorstellen, wenn man an die map-Beispiele denkt: Das ist nämlich eine Funktion, die den Jahresumsatz für jeweils einen Kunden ermittelt. Wie sie das macht, ist an dieser Stelle nicht wichtig. Denkbar wäre, dass sie einfach ein Attribut aus einer Datenstruktur zurück gibt, 12 Monatsumsätze aufaddiert, eine Datenbank-Abfrage macht etc. (Auch eine Datenbank ist in FP eine Sammlung von Funktionen. Denken Sie hier bitte nicht an die Optimierung von DB-Abfragen, es gibt gerade in deklarativer FP-Programmierung ausreichend Möglichkeiten, derlei Probleme zu lösen; das würde diesen Beitrag aber endgültig sprengen)
Es spielt an dieser Stelle also schlichtweg keine Rolle, wie die Funktion das Problem löst, den Wert zu ermitteln, weil wir sie gerade nicht implementieren, sondern nur verwenden. Entscheidend ist nur Ihre Signatur, in diesem Fall (Kunde) -> Jahresumsatz. Da wir max als generische Funktion höherer Ordnung anlegen, können wir sogar verallgemeinern zu (Datenelement) -> VergleichbarerWert. Hier kommt uns erneut entgegen, dass Elixir nicht getypt ist. Das bedeutet: Ohne große Regularien kann jeder eine beliebige Liste in unsere Funktion max geben, solange er uns ebenfalls eine Funktion gibt, die für jedes Element dieser Liste vergleichbare Werte ermittelt. So wird in FP Wiederverwendung erzeugt. Vielleicht anfangs ungewohnt, aber man gewöhnt sich recht schnell daran. Man kann das zwar auch formaler machen (in Elixir über sogenannte Protokolle, vergleichbar den Interfaces in Java), aber für unseren und viele ähnliche Zwecke tut es das ungetypte Verfahren wunderbar.
Was statt wie ist also sehr hilfreich. Aber schauen wir uns jetzt aber an, wie wir diese Funktion max implementieren müssten, wenn es sie noch nicht gäbe. Aber auch das machen wir wieder überwiegend deklarativ. Nochmal zur Erinnerung: Die Funktion erhält eine Menge uns unbekannter Datenelemente (ich sage bewusst nicht Objekte, weil wir in FP eher von Datenstrukturen reden, nicht von Klassen) und eine Funktion, die für jeweils eines dieser Elemente einen Wert ermitteln kann. Aufgabe der max-Funktion ist es den größten dieser Werte zu ermitteln (und zurückzugeben).
Könnten Sie es schon rekursiv lösen? Sie müssen wieder wie ein Mathematiker denken (teile und herrsche) und können drei Fälle definieren, deren letzter mit Listenrekursion arbeitet. Versuchen Sie doch mal die folgenden drei Funktionsdefinionen zu verstehen, bevor ich Ihnen die fehlenden Syntakexlemente erläutere:

def max([], _), do: nil
def max([only |[]], valFn), do: valFn(only)
def max([first | remaining], valFn) do
     case {valFn(first), max(remaining, valFn)} do
         {valF, maxValR} when valF > maxValR -> valF
         {_, maxValR} -> maxValR
     end
end

Nach Jahren mühselig redundanter Programmierung mit Schleifen und temporären Variablen mag es Sie wundern bis frustrieren, aber das ist wirklich alles, was Sie definieren müssen. Und zwar ein für alle Mal. Sie können sich auf diese max-Funktion verlassen (die Sie natürlich im echten Leben mit Unit-Tests gesichert haben) und sich ab jetzt auf fachliche Logiken und sinnvolle Implementierungen von valFn beschränken.
Wie funktioniert max(elemente, wertfunktion), mathematisch gedacht? Im Prinzip wie die Fakultät weiter oben. Der maximale Wert zu einer Menge von Elementen ist definiert als:

  • Undefiniert, wenn die Menge leer ist (wir hätten diese oberste Definition auch weglassen können, aber dann hätte es ggf. Laufzeit-Exceptions gegeben, jetzt muss sich der Aufrufer damit auseinander setzen, dass es ggf. auch keinen Wert gibt, wenn es kein Element gibt, alternativ hätte man einen optionalen Parameter für den Defaultwert einführen können)
  • Der Wert zum einzigen Element, wenn es nur ein Element gibt (Basisfall)
  • Der größere Wert aus dem Wert des ersten Elements und dem maximalen Wert aller übrigen Elemente (Rekursionsfall)

Vergleichen Sie diese drei Fälle mit den drei implementierten Methoden. Sie sollten zumindest erahnen können, dass sie genau analog sind. Zum Verständnis des Codes fehlen Ihnen noch zwei zusammengehörige Konzepte, die ich Ihnen zum Abschluss dieses Beitrags noch antun muss.

Pattern Matching und Wertextraktion

Wenn Sie sich die drei Funktionsdefinitionen des letzten Beispiels ansehen, könnten Sie sich folgendes fragen: Wenn Elixir ungetypt ist und Funktionen somit nur nach Funktionsname und Anzahl der Parameter unterscheiden kann, wie kann es denn sein, dass es drei Funktionen namens max/2 gibt? Wie entscheidet das System, welche Methode die richtige ist?
Hier kommt ein gängiges Muster der FP zum Einsatz: Pattern Matching. Aus einer Reihe formal gleichwertiger Alternativen (max/2, fakultaet/1) wird die jeweils erste benutzt, deren Parametermuster den beim konkreten Aufruf übergebenen Parametern entspricht. Dieses Konzept ist unglaublich mächtig, da es weit über Typisierung und dem aus anderen Sprachen bekannten Overloading nach Typen hinausgeht. Man kann damit beliebig viele spezialisierte Funktionen implementieren, wo man anderswo große switch-Blöcke im Funktionsbody bräuchte. Das zuvor erwähnte Phoenix macht sich das z.B. bei der Router-Implementierung zunutze.
In diesem Fall nutzen wir folgende Pattern, die für die Listenverarbeitung typisch sind:

  • [] stellt eine leere Liste dar
  • [head|tail] stellt eine nicht leere Liste dar. Dabei ist head das erste Element und tail eine Liste der übrigen Elemente. Das ist ein sehr performantes Pattern für die rekursive Verarbeitung der in FP-Sprachen fast ausschließlich benutzten LinkedLists
  • [head|[]] ist ein Beispiel für eine Kombination dieser beiden Pattern. Es besagt, dass es zwar ein Kopfelement gibt, aber der tail eine leere Liste. Wir benutzen das, um unseren Basisfall einer einelementigen Liste zu definieren. Deshalb muss dieser auch vor dem Rekursionsfall stehen, weil dessen Muster auch auf die einelementige Liste passen würde. (Wir könnten auf diese Weise übrigens auch Patter für Listen mit z.B. drei Elementen und/oder bestimmten Werten der Elemente definieren, wenn das fachlich sinnvoll ist; hier brauchen wir es nicht)

Passend zu Pattern Matching gibt es Value Extraction. Wenn Sie sich die Funktionen 2 und 3 anschauen, werden Sie feststellen, dass im Funktionsrumpf der erste Parameter nicht als ganzes angesprochen wird (ja, er hat nicht mal einen Namen wie elements, weil wir diesen nicht brauchen). Stattdessen wird mit den per Pattern zerschnittenen Teilwerten gearbeitet, die wir ja auch in unserer Definition brauchen. Dieses Feature von FP-Sprachen ist extrem hilfreich und vereinfacht das Denken in und das Umsetzen von sauberen mathematischen Definitionen – nicht nur, aber vor allem bei Rekursion.
Wenn Sie die Implementierung der dritten Methode, also des Rekursionsfalls genauer betrachten, werden sie die beiden Muster auch dort wiederfinden. Zunächst allerdings lernen Sie ein neues Sprachkonstrukt kennen, das Tupel ({wert1, … wertn}). Diese Datenstruktur ist sehr hilfreich, wenn mehrere Werte gemeinsam transportiert werden müssen, z.B. als Rückgabewert einer Funktion. Hier machen wir es uns wie folgt zunutze: Wir müssen zwei Werte berechnen: Nämlich den Wert des head-Elements (hier first genannt) und den maximalen aller Werte aus dem tail (hier remaining genannt).
Da vor allem das letztere eine ggf. teure Funktion ist, wir die Ergebnisse für das folgende Pattern-Matching aber zweimal brauchen, berechnen wir sie am Anfang, verpacken sie in ein Tupel und matchen dieses dann gegen die zwei definierten Fälle. Dieses Matching machen wir wiederum mit case … do … end, Elixirs Variante zu Scalas match-Funktion. Eine Art switch, aber viel mächtiger. Im ersten Fall können Sie auch sehen, dass man die Matches mit sogenannten Guards verfeinern kann. Die Match-Klausel würde nämlich für beide Fälle passen. Den ersten wollen wir aber nur anwenden, wenn das erste Element einen größeren Wert hat als der Rest.
Nur noch eine Erläuterung. da sie sich vielleicht über die Underscores (_) wundern. Mit diesen kann man ausdrucken, dass das Element an dieser Stelle für das Muster keine Rolle spielt und/oder man es im Code nicht verwenden möchte. Da der Wert des ersten Elements die erste Bedingung nicht erfüllt hat, brauchen wir ihn im zweiten Fall nicht und können ungeprüft den anderen Wert zurückgeben.

So einfach wie möglich, aber nicht einfacher!

Viel komplizierter als die oben gezeigte Implementierung von max sollten saubere FP-Funktionen typischerweise nicht werden – im Gegenteil: Man sollte sich angewöhnen, bei zu langen Methoden zu fragen, ob sie nicht mehrere Dinge vermischen und man den Code noch einfacher machen kann.
Und das machen wir auch hier: Im letzten Beispiel könnte man argumentieren, dass das Vermischen der Wertermittlung und der Ermittlung des Maximalwerts in einer Funktion max/2 eigentlich ein wenig redundant ist, weil es ja schon eine Funktion Enum.map/2 zum Umwandeln von Wertemengen gibt. In der Tat hätte man auch schreiben können:

kunden
|> map(jahresumsatz)
|> max

Sie können das inzwischen lesen: Zunächst werden die Kunden in ihre Jahresumsätze transformiert (ge”map”t). Danach reicht eine Funktion max/1, die den größten Wert aus einer Liste von Werten bestimmt. Natürlich wäre max/1 etwas einfacher:

def max([]), do: nil
def max([only | [] ]), do: only
def max([first | others]) do
      case {first, max(others)} do
          {first, maxOthers} when first > maxOthers -> first
          {_, maxOthers}                            -> maxOthers
      end
end

Beides sind valide Alternativen. Am Ende zählt, welcher Code lesbarer ist. Und ich persönlich finde, dass max(jahresumsatz) an der Aufrufstelle eigentlich klarer den Sinn (das “Was”) ausdrückt: Wir wollen den größten Jahresumsatz haben. Die max/2-Variante kann für den Aufrufer also lesbarer sein.  Trotzdem müssen Sie die max/2-Variante nicht so kompliziert implementieren wie oben. Verlagern Sie einfach das “Wie” mit map in eine zweite Funktion und schreiben Sie unter Verwendung der ohnehin sinnvollen max/1-Funktion einfach noch das hier:

def max(elements, fnVal), do: elements |> map(fnVal) |> max

So haben Sie den Vorteil beider Ansätze. Am Ende geht es um den Code mit der besten Les-/Wartbarkeit. Sie sollten jetzt auch erkennen, warum ich anfangs die steile These aufgestellt habe, dass FP viel besser für Wiederverwendung geeignet ist als OOP.
Grundsätzlich gilt: In FP gibt es viele Wege zum Ziel. Und das Ziel ist Code der bestmöglichen Qualität. Dabei ist die einfachste und eleganteste (d.h. mathematisch sauberste) Implementierung meistens die beste.

Einfach, sicher, produktiv: Was will man mehr?

Wir sind am Ende unserer Einführung angelangt. Ich hoffe, Sie können nachvollziehen, warum ich FP im Titel mit diesen Attributen versehen habe. Eine gute FP-Funktion ist

  • einfach, indem ihre Arbeitsweise auf den ersten Blick nachvollziehbar ist
  • sicher, indem sie nichts anderes als eine definierte Transformation macht (dadurch übrigens auch hervorragend testbar)
  • gut (wieder)verwendbar und einfach mit anderen Funktionen komponierbar, z.B. in Pipes

FP erhöht die Qualität und Produktivität nachhaltig, indem Code entsteht, der einheitliche Abstraktionen besitzt und auf jeder Abstraktionsebene gut nachvollziehbar ist, ohne den Ersteller oder den Wartungsentwickler mental zu überfordern.
Ich hoffe, die Lektüre hat sich für Sie gelohnt. Vielleicht konnte ich Sie etwas für FP und seine Vorteile interessieren. Auf jeden Fall sollten Sie jetzt etwas konkretere Vorstellungen von diesem geheimnisumwitterten Paradigma haben. Vielleicht mögen Sie selbst etwas rumexperimentieren. Natürlich ist das noch nicht alles, aber das wesentliche Rüstzeug haben Sie jetzt. Und Elixir ist nicht nur die perfekte Sprache zum Einstieg, sondern auch die richtige Wahl in Hinblick auf Betriebsqualität.
Sie haben schon jetzt einen guten Eindruck bekommen, wie einfach man sprechenden Code wie den eingangs erwähnten entwickeln kann. In den nächsten Beiträgen dieser Serie werde ich Ihnen zeigen, wie man Funktionen als Grundbausteine verblüffend einfach zu immer größerer Semantik komponieren kann. Dabei lassen sich auf einmal im Handumdrehen Probleme lösen, deren Komplexität einem in imperativer Programmierung erhebliches Kopfzerbrechen gemacht hätte, wenn man sich überhaupt herangetraut hätte.
Gerne weise ich auch schon daraufhin, dass wir demnächst ein hands-on-Training für FP mit Elixir anbieten werden. Wir bieten das auch gerne als Inhouse-Training an oder begleiten Ihre Teams mit Coaching auf dem Weg in die einfache, sichere und produktive Welt der funktionalen Programmierung.
Bitte sprechen Sie mich bei Interesse an funktionaler Programmierung jederzeit an. Ich freue mich auf das Gespräch mit Ihnen.
 
Michael_Pul_Paulsen_.png

Michael „Pul“ Paulsen
– Principal Consultant, seppmed gmbh –

Print Friendly, PDF & Email

Schlagwörter

Neuste Beiträge

Teile diesen Beitrag:

Share on facebook
Share on linkedin
Share on twitter
Share on xing
Share on whatsapp
Share on google
Share on email
Share on print

Blog per E-Mail folgen

Klicke hier, um diesem Blog zu folgen und Benachrichtigungen über neue Beiträge per E-Mail zu erhalten. 

Besuchen Sie uns online:

RSS Feed

9 Kommentare

  • Avatar Matthias says:

    Hallo Michael,
    ich bin ja versucht von deiner Begeisterung für FP angesteckt zu werden. (Ich mag Linq in C# sehr, andere Sprachen dürften inzwischen ähnliches können). Ich kann mich noch an Zeiten erinnern, als FunctionPtr Teufelswerk waren 😀 Heutzutage lassen sie sich wirklich deutlich komfortabler definieren.
    Aber sowenig wie sich ausschließliches OOO einhalten läßt (Helper-Klassen) wird sich reine FP nicht halten lassen (strukturierte Daten). Die Wahrheit liegt mMn in der Mitte.
    Du schreibst:
    “Als FP-Programmierer sollte und muss ich mich nicht mit so mondänen Problemen wie Prozessor-Wortbreite oder Stacktiefe aufhalten lassen, sondern kann mich auf die zu lösenden Probleme konzentrieren.”
    Wie hängen das Typsystem und das Paradigma der FP zusammen? (abgesehen vom Comiler der beides kann, das eine geht sicher auch ohne das andere).
    Ist die Stacktiefe nicht begrenzt (auch hier müssen Rekursionen doch ein Ende haben)? In Prolog gab es Konstruktionen die in konstantem Speicher abliefen, daber das war nicht die Norm, ist das hier anders?
    Kurzer und Ausdrucksstarker Code:
    Auch bpsw. in den verschiedenen C-Ausprägungen gibt es auch unterschiedliche knackige Operatoren. Hier ist mir allerdings in der Praxis eher der Wunsch weg von “kurz und knackig” hin zu “sprechend, lesbar wie ein Text” begegnet.
    Als Beispiel fällt mir hier der ?: operator ein, auch praktisch aber selten gesehen ist in C# der ?? operator. Beide werden teils ungern gesehen. Die Syntax in den Beispielen oben gibt ihre Bedeutung auch erst nach einigem Studium preis. Paradigma und Notation sind sicher nicht so leicht zu vermitteln.
    Die unteschiedlichen Vorlieben und Auffassungen von ausdrucksstarkem und leicht/schnell verständlichem Code dürften auch der Grund für die vielen Sprachen am Markt sein.
    Rechnen mit sehr großen Zahlen (jenseits der “üblichen” Wortbreite):
    http://www.informatik.uni-ulm.de/ni/Lehre/SS05/CompArith/ArithExact2.pdf
    oder:
    http://www.softwareschule.ch/download/XXL_BigInt_Tutorial.pdf
    Es ist sicher kein Zufall, daß sich Schweizer mit großen Zahlen auskennen 🙂
    Schönen Gruß,
    Matthias

    • Avatar Michael "Pul" Paulsen says:

      Hallo Matthias,
      danke für Deinen Kommentar und die Links. Für mich ist nicht so sehr Frage spannend, ob man Schwächen imperativer Sprachen (wie z.B. die Typbreite) kompensieren kann (genauso möchte ich sie nicht in Bausch und Bogen schlecht machen), sondern eher um die Frage: Sind imperative Sprachen den Herausforderungen unserer Zeit generell noch gewachsen? Ich habe da meine erheblichen Zweifel.
      Natürlich gab und gibt es jede Menge Versuche, die prozessor- und compilerbedingten Grenzen zu überwinden, da ja niemand leugnet, dass die abzubildende Wirklichkeit bekanntlich diese willkürlichen Grenzen nicht hat.
      Es ist aller Ehren wert, wenn jemand Bibliotheken entwickelt (und wie der Schweizer Artikel darstellt), die z.B. Java- oder C-Entwicklern ermöglichen, die Grenzen ihrer Sprachen bzw. ihres Typsystems zu erweitern. Das Problem ist aber: Wenn ich das in einem Entwicklungsumfeld mache, das diese Grenzen nun mal inhärent hat, stehe ich als nächstes vor dem Problem, die Ergebnisse in anderen APIs nicht weiter verwenden zu können. Hmm.
      Und wie sehr Grenzen einer Umgebung gehen können, hat man daran gesehen, dass es gefühlt Jahrzehnte gedauert hat, bis man in Java endlich z.B. Closures benutzen konnte (ein Konzept, das es schon in SmallTalk77 gab), und das auch nur .. naja, wie in Java eben. Diese Trägheit der Modernisierung hat ja Martin Odersky (immerhin Java-Compilerbauer und Erfinder der Java-Generics) dazu gebracht, Scala zu entwickeln (die übrigens eine Hybridsprache auf OOP und FP ist).
      Da ich die imperativen Sprachen (und die durch sie bei Entwicklern induzierten Denkschemata) ohnehin in einer immer komplexer, verteilter und nebenläufigeren Welt als nicht mehr angemessen leistungsfähiges Werkzeug betrachte, werbe ich für Sprachen, die solche Beschränkungen und Pitfalls wie veränderliche Zustände (wer schon mal versucht hat, wirklich stabilen Multi-Threading-Code zu schreiben, wird wissen, was ich meine) erst gar nicht haben. Wir vergleichen hier also Äpfel mit Birnen: FP-Sprachen (jedenfalls die besseren) haben a priori einen viel höheren Reifegrad (nennen es stabileres philosophisches Fundament, wenn Du magst) als imperative. Ich würde vielleicht nicht soweit gehen von Dinosauriern und Insekten zu sprechen, aber in meinem Elixir-Artikel erläutere ich, warum ich dort die Zukunft sehe (für massive verteilte und nebenläufige Systeme – für prozedural beherrschbare Monolithen wird es C#, Java und Co. noch lange geben – wir haben ja auch noch viel COBOL im Einsatz).
      Zu Deinen anderen Punkten:

      Ist die Stacktiefe nicht begrenzt?

      Nicht, wenn man Compiler richtig entwickelt und nutzt. Wegen der extrem häufigen Nutzung von Rekursion (nicht nur für Zahlenprobleme) haben FP-Compiler eine Optimierung für die sogenannte Tail-Recursion. Das bedeutet, wenn der rekursive Ausdruck der letzte in der Funktion ist, wird er nicht über den Stack aufgelöst. Nur so ist die Tiefe und Geschwindigkeit erreichbar. Auch andere Konstrukte dienen diesem Anspruch: Z.B. arbeiten FP-Sprachen typischerweise mit LinkedLists, die für viele Operationen konstante, für andere lineare Ausführungszeit bieten. Eine Option, die imperativen Sprachen nicht zu Gebote steht, da dort an allen Ecken und Enden die Daten geändert werden.

      Kurzer und Ausdrucksstarker Code

      Mit einfach und ausdrucksstark meine ich, dass der Code keine überflüssigen und redundanten Boilerplate-Konstrukte enthält wie z.B. Schleifen und lokale Variablen, um Summen über Datenmengen zu errechnen (s. meinen Artikel zu Reducern). So etwas verwässert nämlich nicht nur die Essenz des Codes, sondern führt auch unnötige (und häufig die einzigen) Fehlerquellen ein. Guter Code sollte sich fast wie eine Spezifikation lesen (“Was” statt “Wie”, halt).
      Der Wahnsinn, der vor allem im Umfeld der C-Familie teilweise Kult war, nämlich möglichst kompakten kryptischen Code zu schreiben, ist das Gegenteil von “ausdrucksstark”. Es mag beim Schreiben bei Cracks zu mehr Effizienz führen, aber spätestens in der Wartung und beim Debugging ist es Unsinn. Und Code wird nun mal viel häufiger gelesen als geschrieben. Programmierer, die sich einer CleanCode-Haltung verweigern, haben in einem professionellen Umfeld nichts verloren. Was ich mir unter sprechendem Code vorstelle, habe ich ja ausführlich mit Beispielen dargestellt.

      wird sich reine FP nicht halten lassen (strukturierte Daten)

      Ich weiß nicht, was Du hier mit “strukturierte Daten” meinst. Beliebig komplexe Datenstrukturen sind in der FP genauso wie in der Mathematik ein übliches Ausdrucksmittel. Das Fatale an der OOP war ja, die Struktur zu sehr mit dem Verhalten zu verheiraten. Ein Fehler, den man teilweise korrigiert hat, als man sich von der Vererbungshörigkeit gelöst hat.
      Nein, wenn ich ein Hindernis sehe, dann bei der Hauptstärke, nämlich der Beschränkung auf Nebeneffektfreiheit bei puren Funktionen. Und wenn man z.B. Haskell programmiert, gehört IO zu den Punkten, wo es hässlich wird. Viele Leute erschaudern, wenn der Begriff Monade fällt.
      Aber auch das ist eigentlich kein Problem: Bei der richtigen philosophischen Betrachtung ist auch eine Datenbank nichts anderes als eine Sammlung von Funktionen, die einen (konsistenten) Datenzustand in einen anderen transformiert (Grundidee der Pattrens Event Sourcing und CQRS). Und wenn man so darüber nachdenkt, versteht man plötzlich, wie eine Architektur wie Onion aka Ports & Adapters gedacht ist. Und warum die Datenbank dort nicht innen, sondern außen ansetzt und nach innen in die Fachlogik nur Funktionen gibt. Saubere fachlich motivierte Funktionen statt der Abhängigkeit von Pseudo-Abstraktions-APIs wie JDBC, Hibernate o.ä. Finde ich gut (und ausdrucksstark).
      Gerne würde ich das alles gelegentlich detaillierter mit Dir besprechen. Vielleicht magst Du am Freitag zu meiner Hands-On-Session auf der ExKon kommen. Gerne können wir die Diskussion auch abends bei einem Bierchen vertiefen…
      Viele Grüße,
      Pul

      • Avatar Matthias says:

        Hallo Michael,
        danke für die sehr ausführliche Antwort.
        Am Freitag wird sich sicherlich Zeit finden 🙂 Daher fasse ich mich hier kurz.
        Ich sehe hinter der Freiheit sich in den oben genannten Beispielen nicht von Typbreiten einschränken zu lassen, eine Bibliothek die einem diese Verantwortung abnimmt (wenn die Prozessoren es nicht anders können, muß es Code geben der es tut).
        Oder: Wenn ich es nicht im Programm tun muß, wird dann pauschal immer die BigNumber Arithmetik verwendet? Damit wäre dann immer ein Overhead im Boot, der sonst nur explizit für Sonderfälle zum Einsatz kommt.
        Reifegrad vs. Paradigma:
        Es ist auch nicht meine Absicht gewesen FP zu zerreden, ich wollte nur anmerken, daß manche Konzepte aus dem Artikel nicht unbedingt aus FP erwachsen (CleanCode/Rekursion geht auch mit OOP). Auch neuere nicht-rein-FP Compiler haben dazugelernt (bspw. Rust).
        Mancher mag eine Menge Steuerzeichen “()[]{} -> |>” als weniger deutlich auffassen als ein als Text lesbarer Code.
        Ich neige dazu, daß es ein Fehler ist, ein Paradigma über das andere zu stellen. Die Herausforderung für ein Entwicklungsprojekt sollte sein “Welcher Ansatz ist der geeignete?”, statt “Wie begründe ich FP?”.
        Kryptisch C:
        ?: und ?? Operatoren gehören sicher nicht in die Kategorie. Sportlichen Ehrgeiz Code unleserlich zu gestalten hatte ich hier nicht im Sinn.
        Grüße,
        Matthias
        PS: “Ich bin besser als du.” war schon in Monkey Island die schlechteste aller Antworten (FP über OOP).

  • Avatar Michael "Pul" Paulsen says:

    Danke Dir auch, Matthias!
    Da ich diese Überlegungen als von allgemeinem Interesse halte, lass mich vorgängig zu Freitag hier antworten – leider kann ich das nicht so kurz wie Du.
    > Die Herausforderung für ein Entwicklungsprojekt sollte sein „Welcher Ansatz ist der geeignete?“, statt „Wie begründe ich FP?“.
    Du bringst es auf den Punkt! Genau um diese Frage geht es. Und die wird nach meiner Erfahrung zuwenig gestellt in einer Branche, in der zu viele zu arglos zum vermeintlich gesetzten OOP greifen – und zwar oft, weil sie noch nicht einmal wissen, dass und welche Alternativen existieren (das gleiche gilt für andere Technologien; es soll Leute geben, die unabhängig vom Anwendungsfall automatisch ein RDBMS wählen, wo vielleicht eine Graph-Datenbank oder ein Document-Store angemessener und viel leistungsfähiger wären).
    Das Bessere ist des Guten Feind. Das war schon immer so, mit allen Folgen, u.a. dem Reflex der Verteidigung, wo gar kein Angriff stattfand. Dass bei Dir als “Wie begründe ich FP?” rüberkommt, was als Werbung für eine angemessene Technologie gemeint ist, hat sicher zwei Hintergründe: Zum einen begeistert mich, dass es auch nach fast 35 Berufsjahren immer noch Produktivitätssprünge möglich sind, mit denen ich nicht gerechnet habe (vor 20 Jahren habe ich das gleiche Hohelied auf OOP gesungen, und damals auch zu Recht). Zum anderen musste ich schon sehr häufig in besagter Karriere erleben, dass Leute aus Bequemlichkeit oder Angst (meist beides) auf vermeintlich bewährten Technologien hängen geblieben sind und diese Quantensprünge verschlafen haben. Deswegen neige ich vielleicht dazu, Vorteile übermäßig zu betonen. Es ist nicht mein Job, Werbung zu machen für etwas, das sich früher oder später ohnehin durchsetzen wird (so wie OOP mit 20 Jahren Verspätung in den Mainstream kam). Mein Job ist es, meinen Kunden (und Kollegen) vorteilhafte Wege früher aufzuzeigen. Aber Du musst keine Sorge haben: Noch in 20 Jahren wird es jede Menge OOP geben, aber halt auch deutlich mehr FP.
    Was machen wir aus Deiner Forderung? Unsere Aufgabe als Berater ist es eben auch, unsere Kunden von suboptimalen Lösungsansätzen allmählich zu besseren zu führen; auch um zu vermeiden, dass “Java das neue Cobol” wird, denn gerade in der Digitalisierung kann sich unsere Volkswirtschaft keinen weiteren Rückstand leisten (glaube mir, ich weiß, wovon ich rede, nachdem ich 5 Jahre die IT-Leitung eines Hauses hatte, das u.a. in 2000 z.T. 40 Jahre alten Cobol-Programmen regelrecht gefangen war – und den dazugehörigen extremen Betriebskosten).
    Auch das Werben für sinnvolle Innovation gehört für mich zu unserem Claim “Qualität sichert Erfolg”. Aber selbstverständlich darf eine neue Technologie nie als Selbstzweck eingeführt werden, sondern immer – wie Du schön ausführst – unter dem Aspekt der Aufgabenangemessenheit (und der Ausgereiftheit; gerade dieser Aspekt begeistert mich ja an der Kombi Elixir/Erlang/OTP so).
    Deswegen stellte ich ja mehrfach ausdrücklich heraus, dass ich FP (und andere Ansätze wie z.B. Erlangs superstabiles Aktorensystem mit seinem “Let it crash!”-Ansatz) besonders für massiv parallele, verteilte und nebenläufige Systeme, wie wir sie im IoT, bei Mobilität, autonomen Fahren und ähnlichen Domänen bekommen werden, als den aktuell geeignetsten Ansatz halte (in Kombination mit anderen Technologien wie Reactive Streams). Dort haben m.E. JEE oder .NET-Ansätze keine Chance. Gleichzeitig räume ich ein, dass für eher abgeschlossene Systeme OOP (das ich ja selbst seit den frühen 90ern praktiziere, bin allerdings heilfroh, dass ich zwischen C++ und Java SmallTalk kennenlernte und damit lupenreines OOP) weiterhin seine Berechtigung hat. Hier könnte man, muss aber nicht unbedingt auf FP umschwenken, weil es nur vergleichsweise geringe Vorteile bringt (und man ja immer auch die Legacy-Basis an Code und Entwicklern in Betracht ziehen muss).
    Ich möchte auch noch ergänzen, dass es innerhalb der OOP große Qualitätsunterschiede gibt, wie ich bei meinen eigenen Teams immer wieder feststellen musste: Wer sich die Mühe gibt, mit final-States u.ä. zu arbeiten, kann sehr nah an FP-Idealen arbeiten (auch wenn man die Möglichkeit, Funktionen höherer Ordnung zu schreiben, mit OOP-Patterns wie Stategy nur bedingt und mit sehr viel Coding-Aufwand emulieren kann; so ist es bei gutem Design-Gespür doch machbar), wird z.B. mit Nebeneffekten deutlich weniger zu kämpfen haben als jemand, der für seine Klassen ganz automatisch getter und setter-Methoden generieren lässt (was die IDEs ja so gefährlich einfach machen) und damit ein zentrales Prinzip der OOP (nämlich Encapsulation bzw. Information hiding) verletzt, bloß weil gewisse Frameworks das verlangen. Eigentlich war die verdammte Java Bean der Anfang vom Ende sauberen OO-Designs.
    Es ist also gewiss richtig, dass nicht das eine Paradigma absolut besser ist als das andere. Aber man muss in Betracht ziehen, dass Rahmenbedingungen sich ändern – und zwar massiv. Und gerade Deinem Argument folgend, muss man dann auch die Notwendigkeit zu einem Paradigmenwechsel erkennen. Und so, wie die alte Idee der neuronale Netze eine massive Renaissance erfährt, weil uns plötzlich die unglaubliche Rechenpower zu Gebote steht, die vor 40 Jahren noch völlig utopisch erschien, hat sich durch das Aufkommen von Multi-Core-Prozessoren und omnipräsenter Vernetzung nicht nur Moores Law und die Schlussfolgerungen daraus grundlegend verändert:
    Durch den Wandel von vertikaler zu horizontaler Skalierung sowie eine Abkehr von Monolithen zu verteilten Services haben sich auch die Rahmenbedingungen für die Auswahl des geeigneten Paradigma so massiv geändert, dass man das in den 90er und 2000er Jahren unangefochtene Paradigma der OOP heutzutage deutlich kritischer betrachten muss. Das gilt ja übrigens auch wiederum für Datenbanken. Dass wir heute als Architekten selbstverständlich über das CAP-Theorem sprechen (und es überhaupt kennen), belegt das schon: Während früher die transaktionale Qualität von Datenbanken (also das C) unangefochtenes Requirement war (und in Branchen wie Banken aus gutem Grunde auch bleiben muss), ist in den meisten Anwendungsfällen heute eher BASE als ACID das Datenbank-Gebot der Stunde, weil in weltweit von Millionen Menschen genutzten Services Verteilung und Verfügbarkeit zentrale Anforderungen sind, während jederzeitige übergreifende Konsistenz in diesem Rahmen ohnehin eine informationstheoretische Schimäre ohne Praxisrelevanz darstellt und eine “eventual consistency” allemal ausreichend ist. In einer asynchronen, verteilten Welt taugen auf Synchronität und zentrale Regelung angelegte Mechanismen nicht mehr optimal.
    Mir geht es mit diesen Blogartikeln nur darum, für diese geänderte Situation Bewusstsein zu schaffen. Es ist nicht meine Absicht, eine Technologie oder ihre Vertreter als Verlierer aus dem Ring zu schicken. Dazu habe ich selbst schon oft genug mein Wissen umwälzen und Irrwege erkennen müssen. So gesehen, halte ich Dein PS als unserem professionellen Gedankenaustausch nicht angemessen. Vielleicht hast Du was in den falschen Hals gekriegt. Sollte ich mich “ehrverletzend” ausgedrückt haben, tut es mir leid.
    Bis Freitag,
    Pul

    • Avatar Matthias says:

      Hallo Michael,
      wow! Das war umfassend.
      Das PS hätte ich gleich nachdem Submit gern wieder gelöscht. Allein schon desswegen, weil es nicht zutrifft. Deine Argumentation weit über das genannte Zitat hinaus. Leider geht mir hier der Edit Button ab.
      Ich bin schlicht auf der Suche nach einem passenden Zitat falsch abgebogen. Daher muß ich mich entschuldigen.
      Abgesehen davon ist nichts gegen gut gemachte Werbung einzuwenden 🙂
      Und Werbung hat es nunmal an sich, die Vorteile einer Neuerung zu betonen. Heutzutage gibt es einfach sehr viele Hypes, so daß ich mir eine gewisse Skepsis angewöhnt habe.
      Wie schon geschrieben finde ich ja einiges an FP durchaus sinnig, hilfreich, elegant… ich tue mich nur bewußt schwer von einem komplett auf das andere Pferd zu wechseln.
      Viele Grüße,
      Matthias

  • Avatar Michael "Pul" Paulsen says:

    Hallo Matthias,
    kein Grund sich zu entschuldigen. Wer wie ich selber ja gerne mal eine stramme verbale Volte setzt, muss auch einstecken bzw. parieren können.
    Skepsis ist ggü. Hypes definitiv angebracht. Bzw. sollte man prüfen, was nur ein Hype ist (SOA z.B. war eine große Gelddruckmaschine für Middleware-Anbieter und in Hinblick auf seine Ziele ggf. sogar kontraproduktiv), bzw. was sich in der Praxis herausge”mendel”t hat. REST z.B. ist deshalb so erfolgreich, weil es keine Kopfgeburt war, sondern weil Fielding im Nachgang erfolgreiche Muster aus dem Internet abgeleitet hat. Und Elixir traue ich zu, mehr als ein Hype zu werden, da es die theoretisch belegbaren Stärken der FP mit sehr moderner Syntax und tollem Tooling (sprich: Entwicklerproduktivität und -akzeptanz) mit der stabilsten Basis kombiniert, die ich kenne: Erlang, das seit Mitte der 80er riesige Telefonnetze mit verteilten, superstabilen Aktorsystemen betreibt (s. hierzu meinen Beitrag zu Elixir).
    > ich tue mich nur bewußt schwer von einem komplett auf das andere Pferd zu wechseln
    Da hast Du recht. Und das sollte man sowieso nie tun, sondern immer schrittweise und gerne auch mal experimentell (z.B. mit Spikes), indem man geeignete Teile in FP implementiert, ohne insgesamt sein Ökosystem zu verlassen (das wäre ja Wahnsinn und ist meistens weder organisatorisch noch ökonomisch darstellbar). Aber Einzelteile lassen sich locker einbinden, z.B.
    – in Java mit Scala (hervorragende Interop)
    – in C#/.NET mit F# (selbst noch nicht gemacht, ich vermute aber, dass MS das gut gelöst hat)
    – in Service-Architekturen hat man eh mehr Freiheit, da kann man dann auch mal mutig sein und Services z.B. mit Elixir/Phoenix (hervorragender und unfassbar schneller Webserver in Pipes & Filter-Architektur, vor kurzem wurden 2 Mio. aktive Websockets auf einem einzigen Server bedient)
    Behalte Dir also gerne Deine gesunde Skepsis bei. Das ist allemal besser, als jedem Trend sofort hinterher zu laufen. Außerdem freut sich jeder ernsthafte Missionar über einen advocatus diavoli, der ihn zwingt und ihm die Möglichkeit gibt, seinen Glauben detaillierter darzulegen 😉
    Viele Grüße
    Pul

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Teile diesen Beitrag:

Share on facebook
Share on linkedin
Share on twitter
Share on xing
Share on whatsapp
Share on google
Share on email
Share on print