Öffentliche Erklärbär-Termine 1. Halbjahr 2025

Veröffentlicht am 23. Januar 2025

Willkommen im Jahr 2025! Wie immer reist der rasende Erklärbär durch die Gegend und verbreitet Neues und Kontroverses zum Thema Frontend-Entwicklung. Ihr könnt entweder bei einem der folgenden Termine dabei sein oder mich direkt zu euch in die Firma einladen – Workshops, Code-Reviews, Beratung, ihr kennt das Programm. Für die Öffentlichkeit fest eingeplant ist für das 1. Halbjahr schon folgendes:

Termine unpassend, Orte alle zu weit weg und Programme nicht genehm? Ich komme auch gerne mit einem maßgeschneiderten Workshop vorbei – mich kann man ganz einfach mieten! Auch für Code-Reviews und Frontend-Feuerwehreinsätze stehe ich gerne zur Verfügung. Einfach anschreiben!

Öffentliche Erklärbär-Termine Herbst 2024

Veröffentlicht am 26. August 2024

Es wird mal wieder Zeit für eine Liste öffentlicher Erklärbär-Termine! Neben dem traditionellen Konferenz-Programm im Herbst könnt ihr mich natürlich wie gehabt direkt mieten, um in eurer Firma die frohe Kunde von Zero-Dependency Web Components, neuen JavaScript-Features oder besserem TypeScript zu verkünden.

  • 11. - 13 und 16 - 18. September online: JavaScript Intensiv-Schulung bei Workshops.de: einmal alles über JavaScript im Druckbetankungsverfahren! Die Schulung verteilt den Inhalt von drei vollen Tagen über 6 halbe Tage, damit sie weniger anstrengend wird und die Teilnehmer:innen nicht komplett in Beschlag nimmt.
  • 21. - 24. Oktober auf den JavaScript- und Angular-Days (Berlin):
    • 21. Oktober: der Ganztages-Workshop „JavaScript-Upgrade für 2024 für Frontend-Entwickler:innen“ (in zwei Teilen) ist das Upgrade für alle, die nicht 24/7 den JavaScript-Nachrichtenzirkus verfolgen und informiert über das Neueste (und das wenig Beachtete) aus den letzten paar ECMAScript-Updates. Dieses Mal sind unter anderem Resizable Array Buffers, Atomics.waitAsync und String.prototype.isWellFormed() neu im Programm.
    • 22. Oktober: im Workshop „Proxies! Exotic Objects für den JavaScript-Alltag“ erschlagen wir alles rund um das Konzept der Exotic Objects in JavaScript. Wir grenzen Exotic Objects von Ordinary Objects ab, entwickeln eigene Exotic Objects mit der Proxy-API und diskutieren erschöpfend die Möglichkeiten und Limitierungen von Proxies im Alltag.
    • 22. Oktober: bei der Veranstaltung „Best of the Worst - JavaScript-Features aus der Hölle“ handelt es sich möglicherweise um ein als Workshop getarntes Comedy-Program, dessen Titel ich einer Youtube-Show entwendet habe.
  • 04. - 08. November auf der W-JAX (München):
  • 11. - 15. November auf der International JavaScript Conference (München):
  • 13. - 14. November auf der c't webdev (Köln): wir kennen den Inhalt des Talks „Best of the Worst“ am 13. November, also auf zum nächsten Termin...
  • 22. - 28. November auf der .NET Developer Conference (Köln): der Ganztages-Workshop „From Zero to Frontend-Hero“ am 22. November hat, wie der Name vermuten lässt, die Frontend-Aufschlauung von Backend-Entwickler:innen zum Thema. Wir besprechen, warum semantisches HTML wichtig ist, was CSS mittlerweile alles kann, warum JS so ist wie es ist, was es mit TypeScript auf sich hat und demystifizieren Konzepte wie Full-Stack-Frameworks und Server-Side-Rendering.

Termine unpassend, Orte alle zu weit weg und Programme nicht genehm? Ich komme auch gerne mit einem maßgeschneiderten Workshop vorbei – mich kann man ganz einfach mieten! Auch für Code-Reviews und Frontend-Feuerwehreinsätze stehe ich gerne zur Verfügung.

Frontend ohne FOMO: ein Erfahrungsbericht

Veröffentlicht am 13. August 2024

Die erste Fassung der Code.Movie-Webseite, damals noch mit dem Playground als SAAS, hatte ich vor ca. 2 Jahren gebaut und dabei alles zum Einsatz gebracht, was moderne Frontend-Entwicklung zu bieten hatte: Next.js und React sowie diverse Packages für React. Dazu zählten unter anderem Redux und React DnD, die freilich ihrerseits Ergänzungs-Packages nach sich zogen (u. a. Redux-Saga und react-dnd-html5-backend), plus natürlich allerlei weiteres Zubehör wie Linter-Plugins für Next und React. Das Endergebnis war eine moderne Webapp nach allen Regeln der Kunst – SPA, SSR, jedes Feature war dabei. Wenn ich heute die package.json von damals aus dem Archiv hole, finde ich die Liste der Dependencies auch gar nicht mal so besonders lang. Die Packages konzentrierten sich auf das Wesentliche und statt für jede Kleinigkeit NPM heranzuziehen, habe ich viel mit eigenem Code gelöst.

Das Projekt litt trotz einer eher überschaubaren intrinsischen Komplexität und einem moderat dimensionierten Dependency Tree daran, dass ich permanent damit beschäftigt war, vorhandenen Code wieder und wieder neu zu schreiben, ohne viel sichtbaren Fortschritt am Projekt zu erzielen. Das könnte allein mein höchstpersönliches Versagen gewesen sein, aber es drängt sich der Eindruck auf, dass nicht nur ich dieses Problem kenne.

Code als Dauer-Baustelle: eine Ursachensuche

Am Dauer-Rewrite waren die Dependencies nicht unbeteiligt; von einem Tag auf den anderen wollte beispielsweise Next.js alle Pages statt in Folder A lieber in Folder B sehen und dort, wo die Dependencies wirklich gestapelt waren, gab es auch bei kleinen Updates immer wieder Kompatibilitätsprobleme. Das größere Problem saß aber in Kiel vor dem Bildschirm, denn mich hatte das Frontend-FOMO weit mehr im Griff, als mir damals klar war.

