Functional programming – Teil 5

Intersection

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

Deinen Kommentar hinzufügen

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.