2 puntos por GN⁺ 2024-09-06 | 1 comentarios | Compartir por WhatsApp
  • Clojure 1.12.0 mantiene el bytecode de Java 8, y se anticipa como la última versión basada en Java 8 antes de que futuras releases muevan la compatibilidad mínima con Java y la referencia de bytecode al Java LTS más reciente
  • En el entorno de hilos virtuales de JDK 21, lazy-seq y delay usan locks en lugar de synchronized, reduciendo los casos en que I/O bloqueante fija hilos reales
  • En el REPL se pueden agregar bibliotecas con add-lib, add-libs y sync-deps sin reiniciar la JVM, pero esta función está limitada al uso interactivo durante el desarrollo
  • La interoperabilidad con Java se amplía con valores de método, :param-tags, sintaxis de clases de arreglos, conversión de interfaces funcionales, Supplier y funciones para procesar Stream de Java
  • En rendimiento y compatibilidad se incluyen spliterator para PersistentVector, procesamiento más eficiente de drop/particiones, una política más estricta de interning de Vars, corrección de CVE-2024-22871 y ordenamiento de identificadores de serialización de Java

Compatibilidad con Java 8, seguridad y ordenamiento de serialización

  • La información de descarga y uso de Clojure 1.12.0 está disponible en la página de Downloads
  • La base de Java 8 se mantiene también en esta release
    • Clojure 1.12 genera bytecode de Java 8, igual que Clojure 1.10 y 1.11
    • Las siguientes releases planean mover el bytecode y la compatibilidad mínima con Java a una versión Java LTS más reciente
  • Se mitiga el problema de fijación de hilos virtuales en JDK 21
    • Antes de 1.12, lazy-seq y delay ejecutaban código del usuario dentro de un bloque synchronized para garantizar un comportamiento de ejecución única
    • En JDK 21, synchronized todavía no participa en el bloqueo cooperativo, por lo que si ese código realiza I/O bloqueante puede fijar un hilo real
    • Al usar -Djdk.tracePinnedThreads=full, JDK 21 puede emitir advertencias sobre esta situación
    • En 1.12, lazy-seq y delay usan locks en lugar de bloques synchronized
  • La corrección de seguridad incorpora CVE-2024-22871, y la advertencia relacionada está en GHSA-vr64-r9qj-h27f
  • Se configuran explícitamente los serialVersionUID de las clases relacionadas con serialización de Java
    • Los tipos de datos de Clojure han implementado la interfaz de serialización de Java desde Clojure 1.0
    • La serialización de Java requiere que, al deserializar, coincida un identificador generado a partir del nombre de clase, la jerarquía de tipos y los campos serializables
    • Clojure no garantiza consistencia de serialización entre versiones, pero aplica este cambio para tener más control en el futuro sin romper compatibilidad más de lo necesario
  • También se actualizan dependencias
    • spec.alpha se actualiza a 0.5.238
    • core.specs.alpha se actualiza a 0.4.74

