1 puntos por GN⁺ 2 시간 전 | 1 comentarios | Compartir por WhatsApp
  • Mercury opera servicios bancarios para más de 300 mil empresas con una base de código de aproximadamente 2 millones de líneas de Haskell sin contar comentarios, y en 2025 procesó un volumen de transacciones de 248 mil millones de dólares y una facturación anualizada de 650 millones de dólares
  • El valor del uso de Haskell en Mercury no está tanto en la pureza en sí, sino en plasmar el conocimiento operativo en las API y los tipos, dejar las operaciones riesgosas detrás de límites estrechos y hacer que el camino seguro sea el camino fácil
  • La confiabilidad no se aborda como evitar por completo las fallas, sino como la capacidad del sistema para absorber variaciones, y el sistema de tipos elimina clases de errores y deja el conocimiento institucional como documentación que el compilador puede hacer cumplir
  • Mercury usa Temporal como framework de durable execution para reintentos, timeouts, cancelaciones y recuperación tras crashes en flujos de trabajo financieros, y liberó como open source el SDK de Haskell hs-temporal-sdk
  • El valor de Haskell en producción no está en meter todo en los tipos, sino en proteger con tipos los invariantes que pueden derivar en pérdida de datos, errores financieros o problemas regulatorios, mientras la complejidad se encapsula y se opera junto con pruebas, documentación y code review

La escala operativa de Haskell en Mercury y su visión de la confiabilidad

  • Mercury opera una base de código en Haskell de aproximadamente 2 millones de líneas, sin contar comentarios
  • Mercury es una fintech que ofrece servicios bancarios a más de 300 mil empresas, y en 2025 procesó 248 mil millones de dólares en volumen de transacciones y 650 millones de dólares de facturación anualizada
  • Tiene alrededor de 1,500 empleados, y su organización de ingeniería contrata principalmente desarrolladores generalistas; la mayoría nunca había usado Haskell antes de entrar
  • Este sistema ha funcionado durante años atravesando crecimiento acelerado, la crisis de SVB en la que entraron 2 mil millones de dólares en nuevos depósitos en 5 días, revisiones regulatorias y situaciones comunes y no comunes de un sistema financiero a gran escala

La confiabilidad no es prevenir fallas, sino la capacidad de absorber variaciones

  • El enfoque tradicional de confiabilidad se concentra en enumerar fallas, agregar verificaciones y pruebas, y encontrar bugs, pero eso por sí solo no basta
  • Mercury trata la confiabilidad como la capacidad del sistema para absorber variaciones
    • El sistema debe poder degradarse con elegancia
    • Los operadores deben poder entender y ajustar el sistema
    • La arquitectura debe hacer que lo correcto sea fácil y que lo incorrecto sea difícil
  • En organizaciones que crecen rápido, las preguntas operativas reales pasan por si un ingeniero recién incorporado puede leer y entender un módulo, si cuando la base de datos se pone lenta el servicio colapsa con ella, y si el compilador detecta el mal uso de una interfaz
  • El sistema de tipos se parece más a un mecanismo de apoyo operativo que a una simple prueba de corrección
    • Excluye ciertas clases de errores
    • Deja el conocimiento institucional en una forma que el compilador puede leer incluso después de que quien escribió el código ya no esté
    • Funciona como documentación que se hace cumplir con más consistencia que una wiki
  • La ingeniería de estabilidad en Mercury no es una policía de calidad que frena el desarrollo del producto, sino una forma colaborativa de tratar desde el inicio del diseño el impacto de una función cuando se rompe
    • Radio de impacto ante una falla
    • Qué operaciones necesitan idempotencia y de qué forma
    • Cómo sería un rollback
    • Cómo se manejan los trabajos en curso
    • Evaluar de antemano qué sistemas absorben fallas y cuáles las amplifican

La pureza no es una propiedad del lenguaje, sino un límite de interfaz

  • La pureza en Haskell no significa que internamente no haya ningún efecto secundario, sino más bien que la interfaz crea un límite que evita que se filtren los efectos secundarios
  • Detrás de funciones puras de librerías como bytestring, text y vector hay implementaciones internas con asignación mutable, escritura en buffers y unsafe coercion
  • El mónada ST usa mutación in-place observable dentro del cálculo y efectos secundarios, pero el tipo rank-2 de runST impide que referencias mutables creadas internamente se escapen
    runST :: (forall s. ST s a) -> a  
    
  • Internamente puede haber comportamiento imperativo, pero hacia afuera solo sale el resultado y el estado mutable no se filtra fuera del límite
  • Este principio se aplica a todo el sistema operativo
    • La capa de base de datos puede usar internamente pooling de conexiones, reintentos y estado mutable
    • El caché puede usar mapas mutables concurrentes
    • El cliente HTTP puede tener circuit breakers, pools de conexiones y bastante bookkeeping
    • La clave es envolver las operaciones riesgosas en interfaces estrechas para dificultar el mal uso
  • En sistemas reales, el objetivo no es evitar por completo el cambio, sino dejar claro dónde está y limitar qué partes del código necesitan saberlo

