Functional programming – Teil 5
Linearen Datenfluss haben wir jetzt scheinbar einigermassen im Griff.
Die meisten Lösungen, die wir produzieren sind aber nicht geradlinig sondern verzweigt und verzwickt.
Angefangen bei schlichten NULL-Checks über Validierung von Eingaben bis hin zu Verzweigungen in der Geschäftslogik.
Es gibt eine ganze Reihe von alternativen zu if/else Verzweigungen oder einem switch/case. Gar nicht selten sind diese Varianten sogar leichter zu erfassen.
Menschen sind für gewöhnlich wesentlich besser darin ein Konzept zu begreifen als einer nicht-linearen Verzweigungslogik zu folgen.
Micro-Branching
Hierbei handelt es sich um eine Form der bedingten Logik, das keinen Einfluss auf die Reihenfolge der Ausführung innerhalb einer Funktion hat.
Der Ternary Operator
Das kommt einem if/else noch am nächsten. Kennen und nutzen wir alle hin und wieder.
gameOver ? stageOne() : nextStage()
Guards und Defaults
Dabei geht es um den Einsatz von logischen Operatoren als guard (&&
) oder als default (||
).
Zugegeben, das geht nicht mit jeder Sprache so gut wie bei JavaScript aber, dort wo man es einsetzen kann lassen sich einige Verzweigungen sehr leicht und verständlich abbilden.
// use port 3000 when port is false-ish
server.listen(port || 3000)
// do not use console unless it is available
window.console && console.log(message)
Nicht-verzweigte Strategien
Hierbei werden Verzweigungen als eigenständige Blöcke behandelt, was sie ja auch sind, und vom aufrufenden Code getrennt.
Für den aufrufenden Code werden die verschiedenen Alternativen dann in eine geeignete Struktur gefasst.
Dispatch tables
Eine Alternative zum switch/case. Im Prinzip wird statt des switch
eine hash table erzeugt, welche auf den zugehörigen callback zeigt.
const userHandlers = {
Created : onUserCreated,
ChangedEmail: onUserChangedEmail,
MovedTo : onUserMovedTo,
default : onUnexpectedEvent
}
const handle = when => (state, {name, payload}) => (when[name]||when.default)(payload, state)
const userEvents = [
{ name: "Created" , payload: {/*...*/}},
{ name: "MovedTo" , payload: {/*...*/}},
{ name: "SwitchedGender", payload: {/*...*/}},
]
const user = userEvents.reduce(handle(userHandlers), {})
Der Either Functor
Dieser Functor besitzt, genau wie jeder andere, eine map
Funktion besteht aber eigentlich aus zwei verschiedenen Sub-Typen Left
und Right
.
const Left = x => ({
map : fn => Left(x),
fold: (onLeft, onRight) => onLeft(x)
})
const Right = x => ({
map: fn => Right(fn(x)),
fold: (onLeft, onRight) => onRight(x)
})
Die map
Funktion von Right
verhält sich ganz wie erwartet und führt die übergebene Funktion mit seinem Wert als Argument aus und packt das Ergebnis in ein neues Right
.
Die map
Funktion von Left
hingegen weigert sich standhaft irgend eine übergebene Funktion auf seinen Wert anzuwenden und gibt einfach ein neues Left
zurück.
Die fold
Methode von beiden erwartet zwei Funktionen, eine wird verwendet, wenn es sich um ein Left
handelt, die andere wird von Right
verwendet.
Praktischer Nutzen
Angenommen, wir sollen nur dann einen Datensatz schreiben, wenn er plausibel ist. Sind die Daten nicht plausibel, soll nichts passieren ausser der Anzeige einer Fehlermeldung.
const fromValidation = ({payload, errors}) => errors ? Left(errors) : Right(payload)
const result = fromValidation(validate(account))
.map(withdrawMoney)
.map(notifyAccounting)
.fold(onError, onSuccess)
Dadurch, dass wir im Fehlerfall ein Left
zurückgeben, können wir völlig sorglos so oft map
nutzen wie wir wollen (oder müssen) und können sicher sein, dass nichts davon ausgeführt wird, wenn die Daten ungültig sind.
Darum müssen wir uns erst wieder Gedanken machen, wenn wir die Transaktion mit fold
abschliessen.
Ein anderes Szenario ist der gute alte NULL-Check.
const fromNullable = x => x != null ? Right(x) : Left(null)
const getMiddleName = ({middleName}) => fromNullable(middleName)
const result = getMiddleName({
firstName: "Karl",
lastName: "Kettenkit"
})
.map(name => name.toUpperCase())
.fold( _ => "You have no middle name",
name => `Your middle name is ${name}`)
// "You have no middle name"
Hier haben wir einerseits sicher gestellt, dass toUpperCase
nicht angewendet wird, wenn der Wert nicht existiert.
Ausserdem muss zusätzlich eben dieser Fall in fold
berücksichtigt werden.
Keine Kommentare