Statt über Smalltalk zu berichten, erscheint hier zunächst ein Artikel über die Entwicklung von JavaScript und seine funktionalen Sprachelemente


ES6 – Die Javascript-Revolution

Tim Berners-Lee's Law:
"The Rule of Least Power"
Computer Science spent the last forty years making
languages which were as powerful as possible.
Nowadays we have to appreciate the reasons for picking
not the most powerful solution but the least powerful
.
w3.org/2001/tag/doc/leastPower-2006-02-23.html

Atwood's Law:
any application that can be written in JavaScript,
will eventually be written in JavaScript.
blog.codinghorror.com/the-principle-of-least-power

49 „Programmiersprachen bzw. –systeme, die in der Schule eingesetzt wurden“ hat Bernhard Koerber seiner „kleinen Geschichte der Programmiersprachen in der Schule“ in einer Übersicht aufgezählt. Dabei ist eine Sprache übersehen worden, die bereits mehrfach in LOG IN behandelt wurde, die jeder Schüler zumindest passiv benutzt und die im Ranking der Programmiersprachen, die in Stellenanzeigen in den USA, GB und Australien am meisten gesucht werden und noch vor JAVA auf Platz 1 steht: die Lingua franca des Internets.

Die Tabelleneinträge seien hier nachgeholt:
Name: LiveScript, JavaScript, ECMA-Script, ES5, ES6.
Bedeutung des Namens: Der Name ist eine reine Marketingentscheidung.
Jahr der Einführung: 1996
Entwickler: Brendan Eich.
Merkmale: Prototypbasiertes Objektsystem mit funktionalen Elementen


JavaScript war von Anfang an als funktionale Programmiersprache angelegt. Im April 1995 wurde Brendan Eich von Netscape mit dem Argument angeworben, "den Browser um (das funktionale) Scheme zu erweitern" [Hug, 2012]. Um alle Zweifler (am Sinn einer zweiten Sprache neben Java) zu überzeugen, schreibt Eich, "musste ich innerhalb von zehn Tagen eine Demoversion erstellen (Das entbehrt nicht einer gewissen Ironie, da Java aus den Browsern fast vollständig verschwunden ist, während JavaScript auf der Clientseite die entscheidende Sprache wurde.) Ich arbeitete Tag und Nacht und baute daher ganz unvermeidlich ein paar Designfehler in die Sprache ein (die aus der Evolution von LISP übernommen wurden)".

Registermaschine

Bild 1: Registermaschine

4 Stifte gleichzeitig

Bild 2: Turtelgrafik mit drei Stiften

Quicksort funktional

Bild 3: Quicksort-Demo

"JavaScript wird eines Tages untergehen", sagt Bredan Eich (vergl. Seibel, 2010), plädiert aber gleich für weitere Verbesserungen seiner Programmiersprache.

Einige Ausdrucksmöglichkeiten von JavaScript sollen anhand dreier Beispielprogramme aus dem Informatikunterricht dargestellt werden.

Registermaschine

Turtlegrafik

Quicksort

Mit HTML5 hat sich das Erstellen einer validen HTML-Seite sehr vereinfacht. Bild 1 zeigt ein mögliches Ergebnis mit Ein- und Ausgabemöglichkeit. Aber es geht noch viel einfacher, wenn man auf HTML ganz verzichtet und einfach die Funktionsdeklaration im <script>-Attribute in eine Browser- oder Node.js-Konsole eingibt.



Vererbung: klassisch oder modern?

JavaScript ungleich Java

"Java verhält sich zu JavaScript wie Ham zu Hamster", steht es in [MEH, 2013]. Anderere Vergleiche im Internet sind "car und carpet" oder "Ei und Eiffelturm". Die meisten der zahlreichen englisch- oder deutschsprachigen Büchern zu JavaScript ...

Emoji-Demonstration
Joey Devilla demonstriert die Funktionsweise von map, filter und reduce mit Emojis → und einer Implementation in Swift

// eine vereinfachte Version von reduce
// mit dem ternären Vergleichsoperator (? :) 

const reduziere = ([kopf, ...rest], fn, akku) =>
    typeof kopf === 'undefined'
    ? akku
    : reduziere(rest, fn, fn(akku, kopf));

reduziere([2,3,7], (a, b) => a * b, 1); // 42

const essen = (akku, x) => akku+"💩 ";
reduziere(["🍔", "🍕", "🍰"], essen, "");
// "💩 💩 💩 "

// map und filter mittels reduziere

const map = (arr, fn) =>
    reduziere(arr, (a, b) => a.concat(fn(b)), []);

map([5,6,7], (x) => x*x); // [25, 36, 49]

