Blogbeitrag:

Reduzieren reduziert Aufwände

Wer gerne kocht, weiß: Das Geheimnis einer guten Soße liegt im Reduzieren, durch das die geschmackliche Essenz stärker zu Tage tritt. Eine ähnliche Technik gibt es bei der Funktionalen Programmierung: Reducer oder Reduce-Funktionen reduzieren Datenmengen auf einzelne Werte (z.B. Summen, Durchschnitte, Statistik). Für viele FP-Anfänger sind Reducer ein Buch mit sieben Siegeln. In diesem Beitrag möchte ich zeigen, dass sie eigentlich ein sehr einfaches und naheliegendes Konzept sind.

Wie schon bei der Einführung in FP verwende ich für die Codebeispiele Elixir, weil ich diese Sprache für elegant und selbsterklärend halte.

Wie summiert man ohne Schleifen und Variablen?

Denken Sie an eine ziemlich einfache Aufgabe: Um eine Anzahl bestellter Artikel in einem Auftrag zu ermitteln, müssen Sie die bestellte Anzahl aller Positionen aufaddieren. Schauen wir uns an, wie man ein solches Problem imperativ gelöst hat, z.B. in Java:

// Aufruf an einer Instanz der Klasse Auftrag
int anzahl = auftrag.summeBestellteArtikel();
// Methode in der Klasse Auftrag
public int summeBestellteArtikel() {
    int summeBestellteAnzahl = 0;
    iterator it = positionen.iterator();
    while(it.hasNext()){
        summeBestellteAnzahl += it.next().getBestellteAnzahl();
    }
    return summeBestellteAnzahl;
}

Eine Menge Code! Bevor es generische Klassen und Iteratoren gab, wäre das noch mehr gewesen. Und noch dazu unsicher durch die Verwendung veränderbarer Zustände, schlecht testbar usw.  Dennoch ist zu befürchten, dass die Mehrzahl der SW-Entwickler das immer noch für den richtigen Weg zur Lösung hält.
Eigentlich möchte man das Konzept „summiere die bestellte Anzahl aller Positionen“ doch auch einfacher im Code ausdrücken können. Schauen wir, wie man das in FP schreiben könnte:

// Aufruf durch Piping
anzahl = auftrag |> positionen |> summeBestellteAnzahl
// Funktion für eine beliebige Liste von Positionen
def summeBestellteAnzahl( positionen ) do
    Enum.reduce( positionen, 0, &( bestellteAnzahl(&1) + &2 )
end

Das ist noch nicht der „richtige“ bzw. eleganteste Weg (der kommt weiter unten). Ich wollte sie zunächst an reduce heranführen.
Zunächst fällt die völlig andere Codestruktur auf: FP arbeitet statt mit Klassen mit Datenstrukturen und Funktionen, die sie umwandeln. Wie und warum und wie die Pipe oben funktioniert, erläutere ich hier. auftrag ist eine Datenstruktur, die den auftrag enthält. Im nächsten Schritt wird diese durch postionen in eine Menge Positionen transformiert (wie, spielt hier keine Rolle, im einfachsten Fall sind die Positionen wie bei der OOP-Klasse Bestandteil der Stuktur von auftrag). Anschließend wird für diese Menge mit einer entsprechenden Funktion die Summe der bestellten Artikel berechnet. (Die Zuweisung zum Alias anzahl ist hier nur erfolgt, um es analog zum Java-Beispiel zu halten, wäre in FP aber eigentlich unüblich.)
Entscheidend ist aber die Implementierung von summeBestellteAnzahl: Aus den 6 Zeilen des Bodys der Java-Funktion ist eine geworden: eine reduce-Funktion.

Reduce macht aus vielen Werten einen

Nur etwas Theorie. Schauen wir uns die Funktionssignatur von reduce an:

reduce: (Enum a) -> b -> ((a,b) -> b) -> b

reduce erhält eine Menge des Datentyps a (unsere Positionen)  und transformiert sie in einen Wert des Datentyps b (hier Int). Sie macht das durch Akkumulation („An-/Aufhäufung“). Dafür braucht sie zwei weitere Parameter: Den Initialwert des Akkumulators vom Typ b (bei Summen typischerweise 0) und eine Akkumulatorfunktion.  Diese bekommt wiederum zwei Parameter: ein Element der Menge vom Typ a, also hier jeweils eine Position, und den Wert des Akkumulators. Ihre Aufgabe ist es, den Wert des Akkumulators durch Akkumulieren des Elements zu ermitteln (hier, indem sie die bestellte ANzahl der jeweiligen Position zur bisherigen Summe dazu addiert).  Das wird vielleicht deutlicher, wenn wir die Akkumulatorfunktion aus dem obigen Beispiel etwas klarer hinschreiben:

// Statt &( bestellteAnzahl(&1) + &2 ) hätten wir auch schreiben können:
..., 0, fn position, summe -> bestellteAnzahl(position) + summe end)

