Por qué Jepsen está escrito en Clojure
(aphyr.com)- Jepsen es una herramienta para verificar sistemas concurrentes como bases de datos, así que al elegir el lenguaje de implementación fueron criterios importantes el modelo de concurrencia y el soporte real para hilos
- Las estructuras de datos inmutables y persistentes de Clojure, junto con las herramientas de concurrencia de la JVM, reducen la posibilidad de errores en el código de prueba y además encajan bien con clientes de bases de datos basados en Java
- El trabajo en Jepsen se parecía más a la experimentación y la exploración que al desarrollo de producto con requisitos fijos, por lo que funciones de Clojure como el REPL, los macros, el threading macro y EDN resultaban ventajosas para manejar datos complejos
- El rendimiento no está entre los mejores, pero el Clojure idiomático suele ser solo entre uno y dos órdenes de magnitud más lento que Java, y los puntos necesarios pueden tratarse con optimización y herramientas de perfilado de la JVM
- Aunque tiene debilidades como una comunidad pequeña, la ausencia de un sistema de tipado estático ampliamente exitoso, la incomodidad al tratar con primitives y mensajes de error deficientes, para Jepsen —mantenido y usado por 1 a 3 personas— era una concesión aceptable
Condiciones del lenguaje adecuadas para una herramienta de pruebas de concurrencia
- Jepsen fue creado para probar sistemas concurrentes, y la mayoría de los objetivos de prueba son bases de datos
- Clojure cumple bien con las condiciones básicas necesarias para escribir código de pruebas de concurrencia
- Las estructuras de datos inmutables y persistentes reducen la posibilidad de errores en código que maneja estado compartido
- Se pueden usar hilos reales, promise, future, atom, lock, queue, cyclic barrier,
java.util.concurrent, etc. - También se consideraron lenguajes como Haskell, con un control más estricto de los efectos secundarios, pero el enfoque menos dogmático de Clojure encajó mejor con Jepsen
- Las pruebas de bases de datos requieren conectarse con distintos clientes
- Casi todas las bases de datos suelen ofrecer clientes de JVM escritos normalmente en Java
- Clojure tiene una buena interoperabilidad con Java, así que es fácil aprovechar esos clientes
Funciones de Clojure favorables para la experimentación y la exploración de datos
- Las pruebas de Jepsen son trabajo experimental, así que se necesitaba un lenguaje conciso, fácil de cambiar y adecuado para prototipado
- La sintaxis concisa de Clojure y su sistema de macros encajan bien con esta forma de trabajo
- El threading macro hace que las transformaciones encadenadas sean más fáciles de leer
- Los macros facilitan implementar manejo de errores reutilizable y control del alcance de recursos
- El REPL de Clojure es útil para explorar en el momento los datos resultantes de la ejecución de pruebas
- Durante las pruebas, hay que representar, transformar e inspeccionar estructuras de datos complejas y anidadas
- Las estructuras de datos de Clojure y las funciones de su biblioteca estándar son fuertes en esta tarea
- Al imprimir muchos datos estructurados en consola y archivos, la sintaxis de datos EDN encaja bien
La concesión entre rendimiento y mantenimiento a largo plazo
- Jepsen no es un sistema enorme, pero maneja una cantidad moderada de datos, por lo que necesita un rendimiento “lo suficientemente bueno”
- Clojure no es el lenguaje más rápido
- El Clojure idiomático normalmente se mantiene dentro de una diferencia de rendimiento de uno o dos órdenes de magnitud frente a Java
- En los cuellos de botella importantes se puede reducir esa diferencia
- Las herramientas de perfilado de la JVM funcionan bien con Clojure
- Jepsen es un proyecto de unos 10 años, así que la madurez del núcleo y la estabilidad eran importantes
- Clojure es estable tanto por su objetivo en la JVM como por el lenguaje en sí
- Las librerías no “se echan a perder” tan rápido como en Scala o Ruby
Debilidades de Clojure que hay que aceptar
- Clojure tiene desventajas que, a la escala de Jepsen, son manejables, pero que podrían ser una carga para equipos más grandes
- La comunidad de ingeniería es pequeña
- No existe un sistema de tipado estático ampliamente aceptado y exitoso
- Como Jepsen se mantiene y usa solo por 1 a 3 personas a la vez, esta limitación pesa menos
- Al trabajar con primitives de la JVM puede resultar frustrante si no se baja a Java, y de hecho a veces se usa Java
- Algunas partes del sistema de polimorfismo son insuficientes, aunque se pueden rodear con librerías, y los mensajes de error tampoco son buenos
- Jepsen se decidió por Clojure después de crear prototipos en varios lenguajes, y aun después de 10 años sigue siendo una concesión bastante buena
1 comentarios
Opiniones en Hacker News
Como alguien que lleva unos 10 años operando un SaaS con Clojure/ClojureScript, estoy totalmente de acuerdo con el texto de Kyle.
Algo que además nos resultó muy valioso fue poder poner la mayor parte del código de dominio en archivos cljc para compilarlo tanto del lado del servidor como del cliente; que los transducers y los pipelines de transducers están subestimados en términos de rendimiento, componibilidad y reutilización; y que, como corresponde a un lenguaje diseñado por Rich Hickey, tiene una estabilidad y compatibilidad a largo plazo excelentes.
La retrocompatibilidad es importantísima: permite enfocarse en la aplicación en vez de quedar a merced de herramientas que cambian todo el tiempo, y no creo que sea casualidad que el programador promedio de Clojure tenga más de 12 años de experiencia y reciba una compensación en el cuartil superior.
Casi no se rompe y actualizar es fácil. Gracias a HMR, REPL y los archivos cljc, la experiencia de desarrollo también es excelente, y escribir código en Clojure es divertido.
No entiendo por qué está bien torcer lenguajes y sistemas hacia atrás para no romper lo que funciona, pero a los dos lenguajes que sostienen una gran parte del software que realmente corre se les espera que corten con su brillante pasado y presente.
El mayor problema de Clojure es, sin duda, el ecosistema.
Si uno mira la distribución de herramientas accesibles y utilizables, o están demasiado a la izquierda, donde son horribles pero fáciles de abordar, o demasiado a la derecha, donde el README tiene citas de El Señor de los Anillos, todo es datos hasta el enésimo grado, y solo un dragón capaz de programar plugins de Emacs con los ojos vendados y colgado boca abajo puede acceder a ellas.
El lenguaje en sí es excelente y la interoperabilidad con Java es mucho mejor de lo que dice Aphyr, pero si Clojure tuviera algo como Rails o Django, o mejor aún Phoenix, sería mucho más fácil de vender y se usaría mucho más.
En especial, si quieres convencer a un equipo de pasar de lenguajes imperativos de la familia Algol a Lisp, lo que se gana tiene que superar el sufrimiento, y hoy, salvo por componentes básicos como el ruteo HTTP, casi en todos los proyectos hay que reinventar la rueda.
Aun así, poder usar cualquier paquete de Maven y compilar a jar, de modo que Clojure corra en cualquier lugar donde corra un jar, es una ventaja enorme.
https://calva.io
Clojure existe en varias formas; se usan principalmente Clojure y ClojureScript, y también están Babashka, Scittle, etc., pero todos derivan de clj o cljs.
Las herramientas de desarrollo dependen de la plataforma y de la preferencia de IDE, así que hay herramientas distintas para Emacs, Neovim, VSCode e IntelliJ, y los sistemas de build también varían según la necesidad.
En clj, deps.edn es lo más usado actualmente, seguido por leiningen; en cljs hay varias opciones además de shadow-cljs.
Para quien recién empieza puede ser abrumador, pero la comunidad es buena y siempre ayuda. He programado profesionalmente en varios lenguajes, pero Clojure es, por lejos, el que más me gusta.
Uso Clojure desde alrededor de 2013 y no se me ocurre ninguna. La mayoría de las bibliotecas populares son excelentes y están muy por delante de la mayoría de los ecosistemas que conozco en estabilidad, retrocompatibilidad y simplicidad.
Gracias a los archivos fuente .cljc, que pueden compartirse entre Clojure y ClojureScript, se puede ejecutar el mismo código Clojure tanto en el frontend como en el backend.
Como dice Rich Hickey, no solo “alcanza” la JVM, también alcanza JavaScript.
Uso Emacs desde hace mucho y puedo configurarlo como quiero o eventualmente escribir el código elisp que necesite, pero de ninguna manera diría que soy bueno en elisp.
Admito que para alguien que nunca aprendió ningún dialecto de Lisp, aprender Emacs, elisp y Clojure al mismo tiempo es difícil y muy complicado de vender.
Hoy existe el servidor LSP de Clojure y funciona no solo en Emacs sino también en otros IDE, así que aunque muchos desarrolladores de Clojure usen Emacs, Emacs no es imprescindible para desarrollar en Clojure.
Llevo 7 años usando solo Clojure y trabajo activamente con una base de código Clojure + ClojureScript de más de 500.000 líneas; mi mayor queja es la falta de un sistema de tipos robusto.
Estoy de acuerdo en que Clojure es expresivo, permite avanzar rápido y no se interpone, pero también permite errores de runtime desordenados que son difíciles de reproducir y depurar.
Si hubiera tenido un sistema de tipos estático ampliamente aceptado y exitoso, Clojure habría sido increíble. Hoy lo veo como una opción bastante buena si te gustan Ruby o Python pero quieres algo más fuerte, y ahora me hace preguntarme si Go será el camino correcto.
Estoy totalmente de acuerdo en que Clojure(Script) es prácticamente una bendición y una excelente opción para muchos proyectos, sean pequeños o grandes.
Después de usar un poco C, Python, PowerShell, Java, Bash y SQL para tareas universitarias, administración de sistemas, análisis de volcados de tráfico, etc., el primer lenguaje con el que programé profesionalmente fue Clojure.
Estamos construyendo OrgPad con Clojure+ClojureScript, y aun con más de 100 mil líneas de código, creo que fue una buena decisión.
Es accesible y, con escribir un poco de código, tengo bastante confianza en que se comportará como lo imaginé. No sentía eso en Python, PowerShell ni Bash; me parece de otro nivel.
El flujo de trabajo con REPL también es realmente bueno, y un colega hizo un video al respecto: https://www.youtube.com/watch?v=4igO7Qbyj9o
Hoy en día, funciones, tareas cortas y proyectos pequeños para los que uno podría plantearse pasar de Bash a Python también se pueden resolver con Babashka/nbb.
Hace tiempo, cuando intenté escribir unos miles de líneas en Clojure y ClojureScript, el mayor problema era que la mayoría de las herramientas tenían estados de error bastante desordenados.
Python también tiene algo parecido, así que si interrumpes la mayoría de los programas de Python con Ctrl+C aparece un stack trace mezclado con cosas incomprensibles, pero para alguien que recién empezaba era difícil avanzar.
Creo que el lenguaje y el ecosistema contienen muchas ideas innovadoras e interesantes. Como mínimo, vale la pena leer el artículo histórico de Clojure e internalizar la mayoría de sus lecciones.
A todos les sonará herético, pero creo que hay un pequeño espacio en el ecosistema para un lenguaje de scripting que se tome tan en serio el rendimiento y la ergonomía de escritura de código como Clojure, pero sin paréntesis.
También estaría bien no tener la práctica de tratar
nilde forma especial, aunque al menos ahí hay algo de fundamento.Sé que
(f x y)es mejor quef(x, y), pero mi problema va más por los casos en los que las formasletprovocan indentación. No creo que haya que pagar al mismo tiempo el costo de la indentación y los paréntesis solo para ponerle nombre a un valor.Cuando vienes de un lenguaje como Python, que usa como información la indentación que de todos modos pondrías en el código, estas cosas a veces se sienten simplemente como ruido.
letgenere indentación es más bien una señal de que se está usando demasiadolet.En un estilo funcional, no suele hacer falta ponerles nombre a muchos argumentos intermedios que se pasan entre funciones.
Normalmente se pasan argumentos de forma implícita a la siguiente función con macros de threading como
->y->>, o se define la propia transformación como una función pura y se deja en el nivel superior.Dicho eso, entiendo que en cierto código de interoperabilidad se necesite mucho
let, especialmente con bibliotecas Java de JWT que modifican argumentos en cadenas procedurales.Por ejemplo, después de depurar durante un buen rato, darse cuenta de que no se volvió a cargar o reinicializar algo.
Siento que la mayoría termina agregando esto a la lista de cosas que revisar cuando pasa algo de “esto no puede ser”. Por ejemplo: “voy a poner una salida de depuración para confirmar si mi cambio de código realmente se está llamando”.
Claro que eso es solo una red de seguridad; principalmente se intenta evitar esas situaciones con otros hábitos de trabajo en la REPL.
(+ clojure clojurescript).Clojure hace muchas cosas bien, y por eso me gusta mucho.
Una cosa que frena a los equipos reales cuando construyen trabajo web real con Clojure es, creo, el deseo casi dogmático de ensamblarlo todo combinando infinidad de bibliotecas, cada una con su propia manera de hacer las cosas.
Creo que Clojure sería mucho más popular y agradable de usar si la comunidad, además de su enfoque actual, también contara con un framework web mantenido activamente, con baterías incluidas y usable de forma general. Me pregunto si Biff cumple ese rol.
Estaría bueno que hubiera una ruta alternativa para quienes aman el lenguaje pero no el enfoque de armar un monstruo por su cuenta.
También puede ser porque estoy malacostumbrado por Ruby on Rails.
Es el enfoque de componer en vez de complejizar/entrelazar, como en la famosa charla de Rich Hickey: (https://github.com/matthiasn/talk-transcripts/blob/master/Hi...)
Aprender Clojure y bases de datos de grafos, y según el caso incluso HTMX, es bastante. Aun así, muchas configuraciones ya vienen hechas, la autenticación está pensada con cuidado y es fácil, y los archivos de configuración del servidor están bien comentados.
También incluye scripts de despliegue para configurar un servidor Linux y entrar por SSH a la REPL de producción. Si quieres usar XTDB, la forma en que Biff configura la validación de documentos es realmente buena.
Personalmente, prefiero esto a las apps que corren con ClojureScript. Hay muchos wrappers de ClojureScript sobre React, y casi se volvió un meme, pero como desarrollador de React no sentí que ninguno fuera más fácil que React.
Más bien me parecieron complejos y poco cómodos. En cambio, HTMX/Hyperscript encaja muy bien con Clojure porque la sintaxis HTML no es más que arreglos y objetos fáciles de manipular.
Por primera vez, el dicho que repiten los fans de Clojure, “el código es datos”, se sintió real.
Hace tiempo intenté usar OCaml varias veces, de a poco, pero no me gustó su sistema de tipos demasiado estricto.
Pasaba más tiempo satisfaciendo al compilador que escribiendo código. En Clojure, Common Lisp y C tengo más libertad para doblar el compilador o el sistema de tipos hacia donde yo quiero.
Claro que hay pros y contras, pero no quiero que me estorben cuando escribo código. Quizás sea mi estilo, pero me encontré con mucha gente que piensa igual.
Creo que los sistemas de tipos estáticos son una cuestión de gustos, y el bando contrario piensa distinto. Aun así, es bueno que existan bandos opuestos, porque se aprende mucho unos de otros.
Rich Hickey dijo varias veces que, como Clojure es un lenguaje anfitrión, está bien bajar a Java o JavaScript cuando haga falta.
Personalmente, creo que esa es una de las funciones más potentes del ecosistema de Clojure, porque es mucho más práctica.
Vi muchos intentos de reimplementar una y otra vez lo mismo en el lenguaje que a uno le gusta solo porque no quieren tocar “esa biblioteca escrita en ese otro lenguaje sucio”. Las razones varían: que es lenta, insegura, poco flexible, etc.
Esto pasó y sigue pasando a menudo tanto en Common Lisp como en la comunidad de Rust.
SBCL las garantiza lo mejor que puede, y probablemente me ahorró varios minutos de dolor de cabeza por proyecto.
Y CL es muy práctico: por ejemplo, nadie se inmuta de que woo esté basado en una biblioteca en C.
Todos los problemas operativos tenían que ver con cuestiones pequeñas y sutiles. Por ejemplo, una actualización del runtime cambiaba el orden de las claves JSON y rompía clientes viejos que deserializaban JSON manualmente, o una biblioteca profunda en el stack empezaba a devolver códigos HTTP como enteros en vez de strings y rompía el parseo de errores.
Por la escala operativa, aunque solo se viera afectado un pequeño porcentaje de clientes, llegaban cientos de reportes de problemas, así que cada incidente era grave.
Ahora trabajo en una startup chica y es completamente distinto. Elegimos un lenguaje estático porque es donde se concentra la atención y el personal, pero extraño cuando trabajaba con lenguajes dinámicos.
Como lanzamos rápido, la calidad del código se deteriora mucho. Por falta de tiempo, en vez de lidiar con el sistema de tipos y refactorizar limpiamente, copiamos varias estructuras de objetos apenas distintas y las usamos por todos lados.
Como generalizar se vuelve complicado por la forma de los tipos, en vez de separar una funcionalidad y referenciarla desde varios lugares, copiamos y pegamos la misma lógica en 10 lugares.
Creo que la solución adecuada depende del dominio del problema. Si te llaman a medianoche por bugs sutiles, es muy probable que el tipado estático estricto sea lo correcto.
Si iteras rápido y un problema puede resolverse con un correo o un mensaje de Slack, el tipado dinámico puede ser mejor.
El truco es no acumular demasiada deuda en el paradigma dinámico por si creces y necesitas pasar al paradigma estático.
Lamentablemente, parece que los lenguajes con muchos fans entusiastas tienden a caer en la actitud de “no quiero usar bibliotecas de otros lenguajes”.
Clojure es, por lejos, mi lenguaje favorito y se siente natural.
Pero hay dos cosas que realmente extraño de otros lenguajes: mensajes de error comprensibles y sugerencias de tipos.
En la empresa tuve que refactorizar una base de código grande y fue doloroso. Es difícil saber qué función va a lanzar una excepción o simplemente devolver nil, y también si una función clave va a devolver un vector, una secuencia u otra colección.
Hay proyectos que intentan resolver esto, como spec, malli y typed clojure, pero no se sienten como sustitutos cercanos a los tipos de Python o TypeScript.
Un lenguaje que no puede decirte qué entra y qué sale de una función me parece casi inútil en una base de código de más de unos pocos archivos.
Intentar inferir el tipo mirando lo que el código actual parece poder hacer con un objeto es buscarse sufrimiento.
Si estás conociendo Clojure por primera vez, conviene revisar este tutorial interactivo: https://tryclojure.org
Es un mejor conjunto de aprendizaje interactivo que va desde niveles fáciles hasta intermedios y difíciles.
Clojure es divertido.
Rails dominó el desarrollo web en la década de 2010, y muchas startups enormemente exitosas se construyeron sobre él.
Mucha gente se burla diciendo que los ingenieros “juegan con juguetes”, pero muchas de las cosas buenas que tenemos hoy alguna vez fueron juguetes. Linux también es un buen ejemplo.
Lo complicado de un lenguaje muy “divertido” es que uno empieza a asumir que puede resolver todo con ese lenguaje.
Al final terminas escribiendo mucho código, y al mirar atrás quizás no escribir código habría sido una solución suficientemente buena.
[1] https://franz.hamburg/writing/clojure-makes-happy.html