Funciones para manejar bibliotecas y herramientas en el REPL

  • Durante el desarrollo hay casos en los que se necesita agregar bibliotecas sin reiniciar la JVM
    • evaluación experimental
    • agregar dependencias conocidas al proyecto
    • agregar bibliotecas para tareas específicas
  • Clojure 1.12 ofrece nuevas funciones para agregar bibliotecas sin perder el estado del REPL
    • add-lib: descarga una lib que no está en el classpath y la agrega al classloader
      • no actualiza libs que ya están en el classpath
      • si no hay coordenadas, puede inferir la versión más reciente de Maven o, si puede inferir el nombre del repositorio git, usar la versión o etiqueta git más reciente
    • add-libs: resuelve varias bibliotecas nuevas y sus versiones en conjunto
    • sync-deps: llama a add-libs para libs que están en deps.edn pero todavía no en el classpath
  • Estas funciones están pensadas solo para uso del REPL durante el desarrollo
    • la forma correcta de construir y mantener código de producción sigue siendo usar deps.edn
    • las tres funciones verifican que *repl* esté enlazado a true
    • clojure.main/repl enlaza esta bandera automáticamente
    • en el REPL de clojure.main, las nuevas funciones se hacen refer automáticamente en el namespace user
    • en otros REPL puede ser necesario (require '[clojure.repl.deps :refer :all])
  • La resolución y descarga de bibliotecas queda a cargo de tools.deps
    • para evitar meter tools.deps y sus dependencias en el classpath del proyecto durante el desarrollo, también se agrega una nueva API para invocar funciones mediante Clojure CLI en un proceso separado
  • clojure.tools.deps.interop/invoke-tool invoca funciones de herramientas en un proceso separado
    • el classpath de la herramienta se define en deps.edn
    • no hace falta agregar dependencias de herramientas al classpath del proyecto
    • la función add-lib fue construida usando invoke-tool, y también puede usarse para construir o invocar herramientas de usuario de forma interactiva
    • el protocolo de ejecución de funciones puede consultarse en la CLI reference

API para ejecutar procesos externos

  • Además del namespace existente clojure.java.shell, ahora hay una vía que aprovecha la nueva API de Java para información, control y redirección de I/O de procesos
  • Clojure 1.12 agrega el nuevo namespace clojure.java.process
    • aprovecha la nueva API de procesos de Java
    • está diseñado para ser más fácil de usar que el enfoque anterior
  • Las funciones principales son las siguientes
    • start: permite control total de los streams y acceso a los objetos Java subyacentes para uso avanzado
    • exec: cubre el caso común de ejecutar un proceso externo y devolver stdout al completarse

Ampliación de la interoperabilidad con Java

  • Se agregan valores de método, lo que permite usar métodos de Java de forma más directa en funciones de orden superior
    • antes, para pasar un método Java a map u otras funciones, había que envolverlo manualmente en una función
    • ese wrapping manual era verboso, podía requerir hints para distinguir sobrecargas y podía introducir reflexión o boxing innecesarios
    • ahora los qualified methods pueden usarse en posición de valor como funciones normales, y el compilador genera automáticamente una función envoltorio
    • si un qualified method no puede resolverse por sobrecarga, el compilador genera una llamada reflexiva
    • los desarrolladores pueden indicar la firma deseada con metadatos :param-tags
  • La sintaxis de qualified method especifica clase y método
    • Classname/method: valor de función de Clojure que invoca un método estático
    • Classname/.method: valor de función de Clojure que invoca un método de instancia
    • Classname/new: valor de función de Clojure que invoca un constructor
    • para distinguir métodos estáticos de métodos de instancia, debe usarse la sintaxis Classname/method y Classname/.method
  • Los metadatos :param-tags se usan para resolver métodos sobrecargados
    • un qualified method usado como valor solo proporciona la clase y el nombre del método, por lo que no puede resolver métodos sobrecargados
    • :param-tags es un vector con forma [tag …], donde cada tag corresponde a un parámetro de la firma deseada
    • se puede usar el placeholder _ para parámetros de tipos no sobrecargados
    • al proporcionar :param-tags, el compilador debe poder resolverlo a un único método en tiempo de compilación
    • la nueva sintaxis de lector de metadatos ^[tag …] adjunta metadatos :param-tags a un símbolo de miembro
  • Se agrega sintaxis de clases de arreglos
    • Clojure ya soportaba símbolos con nombres de clase como valores de objeto Class y como type hints, pero no ofrecía sintaxis de clase de arreglo fuera de strings
    • ahora se puede referenciar una clase de arreglo con un símbolo de la forma ComponentClass/#dimensions
    • ejemplos: String/1, java.lang.String/1, long/2
    • la clase componente puede ser un nombre de clase completo, una clase importada o un primitive
    • la sintaxis de clase de arreglo puede usarse tanto como type hint como valor
  • Mejora la interoperabilidad con interfaces funcionales de Java
    • una interfaz funcional de Java tiene @FunctionalInterface y un solo método
    • una función de Clojure puede pasarse a una llamada de método Java que recibe una interfaz funcional si coincide la aridad
    • el compilador de Clojure crea un adaptador lambda para convertir implícitamente la función de Clojure en la interfaz funcional requerida
    • para evitar crear adaptadores repetidamente dentro de un loop, se puede forzar explícitamente agregando un hint al nombre enlazado con let
  • También mejora la interoperabilidad con Supplier
    • antes, para llamar métodos que reciben un Supplier de valores, había que escribir un adaptador con reify
    • las implementaciones de IDeref de Clojure, como delay, future y atom, ahora implementan directamente la interfaz Supplier

Mejoras en procesamiento de Stream y rendimiento de colecciones

  • Se agregan funciones para consumir al estilo Clojure los Stream que las APIs de Java devuelven cada vez más
    • junto con el soporte para interfaces funcionales de Clojure 1.12, se ofrecen funciones de interoperabilidad con Stream
    • (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
    • todas son operaciones terminales sobre streams y consumen el stream
  • PersistentVector ahora ofrece directamente un spliterator usado en la implementación de stream de colecciones Java
    • un spliterator es un iterador divisible para recorridos paralelos más rápidos
    • el nuevo spliterator personalizado de PersistentVector soporta paralelismo y mejora mucho el rendimiento
  • Mejora la eficiencia de drop, nthrest, nthnext y del procesamiento de particiones
    • CLJ-2713 agrega la interfaz interna IDrop, que indica que una colección puede hacer drop de forma más eficiente que con recorrido secuencial
    • esta interfaz se implementa en colecciones persistentes y colecciones algorítmicas como range y repeat
    • las nuevas funciones partitionv, partitionv-all y splitv-at son más eficientes que sus equivalentes existentes y generan particiones en vector en lugar de particiones de seq materializadas

Política más estricta de interning de Vars

  • Hacer interning de un var en un namespace, a diferencia del aliasing, crea una referencia estable para que todas las referencias obtengan el mismo objeto
  • Antes había algunos casos en que un var internado podía ser reemplazado, y en 1.12.0-alpha1 la política se volvió más estricta
    • cuando esto ocurre, aparece una advertencia con la forma "REJECTED: attempt to replace interned var #'some-ns/foo with #'other-ns/foo in some-ns, you must ns-unmap first"
  • Esta política aborda la causa raíz de un problema que se hizo visible cuando Clojure 1.11.0 agregó nuevas funciones a clojure.core, en particular abs
    • si código compilado con versiones anteriores de Clojure tenía nombres de var iguales a funciones agregadas después en clojure.core, podía quedar unbound al cargarse en el runtime 1.11.0
    • además de CLJ-2711, también se revierte CLJ-1604, una corrección previa en esta misma área

Lista completa de cambios

1 comentarios

 
GN⁺ 2024-09-06
Opiniones de Hacker News
  • Es realmente un lanzamiento grande y trae muchas funciones nuevas geniales.
    Personalmente, lo que más me gusta es add-libs. Ahora se pueden crear demos de un solo archivo o ejemplos mínimos para reproducir issues, lo que reduce mucho la barrera para compartir pequeños fragmentos de código ejecutables.
    También se pueden hacer demos de librerías Java sin el boilerplate de Java. Después de probar cosas en el REPL, uno puede pegar el código en lugares como un comentario de HN, y cualquiera puede reproducir y ejecutar exactamente la misma “configuración”. Ni siquiera hace falta clonar un repositorio.

    • No sé si alguien se acuerda de Groovy. Groovy tiene una anotación @Grab que hace prácticamente lo mismo que add-libs como se describió, y es muy práctica para escribir scripts.
    • Java ahora también tiene REPL y soporte de scripting, pero una función como add-libs todavía no está entre los metacomandos disponibles.
  • Pensé que iban a postergar este lanzamiento hasta Clojure/conj 2024. No tenía ninguna base concreta, pero Clojure 1.10 salió cerca de Clojure/conj 2021 y el anuncio de que Datomic pasaba a ser gratuito también fue a comienzos de Clojure/conj 2023.
    De todos modos, todavía estoy esperando spec2. Por ahora estoy esquivando la rigidez de spec con Malli, pero no es un ciudadano de primera clase en Clojure. Principalmente porque no se pueden inspeccionar macros, y eso es una parte intencional del diseño del compilador de Clojure. Aun así, si se manipulan los esquemas de Malli como datos, se puede imitar la idea de schema/select.
    Gracias al cambio en las interfaces funcionales, ahora se puede pasar una función directamente sin tener que mantener macros utilitarias como (defmacro ->Consumer [f] ...).

    • Al ver “maybe not”, realmente necesitaba schema/select, así que hice una librería para Malli: https://github.com/eval/malli-select
    • Me da curiosidad qué significa “no se pueden inspeccionar macros”. Con spec se puede adjuntar s/fdef a una macro, y las llamadas se inspeccionan en tiempo de compilación.
      De hecho, Clojure core también inspecciona llamadas a macros con spec, y a veces se puede ver ese rastro en el stack trace cuando se las llama mal. Me pregunto si se refería a otra cosa.
  • Me encanta que, aun con un montón de funciones nuevas, el código existente siga funcionando igual. Se nota el esfuerzo constante por evitar romper la compatibilidad.

  • Si quieren conocer más sobre Clojure, vale la pena ver la conferencia Clojure/conj, que se realizará del 23 al 25 de octubre en Alexandria, Virginia: https://2024.clojure-conj.org

  • Me alegra que hayan llegado add-libs y sync-deps. Ahora parece que casi no hay, o directamente no hay, motivos para tener que cerrar una sesión.
    Este lanzamiento parece bastante distinto en alcance respecto de los anteriores, y es interesante por la cantidad de cosas que incluye. Eso sí, espero que por la mayor velocidad no termine convirtiéndose en una bola enredada dentro de algunos lanzamientos.

    • Rich Hickey y el Clojure Team son diseñadores muy cuidadosos, así que creo que no hace falta preocuparse demasiado.
    • Es pura especulación, pero como es el primer lanzamiento después de que Rich Hickey dejó nubank, quizás pudo dedicarle más atención.
  • El cambio de interfaces funcionales es enorme. Clojure está en su mejor versión cuando se mantiene cerca de Java mediante una interoperabilidad cuidadosa, y este cambio cubre un hueco importante.

  • Me pregunto qué pasó con spec. ¿Lo abandonaron? Me gustaría saber si hay alguna noticia para esperar.

    • spec sigue existiendo y se sigue usando. También hubo bastante trabajo posterior, pero está detenido mientras se evalúa qué hacer con varios temas.
  • Parece un lanzamiento bastante sólido, y me alegra que Clojure siga andando bien.

    • Me pregunto si realmente le está yendo tan bien. Estoy evaluándolo para usarlo en un proyecto nuevo, y lo estoy considerando junto con Clara[0]. Pero no parece tan mainstream como antes y el ecosistema se siente más disperso que antes.
      No intento trolear. Quiero elegirlo, y desde el punto de vista de ingeniería parece una buena decisión. Pero si su popularidad y sus contribuidores están cayendo fuerte, eso podría convertirse en un obstáculo en el futuro cercano.
      [0] https://www.clara-rules.org/
  • Convertir desarrolladores existentes en desarrolladores Clojure es más fácil que nunca.
    El gran problema inicial[0] es leer el código, y las IA como ChatGPT o Claude son muy buenas explicando código Clojure existente. Como resultado, el onboarding de desarrolladores puede ser mucho más rápido.
    [0] Después de unas semanas, leer Clojure se vuelve natural, y hasta uno se olvida de que antes no podía leerlo.

  • Muchas mejoras geniales. Normalmente es el principal lenguaje de la familia Lisp al que recurro.