Die reduce-Funktion macht also (natürlich) inhaltlich das gleiche wie die Schleifenimplementierung in Java: Sie ruft rekursiv die Akkumulatorfunktion mit den Elementen auf, bis diese aufgebraucht ist, und antwortet dann den akkumulierten Wert. Und sie macht das rein funktional, ohne (unsichere) Variablen und ohne überflüssigen Schreibaufwand – und wiederverwendbar. 

Das ist noch nicht funktional genug

Wenn man schon etwas mehr FP-Erfahrung hat, stört einen womöglich, dass sowohl der Aufruf als auch die Funktion summeBestellteAnzahl nicht wirklich eleganter FP-Code sind. Man sollte immer versuchen, atomare Funktionen zu schreiben und wiederzuverwenden. Was wir eigentlich schreiben wollen, ist:

auftrag
 |> positionen
 |> Enum.map &(bestellteAnzahl(&1))
 |> sum
// möglich ist das durch die Existenz einer generischen Funktion sum
def sum( werte ), do: Enum.reduce( werte, 0, &+/2 )

Hier sehen wir wieder den Vorteil von FP-Code: Ausdrucksstärke. Die Pipe oben beschreibt nämlich genau, wie laut Spezifikation die Gesamtanzahl definiert ist: Nimm vom Auftrag die Positionen, von diesen die bestellte Anzahl und summiere das auf. Voila! Einfacher kann Code nicht sein. Schwieriger sollte er nicht sein.

Reduce ist so mächtig wie map

Dem einen oder anderen imperativen Programmierer mag das alles als zu umständlich erscheinen, um bloß eine Summe zu berechnen. Natürlich wäre der Lernaufwand unverhältnismäßig, nur um ein paar Zeilen Code zu sparen. Aber gehen Sie mal vor ihrem geistigen Auge typische Java- oder C++-Programme durch: 60-80% des Codes bestehen typischerweise aus solchem imperativen Boilerplate-Code wie Schleifen, Zählervariablen etc.
Der eigentliche Witz bei FP-Funktionen ist ja ihre vielgestaltige Einsetzbarkeit, weil sie Funktionen als Parameter erhalten können (Sie erinnern sich: man spricht von „Funktionen höherer Ordnung„). Und das macht sie universell wiederverwendbar in einem Maße, wie es OOP nie erreicht hat (weil jeder Wiederverwendungsersparnis immer auch Mehraufwand wie abstrakte Klassen, Interfaces u.ä. erforderte, was den Code oft zusätzlich unlesbarer machte). Hier als Beispiel drei sehr häufig verwendete Funktionen zum Transformieren von Mengen:

  • map kann eine Menge  von Typ a in eine gleichmächtige Menge von Typ b umwandeln und beötigt dazu eine Funktion a -> b  (a und b können gleich sein)
  • filter kann von einer Menge eines Typs eine Untermenge des gleichen Typs bilden und benötigt dazu ein sogenanntes Prädikat, eine Funktion a -> Boolean
  • reduce transformiert eine Menge von a auf einen Wert von b und benötig dazu zwei Parameter, den Initalwert von b und die Akkumulatorfunktion (a, b) -> b

