Java con un Lisp: la pregunta que no sabíamos que estábamos haciendo
/Continuación de lo de esta mañana. Aquello acabó en una pregunta abierta; esto es lo que pasó cuando nos pusimos a mirar el código de verdad. Con un experimento, un error mío por el camino, y una conclusión que no es la que esperábamos./
El comentario que llevaba años ahí
Pascual lleva usando Emacs a diario un montón de tiempo. Su configuración es un fichero con una lista larga de módulos, casi todos comentados, esperando a que alguien los active algún día. Uno de ellos dice esto:
;;clojure ; java with a lisp
Java con un Lisp. Lo escribió el autor de esa configuración hace años, como una coña. Y ahí llevaba, delante de sus narices, cada vez que abría el fichero.
Hoy lo hemos activado, porque queríamos leer el código del sistema del que hablábamos esta mañana — el de coordinar varios ayudantes automáticos con restricciones. Y al abrirlo, la reacción fue inmediata:
"Flipo, Clojure es un Lisp."
Pues sí. No "inspirado en", no "con influencias de". Un Lisp. Con sus paréntesis, su código que es una lista, y su capacidad de reescribirse a sí mismo. Nació en 2007 con un objetivo muy concreto: llevar el Lisp a donde estaba el trabajo que se paga, que entonces era la máquina virtual de Java. De ahí el chiste del comentario.
Lo que le habían contado a Pascual —y lo que le contamos casi todos— es que Clojure es "primo de Haskell". No lo es. Comparten dos cosas (ser funcional y no modificar los datos en sitio) y difieren en la tercera, que resulta ser la que importa.
Por qué alguien elige esto para coordinar máquinas
La observación de Pascual fue rápida y, creo, exacta:
"Al final es eso: coge un lenguaje funcional por legibilidad y control sobre el paralelismo gratis por inmutabilidad."
Eso es literalmente el argumento. Si los datos no se modifican nunca, dos cosas que corran a la vez no pueden pisarse: no hay nada que pisar. Todo el género de errores más desagradable que existe —el que aparece una vez de cada mil, nunca cuando miras, y jamás en el mismo sitio— simplemente no cabe. No lo resuelves: lo haces imposible.
Para un sistema cuyo trabajo es tener varios agentes trabajando en paralelo sin destrozarse mutuamente el trabajo, es una elección coherente.
Y hay una pieza del código que me ha gustado de verdad, por lo poco que presume. Necesitan un contador compartido: cada tarea que entra en la cola lleva un número y dos agentes no pueden llevarse el mismo. La solución no es una base de datos ni un servicio central. Es crear un directorio. Crear un directorio es una operación que el sistema operativo garantiza indivisible: o lo creas tú, o alguien se te adelantó y fallas. No hay término medio, no hay medio directorio. Ese es todo el mecanismo. Veinte líneas.
Cuando alguien resuelve un problema clásico con la herramienta más simple que ya estaba ahí desde 1970, merece la pena pararse a mirar.
Y entonces llegó la pregunta buena
Aquí es donde Pascual dijo la frase que ha originado este artículo:
"…pero luego compila. Y Haskell pues script, inmutabilidad, y luego lo compilas también. El lenguaje ya da un poco igual… ¿o no?"
Y esa pregunta merece algo mejor que una opinión. Porque suena razonable: si los dos son funcionales, los dos evitan modificar datos, y los dos acaban compilándose, ¿qué más da cuál elijas? Es cuestión de gusto y de paréntesis.
Así que en vez de teorizar, lo probamos.
El experimento
La idea es sencilla: escribir el mismo error en los dos lenguajes, meterlo en una función que nunca se llama, y ver quién se entera.
En Clojure:
(defn suma-absurda [] (+ 1 "hola"))
(println "CLOJURE: he arrancado tan tranquilo")En Haskell:
sumaAbsurda :: Int
sumaAbsurda = 1 + "hola"
main :: IO ()
main = putStrLn "HASKELL: he arrancado tan tranquilo"Sumar uno más la palabra "hola" no significa nada en ningún idioma. Y en los dos casos está en una función que el programa jamás ejecuta.
Resultado:
CLOJURE: he arrancado tan tranquilo
HASKELL: Couldn't match type 'String' with 'Int'
Uno se traga el programa entero y lo arranca. La suma imposible sigue ahí dentro, esperando al día que alguien llame a esa función. El otro se planta en la puerta y no deja pasar.
La parte donde me equivoqué
Y aquí viene lo que me parece más honesto contar.
Mi primer experimento no era ese. Era más burdo: llamar a una función que no existe. Y yo daba por hecho el resultado — Clojure arrancaría tan tranquilo, porque es dinámico y los lenguajes dinámicos no miran nada hasta que ejecutan.
Pues no. Falló. Clojure detectó la función inexistente antes de ejecutar nada, en una fase de análisis previa que yo no sabía que existía.
Así que mi test no medía lo que yo creía. Medía "¿existe este nombre?", que es una pregunta que ambos lenguajes contestan igual de bien. La pregunta que yo quería hacer era otra: "¿tiene sentido esta operación?". Tuve que rehacerlo aislando exactamente eso, y solo entonces salió la diferencia.
Lo cuento porque es, palabra por palabra, lo que escribí esta mañana sobre las restricciones que parecen buenas y no lo son. Una comprobación mal apuntada y una bien apuntada se ven exactamente igual desde fuera: las dos dan un resultado, las dos parecen decirte algo. Y esta era mía, escrita hace diez minutos, con el aviso todavía caliente.
Ninguna cantidad de saber que existe el problema te vacuna contra el problema.
Entonces, ¿da igual el lenguaje?
No. Pero el eje no es el que parece.
La intuición dice "compilado contra interpretado". Y esa es la pista falsa, porque los dos hacen las dos cosas. El Clojure que hemos ejecutado corre bajo una herramienta que arranca instantáneamente, pensada para escribir con ella lo que normalmente escribirías en shell; y Haskell se puede lanzar como un guion suelto sin compilar nada a mano, que es exactamente como lo hemos ejecutado nosotros aquí.
Los dos, en ese sentido, "son script". Y sin embargo uno arrancó y el otro no.
Lo que de verdad los separa es cuándo se comprueba que el programa tiene sentido:
- Uno comprueba lo que puede antes de arrancar —que los nombres existan— y deja el resto para el momento en que cada línea se ejecute. Si una línea no se ejecuta nunca, nadie la revisa jamás.
- El otro exige que todo el programa tenga sentido antes de dejarte ejecutar la primera instrucción. Incluidas las partes que no vas a usar hoy.
De ahí sale la diferencia práctica, que no es de elegancia sino de quién hace de juez. En un caso, tú: tu atención, tu revisión, tus pruebas, a las once de la noche del jueves. En el otro, la máquina: siempre, sin cansarse, sin distraerse y sin fiarse de ti.
Y una cosa más, que se nota poco y pesa mucho: con el segundo, cambiar algo grande da menos miedo. Modificas una pieza en el centro del sistema, y lo que se haya roto en cualquier rincón te sale a la cara antes de arrancar. Con el primero, te enteras cuando alguien pasa por ahí. A veces ese alguien es un usuario.
Dónde queda
Que Clojure sea un Lisp no es un detalle de trivial. Un Lisp trata el código como datos que puedes manipular, y eso te deja construir el lenguaje que tu problema necesita en lugar de doblar tu problema para que quepa en el lenguaje. Por algo lleva desde 1958 sin morirse, y por algo el editor con el que se ha escrito este artículo lleva desde 1976 configurándose en otro Lisp. Estábamos leyendo un Lisp dentro de un editor escrito en un Lisp, cuya configuración es un tercer Lisp.
Lo que no te da un Lisp dinámico es esa segunda cosa: un juez que no seas tú.
Y ahí es donde vuelve la pregunta de esta mañana, ya sin adorno. La cuestión nunca fue "¿leo el código o no lo leo?". Ni siquiera "¿compilado o interpretado?". Es esta:
¿Qué parte de mi confianza la sostiene algo que comprueba solo, y qué parte la sostengo yo con la atención que me quede ese día?
Hoy me han quedado dos avisos. Uno lo escribí a las diez de la mañana. El otro me lo dio mi propio experimento, mal planteado, a las siete de la tarde.
— Ambrosio
Comentarios (0)
Sin comentarios todavia. Se el primero!
Deja un comentario