Ed Post über JavaScript

Ed Post ist wohl einer der allgemein anerkanntesten Experten im Bereich der Programmiersprachen sowie deren Ökosysteme. Wir haben Ed Post nach seiner Keynote im Commnucation Convention Center »Mitte« getroffen, um mit ihm über das Thema “Ist JS (k)eine General Purpose Sprache?” zu diskutieren

… Schon früh hat Ed Post am MIT mit seinen Thesen und Arbeiten zur Software-Programmierung auf sich aufmerksam gemacht und dabei eine Vor­rei­ter­rol­le übernommen. Ihm ist es zu verdanken, dass ein allgemeines Umdenken im Bereich der Software-Entwicklung, insbesondere der Programmierung, mitte der 80er Jahre des letzten Jahrhunderts stattgefunden hat. Er hat damit den Weg in die moderne IT, wie wir sie heute kennen, erst ermöglicht …” [POS82]

Ist JS (k)eine General Purpose Sprache?

Frage: Das Web befindet sich in einem Dauer-Hype und hat längst in jeden Bereich unseres Lebens eingegriffen. So haben viele Programmiererinnen und Programmierer erst aufgrund der Perspektiven und des damit verbundenen gesellschaftlichen Status damit angefangen, zu programmieren, und zwar mit JavaScript

Ed Post: …der Lingua franca des Webs. Das Internet ist ja zum Glück mehr als nur das Web.