Neue Versionen von Software bedeuten in aller Regel Fortschritt. Klar, manchmal bricht eine API und manchmal wird etwas verschlimmbessert, aber in aller Regel geht es vorwärts – und wer möchte nicht gern vorn mit dabei sein? Das Problem ist nur, dass Fortschritt, wenn er auf bestehenden Code trifft, nicht ohne Arbeitseinsatz eingebaut werden kann … manchmal, ohne wahrnehmbare Verbesserungen zu erzielen. In meinem Fall passierte das mehrfach:

  • Dependency-Updates mit API-Änderungen (wie das angesprochene Next.js-Update) bringen Umbauten des betroffenen Codes mit sich. Diese können in sich eine Verbesserung darstellen, aber genauso gut einfach eine reine Umformulierung des Bestehenden ohne spürbaren Gewinn bedeuten. Dass am Ende von den eventuellen Verbesserungen etwas Wahrnehmbares bei den Nutzer:innen des Projekts ankommt, ist sehr unwahrscheinlich und war beim Next.js-Update am Ende auch nicht der Fall.
  • Eines Tages ließ ich mich überzeugen, das Redux-Toolkit einzubauen und im Zuge dessen das gesamte bisherige State-Management der App zu ersetzen. Viel Arbeit, keine Änderung an UI oder UX. Der neue Code war im Vergleich zu meinem vorher verwendeten DIY-Redux-Toolkit deutlich weniger idiosynkratisch, aber außerhalb des Codes gab es keine wahrnehmbare Änderung. Da mit dem Code nur Menschen in Kontakt kamen, die auch schon vorher wussten, wie das System funktioniert, war dieser Umbau nicht wirklich hilfreich.
  • Ich muss vermutlich nicht erklären, was mit dem frisch umgebauten State Management passiert ist, als ein Jahr später Signals um die Ecke kamen. Verbesserungen an UI, UX und Feature-Set: keine.

Jetzt könnte man mir vorwerfen, dass ich hätte kommen sehen sollen, dass keiner dieser Umbauten mir weiterhelfen würde. Aber ich würde nicht sagen, dass mir nichts davon geholfen hat: das Next.js-Update mitzunehmen war nötig, um auch in Folge Updates mitgehen zu können und das mehrfache Neuschreiben hat das State Management ganz am Ende in einen hervorragenden Zustand gebracht! Letzteres lag vermutlich weniger an Redux-Toolkit oder Signals als eher daran, dass ich beim dritten Rewrite ein viel besseres Bild davon hatte, was ich zu erreichen versuchte … aber der Fortschritt an sich ist nicht wegzudiskutieren. Selbst wenn es keine wirklichen Verbesserungen gegeben hätte, so wäre es doch keine gute Idee gewesen, die Updates zu ignorieren, denn das frische Update von heute ist der etablierte Standard für die nächsten Updates der Dependencies von morgen.

Wir müssen aber auch festhalten, dass alle genannten Updates und Verbesserungen sich nur dort manifestiert haben, wo sie eine untergeordnete Rolle spielen: im Programmcode. Code ist nicht unwichtig, aber wenn wir Frontend-Web-Apps bauen, ist das Ziel ein anklickbares Produkt, in dem eine dritte Iteration von State Management nur mittelbar eine Rolle spielt. Das dazu nötige JavaScript, CSS und HTML kann sich dabei innerhalb eines relativ breiten Qualitätskorridors bewegen, ehe es zu einem Problem wird.

Nun bin ich, wie alle Hörer:innen von Working Draft wissen, weit entfernt davon, unkritisch jedem Hype hinterherzulaufen. Trotzdem war es mir, als ich mich in das React-Universum begeben hatte, nicht möglich, sich dem permanenten Neuschreiben und dem Dauer-Update von bereits Funktionierendem zu entziehen. Es gab jederzeit genug Neuerungen zum Einbau und genug Gründe (v.a. Kompatibilität aktueller und zukünftiger Dependencies untereinander) für all die Neuerungen. Am Ende hat mich das permanente Neuschreiben ausreichend genervt, um das betroffene Projekt eines Tages einfach abzubrechen.

Wo kommt das ständige Neuschreiben her?

Web-Frontends könnten eigentlich, wenn einmal gebaut, fast auf ewig unverändert weiter funktionieren. Browser sind gnadenlos abwärtskompatibel und wenn wir wollten, könnten wir auch heute noch Seiten aus <frameset> und document.write() zusammenbauen. Warum schreiben wir dann unsere Frontends (ganz oder in Teilen) ständig neu?

Neben den zweifellos nötigen Updates (neue Features im betroffenen Frontend oder globale Neuheiten, auf die regiert werden muss – z. B. Sicherheits-Features, Responsive Design oder Dark Mode) kommt meines Erachtens viel Rewrite daher, dass die meisten Web-Apps keine Berührungspunkte mehr mit der eigentlich so stabilen Web-Plattform haben. Das ist kein Zufall, denn der Sinn und Zweck von Tools wie React ist, die Plattform wegzuabstrahieren. Die Webstandards definieren allerlei unkomfortable APIs und statt eines konsistenten Big-Picture-Designs herrscht ein wildes Durcheinander von Paradigmen und Konventionen. Angesichts dessen ist eine komplette Neukonzeption natürlich sehr attraktiv. Chaos und OOP? Einfach React installieren und es herrschen ein klares Konzept und funktionale Programmierung!

Solche Abstraktionsschichten (und alle darauf aufbauenden Schichten, wie etwa clientseitige Router und UI-Toolkits) sind aber nicht, wie ein Webbrowser, der unbedingten Abwärtskompatibilität verpflichtet, wodurch sich ein Teil des permanenten Neuschreibens erklärt. Die Frontend-Frameworks dieser Welt haben aber auch ein ganzes Ökosystem um sich herum angesammelt, das nicht nur Software, sondern auch Ideen und Praktiken generiert – weit schneller, als die dem Ökosystem zugrundeliegenden Frameworks API-Änderungen produzieren. Abertausende Nerds finden jeden Tag neue Patterns und neue Best Practices und publizieren dazu Texte, Code und Videos ohne Unterlass. Wie es Software-Entwickler:innen angesichts dessen gelingen soll, nicht FOMO anheimzufallen (und andererseits nicht zum Fortschritts-Totalverweigerer zu werden), weiß ich nicht. Und mir ist es am Ende auch nicht besonders gut gelungen.