Hacer que lo correcto sea lo fácil

  • En bases de código grandes, suelen aparecer patrones cuya corrección depende de cierto orden o de pasos adicionales invisibles
    • Hay que hacer flush del audit log después de una transacción
    • Hay que revisar un feature flag antes de llamar a un endpoint
    • Hay que hacer enqueue de una notificación dentro de la transacción de base de datos
  • Si ese conocimiento operativo vive solo en una wiki, documentos de onboarding, revisiones de diseño antiguas, hilos de Slack o la memoria de algunos ingenieros senior, desaparece rápido
  • Haskell permite codificar esos procedimientos en los tipos para que no se puedan olvidar
  • La mala forma es pedir que se use la función correcta, pero dejar abierta una ruta de escape
    -- Please use this one, not the other one  
    writeWithEvents :: Transaction -> [Event] -> IO ()  
    
    -- Don't use this directly (but we can't stop you)  
    writeTransaction :: Transaction -> IO ()  
    publishEvents :: [Event] -> IO ()  
    
    • Una mejor forma es reestructurar los tipos para que la única ruta de ejecución de la operación incluya la publicación de eventos
    data Transact a -- opaque; cannot be run directly  
    record :: Transaction -> Transact ()  
    emit :: Event -> Transact ()  
    
    -- The *only* way to execute a Transact: commit and publish atomically  
    commit :: Transact a -> IO a  
    
  • Aquí el sistema de tipos, más que probar un teorema profundo sobre eventos, hace que el procedimiento operativo correcto sea el camino más fácil
  • Si un ingeniero nuevo pregunta cómo escribir una transacción, la firma de tipos y la API pública dan la respuesta, y el conocimiento permanece aunque un ingeniero senior se vaya

Ejecución durable y Temporal

  • Los flujos de trabajo de los sistemas financieros no se quedan dentro de una sola transacción
    • envío de pagos
    • espera de aprobación del socio
    • actualización del libro contable
    • notificación al cliente
    • manejo de cancelaciones y timeouts
    • casos en los que el socio tuvo éxito pero el worker murió antes de registrarlo
    • casos en los que no hay respuesta por problemas de red
  • Estos flujos necesitan estado, reintentos, timeouts, idempotencia y una ejecución que perdure más allá de crashes de procesos y despliegues
  • Antes, Mercury coordinaba estos procesos con máquinas de estado basadas en base de datos, tareas de cron, workers en segundo plano y manejo de reintentos y timeouts disperso por todo el código
    • Funcionaba, pero era frágil, difícil de entender y una causa desproporcionada de incidentes operativos
  • Temporal es el framework de durable execution de Mercury, donde los workflows se escriben como código secuencial normal y la plataforma registra cada paso en el historial de eventos
  • Si un worker se cae a la mitad de un workflow, otro worker hace replay del prefijo determinista para reconstruir el estado y continuar desde el punto de interrupción
  • La plataforma provee reintentos, timeouts, cancelación y manejo de errores en lugar de que cada equipo tenga que reimplementarlos por su cuenta
  • Un workflow de Temporal tiene una naturaleza parecida a una función pura sobre el historial de eventos
    • un workflow reproducido con replay debe generar la misma secuencia de comandos que el original
    • este requisito de determinismo se parece a la restricción de mismo input, mismo output del código puro
    • los efectos secundarios se aíslan en activities, que corresponden al IO del workflow
  • Mercury creó y publicó como open source el SDK de Haskell hs-temporal-sdk, que envuelve el Core SDK oficial de Temporal con Rust FFI
  • El patrón de adopción de Temporal también se trató en la charla de Temporal Replay conference, y Mercury logró mejoras operativas al reemplazar cadenas frágiles de cron y máquinas de estado por durable workflows

El dominio se diseña con lenguaje de negocio, no con la capa de transporte

  • Un error común en sistemas que crecieron es que los conceptos del sistema que llama se filtren al modelo de dominio
  • Si código escrito para handlers de requests HTTP luego se reutiliza en tareas de cron, workers en segundo plano basados en colas y workflows de Temporal, excepciones HTTP como StatusCodeException 409 "Conflict" pueden propagarse a contextos no HTTP
  • En una tarea de cron no hay ningún llamador esperando una respuesta 409, y el código de estado arrastra significado de negocio a una capa equivocada
  • La solución es modelar los errores de dominio como tipos de dominio
    • saldo insuficiente debe ser InsufficientFunds
    • una solicitud duplicada debe ser DuplicateRequest
    • un timeout del socio debe ser PartnerTimeout
  • En cada borde se coloca una capa delgada de transformación
    data PaymentError  
      = InsufficientFunds  
      | DuplicateRequest RequestId  
      | PartnerTimeout Partner  
    
    toHttpError :: PaymentError -> HttpResponse  
    toHttpError InsufficientFunds       = err402 "Insufficient funds"  
    toHttpError (DuplicateRequest _)    = err409 "Duplicate request"  
    toHttpError (PartnerTimeout _)      = err502 "Partner unavailable"  
    
    toWorkerStrategy :: PaymentError -> WorkerAction  
    toWorkerStrategy InsufficientFunds    = Fail "Insufficient funds"  
    toWorkerStrategy (DuplicateRequest _) = Skip  
    toWorkerStrategy (PartnerTimeout _)   = RetryWithBackoff  
    
  • Los intereses de la capa de transporte deben quedar en los bordes, y el modelo de dominio no debería andar cargando códigos de estado HTTP sin importar si lo llaman desde un handler web, un CLI, una tarea de cron, un worker en segundo plano o un motor de workflows

