Functional programming – Teil 3
Ok. Jetzt haben wir uns einen groben Überblick darüber verschafft wie funktionale Programmierung auf… erm, ja… Funktionsebene funktioniert.
Allerdings haben wir es auch bei diesem Stil mit einer ganzen Reihe von Typen zu tun.
Sehen auf den ersten Blick zwar aus wie Objekte und implementieren auch ein oder mehrere Interfaces. Sie sind aber beileibe nicht das, was man sich im OOP Sinne unter Klassen vorstellen würde.
Fantastic terms and where to find them
Bei den Typen von denen eben die Rede war handelt es sich um sogenannte algebraische Strukturen. Für den normalsterblichen Programmierer würde ich eher von einem generalisierten Interface oder etwas in der Art sprechen wollen. Wie dem auch sei…
Funktionale Programmierung driftet spätestens hier konzeptionell drastisch vom OOP Prinzip ab.
Anstatt ganz konkrete Dinge, Prozesse oder ähnliches zu formulieren (DDD anyone?) wird bei funktioneller Programmierung der verallgemeinernde mathematische Zweig und dessen Terminologie bevorzugt.
Tatsächlich verbergen sich hinter den meisten fremdartigen Begriffen dann aber doch eher einfache, wenngleich mächtige Konzepte. Zumindest für jemanden wie mich, der Mathe im Abi abgewählt hat.
Was mich zu der Feststellung führt, dass der Versuch die FP-Terminologie so weit wie möglich aussen vor zu halten eher vergebens sein wird.
Na gut, dann versuche ich zumindest die Begriffe auf ihren praktischen Nutzen herunter zu brechen.
Functors
Im Grunde nichts anderes als ein Container mit einer map
Funktion, der einen beliebigen Wert umschliesst und mir damit direkten Zugriff auf diesen Wert verwehrt.
const Bag = x => ({
map: fn => Bag(fn(x))
})
Als higher order function erwartet map
als parameter eine weitere Funktion welche mit dem Wert innerhalb des Containers aufgerufen wird. Das Ergebnis hiervon wird nun wieder in einen neuen Container gepackt.
Tatsächlich muss ein Functor garantieren, dass map
einen neuen Functor zurück liefert.
Dadurch kann ich map
beliebig oft aneinander hängen. Genau wie bei compose
oder pipe
ermöglicht mir das composition.
Wir können das composition Beispiel von Teil eins auch hier verwenden.
Originalbeispiel
const upper = str => str.toUpperCase()
const ask = str => `${str}?`
const askLoudly = composeTwo(ask, upper)
askLoudly("will it blend") // WILL IT BLEND?
Functor Variante
Bag("will it blend").map(upper).map(ask) // Bag('WILL IT BLEND?')
Der Wert in unserem Container ist nun gleichermaßen umgeformt. Aber eben wieder in einem neuen Container, der uns direkten Zugriff auf den Wert verweigert.
In der Regel bietet jeder dieser Container-Typen wenigstens eine Methode an, um den bloßen Wert aus dem Container auszupacken.
const Bag = x => ({
map : fn => Bag(fn(x)),
fold: fn => fn(x)
})
Bag("will it blend").map(upper).fold(ask) // 'WILL IT BLEND?'
Mit diesen beiden Methoden können wir das Ergebnis des composition Beispiel genau nachbilden.
Bordmittel
Würde man jetzt behaupten, dass ein JavaScript Array auch ein Functor ist, hätte man völlig Recht damit.
Array.map
macht genau das gleiche wie unser Bag.map
, abgesehen davon, dass die übergebene Funktion für jedes Element ausgeführt wird.
Am Ende stehen wir aber trotzdem wieder mit einem neuen Array von gleicher länge da.
Praktischer Nutzen
Mit Hilfe von Array.map und Array.filter können wir eine ganze Reihe von imperativen Loops ersetzen.
Angenommen, wir bekommen folgende Datenstruktur geliefert und sollen daraus eine Liste mit ISO-Codes erzeugen.
const countries = [
{ "iso": "yt", "name": "Mayotte" },
{ "iso": "mq", "name": "Martinique" },
{ "iso": "gp", "name": "Guadeloupe" },
{ "iso": "cw", "name": "Curaco" },
{ "iso": "ic", "name": "Canary Islands" }
]
Imperative Variante
let isoCodes = []
for(let i = 0; i < countries.length; i++ ) {
isoCodes.push(countries[i].iso)
}
// ["yt", "mq", "gp", "cw", "ic"]
// 101 Zeichen Code
Funktionale Variante
const isoCodes = countries.map(country => country.iso)
// -> ["yt", "mq", "gp", "cw", "ic"]
// 54 Zeichen Code
Die funktionale Variante ist nicht nur um fast die hälfte kürzer sondern, meiner Ansicht nach, auch deutlich ausdrucksstärker was die eigentliche Geschäftslogik angeht.
Bei der Verkürzung handelt es sich auch nicht um syntactic sugar oder ähnliches. Es ist schlicht weniger aktiver Code.
Für mich ist das ein zusätzlicher Vorteil, den Funktionale Programmierung bietet.
Die gesamte aktive Code-Masse verringert sich erheblich weil wir in der Lage sind einen großen Teil des üblichen „Füllstoffs“ nicht schreiben zu müssen.
Keine Kommentare