Zum Dauer-Rewrite im Frontend kommt meines Erachtens in drei Schritten:

  1. Moderne Frameworks versuchen der Web-Plattform ein neues Paradigma überzustülpen, um ihre Nutzer:innen vor den Bugs und Inkonsistenzen der nativen APIs zu bewahren und die Implementierung von relevanten Patterns möglichst einfach zu machen.
  2. Auf der frisch erschaffenen Plattform erwächst ein Ökosystem, das iterativ immer neue Tools und Patterns generiert, die die neu entstandene Plattform möglichst gut zu nutzen versucht. Es wird ausprobiert, verfeinert, verworfen, verbessert.
  3. Entwickler:innen sehen, dass stets und ständig neue Tools und Patterns als aktuell gelten und verwenden Packages, die früher oder später diese neuen Tools und Patterns voraussetzen werden und versuchen, stets ihren gesamten Stack aktuell zu halten, statt (mögliche) technische Schulden anzuhäufen.

Keine der beteiligten Parteien macht bei irgendeinem dieser Schritte etwas wirklich falsch! Die Entwickler:innen des Frameworks wollen eine objektiv bessere Plattform bauen, die Entwickler:innen der Tools und Patterns versuchen, aus der neuen Plattform alles herauszuholen und die Entwickler:innen der Web-Apps tun verständlicherweise alles, um mit der Gesamtentwicklung Schritt zu halten. Das bedeutet aber auch, dass sich alle Beteiligten in einem Zustand des permanenten Experimentierens befinden und eine experimentelle Plattform ist keine stabile Plattform. Sie mag in mancherlei Hinsicht besser sein, aber diese Verbesserungen müssen jeden Tag neu erarbeitet und ausgebaut werden.

Es sei denn, man verwendet statt der besseren, aber weniger stabileren Plattform eine stabile Plattform und schluckt ein paar DX-Kröten.

Meine eigene, kleine Hütte im Wald

In der oberen Kreidezeit schreib’ ich einst, dass man nicht kein JavaScript-Framework verwenden kann und diese Aussage würde ich weiter so vertreten. Ein Framework besteht aus Entscheidungen, die durch Software in Form gegossen werden – und wenn man programmiert, trifft man Entscheidungen und schreibt Software. Früher oder später hat man ein Framework in der Hand, entweder ein im Vorfeld installiertes, ein am Anfang designtes oder ein im Laufe der Entwicklung passiertes. Um irgendeine Form von Struktur kommen wir nicht herum.

Wenn aber Entscheidungen und Software eigentlich genau das Metier von uns Softwareentwickler:innen sind, sollten wir eigentlich alle in der Lage sein, ein eigenes Frontend-Framework zu bauen. Diese Idee ist in einer Welt, in der Web Components existieren, gar nicht mal so absurd (denn immerhin müssen wir zumindest kein ganz neues Komponentenmodell erfinden) und eröffnet einige neue Möglichkeiten:

  1. Wir können das Framework so gestalten, wie wir wollen und die Tradeoffs so wählen, wie wir möchten. Insbesondere können wir uns entscheiden, das Framework vergleichsweise wenig machen zu lassen und uns stattdessen mit den Macken der Web-Plattform arrangieren … und uns auf diese Weise Stabilität erkaufen. Falls uns gewisse Aspekte gar nicht interessieren (z. B. TypeScript oder Server-Side-Rendering), können wir diese komplett ignorieren und an sie kein einziges Byte und keinen einzigen Design-Kompromiss verschwenden.
  2. Updates und Verbesserungen finden statt, wenn wir wollen, statt in Abhängigkeit von irgendwelchen Third-Party-Autoren von Frameworks oder Dependencies. Welche Teile des Frameworks wir selbst schreiben und welche Teile aus Drittpaketen stammen, entscheiden wir ebenfalls selbst.
  3. Wir entkoppeln uns vom in anderen Framework-Ökosystemen grassierenden FOMO. Einerseits fehlt uns damit eine breite Auswahl an fertigen Plugins, die wir etwa im React-Universum vorfinden, und wir werden vieles mit mehr Aufwand als npm install lösen müssen. Andererseits werden wir uns auf diese Weise auch keinen permanent wackelnden Turm aus Dependencies aufbauen können, was sich langfristig auszahlen könnte.

Es versteht sich von selbst, dass es nicht für jedes Projekt und jedes Team möglich ist, ein eigenes Framework zu bauen, aber ich bin überzeugt, dass es für viele Teams und Projekte machbar und sinnvoll ist, sich ein pareto-optimales DIY-Framework zu leisten. Ein solches könnte die wildesten Tradeoffs machen, die sich keins der etablierten Frameworks je erlauben würde:

  1. TypeScript komplett ignorieren und stattdessen originelle APIs bereitstellen, die TS nie verstehen würde? Kein Problem!
  2. Spielt Performance eine untergeordnete Rolle? Warum dann nicht Dinge wie Templates oder JSX im Frontend kompilieren und sich den Compile-Schritt ersparen?
  3. Neueste Web-Component-Standards mit Oldschool-Templating wie z.B. Handlebars.js kombinieren? Warum nicht!
  4. Heutzutage unpopuläre Architekturen wie MVC oder Two-Way-Databinding aus der Gruft holen, weil sie gut auf das zu lösende Problem passen? Nur zu!

Das eigene kleine Framework muss nicht komplex oder vielseitig sein: hier ist zum Beispiel der Code des Frameworks hinter Code.Movie, alle 180 Zeilen inkl. Kommentaren und Whitespace. Das Ganze setzt sich wie folgt zuammen:

  • Dependency 1: Ornament für Web-Component-APIs,
  • Dependency 2: uhtml für Templating und DOM-Diffing,
  • Eine Basisklasse namens BaseElement, die HTMLElement erweitert, initialisiert das Shadow DOM und führt ein UI-Update aus, wenn sich auf der Klasse entsprechend dekorierte Accessors ändern,
  • Ein Decorator @listen() implementiert Event Delegation von Klicks und andere Events im Shadow DOM,
  • Der Ornament-Decorator @define() ist so erweitert, dass er Stylesheets laden und dem Shadow DOM bereitstellen kann

Das ganze „Framework“ besteht also aus einer Klasse, die zwei Dependencies zusammenknotet, drei Konventionen etabliert (UI lebt in Shadow DOM, lokaler State lebt in Accessors und das Templating ist in der Klassenmethode render() definiert) und den zwei Hilfsfunktionen bzw. Hilfs-Decorators @listen() und @define(). Die so entstehenden Web Components sehen wie folgt aus:

import {
  BaseElement,
  define,
  attr,
  literal,
  string,
} from "../../framework/index.js";

@define("cm-page-layout", {
  sheets: [new URL("./styles.css", import.meta.url)],
})
export class CMPageLayout extends BaseElement {
  @attr(literal({ values: ["page", "docs"], transform: string() }))
  accessor mode = "page";

  render() {
    return this.html`<div class=${this.mode}>
  <div class="sidebar"><slot name="sidebar"></slot></div>
  <div class="main"><slot></slot></div>
</div>`;
  }
}

Das ist nicht so kompakt wie eine React-Komponente und (mangels TypeScript) nicht so typsicher wie es das Angular-Äquivalent wäre, aber auf jeden Fall ein relativ simples und einfach zu verstehendes Konstrukt. Die Syntax für das Einbinden von Stylesheets ist etwas umständlich und dass im Shadow DOM verwendete HTML-Tags manuell importiert werden müssen, nervt ein wenig. Es gibt kein SSR, keinen Router und nur einen sehr basalen Build-Schritt mit Parcel, da das gesamte Projekt tendenziell eher eine MPA denn eine SPA ist. Komponenten enthalten diverse Ad-hoc-Lösungen für Probleme, die das Framework nicht berücksichtigt und es existiert ein nur mäßig gelungenes State-Management-System, das nochmals um die 290 Zeilen wiegt (dazu später mehr).

Diesen diversen Nachteilen steht gegenüber, dass das verwendete Framework nur 180 Zeilen und nur zwei austauschbare Mini-Dependencies zu Felde führt, auf Web Components und damit stabilen Webstandards basiert und einfach nur ein unspektakuläres Werkzeug ist … und daher mir bisher null Frontend-FOMO eingebracht hat! Seitdem das Framework vor ein paar Monaten ein Feature-Equilibrium gefunden hat, habe ich es nicht mehr angerührt. Ich kann damit Komponenten definieren, die einen Rahmen für UIs darstellen, aber auch flexibel genug sind, um Ad-hoc-Problemlösung zu ermöglichen. Ich will etwas machen, für das es keine Lösung aus der Dose gibt? Einfach den entsprechenden Code in die betroffene Klasse schreiben und fertig!

Vor- und Nachteile des DIY-Frameworks

Als ich mich dazu aufmachte, Code.Movie mit einem DIY-Framework neu zu schreiben, sahen meine Erwartungen wie folgt aus:

  • Weniger npm update durch weniger Dependencies und dadurch weniger permanentes Neuschreiben von vorhandener Funktionalität,
  • vergleichsweise wenig Entwicklungsaufwand am Framework, denn Web Components können ja so schwer nicht sein,
  • relativ viel Rad-Neu-Erfinden, denn es fehlt der vorher bestehende Zugriff auf das React-Ökosystem
  • etwas schlechtere Performance durch das Bevorzugen von einfachen Lösungen gegenüber besonders anspruchsvoll-optimierten Lösungen.

Diese Erwartungen wurden nur teilweise erfüllt. So hat sich etwa die Performance (v.a. Rendering und die reine JavaScript-Performance bei Interaktionen wie etwas Drag & Drop) im Vergleich zum alten React-Projekt deutlich verbessert. Ich führe das darauf zurück, dass der neue Code im Vergleich einfach extrem stark reduziert ist. Kein Code ist so schnell wie kein Code, egal wie fleißig und trickreich die Performanceoptimierung auch ist. Die Ladeperformance ist im Vergleich zum Next.js-Projekt mit all seinem SSR und all seinen Optimierern auch ungefähr gleich geblieben. Weniger ist einfach mehr.

Überraschenderweise würde ich auch behaupten, dass ich nicht häufiger das Rad neu erfinden musste als mit der React-App. Viele Web-APIs wie Drag & Drop, für die man in einem React-Projekt eigene Kompatibilitätsmodule benötigt, konnte ich einfach direkt benutzen! Einen Adapter für CodeMirror musste ich tatsächlich selbst schreiben, aber das war dank origineller Anforderungen auch im React-Projekt der Fall.

Der Entwicklungsaufwand am Framework war etwas höher als erwartet, was primär daran lag, dass ich offenbar meinte, unbedingt einen besonders ausgebufften State Manager entwickeln zu müssen. Dieser hat eine API auf Basis von Decorators und verwendet unter der Haube proxybasiertes Change Tracking …

import { BaseElement, define, listen } from "../../../framework/index.js";
import { PlaygroundState, languages } from "../../../data/index.js";

@define("cm-project-panel", {
  sheets: [new URL("index.css", import.meta.url)],
})
export class CMProjectPanel extends BaseElement {

  // Trackt eine State-Variable im accessor #open
  @PlaygroundState.use("ui.projectPanel.open")
  accessor #open;

  // Event Listener für toggles auf <details> im Shadow DOM
  @listen("toggle", "details")
  #handleToggle(evt) {
    // Neuer state? Einfach accessor updaten!
    // Löst Re-Render der betroffenen Komponenten aus usw.
    this.#open = evt.target.open;
  }
  
  // ... Render-Methode und viel mehr
}

… was eine ganz brauchbare Developer Experience bietet, aber auch ziemlich aufwendig war (ich ungebildeter Informatik-in-der-Realschule-Abwähler musste eine eigene Trie-Implementierung entwickeln!) und 290 LOC wiegt. Eine simplere Lösung um den Preis einer etwas umständlicheren API oder der Rückgriff auf einen de-facto-Standard wie Signals hätte mir in der Gesamtschau viel Zeit erspart.

Wenig überraschend gibt es im Projekt jetzt weniger Dependency-Updates, aber auch nicht null, denn ein Buildsystem inkl. Dev-Server gibt es weiterhin. Das Ende des permanenten Neuschreibens hat sich vielmehr dadurch eingestellt, dass ich nicht mehr Teil des Next.js/React-Ökosytems bin und mich daher in viel geringerem Maße von FOMO betroffen fühle. Ich koche mein eigenes Süppchen und radikale Umwälzungen wie der React-Compiler und React Server Components betreffen mich nicht mehr. Dieser Effekt ist ausgesprochen befreiend und ist meines Erachtens der eigentliche Grund, warum ich bei Code.Movie dieser Tage mehr an Features statt an Rewrites arbeiten kann.