const filter = (arr, fn) => reduziere(arr, 
   (a, b) => fn(b) ? a.concat(b) : a, []);

filter([5,55,33,4], (x) => x > 10); // [55, 33]

const quicksort = ([kopf, ...rest]) =>
    kopf === undefined // Liste leer?
    ? []               // ja, dann []
    : [].concat(       // sonst:
        quicksort(filter(rest, (a) => a < kopf)),
        kopf,     
        quicksort(filter(rest, (a) => a >= kopf))
    );

let liste = [5,1,3];
console.log(quicksort(liste), liste);
// [1, 3, 5] [5,1,3]

Fazit – JavaScript jetzt?

Crockford, D., Das Beste an JavaScript, Köln: O'Reilly, 2008
Hughes-Croucher, T; Wilson, S.: Einführung in Node.js, Köln: O'Reilly, 2012, S. XI
Seibel, P.: Coders at Work. Bedeutende Programmierer und ihre Erfolgsgeschichten. mitp-Verlag, 2011



Teil 2

ES6 Beispiele

Beispiel 1: Funktionsabschlüsse (Closures)

In der Fabrikfunktion "erzeugePerson" sind zehn Variablen zu unterscheiden, fünf im Funktionsabschluss (vname, name, ort, getOrt und getName) und fünf Eigenschaften des zurückgegebenen Objekts (vname, name, alter, getOrt, getName) . Die Variablen name und ort bleiben auch nach dem Beenden der Funktion erzeugePerson in einem Funktionsabschluss (Closure) erhalten und zwar für jedes Personenobjekt getrennt. Das gilt allerdings nur solange wie es Funktionen gibt (gibOrt, gibNamen), die auf sie zugreifen. Mit der Anweisung "person1.gibNamen = function () {return this.name;};" wäre die Variable name im Funktionsabschluss nicht mehr erreichbar, sondern nur die Eigenschaft name, und kann bei einer Speicherbereinigung entfernt werden. Pfeilfunktionen sind insbesondere bei der funktionalen Programmierung sehr praktisch.

let erzeugePerson = (vname, name) => {
  let ort = "Buxtehude";
  let gibOrt = () => ort;
  let gibNamen = () => name;
  return {
    "vname": vname,
    "name": name,
    "alter": 0,
    "gibOrt": gibOrt,
    "gibNamen": gibNamen,
  };
};

let person1 = erzeugePerson("Grete", "Meier");
let person2 = erzeugePerson("Peter", "Meier");

// einige Tests und ihre Rückgabewerte
console.log(person1.name, // 'Meier'
person1.name = "Müller",
person1.name, // 'Müller'
person2.name, // 'Meier'
person1.gibNamen(), // 'Meier'
person1.ort = "Berlin",
person1.ort, // 'Berlin'
person2.ort, // undefined
person1.gibOrt() // 'Buxtehude'
);

Beispiel 2: Pfeilfunktionen und Funktionen als Methoden

Bei den neuen Pfeilfunktionen (wie g) erfolgt die Bindung an das this-Objekt bei der Definition und kann nicht mehr geändert werden, während bei der "normalen" Funktion (wie f) this das Objekt referenziert, von dem aus die Funktion ausgeführt wird.

Die bisherigen Funktionen sollten für Methoden verwendet werden, während die Pfeilfunktionen für die funktionale Programmierung besonders geeignet sind.

var person = {};
var ID = 42; //globale Variable
const f = function () {
    return this.ID; 
};
const g = () => this.ID; // this === Window

person.ID = Math.random(); // 0.165700
person.gibID = f;
person.getID = g; 
person.gibID(); // 0.165700
person.getID(); // 42

Beispiel 3: Verkettung von Funktionen

const erzeugeMult = (faktor) => (x) => faktor * x;
const mwSteuer19 = erzeugeMult(1.19);
mwSteuer(200); // 238

const erzeugeAdd = (a) => (x) => a +x;
const mitPorto5 = erzeugeAdd(5);
const endPreis = (x) => mitPorto5(mwSteuer19(x));
endPreis(200); // 243

Beispiel 4: Rest-Operator und destrukturierende Zuweisung

Der Rest-Operator (...) erleichtert funktionales Programmieren mit JavaScript außerordentlich. Bei der print-Funktion tritt er zweimal auf. Die Paramterliste wird in der Reihung args gesammelt und die Elemente dieser Reihung werden einzelnd als Parameter in die log-Funktion eingesetzt. Die destrukturierende Zuweisung erspart eine temporäre Variable beim Tausch von x und y und vereinfacht die Aufteilung

const add = (...args) =>
    args.reduce((a, b) => a+b);
add(9, 10, 11, 12); // 42

const print = (...args) =>
    console.log(...args);

