Suche
Die Code-as-data-Philosophie

Clojure: das Lisp für die Java-Plattform [Pirates of the JVM]

Redaktion JAXenter

Auf unserer Reise durch die Welt der Pirates of the JVM machen wir heute Halt auf der Clojure-Insel. Hier empfängt uns Mark Engelberg, aktives Mitglied der Clojure Community, und zeigt, warum JVM-Segler etwas länger auf der Insel verweilen sollten.

Vor zehn Jahren tauchte Clojure am Horizont des funktionalen Ozeans auf. Seitdem wird die Insel vor allem von Lisp-Anhängern bewohnt. Auf der Suche nach etwas Neuem entdeckten sie die Insel – und blieben. Code-as-data ist die allgemeine Philosophie der Insel-Bewohner. Was Clojure noch zu bieten hat, zeigt Ihnen Mark Engelberg in dieser Folge der JVM-Piraten.

Klick auf die Karte für die komplette Infografik.

JAXenter: Was hat dich dazu bewogen, mit Clojure zu arbeiten? Welche Vorteile bietet Clojure, die andere Programmiersprachen nicht haben?

Mark Engelberg: Clojure kommt mit einem großen Sortiment an unveränderlichen (persistenten) Datenstrukturen, die in der Sprache selbst verwoben sind: Lists, Lazy Lists, Vektoren (ähnlich wie Arrays), Associative Maps (ähnlich wie Hash Tables), Sets, Sorted Maps, Sorted Sets und Queues. Das bedeutet, dass Daten-Updates neue Kopien erstellen, die faktisch unveränderte Informationen mit der ursprünglichen Datei teilen, statt das Original destruktiv zu verändern. Dieses Faktum macht es deutlich leichter, komplexe Programme zu schreiben und darüber zu diskutieren, vereinfacht Datenegalität und erlaubt es jedem Objekt, sicher als Schlüssel einer Map zu fungieren.

Zudem ist Clojure eine dynamisch-typisierte Sprache – eine der schnellsten und performantesten dynamisch-typisierten Sprachen überhaupt. Ich habe mich immer am produktivsten gefühlt, wenn ich mit dynamischen Sprachen programmiert habe. Sie tendieren dazu, weniger Boilerplate-Code und Syntax-Umschweife zu haben, wodurch man sich über die Lebensspanne eines Projekts leichter auf geänderte Anforderungen umstellen kann. Zudem ist es mit dynamischen Sprachen gemeinhin leichter, Daten „über das Netz“ zu erstellen, zu tauschen oder zu konsumieren. Clojure bietet Type-Hinting und (schemaähnliche) Spezifikationen, um einige der Leistungs- und Typsicherheitsbedenken zu adressieren, die mit dynamischer Typisierung einhergehen.

Klicken Sie auch: Funktionales Denken in Java 8, Clojure, Groovy und Scala

Clojures Schwerpunkt auf Unveränderlichkeit sticht deutlich hervor, wenn es um das Schreiben von parallelen Programmen geht. Stateful Entitys können in veränderbaren Boxen zu unveränderbaren Daten geformt werden. Man kann jederzeit risikolos in die Box schauen (non-blocking reads) und sich sicher sein, dass die enthaltenen Daten einen konsistenten Zustand haben. Ein Update der Stateful Entity bedeutet nur einen Austausch des Boxinhalts. Clojure bietet eine Vielzahl an Konstrukten mit klar definierter, auf Parallelität ausgelegter Semantik, sodass man stets zu der Option greifen kann, die die eigenen Bedürfnisse am besten erfüllt, z. B. Atoms, Agents, Volatiles, Refs, Futures, Promises oder Channels. Abhängig von dem gewünschten Level an Koordination, kann synchrones oder asynchrones Verhalten, oder ob man für Anwender einen Weg einbauen möchte, Gegendruck zu erzeugen – was wiederum Produzenten einschränkt –, ausgewählt werden. Das Auswählen und Nutzen der richtigen Box für die Daten ist wesentlich einfacher als traditionelle, manuelle Lock-basierte Ansätze für Concurrency (Nebenläufigkeit).

Auch wenn es keinen Anteil meiner ursprünglichen Wahl von Clojure hatte, habe ich inzwischen Clojures außergewöhnliche Fähigkeiten zu schätzen gelernt, was die Interoperabilität mit Java Libraries betrifft und wie sich JavaScript mit dem Kompilierer von ClojureScript kompilieren lässt, um in der Lage zu sein, Code über Klienten wie auch über Server zu sharen.

