2 Punkte von GN⁺ 2024-09-06 | 1 Kommentare | Auf WhatsApp teilen
  • Clojure 1.12.0 wird als letzte auf Java 8 ausgerichtete Version angekündigt, die weiterhin Java-8-Bytecode beibehält, bevor in späteren Releases die minimale Java-Kompatibilität und die Bytecode-Basis auf ein aktuelles Java-LTS verschoben werden
  • In Umgebungen mit virtuellen Threads von JDK 21 verwenden lazy-seq und delay Locks statt synchronized, wodurch Situationen reduziert werden, in denen blockierendes I/O reale Threads pinnt
  • Im REPL können mit add-lib, add-libs und sync-deps Bibliotheken ohne Neustart der JVM hinzugefügt werden; diese Funktion ist jedoch auf interaktive Nutzung während der Entwicklung beschränkt
  • Die Java-Interoperabilität wurde erweitert: Method Values, :param-tags, Syntax für Array-Klassen, Umwandlung funktionaler Interfaces, Supplier sowie Funktionen zur Verarbeitung von Java Stream wurden hinzugefügt
  • Bei Performance und Kompatibilität enthält die Version einen PersistentVector-Spliterator, effizienteres drop/Partitioning, eine strengere Var-Interning-Policy, die Behebung von CVE-2024-22871 sowie eine Bereinigung von Java-Serialisierungskennungen

Java-8-Kompatibilität, Sicherheit und Serialisierung bereinigt

  • Informationen zu Download und Nutzung von Clojure 1.12.0 finden sich auf der Seite Downloads
  • Die Java-8-Basis bleibt auch in diesem Release erhalten
    • Clojure 1.12 erzeugt wie Clojure 1.10 und 1.11 Java-8-Bytecode
    • Spätere Releases sollen Bytecode und minimale Java-Kompatibilität auf ein aktuelleres Java-LTS-Release verschieben
  • Das Problem mit Pinning virtueller Threads in JDK 21 wird abgemildert
    • Vor 1.12 führten lazy-seq und delay Benutzercode innerhalb eines synchronized-Blocks aus, um sicherzustellen, dass er nur einmal ausgeführt wird
    • Unter JDK 21 nimmt synchronized noch nicht an kooperativem Blocking teil; wenn dieser Code blockierendes I/O ausführt, kann dadurch ein realer Thread gepinnt werden
    • Bei Verwendung von -Djdk.tracePinnedThreads=full kann JDK 21 für diese Situation eine Warnung ausgeben
    • In 1.12 verwenden lazy-seq und delay statt synchronized-Blöcken Locks
  • Als Sicherheitsfix wurde CVE-2024-22871 berücksichtigt; die zugehörige Advisory findet sich unter GHSA-vr64-r9qj-h27f
  • Die serialVersionUID von Klassen im Zusammenhang mit Java-Serialisierung wird explizit gesetzt
    • Clojure-Datentypen implementieren seit Clojure 1.0 das Java-Serialisierungsinterface
    • Java-Serialisierung funktioniert bei der Deserialisierung nur, wenn die aus Klassenname, Typhierarchie und serialisierten Feldern erzeugte Kennung übereinstimmt
    • Clojure garantiert keine Serialisierungskonsistenz zwischen Versionen, führt aber Änderungen ein, die künftig mehr Kontrolle ermöglichen, um Kompatibilität nicht stärker als nötig zu brechen
  • Auch Abhängigkeiten wurden aktualisiert
    • spec.alpha wurde auf 0.5.238 aktualisiert
    • core.specs.alpha wurde auf 0.4.74 aktualisiert