Fazit: baut eure eigenen Frameworks!

Ich bin überzeugt, dass es für viele Projekte von Vorteil wäre, wenn sie sich aus dem Dauer-Rewrite-Zyklus der modernen Frontend-Entwicklung befreien könnten – auch für Projekte, die größer sind, als meine 1-Personen-MPA. Klar, React und Co bieten viel, aber sie kosten auch viel: viele Kilobytes, viel zu lernen und viel Aufwand bei entweder dem andauernden Neuscheiben vorhandener Features oder dem Umgang mit Frontend-FOMO. Viele Gründe, die normalerweise gegen DIY-Frameworks zu Felde geführt werden, greifen heutzutage im Frontend nicht mehr:

  • Ein minimales Framework hat nur wenige Zeilen Code, enthält nur die wichtigsten Features und ist damit keine signifikante Wartungs-Belastung.
  • Der Bus-Faktor spielt bei ausreichend minimalistischer Konzeption keine Rolle, denn zwei Dependencies mit unter 200 Zeilen Glue Code am Funktionieren zu halten, dürfte für die meisten Teams im Bereich des Machbaren liegen.
  • Wenn das Framework auf Webstandards setzt (speziell Web Components) ist es kein Problem, Entwickler:innen hierfür anzuheuern, denn HTML kennt jeder und die Kombination aus einer Basisklasse und einer Handvoll Decorators und Hilf-Funktionen lässt sich jedem und jeder schnell erklären.

Diese Einwände lassen sich eher noch besser an die Adresse der etablierten Frontend-Frameworks richten. Ist die Wartungs-Belastung durch ein sich permanent updatendes Framework nebst Ökosystem rechtfertigen? Bedeutet ein großer Bus-Faktor und der Support durch eine große Firma nicht auch, dass es viele Personen (freidrehende CEOs, wegbeförderte Entwickler:innen) gibt, die das Projekt mit individuellen Entscheidungen in Schwierigkeiten bringen könnten? Sollte so etwas Zentrales wie das Framework des Projekts nicht unbedingt der eigenen Kontrolle unterliegen? Kann man es sich leisten, nicht auf die zu jeder anderen Technologie kompatiblen Web Components zu setzen?

Der Programmcode hinter der Code.Movie-Webseite hat sich nach dem Abwandern von React definitiv vom Ideal der modernen Frontend-Entwicklung entfernt. Es gibt ein wenig mehr Boilerplate, weniger Typsicherheit und die vielen Klassen und HTML-Strings gewinnen keinen Coolness-Preis. heutzutage für zentral befundene Features wie SSR fehlen komplett. Es handelt sich ohne Zweifel um eine auf die vorliegenden Anforderungen stark zugeschnittene 80/20-Lösung. Aber aus einer Reihe von Gründen – mit dem fehlenden Frontend-FOMO als dem meines Erachtens gewichtigsten Grund – fasse ich den einmal geschriebenen Code nur noch an, wenn ich daran etwas ändern möchte, das sichtbare Features betrifft. Endergebnis: deutlich mehr Produktivität und Entspanntheit. Und alles lädt und kompiliert so viel schneller! Man kann direkt mit den Web-APIs reden! Ich kann diesen Schritt jenen, die ihn sich leisten können, nur empfehlen.

Code Challenge: Quicksort in 100% TypeScript-Typen

Veröffentlicht am 16. Juli 2024

Vor kurzem hat Chris Heilmann auf Mastodon einen Code-Challenge veröffentlicht: Was ist der kürzeste Code, um eine Liste von Strings anhand des vierten Zeichens in den Strings zu sortieren? Und da ich im Moment nichts Besseres zu tun habe (ernsthaft, ich habe gerade wirklich nichts zu tun; heuert mich an, ich hacke alles rund um Low-Level-Webtech wie JS/TS, mache Code-Reviews, coache Nerds etc.), habe ich mich natürlich sofort der Herausforderung gestellt. Und zwar in reinen TypeScript-Typen, damit es bloß nicht zu einfach wird!

TypeScript-Typen als eigene Programmiersprache

Statt TypeScript-Typen bestimmungsgemäß zur Beschreibung von JavaScript-Programmen einzusetzen, können wir sie auch als ganz eigene Programmiersprache behandeln. Die relevanten Sprachkonstruktionen sind hierfür allesamt vorhanden:

// Typ-Alias ≈ Variable
type A = number;

// Generischer Typ ≈ Funktion
type B<T> = [T];

// Tuple ≈ Liste
type C = ["a", "b", "c"];

// Union ≈ Set
type D = "a" | "b" | "c";

// Conditional Type === Conditional
type E<T> = T extends 1 ? true : false;
type R1 = E<1>; // > true
type R2 = E<2>; // > false

Der wichtigste Bestandteil am Konzept „Typen als Programmiersprache“ sind die generischen Typen, die wir als Funktionen betrachten können. Ähnlich wie JavaScript-Funktionen sind generische Typen technisch gesehen einfach nur normale Typen, aber praktisch gesehen würde besteht ihr einziger Sinn darin, mit Parmetern gefüttert zu werden um neue Ergebnisse zu erzeugen:

// Variablen
let a = 42;
type A = number;

// Funktionen
let b = (x) => [x];
type B<X> = [X];

a und A sind beides normale „Variablen“, die für sich genommen nützlich sind, während b und B wie Funktionen arbeiten und verwendet werden. Kein Web-UI wird je den Inhalt der Variablen b anzeigen und kein konkretes JS-Objekt wird je den Typ B haben. Wenn man aber b oder B mit Inputs füttert, liefern sie nützliche Werte oder Typen – Funktionen eben!

Mit Variablen, Funktionen und Conditionals haben wir alle nötigen Programmier-Zutaten zur Hand. Schwierig am Arbeiten in reinen TypeScript-Typen sind (wenn wir die manchmal auch nicht ganz triviale Frage des „warum“ ausklammern) eigentlich nur zwei Dinge:

  1. TS-Typen funktionieren wie eine rein funktionale Programmiersprache. Es gibt weder imperative Sprachkonstrukte (if, for o. Ä.), noch eine wirklich gute Möglichkeit temporäre Variablen anzulegen, noch Features für Debugging. Das ganze „Programm“ ist ein unmittelbar von entweder dem Editor oder dem TypeScript-Compiler ausgewertetes Gleichungssystem, was etwas gewöhnungsbedürftig ist.
  2. Das Typsystem ist eine domänenspezifische Sprache für die Beschreibung von JavaScript-Programmen. Alles außerhalb dieser Domäne (die im Wesentlichen aus der Frage „kann Typ A der Variablen B zugewiesen werden“ besteht) ist nur mit großem Aufwand möglich. Strings manipulieren? Sehr schwierig. Zahlen addieren? Fast unmöglich.