let x = 5, y = 8;
[x, y] = [y, x];
print(x, y); // 8 5

let liste = [9, 10, 11];
[kopf, ...rest] = liste;
print(kopf, rest, liste); // 9 [10, 11] [9, 10, 11]


Beispiel 5: Vorbelegte Parameter und Endrekursion

Will man endrekursive Funktionen schreiben, dann benötigt man meistens einen oder mehrere Akkumulatoren (hier vorbelegte Paramerter) zum Speichern von Zwischenergebnissen. Endrekursion wird von künftigen Browsern unterstützt werden.

const fakultaet = (n, akku = 1) =>
    n <= 1
    ? akku
    : fakultaet(n - 1, n * akku);

fakultaet(69) // 1.7112245242814127e+98

const fibonacci = (n, f1 = 1, f2 = 1) =>
     n === 1
     ? f1
     : fibonacci(n-1, f2, f1 + f2);
fibonacci(69) // 117669030460994

Beispiel 6: Quicksort funktional

Mit den neuen Sprachelementen von ES6 lässt sich Quicksort funktional und damit deklarativ ohne zusätzliche Variablen und ohne Seiteneffekte programmieren.

const quicksort = ([kopf, ...rest]) =>
    kopf === undefined // Liste leer?
    ? []               // ja, dann []
    : [].concat(       // sonst:
        quicksort(rest.filter((a) => a < kopf)),
        kopf,     
        quicksort(rest.filter((a) => a >= kopf))
    );

quicksort([5,1,3]); // [1,3,5]

// Zeitkomplexität testen:
let test = function (n) {
    "use strict";
    let i, start, reihung = [];
    for ( i = 0; i < n; i += 1 ) {
        reihung.push(Math.random());
    }
    start = Date.now();
    quicksort(reihung); // reihung.sort()
    return (Date.now() - start) ;
};

test(2E6) / test(1E6) // 2.098208770846201

Das passt zur Kostenfunktion T(n) = n*log(n). Aber wie schnell wird sortiert?

test(1E6) // 4776

1000000 Elemente in knapp 5 s. Zum Verleich wird in der Funktion test2 "reihung.sort()" verwendet. sort ist eine eingebaute Arrayfunktion von JavaScript.

test2(1E6) // 5861

Das Ergebnis kann nicht stimmen und den Grund erkennt man bei folgenden Tests:

[3, 1, 12, 5, 1].sort(); // [1, 1, 12, 3, 5]
[3, 1, 12, 5, 1].sort((a, b) => a - b) // [1, 1, 3, 5, 12]
[3, 1, 12, 5, 1].sort((a, b) => b - a) // [12, 5, 3, 1, 1]

Der Array-Methode sort muss eine Vergleichsfunktion als Parameter übergeben werden, ansonsten wird lexikographisch sortiert. Die anonyme Vergleichstfunktion bestimmt im Beispiel, ob die Zahlen auf- oder absteigend sortiert werden. Ein Funktionsaufruf test3(1E6) mit Sortierung nach Größe ergibt damit z. B. 1406 (ms). Auch eine Reihung von Personen kann auf diese Weise sortiert werden, ohne den sort-Algorithmus neu schreiben zu müssen.

[person1, person2].sort((a, b) => a.alter - b.alter);

Beispiel 7: UPN-Terme

let upn = [[[2, 2, "*"], 3, "+"], [2, 4, "+"], "*"];

const plaette = (liste) => liste.reduce( //       *
        (akku, element) => akku.concat(  //     /   \
            Array.isArray(element) ?     //    +     +
            plaette(element) :           //   / \   / \
             element                     //  *   3 2   4
        ),                               // / \
    [] // Startwert für akku               2   2
);

plaette(upn); // [2, 2, "*", 3, "+", 2, 4, "+", "*"]
plaette(upn).join(" "); // "2 2 * 3 + 2 4 + *"

const berechne = (op1, op2, op) => { // Hilfsfunktion
        if (op == "+") return op2 + op1; 
        if (op == "-") return op2 - op1;
        if (op == "*") return op2 * op1;
        if (op == "+") return op2 / op1;
    };
    
berechne(45, 49, "-"); // 4

var berechneUPN = (upnString) => 
        upnString.split(" ")
        .reduce((stapel, element) =>  // stapel als Akkumulator
            ["+", "-", "*", "/"].includes(element) ?
            stapel.concat([berechne(stapel.pop(),  stapel.pop(), element)]) :
            stapel.concat([+element]) // Typwandlung ergibt Zahl
        , [] // beginne mit leerem Stapel
        )[0];
        
berechneUPN(plaette(upn).join(" ")); // 42
berechneUPN("49 45 - 3 + 2 4 + *") === 42 // true