Erste Schritte in ClojureScript
Funktionale Programmierer möchten auch in der Web-Entwicklung auf dem Browser ihre bevorzugte funktionale Sprache nutzen. Zwar hat JavaScript Wurzeln in der funktionalen Programmiererung, aber die Sprache hat eben auch noch eine objektorientierte und verschiedene hässliche Seiten. Aus diesem Grund gibt es inzwischen eine kleine Industrie von Compilern von anderen Programmiersprachen nach JavaScript, zum Beispiel für OCaml, Scala, Haskell und Racket.
Heute beschäftigen wir uns mit einer besonders populären funktionalen Sprache mit JavaScript als Ziel: ClojureScript.
ClojureScript ist ein Dialekt von Clojure, und damit ein dynamisch getypter Lisp-Dialekt mit besonderer Betonung auf rein funktionalen Datenstrukturen. ClojureScript ist nicht identisch mit Clojure - es gibt eine Reihe subtiler Unterschiede, z.B. bei der Repräsentation von Zahlen, beim Feld-/Methodenzugriff oder bei Makros. In diesem Beitrag interessiert uns vor allem aber ein schneller Einstieg, um ein Gefühl für die Sprache, ihr Ökosystem und die Anbindung von JavaScript-Frameworks zu entwickeln. Programmieren mit ClojureScript macht nämlich richtig Laune, vor allem für Leute wie mich, die kaum JavaScript können..
Projektstart
Wie bei Clojure sonst auch ist bei der ClojureScript-Entwicklung Leiningen unerlässlich, das Standard-Build-Tool für Clojure-Projekte. Leiningen erlaubt das Anlegen eines Projektskeletts mit Hilfe im Internet hinterlegter Schablonen. Wir brauchen ein minimales ClojureScript-Projekt, das ist mit einem Befehl gemacht:
lein new mies cljs-hello
mies
ist der Name der Schablone - der Befehl macht ein Verzeichnis
namens cljs-hello
, in dem sich das Projekt befindet. Dieses kann wie folgt in Betrieb genommen werden:
cd cljs-hello
./scripts/watch
Das Skript sorgt dafür, dass der ClojureScript-Code im Projekt nach JavaScript compiliert wird und hält danach Ausschau nach Änderungen. Wenn es welche feststellt, wird recompiliert.
Wenn lein cljsbuild
einmal durchgelaufen ist, ist die Datei
index.html
(ebenfalls von lein new
generiert)
im Projekt-Wurzelverzeichnis fertig zum Aufmachen mit dem
Browser. Konfiguriert ist das ganze in der Leiningen-Konfiguration
project.clj
.
Programmieren mit ClojureScript
In der Datei src/cljs_hello/core.cljs
ist die eigentliche
Webanwendung. Vorgefertigt ist da nur das folgende kümmerliche
Programm:
Die erste Zeile ist die aus Clojure bekannte Namespace-Deklaration.
(enable-console-print!)
sorgt dafür, dass println
und Konsorten in
die Konsole des Browsers drucken. Es empfiehlt sich also, die
JavaScript-Konsole im Browser aufzumachen. (Im Firefox über Tools ->
Web Developer -> Web Console
zum Beispiel, im Chrome über View ->
Developer -> Developer Tools
.)
Der Code wird von index.html
aus geladen:
Von cljs_hello.js
schließlich wird der ganze generierte
JavaScript-Code geladen, zusammen mit den benötigten Libraries.
Programmieren in ClojureScript
Da die direkte Manipulation des DOM von JavaScript aus wenig Spaß macht, empfiehlt sich auch in ClojureScript die Verwendung eines GUI-Frameworks. Besonders gut passt zum Beispiel das kürzlich von Facebook und Instagram veröffentliche React, das insbesondere den Konzepten aus der funktionalen Programmierung sehr nah ist:
React-Komponenten sind Objekte mit beliebigem inneren Zustand, den
sie jederzeit in eine GUI rendern können, indem sie eine
render
-Methode spezifizieren. Diese liefert ein virtuelles
DOM-Objekt, das React dann in das reale DOM überträgt. Dabei
überträgt React nur die Teile, die sich geändert haben, was das
Framework extrem schnell
macht.
Als Beispiel machen wir eine ganz einfache Kommentar-Applikation, in
der eine Liste von Kommentaren - jeweils Autor und Text - angezeigt
werden und neue Kommentare hinzugefügt werden können. Als
Vorbereitung müssen wir erstmal in index.html
eine Zeile hinzufügen,
die React einbindet:
Hier ist die Definition für eine React-Komponenten-Klasse für einen einzelnen Kommentar in ClojureScript:
An Objekte aus dem JavaScript-Namensraum kommen Programme mit dem
Präfix js/
heran. Entsprechend gehört die Funktion createClass
zu
React - sie erwartet eine JavaScript-Hashmap, die verschiedene Aspekte
einer Komponente definiert. Im einfachsten Fall wie hier ist das nur
eine einzelne Funktion render
. Allerdings ist zu beachten, dass
ClojureScript-Maps nicht direkt als JavaScript-Hashmaps verwendet
werden können. (In ClojureScript ist eine Map wie in Clojure auch
eine persistente Datenstruktur.) Der Präfix #js
sorgt aber dafür, dass
die darauffolgende Map als JavaScript-Hashmap angelegt wird.
Aus dem Keyword-Schlüssel :render
wird der Schlüssel render
in der Hashmap.
React schreibt vor, dass es mindestens den render
-Eintrag geben
soll, und der sollte eine Funktion sein, die ein virtuelles DOM-Objekt
zurückliefert. Dazu muss das Kommentar-Objekt wissen, wer Autor und
was der Text der Kommentars ist. In React sind dafür die Properties
einer Komponente zuständig, die bei der Erzeugung der Komponente
mitgeliefert werden können, ebenfalls in Form einer Hashmap, zum
Beispiel in folgender Form:
Innerhalb der render
-Funktion stecken die Properties im Feld props
von „This“. „This“ muss in ClojureScript explizit an einen
Bezeichner gebunden werden: Die Form (this-as this ...)
bindet ihn
an this
. Dann extrahiert (.-props this)
den Inhalt des
props
-Feldes. (Das -
unterscheidet Feldzugriffe von
Methodenaufrufen, die kein -
aufweisen.)
Die js/React.DOM
-Aufrufe konstruieren das virtuelle DOM-Objekt.
Die nil
s sagen jeweils, dass bei den Elementen keine Attribute
stehen. In HTML ausgedrückt sähe der Rumpf einer
Comment
-Komponente so aus:
Ganz ähnlich funktioniert die CommentList
-Klasse für eine ganze
Liste von Kommentaren:
Gedacht ist, dass so eine CommentList
-Komponente zum Beispiel so
instanziert wird:
Auch hier ist zu beachten, dass ein ClojureScript-Vektor kein
JavaScript-Array ist - die React-DOM-Konstruktoren wollen aber Arrays
sehen. Darum die Konversion mit into-array
.
Als nächstes machen wir eine Komponente, die das Eingeben eines neuen Kommentars erlaubt. Dafür legen wir ein Formular mit zwei Textfeldern an - für Autor und Text des Formulars:
Die beiden input
-Elemente in der render
-Methode weisen jeweils
ref
-Attribute mit Namen für die Elemente auf: diese sind dazu da,
damit der Callback des Formulars auf die Inhalte der Textfelder
zugreifen kann. Der Callback ist in der handleSubmit
-Funktion, die
bei der Konstruktion der NewComment
-Komponente ebenfalls in der
Hashmap ist, in der auch render
steht. React steckt sie unter dem
gleichen Namen in das Komponenten-Objekt, so dass render
mit
(.-handleSubmit this)
darauf zugreifen kann.
Die handleSubmit
-Funktion kann dann auf das refs
-Feld von
this
-Zugreifen, in dem alle DOM-Elemente stehen, die ein
ref
-Attribut in render
abbekommen haben.
Das Feld author
ist das Element mit ref
-Wert author
, entsprechend
für text
. Die Methode getDOMNode
liefert dann das entsprechende
echte DOM-Element, in dem das value
-Feld dann den entsprechenden
Text holt.
Aber was tun mit dem neuen Kommentar? Er sollte natürlich zu einer
Liste aller Kommentare hinzugefügt werden, die sich aber außerhalb der
NewComment
-Komponente befindet. Die handleSubmit
-Funktion geht
deshalb davon aus, dass sich unter den Properties eine Funktion namens
newComment
befindet, die diese Aufgabe erledigt. Die muss dann bei
der Erzeugung der NewComment
-Komponente mitgeliefert werden. Diese
ruft handleSubmit
mit dem neuen Kommentar auf.
Wichtig: handleSubmit
muss false
liefern, damit der Browser das
Submit-Event als fertig behandelt ansieht und nicht das Formular
versucht an die Webseite auszuliefern.
Schließlich schreiben wir noch eine Komponentenklasse, um alles
zusamenzusetzen. Diese Klasse namens CommentBox
hat nun (als
einzige) Zustand, nämlich die aktuelle Liste aller Kommentare. Für
die Verwaltung des Zustands will React eine Methode namens
getInitialState
sehen, die den Anfangszustand setzt. Wie bei den
Properties auch muss der Zustand eine Hashmap sein. Die Funktion
holt sich den Anfangszustand aus den Properties (die wir noch
entsprechend übergeben müssen):
Die render-Methode
baut jetzt eine CommentList
- und eine
NewComment
-Komponente ein und übergibt an NewComment
in den
Properties die newComment
-Methode, die auf render
folgt:
Die newComment
-Methode schließlich benutzt die React-Methode
setState
, um das Kommentarfeld im Zustand um den neuen Kommentar zu
erweitern.
Fertig! Na ja, fast: Um im Browser etwas zu sehen, müssen wir noch eine
CommentBox
-Komponente im DOM aufhängen:
Damit das funktioniert, müssen wir in index.html
noch ein
Platzhalter-div
mit id
-Feld content
unterbringen. Das sieht
dann fertig so aus:
Jetzt fertig!
Clojure und React
Aus dem eleganten Modell von React lässt sich durch die Verwendung von ClojureScript durch rein funktionale Programmierung noch mehr herausholen. Es sind schon mehrere ClojureScript-Frameworks um React herum entstanden, zum Beispiel Om und Reagent.
Der komplette Code für dieses Beispiel befindet sich auf github oder als Zip-Datei hier.
Viel Spaß beim Ausprobieren!