3 puntos por GN⁺ 2023-07-28 | 1 comentarios | Compartir por WhatsApp
  • En el marco de pensar en lenguajes que puedan seguir usándose en el futuro, esta elección tiene menos que ver con la practicidad y más con experimentar directamente con un flujo de desarrollo antiguo pero moderno
  • Janet es un lenguaje de scripting pequeño de la familia Lisp/Clojure que apunta a un espacio similar al de Lua, y llamó la atención por su biblioteca estándar y su metaprogramación basada en macros
  • Ventajas aparentes de Janet como la portabilidad, la posibilidad de embeberlo y las Parsing Expression Grammars tenían una relación limitada con las necesidades actuales, y alternativas existentes como CHICKEN Scheme, CLISP, SBCL y Packrat parecían suficientes
  • El punto clave del desarrollo guiado por REPL de Common Lisp es que, desde un estado de error en tiempo de ejecución, se puede inspeccionar la pila, las variables y los datos vivos, modificarlos y luego reanudar la ejecución interrumpida
  • Aunque aprender un nuevo lenguaje y su biblioteca estándar tiene un costo, se consideró que valía la pena probar directamente este flujo de trabajo en un entorno donde fue incorporado al diseño desde el inicio

Por qué el interés pasó de Janet a Common Lisp

  • Venía pensando en future-proof programming languages porque quería escribir código que pudiera seguir usando en el futuro, pero al crear algo para compartir adopto una postura pragmática: elegir un lenguaje popular, portable y cómodo
  • Al mismo tiempo, también exploro otros lenguajes y herramientas de programación por diversión y experimentación
    • Estaba siguiendo lenguajes interesantes de nombres cortos como Nim y Zig
    • Estos experimentos a veces llevan a descubrir nuevas formas de programar o herramientas que luego se vuelven indispensables
  • Hace poco, al encontrar el libro gratuito Janet for Mortals, surgió mi interés por Janet
    • Janet es un lenguaje de scripting relativamente pequeño inspirado en Lisp/Clojure, que apunta a un nicho similar al de Lua
    • Ofrece una biblioteca estándar real, metaprogramación al estilo Lisp y ejecución en tiempo de compilación mediante macros
  • Lo que más me atrajo de Janet, además de su carácter Lisp en general, fue su portabilidad, la posibilidad de embeberlo y las Parsing Expression Grammars
    • Como actualmente no necesito embeber un lenguaje, esa posibilidad no fue una razón decisiva
    • En cuanto a portabilidad, CHICKEN Scheme, CLISP y Steel Bank Common Lisp también parecían buenas opciones
    • Para parsing, Packrat parecía una elección razonable
  • Al final, quedaron menos razones para elegir Janet en lugar de un Lisp existente

El flujo guiado por REPL que me hizo aprender Common Lisp

  • Un artículo sobre el flujo de trabajo guiado por REPL, casi exclusivo de Common Lisp, se convirtió en la motivación para aprenderlo
  • El ejemplo es una situación en la que la función foo llama a una función bar que aún no está definida, y se produce un error durante la ejecución de foo
    • En entornos Lisp antiguos o Smalltalk, la interrupción de foo lleva a un breakloop
    • El breakloop es un REPL completo dentro del entorno dinámico de la función interrumpida
    • Se puede recorrer la pila de llamadas interrumpida y revisar las variables visibles en cada stack frame
    • Se pueden inspeccionar y modificar los datos vivos del programa en ejecución
    • Si la causa es una variable o un valor de campo incorrecto, se puede cambiar el valor y luego reanudar la función interrumpida
    • Dentro del REPL se puede definir la función bar faltante y reanudar foo para obtener un resultado razonable
  • Ya había visto varias funciones de “edit and continue”, pero me resultó novedoso que Common Lisp haya sido creado teniendo en mente este tipo de flujo de trabajo
  • Mi experiencia de depuración ya había pasado de printf debugging al uso de depuradores reales
    • printf debugging consiste en insertar temporalmente código de logs, recompilar y revisar el resultado de la ejecución
    • En situaciones donde no se puede modificar o volver a ejecutar el programa, tuve que aprender a usar un depurador
    • Un depurador real es la herramienta correcta, pero configurar el entorno de depuración puede ser doloroso o, a veces, imposible
  • Common Lisp parecía un entorno que lleva la depuración un paso más allá
    • Ya había tenido experiencias modificando memoria desde un depurador para probar posibles arreglos
    • Pero la posibilidad de reescribir y parchear código dentro de un proceso en ejecución resultaba especialmente atractiva
  • El objetivo de aprender Common Lisp es comprobar directamente si construir programas de forma interactiva con este enfoque es una manera agradable de trabajar
  • No está claro si aprender un lenguaje nuevo y su biblioteca estándar solo para explorar un flujo de trabajo nuevo sea la mejor decisión, pero tampoco hay una alternativa claramente mejor
    • Otros lenguajes pueden tener integraciones similares entre REPL y editor
    • Sin embargo, considero que esas funciones podrían tener más bugs porque no fueron soportadas desde el inicio, sino agregadas después
    • Aunque no quede satisfecho, quedará el hecho de haberlo intentado en las mejores condiciones, con las herramientas estándar
  • Los recursos que ayudaron a aprender Common Lisp están recopilados en otro artículo, Common Lisp resources