Wie können wir unter diesen Bedingungen zwei Strings anhand ihres vierten Zeichens sortieren? Mit gar nicht mal so viel Aufwand, wenn wir es schaffen, String-Vergleichbarkeit herzustellen.

Das Vergleichbarkeits-Problem

Da Chris’ Aufgabenstellung den relevanten Zeichensatz auf ASCII-Kleinbuchstaben zu beschränken scheint, könnte uns als erste Idee eine Lookup-Tabelle in den Sinn kommen, mit der wir einzelne Zeichen über den Umweg ihres ASCII-Codes miteinander vergleichbar machen können:

type ASCII = {
  a: 97,
  b: 98,
  c: 99,
  ...
};

Einfach das vierte Zeichen der zu vergleichenden Strings mithilfe der Tabelle in Zahlen übersetzen und dann einen Größenvergleich machen. Das Haken daran ist, dass ein Vergleich von zwei Zahlen genau das ist, was TypeScript nicht kann. Die einzige Beziehung zwischen zwei „Werten“ (Typen), die TypeScript kennt, ist die Zuweisungskompatibilität, ein Konzept von „größer als“ gibt es hingegen nicht. Für TypeScript sind die Zahlen 1 und 7 wie true und false – sie sind einfach unterschiedliche Werte und haben darüber hinaus keine weiteren Beziehungen wie eben „größer als“ zueinander.

Wie können wir dann modellieren, ob „a“ vor oder nach „b“ kommen soll? Es gibt nur einen Ausweg: wenn alles, was uns TypeScript sagen kann, ist, ob ein „Wert“ (Typ) einem anderen „Wert“ (Typ) zugewiesen werden kann, müssen wir einen Weg finden, die Frage der Buchstaben-Sortierung in eine Frage der Typ-Zuweisungskompatibilität zu übersetzen.

Mein Plan bestand darin, zunächst einen Tuple-Typ mit dem unterstützten Zeichensatz anzulegen:

type Charset = ["a", "b", "c", ...];

Alle Buchstaben manuell aufzulisten wäre recht mühsam, weshalb ich eine generische String-Typ-Split-Funktion aus meinem Code-Giftschrank gekramt habe:

type Split<T, R extends string[] = []> = T extends `${infer First}${infer Rest}`
  ? Rest["length"] extends 0
    ? [First, ...R]
    : [First, ...Split<Rest, R>]
  : [];

Die Syntax sieht ein wenig wild aus, beschreibt aber eigentlich nur eine rekursive Funktion:

  1. Die „Funktion“ Split<T, R> hat die zwei Parameter T und R, wobei R auf Subtypen von string[] beschränkt ist und mit einer leeren Liste belegt wird, wenn der Parameter nicht explizit angegeben wurde. Die Klausel extends string[] fungiert praktisch als Typ für den Typ (-Parameter) R.
  2. Mit einem Conditional Type prüft die Funktion, ob T dem String Typ ${infer First}${infer Rest} zuweisbar ist. Das ist der Fall, wenn T ein String ist und es der Typinferenz möglich ist, mindestens ein Zeichen am Anfang des Strings zu identifizieren (infer First). Ist das nicht der Fall, liefert die Funktion ein leeres Tuple.
  3. Danach wird geprüft, ob der auf First folgende String-Rest (infer Rest) eine Länge gleich 0 hat. Man beachte: Das ist kein Größenvergleich, sondern nur die Feststellung, ob Rest["length"] zuweisungskompatibel zu 0 ist, was nur der Fall ist, wenn der String aus 0 Zeichen besteht. Sollte das der Fall sein, besteht der String offenbar nur aus nur dem Zeichen First und es wird ein Tuple-Typ mit First und dem Inhalt des Tuple-Typs R zurückgegeben.
  4. Wenn der Rest-String nicht leer ist, liefert die Funktion einen Tuple-Typ mit First und dem, was Split<Rest, R> liefert.

Anders gesagt: Die Funktion schneidet von einem String-Typ T das erste Zeichen ab und ruft sich selbst wieder auf, um vom Rest-String das nächste erste Zeichen abzuschneiden – und immer so weiter. R wird nur verwendet, um die Liste der vorher abgeschnittenen Zeichen bei der Rekursion durchzureichen. Und Split<T, R> ist eine ganz klassische rekursive Funktion: einen gibt einen Fall für keinen Input (leerer String = leeres Tuple), einen für einen Input (String mit einem Zeichen = Tuple mit einem Zeichen) und einen Fall für alles mit mehr als einem Input (String mit N Zeichen = Tuple mit dem ersten Zeichen darin plus dem Ergebnis der Rekursion über den Rest).

Mit dieser Split-Funktion ist der Zeichensatz einfach und schnell erstellt:

type Charset = Split<"abcdefghijklmnopqrstuvwxyz">;
// > ["a", "b", "c", ..., "y", "z"]

Ähnlich wie ein JavaScript-Array ist das Tuple eine Sammlung von Elementen in einer definierten Reihenfolge, in der Einträge doppelt und dreifach vorkommen können. Und ebenfalls wie in JS können wir Elemente anhand ihres Index aus dem Tuple herauskramen; Charset[1] liefert "b".

Aber wie hilft uns das für Vergleiche weiter?

Von Tuples zu Unions

Die Domäne der TypeScript-Typen ist Zuweisungskompatibilität. Ein Typ A ist einem Typ B zuweisbar, wenn A entweder gleich B oder ein Subtyp von B ist:

type R0 = 1 extends number ? true : false;
// true: 1 ist number zuweisbar

type R1 = "a" extends number ? true : false;
// false: "a" ist keine number

type R2 = { x: number, y: any } extends { x: number } ? true : false;
// true: { x: number, y: any } ist Subtyp von { x: number }

Union Types sind Sets von Typen:

type A = number;    // Set aller Zahlen
type B = 1 | 2;     // Set der Zahlen 1 und 2
type C = 1 | 2 | 3; // Set der Zahlen 1, 2 und 3