El costo y el punto justo de la codificación en tipos

  • Poner invariantes en los tipos es poderoso, pero tiene costos de carga cognitiva, rigidez y dificultad cuando cambian los requisitos
  • Si una violación puede llevar a pérdida de datos, errores financieros, problemas regulatorios o incidentes por callers en espera, el costo de codificarlo en tipos se justifica
  • Si solo se hace porque así es la forma actual o porque se quiere probar técnicas a nivel de tipos, hay muchas probabilidades de que vuelva más difícil cambiar el codebase
  • El extremo de codificar demasiado

    • los estados ilegales se vuelven imposibles de representar y el dominio queda modelado fielmente en los tipos
    • un cambio en una regla de negocio termina convirtiéndose en cambios de tipos que atraviesan 50 módulos y alargan el refactor
    • a los nuevos ingenieros les cuesta más entender las firmas de tipos
  • El extremo de no codificar nada

    • los tipos terminan pareciéndose a String, IO () o, en el peor caso, a Dynamic
    • el código es fácil de cambiar, pero no hay contratos y el significado depende de la memoria de quienes lo escribieron
    • cuando esas personas se van, resulta difícil saber por qué el sistema no funciona
  • Criterios útiles

    • las invariantes que previenen corrupción silenciosa conviene ponerlas en los tipos
      • transacciones confirmadas sin evento
      • pagos procesados sin registro de auditoría
      • transiciones de estado aparentemente posibles pero semánticamente imposibles
    • las invariantes que fallan de forma ruidosa pueden resolverse con checks en runtime y buenos mensajes de error
      • respuesta 500
      • falla de assertion
      • incompatibilidad de tipos en el borde JSON
    • hay que contener el impulso de modelar todo el dominio con tipos
      • los dominios tienen excepciones, reglas heredadas por compatibilidad, reglas que entran en conflicto y comportamientos especiales para ciertos clientes
    • los tipos son una herramienta no solo para el compilador, sino para el equipo
      • deben formar una capa de defensa junto con pruebas, documentación, code review, ejemplos y playbooks
    • Dentro de Mercury también hay librerías que usan mecanismos complejos a nivel de tipos, como GADT, type family y phantom types para rastrear transiciones de estado
    • En mecanismos donde un error puede mover dinero de forma incorrecta o romper invariantes regulatorias, esa complejidad es necesaria
    • La clave es encapsular la complejidad
    • El módulo que implementa una máquina de estados a nivel de tipos debe tener pocos autores que la entiendan a fondo y suficientes pruebas
    • La API para quienes la usan debería verse como unas pocas funciones con tipos normales
    • Un product engineer debería poder llamarla de forma segura sin conocer los mecanismos internos de prueba a nivel de tipos
    • Si en un code review un PR que toca otros módulos está lleno de anotaciones de tipo copiadas para calmar al compilador, es señal de que la abstracción se está fugando más allá de sus límites

Diseño para la introspección posible

  • Si la confiabilidad es capacidad de adaptación, la introspección es una de las formas de obtener esa capacidad
  • Los operadores no pueden operar lo que no pueden ver, y a los equipos les cuesta adaptarse a sistemas cuyo interior es opaco
  • Haskell no tiene monkey patching, así que es difícil cambiar en runtime el cliente HTTP interno de una librería o reemplazar llamadas a base de datos por funciones que emitan spans de OpenTelemetry
  • Rust tiene la misma limitación, pero mientras el ecosistema de Rust convergió en el patrón de middleware de tower, el ecosistema de Haskell está dividido entre varios enfoques
  • Si una librería solo expone un conjunto de funciones concretas de nivel superior, para instrumentarla hay que envolverla en un módulo nuevo y esperar que la gente importe ese módulo en lugar del original
  • Registros de funciones

    • La solución más usada es exponer registros de funciones en vez de funciones concretas
      -- A concrete module gives you no leverage:  
      sendRequest :: Request -> IO Response  
      -- A record of functions gives you all of it:  
      data HttpClient = HttpClient  
      { sendRequest :: Request -> IO Response  
      , getManager  :: IO Manager  
      }  
      
    • Con este enfoque, se puede envolver sendRequest con medición de tiempo y devolver un HttpClient nuevo
    • Permite agregar en runtime intereses transversales como fault injection para pruebas, reemplazo por mocks, reintentos, tracing, rewrite de requests y comportamiento por tenant
    • Un patrón como el type Middleware = Application -> Application de WAI, que vuelve componibles las transformaciones de comportamiento, es muy útil operativamente
  • Interceptores compuestos con Monoid

    • Los tipos de middleware e interceptor normalmente pueden tener instancias de Semigroup y Monoid
    • El Middleware de WAI es un endomorphism, y los endomorphisms forman un monoid bajo composición e id
    • Los registros de hooks de interceptores pueden componerse campo por campo, así que intereses como tracing, timeout o rewrite de task queue pueden combinarse con mconcat sin plomería adicional
      appTemporalInterceptors =  
      mconcat  
        [ retargetingInterceptor  
        , otelInterceptor  
        , sentryInterceptor  
        , sqlApplicationNameInterceptor  
        , loggingContextInterceptor  
        , statementTimeoutInterceptor  
        , teamNameInterceptor  
        , clientExceptionInterceptor  
        , workflowTypeNameInterceptor  
        ]  
      
    • Cada interceptor vive en un módulo independiente y solo maneja un interés; sobrescribe en mempty solo los campos necesarios, y el orden se declara en la lista
  • Sistemas de efectos

    • Los effect systems como effectful, polysemy, fused-effects y cleff también ofrecen otra ruta
    • Se definen las operaciones disponibles como tipos de effect, y los intérpretes para producción, testing o tracing pueden cambiarse en el punto de invocación
    • Se puede interceptar un effect para registrar métricas o inyectar latencia antes de reenviarlo al handler real
    • La desventaja es que agregan mecanismos como listas de effects a nivel de tipos, stacks de handlers y errores de tipos complicados
    • Los registros de funciones son lo bastante simples como para que un ingeniero nuevo los entienda en una tarde
  • Un ejemplo positivo de persistent

    • El SqlBackend de persistent es un registro de funciones como connPrepare, connInsertSql, connBegin, connCommit y connRollback
    • Al agregar instrumentación de OpenTelemetry, fue posible envolver los campos relevantes y adjuntar spans de tracing a todas las operaciones de base de datos
    • Se consiguió visibilidad de la capa de base de datos sin hacer fork y casi sin cambiar el código fuente
  • Librerías difíciles de operar

    • Mercury casi no usa bindings públicos de clientes de web API publicados en Hackage
    • Si un binding de terceros hace llamadas HTTP mediante funciones concretas, se vuelve difícil hacer tracing, aplicar timeouts acordes al SLO, simular caídas de partners o explicar un hueco de 400 ms en un trace
    • Por eso escriben sus propios clientes y los hacen observables desde el principio
  • El costo de un ecosistema pequeño

    • Algunas librerías de Haskell no están abandonadas, pero quedan como infraestructura pública sin una entidad claramente responsable que las mejore rápido
    • Se mantienen interfaces antiguas, y la adopción de diseños nuevos sobre observabilidad, diseño de límites y operabilidad puede ser lenta
    • http-client solo soporta HTTP/1.1 de forma directa; es suficientemente usable, pero en ciertos momentos puede requerir workarounds