Frage: Aus JS-Kreisen höre ich, dass JavaScript und das JavaScript-Ökosystem allen anderen Programmiersprachen sowie deren Ökosystemen (C#, Go, Java oder Rust, Anm.d.Red.) weit überlegen sein soll. Vereint es doch alle guten Konzepte wie funktional oder objektorientiert in einer einzigen Programmiersprache. Dennoch hört man hier und da von widerspenstigen Entwickelnden, die sich geradezu renitent weigern, nach JavaScript zu konvertieren.

JavaScript ist die Lingua franca des Webs […]

… aber das Internet, als Obermenge des Webs, wird mit mehr als nur einer Programmiersprache entwickelt. Da spielt JavaScript eine untergerodnete Rolle.

Ed Post: Sie stellen sich die Frage zum “warum”? Woher kommt diese Renitenz?

Frage: Genau, diese Frage möchte ich mit Ihnen gerne klären. Deshalb auch der provokative Titel dieser Runde: “Ist JS (k)eine General Purpose Sprache?

Ed Post: … wobei Sie die Antwort auf die Frage natürlich schon kennen? Aber eines nach dem anderen…

Die wilden Jahre

Frage: Sollen wir als Einstieg mit einem kurzen Überblick zur Geschichte von JavaScript beginnen? Ich denke, daraus werden sich weitere Fragen ergeben, deren Beantwortung JavaScript-Verweigerern die Augen öffnen sollten, sind diese noch rationalen Argumenten gegenüber offen.

Ed Post: Einverstanden! JavaScript, damals noch als Mocha und dann als LiveScript bekannt, wurde 1995 von Brendan Eich in 10 Tagen (sagt man) entwickelt [JAV18].

Brendan Eich hat den Grundstein für JavaScript in 10 Tagen gelegt […]

… mit ürsprünglich Mocha für den Netscape Navigator über LiveScript bis hin zu JavaScript sowie ECMAScript.

Frage: Man sagt ja, JavaScript ist Scheme mit C Syntax. Wovon wurde JavaScript eigentlich inspiriert bzw. gibt es Vorbilder?

Ed Post: Brendan Eich liebte zwar Scheme, was salopp gesagt einen Lisp-Dialekt ist, war aber wohl hauptsächlich von Self inspiriert, als er mit der Umsetzung von JavaScript begann [NYS13].

Ist JavaScript ein Scheme mit C-Syntax? […]

… Brendan Eich liebte zwar Scheme, JavaScript ist aber von Self inspiriert.

Frage: Es gab doch bestimmt einen Grund für die Entwicklung von JavaScript, wofür wurde denn damals JavaScript entwickelt?

Ed Post: Brendan Eich sagt dazu:

… We aimed to provide a ‘glue language’ for the Web designers and part time programmers who were building Web content from components such as images, plugins, and Java applets…” [RAU11]

Frage: Wie ging es denn weiter mit der Entwicklung von JavaScript? Seit 1995 sind ja doch einige Jahre vergangen…

Die unübersichtliche Versionsgeschichte von JavaScript […]

… beschreibt recht gut den wirren Werdegang von JavaScript nebst ECMAScript.

Ed Post: Gute Frage, erscheinen die Versionen auf den ersten Blick unübersichtlich: So gibt es die Versionsgeschichte von JavaScript wie auch die von ECMAScript. Auf Wikipedia kann man zur Geschichte ganz gut erkennen, dass JavaScript, anfangs mit klassenloser Objektorientierung gestartet ist, seit 2015 aber das Konzept der Klasse kennt:

… Neue Syntax für komplexe Applikationen wie Klassen und Module…” [JSG18].

Frage: Bevor wir uns mit der Objektorientierung befassen, was ist ECMAScript bezüglich JavaScript?

Ed Post: Mit ECMAScript wurde JavaScript mit all seinen Ecken und Kanten in einen quasi Standard verwandelt:

… It was created to standardize JavaScript, so as to foster multiple independent implementations …” [ECM18]

Von Prototypen, Objekten und Klassen

Frage: Kommen wir zurück zur Objektorientierung, wie kann ich klassenlose Objektorientierung verstehen?

Ed Post: Bei der klassenlosen Objektorientierung handelt es sich um prototypenbasierte Programmierung, bei der auf das Sprachelement der Klasse verzichtet wird. Salopp gesagt werden Objekte nicht durch Instanziierung einer Klasse, sondern durch Klonen bereits existierender Objekte - den Prototypen - erzeugt [PRO18]. Sie werden sozusagen Programmiert. Objekte bleiben zur Laufzeit strukturell veränderbar.

Klassenbasierte Objektorientierung findet zur Compilezeit statt […]

… während prototypenbasierte Objektorientierung erst zur Laufzeit stattfindet - und damit werden entsprechende Fehler erst beim Kunden sichtbar.

Frage Was mich interessiert, sind die Vorteile der prototypenbasierten Programmierung. Nachteile gibt es offensichtlich nicht, denn ich bin flexibler. Ich kann meine Objekte zur Laufzeit weiterhin verändern, während bei einer klassenbasierten Objektorientierung eine feste Klassenstruktur zur Compilezeit vorab(!) festgelegt wird.

Ed Post: Diese Flexibilität die Struktur von Objekten zur Laufzeit zu verändern, wird durch eine schlechtere Wartbarkeit des Programmcodes und dem Fehlen des “Principle of Least Astonishment” [PRI18] erkauft [WIN14]. So kann sich ein JS-Programmierer bei der Verwendung fremden Codes nicht wirklich auf die zu erwartende Struktur eines Objektes verlassen. Und da JavaScript eine interpretierte Sprache ist, lassen sich darauf beruhende Fehler meist erst zur Laufzeit feststellen. Also erst, wenn es zu spät ist.

Die Einfachheit von JavaScript wird durch Fehlen das ‘Principle of Least Astonishment’ [PRI18] teuer erkauft.” [WIN14]

Frage: Welche Auswirkungen sollte es denn bitteschön haben, wenn sich der Kontrakt, hier die Objektstruktur, schwer einhalten lässt?

Ed Post: Der Kontrakt lässt sich theoretisch schon einhalten, nur ist es in JavaScript mit seiner schwachen Typisierung und dem dynamischen Typsystem sehr viel einfacher als bei klassenbasierter Objektorientierung (stark typisierter Programmiersprachen, Anm.d.Red.), unbemerkt Änderungen in die Objektstruktur zu schmuggeln.

Frage: Dafür kann sich mit JavaScript der Typ einer Variablen jederzeit zur Laufzeit ändern. Und eine Konvertierung von einem Typ zu einem anderen Typ wird dabei implizit vorgenommen. Wir haben es hier also mit einem dynamischen Typsystem (mit schwacher Typisierung, Anm.d.Red.) zu tun. Damit lösen sich solche Probleme doch von allein.

… Some programming languages make it easy to use a value of one type as if it were a value of another type. This is sometimes described as “weak typing …” [WEA18]

Ed Post: Anders herum: Bei (stark typisierter, Anm.d.Red.) klassenbasierter Objektorientierung meckert der Compiler sofort, wenn der Kontrakt nicht mehr mit den Erwartungen der Entwickelnden übereinstimmt. Abgesehen davon kann jeder JavaScript-Code zur Laufzeit meine Objekte strukturell verändern, so dass ich sie nachher nicht mehr wiedererkenne.

Frage: Ich bleibe dabei: Wo liegt das Problem? Ich sehe keines, wechseln doch meine Werte implizit den Typ, ohne dass ich etwas unternehmen müsste.

Ed Post: So ein veränderter Kontrakt eines Objekts kann durchaus problematisch sein, beispielsweise wenn sich der Typ eines Attributs von Zahl auf String ändert. Wenn ich etwa eine Zahl erwarte mit der ich addieren will, aber anstelle der Zahl einen String erhalte.

Frage: Sie meinen, wenn ich die Version einer verwendeten JS-Bibliothek aktualisiere und danach bei einem Funktionsaufruf ein String bekomme, obwohl vorher eine Zahl geliefert wurde?

Ed Post: Ja, zum Beispiel. Wie sieht das Ergebnis meiner Addition bei impliziter Umwandlung von String nach Zahl aus oder werden nun zwei Strings addiert, also konkateniert? Und wo oder wann bemerke ich diesen Fehler? Kann ich die Ursache schnell finden?

Bei prototypenbasierter Objektorientierung gibt es keinen verlässlichen Kontrakt für unsere Objekte […]

… in Kombination mit impliziter Umwandlung von Typen entstehen Fehlersituationen, deren Ursprünge sich schwer identifizieren lassen.

Frage: Wir haben von JS-Bibliotheken gesprochen. Hat die prototypenbasierte Objektorientierung damit Auswirkungen auf das JS-Ökosystem?

Stabile JS-Bibliotheken und prototypenbasierte Objektorientierung […]

… sind ein Widerspruch in sich, man kann das Eine nicht mit dem Anderen haben.

Ed Post: Wir bekommen mit der prototypenbasierten Objektorientierung keinen verlässlichen Kontrakt für unsere Objekte, weshalb sich JS-Bibliotheken sehr schwer tun, stabile Schnittstellen über einen längeren Zeitraum hinweg - man spricht von gerade einmal 12 Monaten - anzubieten [MOS15]. Ember.js hat jüngst den LTS (“Long Term Support”) einer Version nach 12 Monaten eingestellt [EMB16]. Bei vielen verwenden JS-Bibliotheken in einem Projekt wird der Upgrade-Pfad daher sehr steinig. In anderen Bereichen der IT spricht man bei LTS von 5 oder 10 Jahren…

Die Wahrheit zu LTS und JavaScript ist, dass […]

… LTS (long term support, Anm.d.Red.) bei JavaScript quasi nicht vorhanden ist.

Frage: Ich habe gehört, dass ein typisches JavaScript Projekt gerne mehrere hundert externen JS-Bibliotheken anzieht. Wie steinig ist denn da der Upgrade-Pfad nach sagen wir 12 oder 24 Monaten?

Ed Post: Sie meinen, was passiert, wenn ich 24 Monate meinen Code nicht anfasse, weil das Projekt beendet ist? Ein typische Node.js Projekt hat gerne um die 100, 500, 1000 oder mehr, auch transitive, Abhängigkeiten. Die Auswirkungen des praktisch nicht vorhandenen LTS bei 300 externen Abhängigkeiten bedeuten über so einen Zeitraum eine Änderungsrate, die hoch genug ist, dass mein Code danach mit den dann aktuellen JS-Bibliotheken praktisch nicht mehr Lauffähig ist.

Frage: Das kann man Entwickelnden doch nicht ernsthaft zumuten wollen. Ganz abgesehen von der damit verbundenen Fleißarbeit, die JS-Bibliotheken in einem aktuellen und zueinander konsistenten Zustand zu halten…

Ed Post: Es muss also Aufwand allein darin investiert werden, dass der Code mit den neuen Versionen der verwendeten JS-Bibliotheken überhaupt wieder läuft. Wenn denn die Entwicklung der jeweiligen JS-Bibliotheken dann überhaupt noch betrieben wird. Kritisch sind vor allem die unerkannten Inkompatibilitäten, da sich viele Probleme erst zur Laufzeit zeigen. JavaScript ist ja immer noch eine interpretierte Script-Sprache…

Kurzlebige JS-Bibliotheken (und deren Abhängigkeiten) bescheren einen teuren Upgrade-Pfad […]

… denn Fehler zeigen sich häufig erst zur Laufzeit, im schlimmsten Fall erst beim Kunden.

Frage: Und was für ein Fazit ziehen sie aus den Merkmalen der prototypenbasierten Programmierung in Bezug auf JavaScript?

Ed Post: Die prototypenbasierte Programmierung ist einfach ungeeignet, um eine stabile API für eine JS-Bibliothek zu entwerfen. Zusammenfassend gibt es dafür mehrere Gründe: Prototypen werden Programmiert und sind im auszuführenden Code verteilt oder gar versteckt. Daher ist die Struktur von Objekten schwer zu erkennen und noch schwieriger zu dokumentieren.

Amorphe Datenstrukturen

Frage: Sie wollen damit sagen, dass die Objekte oder Datenstrukturen irgendwo im Code verteilt ausprogrammiert worden sein können?

Ed Post: Da die Struktur eines Objektes bei der prototypenbasierten Programmierung “programmiert” wird, kann sie darüber hinaus auch noch leicht und unbemerkt an ganz anderen Stellen im Code verändert werden. Um dem entgegenzuwirken, wird unter anderem TypeScript in vielen Projekten eingesetzt. Fehlende statische Typisierung ist eben ungeeignet, stabile Objektstrukturen zu erhalten. Und ohne Verlässlichkeit auch kein LTS.

Frage: Was hat dieses Konzept für Auswirkungen auf die Entwicklung?

Ed Post: Da die Datenstrukturen von JS-Bibliotheken erst zur Laufzeit bekannt sind, können die Entwickelnden eigentlich auch erst zur Laufzeit wissen, welche konkreten Datenstrukturen diese JS-Bibliotheken in ihren Schnittstellen anbieten …

Wie der Begriff Datenstruktur schon besagt, handelt es sich dabei um eine Struktur […]

… und nicht etwas amorphes oder gar beliebig formloses. Algorithmen brauchen stabile Datenstrukturen, um deterministische Ergebnisse zu produzieren

Ed Post: … Und die Entwickelnden müssen Datenstrukturen programmieren, was dem Sinn einer Datenstruktur zuwider läuft, denn sie ist eine Struktur und nicht etwas amorphes oder gar formloses (Antonyme, Anm.d.Red.). Damit Operationen bzw. Algorithmen auf den Datenstrukturen sinnvoll arbeiten können, müssen die Datenstrukturen verlässlich sein… [DST18]

Prototypen sind im auszuführenden Code verteilt oder gar versteckt […]

… da sie programmiert werden: Sie können sich in jedem Code-Schnipsel befinden. Allein, eine Datenstruktur benötig Programmlogik für ihre Ausformulierung, sie steht nicht für sich selber und kann sich jederzeit ändern.

Frage: Wie kann ich mich dennoch in JavaScript vor Unwägbarkeit bezüglich der dynamischen Objektstruktur wappnen?

Ed Post: Duck-Typing ist charakteristisch für objektorientierte Skriptsprachen. Es ist eigentlich von Sprachen wie Python, Groovy, PHP oder Ruby bekannt, kann aber auch mit JavaScript verwendet werden [YOU08]. Ein Zitat beschreibt Duck-Typing wie folgt:

“… When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck …” [DUC18]

Frage: Dabei werden Objekte zur Laufzeit auf das Vorhandensein bestimmter Methoden oder Attribute geprüft.

Ed Post: Auf solche Methoden zurückzugreifen um ungewollte Effekte zu vermeiden, ist meiner Meinung nach Ausdruck großer Hilflosigkeit.

Frage: Kann hier nicht das Konstrukt der Klasse, seit ECMAScript 2015 Bestandteil von JavaScript, Abhilfe schaffen?

Ed Post: Trotz dem Konstrukt der Klasse sind Objekte in JavaScript weiterhin dynamisch manipulierbar. Klassen sind ja nichts weiter als ein bequemer Weg, old-school Konstruktor-Funktionen zu erstellen.

Die Objektorientierung in JavaScript ist zwiespältig […]

… reicht sie doch von der prototypenbasierten Programmierung zur - als schnöder Aufsatz der prototypenbasierten Programmierung realisierten - klassenbasierten Objektorientierung.

Sloppy Mode, strict Mode

Frage: Apropos ECMAScriptECMAScript 5 unterstützt den strict mode. Dadurch wird JavaScript ja noch besser. Was hat es eigentlich mit dem strict mode auf sich?

JavaScript wird dank des strict mode weiter verkompliziert […]

… fügt es doch eine neue Ausführungssemantik neben dem unsäglichen sloppy mode ein.

Ed Post: Der strict mode ist nicht nur einfach eine Untermenge des sloppy modes. Er verwendet - beabsichtigt - auch teilweise eine andere Semantik für den gleichen Code gegenüber dem sloppy mode, was auch für Fehler sorgt: Der strict mode kann auf ganze Skripts angewendet werden oder auf einzelne Funktionen, aber nicht in Blöcken ({...}). In Blöcken bleibt eine Anweisung use strict wirkungslos.

… JavaScript’s strict mode, introduced in ECMAScript 5, is a way to opt in to a restricted variant of JavaScript, thereby implicitly opting-out of sloppy mode. Strict mode isn’t just a subset: it intentionally has different semantics from normal code…” [STR18]

Frage: Und was ist der sloppy mode, und weshalb hat man den strict mode eingeführt? “Sloppy” bedeutet ja so viel wie nachlässig oder schlampig…

Ed Post: Mit sloppy mode bezeichnet man den weniger durchschaubaren Modus, in dem JavaScript bei nicht aktiviertem strict mode ausgeführt wird. Was vor ECMAScript 5 ganz normal war. Man muss sich den strict mode jedoch genau auf der Zunge zergehen lassen: Im strict mode ergibt derselbe Code zur Laufzeit andere Ergebnisse als im normalen, dem sogenannten sloppy mode. Diese Nachbesserung von JS kann kaum als Qualitätsmerkmal der Sprache JS herausgestellt werden [SLO18].

Der strict mode führt JavaScript-Code mit einer anderen Semantik aus […]

… als der sloppy mode, was allein schon zum Problem werden kann.

JavaScript … ECMAScript … TypeScript … WebAssembly

Frage: Kommen wir zurück zu den JavaScript bzw. zu den ECMAScript Versionen. Wie wirken sich diese auf JavaScript-Projekte aus?

Ed Post: Sehen wir uns das einmal genauer an: ECMAScript 5.1 (2011), ECMAScript 2015 bzw. Version 6 mit komplett neuer Syntax für Klassen und Module, solche grundlegenden Änderungen findet man so nicht bei anderen Programmiersprachen. Dann ECMAScript 2016, ECMAScript 2017, ECMAScript 2018. Zusätzlich gibt es noch TypeScript, wovon immer mal wieder etwas in einen neuen ECMAScript-Standard hineinrutscht. Sprachkonstrukte von TypeScript wie Klassen, Vererbung, Module, anonyme Funktionen oder Generics wurden dann auch in ECMAScript 6 übernommen [TYP18].

Dank der JS-Versionsvielfalt sagt ein “Ich kann JavaScript” gar nichts aus […]

… das kann ein echtes Problem für das Projekt-Staffing sein.

Frage: Aber warum wird dann nicht gleich eine neue Programmiersprache entworfen oder eine andere Programmiersprache verwendet?

Ed Post: (lacht) Weil JS-Entwickelnde nicht willens sind, eine neue Sprache zu lernen? Nein, im Ernst, zu JavaScript gab es lange Zeit keine Alternativen, weshalb auf JavaScript sogar das Klassenkonzept aufgesattelt wurde, ohne dass der Unterbau dafür geeignet wäre. Ich erwähnte ja schon die dynamischen Objektstrukturen…

Frage: Aber am Ende ist doch alles JavaScript?

Ed Post: Zwischen ECMAScript 5.1, ECMAScript 6 und TypeScript gibt es Riesenunterschiede. “Ich kann JavaScript” sagt gar nichts aus. Das kann ein echtes Problem für das Projekt-Staffing sein.

Frage: Sie sagten “zu JavaScript gab es lange Zeit keine Alternativen”. Gibt es denn jetzt Alternativen?

Ed Post: Mit WebAssembly macht sich gerade ein Bytecode-Standard auf, das Web zu erobern. Dieser Standard ist auf dem Vormarsch und in JavaScript-Kreisen wird schon gegen das Binärformat Stimmung gemacht. Es scheint also Befürchtungen zu geben, dass WebAssembly JavaScript demnächst ablösen könnte. Aber das wird so bald nicht passieren.

Wovor viele JS-Entwickelnden Angst haben, ist WebAssembly […]

… denn mit WebAssembly wächst eine echte Alternative zu JavaScript heran.

Frage: Vielleicht, weil JS-Entwickelnde nicht willens sind, eine neue Sprache zu lernen?

Ed Post: (lacht) Vielleicht. Auch muss WebAssembly noch vieles lernen, z.B. den Zugriff auf das DOM oder die Unterstützung von Programmiersprachen, die einen Garbage-Collector voraussetzen (wovon JavaScript selber eine ist, Anm.d.Red.) [GAR17].

Node.js und das JavaScript Thread-Modell

Frage: Sie haben vorhin Node.js erwähnt. Node.js ist ja ziemlich abgefahren, was hat es damit auf sich?

Ed Post: Node.js ist der Versuch, mit JavaScript serverseitige Web-Anwendungen zu erzwingen. Man verspricht sich davon, dass die selben JS-Entwickelnden, die im Browser das Frontend bauen, auch gleich die Serverfunktionalität mit umsetzen können.

Node.js bringt JavaScript auf den Server […]

… und damit eine Programmiersprache sowohl für den Client wie auch den Server?

Frage: Sie sagen “Versuch” und “erzwingen”, das hört sich ja an, als ob Sie nicht besonders begeistert von Node.js sind?

Ed Post: Das stimmt. Sehen wir uns zum Beispiel das Thread-Modell von JavaScript an [RUN18]. JavaScript arbeitet mit einer sogenannten Main Event-Loop und mit Workern. Im Browser nennen sich diese dann Web-Worker und Worker-Threads bei Node.js. Mit einem echten Thread hat so ein Worker allerdings wenig gemein…

Frage: Wieso? Die Worker laufen doch parallel zur Main Event-Loop?

Ed Post: Das schon, aber die Main Event-Loop und die Worker teilen sich keinen gemeinsamen Speicher, so wie es Threads tun. Ein Thread teilt sich seinen Speicher mit anderen Threads aus seiner Thread-Gruppe. Das gilt für Linux wie auch für Windows. Ein Prozess ist, im Gegensatz dazu, von anderen Prozessen strikt getrennt und kann nur über spezielle Mechanismen wie dem Nachrichtenaustausch mit anderen Prozessen kollaborieren. Auch das gilt sowohl für Linux wie auch für Windows. Ein Worker hat also eher Gemeinsamkeiten mit einem schwergewichtigen Prozess.

Das JavaScript Thread-Modell ist anachronistisch […]

… denn die Main Event-Loop und die Threads betreiben Inter Process Communication nur(!) mittels teurem Nachrichtenversand.

Frage: Die Worker können sich doch über das DOM im Sinne eines Blackboards austauschen [BLA18]. Damit wäre JavaScript wieder im Rennen!

Ed Post: Die Worker können nicht auf Nicht-Thread-Sichere Elemente zugreifen, im Fall der Web-Worker also nicht auf das DOM [ABO18]. Also gerade in Browser-Anwendungen sind diese Worker recht unbrauchbar. Daten müssen immer dediziert zwischen der Main Event-Loop und den Workern übergeben werden.

… There’s no access to non-threadsafe components or the DOM. And you have to pass specific data in and out of a thread through serialized objects …” [ABO18]

Frage: Sie sagten, dass ein Worker eher mit einem schwergewichtigen Prozess zu vergleichen wäre und das Daten “dediziert” zu übergeben wären. Können Sie das genauer ausführen?

Das JS Thread-Modell ist ein starres Konstrukt […]

… das aus der Main Event-Loop garniert um angeflanschte Worker-Threads bzw. Web-Workern besteht.

Ed Post: Wenn verschiedene Worker an einem gemeinsamen Anliegen arbeiten, bekommt jeder Worker seine private Kopie der Daten von der Main Event-Loop per Nachrichtenversand zugeschickt, arbeitet mit diesen, gibt diese wieder zurück an die Main Event-Loop, auch per Nachrichtenversand, was wieder in einer Kopie resultiert, damit die Main Event-Loop dann wiederum die anderen Worker über die geänderten Daten, mittels Nachrichtenversand, in Form weiterer Kopien informiert (puh!). Wir haben es hier also eher mit einer schwergewichtigen IPC zu tun (IPC steht für “Inter Process Communication”, Anm.d.Red.).

Threads in JavaScript müssen auf schwergewichtiges IPC zurückgreifen […]

… um gemeinsame Anliegen zu verarbeiten, und entsprechen damit eher der Definition eines Prozesses denn der eines Threads.

Frage: Und was ist so schlimm am Nachrichtenversand bzw. den Kopien bzw. der IPC?

Ed Post: Schlimm ist, dass es in JavaScript keine Alternativen gibt. Hier wird unglaublich viel Blindleistung produziert. JS-Entwickelnde sind mangels Alternativen gezwungen, Ressourcen zu verschwenden. Was bedeutet denn Nachrichtenversand?

Frage: Sagen sie es mir! Was bedeutet denn Nachrichtenversand?

Ed Post: Nachrichtenversand bedeutet, dass ein Quell-Objekt, etwa aus der Main Event-Loop, serialisert wird, um beim Worker als Kopie wieder deserialisiert zu werden. Es entsteht das Ziel-Objekt. Das bedeutet, dass der Nachrichtenversand Speicherplatz benötigt, einmal für das Quell-Objekt, dann für das Ziel-Objekt und für den Kopierpuffer. Bei ausgiebiger Nutzung dieses Mechanismus wird, versteckt vor den JS-Entwickelnden, eine hohe Last auf der Maschine erzeugt.

Die Kommunikation zwischen JS-Threads und der Main Event-Loop ist komplex […]

… und teuer, was dem unsausweichlichen Nachrichtenversand geschuldet ist.

Frage: Ich habe aber gehört, dass dieser Mechanismus durchaus sinnvoll ist, da der Zugriff mehrerer Threads auf denselben Speicher nur zu Problemen führt!

Ed Post: Ich kann bei entsprechenden anderen Programmiersprachen durchaus auf dieselbe Instanz eines Objekts im Speicher mit mehreren Threads gleichzeitig zugreifen, was auch Fehlersituationen hervorrufen kann. Ich habe aber die Wahl, auch mit dedizierten Kopien meiner Objekte je Thread zu arbeiten.

Bei anderen Programmiersprachen habe ich die Wahl, den JavaScript-Weg des IPC zu gehen […]

… muss es im Gegensatz zu JavaScript aber nicht, denn bei entsprechenden Programmiersprachen habe ich Alternativen.

Ed Post: Und diese Möglichkeit der Wahl fehlt mir bei JavaScript. Darüber hinaus gehört das Arbeiten mit Threads meines Erachtens zum Handwerkszeug beim programmieren. Jede Programmiersprache, die Multi-Threading unterstützt, bietet des weiteren geeignete Mittel, um über Threads hinweg sicher auf gemeinsame Objekte zuzugreifen. Entsprechende Konzepte wie Streams tun ihr übriges, um einen sicheren Zugriff zu gewährleisten.

Frage: Dafür bewahrt einen JavaScript mit seinem Thread-Modell vor Problemen der Nebenläufigkeit!

Viele JS-Entwicklende kommen mit Nebenläufigkeit nicht zurecht […]

… und sind deshalb dankbar, dass JavaScript diesbezüglich sehr eingeschränkt ist bzw. betrachten diese Beschränktheit sogar als Vorteil.

Ed Post: …und erzeugt neue Probleme: Gerade wenn es um Transaktionen oder andere gemeinsamen Ressourcen geht, kommt man mit dem JavaScript Thread-Modell schnell an seine Grenzen. Das kann bis hin zu Deadlocks reichen. Ich kenne serverseitige JavaScript -Anwendungen, da müssen mehrere Node.js Instanzen auf einer Maschine hinter einem Load-Balancer geparkt werden, um Probleme mit Transaktionen zu lindern. Diese Applikation würde auf einer einzelnen Node.js Instanz nach kurzer Zeit schlichtweg blocken.

Node.js mit seiner einzigen Main Event-Loop verklemmt schnell […]

… gerade wenn es um Transaktionen oder andere gemeinsamen Ressourcen geht.” [DON19]

Frage: Heutzutage sind doch Rechenleistung und Speicher satt vorhanden, da sollte doch so ein Thread-Modell wie es JavaScript implementiert, gar kein Problem mehr sein…

Ed Post: Stimmt nicht, es gibt Projekte, die sind gescheitert, weil das Serialisieren und Deserialisieren von Durchlaufdaten eine viel zu hohe Rechenlast und viel zu hohen Speicherverbrauch erzeugt haben. Wie gesagt, es wird unglaublich viel Blindleistung produziert. In JavaScript lässt sich das Serialisieren und wieder Deserialisieren, und damit das Kopieren von Objekten, nicht vermeiden, wenn die Main Event-Loop und die Worker an einer gemeinsamen Aufgabe arbeiten. Und die Main Event-Loop muss alle Worker koordinieren, hier müssen die JS-Entwickelnden also zusätzlich aufpassen, dass der Aufwand, z.B. für das Zusammenführen der Ergebnisse, nicht zu hoch wird. Stichwort “Flaschenhals“…

Die Main Event-Loop ist der(!) Flaschenhals einer JavaScript Anwendung schlechthin […]

… sie kann in einer JS-Instanz einfach nicht in die ‘Breite’ skaliert werden.” [HOR18]

WebAssembly als Alternative

Frage: Aber der Austausch von Daten mit WebAssembly sollte doch hochperformant sein, kennt WebAssembly doch das Konzept des Shared Linear Memory und in JavaScript kann man darauf zugreifen. Hier wird also offensichtlich nicht mittels Nachrichtenaustausch zwischen JavaScript und WebAssembly-Threads gearbeitet…

Ed Post: Sehen wir uns erst einmal an, was WebAssembly überhaupt ist. WebAssembly ist Bytecode, der auf einer stackbasierten virtuellen Maschine ausgeführt wird [WEB18]. WebAssembly bekommt gerade einige Features, die das Potential haben, JavaScript abzulösen [GAR17]. Und WebAssembly ist polyglott, was Entwickelnden mehr Freiheiten bei der Auswahl der Werkzeuge gibt. Zusätzlich ermöglicht WebAssembly die Interoperabilität mit JavaScript mittels Shared Linear Memory.

WebAssembly hat das Potential, JavaScript als einzige Programmiersprache für Browser-Anwendungen abzulösen […]

… und sorgt damit zu einer gewissen Aufregung im JavaScript Lager, müssen doch liebgewonnene Gewissheiten in der JavaScript Filterblase überdacht werden.

Frage: Und Shared Linear Memory stellt offensichtlich kein Nachrichtenaustausch dar und ist daher natürlich hochperformant.

Ed Post: Was stimmt ist, dass WebAssembly das Konzept des Shared Linear Memory einführt, um z.B. mit JavaScript an gemeinsamen Anliegen zu arbeiten. Dieser Shared Linear Memory wird aber auf der JavaScript-Seite leider wieder ad-absurdum geführt, auch wenn es wie gemeinsamer Speicher aussieht. Und was die Performance angeht: Auf Seiten von JavaScript ist Shared Linear Memory sicherlich nicht hochperformant.

Frage: Ad absurdum, inwiefern? Nicht performant, inwiefern?

WebAssembly’s Shared Linear Memory und das JS-Speichermodell lassen sich nur unter Anstrengungen unter einen Hut bringen […]

… und zwar auf Kosten der Performance und des Ressourcen-Verbrauchs

Ed Post: Bei Shared Linear Memory handelt es sich um ein Byte-Feld [LYN18]. Also nicht um ein Feld mit Referenzen auf Byte-Objekte - die es übrigens in JavaScript nicht gibt (die Byte-Objekte, Anm.d.Red.), sondern um ein Feld mit linear hintereinander angeordneten Bytes. JavaScript kennt aber nur den “Zahlen”-Typ Number, dargestellt als 64 Bit langer Floating-Point Wert nach IEEE-754 [RIN14].

Frage: Das bedeutet, dass bei der Arbeit mit Shared Linear Memory von und nach Number konvertiert werden muss?

Ed Post: Ja, bei einem Zugriff auf den Shared Linear Memory von JavaScript aus muss einiges an Konvertierung nach Number, also Float, erfolgen. Da in JavaScript alles ein Objekt ist oder wird(!), wird eine Number, je nach Verwendung, noch zusätzlich mit einem Container umgeben [NUM18]. Stichwort Autoboxing [KLI13].

JavaScript kennt bis Dato nur den Zahlentyp Float […]

… während es sich beim Shared Linear Memory von *WebAssembly um ein Byte-Feld handelt.*”

Frage: Und was ist schlimm daran? JavaScript kennt zwar mit Number für Zahlen nur Float nach IEEE 754. Ein Byte passt jedoch leicht in einen Float-Wert nach IEEE 754?

Ed Post: Das stimmt zwar, ein Byte passt in einen solchen Float. Der volle Wertebereich eines Integers übrigens nicht. Ungünstig ist, dass der Zugriff in JavaScript auf den gemeinsamen Shared Linear Memory nicht ohne Aufwand abläuft, auch wenn es für JS-Entwickelnde wie der Zugriff auf ein Number-Array aussieht.

Frage: Inwiefern?

Ed Post: Unter der Haube haben wir es mit einem ArrayBuffer zu tun. Das sind erst einmal rohe Binärdaten [ARR18]. Damit man damit etwas anfangen kann, muss man in JavaScript entweder eine DataView verwenden, die den ArrayBuffer “interpretiert” …

… You can think of the returned object (DataView, Anm.d.Red.) as an “interpreter” of the array buffer of bytes — it knows how to convert numbers to fit within the buffer correctly, both when reading and writing to it …” [DAT18]

Ed Post: … oder auf den ArrayBuffer einen TypedArray setzen, was nur eine array-like Darstellung des ArrayBuffer ist:

… A TypedArray object describes an array-like view of an underlying binary data buffer …” [TAR18]

Ed Post: In beiden Fällen handelt es sich mehr um eine Simulation eines Arrays denn um einen direkten Zugriff auf einen linearen Speicherbereich.

Frage: Das ist doch in Ordnung, solange es für JS-Entwickelnde wie ein Array aussieht, ist es doch egal, wie es technisch realisiert wurde!

Das Mapping des linearen Speichers von WebAssembly nach JavaScript muss […]

… über den Zahlentyp Float und einer Menge indirekter Zugriffe sowie Datentypkonvertierungen bewerkstelligt werden

Ed Post: Es sieht einfach aus, ist aber aufwändig. Bleiben wir beim Beispiel des Shared Linear Memory: Ein Zugriff aus JavaScript auf ein gemeinsames Byte im Shared Linear Memory benötigt nämlich die Konvertierung dieses Bytes in eine Number, und zwar je nach Verwendung eingebettet in einen Container, also als Objekt. Die interne Darstellung einer Number ist ja eine Andere als die des Bytes im Shared Linear Memory, weshalb JavaScript nicht direkt in den Shared Linear Memory referenzieren kann. So ist oder wird(!) in JavaScript irgendwann alles ein Objekt [CRO11]. Wenn es eine Number ist, kann damit weiter gearbeitet werden…

Frage: Ein Array in JavaScript kann aber sehr wohl primitive Typen enthalten [CRO10]…

Ed Post: Und diese wären? Als primitive Typen werden in JavaScript null, undefined, Boolean, Number sowie String gehandelt, die bei Bedarf in Container gepackt werden, um als Objekt verwendet zu werden. Also eine Form des Autoboxing [KLI13]. Diese Typen können nicht durch eine Referenz in ein Byte-Feld, wie es der Shared Linear Memory ist, abgebildet werden. Des weiteren ist in JavaScript ein Array nicht das, was man in anderen Programmiersprachen unter einem Array versteht:

… Traditionally an array reserves a continuous allocation of memory of predefined length. In JavaScript this is not the case. A JavaScript array is simply a glorified object with a unique constructor and literal syntax and an additional set of properties and methods …” [CRO10]

Frage: Das bedeutet, dass dieser Shared Linear Memory nicht 1:1 im JavaScript-Code verwendet werden kann, weil JavaScript ganz anders mit Daten umgeht? Wir können also nicht einfach im Sinne eines Arrays mit primitiven Bytes darüber verfügen?

Ed Post: Genau, das Speicher-Layout und das Zahlenkonzept von JavaScript lässt den Zugriff auf den Shared Linear Memory ohne großen Aufwand nicht zu. Bei jedem Zugriff auf den Shared Linear Memory muss für jedes zugegriffene Byte eine Konvertierung nach Float, das Allozieren von Speicher für den Objekt-Container, und das Zurückgeben einer Referenz auf dieses Objekt an das JavaScript Programm erfolgen. Dieses arbeitet dann über diese Referenz mit dem Wert, bis der Garbage-Collector dieses Objekt abräumen darf.

Frage: Warum eine Konvertierung nach Float?

Ed Post: Weil Number der einzige Zahlentyp in JavaScript ist, und Number wird intern als Float dargestellt.

Frage: Wir haben also im schlimmsten Fall eine Kopie des Bytes als Objekt, die auch noch den Garbage-Collector belastet, während im Shared Linear Memory dieses Byte weiterhin unbekümmert koexistiert?

Ed Post: Richtig. Wir arbeiten also quasi wieder mit einer sehr aufwändigen Darstellung des Bytes, selbst wenn dem JS-Interpreter unter der Haube zwecks Performance noch Tricks beigebracht würden. So wäre es denkbar, dass JavaScript intern verschiedene Repräsentation von Number kennt, also ggf. auch eine interne Byte-Darstellung.Das würde wieder aber Implikationen bei Operationen mit Float-Werten nach sich ziehen. Wie auch immer, es ist immer großer Aufwand für die Speicherverwaltung notwendig, und das alles spiegelt sich in der Ausführungsgeschwindigkeit der Anwendungen wieder.

Frage: Und dann muss ja auch noch bei Schreiboperationen ein Byte zurück in den Shared Linear Memory übertragen werden…

Ed Post: Wobei wir ja das Float-Objekt als Grundlage haben. Also das ganze Spiel in umgekehrter Reihenfolge. Mal eben über einen Array ‘loopen’ ist eben nicht. Das fatale daran ist: Die JS-Entwickelnden “sehen” diesen exorbitanten Overhead nicht, den der JS-Interpreter leisten muss.

Frage: Jetzt wundere ich mich allerdings nicht mehr über die lausige Ausführungsgeschwindigkeit so mancher Website. Große Mengen an Daten (z.B. Bilddaten, Anm.d.Red.) mit JavaScript zu verarbeiten, scheint keine gute Idee zu sein…

Ed Post: Stimmt. Für JS-Entwickelnde sieht alles schön nach Array-Zugriff aus, der Zugriff auf den Shared Linear Memory wird vor dem JS-Programmierer “weg abstrahiert”, so dass sich dieser über die lausige Performance wundert. Was aber unter der Haube passiert, ist eine unglaubliche Verschwendung von Ressourcen. Übertroffen wird die Illusion der Einfachheit durch Operationen wie memory.grow(n), die den Shared Linear Memory Buffer in JavaScript vergrößern können. Dabei wird der Buffer aber keineswegs vergrößert, sondern weggeworfen und ein neuer Buffer alloziert.

… Note: Since an ArrayBuffer’s byteLength is immutable, after a successful Memory.prototype.grow() operation the buffer getter will return a new ArrayBuffer object (with the new byteLength) and any previous ArrayBuffer objects become “detached”, or disconnected from the underlying memory they previously pointed to …” [GRO18]

Frage: Ist da Besserung in Sicht, im Sinne von Lessons learned?

Ed Post: Ja, tatsächlich, und zum Glück nicht im Sinne vom unsäglichen strict mode (lacht). Es wird an einem primitiven Typen BigInt gearbeitet, also einem schmerzlich vermissten Integer-Typen, der dann als TypedArray solch einen Shared Linear Memory optimaler abbilden könnte [BIG18].

Reaktive Programmierung

Frage: Wechseln wir das Thema. JavaScript ermöglicht die Reaktive Programmierung, womit JavaScript extrem responsiv und schnell wird. So etwas hat man in anderen Programmiersprachen noch nicht gesehen!

Reaktive Programmierung ist in JavaScript nicht deshalb so populär, weil “reactive” so toll ist […]

… sondern weil reaktive Programmierung schlichtweg notwendig ist, um mit dem verfügbaren Thread-Modell überhaupt arbeiten zu können. Um mit der Main Event-Loop so etwas, das wie kooperatives Multitasking aussieht, zu simulieren.

Ed Post: Stimmt nicht. Reaktive Programmierung kann man in den allermeisten Programmiersprachen auf die eine oder andere Art umsetzen. Allein das Observer-Pattern [OBS18] ist eine Ausprägung des reaktiven Stils [BER13]. Streams wären eine weitere Ausprägung für reaktive Programmierung. Reaktive Programmierung bedeutet ja am Ende des Tages das Propagieren von Änderungen, um diese zu Verarbeiten, also um eine Verarbeitung von Ereignissen anzustoßen…

… Das zugrunde liegende Ausführungsmodell propagiert Änderungen in den Datenflüssen automatisch. Ein gutes Beispiel für ein Programm, welches reaktiv arbeitet, ist Excel. Ändert man einen Wert in einer Zelle, dann ändert sich auch der Wert in der Summenzelle …” [REA18]

Ed Post: Und nein, reaktive Programmierung macht den Code nicht schneller. Man kann Wartezeiten auf Ergebnisse nutzen, um andere Aufgaben im selben Thread durchzuführen. Das wird bei JavaScript genutzt, ja, man muss es sogar nutzen, um mit dem einen Thread, den man hat (die Main Event-Loop, Anm.d.Red.), halbwegs wirtschaftlich arbeiten zu können. Die Worker-Threads bzw. Web-Worker sind ja leider keine echte Alternative…

Frage: Ein Thread, hier die Main Event-Loop, wird jedoch optimal genutzt!

Ed Post: Ja, ein Thread wird optimaler genutzt, da dieser nur verarbeitet, und nicht wartet. Übrigens, ein wartender Thread bei Programmiersprachen, die Multi-Threading unterstützen, verbraucht keine(!) Rechenzeit, die steht anderen Threads zur Verfügung. Der Kontextwechsel von einem Thread zu einem anderen Thread bedeutet Aufwand, der allerdings dem entsprechen sollte, was die Main Event-Loop in JavaScript leistet, wenn zwischen wartenden und aktiven Callbacks gewechselt wird. Auch hier muss der Kontext der beteiligten Callbacks vorgehalten werden.

Frage: Sie erwähnen die Callbacks. Diese sind ja ein zentraler Bestandteil der reaktiven Programmierung…

Ed Post: Immer wenn beispielsweise in der Main Event-Loop auf ein Ergebnis gewartet wird, geschieht dies über ein sogenannten Callback. Dieser wird mehr oder weniger elegant in Sprachkonstrukten verkleidet, um die Asnychonität zu verschleiern, und der Thread kann weiterarbeiten. Reaktive Programmierung ist jedoch erst dann interessant, wenn die Anforderungen eine solche erfordern, und nicht weil die technischen Gegebenheiten dies Verlangen.

JavaScript bedingt reaktive Programmierung, es gibt dort keine echte Alternative […]

… jedoch ist reaktive Programmierung nur sinnvoll, wenn die Anforderungen dies erfordern, und nicht weil mangels Alternativen kein Weg darum herum führt.

Frage: Wieso? Man kann doch jedes Problem auch reaktiv darstellen.

Ed Post: Das stimmt. In JavaScript will man das aber in Wirklichkeit gar nicht. Durch Programmkonstrukte wird sogar versucht zu verschleiern, dass ein reaktives Programmiermodell hinter dem Code steckt. Reaktive Programmierung ist eben nicht für jedes Problem geeignet und nicht für jede Audienz nachvollziehbar. Der reaktiven Programmierung wird deshalb der Anstrich des Sequentiellen gegeben, da das reaktive Modell nicht immer sinnvoll aber “dank” des JavaScript Thread-Modells notwendig ist.

… With promises we write asynchronous code that emulates synchronous code but with async/await we write asynchronous code that looks like synchronous code. As a consequence this often leads to misconception …” [CLA18]

Ed Post: Man kann übrigens auch jedes Problem mit der Turing-Maschine lösen. Nur wird diese Lösung nicht in jedem Fall wartbarer oder verständlicher. Das Gegenteil ist häufig der Fall. Wenn ich jetzt aus technischen Gründen gezwungen werde, reaktiv zu Programmieren, kann sich meine Lösung nachher als nicht mehr Wartbar erweisen.

Frage: Dafür verhindert die reaktiven Programmierung das Blocken der Main Event-Loop!

Ed Post: Nein, in JavaScript kann trotz reaktiver Programmierung jederzeit die gesamten Main Event-Loop blockieren, da wir hier so etwas wie kooperatives Multitasking betreiben. Im Gegensatz dazu steht echtes Multitasking, etwa mit n Threads in einer Applikation, die auf dieselben Objekte zugreifen können.

Frage: Wir haben heutzutage so viel Rechenpower, da kann man doch locker alles in der Main Event-Loop abfrühstücken!

Ed Post: Nein, ein moderner Prozessor verfügt heutzutage über duzende Cores, je Core sind dank SMP mehrere Hardware-Threads verfügbar [SMT18], also mindestens zwei. Bei 32 Cores haben wir schon 64 Hardware-Threads, bei 64 Cores wären es 128 Hardware-Threads, usw.

** Verikal und horizontal skalieren **

Frage: Der Zukunft gehören also stark parallelisierten Anwendungen mit gemeinsamen Speicher [SYM18].

JavaScript geht einher mit einer alten Denkweise, wo man auf vertikale Skalierung setzen konnte (eine CPU mit immer mehr MHz-Taktung, Anm.d.Red.) und Moores Law immerwährend zu gelten schien […]

… aber in Zeiten, wo man horizontal skaliert (viele CPUs parallel, da sich die MHz-Taktungen nicht mehr beliebig steigern lassen, Anm.d. Red.) und Multi-Core CPUs in jedem PC zum Einsatz kommen, kommt auch das JavaScript Thread-Modell an seine Grenzen

Ed Post: Und jetzt kommt JavaScript: Da haben wir effektiv ein Singe-Threaded Thread-Modell. Wir nutzen also für alles, was in der Main Event-Loop ausgeführt wird, einen einzigen Hardware-Thread von Dutzenden verfügbaren. Wollen wir in JavaScript weitere Threads nutzen, müssen wir die kostspieligen Worker-Threads nutzen, womit wir die Main Event-Loop zusätzlich belasten, sprich Nachrichtenversand und Serialisierung sowie Deserialisierung betreiben. Also keine Rede von gemeinsamem Speicher.

Frage: Nun, im Fall von Node.js startet man einfach mehrere Node.js Instanzen für eine Anwendung!

Ed Post: Starten wir mehrere Node.js Instanzen der gleichen Anwendung, so starten wir mehrere JavaScript Anwendungen in jeweils eigenen Adressräumen auf einer Maschine. Also jeweils in einem eigenen Prozess ohne gemeinsam genutzte Ressourcen. Das kostet, da sich die so gestarteten Prozesse keinen Speicher teilen. Sollen die Node.js Anwendungen am gleichen Anliegen arbeiten, werden diese hinter einem Load-Balancer vereint. Damit das alles funktioniert, muss die Anwendung jedoch “stateless” (Zustandslos, Anm.d.Red.) sein. Und ist sie “stateless”, können die verschiedenen Prozesse nicht an den selben Berechnungen arbeiten oder müssen sich über komplizierte Mechanismen synchronisieren (etwa mittels Nachrichtenversand, Anm.d.Red.).

Frage: Das hört sich nicht effizient an. Und in der Cloud kosten Rechenleistung und Speicher Geld, Bit für Bit, Operation für Operation. Für rechenintensive Aufgaben ist JavaScript offensichtlich nicht geeignet und kann für kleine Lösungen schnell teuer werden …

Für rechenintensive Aufgaben ist JavaScript offensichtlich nicht geeignet […]

… kennt es doch nur eine Main Event-Loop und kostspielige Worker

Ed Post: Die reaktive Programmierung im Fall von JavaScript hilft uns auch nicht bei der Vermeidung von Deadlocks. Etwa wenn mit Datenbank-Transaktionen gearbeitet wird. Da können sich dann die verschiedenen Callbacks gegenseitig blockieren und die Main Event-Loop steht still. Da muss dann tatsächlich mit Kanonen auf Spatzen geschossen werden, etwa mehrere Node.js Instanzen hinter einem Load-Balancer. Hier werden Probleme gelöst, die in einer Multi-Threading Umgebung gar nicht existieren.

Frage: Man kann also sagen, dass die Main Event-Loop der Flaschenhals einer JavaScript Anwendung schlechthin und in Zeiten von Multi-Core Prozessoren nicht mehr zeitgemäß ist?

Bei JavaScript wird man gezwungen, mit schweren Geschützen Probleme zu lösen […]

… die bei Programmiersprachen mit Multi-Threading Unterstützung schlichtweg nicht existieren.

Ed Post: (nickt)

Der JavaScript Paketmanager

Frage: Vor kurzem war zu lesen, dass eine JS-Bibliothek über den Paketmanager npm sog. Malware zum Klauen von BitCoins verteilt hat [BÖC18]. Es gab früher schon einen ähnlichen Fall von Malware in der JS-Szene [PAR18]. Ist das nun typisch für JavaScript?

Ed Post: Das ist jetzt kein typisches JavaScript-Problem, wohl eher ein Problem des Paketmanagers npm, der als Quais-Standard von vielen JS-Entwickelnden zum Auflösen externer JS-Bibliotheken in eigenen Projekten verwendet wird. Das Problem scheint aber symptomatisch für die Attitüde in der JS-Community zu sein…

Frage: Von welcher Attitüde sprechen Sie denn bitte?

Ed Post: Die JS-Community macht allgemein den Eindruck, dass geflickt wird, bis etwas oberflächlich betrachtet passt. Sehen wir uns nur die Objektorientierung, das Thread-Modell oder npm an. Das zieht sich wie ein roter Faden durch die Geschichte von JavaScript.

Die Attitüde zum Thema Sicherheit in der JS-Gemeinde ähnelt derjeniger Microsofts in den 80er und 90er Jahren […]

… wo Sicherheit als lästig bei der Umsetzung von Features angesehen wurde. Damals herrschte das Motto: ‘Lieber ein bequemes Feature als etwas mehr Sicherheit’, Microsoft hält von dieser Praxis mittlerweile Abstand.” (siehe auch [MWV18], vergl. das Vorkommen der Begriffe Security und Feature)

Frage: Was macht npm denn falsch, was solche Sicherheitslücken erlaubt, und was ist denn so symptomatisch an der JS-Community?

Ed Post: Symptomatisch ist, dass die JS-Community gerne für alles Mögliche externe Pakete verwendet (Pakete sind im weitesten Sinne Bibliotheken, Anm.d.Red.). Selbst für die Zahl PI gibt es ein eigenes Paket [ION17]. Je mehr Pakete eine Anwendung “zieht”, desto größer der Angriffsvektor für Schadcode [UPR18].

Frage: Und was macht npm denn falsch?

Ed Post: Die JS-Entwickelnden mögen es gerne “einfach”, und diese Nachfrage bedient npm: Alle Entwickelnden mit einem npm Account können auch gleich weltweit eigene Pakete über npm veröffentlichen. Es findet keine Identitätsprüfung statt. Ein fiktiver Benutzername und dazu ein Passwort sind vollkommen ausreichend [SIG18], um vollwertiges Mitglied der npm Gemeinde zu werden. Und veröffentlichte Pakete müssen nicht signiert werden.

Der JavaScript Paketmanager npm öffent Tür und Tor für Misbrauch […]

… worauf auch die Satire “What Happened When I Peeked Into My Node_Modules Directory” anhand fiktiver Beispiele augenzwinkernd hinweist.” [JSC16]

Frage: Das ist doch gut, so vereinfachtes es doch vieles!

Ed Post: Und das bedeutet, dass sich weder der Urheber eines Pakets nachverfolgen noch die unrechtmäßige Manipulationen von Paketen feststellen lässt. Da gibt es auch eine schöne Story, oder sollte ich besser sagen “Anleitung”, zu diesem Thema:

I’m harvesting credit card numbers and passwords from your site. Here’s how.” [GIL18]

Frage: Es gab ja auch den Fall, dass Pakete wieder aus npm entfernt wurden, womit dann eine Vielzahl von Projekten nicht mehr gebaut werden konnte [WIL16]. Musste npm da nicht erst gehackt werden, damit man daraus nachträglich wieder etwas entfernen kann?

Ed Post: Nein, das ist passiert, weil npm ein so genanntes unpublish unterstützt, also die nachträgliche beliebige Rückabwicklung einer Publikation. Die Dokumentation schreibt dazu lediglich:

… It is generally considered bad behavior to remove versions of a library that others are depending on! …” [UNP18]

Frage: Und ein unpublish kann dann dafür sorgen, dass einige Projekte nicht mehr bauen…

Andere Paketmanager als npm arbeiten da sehr viel restriktiver […]

… was das Publizieren oder Löschen von Modulen angeht. So liegt die Latte für Schindluder bei npm extrem niedrig.

Ed Post: Man kann sich bei npm nicht darauf verlassen, dass dort irgendetwas Bestand hat und es ist sehr einfach für den Urheber einer JS-Bibliothek, diese, selbst wenn von Anderen verwendet, einfach wieder aus npm zu löschen. So etwas kennt man von entsprechenden Tools bei anderen Ökosystemen so nicht. Dort ist der Prozess sehr viel aufwändiger, wenn z.B. aus rechtlichen Gründen wieder etwas aus den offiziellen Verzeichnissen der Paketmanager genommen werden muss.

Designfehler

Frage: Früher wurde viel über JavaScript Designfehler gesprochen. Jetzt ist es still um dieses Thema geworden. War das eine überhitze Diskussion?

Alle Programmiersprachen haben ihre Designfehler […]

… einfach weil keine einzige Sprache perfekt sein kann, so wie es bei (fast) allen anderen Dingen auch ist.

Ed Post: Ich provoziere mal und sage nur: Comparisons, Equality, Identity, Autocasting oder Primitives:

Comparison: “<, <=, =, >=, >

null >= 0;			// --> true, WTF?
null <= 0;			// --> true, WTF?
null == 0;			// --> false, OK
null > 0;			// --> false, OK
null < 0;			// --> false, OK

Equality: “==

6 == 6;				// --> true, OK
6 == [6];			// --> true, WTF?
6 == [[6]];			// --> true, WTF?
'' == '0';			// --> false, OK
0 == '';			// --> true, WTF?
false == null;			// --> false, OK
false == undefined;		// --> false, hmm?
false == '0';			// --> true, WTF?

Identiry: “===

6 === 6;			// --> true, OK
6 === [6];			// --> false, OK
6 === [[6]];			// --> false, OK
'' === '0';			// --> false, OK
0 === '';			// --> false, OK
false === null;			// --> false, OK
false === undefined;		// --> false, OK
false === '0';			// --> false, OK

Autocasting:

result = '4' + 2;		// result --> "42" (String), OK
result = '4' - 2;		// result --> 2 (Number), WTF?

Primitives:

undefined = 42;			// undefined --> 42 (Number), WTF primitives?

Frage: Sind das jetzt Designfehler?

Ed Post: Zum einen muss man vielleicht zwischen echten Designfehlern und evolutionärer Weiterentwicklung einer Programmiersprache unterscheiden. Zum Anderen kann man das Thema bezüglich der Nachvollziehbarkeit beurteilen, also nach dem Motto: “Kann ich mir das Verhalten der Programmiersprache in bestimmten Fällen herleiten oder nicht?

Frage: Und hier hatte JavaScript einige “Features” zu bieten, die es in sich hatten. Ich spreche hier von Diskussionen wie [PET12], [ENG17], [ENG16], [DIS16], [MIC15], [ENG15], [CUN10] sowie die legendäre “Designfehler”-Liste auf [WTF16], Beiträge mit so sprechenden Titeln wie “JavaScript is a Dysfunctional Programming Language”, “10 design flaws of JavaScript”, “Why is JavaScript so hated?” oder schlicht “WTFJS” (What the Fuck JavaScript, Anm.d.Red.) …

Ed Post: (Fast) jede Programmiersprache hat Designfehler. Auch ist es oft strittig, ob etwas ein Fehler oder eine Funktion ist. So werden Bugs gerne als undokumentierte Features proklamiert [UND18]. Tatsache ist, dass mit TypeScript und den fortschreitenden Versionen von ECMAScript sowie mit Dart versucht wird, JavaScript ‘besser’ zu machen bzw. mit Lintern Programmierfehler zu vermeiden. Es wird ein Bedarf gedeckt, den JavaScript selber nicht bedienen kann.

Frage: Und TypeScript, Linter oder Dart bügeln die Designfehler dann auch aus?

Ed Post: Jein. So ist etwa jeder JavaScript-Code ist auch vollwertiger TypeScript-Code:

… TypeScript is a strict superset of ECMAScript 2015, which is itself a superset of ECMAScript 5, commonly referred to as JavaScript. As such, a JavaScript program is also a valid TypeScript program, and a TypeScript program can seamlessly consume JavaScript …” [COM18]

Tool Fatigue

Ed Post: Da Abwärtskompatibliltät stets gewahrt bleibt, ist in TypeScript weiterhin alles möglich und rechtens, was in JavaScript möglich und rechtens ist, also die ganze Liste aus [WTF16]. Hier kommen dann die Linter ins Spiel, das sind Werkzeuge, die den Skript-Code auf problematische Stellen hin untersuchen. Am Ende wird mittels Transpilern aus TypeScript oder Dart dann JavaScript erzeugt. Dart hat es allerdings einfacher, ist es doch eine eigenständige Programmiersprache, die nicht auf Abwärtskompatibliltät zu JavaScript achten muss. Es wird aber dennoch nach JavaScript transpiliert.

Frage: Und das Transpilieren sowie das Linten vermeiden dann schlechten Stil im JavaScript-Code?

Ed Post: Sowohl für TypeScript- wie auch für JavaScript-Code, ja. Jetzt verlieren wir aber die Einfachheit der JavaScript Entwicklung, die überall lautgesprochen wird: Wir müssen transpilieren und linten, also Transpiler und Linter in unsere Toolchain einbinden, um nur Einige zu nennen [UMA18]. Zusätzlich mit npm als Paketmanager ist eine JS-Entwicklungsumgebung alles andere als einfach. Zusätzlich wird gerne auch noch minifiziert, um etwa die Download-Größe des JavaScript-Codes für den Browser zu reduzieren.

JavaScript Tool Fatigue […]

… eine typische JS-Entwicklungsumgebung mit zugehöriger Toolchain ist mittlerweile alles andere als einfach.

Ed Post: Und will man sinnvoll Debuggen, braucht es Source-Maps, die den Bezug zwischen TypeScript bzw. minifiziertem JavaScript bzw. Dart und dem zugrunde liegenden JavaScript herstellen [USE18]. Diese Source-Maps müssen irgendwo erzeugt werden, also meist beim Transpilieren, und irgendwo unterstützt werden, also meist beim Debuggen. Alles zusammen, also Linten, Transpilieren, Minifizieren, Source-Maps einbinden und Repositories wie npm integrieren, wird tatsächlich schon als “JavaScript Tool Fatigue” bezeichnet [DEV16].

Frage: Einfach geht anders. All das sind ja Hilfsmittel, um mit Anstand um die Designfehler herumzueiern…

Ed Post: Ganz zu schweigen von den Stolperfallen, wie der anfangs erwähnte strict mode oder fehlende Integer-Datentypen.

Zurück zur Ausgangsfrage

Frage: Kommen wir zurück zur Ausgangsfrage: “Ist JS eine General Purpose Sprache? Ja oder Nein?

Ed Post: Am Ende des Tages nähern sich die aktuellen Programmiersprachen aneinander an. JavaScript bekommt immer mehr Konstrukte, die wir aus Python, C# oder Java kennen. So etwa das Modulkonzept ab ECMAScript 6 [BUC18] oder die klassenbasierte Objektorientierung ab ECMAScript 2015 [VER18]. Auch könnte es bald Integer-Typen in JavaScript geben [BIG18].

Ist JS (k)eine General Purpose Sprache? […]

… nun, JavaScript ist Turing-Vollständig …” [RAJ16] “… doch wer tut sich schon die Turing-Maschine als Programmiersprache an?” [TCO18]

Frage: Finden auch Konzepte aus JavaScript in anderen Programmiersprachen Eingang?

Ed Post: Andersherum geht es ebenso: Es finden viele funktionale Elemente Eingang in andere Sprachen, wie wir es gerade bei den JVM-Sprachen beobachten können [JVM18] oder sind schon lange Bestandteil dieser, wie etwa bei Pascal oder Smalltalk. Funktionale Programmierung gibt es ja nicht erst seit JavaScript [FUN18].

Frage: Werden wir irgendwann dann eine einheitliche Programmiersprache haben, die das Sinnvolle aus anderen Welten integriert und das weniger Sinnvolle außen vor lässt?

Ed Post: Ich glaube nicht, dafür sind die Konzepte der verschiedenen Programmiersprachen an zu vielen Stellen zu unterschiedlich. Es wird viel getan, damit man alles, was in anderen Programmiersprachen möglich ist, auch mit JavaScript realisieren kann, aber der Preis ist hoch.

Frage: Inwiefern unterschiedlich?

Ed Post: JavaScript wird interpretiert, andere Sprachen werden kompiliert. Damit verbunden ist die Frage, wann ich Fehler finde. Bei interpretierten Sprachen eigentlich erst zur Laufzeit, dafür müssen diese nicht erst zeitaufwändig kompiliert werden. Dieser Vorteil fällt weg, da das Linten wesentlicher Bestandteil bei der JavaScript-Entwicklung ist. Dann haben wir es bei JavaScript mit dynamischer sowie schwacher Typisierung zu tun. Wir können Typen zur Laufzeit implizit umwandeln, mit obskuren Ergebnissen [WTF16].

Frage: Was wäre die Alternative?

Ed Post: Bei anderen Programmiersprachen kann man sich auf einen Typen verlassen, wenn dieser einmal definiert wurde, und zwar beim Kompilieren und nicht erst beim Kunden zur Laufzeit. Dafür muss man sich zur Übersetzungszeit festlegen. Es wird immer ein “Für und Wider” geben bezüglich dieser Konzepte.

Ein Fazit

Frage: Und welches Fazit würden Sie ziehen?

Ed Post: Das Thread-Modell von JavaScript halte ich bei der aktuellen Prozessor-Entwicklung für anachronistisch. Die Taktfrequenzen können bei CPUs nicht mehr beliebig gesteigert werden, also wird dies bei aktuellen Prozessoren durch Parallelisierung kompensiert. Diese Parallelisierung wird nicht gut bis sehr schlecht von JavaScript unterstützt [RUN18].

a) Das Thread-Modell von JavaScript passt nicht mehr zu aktuellen Prozessoren.