1 comentarios

 
GN⁺ 2023-07-28
Opiniones de Hacker News
  • No esperaba ver mi artículo por aquí. Algún día me gustaría escribir una continuación, pero todavía soy principiante.
    Hasta ahora, lo que me ha gustado de Common Lisp es que el sistema de condiciones es limpio, y que con los reinicios (restarts) se puede controlar fácilmente código que está muy lejos. El desarrollo guiado por REPL también resulta cómodo en situaciones donde no se sabe bien qué va a pasar, por ejemplo al parsear una fuente de datos desconocida, porque permite corregir el código y seguir sin perder el contexto.
    Hay muchas implementaciones y la interoperabilidad también es buena; en un caso pude alternar entre SBCL para velocidad y CLISP para reducir el uso de memoria. Una de las razones por las que me incliné más por CL que por Scheme para aprender Lisp fue precisamente que hay varias implementaciones compatibles.
    Aunque soy principiante en Emacs, la integración con Common Lisp fue excelente, y funciona bien incluso en la netbook lentísima que uso para desarrollar. En una época de computadoras rápidas, VS Code y servidores de lenguaje, ya no es una ventaja tan grande, pero definitivamente tiene una vibra retrofuturista.
    También hay cosas que lamento. QuickLisp, el gestor de paquetes más popular, está bien, pero no llega al nivel de funcionalidades al que nos acostumbraron los ecosistemas de lenguajes modernos. El lenguaje en sí quedó congelado en el tiempo, así que para muchas cosas como hilos, sincronización y argumentos de línea de comandos se necesitan bibliotecas de interoperabilidad. Y sería realmente bueno que SBCL soportara compilaciones completamente estáticas para poder distribuir binarios también en distribuciones de Linux que no usan glibc.
    • También hay algunas desventajas. La última vez que lo revisé, QuickLisp obtenía paquetes únicamente por HTTP en texto plano, sin cifrado ni ningún mecanismo de verificación para detectar modificaciones durante la transmisión.
      Si no se admite cifrado ni autenticación para una tarea crítica como obtener código fuente, creo que QL difícilmente sea una opción para alguien que se preocupa por la seguridad.
      Otro problema es que SBCL esté alojado en SourceForge. SourceForge en el pasado inyectó malware en archivos de descarga, así que también considero esto un problema de seguridad. Hoy no parece haber una razón válida para seguir usando SourceForge, y no entiendo bien por qué un proyecto importante todavía permanece ahí.
      Me gustan mucho Lisp y, en especial, Common Lisp, pero cosas como estas me alejaron, y tampoco parece haber mucho interés en corregirlas.
    • Si necesitás compilaciones estáticas y te sirve usar un SBCL algo antiguo, el trabajo de daewok, que compiló y enlazó SBCL en un entorno musl, puede ser la solución que estabas buscando. Intenté portar el parche a una versión más reciente, pero por cambios en upstream sigue habiendo fallos de segmentación.
      https://www.timmons.dev/posts/static-executables-with-sbcl.h...
      https://www.timmons.dev/posts/static-executables-with-sbcl-v...
    • Te doy un cons.
      https://cons.io
      Gerbil/Gambit Scheme puede generar binarios completamente estáticos como alternativa a CL.
    • Estoy creando CIEL para cuando quieras empezar con una buena colección de bibliotecas (json, csv, bases de datos, cliente HTTP, argumentos de línea de comandos, extensiones del lenguaje, etc.): https://github.com/ciel-lang/CIEL/
      Se puede usar como biblioteca normal de Quicklisp, como imagen core de arranque instantáneo, o como binario. También puede ejecutar scripts casi al instante, así que en ese aspecto se parece a Babashka. Todavía estoy puliendo detalles y no es v1.0.
      Lo de “manejar un error en tiempo de ejecución corrigiendo el código roto en el lugar, sin reiniciar” del blog debería haberse mostrado aquí: https://www.youtube.com/watch?v=jBBS4FeY7XM
      Si estás corriendo un cálculo largo y pesado y aparece un error en el último paso, en lugar de volver a ejecutar todo desde el principio, entrás al depurador interactivo, vas a la línea con el error, compilás la función corregida, volvés al depurador y reanudás la ejecución desde un punto del stack frame. Después podés ver cómo el programa pasa.
  • En el trabajo uso Clojure, pero cada vez que tengo que depurar o necesito código rápido, extraño casi todo de Common Lisp. Poder hacer clic en cualquier parte del stack dentro de errores anidados para inspeccionar los enlaces léxicos es muy útil, y siento que hacer clic en un objeto y luego copiarlo al REPL con M- es mucho mejor que lo que ofrece Clojure con tap>. Incluso usando herramientas como Portal, me parece que tap> está más cerca de ser un pretty-print glorificado.
    En cuanto al rendimiento, Common Lisp también permite especificación de tipos estáticos, y SBCL puede generar código muy eficiente cuando se hace eso. Ayuda ejecutar DISASSEMBLE sobre código propio para ver qué se emite realmente y optimizarlo. En particular, paquetes como SB-SIMD y Loopus ayudan muchísimo en aplicaciones de cálculo numérico.
    • Resume bien las frustraciones que sentí mientras escribía mi tesis de maestría en Clojure. No es que me moleste la elección en sí. Clojure, con esa sensación de “todo es un mapa clave-valor” y el enfoque de “si la clave :quack es true, trátalo como pato”, encaja bien con el diseño de juegos de tipo entidad-componente-sistema.
      Pero durante el último año extrañé muchísimo la forma de desarrollar en Common Lisp y, por Dios, el sistema de condiciones. Ni siquiera soy un hacker tan hábil en CL, pero se me incorporó rápidamente. Ojalá CLOS y los tipos de datos primitivos de CL encajaran mejor de lo que parece.
    • En Clojure vale la pena mirar flowstorm. Permite hacer step hacia adelante y hacia atrás dentro de una función, y enviar mapas al REPL junto con esas funciones.
    • No creo que ninguno de los dos lenguajes ofrezca como parte del lenguaje la función de enviar formularios al REPL; eso es una función de las herramientas.
      En Cider definitivamente se puede hacer con facilidad, y supongo que las herramientas principales de otros editores también lo hacen suficientemente bien.
    • En Clojure también se puede hacer algo parecido a DISASSEMBLE hasta cierto punto.