Requisitos operativos para quienes escriben paquetes

  • Quienes desarrollan librerías deberían ofrecer vías de escape como registros de funciones, tipos de effect o callbacks para que los usuarios puedan inyectar comportamiento sin modificar el código fuente
  • Agregar hs-opentelemetry-api como dependencia y poner spans alrededor de las operaciones clave de IO ya ayuda a quienes operan la librería en producción
    • El paquete API es conservador con los breaking changes y está diseñado para funcionar de manera inerte si la aplicación no inicializa el SDK de OpenTelemetry
    • El overhead de rendimiento es mínimo y no genera excepciones inesperadas ni logging desde la aplicación usuaria
    • La huella de dependencias todavía no es tan pequeña como se quisiera y se está trabajando en mejorarla
  • No se debería escribir logs directamente desde el código de la librería
    • En vez de importar un logging framework y escribir directo a stdout o stderr, se deberían ofrecer callbacks, parámetros logger o un tipo de datos para mensajes de log que quien llama pueda enrutar
    • A dónde van los logs es una decisión que pertenece al entorno operativo de la aplicación
    • Mercury envía pipelines de logs estructurados a su stack de observabilidad, y si una librería escribe directo a stderr, eso obliga a separar el stream de JSON lines y agregar plomería aparte
  • También se puede considerar exponer módulos .Internal
    • La preocupación de que los usuarios dependan de APIs internas y eso dificulte refactors es válida
    • Pero rara vez está justificada la certeza de que la API pública ya cubre todos los casos de uso
    • Un módulo .Internal con advertencias explícitas de estabilidad puede ser mejor que que los usuarios hagan fork del paquete y lo incluyan vendorizado
    • containers, text y unordered-containers son buenos ejemplos de este enfoque dentro del ecosistema Haskell
    • Aun así, si los usuarios resuelven lo que necesitan usando silenciosamente módulos internos, puede reducirse el feedback sobre defectos de la API pública

Lo que no se pone en los tipos

  • Incluso en Haskell de producción existen partes no tan elegantes
  • unsafePerformIO se usa dentro de bibliotecas de las que dependemos a diario
    • bytestring y text internamente asignan buffers mutables, escriben en ellos y luego los congelan para producir el resultado
    • El tipo no dice qué ocurrió durante la construcción
    • Los límites se mantienen mediante convención, razonamiento cuidadoso y revisión de código
  • Si una alternativa type-safe hace que el costo de rendimiento o complejidad sea excesivo, uno mismo también puede escribir este tipo de concesiones
    • Hay que documentar los invariantes que el sistema de tipos no verifica
    • Hay que mantener la incomodidad y revisar periódicamente si una alternativa type-safe ya se volvió práctica
    • Haskell de producción no es la ausencia de concesiones, sino su aislamiento disciplinado
  • Muchas bibliotecas de Haskell en Hackage tienen pocas pruebas o ninguna
    • La idea de que “si compila, funciona” a veces puede ser cierta con código pequeño, puro y con tipos fuertes
    • Casi nunca aplica para código con mucho IO, integración con sistemas externos o código cuyos errores están en el significado más que en la estructura
  • Los tipos pueden decir que algo devuelve Either ParseError Transaction, pero no pueden decir lo siguiente
    • si el campo amount se parsea en centavos o en dólares
    • si la API del partner interpreta de manera distinta un campo omitido y un campo null
    • si la lógica de reintentos puede causar cobros duplicados en una ventana temporal específica de un año bisiesto
  • En producción se construyen sistemas sobre estas bibliotecas, heredando supuestos no verificados, así que hay que compensarlo con pruebas de integración en tu propia capa
  • También se acumulan concesiones como orphan instances, funciones parciales que se cree que son total en contexto, error prometidos como inalcanzables, wrappers de FFI incómodos y jerarquías de excepciones hechas a mano
  • El objetivo no es la pureza moral, sino que mediante revisión de código, documentación, ejemplos y pruebas se pueda saber dónde está cada concesión, por qué se hizo y qué se rompe si se elimina