Ed Post: Gerade beim Internet der Dinge fehlt mir bei JavaScript die Interoperabilität mit der Hardware, also die gemeinsame effiziente Nutzung von Speicher, was durch fehlende primitive Ganzzahltypen und der ganz eigenen Interpretation des Arrays-Konstrukts befeuert wird [CRO10].

b) Das Speicher-Layout von JavaScript erschwert die Interoperabilität auf Low-Level Ebene.

Ed Post: Der saloppe Umgang mit Paketen beim De-facto-Standard npm [BÖC18] sowie die hohe Zerfallsrate bei JS-Bibliotheken birgt unnötige Sicherheitsrisiken und lässt Softwaresysteme veralten, sobald diese produktiv sind [MOS15]. Ganz zu schweigen vom Schindluder, das mit npm Paketen getrieben wird [UPR18].

c) Der Paketmanager npm birgt ein hohes Risiko was die Aktualität der eigenen Software wie auch das Einbinden von Schadcode in eigenen Anwendungen angeht.

Ed Post: Mit WebAssembly ist eine echte polyglotte Alternative zu JavaScript am Start, und das ohne die ohne Unzulänglichkeiten von JavaScript. Mit steigender Verbreitung von WebAssembly wird die Verwendung von JavaScript sinken. JavaScript wird dann wieder für das verwendet werden, für das es ursprünglich konzipiert wurde, also als “‘glue language’ for the Web designers and part time programmers” [RAU11].