Funktionen für Bibliotheken und Tools im REPL

  • Während der Entwicklung gibt es Situationen, in denen Bibliotheken hinzugefügt werden müssen, ohne die JVM neu zu starten
    • experimentelle Auswertung
    • Hinzufügen einer bekannten Abhängigkeit zu einem Projekt
    • Hinzufügen einer Bibliothek für eine bestimmte Aufgabe
  • Clojure 1.12 stellt neue Funktionen bereit, um Bibliotheken hinzuzufügen, ohne den REPL-Zustand zu verlieren
    • add-lib: lädt eine nicht im Classpath vorhandene Lib herunter und fügt sie dem Classloader hinzu
      • Libs, die bereits im Classpath vorhanden sind, werden nicht aktualisiert
      • Wenn keine Koordinaten angegeben sind, wird die neueste Maven-Version verwendet oder, falls der Name eines Git-Repositorys abgeleitet werden kann, die neueste Git-Version bzw. ein Tag
    • add-libs: löst mehrere neue Bibliotheken und Versionen gemeinsam auf
    • sync-deps: ruft add-libs für Libs auf, die in deps.edn stehen, aber noch nicht im Classpath sind
  • Diese Funktionen sind nur für die REPL-Nutzung während der Entwicklung gedacht
    • Der richtige Weg zum Bauen und Pflegen von Produktionscode bleibt weiterhin die Nutzung von deps.edn
    • Alle drei Funktionen prüfen, ob *repl* auf true gebunden ist
    • clojure.main/repl bindet dieses Flag automatisch
    • Im clojure.main-REPL werden die neuen Funktionen automatisch in den Namespace user referenziert
    • In anderen REPLs kann (require '[clojure.repl.deps :refer :all]) erforderlich sein
  • Auflösung und Download von Bibliotheken übernimmt tools.deps
    • Damit tools.deps und seine Abhängigkeiten während der Entwicklung nicht in den Projekt-Classpath gelangen, wurde außerdem eine neue API hinzugefügt, die die Funktionen über die Clojure CLI in einem separaten Prozess aufruft
  • clojure.tools.deps.interop/invoke-tool ruft Tool-Funktionen in einem separaten Prozess auf
    • Der Classpath des Tools wird in deps.edn definiert
    • Tool-Abhängigkeiten müssen nicht zum Projekt-Classpath hinzugefügt werden
    • Die add-lib-Funktion wurde mit invoke-tool umgesetzt und kann auch genutzt werden, um eigene Tools interaktiv zu bauen oder aufzurufen
    • Das Protokoll zur Funktionsausführung ist in der CLI reference beschrieben

API zum Ausführen externer Prozesse

  • Zusätzlich zum bestehenden Namespace clojure.java.shell gibt es nun einen Weg, der die neueren Java-APIs für Prozessinformationen, Steuerung und I/O-Umleitung nutzt
  • Clojure 1.12 fügt den neuen Namespace clojure.java.process hinzu
    • Er nutzt die neueren prozessbezogenen Java-APIs
    • Er ist benutzerfreundlicher gestaltet als der bisherige Ansatz
  • Die wichtigsten Funktionen sind:
    • start: bietet vollständige Kontrolle über Streams und Zugriff auf zugrunde liegende Java-Objekte für fortgeschrittene Nutzung
    • exec: deckt den üblichen Fall ab, einen externen Prozess auszuführen und nach Abschluss stdout zurückzugeben

Erweiterte Java-Interoperabilität

  • Method Values wurden hinzugefügt, sodass Java-Methoden direkter in Higher-Order Functions verwendet werden können
    • Bisher mussten Java-Methoden manuell in Funktionen verpackt werden, um sie etwa an map zu übergeben
    • Manuelles Wrapping ist umständlich und kann Hinweise zur Unterscheidung von Overloads erfordern oder zusätzliche Reflection bzw. Boxing verursachen
    • Nun können qualified methods an Wertpositionen wie normale Funktionen verwendet werden; der Compiler erzeugt automatisch eine Wrapper-Funktion
    • Wenn eine qualified method wegen Overloads nicht aufgelöst werden kann, erzeugt der Compiler einen Reflection-Aufruf
    • Entwickler können mit :param-tags-Metadaten die gewünschte einzelne Methodensignatur angeben
  • Die Syntax für qualified methods gibt Klasse und Methode explizit an
    • Classname/method: Clojure-Funktionswert, der eine statische Methode aufruft
    • Classname/.method: Clojure-Funktionswert, der eine Instanzmethode aufruft
    • Classname/new: Clojure-Funktionswert, der einen Konstruktor aufruft
    • Zur Unterscheidung zwischen statischen und Instanzmethoden müssen die Syntaxformen Classname/method und Classname/.method verwendet werden
  • :param-tags-Metadaten dienen zur Auflösung überladener Methoden
    • Eine als Wert verwendete qualified method liefert nur Klasse und Methodenname und kann daher überladene Methoden nicht auflösen
    • :param-tags ist ein Vektor der Form [tag …], wobei jedes Tag einem Parameter der gewünschten Signatur entspricht
    • Für Parameter mit nicht überladenem Typ kann der Platzhalter _ verwendet werden
    • Wenn :param-tags angegeben wird, muss der Compiler die Methode zur Compile-Zeit eindeutig auf eine einzelne Methode auflösen können
    • Die neue Metadaten-Reader-Syntax ^[tag …] fügt einem Member-Symbol :param-tags-Metadaten hinzu
  • Syntax für Array-Klassen wurde hinzugefügt
    • Clojure unterstützte Klassennamenssymbole als Klassenobjektwerte und Type Hints, bot aber außer Strings keine Syntax für Array-Klassen
    • Nun können Array-Klassen über Symbole der Form ComponentClass/#dimensions referenziert werden
    • Beispiele: String/1, java.lang.String/1, long/2
    • Die Komponentenklasse kann ein voll qualifizierter Klassenname, eine importierte Klasse oder ein Primitive sein
    • Die Syntax für Array-Klassen kann sowohl als Type Hint als auch als Wert verwendet werden
  • Die Interoperabilität mit Java-Functional Interfaces wurde verbessert
    • Java Functional Interfaces sind mit @FunctionalInterface annotiert und haben genau eine Methode
    • Clojure-Funktionen können an Aufrufe von Java-Methoden übergeben werden, die ein Functional Interface erwarten, sofern die Arity passt
    • Der Clojure-Compiler erzeugt einen Lambda-Adapter, der die Clojure-Funktion implizit in das benötigte Functional Interface umwandelt
    • Um in Schleifen wiederholte Adapter-Erzeugung zu vermeiden, kann durch einen Hint am Namen einer let-Binding explizit erzwungen werden
  • Auch die Supplier-Interoperabilität wurde verbessert
    • Um eine Methode aufzurufen, die einen wertebereitstellenden Supplier erwartet, musste bisher ein Adapter mit reify geschrieben werden
    • Clojure-Implementierungen von IDeref wie delay, future und atom implementieren nun direkt das Supplier-Interface

Stream-Verarbeitung und bessere Collection-Performance

  • Es wurden Funktionen hinzugefügt, um Stream, die Java-APIs zunehmend zurückgeben, auf Clojure-Art zu konsumieren
    • Zusammen mit der Unterstützung von Functional Interfaces in Clojure 1.12 werden Stream-Interop-Funktionen bereitgestellt
    • (stream-seq! stream) ⇒ seq
    • (stream-reduce! f [init-val] stream) ⇒ val
    • (stream-transduce! xf f [init-val] stream) ⇒ val
    • (stream-into! to-coll [xf] stream) ⇒ to-coll
    • Alle Funktionen sind terminale Stream-Operationen und konsumieren den Stream
  • PersistentVector stellt direkt einen Spliterator bereit, der in der Stream-Implementierung von Java-Collections verwendet wird
    • Ein Spliterator ist ein Iterator, der für schnellere parallele Traversierung aufgeteilt werden kann
    • Der neue Custom-Spliterator von PersistentVector unterstützt Parallelität und verbessert die Performance deutlich
  • Die Effizienz von drop, nthrest, nthnext und Partitioning wurde verbessert
    • CLJ-2713 fügt das interne Interface IDrop hinzu, das anzeigt, dass eine Collection effizienter droppen kann als durch sequenzielles Traversieren
    • Dieses Interface ist für Persistent Collections und algorithmische Collections wie range und repeat implementiert
    • Die neuen Funktionen partitionv, partitionv-all und splitv-at sind effizienter als ihre bestehenden Gegenstücke und erzeugen Vektor-Partitionen statt realisierter Seq-Partitionen

Strengere Var-Interning-Policy

  • Das Interning einer Var in einem Namespace erstellt, anders als Aliasing, eine stabile Referenz, sodass alle Referenzen dasselbe Objekt erhalten
  • Bisher gab es einige Fälle, in denen eine interned Var ersetzt werden konnte; in 1.12.0-alpha1 wurde die Policy verschärft
    • Wenn eine solche Situation auftritt, erscheint eine Warnung der Form "REJECTED: attempt to replace interned var #'some-ns/foo with #'other-ns/foo in some-ns, you must ns-unmap first"
  • Diese Policy behandelt die zugrunde liegende Ursache eines Problems, das mit neuen Funktionen in clojure.core in Clojure 1.11.0 sichtbar wurde, insbesondere mit abs
    • Wenn Code, der mit einer früheren Clojure-Version kompiliert wurde, einen Var-Namen hatte, der einer neu in clojure.core hinzugefügten Funktion entsprach, konnte er beim Laden in der Runtime 1.11.0 unbound werden
    • Neben CLJ-2711 wurde auch der frühere Fix in diesem Bereich, CLJ-1604, zurückgerollt

Vollständige Änderungsliste

1 Kommentare

 
GN⁺ 2024-09-06
Meinungen auf Hacker News
  • Wirklich ein großes Release mit vielen tollen neuen Funktionen.
    Persönlich gefällt mir add-libs am besten. Damit kann man jetzt Single-File-Demos oder Minimalbeispiele zur Reproduktion von Issues erstellen, wodurch die Hürde, kleine ausführbare Code-Snippets zu teilen, deutlich sinkt.
    Man kann auch Java-Bibliotheken ohne Java-Boilerplate demonstrieren. Nachdem man im REPL etwas ausprobiert hat, kann man den Code etwa in einen HN-Kommentar einfügen, und jeder kann exakt dasselbe „Setup“ reproduzieren und ausführen. Es ist nicht einmal nötig, ein Repository zu klonen.

    • Ich weiß nicht, ob sich noch jemand an Groovy erinnert. Groovy hat eine @Grab-Annotation, die praktisch dasselbe tut wie das beschriebene add-libs, und sie ist für das Schreiben von Skripten sehr praktisch.
    • Java hat inzwischen ebenfalls REPL- und Scripting-Unterstützung, aber eine Funktion wie add-libs gehört noch nicht zu den verfügbaren Meta-Commands.
  • Ich dachte, sie würden dieses Release bis zur Clojure/conj 2024 verschieben. Dafür gab es zwar keinen konkreten Grund, aber Clojure 1.10 kam ungefähr zur Clojure/conj 2021 heraus, und die Ankündigung, dass Datomic kostenlos wird, gab es ebenfalls zu Beginn der Clojure/conj 2023.
    Trotzdem warte ich noch immer auf spec2. Derzeit umgehe ich die Starrheit von spec mit Malli, aber das ist in Clojure kein First-Class Citizen. Hauptsächlich, weil man Makros nicht prüfen kann; das ist im Design des Clojure-Compilers so vorgesehen. Wenn man Malli-Schemas jedoch als Daten manipuliert, kann man die Idee von schema/select nachahmen.
    Durch die Änderung bei funktionalen Interfaces muss man jetzt keine Utility-Makros wie (defmacro ->Consumer [f] ...) mehr pflegen, sondern kann Funktionen direkt übergeben.

    • Als ich „maybe not“ sah, brauchte ich schema/select wirklich und habe eine Bibliothek für Malli gebaut: https://github.com/eval/malli-select
    • Ich frage mich, was mit „man kann Makros nicht prüfen“ gemeint ist. Man kann Makros mit spec per s/fdef versehen, und Aufrufe werden zur Compile-Zeit geprüft.
      Tatsächlich prüft auch Clojure core Makroaufrufe mit spec, sodass man bei falschen Aufrufen manchmal Spuren davon im Stacktrace sieht. Ich frage mich, ob etwas anderes gemeint war.
  • Es ist wirklich schön, dass bestehender Code unverändert weiterläuft, obwohl so viele neue Funktionen hinzugekommen sind. Die kontinuierliche Anstrengung, Breaking Changes zu vermeiden, zahlt sich aus.

  • Wer mehr über Clojure erfahren möchte, sollte sich die Clojure/conj-Konferenz vom 23. bis 25. Oktober in Alexandria, Virginia, ansehen: https://2024.clojure-conj.org

  • Ich freue mich, dass add-libs und sync-deps hinzugekommen sind. Jetzt scheint es kaum noch oder gar keinen Grund mehr zu geben, eine Session unbedingt zu schließen.
    Dieses Release wirkt vom Umfang her ziemlich anders als frühere Releases, und es ist interessant, weil so viel enthalten ist. Ich hoffe nur, dass es durch das höhere Tempo in ein paar Releases nicht zu einem verhedderten Klumpen wird.

    • Rich Hickey und das Clojure Team sind sehr vorsichtige Designer, daher glaube ich, dass man sich nicht allzu viele Sorgen machen muss.
    • Reine Spekulation, aber da es das erste Release ist, nachdem Rich Hickey nubank verlassen hat, konnte er vielleicht mehr Aufmerksamkeit darauf verwenden.
  • Die Änderung bei funktionalen Interfaces ist riesig. Clojure ist am besten, wenn es durch sorgfältige Interoperabilität nah an Java bleibt, und diese Änderung schließt eine große Lücke.

  • Ich frage mich, was aus spec geworden ist. Wurde es aufgegeben? Gibt es etwas, worauf man sich freuen kann?

    • spec existiert weiterhin und wird genutzt. Es gab auch viel Folgearbeit, aber sie liegt derzeit auf Eis, während geprüft wird, was in mehreren Punkten getan werden soll.
  • Sieht nach einem ziemlich soliden Release aus, und ich freue mich, dass Clojure weiterhin gut läuft.

    • Ich frage mich, ob es wirklich gut läuft. Ich evaluiere es gerade für ein neues Projekt und ziehe es zusammen mit Clara[0] in Betracht. Allerdings scheint es nicht mehr so Mainstream zu sein wie früher, und das Ökosystem fühlt sich lückenhafter an als zuvor.
      Ich will nicht trollen. Ich möchte mich dafür entscheiden, und aus Engineering-Sicht wirkt es wie eine gute Entscheidung. Aber wenn Popularität und Zahl der Beitragenden stark zurückgehen, könnte das in naher Zukunft zum Problem werden.
      [0] https://www.clara-rules.org/
  • Es ist einfacher denn je geworden, bestehende Entwickler zu Clojure-Entwicklern zu machen.
    Das große Anfangsproblem[0] ist das Lesen von Code, und KI wie ChatGPT oder Claude ist sehr gut darin, bestehenden Clojure-Code zu erklären. Dadurch kann das Onboarding von Entwicklern deutlich schneller werden.
    [0] Nach ein paar Wochen fühlt sich das Lesen von Clojure natürlich an, und man vergisst sogar, dass man es früher nicht lesen konnte.

  • Viele schöne Verbesserungen. Normalerweise ist es die wichtigste Lisp-Familien-Sprache, zu der ich greife.