Wenn Sie dieses Grundprinzip verinnerlicht haben (und das geht ziemlich schnell, wenn man nicht an Schleifen, Variablen und Boilerplate hängt), werden Sie verblüfft sein, wie häufig sich diese funktionalen Lego-Bausteine in beliebigen Kombinationen einsetzen lassen (die o.g. Funktion sum: Enum Int -> Int lässt sich zum Summieren jeder Menge von Zahlen verwenden) und wie einfach man selber welche daraus bauen kann. Das ist die Magie der funktionalen Programmierung!
Wie leicht man eine Funktion wie reduce auch selbst implementieren kann, haben dieses Muster im ersten Beitrag zur FP bereits als Listenrekursion kennengelernt:
reduce für eine Liste, einen Akkumulator  und eine Akkumulationsfunktion ist

  • der Akkumulator, wenn die Liste leer ist (Basisfall)
  • reduce für den Rest der Liste mit dem Wert der Akkumulationsfunktion für das erste Element und den bisherigen Akkumulator (gibt einen neu akkumulierten Wert), wenn die Liste mindestens ein Element (head) und restliche (tail) enthält
def reduce([], acc, _fnAcc), do: acc
def reduce([head|tail], acc, fnAcc), do: reduce( tail, fnAcc(head,acc), fnAcc)

Hieraus wird deutlich, dass sich Enum.reduce weder für den Typ a noch b interessiert, sondern es komplett fnAcc überlässt, die Elemente in acc zu aggregrieren. Das heißt: Natürlich kann Typ b bei Enum.reduce eine beliebig komplexe Datenstruktur sein, z.B. eine Statistik oder auch eine andere Menge. Konkrete Beispiele dafür werde ich mir für ein anderes Mal aufheben.
Zum Abschluss daher hier nur ein weiteres einfaches Beispiel. Was es macht, sollte selbst erklärend sein:

// Wenn ich statt Summen häufiger maximale Werte berechne, brauche ich
def max( wert1, wert2 ) when wert2 > wert 1, do: wert2
def max( wert1, _ ), do: wert1
def max( werte ), do: Enum.reduce( werte, LOW_VALUE, &max/2 )

Hinweis: Normalerweise hätte man dieses max auch als Listenrekursion implementieren können. Ich wollte Ihnen hier nur zeigen, wie man reduce problemlos für andere Dinge als Summen verwenden kann.

Schreiben Sie Code, den Sie auch nächstes Jahr noch verstehen

Wie ich schon mehrfach betonte, ist FP kein Selbstzweck, sondern führt zu einfachem, sicheren und ausdrucksstarken Code, der massiv die Produktivität der SW-Entwicklung erhöhen kann, denn: Boilerplate-Code wie der eingangs gezeigte muss nicht nur geschrieben, sondern auch getestet und gelesen werden – und erfordert häufig genug Debugging, weil mehr Code mehr Fehlermöglichkeiten bedeutet; welcher Programmierer hat z.B. noch nie versehentlich die Summenvariable in der Schleife neu definiert und sich dann gewundert, dass die äußere nicht aufaddiert wird?
Ich kann Sie nur ermutigen, sich allmählich an dieses Paradigma der Zukunft anzunähern. In den nächsten Jahren wird FP die imperative Programmierung (inkl. OOP) verdrängt haben (von der Java-Legacy abgesehen, die uns natürlich als Ballast erhalten bleiben wird wie die COBOL-Systeme, die in den 60er bis 80er Jahren eingeführt wurden). Das ist nicht nur wegen der Produktivitätsvorteile und des Fachkräftemangels zwangsläufig, sondern auch eine technische Notwendigkeit, weil imperative Programme einfach für die Herausforderungen massiv verteilter und nebenläufiger Systeme ungeeignet sind, wie Digitalisierung und IoT sie erfordern.
Wenn Sie Hilfe brauchen oder sich einfach nur austauschen wollen, sprechen Sie mich bitte jederzeit an oder benutzen Sie die Kommentarfunktion. Ich freue mich auf das Gespräch mit Ihnen.
 
Michael_Pul_Paulsen_.png

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

Print Friendly, PDF & Email

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