lars
webmobiledatenbankendevopsarchitektur
hello (at) larskoelpin.de

Tiefe Strukturen aktualisieren mit Lenses

September 19, 2019
typescript

React fährt mit dem Funktionalen Denkmodell ein anderes Konzept als das Klassen-basierte objektorientierte Denken mit classes. Daten bzw. Zustände werden stattdessen durch Funktionen verändert, die einen aktuellen Zustand annehmen und einen neuen Zustand wiedergeben, anstatt den alten Zustand “in-place” zu verändern.

Leider sind Mutationen in der funktionalen Programmierung nicht erlaubt und die Daten sind konzeptionell für immer ins Stein gemeißelt. Das führt dazu, dass Zustandsveränderungen anders modelliert werden, als es in klassischen OOP Modellen der Fall ist, erkauft sich dafür aber eine “kostenlose” Change-Detection durch einen einfachen equality-check und das modellieren durch einfache Datenflüsse (und nicht durch 100 Objekte die gegenseitig Setter aufeinander aufrufen).

Da wir die Daten nicht direkt verändern dürfen müssen wir stattdessen eine Funktion bereit stellen, die einen neuen Wert liefert, wenn diese auf den Daten angewandt wird. Die Quelldaten selbst werden nie berührt.

Angenommen wir wollen den Namen einer Person ändern, bleibt uns also nichts anderes übrig, als eine Funktion bereitzustellen, die ein Objekt kopiert und das geänderte Attribut enthält. Der Spread-Operator ist dafür gut geeignet.

const quelleLars = {name: "Lars"}
const withChangedName = (newName, person) => ({...person, name: newName})
expect(quelleLars.name).toBe("Lars");
expect(withChangedName("Sven", quelleLars)).toBe("Sven");
expect(quelleLars.name).toBe("Lars");

Spaßig wird es allerdings, wenn wir tiefe, verschachtelte Strukturen aktualisieren wollen.

const lars = {
  name: "lars",
  address: {
    street: {
      name: "Hauptstraße",
      number: 1
    }
  }
};

const withChangedHouseNumber = (newNr, person) => ({...lars, address: { ...lars.address, street: {...lars.address.street, number: newNr}}})

Ziemlich viel Kopieren für das ändern einer simplen Hausnummer.

Zum Glück gibt es “Lenses”. Mit Lenses ist es möglich einzelne Teile (z.b “name” von “street”) eines großen Objektes (so wie “lars”) zu fokussieren und dieses zu aktualisieren. Das Ergebnis dieser Funktion ist dann das ganze Objekt (“lars’”), mit dem geändertem Attribut(“name” der “street”). Das alte Objekt (“lars”) bleibt unverändert. Klingt ganz nach dem was wir brauchen, oder? Also, wie sieht das in der Praxis aus?

Zunächst müssen wir eine Linse erstellen. Dazu bietet uns Ramda die Funktion “lensProp” oder auch “lensPath” für verschachtelte Pfade an.

import { lensPath } from 'ramda';

const HouseNumberLens = lensPath(["address", "street", "number"]) // Lens Objet

Was wir nun erhalten sind nicht wie in der Funktion “path” die Daten am Endpunkt, sondern ein Objekt, eine Linse, die wenn sie in die richtige Funktion gesteckt wird, verwendet werden kann. Verwenden können Linsen beispielsweise die Funktionen “set” und “view”.

import { lensPath, view } from 'ramda';

const HouseNumberLens = lensPath(["address", "street", "number"]); // Lens Objet
const getHouseNumber = view(HouseNumberLens);
const setHouseNumber = (newNumber, person) => set(HouseNumberLens, newNumber, person);
console.log(getHouseNumber(lars)); // 1
console.log(setHouseNumber(12, lars)); // { address: { street: { name: "Hauptstraße", number: 12 } }

Das war’s auch schon. Man sieht, dass es durchaus möglich ist Daten zu verändern, ohne die Quelldaten zu ändern. Mit Lenses ist es möglich Daten durch komplexe Datenstrukturen hindurch anzusehen und zu verändern. Lenses sind kombiniert mit den Accessoren view und set normale Funktionen, die wie jede andere Funktion auch composed werden können.