JAXenter: Kannst du die Kernprinzipien der Sprache darlegen? 

Mark Engelberg: Die wichtigsten, zentralen Prinzipien für den Code sind Unveränderlichkeit und dynamische Typisierung. Ein weiteres zentrales Prinzip ist die Idee des REPL-driven Development, um beispielsweise zu Entwickeln und Code interaktiv zu testen. Das sollten wir uns in Aktion anschauen. Hier ist eine Clojure-Funktion, die beliebige Dateitypen inkrementiert, die ein Age-Feld haben:

(defn increment-age [data]
  (update data :age inc))

Aber wichtiger noch, lass uns die Funktion an dem REPL ausprobieren:

=> (increment-age {:name "Jim", :age 25})
{:name "Jim", :age 26}

Es ist wichtig, dass Clojure es vermeidet, Daten hinter Objekten mit privaten Feldern zu verkapseln, also Getters und Setters. Es ergibt wenig Sinn, Informationen zu verstecken, solange Anwender sie nicht destruktiv verändern können. Die meisten Daten können als einfache assoziative Maps von Key-Value-Pärchen repräsentiert werden (ähnlich wie JSON) und Clojure stellt uns ein einheitliches API bereit, um diese Daten wiederzuholen und zu aktualisieren. Um besser zu verstehen was passiert, geben wir den Namen p zu unserem Input:

=> (def p {:name "Jim", :age 25})
=> p
{:name "Jim", :age 25}
=> (increment-age p)
{:name "Jim", :age 26}
=> p
{:name "Jim", :age 25}

Man beachte, dass, selbst nachdem die Increment-Age Funktion aufgerufen wird, der Wert von p unverändert bleibt. Unsere Funktion Increment-Age ist demzufolge eine reine Funktion, die immer den selben Output für einen bestimmten Input zurückgeben wird. Das macht es viel einfacher, zu testen. Und man kann sich sicher sein, dass es korrekt ist. In Java können nur einige Datentypen (z. B. Nummern und Strings) auf diese Weise sicher und nicht-destruktiv manipuliert werden. In Clojure haben alle Daten diese nützliche Eigenschaft.

JAXenter: Wie würde ein typisches Programm in Clojure aussehen? 

Mark Engelberg

Mark Engelberg: Um Clojure-Programme lesen zu können, ist es wichtig zu verstehen, dass sie in der Lisp-Familie der Programmiersprachen sind und die Prefix-Notation benutzen, was bedeutet, dass eine Funktion zunächst immer als Funktionsanwendung kommt, umgeben von Klammern. Anstatt eines f(x) für Funktionsanwendung, sagen wir (f x). Anstelle von 2+3 sagen wir (+ 2 3). Klammern werden auch dazu verwendet, um Listen darzustellen. Diese Dualität zwischen Code und Daten macht es einfacher, Meta-Programmierhilfsmittel wie Macros und Eval zu nutzen.

Programme beginnen typischerweise mit einer Namespace-Deklaration, die Funktionen auflistet, die für andere Namespaces gebraucht werden. Daten, die von [] umschlossen werden, sind ein Vektor und #{} deutet auf ein Set hin. def gibt etwas innerhalb des Namespace einen Namen und defn definiert eine Funktion. for ist Clojures Sequence Comprehension Syntax.

Das hier ist ein spaßiges kleines Programm, dass ich kürzlich geschrieben habe, um alle Wege zu finden, die Nummern mit grundlegenden arithmetischen Operationen zu kombinieren, damit eine bestimmte Target-Nummer erreicht werden kann. Die Ausdrücke sind gebaut und gedruckt durch die Verwendung von Prefix-Notation. Hier ein Anwendungsbeispiel, um alle Möglichkeiten zu finden, das Target Nummer 24 zu finden, bei der Nutzung der Nummern 3, 4, 5 und 5:

=> (ways-to-reach [3 4 5 5] 24)
((+ (+ (* 3 5) 4) 5)
 (+ (- 3 4) (* 5 5))
 (- (* 5 5) (- 4 3))
 (+ (+ (* 3 5) 5) 4)
 (- (+ 3 (* 5 5)) 4)
 (+ (* 3 5) (+ 4 5))
 (- 3 (- 4 (* 5 5)))
 (+ 3 (- (* 5 5) 4)))
 