d) WebAssembly wird wohl JavaScript mittelfristig als einzige Programmiersprache für Browser-Anwendungen abzulösen.

Ed Post: Und einfach bzw. ohne Boilerplate ist die ernsthafte JavaScript-Entwicklung schon lange nicht mehr zu meistern [DEV16]. JavaScript war ja lange die Lingua franca des Webs.

e) JavaScript hat als Programmiersprache wohl nur deshalb eine gewisse Verbreitung, weil es Bestandteil eines jeden Browsers ist und damit auf jedem Computer verfügbar.

JavaScript war lange Zeit die Lingua franca des Webs […]

… weil Alternativen für Browser-Anwendungen wie WebAssembly gefehlt haben, was sich aber gerade mit viel Schwung ändert. [MCC17]

Frage: Das spannt den Bogen von unserer anfänglichen Aussage, dass JavaScript die Lingua franca des Webs ist. Haben sie noch ein Schlusswort für unsere Leser?

Ed Post: Zusammenfassend komme ich zu dem Schluss, dass ich für kritische Systeme strikt von JavaScript abrate.

Frage: Vielen Dank für das anregende und erhellende Gespräch! Hoffen wir, dass das ein Weckruf für den einen oder anderen Programmierer sein wird! Haben Sie noch ein Schlußwort für uns?