Por qué vale la pena usar Haskell en producción

  • Haskell no es la opción rápida desde el primer día
    • El ecosistema actual no ofrece de inmediato un entorno de desarrollo batteries-included con hot reloading como Next.js o Rails
    • Puede que no exista la biblioteca que necesitas, o que sí exista pero la mantenga una sola persona en su tiempo libre
    • A veces los mensajes de error pueden ser muy crípticos
  • El problema de contratación está exagerado
    • Max Tagher, CTO de Mercury, ha dicho públicamente que el rol de backend Haskell engineer es el más fácil de cubrir en toda Mercury
    • La demanda por trabajos de Haskell supera a la oferta, invirtiendo la dinámica habitual de contratación
    • Mercury contrata tanto a personas con mucha experiencia en Haskell como a personas sin ninguna, y a estas últimas las lleva a la productividad con un programa de capacitación de 6 a 8 semanas
    • Si mañana necesitaras 100 expertos en Haskell, el problema del pool de contratación sería real, pero es menos realista si estás dispuesto a contratar buenos desarrolladores generalistas y enseñarles
  • El mayor riesgo de contratación no es el tamaño del pool, sino la disposición
    • Haskell atrae a idealistas que se preocupan por la corrección y la abstracción, disfrutan leer papers y cuestionan los supuestos existentes
    • Si esa fortaleza no se controla, puede convertirse en una responsabilidad en producción
    • Intentar reescribir la capa de base de datos con una nueva codificación de álgebra relacional a nivel de tipos, rechazar un merge porque alguien usó String en lugar de Text en un script desechable, o empujar todos los diseños hacia una reescritura total inspirada en el paper más reciente ralentiza al equipo
  • En Haskell de producción se necesita una cultura de pragmatismo
    • El sistema de tipos es una herramienta eléctrica, no una religión
    • Tomar un problema que ya tiene una buena solución como oportunidad para inventar un nuevo mecanismo no encaja con producción
  • Los beneficios aparecen con el tiempo
    • Un refactor que tomaría semanas en una base de código con tipado dinámico puede terminarse en horas después de cambiar los tipos, porque el compilador te muestra todos los call sites
    • Un nuevo engineer puede leer las firmas de tipos y entender el contrato de un módulo
    • Puede que no ocurra un incidente en producción porque un estado imposible realmente no se puede representar
  • Mercury considera que el retorno de inversión aparece no en años, sino en meses
    • Especialmente en servicios financieros, el costo de un bug de integridad de datos no se mide en molestia del usuario, sino en observaciones regulatorias y dinero ajeno
    • El sistema de tipos no elimina el riesgo, pero sí proporciona herramientas que hacen más difícil introducirlo por accidente en una base de código que crece rápido
  • El valor de Haskell en producción no está en ser una bala de plata ni un movimiento moral, sino en un potente conjunto de herramientas que permite que incluso equipos con distintos niveles de dominio de Haskell mantengan los artefactos peligrosos dentro de límites, preserven el conocimiento operativo y hagan que el camino seguro sea el camino fácil

