Functional programming – Teil 6
Von den alternativen Kontrollstrukturen, die wir uns letztes mal angesehen haben, war eigentlich nur das Either
streng funktional. Dennoch ist es immer von Vorteil den Code so einfach und erweiterbar wie möglich zu gestalten.
Werfen wir diesmal einen Blick darauf, wie wir mit verschachtelten Container-Typen umgehen.
Pointed functors
Hierbei handelt es sich um einen Typen der einen Typkonstruktor anbietet. Im Grunde nichts anderes als eine Factory-Methode, die es uns erlaubt, unseren Wert in einem Container zu platzieren ohne uns Gedanken über eventuelle Komplexität im Konstruktor machen zu müssen.
Dieses Verfahren nennt sich häufig auch lifting im FP Kontext. Die Methode hierfür heisst für Gewöhnlich of
.
Either.of('abc') // -> Right('abc')
Task.of('abc') // -> Task('abc')
Maybe.of('abc') // -> Just('abc')
Monads
Sehen wir uns hier für noch mal eines der Beispiele von letztem mal an.
const fromValidation = ({payload, errors}) => errors ? Left(errors) : Right(payload)
const result = fromValidation(validate(account))
.map(withdrawMoney)
.map(notifyAccounting)
.fold(onError, onSuccess)
Angenommen dass withdrawMoney
ebenfalls eine Prüfung durchführt und ebenfalls ein Either
zurück liefert. Damit hätten wir unsere Nutzlast zwei Either
tief verschachtelt.
Wir müssen im weiteren Verlauf also zwei mal map
bemühen um an den eigentlichen Wert zu gelangen. Ziemlich unhandlich und verwirrend.
const result = fromValidation(validate(account))
.map(withdrawMoney) // -> Either(Either(account))
.map(withdrawn => withdrawn.map(notifyAccounting)) // 2x map
.fold(onError, onSuccess)
Einheitsfunktion
Um diese Verschachtelung von Typen unter Kontrolle zu halten gibt es die sog. Einheitsfunktion. Diese muss in der Lage sein, zwei gleiche Typen auf einen zu reduzieren.
Der konkrete Name dieser Funktion kann durchaus unterschiedlich sein. Gängig sind jedoch chain
, join
oder flatMap
.
Für unser Right
sieht das folgendermassen aus
const Right = x => ({
chain: fn => fn(x),
map : fn => Right(fn(x)),
// ...
})
Anstatt das Ergebnis von fn(x)
in ein neues Right
zu packen, geben wir einfach das Ergebnis zurück.
Da Left
konzeptionell schlicht jegliche Transformation seines Wertes ignorieren soll, sieht das chain
hier ein wenig anders aus.
const Left = x ({
chain: fn => Left(x),
map : fn => Left(x)
// ...
})
Mit dieser Funktion im Arsenal können wir unsere Komposition wieder deutlich vereinfachen.
const result = fromValidation(validate(account))
.chain(withdrawMoney) // -> Either(account)
.map(notifyAccounting) // 1x map
.fold(onError, onSuccess)
Abhängig davon, was uns die Funktionen in unserer Komposition zurückgeben können wir zwischen map
und chain
wählen.
Wie eingangs erwähnt ist ein Functor der einen Typkonstruktor (of
) und eine Einheitsfunktion (chain
) besitzt ein monad.
Wie bei den anderen algebraischen Datentypen steckt hier noch etwas mehr mathematische Gesetzmässigkeit dahinter.
Left identity (Linksneutral, Linkseins)
Verwende ich chain
auf einer Monade mit einer Funktion die eine gleichartige Monade zurückgibt ist es das gleiche als würde ich besagte Funktion direkt mit dem Wert der Monade ausführen.
const plusOne = x => Right(x + 1)
Right(10).chain(plusOne) ==== fn(a) // -> 11
Right identity (Rechtsneutral, Rechtseins)
Ein Element in einer Monade welches mittels chain
an eine Funktion übergeben wird, die den Wert in eine gleichartigen Monade packt bleibt unverändert.
Right(1).chain(Right) ==== 1
Assoziativität
Bei einer Reihe von Funktionen, die gegen eine Monade arbeiten muss, bei gleicher Reihenfolge, die Verschachtelung von chain
beliebig gewählt werden können.
const plusOne = x => Right(x + 1)
const timesTwo = x => Right(x * 2)
Right(1).chain(plusOne).chain(timesTwo) ==== Right(1).chain(v => plusOne(v).chain(timesTwo))
Ist wenigstens eines dieser Gesetze nicht erfüllt, dann ist es keine Monade.
Keine Kommentare