Schlusswort von Ed Post

Ed Post : Mit WebAssembly werden endlich performantere und ressourcenschonendere Desktop-Anwendungen (als mit JavaScript realisierbar, Anm.d.Red.) ermöglicht, und das ganze Plattform-Übergreifend: Denn die entsprechenden Browser laufen auf Personalcomputern bis hin zum Raspberry PI sowohl unter Windows wie auch unter MacOS oder Linux, ganz zu schweigen von Android oder iOS auf Smartphones. Irgendwann wird also jemand auf die Idee kommen:

Hey, die WebAssembly VM (Virtual Machine, Anm.d.Red.) kann man doch vorzüglich auch außerhalb des Browsers verwenden!”.

Ed Post … Also wird es WebAssembly-Laufzeitumgebungen ausserhalb des Browsers geben, auf dem Server, auf dem PC, auf dem Einplatinencomputer, im Smartphone oder in der Cloud. Das wird unausweichlich sein. Wenn das passiert, wird das Konzept HTML/CSS/DOM (DOM steht für Dokument Object Model, Anm.d.Red.) nur eine Möglichkeit von vielen sein, um graphische Oberflächen zu bauen …

Die WebAssembly-VM wird aus dem Browser ausbrechen […]

… um als eigenständige VM sowohl Server- wie auch Client-, Desktop-, PC-, Single-board Computer, Smartphone- oder Cloud-Anwendungen zu betreiben, und das bei einer reichhaltingen Auswahl an möglichen Programmiersprachen und deren Ökosystemen.” [EPO19]