Hier gilt, dass Subsets Supertypen von Supersets sind und entsprechend Subsets Supersets zuweisbar sind:

type Subset = 1 | 2;
type Superset = 1 | 2 | 3;

type R0 = Subset extends Superset ? true : false;
// true: "1 | 2" ist "1 | 2 | 3" zuweisbar

type R1 = Superset extends Subset ? true : false;
// false: "1 | 2 | 3" ist nicht "1 | 2" zuweisbar

An dieser Stelle funktioniert extends wie ein Ist-Subset-von-Operator. Und das ist für unsere Buchstaben-Vergleichbarkeit extrem hilfreich, wenn wir es schaffen, für ein gegebenes Zeichen ein Set der im Alphabet vor diesem Zeichen stehenden Zeichen zu bilden! Für "c" wäre das das Set "a" | "b", während das Set für das Zeichen "d" aus "a" | "b" | "c" besteht. Da das erste Set ein Subset des zweiten Sets ist, ist ersteres letzterem zuweisbar, was wir als „c kommt vor d“ interpretieren können!

type CharsBeforeC = "a" | "b";
type CharsBeforeD = "a" | "b" | "c";

type CBeforeD = CharsBeforeC extends CharsBeforeD ? true : false;
// > true

Zu diesen Unions von Zeichen vor einem gegebenen Zeichen kommen wir über eine weiter rekursive Typ-Funktion, die sich so lange durch das Zeichensatz-Tuple arbeitet, bis es ein bestimmtes Zeichen in diesem Tuple erreicht hat:

type ValueOf<Needle, List extends string[] = []> = Charset[List["length"]] extends Needle
  ? [...List, Charset[List["length"]]][number]
  : ValueOf<Needle, [...List, Charset[List["length"]]]>;

type CharsBeforeC = ValueOf<"c">;
type CharsBeforeD = ValueOf<"d">;

type CBeforeD = CharsBeforeC extends CharsBeforeD ? true : false;
// > true

Der Aufbau gleicht der Split-Funktion, mit nur drei Unterschieden:

  1. ValueOf arbeitet sich durch einen Tuple-Typ statt durch einen String, verwendet aber die gleiche rekursive Element-vom-Anfang-abschneiden-Technik.
  2. Die Abbruchbedingung ist nicht mehr ein komplettes Durcharbeiten des Inputs, sondern die Rekursion endet, wenn ein Zeichen gleich des Such-Parameters Needle gefunden wird.
  3. Statt eines Tuples liefert die Funktion eine Union der Tuple-Members.

Ein Tuple funktioniert in TypeScript ziemlich genau wie ein JavaScript-Array und wir können entsprechend seine Einzel-Indizes abfragen:

type Stuff = ["Apples", "Oranges"];
type First = Stuff[0]; // > "Apples"
type Last = Stuff[1]; // > "Oranges"

Weil aber in TypeScript-Typen alle Operationen sowohl mit Einzel-Typen als auch mit Sets von Typen (d. h. Unions) durchgeführt werden können, können wir ein Tuple auch mit dem Typ number (dem Set aller Zahlen) indizieren und bekommen als Ergebnis eine Union aller Inhalte des Tuples:

type Stuff = ["Apples", "Oranges"];
type All = Stuff[number]; // > "Apples" | "Oranges"
// Entspricht type All = Stuff[0] | Stuff[1]

Wenn wir uns nun noch eine Funktion bauen, die uns für einen gegebenen String das Zeichen an einem gegebenen Index liefern kann …

type CharAt<T extends string, I extends number> = Split<T>[I];

type First = CharAt<"hello", 0>; // > "h"
type Second = CharAt<"hello", 1>; // > "e"

… ist es fast schon trivial, eine Funktion zu schreiben, die für die von Chris gestellte Anforderung einen Vergleich durchführt:

type Comparator<A extends string, B extends string> = ValueOf<CharAt<A, 3>> extends ValueOf<CharAt<B, 3>>
  ? ValueOf<CharAt<B, 3>> extends ValueOf<CharAt<A, 3>>
    ? 0
    : 1
  : -1;

type A = Comparator<"aaaa", "bbbb">; // > 1
type B = Comparator<"zzzz", "yyyy">; // > -1
type C = Comparator<"ffff", "ffff">; // > 0

Kommt A vor B, bekommen wir 1, für B vor A gibt’s -1, und bei Gleichheit wird 0 ausgespuckt. Wären wir in normalem JavaScript unterwegs, könnten wir diese Funktion in Array.prototype.sort verwenden und hätten die Aufgabe bewältigt. Aber natürlich gibt es auf Ebene von TypeScript-Typen keine eingebaute Sortierfunktion, sodass wir uns auch hierum selbst kümmern müssen. Das stellt sich allerdings als das kleinste Problem heraus.

Quicksort und Haskell

Quicksort ist ein rekursiver Sortieralgorithmus und damit in TypeScript-Typen wunderbar einfach umzusetzen:

type Quicksort<List extends string[]> =
  List extends [infer First extends string, ...infer Rest extends string[]]
    ? [...Quicksort<FilterLte<Rest, First>>, First,...Quicksort<FilterGt<Rest, First>>]
    : [];
  1. Erste Zeile: Funkionssignatur-Äquivalent
  2. Zweite Zeile: Aufteilung von nicht leeren Input-Liste in ein erstes Element First und den Rest Rest
  3. Dritte Zeile: Der Rest der Liste Rest wird in eine Liste von Elementen kleiner oder gleich First und eine Liste größer First einsortiert, die Teillisten werden per Rekursion sortiert und via Spread-Operator mit First in der Mitte verkettet
  4. Vierte Zeile: falls die Input-Liste leer ist, eine leere Liste zurückgeben

Da weder Algorithmen noch funktionales Programmieren besondere Stärken von mir sind, habe ich aus dem Wikipedia-Artikel zu Quicksort eine Haskell-Implementierung gemopst und nach TypeScript portiert. Haskell ist auch eine rein funktionale Sprache und meist gut genug lesbar, um als Quell der Inspiration zu dienen. Ich bediene mich für meiner TypeScript-Mentalgymnastik öfter an Algorithmen und Verfahren aus Haskell – man soll schließlich nicht ständig das Rad neu erfinden!

Was nun noch fehlt, sind die Filter-Funktionen FilterLte<List, X> (List auf alle Werte kleiner oder gleich X reduzieren) und FilterGt<List, X> (List auf alle Werte größer X reduzieren), doch die sind, da wir schon eine Vergleichsfunktion haben, schnell gebaut:

type FilterGt<List extends string[], Element extends string> =
  List extends [infer First extends string, ...infer Rest extends string[]]
    ? Comparator<First, Element> extends -1
      ? [First, ...FilterGt<Rest, Element>]
      : FilterGt<Rest, Element>
    :[];

type FilterLte<List extends string[], Element extends string> =
  List extends [infer First extends string, ...infer Rest extends string[]]
    ? Comparator<First, Element> extends -1
      ? FilterLte<Rest, Element>
      : [First, ...FilterLte<Rest, Element>]
    : [];

Es ist zweimal der fast gleiche Code, nur wird einmal First behalten, wenn die Vergleichsfunktion -1 liefert und einmal weggeworfen. Da sich nun herausstellt, dass die Vergleichsfunktion nur den Fall „A kleiner B“ identifizieren können muss, können wir sie noch ein wenig vereinfachen …

type Comparator<A extends string, B extends string> =
  ValueOf<CharAt<A, 3>> extends ValueOf<CharAt<B, 3>> ? 1 : -1;
// Kein Anlass, Gleichheit von A und B gesondert zu behandeln

... und schon haben wir Quicksort in TypeScript fertig implementiert und Chris' Code Challenge bewältigt:

type Result1 = Quicksort<["bbbb", "aaaa", "dddd", "cccc", "eeee"]>;
// > ["aaaa", "bbbb", "cccc", "dddd", "eeee"]

type Result2 = Quicksort<["strawberry", "helicopter", "wales", "acorn"]>;
// > ["strawberry", "wales", "helicopter", "acorn"]

Was lernen wir daraus?

Zugegeben: für das Beschreiben der Typen des durchschnittlichen JavaScript-Programms – also das, wofür TypeScript eigentlich gedacht ist – braucht es vermutlich keine Sortieralgorithmen. Ich finde aber, dass die Übung zwei sehr relevante Aspekte von Typ-Level-Programmierung in TS aufzeigt.

Vorletzte Woche war ich auf einer etwas enterprisigen Konferenz und im Rahmen eines TypeScript-Talks warf jemand aus dem Publikum ein, dass das gezeigte Typ-Gehacke (das deutlich weniger abgefahren als der Inhalt dieses Artikels war) ja wohl „komplett unlesbar“ und daher „in ernsthaften Projekten“ komplett unbrauchbar sei. Einmal abgesehen davon, dass die einzige Alternative zu Typ-Level-Gebastel any, die bête noire der „ernsthaften Entwickler“ ist, würde ich sagen, dass Leserlichkeit nicht das relevante Problem ist. Nicht zuletzt durch die sehr einfache Übernahme des Quicksort-Algorithmus aus Haskell wird deutlich, dass die Struktur eines Typ-Programms gar nicht mal so seltsam ist. Haskell ist freilich nicht ganz so Mainstream wie Java oder JavaScript, aber auch kein totaler Exot – es ist einfach eine funktionale Programmiersprache und damit, sobald man sein Hirn auf Rekursion eingestellt hat, nicht komplett undurchdringbar oder gar „unlesbar“.

Zum Anderen sehen wir hier aber auch, wo die Grenzen der Programmiersprache „TypeScript-Typen“ wirklich liegen: nämlich in der Tatsache, dass es sich um eine domänenspezifische Sprache für die Beschreibung von JavaScript-Programmen handelt. Alles, was TS-Typen beschreiben können, ist die Kompatibilität von Typen zu anderen Typen. Nichts jenseits von basaler Set-Logik existiert. Die Verrenkungen, die ich anstellen musste, um zu ermitteln, ob der Buchstabe „a“ im Alphabet vor „b“ steht, fand ich tatsächlich nicht unerheblich. Das Bemerkenswerteste an ts-sql (einer zu 100% in TypeScript-Typen implementierten Mini-SQL-Datenbank) ist nicht der Query-Parser, sondern der manuell angelegte Typ Integers:

type Integers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ..., 256]

TypeScript hat einfach kein Konzept von Zahlen, und sobald man irgendwas mit Zahlen anstellen möchte, wird der Aufwand enorm und absurd.

An sich sind die Type-Level-Programmier-Features von TypeScript schon extrem praktisch zu Beschreibung von sehr dynamischen JavaScript-Programmen. Auch wenn TS längst nicht jeden Aspekt von JS modellieren kann, so kann es doch sehr viel. Nutzt man diese Möglichkeiten nicht, schränkt man sich in der Nutzung von dem, was TypeScript eigentlich kann, unnötig stark ein. Ich würde sogar so weit gehen, dass TypeScripts diverse Nachteile bzgl. Kosten (komplexere Toolchain, Breaking Changes, Kompilier-Zeitaufwand) nur dann überkompensiert werden, wenn man sich seiner kompletten Feature-Bandbreite bedient und mithilfe von Typ-Hacking eine Kombination aus Typsicherheit und Flexibilität herstellt.

Aber wie immer bei der Programmierung ist irgendwann ein Punkt erreicht, an dem Aufwand und Ertrag in keinem Verhältnis mehr zueinander stehen. Der Zwischenrufer auf der Enterprise-Konferenz sah diesen Punkt offenbar in der originellen Syntax der TypeScript-Typen erreicht – daher der Einwand der „Unlesbarkeit“. Ich würde diesen Punkt aber eher im mit der Komplexität größer werdenden Semantik-Delta zwischen dem, wofür TypeScript gemacht ist (JavaScript beschreiben) und dem, was es so gerade eben kann, verorten. Das Quicksort-Typ-Programm, das wir in diesem Artikel gesehen haben, besteht eigentlich nur ein paar fast identisch aufgebauten rekursiven Funktionen. Sein Kern, die Quicksort-Funktion, sieht noch nicht mal besonders seltsam aus. Die Syntax ist nicht das Problem. Ein Problem entsteht erst, wenn wir gegen unsere Tools ankämpfen müssen, um unsere Ziele zu erreichen – und dieser Punkt ist meiner Meinung nach dann erreicht, wenn wir TypeScript über die Bedeutung von Strings und Zahlen räsonieren lassen wollen. Die Limitierungen von TypeScript machen an dieser Stelle Dinge, die in anderen Sprachen einfach sind, vielleicht nicht unleserlich, aber fast unverständlich und damit auch nicht wirklich empfehlenswert.