Hay proyectos auxiliares como https://github.com/Bronsa/tools.decompiler, y también está JitWatch de OpenJDK (https://github.com/AdoptOpenJDK/jitwatch). También hay herramientas similares para otras JVM
No es tan intuitivo como Lisp, pero aun así se puede

  • Steel Bank Common Lisp es el caballo de batalla que me permitió crear empresas de software rentables. Sin esto no creo que hubiera sido tan productivo. El flujo de trabajo guiado por REPL es excelente, las imágenes de Lisp son muy sólidas y el rendimiento también es alto
    • Para quienes tengan curiosidad, ¿podrías compartir cuáles son esas empresas?
    • Se ve genial, pero ahora me da demasiada pereza volver a Emacs. Para desarrollo principalmente en Python y JavaScript uso VSCode casi con la configuración por defecto. Estoy en un rol de CTO, así que no programo full time
    • Creo que ya pasó en gran parte la época en que el stack tecnológico importaba aunque fuera un poco
      Está bien haber creado algo esotérico que funciona de manera atractiva, pero ahora nadie querrá tocarlo salvo personas cuyo sueldo se puede expresar en cantidad de ceros
    • Dicen que el flujo de trabajo guiado por REPL es asombroso, pero me pregunto si no saben que hoy en día prácticamente todos los lenguajes de máquina virtual/intérprete tienen REPL
      https://www.digitalocean.com/community/tutorials/java-repl-j...
      https://github.com/waf/CSharpRepl
      https://pub.dev/packages/interactive
      Sin mencionar Ruby, Python, PHP y Lua; incluso C++ tiene un REPL, aunque algo tosco: https://github.com/root-project/cling
      Cada vez que digo que el REPL ya no es un factor diferenciador de Lisp, me llenan de downvotes
  • Si te interesan los gráficos 3D, hay una base de código en Common Lisp que vale la pena revisar. Intenté mantenerla simple y fácil de entender
    https://github.com/kaveh808/kons-9
    • Conviene buscar en YouTube los videos tutoriales de CL de Kaveh. Están realmente bien hechos
  • Como desarrollador de Clojure, el break loop y el flujo de trabajo guiado por REPL se ven geniales, y creo que sin duda también ayudarían a Clojure. Podría parecerse más a programar frontends en JS/TypeScript usando las excelentes herramientas de depuración del navegador. Lamentablemente, el estado actual de las herramientas y el soporte de la comunidad en el ecosistema de Clojure parece bastante pobre
    • Clojure también puede imitar hasta cierto punto un flujo REPL real cuando se trata de llamadas que ocurren en cada request desde capas profundas del código, como en un servidor web. Mientras el servidor sigue corriendo, puedes reescribir y recargar varias funciones y luego volver a hacer el request desde el navegador
      Pero esas redefiniciones y sobrescrituras terminan contaminando el namespace, y en algún momento algo se rompe y hay que recargar el servidor
  • Me encanta CL y lo extraño mucho cuando hago otras cosas. Comparado con mi experiencia con Lisp de hace décadas, muchas cosas en los lenguajes “modernos” son tan dolorosas que ya ni da risa
    También tiene muchas desventajas, pero no son peores que las desventajas puramente técnicas de casi todos los demás lenguajes y entornos de desarrollo. Python y JS se usan más y tienen más bibliotecas, pero siento que la experiencia de desarrollo es mucho peor. Durante años he usado mucho C#, TS, Py, Hs y hasta lenguajes más esotéricos, pero cada vez que me atasco con ellos o me enojo seriamente por cosas claramente malas, termino volviendo a CL(SBCL + Emacs + Slime). Entonces me tranquilizo y recupero la certeza de que todavía hay cosas buenas en el mundo
    Ahora estamos levantando inversión para un producto que tontamente bootstrappeamos en TypeScript, pero pensamos rehacer la versión de lanzamiento en CL. Eso significa que durante los próximos 3 a 5 años, antes del lanzamiento, voy a trabajar con CL y dentro de CL, y me alegra poder hacer cosas divertidas como implementar DSL, generar código, trabajar con macros e implementar un analizador de tipos estáticos
    • No puede existir un Lisp sin cons
      Cuando trabajas en C#, ¿qué extrañas de Lisp?
    • ¿Por qué TypeScript no es adecuado para ese producto?
  • En el blog y en los comentarios del autor aquí se habla mucho de “programar”, pero casi no se ve qué tipo de software crean ni a qué casos de uso apuntan
    Mi idea radical es que la razón por la que la programación funcional no se volvió masiva es que, aunque claramente sirve para escribir programas, la mayor parte del software actual no es “un programa que se ejecuta desde la línea de comandos en mi PC/servidor y corre hasta terminar”, sino “un programa que arranca, responde a entradas del usuario y luego el usuario cierra”, o “un programa que arranca y luego responde a la red u otro I/O automático hasta que otro software le dice que se detenga”
    Este tipo de tareas es mucho más difícil de hacer en un estilo funcional puro, o al menos así fue en las implementaciones funcionales con opiniones fuertes que probé. Porque ya no se trata simplemente de evaluar una expresión, sino de inicializar estado, reaccionar a I/O, actualizar estado o realizar I/O adicional, e incluso usar paralelización para monitoreo, recepción, I/O adicional y actualización de estado
    Por supuesto, no es imposible hacer estas cosas con Lisp, pero por mi experiencia con algunos semestres de programación funcional en la universidad y usando profesionalmente características funcionales en C++/Scala para resolver estos problemas, me pareció bastante difícil hacer que la programación funcional funcionara bien en este tipo de aplicaciones, y esa falta de encaje me hace dudar a la hora de profundizar más en la programación funcional
    • Buen punto. Por ahora todo es solo un hobby. Lo que he hecho con Common Lisp es un cliente común y corriente de Battlesnake y un juego multijugador de mezclar palabras que cerró ayer. Ninguno de los dos obtuvo una gran ventaja por estar hecho en Lisp, pero aprendí mucho en el proceso, y eso era lo importante