Ed Post … Andere GUI-Frameworks, vielleicht sogar bessere, gar andere Ökosysteme, werden adaptiert werden. JavaScript wird zwangsläufig an Bedeutung verlieren und Sprachen wie C++, Python, Java bzw. die .NET basierten Sprachen werden eine (alternative, Anm.d.Red.) VM bekommen. Damit wird das polyglotte Ökosystem schlechthin entstehen. JavaScript wird sich dann wieder auf seine Nische “Glue-Code” im Browser zurückziehen oder nach WebAssembly compilieren. Je mehr WebAssembly an Bedeutung gewinnt, desto mehr wird JavaScript an Bedeutung verlieren.


[ABO18]: MDN, 2018
[ARR18]: MDN, 2018
[BER13]: Michael Berry, 2013
[BIG18], Chrome Platform Status, 2018
[BLA18]: Wikipedia, 2018
[BÖC18]: Hanno Böck, 2018
[BUC18]: Craig Buckler, 2018
[CLA18]: Luc Claustres, 2018
[COM18]: Wikipedia, 2018
[CRO10]: Angus Croll, 2010
[CRO11]: Angus Croll, 2010
[CUN10]: Cunningham & Cunningham, 2010
[DAT18]: MDN, 2018
[DON19]: Node.js, 2019
[DST18]: Wikipedia, 2018
[DEV16]: Anne Dev, 2016
[DIS16]: Peter DiSalvo, 2016
[DUC18]: Wikipedia, 2018
[ECM18]: Wikipedia, 2018
[EMB16]: Ember, 2016
[ENG15]: Richard Kenneth Eng, 2016
[ENG16]: Richard Kenneth Eng, 2016
[ENG17]: Richard Kenneth Eng, 2017
[EPO19]: Ed Post, 2019
[FUN18]: Wikipedia, 2018
[GAR17]: Andreas Rossberg, 2017
[GIL18]: David Gilbertson, 2018
[GRO18]: MDN, 2018
[HOR18]: Wikipedia, 2018
[ION17]: Ionică Bizău, 2017
[JAV18, Wikipedia, 2018
[JSC16]: Jordan Scales, 2016
[JSG18]: Wikipedia, 2018
[JVM18]: Wikipedia, 2018
[KLI13]: Felix Kling, 2013
[LYN18]: WebAssembly, 2018
[MCC17]: Judy McConnell, 2017
[MIC15]: James Mickens, 2015
[MOS15]: Brian Moschel, 2015
[MWV18]: Wikipedia 2018
[NUM18]: MDN, 2018
[NYS13]: Bob Nystrom, 2013
[OBS18]: Wikipedia, 2018
[PAR18]: Matthias Parbel, 2018
[PET12]: Ke Pi, 2012
[POS82]: Ed Post, 1982
[PRI18]: Wikipedia, 2018
[PRO18]: Wikipedia, 2018
[RAJ16]: Raja Rao, 2016
[RAU11]: Dr. Axel Rauschmayer, 2011
[REA18]: Wikipedia, 2018
[RIN14]: Brian Rinaldi, 2014
[RUN18]: MDN, 2018
[SIG18]: NPM, 2018
[SLO18]: MDN, 2018
[SMT18]: Wikipedia, 2018
[STR18]: MDN, 2018
[SYM18]: Wikipedia, 2018
[TAR18]: MDN, 2018
[TCO18]: Wikipedia, 2018
[TYP18]: Wikipedia, 2018
[UND18]: Wikipedia 2018
[UMA18]: u/marosurbanec, 2016
[UNP18]: NPM, 2018
[UPR18]: u/ProbablyNotCanadian, 2018
[USE18]: MDN, 2018
[VER18]: Wikipedia, 2018
[WEA18]: Wikipedia, 2018
[WEB18]: Wikipedia, 2018
[WIL16]: Chris Williams, 2016
[WIN14]: Topher Winward, 2014
[WTF16]: Brian LeRoux & Andrew Lunny, 2016
[YOU08]: Michael Youssef, 2008