1 comentarios

 
GN⁺ 2 시간 전
Comentarios de Hacker News
  • Es cierto que Haskell está entre los lenguajes más potentes para forzar este tipo de cosas con tipos, pero el mismo patrón también funciona bastante bien en Rust y TypeScript
    También me gusta la forma de evitar bugs de autorización evidentes que se repiten en apps web con flujos como User -> LoggedInUser -> AccessControlledLoggedInUser
    Creo que en la industria este patrón se usa muchísimo menos de lo que debería

    • Esto no aplica solo a Rust o TypeScript; en realidad se puede hacer en casi cualquier lenguaje
      Si por seguridad necesitas distinguir entre cadenas antes y después de escape, incluso en un lenguaje de tipado dinámico puedes envolverlas en una clase Escaped y tener funciones como escape(str)->Escaped, dangerouslyAssumeEscaped(str)->Escaped
      Hay un costo de rendimiento, así que hace falta un equilibrio, pero se puede
      Otra forma es Application Hungarian, aunque eso depende más de la disciplina del programador que del compilador: https://www.joelonsoftware.com/2005/05/11/making-wrong-code-...
    • Esto está más cerca de ser un problema de affordance que del sistema de tipos en sí
      Por ejemplo, también se puede hacer perfectamente en C#, pero el ruido visual termina siendo mayor que la definición real de tipos
    • Tanto Rust como TypeScript claramente recibieron una fuerte influencia de Haskell
      Solo que tienden a no decirlo ni a usar los mismos nombres para evitar el efecto de “los monads dan miedo, mejor escribo un tutorial”
      Más que los monads, la influencia mayor viene por el lado de cosas como las type classes
    • No estoy seguro de que esto realmente funcione tan bien en TypeScript
      Como no hay tipado nominal, para crear algo parecido a un newtype que envuelva un tipo primitivo tienes que recordar varios trucos medio hacky
      En mi experiencia, OCaml era más potente que Rust para forzar este tipo de seguridad de tipos
      Tiene más expresividad con GADT, más comodidad con variantes polimórficas y tipos objeto/tipos de fila en registros, y además tiene sistema de módulos y functors
      Si la recolección de basura es suficiente para tu dominio, también evitas las restricciones de abstracción y dificultades que trae el borrow checker de Rust
    • Es lo mismo que decir: haz imposible representar estados inválidos: https://news.ycombinator.com/item?id=40150159
  • De verdad me encantó trabajar varios años con Haskell
    No era algo que hubiera estado buscando a propósito, pero la oportunidad apareció por casualidad y fue interesante e intelectualmente estimulante
    Pero, lamentablemente, incluso después de usar solo Haskell durante 3 años, mi productividad en Rust seguía siendo fácilmente el doble que en Haskell
    Haskell tiene más trampas que necesitas conocer de antemano para evitarlas, y según quién lo escriba, a veces puede ser tan difícil de procesar que casi parece un lenguaje de solo lectura
    La toolchain a menudo viene combinada con Nix, y Nix mismo es un monstruo complejo, además de que las extensiones del lenguaje parecen estar por todos lados
    Los archivos Cabal tampoco son muy buenos, y toma tiempo acostumbrarse a los errores del compilador

    • Curiosamente, mi experiencia fue casi la opuesta
      En el último producto empecé a mover el backend de Typescript a Rust, porque estaba harto de los crashes
      Ahora lo veo como uno de los mayores errores técnicos que he cometido, porque la productividad se volvió lentísima
      Un ejemplo de tiempo perdido que solo me pasó en Rust fue intentar escribir una función de orden superior que abriera una conexión a la base de datos, hiciera algo y luego la cerrara; en Haskell, TypeScript, JavaScript, C++ y PHP eso es trivial, pero en Rust terminó siendo prácticamente imposible incluso preguntándole a amigos expertos en Rust, así que me rendí
      También hubo varias veces en que intenté un refactor, pasé todo el día arreglando errores de tipos y luego me topé con un error en el archivo de más alto nivel, solo para darme cuenta de que una parte fundamental del diseño hacía imposible todo el refactor, así que tuve que revertirlo completo
      Además, Rust es el único lenguaje moderno que se me ocurre donde usar valores a través de interfaces en vez de tipos concretos queda, según el caso, entre una técnica avanzada y algo imposible
      Por eso llegué a la conclusión de que el código de aplicación, o sea, código que no es de sistema ni de bibliotecas, más o menos no debería escribirse en Rust
    • Me pregunto si tu productividad era el doble en general, o si había áreas donde Rust también te hacía menos productivo
      Y también me pregunto qué quieres decir con “solo lectura”
  • A diferencia de la percepción general, creo que parte no menor del éxito puede haber venido de que Mercury eligió Haskell y de que sus líderes iniciales tenían mucha experiencia con Haskell
    Como cliente de Mercury, esta empresa es una de las piezas centrales de mi caja de herramientas, y no puedo quitarme la sensación de que elegir Haskell mejoró su avance, su desarrollo y su trayectoria en general
    Claro, se podría hacer una afirmación parecida sobre la mayoría de los lenguajes, y no significa que los lenguajes funcionales como Haskell sean la fórmula del éxito
    Pero tomar una decisión tan intencional antes de la era de “vibe coding” y de los LLM sí se ve especialmente visionario, y lo veo como parte del resultado combinado con la cultura de ingeniería detallada en el artículo

    • Es más probable que el factor de éxito haya sido su enfoque fintech orientado a startups y su capacidad de ejecución
      A mí también me encanta una buena cultura técnica, pero he visto empresas con excelente cultura técnica morir por tener un mal enfoque de negocio
      Incluso puede ser que una cultura fintech estilo startup haya producido una buena cultura técnica
      Como no empezaron como banco, no necesitaban ser tan conservadores ni integrarse con un stack tecnológico antiguo y horrible, a diferencia de, por ejemplo, SVB
      Me alegra que hayan tenido éxito con Haskell, pero como Jane Street con OCaml, creo que, a diferencia de lo que a la empresa le gustaría hacer creer, la elección del lenguaje en términos de negocio es casi accidental
      Eso sí, me da curiosidad qué usan en frontend. Supongo que todo este Haskell es backend
    • Incluso contratar generalistas sin experiencia en ese lenguaje puede haber ayudado
      Porque así podías inculcarles la cultura y el estilo desde cero
      Antes del vibe coding, la mayoría de esa gente probablemente no se habría lanzado a hackear por su cuenta sin ninguna guía
    • Se nota que todo en la app simplemente funciona
      Cuando vienes de otros servicios, de verdad se agradece
  • Un amigo muy cercano trabaja en esta empresa y, incluso desde afuera, su cultura de ingeniería se ve buena
    Creo que Haskell es una herramienta adecuada para este trabajo y que están aprovechando bien sus fortalezas, pero también me queda la impresión de que buena parte del éxito podría deberse simplemente a que la empresa está bien gestionada en general

    • Esa misma fue la impresión que me dejó el artículo
      Da la sensación de que este autor habría dirigido una organización de ingeniería exitosa usando prácticamente cualquier lenguaje
    • Tampoco contradice la idea común de que usar lenguajes de programación funcional filtra un grupo de talento/postulantes de mayor calidad
  • Ahorita estoy leyendo Real-World OCaml; ya conocía algunas cosas, pero estoy aprendiendo más programación funcional
    Da la impresión de que con programación funcional se pueden construir piezas de software sorprendentemente robustas
    Pero también me genera dudas
    El backend actual del producto corre sobre NiceGUI y cumple bien su papel
    El código es razonable, MVVM, y lo más importante es conectarse por websocket para cada cliente, consumir datos y mostrar análisis
    No vamos a tener muchos clientes, y los visitantes del sitio probablemente serán desde unas decenas hasta, como mucho, unos cientos
    También quiero REPL o hot reload, pero sé que conforme crezcan las funcionalidades, programación funcional podría encajar bien con transformaciones de pipelines de datos en cosas como paneles de administración de usuarios, más análisis, etc.
    Pero Haskell u OCaml son lenguajes estáticos
    Si quieres algo dinámico más adelante a medida que crece y escala, Clojure o Elixir parecen buenas opciones
    Al mismo tiempo, me da miedo que si algún día hace falta refactorizar, todo se rompa
    Por ahora uso Python con Mypy, y el frontend lo genera NiceGUI desde el backend

    • No conozco OCaml, pero en Haskell puedes recargar muy rápido una webapp en desarrollo con ghci/cabal repl
      Sinceramente, creo que muchos usuarios de Haskell no le sacan suficiente provecho a eso
  • Trabajé en un sistema parecido usando un lenguaje relativamente de nicho, Scheme y después Racket, y aunque creció de tamaño, un equipo pequeño pudo mantenerlo por mucho tiempo y seguir avanzando rápido
    No producíamos muchos bugs y normalmente podíamos añadir funcionalidades muy deprisa
    Por ejemplo, fuimos los primeros en conseguir cierta certificación para alojar datos sensibles en AWS
    A veces agregar funciones era más lento porque había que construir desde cero cosas que en plataformas populares resolverías con componentes ya hechos
    Pero una vez construidas, funcionaban bien, recuperábamos la velocidad de antes y no nos ralentizaban la hinchazón ni la complejidad de decenas de frameworks prefabricados
    Como teníamos control directo sobre una plataforma manejable, también podíamos movernos rápido a AWS cuando hizo falta
    El sistema también tenía desde el principio cierta magia de arquitectura para datos complejos e interacciones web, y eso nos permitió desarrollar muchas funciones rápidamente y seguir empujando en direcciones inteligentes después
    La diferencia con la fintech en Haskell es que el tamaño del equipo era muy pequeño
    A la vez solo había 2 o 3 ingenieros de software, además de alguien a cargo de operaciones
    Así que no existía la dificultad de coordinar a cientos de personas mientras mantienes un sistema consistente
    Normalmente una persona se encargaba de los cambios de código más técnicos y arquitectónicos, y otra añadía muy rápido funcionalidades de lógica de negocio amplias para procesos complejos
    Si se usan con cuidado las herramientas de IA tipo LLM actuales o del futuro cercano, parece posible recuperar parte de la eficiencia de equipos de software muy pequeños y extremadamente efectivos
    El modelo que me viene a la mente no es producir una enorme hinchazón para eliminar story points y dejar la sostenibilidad como problema de otro, sino unos pocos pensadores muy agudos manteniendo el sistema en un camino que siga siendo potente pero manejable

  • Es un arma de doble filo
    2 millones de líneas es un logro impresionante, pero al mismo tiempo también es una carga de mantenimiento considerable
    Las ventajas de Haskell son teóricamente claras, pero sus desventajas son menos intuitivas
    La tentación está en modelar todo con tipos
    La base de código misma deja de ser la aplicación y se convierte en la especificación del negocio
    Cada cambio de política termina siendo un gran refactor, y gracias a la seguridad de Haskell eso puede ser sorprendentemente laborioso
    Al final no puedes tenerlo todo, y tarde o temprano acabas atrapado por los tipos
    Haskell es realmente impresionante y potente, especialmente a esta escala, pero también trae problemas propios
    La tentación de modelar la lógica de negocio con tipos puede producir estructuras rígidas, y la seguridad que esa estructura da puede impedirte ver otros tipos de riesgo

    • Si ingenieros con buen criterio y experiencia construyen las partes clave, pueden manejar bastante bien esa línea
      No puedes tenerlo todo, pero sí puedes tener mucho
      Hice una pasantía en Jane Street hace unos años; no era Haskell sino OCaml, pero parecía que equilibraban eso muy bien
      Era un dominio con alta complejidad intrínseca, donde confiabilidad y precisión están directamente ligadas a la viabilidad del negocio, y aun así se movían sorprendentemente rápido
      Viéndolo en retrospectiva, la clave de Jane Street estaba en contratar programadores de OCaml experimentados y con excelente criterio, como Stephen Weeks, y dejar que construyeran las bibliotecas centrales desde el principio y guiaran toda la base de código
      Lamentablemente, Mercury no fue tan bueno en esa parte
    • Lo mismo pasa con TypeScript: https://www.richard-towers.com/2023/03/11/typescripting-the-...
      Sinceramente, la mayor desventaja de un sistema de tipos Turing-completo es que, en teoría, puedes implementar una aplicación que al compilar se convierta en polvo
  • Un caso parecido de éxito con Haskell en Bellroy será el tema de una próxima reunión de Melbourne Compose: https://luma.com/uhdgct1v

  • El problema que yo tengo con la programación funcional es el debugging
    Más exactamente, lo veo como una fortaleza de la programación imperativa, especialmente del estilo procedimental
    En el estilo funcional/declarativo normalmente describes no cómo se construye algo, sino en qué estado debería estar, y el lenguaje ensambla todo para darte el resultado final
    Si todo salió bien, perfecto, incluso puede ser mejor, pero si no y no obtienes el resultado esperado, entonces el problema es cómo encontrar el bug
    En un lenguaje como C es relativamente simple
    Vas línea por línea viendo el estado de ejecución entre cada paso, básicamente la RAM, y si algo no coincide con lo esperado, entonces algo salió mal en esa línea y sigues profundizando así
    Mientras más intenta el lenguaje ocultar el estado, como en programación funcional, más difícil se vuelve esto
    También es interesante que la sección más larga del artículo trate justo de este problema, es decir, “design for introspection”
    El autor tuvo que hacer mucho esfuerzo deliberado para que el código fuera depurable, y eso da una buena perspectiva sobre el uso práctico de Haskell, algo que a menudo se pasa por alto

    • Mi truco de debugging es hacer que todo código que tenga aunque sea un poco de importancia devuelva la misma salida para la misma entrada
      Incluso el código trivial
      Ningún otro lenguaje mainstream se acerca a eso
      En los casos donde no puedes escribirlo así, como con concurrencia de memoria compartida, usas transacciones
      Y en eso tampoco se acercan otros lenguajes mainstream
      Ni siquiera estoy contando ventajas fáciles como no tener null o no tener conversiones implícitas de enteros
      Es totalmente cierto que depurar código Haskell es más difícil que en otros lenguajes
      Pero si eliminas el 90% de las trabas más comunes, es natural que pase eso
    • El debugging en programación funcional, a diferencia de la imperativa, muchas veces está guiado por REPL
      Claro, no es algo exclusivo de lo funcional; en lenguajes más bien imperativos como Python o JavaScript también se usa mucho el shell de Python, la consola del navegador, el shell de Node/Deno/Bun, notebooks, etc., como primera capa de depuración
      El debugging centrado en REPL tiene compensaciones interesantes
      En lenguajes como C, muchas veces empiezas depurando el programa completo y tratando de acertar el punto exacto donde crees que puede estar el problema mediante breakpoints
      En un mundo centrado en REPL, intentas hacer más testeables directamente en el REPL los componentes del programa
      Por eso los límites de módulo/API/tipo se parecen cada vez más a los límites de depurabilidad
      A veces existe más presión para que esos límites estén bien hechos y sean cómodos de usar que en lenguajes imperativos como C/C++
      En cambio, frente al debugging centrado en todo el programa, a veces se vuelve más difícil aislar problemas complejos de integración entre unidades en escenarios raros del mundo real
      Pero el enfoque REPL-first muchas veces empuja a minimizar la superficie de integración, así que en lenguajes funcionales suelen aparecer menos de esos efectos de integración que en lenguajes imperativos
      No creo que sea correcto decir que los lenguajes funcionales ocultan el estado
      Estos lenguajes también corren sobre hardware imperativo y manejan estado real del hardware
      En algún punto hay una traducción entre ambos mundos, y probablemente no es tan distinta como parece
      Si hace falta, todavía puedes volver a breakpoints imperativos y a un debugger imperativo
      Por eso lo llamo debugging “guiado por REPL”
      Con el REPL puedes ir acotando la unidad problemática, es decir, el módulo/API/función exacto junto con la entrada que produce una salida inesperada
      Si el bug no se hace evidente solo viendo el código fuente, puedes pasarlo a un debugger imperativo para ver una experiencia casi igual de ejecución línea por línea y obtener más contexto
      En ese punto probablemente ya lo acotaste lo suficiente con el REPL como para que la unidad en sí sea pequeña y estrecha, así que ni siquiera haga falta elegir buenos breakpoints
      Creo que interpretaste mal el mensaje de la sección “design for introspection” del artículo
      Esa sección no trata de depurabilidad sino de observabilidad
      Hablaba de conectar correctamente sistemas de logging/telemetría, de simular dobles durante pruebas y de agregar retries/circuit breakers a nivel de todo el sistema en vez de dejarlo en manos de bibliotecas individuales
      Incluso en el mundo imperativo eso no es un problema de debugging sino un problema de descomposición, como inyección de dependencias, instalación de middleware o uso de interfaces abstractas en vez de clases concretas en los límites de una API pública
      Ese tipo de sugerencias de diseño son refactors, y afectan menos a la depurabilidad que a qué tan fácil es instalar middleware de observabilidad sobre la API pública de otra persona
  • Me cuesta imaginar qué podría estar haciendo exactamente un código Haskell de 2 millones de líneas
    Es muchísimo código, y Haskell tiene fama de ser un lenguaje “denso” que puede hacer mucho con poco código
    Me pregunto si será por tener muchas bibliotecas para cosas como serialización/deserialización JSON, frameworks de API REST, logging, etc.

    • Según el texto original, el problema es que el código que no se puede instrumentar no se puede confiar
      Si bindings de terceros hacen llamadas HTTP con funciones concretas, no tienes forma de añadir tracing, ni de inyectar timeouts alineados con tus SLO, ni de simular fallas del partner en pruebas, ni de explicar un hueco de 400 ms en un trace más allá de inventar teorías
      Así que lo escriben ellos mismos
      Al principio es más trabajo, pero los clientes propios están construidos para ser observables desde el comienzo
    • A esa cualidad que llamaste “densa” normalmente se le dice alta expresividad
      Significa que puedes expresar ideas relativamente abstractas con pocos caracteres
      Algunas personas también le llaman “alto nivel”
      Aun así, no creo que 2 millones de líneas sean tantas como suena al principio
      Menos todavía si es una empresa en un dominio muy regulado como finanzas y es código acumulado durante varios años
    • No es una métrica objetiva en absoluto, pero siempre sentí que Haskell simplemente tiene una relación de aspecto diferente
      Tal vez el número de líneas sea algo menor, pero la cantidad de palabras suele ser parecida a la de lenguajes orientados a objetos más imperativos
    • No sé cómo será realmente esa base de código, pero parte de la reputación de concisión de Haskell viene de que hay una sobrerrepresentación del mundo académico o de teoría de categorías
      Ahí expresiones como St M -> C T pueden parecer aceptables, pero en software real es mucho más útil escribir TransactionState Debit -> Verified Transaction
      Otra parte es un factor cultural que se remonta hasta LISP
      La gente tiende a ponerse demasiado lista para ahorrar líneas usando trucos o macros difíciles de entender
      Sospecho que en una empresa financiera como Mercury se fomenta más la claridad y la legibilidad que ese estilo
      Por ejemplo, quizá el linter haga que, en vez de escribir todo en una línea con >> y >>=, el código monádico se divida en expresiones do meticulosas de varias líneas