Al margen de Common Lisp, con frecuencia tuve que escribir código que genera código. En este ámbito parece que Lisp podría brillar, pero todavía no tuve oportunidad de probarlo. Algunos ejemplos de proyectos recientes que hice antes de pensar en Common Lisp incluyen generar, en un generador de sitios estáticos, código para validar cierto JSON Schema, y generar JSX a partir de Markdown para usarlo en el contenido narrativo de un juego de programación
Por no hablar de la incontable cantidad de macros de C que he usado todo este tiempo

  • Common Lisp incluye CLOS, que aún hoy es uno de los sistemas de orientación a objetos más avanzados. La mayoría de los Lisp no son lenguajes funcionales como Haskell
  • Elixir usa programación funcional y es excelente cuando se necesita desarrollo web o sistemas tolerantes a fallas
    Tampoco hace falta tirar a la basura todas las buenas funciones de otros estilos. Parte de la programación funcional se está volviendo cada vez más común incluso en lenguajes generales. Por ejemplo, Rust también usa patrones funcionales en muchos casos
    Y Lisp también se puede usar, si uno quiere, con estilo orientado a objetos o imperativo. No es Haskell
  • Me llevó un tiempo entender las mónadas y la mónada IO, y más tiempo aún aprender a componerlas de forma segura y a manipular el orden de ejecución. Pero ahora puedo escribir aplicaciones con seguridad de tipos y cometo menos bugs incluso cuando trabajo en lenguajes que no son funcionales. El lenguaje por el que me pagan es Java
    Veo a Lisp como el punto de partida y a Haskell como lo verdadero. Recomiendo aprender ese estilo, aunque no se escriba código real de producción
  • Common Lisp no es un lenguaje “puramente funcional”, sino que soporta todos los paradigmas. También permite cosas raras como esta
    (let ((pair (cons 1 nil)))
    (setf (cdr pair) pair)
    (list (first pair) (second pair) (third pair)))
    ;; => (1 1 1)
  • Si quieres algo que “se ejecute para siempre”, escríbelo en Rust
    Si quieres algo que “compile para siempre”, escríbelo en ANSI C
    Si quieres algo que “sobreviva para siempre”, escríbelo en Python 2.7 y conviértelo en la columna vertebral de las plantillas de infraestructura de toda la organización. Puntos extra si es un módulo Ansible personalizado
  • Igual que en la frase “como es 2023, obviamente estoy aprendiendo Common Lisp”, yo también lo estoy aprendiendo, pero todavía no siento que las funciones mencionadas sean útiles. Quizá cambie de opinión cuando me acostumbre
    Lo que espero es que Lisp minimice o abstraiga lo accesorio y el boilerplate para que sea más fácil expresar ideas como código. Para mí, las expresiones Lisp parecen la forma más natural de expresar ideas
    Me interesa sobre todo escribir apps con GUI, así que quiero aprender Clog o encontrar, o desarrollar, un buen wrapper sobre algún toolkit de GUI
  • El punto central es que Scheme y Janet también son excelentes, pero el autor quiere un lenguaje más autónomo. Lo que marca la diferencia es el breakloop, un REPL completo que se abre cuando ocurre un error en el programa. No es un stack trace ni un depurador, sino una forma de construir directamente desde el punto actual que está roto
    • Suena realmente impresionante, ¿por qué Common Lisp no es el lenguaje más popular? Lo pregunto como alguien que casi no escribe código
    • Como desarrollador de Clojure, suena interesante, pero me pregunto qué pasa en el siguiente caso. No es código muy “lispero”; es solo un ejemplo para mostrar el punto que no entiendo
      (do-it (do-it first))
      (do-it first) funciona bien, pero ¿qué pasa si falla la llamada a (do-it (do-it first))?
      Sería bueno poder obtener el control en el punto roto y corregir la definición de do-it. Pero si la corriges, también cambia el resultado de (do-it first)
      Entonces el punto en el que la máquina se encuentra actualmente pasa a ser un punto al que ya no se puede llegar con el código actual
      Realmente no entiendo cómo funciona esto cuando haces una modificación que permitiría continuar, pero esa misma modificación cambia el estado en el que recibiste el control para hacerla
    • Escuchar que en el punto donde ocurre el error se abre un REPL completo, y que no es un stack trace ni un depurador sino que se construye directamente desde el punto actual roto, me dan ganas de abrir una pequeña imagen de Smalltalk
    • Esto también es estándar en Gambit Scheme
    • ¿Estas cosas no se convierten en vectores de ataque para código malicioso o vulnerabilidades de seguridad en producción?