Dejar atrás Haskell
(journal.infinitenegativeutility.com)- Un programador que usó Haskell como lenguaje principal desde alrededor de 2009 hasta alrededor de 2019 ya no lo considera la opción predeterminada para proyectos personales ni para buscar trabajo
- El atractivo de Haskell no estaba tanto en las mónadas o los DSL en sí, sino en el razonamiento algebraico sobre el código, la refactorización segura que permite su sistema de tipos fuerte y la expresión de invariantes
- Se fue alejando por la acumulación de factores como una cultura que favorece funciones avanzadas de tipos, herramientas poco fluidas y cambios incompatibles constantes en GHC y la biblioteca estándar
- Aun así, las funciones de orden superior, la notación
do, las lentes, las bibliotecas declarativas y las abstracciones avanzadas de tipos siguen siendo ventajas difíciles de reemplazar por otros lenguajes - Haskell sigue teniendo mucho valor como lenguaje para aprender y convertirse en mejor programador, pero adoptarlo como lenguaje predeterminado de una organización implica asumir el costo de la calidad de sus herramientas y la estabilidad del lenguaje
La etapa de usar Haskell como lenguaje predeterminado y su posición actual
- Durante unos 10 años, Haskell fue el lenguaje predeterminado para proyectos nuevos
- Tras conocer Haskell alrededor de 2009, y hasta cambiarse alrededor de 2019 a un trabajo centrado principalmente en Ruby y C++, se consideraba a sí mismo programador de Haskell
- Usó Haskell para varios fines, como herramientas de línea de comandos, servicios web, aplicaciones gráficas y pequeños scripts
- Hoy no rechaza Haskell por completo, pero ya no es su lenguaje predeterminado ni siquiera para proyectos personales, y tampoco busca deliberadamente “trabajos en Haskell” como antes
- Este juicio no es una conclusión de que Haskell sea malo, sino más bien una explicación personal de por qué se fue alejando de Haskell
Por qué le atraía Haskell
-
La capacidad de tratar el código algebraicamente
- El mayor atractivo de Haskell era poder razonar sobre el código de forma simbólica y algebraica
- Por ejemplo, si existe una función
double n = n + n, en Haskell transformardouble xenx + xsiempre es correcto - En muchos lenguajes imperativos,
double(i++)y(i++) + (i++)pueden comportarse de manera distinta- El primero incrementa
iuna vez - El segundo incrementa
idos veces
- El primero incrementa
- Gracias a esta propiedad, se pueden acumular pequeñas transformaciones mecánicas para realizar grandes refactorizaciones con más seguridad
- Una gran refactorización en una base de código Ruby puede ser muy inquietante, pero en una base de código Haskell puede ser mucho más manejable
-
Un sistema de tipos estricto, pero no excesivamente ruidoso
- El sistema de tipos de Haskell es estricto y, gracias a la inferencia de tipos, no requiere tantas anotaciones de tipo como Java
- Las typeclasses reducen la fricción incluso en situaciones en las que hay que prestar atención a los tipos
- Un buen código Haskell tiene la propiedad de ser “frágil” (brittle) en el buen sentido
- No permite que una abstracción incorrecta se doble en silencio y acepte un programa equivocado
- Si hay un problema, falla la compilación
- El tipo
NonEmptyrepresenta una lista con al menos un elemento- Una operación que preserva la longitud, como
map, devuelveNonEmpty - Una operación que aumenta la longitud, como
append, también devuelveNonEmpty - Una operación cuyo resultado podría estar vacío, como
filter, devuelve una lista común
- Una operación que preserva la longitud, como
- A una función que requiere que la lista de rutas de archivos sea
NonEmptyno se le pueden pasar directamente los argumentos de línea de comandos; primero hay que comprobar que no estén vacíos - Si más adelante se agrega un paso de filtrado por extensión, el resultado podría quedar vacío, así que hay que volver a comprobarlo, y el compilador bloquea las rutas por las que se pasaría accidentalmente una lista vacía
-
Mantener invariantes mediante tipos
- Después de usar Haskell durante mucho tiempo, uno se acostumbra a expresar las invariantes del programa mediante tipos y hacer que el compilador las verifique
- Esto no implica únicamente cálculos complejos a nivel de tipos
- Se puede distinguir si una cadena fue escapada para SQL usando tipos envoltorio como
RawStringySanitizedString- Aunque una API web entregue un
RawString, se puede hacer que la capa de base de datos acepte soloSanitizedString
- Aunque una API web entregue un
- Una representación superficial con muchos campos
Maybepuede transformarse en una representación interna que garantice la existencia de la información necesaria - También es posible hacer algo genérico solo hasta el punto necesario para permitir pruebas independientes
-
Un lenguaje donde los DSL y las funciones de orden superior se sienten naturales
- Haskell facilita crear lenguajes específicos de dominio concisos incluso sin macros
- config-ini es un ejemplo de biblioteca para manejar archivos de configuración INI
- Usa una interfaz declarativa en lugar de una interfaz de parsing imperativa
- Mapea partes específicas de un tipo de configuración a partes específicas de la estructura de un archivo INI
- Con la misma interfaz realiza lectura, escritura y actualizaciones con diff mínimo
- Los DSL de Ruby pueden depender de metaprogramación dinámica y monkeypatching global, mientras que los DSL de Haskell, gracias a funciones como su sintaxis flexible y la sobrecarga de mónadas, son más fáciles de limitar a un archivo o región de código específicos
- Haskell también vuelve natural trabajar con funciones de orden superior
(+)puede usarse directamente como una función que recibe dos argumentos numéricos y los suma- Una multiplicación elemento por elemento de dos listas puede expresarse de forma muy breve como
pairwiseProduct = zipWith (*)
- Encaja la analogía de que Haskell permite manipular funciones del mismo modo en que Perl permite manipular cadenas
Tres factores que lo alejaron de Haskell
-
Una atmósfera que prefiere estilos difíciles
- El primer factor es la preferencia estilística por novedades técnicas
- La comunidad de Haskell experimenta y construye nuevas abstracciones continuamente
- Enfoques tomados del álgebra abstracta o de la teoría de categorías
- Enfoques que suben más cálculos al nivel de tipos para restringir estados inválidos
- Las API web a nivel de tipos de Servant o un caso que expresa un sistema de menús con comonads son experimentos para representar dominios de problemas de una forma nueva
- Pero en el código cotidiano, estos enfoques experimentales pueden generar más problemas que las ventajas que muestran al principio
- En un ejemplo de un proyecto de compilador, los nodos del AST tenían el tipo de expresión como parámetro de tipo
- Si se construía un árbol sintáctico con errores de tipo, el propio compilador no compilaba
- Al principio detectó algunos bugs
- Cuando muchas pasadas de optimización no encajaban con la disciplina de tipos, el código se complicó con casts e intentos de apaciguar a GHC
- Al final, se eliminó el parámetro de tipo del AST
- En alrededor de tres cuartas partes de los proyectos donde intentó el enfoque de “fancy types”, evaluó que no aportó suficiente valor
- La corriente de Simple Haskell se basa en la idea de que el lenguaje ofrece más beneficios cuando se evitan funciones experimentales y se permanece con extensiones mínimas
- Las funciones de tipos más complejas están muy extendidas en la comunidad y las bibliotecas
- Las abstracciones complejas de tipos pueden dificultar los cambios y el mantenimiento, y la experiencia de intentar reemplazar una abstracción simplificada por una abstracción moderna a nivel de tipos terminó en más tamaño de código, peor rendimiento y menor legibilidad
- Aun así, estructuras de datos indexadas por etapas del compilador como Trees that Grow siguen siendo un caso útil de “fancy type-level feature”
-
Herramientas que no brillaban en el trabajo cotidiano
- El segundo factor es un sistema de herramientas incómodo
- Haskell tiene varios sistemas de build y herramientas de desarrollo casi estándar
- En general, estas herramientas hacen lo necesario, pero no llegaban a ser “queribles”
- Había herramientas como
ormolu, que hace su trabajo con mínima fricción, pero algunas requerían lidiar constantemente con bugs y workarounds - Las herramientas de Haskell mejoraron durante 10 años, pero en 2019 no había ninguna cercana a la usabilidad y estabilidad de Rust Cargo
- La tendencia Not-Invented-Here de querer crear continuamente algo nuevo y mejor también era un problema
- Existe una corriente que incluso vuelve a abordar el formato textual de paquetes con hpack
- Es frustrante que estos intentos permanezcan durante años en un estado de 95% completado
- No se trata de decir que las herramientas de Haskell sean pésimas, pero “más o menos bien” no alcanza para satisfacer estándares más altos
-
Cambios incompatibles continuos
- El tercer factor son los cambios continuos que atraviesan el lenguaje Haskell y GHC
- GHC se trata en la práctica como la única implementación pública razonable de Haskell
- Haskell intenta corregir gradualmente errores e inconsistencias del pasado incluso en la biblioteca estándar y las partes básicas del lenguaje
- Estos cambios pueden llevar al lenguaje hacia elementos básicos más limpios y coherentes, pero para los usuarios crean pequeñas fricciones continuas
- Los cambios Foldable-Traversable en Prelude de 2015 son un ejemplo de este tipo de cambio
- Un proyecto antiguo en Haskell puede no compilar con el GHC más reciente aunque no tenga dependencias externas
- Agregado de superclases
- Eliminación de imports
- Agregado de firmas de tipo debido a polimorfismo que se volvió ambiguo
- Las correcciones suelen ser pequeñas, pero se acumulan, y las partes que cambiaron poco a poco respecto de antes exigen la atención del usuario
- El enfoque de actualización por epochs de Rust contrasta con esto al explicitar un contrato de compatibilidad hacia atrás para funciones estables y ocultar los breaking changes intencionales detrás de epochs
Ventajas de Haskell que todavía extraña
- La escritura algebraica de código y la refactorización en Haskell siguen siendo fortalezas
- Rust también comparte muchas ventajas con Haskell, pero no resulta tan fácil como en Haskell tener la certeza de que pequeñas refactorizaciones mecánicas preservan el significado del programa
- Su sistema de tipos también ofrece capacidades que otros lenguajes no suelen tener
- Los lenguajes mainstream de 2023 avanzaron mucho en capacidades de tipado estático respecto de 2009
- Aun así, funciones como abstracciones sobre higher-kinded types siguen diferenciando a Haskell
- El ecosistema de bibliotecas de Haskell es mixto
- Bibliotecas open source mantenidas a medias
- Pequeños reinos donde autores individuales intentan reconstruir el mundo a su manera
- Tipos parecidos a cadenas que son necesarios, pero distintos entre sí
- Aun así, Haskell tiene bibliotecas y abstracciones excelentes que en otros lenguajes solo son posibles a medias
- Las lentes son una abstracción poderosa cuyas ventajas no se perciben bien hasta haberlas usado lo suficiente
- Brick ofrece una muy buena biblioteca de UI de terminal con bloques declarativos
- diagrams y music-suite también son buenos ejemplos de API declarativas
- La notación
dotambién es una función que extraña mucho- Lo esencial no es
doen sí, sino una notación que permite agregar bindings sin que el nivel de indentación siga creciendo - Los
flat_mapanidados de Ruby profundizan la indentación cada vez que aumenta un eje - La notación
dode Haskell expresa la misma composición de forma plana
- Lo esencial no es
- La notación
dopermite embeber con la misma sintaxis ligera concurrencia estructurada al estiloasync/await, transacciones concurrentes y programas probabilísticos
¿Hay que aprender Haskell? ¿Hay que usarlo?
- La respuesta a “¿hay que aprender este lenguaje o usarlo?” depende del objetivo
- Si el objetivo es convertirse en mejor programador, Haskell vale mucho la pena aprenderlo
- Haskell es un lenguaje interesante y poderoso
- Se evalúa que la experiencia de aprender Haskell mejoró la capacidad de programación
- Si el objetivo es usarlo en la práctica, hay que ser más cuidadoso
- Las ventajas de Haskell existen de verdad
- Hay que compararlas con cuánta fricción pueden soportar una persona u organización por los puntos mencionados
- También existen personas y organizaciones que obtienen suficiente beneficio de Haskell como para mantener las pequeñas molestias al nivel de pequeñas heridas
- Si se exige un sí o no claro, la respuesta está más cerca de “no uses Haskell”
- Si creara una organización de ingeniería, no elegiría Haskell como lenguaje predeterminado
- Porque conoce tanto el valor de las buenas herramientas como el costo de las malas
- Prefiere lenguajes que puedan ofrecer de inmediato un lenguaje estable, un ecosistema de código claro y excelentes herramientas
- Aun así, Haskell ofrece herramientas sobresalientes para la seguridad y corrección de los programas
- En ciertos contextos, puede valer la pena aceptar malas herramientas y linting para bloquear type families innecesarias
- Haskell ya no es el lenguaje que encaja personalmente con él, pero la conclusión es que, si Haskell no existiera en el mundo, el mundo sería peor
1 comentarios
Opiniones de Hacker News
Usé Haskell durante unos 10 años y hace poco dejé de usarlo como medio de vida, y definitivamente coincido con la primera observación del autor: la comunidad de Haskell pone muchísimo énfasis en el aprendizaje.
Es bueno trabajar con gente curiosa y tener siempre algo que enseñar y aprender, pero la comunidad no es muy buena para descartar ideas probadas, así que en bases de código profesionales en Haskell, si no se gestionan con rigor, tienden a acumularse muchas “cosas que se pueden hacer, pero que sería mejor no usar”.
Sin embargo, discrepo en cuanto a la toolchain. Es cierto que las herramientas de Haskell no son muy buenas, pero después de usar Python, JS, Java, Rust y Elm, la mayoría de las herramientas también me parecieron malas, y de hecho extrañé la toolchain de Haskell. cargo es excelente, pero se acerca más a una excepción, y me pregunto si Rust podrá evitar esta situación cuando envejezca.
Tengo un proyecto en el que trabajo de vez en cuando, y estoy 100% seguro de que la próxima vez que lo abra no va a funcionar otra vez. La gestión de dependencias y entornos virtuales de Python es igual de mala, pero Poetry me pareció bastante decente; en Ruby, uses chruby/rbenv/rvm, todo funciona de forma parecida y bien, y Bundler también anda bien. Java tiene sus defectos, pero la experiencia con IDE es de primer nivel, y aunque el refactoring no sea tan seguro como en Haskell, se puede hacer con mucha facilidad.
Bundler en general no está mal, y aunque haya decisiones de diseño con las que no esté de acuerdo, si sigues el camino correcto muchas piezas encajan bien. Las herramientas de lenguaje también pueden hacerse de una forma suficientemente menos rota, y aunque no he usado cargo mucho directamente, si de verdad es el mejor de su categoría, no me sorprendería que mantuviera ese estado.
En términos de herramientas, Haskell tiene una cosa que, hasta donde sé, otros lenguajes difícilmente pueden igualar: Hoogle.
Hoogle es sorprendentemente bueno. Le dices en Haskell lo que quieres, y te dice qué se puede hacer en Haskell. Hubo intentos similares en Rust, y también intenté hacer Noogle para Nim, pero en lenguajes donde “pasar argumentos a una función” y “llamar a una función” están claramente separados, no funciona de la misma manera.
Cada vez que veo código Rust con
ResultyBoxentrelazados, quisiera que existiera un Roogle tan útil como Hoogle. El resto de las herramientas de Haskell no tiene ventajas. En buena medida parece deberse al instinto de la comunidad de querer crear algo “innovador” en vez de mejorar lo existente.No es perfecto, pero comparado con hace 5 años está mucho mejor.
Ojalá más proyectos tomaran esta dirección.
Creo que el mayor problema es que GHC está atado a una versión específica de
base, que es la biblioteca estándar.Si
basecambia y una nueva versión de GHC solo soporta esa versión, para usar un GHC más nuevo en la práctica tienes que cambiar todas tus dependencias. Todavía no entiendo por qué el código compilado con GHC 9.6 tiene que usarbase4.18.0.0. ¿Por qué debería importarle al binario de GHC qué versión del móduloData.Listusa el código que va a compilar?Entiendo que los elementos específicos de GHC que expone
baseestén ligados a una versión concreta de GHC, pero no veo por qué el resto también tendría que estarlo. Dicho eso, entiendo que está en marcha un trabajo para dividirbaseen varios paquetes con el fin de arreglar esto: https://gitlab.haskell.org/ghc/ghc-wiki-mirror/-/blob/master...basepor considerarlo inválido.basese pasan entre sí instancias deData.List, eso causaría problemas.Por ejemplo, si tienes una lista de
Data.List, ¿podrías concatenarle los resultados de dos funciones de bibliotecas distintas?basereinstalable es un objetivo que todos quieren. La situación actual se debe en gran parte a razones históricas y a que elementos básicos ligados al interior del compilador quedaron acoplados con elementos no básicos; con suficiente esfuerzo, puede mejorar.Si hubiera tres grandes razones por las que el interés en Haskell se fue enfriando, fueron la preferencia estilística por lo novedoso, que celebra el código críptico y dificulta el mantenimiento; herramientas incómodas que vuelven más torpes las tareas cotidianas; y cambios continuos que, aunque ocasionales, había que atender constantemente y que rompían cosas con regularidad.
Es una crítica válida. Aprendí bastante Haskell entre 2010 y 2012, y aunque el lenguaje en sí era excelente, la documentación y las herramientas eran difíciles de manejar. La comunidad pasó de Cabal y el infame Cabal hell a Stack, y luego volvió a Cabal; en general, las cosas mejoraron.
Mientras tanto, otros lenguajes de programación también absorbieron elementos de la programación funcional. Por ejemplo, Java agregó Streams, funciones, lambdas, tipos de datos algebraicos, records y pattern matching; aunque la sintaxis no sea tan elegante como la de Haskell, los conceptos básicos de la programación funcional están ahí.
Así crearon para sí mismos un pozo de sufrimiento interminable, apenas soportable porque los rayos de luz no llegan lo bastante bajo como para ver los colores que la luz produce en los árboles fuera de la ventana. Quienes vienen de afuera se desgarran entre un dialecto adaptado a la oscuridad que ellos mismos crearon y un dialecto que prospera en la luz, pero que allí los asfixia, y pierden rápidamente la cordura.
Si la comunidad volvió a Cabal, me pregunto qué me perdí.
Por estas razones me pasé a F# y prácticamente no miré atrás. A veces extraño los tipos de kind superior, pero F# también tiene genéricos y, francamente, en cierto modo te obliga a escribir código más simple que en Haskell, y en general el resultado es mejor.
Eso sí, F# no da esa sensación de “experto” como Haskell. Es como Haskell, pero completamente pragmático. Hago muchas cosas con F# y lo disfruto, aunque a veces miro hacia atrás con cierta nostalgia y me pregunto por qué lo de Haskell no funcionó.
A menudo me preguntaba si me estaba perdiendo algo por no usar Haskell, pero para mis objetivos probablemente no sea así.
Aprender Haskell se siente como intentar descifrar un idioma extraterrestre. Si estás acostumbrado a bucles if-else normales y a asignaciones simples de variables, prepárate para que se te enrede la cabeza.
Una monad no es un monstruo cósmico, sino una abstracción extraña que puede hacer que te rasques la cabeza y cuestiones tus decisiones de vida. Las bibliotecas también se sienten como hurgar en una tienda de segunda mano medio vacía para encontrar lo que necesitas, y la documentación se lee a medias como jeroglíficos.
En la práctica, el rendimiento también puede hacerte pasar un buen rato preguntándote por qué la evaluación perezosa consume más memoria de la esperada y vuelve todo más lento. Los empleos son difíciles de encontrar salvo en empresas muy de nicho que apuestan todo por Haskell, y depurar se siente como buscar una aguja en un montón de jeroglíficos funcionales.
He usado Scala y creo que puede dar lo mejor de ambos mundos. Puedes razonar algebraicamente y, si compila después de refactorizar, por lo general funciona.
La inferencia de tipos y las monads también funcionan bien, y además obtienes las ventajas del ecosistema Java. Si algo se vuelve demasiado críptico, también puedes romper las reglas y programar de una forma más cercana a Java. Me da curiosidad saber qué piensan otros.
Eso sí, sbt me vuelve loco y probablemente sea la parte que menos me gusta de la experiencia de desarrollo.
Los responsables y mantenedores de Haskell realmente parecen tener la intención de que este lenguaje sea favorable para la investigación, la experimentación y la exploración académica.
Eso en sí está bien, pero en algún punto inevitablemente entra en conflicto con los intereses de los programadores que quieren usar Haskell como una herramienta práctica y estable. Lo que parece necesario es la capacidad de marcar las cosas experimentales y de vanguardia en una pista separada. Ya sea con un pragma o con una declaración a nivel de paquete, debería poder saberse con solo mirar un paquete si conviene incorporarlo.
Así, los desarrolladores orientados a la práctica podrían adoptar políticas como “solo usamos stable Haskell”. Hay que reconocer la dinámica de “Avoid success at all costs”: si la adopción de Haskell crece demasiado, el lenguaje queda atrapado por la inercia y no puede impulsar conceptos de vanguardia; pero hace falta una pista oficial que permita a los desarrolladores cotidianos permanecer en un subconjunto más limitado y estable de Haskell.
Java jamás obtendrá algo así. https://www.stackage.org/
Al final, la simplicidad depende de quién la mire. Las características de tipos sofisticadas, si ya interiorizaste cómo funcionan, también tienen usos que vuelven los tipos más expresivos y hacen el código más seguro e incluso más simple.
Es un artículo bastante bueno. Aunque su punto débil es que nunca he sentido presión para usar alguna función de tipos nueva y vistosa si no quiero hacerlo.
Si no te gusta la API HTTP a nivel de tipos de Servant, puedes bajar un nivel y usar warp. Las bibliotecas entienden que los tipos excesivamente complejos pueden convertirse en una trampa, así que a menudo están organizadas por capas de esta manera. Hace falta intuición para saber hasta dónde llegar, y ese proceso incluye cometer errores.
Leí la documentación de Servant, pero, como era de esperarse, asumía más comprensión de Haskell de la que tengo.
Este artículo realmente me llegó. Llevo más o menos 1,5 años usando Haskell en una aplicación bastante industrial, y antes de eso también tenía bastante experiencia con cosas como https://github.com/mattgreen/hython.
Hacía tiempo que no tenía que aprender tantas cosas en 20 años de carrera, y es el resultado de combinar el dominio con el lenguaje. Es emocionante pero también agotador, y quizá sea una carga bastante grande para llevar junto con una vida familiar ocupada.
Aun así, el lenguaje es excelente. Mi gerente suele recomendar usarlo de manera top-down, en el sentido de pensar profundamente cómo debería leerse, porque la flexibilidad de Haskell comunica bien la intención de desarrollo, y empezar desde ahí. Es un cambio de mentalidad grande frente a la mayoría de los lenguajes, donde uno escribe
functionuna y otra vez y el cerebro entra en modo de ahorro de energía; realmente se siente diseñado para ser amigable al escribir.Pero incluso manejando efectos controlados con mónadas, la sintaxis sigue siendo difícil de leer por encima. No sé si es porque una línea puede volverse demasiado densa. Extraño muchísimo los retornos tempranos, y también el excelente LSP de Rust. Haskell perdió hace poco incluso la función de completar pattern matching por cambios internos en 9.x, y apilar mónadas sigue sin gustarme y me equivoco seguido. Los tiempos de compilación pueden ser duros, especialmente cuando te encuentras con la biblioteca lens.
Tampoco me gusta la evaluación perezosa por defecto. La comunidad ya reconoce en cierta medida que, por ese valor por defecto, los programas Haskell son mucho más vulnerables a fugas de espacio, y que en muchos programas pueden pasar bastante tiempo sin detectarse. Como programador de sistemas, me dan ganas de gritar.
Aun así, la comunidad es uno de los grupos de programadores más fuertes que he visto. No quiero ponerme a repetir el meme de que son inteligentes; lo importante es que realmente piensan en profundidad y empujan las cosas hacia adelante. En nuestro dominio, el análisis de programas, Haskell encaja como un guante, pero en otros ámbitos todavía soy un poco cauteloso. Sigo teniendo sed de un lenguaje de programación que sea esencialmente OCaml, pero con mejor sintaxis.
Ni siquiera hace falta exponer ExceptT en la interfaz; puedes usarlo internamente solo para retornos tempranos y hacer que la función devuelva Either.
Es una pena que al final no haya conseguido mucha traction.
Integra mejor la programación orientada a objetos y la funcional que OCaml, y se parece lo suficiente como para que el código idiomático en OCaml sea fácil de portar con una transformación simple.