La transición a Elixir
(leemeichin.com)- Un desarrollador backend de Ruby se unió a una empresa que usa Elixir como lenguaje principal y, gracias a una sintaxis influenciada por Ruby, pudo adaptarse relativamente rápido incluso sin experiencia previa con Elixir/Erlang
- Elixir ofrece, como Ruby, una sintaxis divertida y afinidad con los DSL, pero con un modelo funcional inmutable sin clases, instancias ni herencia, lo que reduce la carga del manejo de estado
- Mientras que los DSL de Ruby dependen de definir dinámicamente objetos y métodos en tiempo de ejecución, los DSL de Elixir usan macros en tiempo de compilación para generar código de runtime, haciendo que los errores aparezcan antes
- La sintaxis de pipeline, la expresión
withy las tuplas Result con forma de{:ok, result}son útiles para separar el flujo normal del manejo de errores recuperables - Frente al enfoque de Ruby, que suele apoyarse en excepciones,
nil/falsey estados de error internos en objetos, las devoluciones explícitasok/errorde Elixir resultan más satisfactorias, y después de 3 meses de uso sigue siendo agradable escribirlo
Cómo alguien con experiencia en Ruby se adaptó a Elixir
- En su nuevo trabajo usan Elixir como lenguaje principal del backend, y antes de empezar no tenía experiencia escribiendo código en Elixir ni en Erlang
- Erlang no le resultó del todo ajeno porque había tocado un poco de Prolog, y Elixir pudo aprenderlo rápido gracias a una sintaxis fuertemente influida por Ruby
- Aunque todavía no domina las mejores prácticas, la arquitectura ni conceptos más de bajo nivel de Erlang, eso no le impidió entrar a Elixir
- Elixir se percibe como un lenguaje que mantiene la diversión de programar que ofrece Ruby, pero reduce las trampas relacionadas con el estado que son comunes en los lenguajes OOP
- No hay clases, instancias ni herencia
- Se basa en la inmutabilidad y un estilo funcional
- No está atado a un sistema de tipos estático
- El código con estado mutable es mucho más difícil de mantener, y no siempre está claro si el estado es local a una función, de una instancia o de una clase
- En Ruby todo es un objeto, así que cada objeto puede tener estado, y al intentar crear DSL intuitivos es fácil mezclar estado a nivel de función, instancia y clase
- La eigenclass de Ruby es algo así como la clase de una clase, y una instancia puede modificar su eigenclass y afectar también el estado de otras instancias
La diferencia entre los DSL y las macros en tiempo de compilación
- Elixir, igual que Ruby, también soporta DSL y su sintaxis también resulta familiar, pero la diferencia clave está en cuándo se crean esos DSL
- Los DSL de Elixir son macros en tiempo de compilación y generan código que se ejecuta en runtime
- Los DSL de Ruby dependen de modificar el runtime para definir dinámicamente objetos y métodos
- Si se usan mal las macros de Elixir, los errores aparecen en la librería, y si el código generado por la macro está mal, el compilador lanza un error
- Como esos errores aparecen en tiempo de compilación, las pruebas pueden enfocarse más en la lógica real de la aplicación
El flujo que crean el pipeline y with
- El operador de pipeline
|>de Elixir es útil, pero los operadores de threading de Clojure también tienen ventajas->en Clojure inserta el resultado de la expresión anterior como primer argumento de la siguiente función, igual que|>en Elixir->>en Clojure inserta el resultado anterior como último argumento de la siguiente función- Aunque la diferencia parezca pequeña, es útil al escribir código de interoperabilidad sin recurrir a funciones anónimas
- La expresión
withde Elixir recuerda alletde Haskell o Lisp, pero funciona como si llevara unotherwiseintegrado- Permite dividir funciones complejas entre la ruta normal arriba y el manejo de errores abajo
- Si no se puede hacer pattern matching con
{:ok, result}, puede tratarse como un error recuperable, así que encaja bien con las tuplas Result
Las ventajas de devolver errores de forma explícita
- En Ruby es común usar excepciones para el flujo de control y, en lugar de tipos de error explícitos, muchas veces se depende de un resultado exitoso,
nil,falseo un estado de error dentro del objeto- Por ejemplo,
model.update(params)devuelvefalsesi falla y guarda la razón del fallo enmodel.errors - Esto vuelve a llevar al problema del estado mutable
- Por ejemplo,
- Un enfoque como los mónadas
Result/Either, dondeok/erroroleft/rightse expresan explícitamente en el valor de retorno, se siente más satisfactorio que el flujo basado en excepciones típico de Ruby - Incluso después de 3 meses usando Elixir, sigue siendo un lenguaje que conserva esa sensación de que “programar es divertido” que Ruby mostraba
1 comentarios
Opiniones de Hacker News
En las últimas semanas he estado viendo videos de Elixir en YouTube casi todos los días.
Parece que hace poco hubo una conferencia de Elixir y, después de ver algunos, YouTube no deja de recomendarme contenido sobre Elixir.
Me gustan las ideas de Erlang, pero cuando antes trabajé con cosas como el servidor de chat ejabberd, me pareció demasiado peculiar.
Cada vez que veo videos de Joe Armstrong pienso que fue un genio subestimado, y los procesos aislados y el paso de mensajes, es decir, el modelo de actores, se ven como un buen candidato para el futuro de la programación distribuida.
También me llamó la atención que en el video del nuevo lenguaje Misty de Crockford https://www.youtube.com/watch?v=R2idkNdKqpQ las preguntas se fueron hacia Elixir/Erlang.
Dicho eso, al ver la “magia” de LiveView y LiveBook todavía no estoy convencido al 100%. Las cosas que se ven demasiado buenas suelen tener una trampa, y términos desconocidos como GenServer y el conocimiento específico de la plataforma se sienten como entrar por un portal de un solo sentido; no sé si sea conocimiento transferible a otros lugares.
En un contexto en el que Go también tiene una historia bastante buena de concurrencia, un lenguaje y una plataforma con una comunidad pequeña implican muchos riesgos en contratación, bibliotecas y material. Aun así, lo que ha logrado la comunidad de Elixir es realmente impresionante, y también tengo ganas de ver cómo resultará la implementación del sistema de tipos.
send.GenServer es una generalización natural de un patrón que terminarías escribiendo tú mismo incontables veces, y el conocimiento sobre actores sí se puede trasladar a otros lugares.
En Go también hay bibliotecas que implementan abstracciones de actores, y el buzón de un proceso puede verse como una cola de mensajes, igual que los canales de Go. La diferencia está más en cómo funcionan el intérprete y la cesión de tiempo del procesador.
En cambio, sí es cierto que LiveView parece magia. En la práctica tiene muchas piezas móviles encajando entre sí, y es el resultado de trabajo acumulado durante los últimos 10 años en varias comunidades; aunque hay muchas abstracciones, la idea en sí ya está madura.
Cosas como Riak se adelantaron a su época, y casi tenían la idea de que se podían construir sistemas distribuidos robustos de la misma forma en que se construye un GenServer.
Tanto Chris como José se están moviendo mucho para aumentar la exposición fuera del círculo inicial de Elixir, y la gente dentro de la comunidad también está haciendo lo mismo a su propio nivel.
Contratar no es tan complicado como uno pensaría. Hay bastante gente que quiere trabajar con este lenguaje, y los buenos desarrolladores se ponen al día bastante rápido.
Aun así, hay que asumir y manejar el hecho de que es un lenguaje de nicho. Eso es un riesgo para el lenguaje en sí, pero a corto plazo también puede recompensar el dominio de una tecnología de nicho.
En la mayoría de los casos puedes ver qué hace algo, entenderlo y leer el código. Con JavaScript muchas veces no pasa eso.
Es cierto que contratar y encontrar tutoriales es difícil. Por ejemplo, encontrar a alguien que ya haya hecho en Elixir lo que yo quiero hacer es mucho más difícil que en JavaScript.
Las bibliotecas no han sido un gran problema; hay bastantes. Al trabajar con APIs, a veces existen bibliotecas cliente para JavaScript o Python pero no para Elixir, pero normalmente no es tan difícil usar una API solo con un cliente HTTP.
Ahora uso Node.js no porque quiera, sino porque tengo que hacerlo. Después de usarlo, pienso que jamás empezaría un proyecto nuevo con Node.js.
Las herramientas de control de concurrencia, escalabilidad, confiabilidad y observabilidad de Node.js están muy por detrás del ecosistema BEAM.
Creo que cuando a la gente le cuesta creer que Elixir sea tan bueno, a veces es porque está demasiado acostumbrada a plataformas que no fueron diseñadas para la resiliencia.
LiveBook es bastante fácil de modelar mentalmente de forma correcta, salvo por los detalles exactos del cálculo de diferencias o la compresión de datos. Si hay apuro, incluso puedes mirar los mensajes de WebSocket y entender bastante bien qué está pasando.
LiveView es sorprendentemente intuitivo.
Un gran atractivo de Elixir/Erlang es que, en el contexto de un servidor web, permite ejecutar trabajos en segundo plano con mucha facilidad, sin tener que preocuparse de que una E/S bloqueante detenga todo el servidor.
En mi trabajo anterior teníamos que hacer muchas solicitudes HTTP dentro de un manejador de webhooks, y cuando ocurrían suficientes al mismo tiempo, todos los procesos del sistema operativo se ocupaban y todo el sitio se caía.
En esos momentos pensaba con fuerza que, si hubiera sido Elixir, habría bastado con lanzar una Task y seguir adelante.
La idea de compartir objetos entre procesos asíncronos de Tokio se sentía difícil frente a una funcionalidad de primera clase de Elixir. No digo que sea imposible ni intrínsecamente difícil, pero parecía más difícil hacerlo correctamente.
Quiero poner los trabajos en segundo plano sobre una capacidad de cómputo distinta a la de las solicitudes HTTP. Tienen patrones de uso de recursos diferentes, y quiero tener estado o una cola por delante para dejar claros los reintentos, el manejo de errores y la contrapresión.
Claro que sería genial no tener esa complejidad y que el mismo framework se encargara de todo, pero creo que es difícil llegar hasta ahí solo con un lenguaje de programación o un runtime. El monitoreo y los procedimientos operativos también tienen que formar parte de la solución.
La charla The Soul of Erlang and Elixir de Sasa Juric muestra muy bien lo poderoso que puede ser este lenguaje.
https://www.youtube.com/watch?v=JvBT4XBdoUE&t=4
La recomiendo mucho para quienes han oído frases como “déjalo fallar” o “BEAM hace bien la concurrencia”, pero todavía no terminan de captar la idea. Es una charla realmente excelente.
Desde 1991 programo todos los días en Erlang, y pensaba seguir haciéndolo.
En ese tiempo también probé Haskell, Rust, Elixir, etc. Los tipos fuertes son muy valiosos, pero al final todo se reduce a dónde te diviertes más.
Erlang es el lenguaje de programación que más disfrute me da, y planeo seguir hackeando en Erlang al menos hasta que me jubile dentro de unos años.
Conocí Elixir hace poco más de un año, y el proceso de aprenderlo e incorporarlo en varios componentes fue bastante entretenido.
La mayor satisfacción viene de que BEAM/OTP trae muchas cosas de base por sí solo.
Alguien del equipo con el que trabajaba dijo una vez: “BEAM/OTP es como k8s, pero sin las partes complicadas”.
Aprendí Elixir durante varios años, pero una migración a OpenShift, es decir, k8s, que diseñé recientemente diluyó en cierta medida esa ventaja.
Si no existiera k8s, habría sido una propuesta mucho más atractiva. En cambio, mantuvimos el stack de desarrollo existente y logramos con conceptos de k8s lo que habríamos hecho con OTP. Claro que los trade-offs fueron distintos.
“Elixir te da un lenguaje divertido como Ruby, pero te quita las minas antipersonales relacionadas con el estado de los lenguajes orientados a objetos. No hay clases, ni instancias, ni herencia; es inmutable y funcional, y no te ata a un sistema de tipos estático”.
Quiero eso, pero también quisiera que tuviera tipos. En una base de código grande, estoy convencido de que los tipos fuertes son lo correcto, porque exponen mucha magia y hacen que sea más fácil razonar.
Gracias al pattern matching/unificación de las funciones, se puede saber bastante bien qué forma tienen los datos cuando van y vienen. Aun así, si te preocupa, está Dialyzer.
Dialyzer es una herramienta de análisis estático para Erlang/Elixir. Viene incluida en la distribución estándar de Erlang, y su nombre significa “DIscrepancy AnaLYZer for ERlang programs”.
Detecta discrepancias como errores de tipo, código muerto y pruebas innecesarias en un solo módulo de Erlang o en un conjunto de aplicaciones. Infiere tipos a partir de programas correctos, y las anotaciones de tipo no son obligatorias, aunque si existen ofrecen mejores advertencias.
https://fly.io/phoenix-files/adding-dialyzer-without-the-pai...
https://elixir-lang.org/blog/2023/06/22/type-system-updates-...
GenServer se parece más a un objeto e interactúa mediante mensajes, no es simplemente un struct con funciones asociadas.
Me gustaría poder tener una plantilla base para los GenServer del proyecto, con registro común, configuración de memoria, logging, etc., y especializarla para tres variantes de tareas específicas.
Elixir tapa este problema con generación de código mediante macros, pero no es gran cosa. Mis GenServer están llenos de repeticiones triviales.
Creo que decir que “en Ruby es común usar excepciones para el flujo de control” simplemente es falso.
El ejemplo después de ese párrafo es el método
updatede Rails, pero la forma usada en los ejemplos y generadores estándar de Rails es la versión deupdateque no usa excepciones.Cuando una biblioteca o aplicación de cierto tamaño se vuelve lo suficientemente compleja, pasa a clases de error descriptivas. El manejo de errores basado en estado, como en ActiveRecord, es doloroso.
Como Ruby ahora tiene cierto grado de pattern matching, creo que puede reemplazar las excepciones para flujo de control. En vez de lanzar una excepción, se puede devolver la propia clase y hacer matching sobre ella.
Muchas gem usan excepciones para flujo de control, y también era común en apps y bibliotecas que escribí o mantuve.
Rails tiene muchos métodos
bang!que lanzan excepciones en caso de error. Pero, en general, en Ruby, igual que en Python, realmente es común usar excepciones para flujo de control.Vengo de F# y apenas he probado un poco Elixir, pero lamentablemente el lenguaje me resulta algo confuso.
Por ejemplo, las firmas de las funciones solo muestran el nombre y la cantidad de parámetros, no los tipos, así que en bibliotecas grandes es bastante difícil entender mentalmente qué va en dónde.
A menudo uno se da cuenta de que pasó un parámetro incorrecto recién después de ejecutar y depurar el código. En lenguajes con tipado estricto como F#, estos problemas se detectan durante la codificación, lo que reduce mucho el tiempo y el esfuerzo.
Por eso me pregunto cuál es el principal atractivo de Elixir fuera del entorno Erlang.
https://elixir-lang.org/blog/2023/09/20/strong-arrows-gradua...
Hoy por hoy sí es un lenguaje dinámico, así que no se obtiene mucha información de tipos del lenguaje en sí. Todavía no es un lenguaje de tipado estático.
Pero lo que realmente le da ventaja a Elixir frente a otros lenguajes que he usado es BEAM/OTP.
Por experiencia, muchas funcionalidades que en otros lenguajes tendrías que traer como bibliotecas vienen incluidas de base. Esas bibliotecas suelen aumentar mucho la complejidad y a veces también tienen una curva de aprendizaje pronunciada. Las funcionalidades relacionadas con asincronía son un buen ejemplo.
Creo que, después de usar Elixir un tiempo y familiarizarte con estas herramientas y abstracciones integradas, entiendes la potencia de todo el ecosistema.
El principal atractivo probablemente sea que corre sobre BEAM con una sintaxis con aire a Ruby. Creo que BEAM es una ventaja bastante grande.
No se compara bien con los tipos de F#, pero se pueden usar typespecs: https://hexdocs.pm/elixir/1.15.7/typespecs.html
Por ejemplo, hay un flujo en el que el machine learning se integra de forma nativa con Axon, Nx y BumbleBee, e incluso permite compilar para GPU: https://www.youtube.com/watch?v=HK38-HIK6NA
La comunidad realmente está impulsando fuerte esa área.
También es atractivo cómo funciona LiveView. Aunque .NET tiene Blazor.
Elixir puede usarse en distintos contextos, como Nerves para embebidos o scripting. Entiendo que F# también puede usarse para scripting.
En general, BEAM y sus estructuras relacionadas son sus puntos fuertes, y el gestor de paquetes Hex también es realmente bueno. Claro que .NET también tiene buenas herramientas.
Puedes arreglártelas con typespecs, pattern matching y guards, pero es bastante engorroso y, al menos en el plugin de JetBrains impulsado por la comunidad, no es muy amigable para IDE/IntelliSense.
Estaría bueno escribir sobre esto no al empezar, sino después de usarlo 2 años.
Lo nuevo al principio siempre parece una solución universal brillante y sin defectos.
Las primeras impresiones también tienen su lugar, y eso es lo que estoy ofreciendo. El público no son expertos en Elixir/Erlang, sino expertos en Ruby.
Elixir era una joya entonces y lo sigue siendo ahora. No es perfecto, pero aguanta sorprendentemente bien.
Uso Ruby desde alrededor de 2007.
Cada vez que intenté pasarme a la siguiente cosa “genial”, terminé volviendo a Ruby, porque me gusta Ruby, trabajo más rápido con Ruby y eso era todo.
Pero últimamente veo muchas más conversaciones sobre Elixir, y me gusta su aspecto y sensación. Creo que pronto voy a empezar un nuevo proyecto paralelo y probarlo.