Program:
(ns twentyfour.core
  (:require [clojure.math.combinatorics :refer [partitions]]))
 
(def ops ['+ '- '* '/])
(def commutative #{'+ '*})
 
(defn expressions [nums]
  (if (= (count nums) 1) nums
	(for [[left right] (partitions nums :min 2 :max 2),
      	left-expr (expressions left),
      	right-expr (expressions right),
      	op ops,
      	expr (if (or (commutative op) (= left-expr right-expr))
             	[(list op left-expr right-expr)]
             	[(list op left-expr right-expr) (list op right-expr left-expr)])]
  	expr)))
 
(defn ways-to-reach [nums target]
  (for [expr (expressions nums)
    	:when (= target (try (eval expr)
                      	(catch ArithmeticException e nil)))]
	expr))

JAXenter: Für welche Art von Anwendungen / Use Cases eignet sich Clojure besonders gut? Für welche eher weniger?

Mark Engelberg: Clojure ist ein hervorragendes Tool, um komplexe Informationssysteme zu bauen, besonders diejenigen, die mit anderen Sprachen und Systemen interagieren. Wegen seiner großen Interoperabilität mit Java eignet es sich hervorragend für Java Libraries, die Flexibilität und Produktivität mit dynamischem Stil haben. Besonders populär ist es für die Web-Entwicklung, Datenanalyse und Big Data. Clojure teilt Javas Schwäche bei Anwendungen, die viele Nummern mit nativer Geschwindigkeit verarbeiten müssen. Für diese Art von Anwendungen ist einem wahrscheinlich besser damit geraten, eine Sprache zu nutzen, mit der man einfach auf C gehen kann oder auf die Konstruktionsebene für Nummern-intensive Performanz.

JAXenter: Wie ist der derzeitige Status Quo der Sprache?

Mark Engelberg: Bereits seit seinem 1.0-Release in 2009 ist Clojure eine stabile und reife Plattform. Clojure legt bei seiner Entwicklung auch weiterhin großen Wert auf Stabilität und Backwards-Kompatibilität. ClojureScript ist eine robuste Variation von Clojure, um zu JavaScript zu kompilieren und Klienten-seitige Anwendungen zu bauen. Der große Fokus des nächsten Releases, Version 1.9, liegt auf Specs, einer Möglichkeit, um Schemas für die Daten zu spezifizieren, die durch die Funktionen fließen.

JAXenter: Wie steht es mit Deinen Plänen?

Mark Engelberg: Clojure passt weiterhin am besten auf die Bedürfnisse für meine Arbeit. Ich plane auch Clojure weiterhin für eine lange Zeit zu nutzen.

JAXenter: Wie können Interessierte in Clojure einsteigen?

Mark Engelberg: Clojure Support ist verfügbar u. a. für Emacs, Vim, Eclipse, IntelliJ und Atom. Siehe auch die Sektion Tools, um die für einen am besten geeignete Entwicklerumgebung zu finden. Es gibt viele hervorragende Bücher zur Auswahl. Anfängern empfehle ich regelmäßig „Clojure Programming“ und „Living Clojure“. Die Website https://www.4clojure.com/ ist ein spaßiges Set von Programmierherausforderungen, die einem dabei helfen, die Möglichkeiten der vielen in Clojure eingebauten Funktionen zu lernen.

Mark Engelberg has been an active member of the Clojure community ever since Clojure turned 1.0, and is a primary developer of several popular open-source Clojure libraries: math.combinatorics, math.numeric-tower, data.priority-map, ubergraph, and instaparse. He invents logic puzzles and games, using Clojure as a „secret weapon“ to build his own puzzle development tools. His recent focus has been on making computer science education fun by developing programming-themed puzzle toys for kids, including the award-winning Code Master and the upcoming games On the Brink, Rover Coder, and Robot Repair, produced by Thinkfun.

 

Die Pirates of the JVM

Gehen wir auf Schatzsuche! In der Infografik „Pirates of the JVM“ verbirgt sich so manches Programmiersprachen-Juwel, das inspirierte Entwickler auf der Java-Plattform für sich entdecken werden. Doch sehen Sie selbst…

Piratenflagge via Shutterstock.com / katyam1983

Geschrieben von
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.