1 points par GN⁺ 3 시간 전 | 1 commentaires | Partager sur WhatsApp
  • Mercury fournit des services bancaires à plus de 300 000 entreprises avec une base de code d’environ 2 millions de lignes de Haskell hors commentaires, et a traité en 2025 un volume de transactions de 248 milliards de dollars ainsi qu’un chiffre d’affaires annualisé de 650 millions de dollars
  • La valeur de Haskell chez Mercury ne tient pas tant à la pureté elle-même qu’au fait d’encoder le savoir opérationnel dans les API et les types, de placer les comportements risqués derrière des frontières étroites, et de faire du chemin sûr le chemin le plus simple
  • La fiabilité n’est pas abordée comme la prévention totale des pannes, mais comme la capacité d’un système à absorber la variabilité ; le système de types exclut des classes d’erreurs et laisse un savoir institutionnel sous forme de documentation que le compilateur peut faire respecter
  • Mercury utilise Temporal comme framework de durable execution pour les retries, timeouts, annulations et reprises après crash dans les workflows financiers, et a publié en open source le SDK Haskell hs-temporal-sdk
  • La valeur de Haskell en production ne consiste pas à tout mettre dans les types : les invariants qui pourraient entraîner des pertes de données, des erreurs financières ou des problèmes réglementaires sont protégés par les types, tandis que la complexité est encapsulée et gérée avec les tests, la documentation et la code review

L’échelle opérationnelle de Haskell chez Mercury et sa vision de la fiabilité

  • Mercury exploite une base de code Haskell d’environ 2 millions de lignes, hors commentaires
  • Mercury est une fintech qui fournit des services bancaires à plus de 300 000 entreprises, et a traité en 2025 248 milliards de dollars de volume de transactions ainsi qu’un chiffre d’affaires annualisé de 650 millions de dollars
  • L’entreprise compte environ 1 500 employés, et l’organisation engineering recrute principalement des développeurs généralistes, dont la plupart n’avaient jamais utilisé Haskell avant de rejoindre la société
  • Ce système fonctionne depuis des années à travers une forte croissance, la crise de SVB avec 2 milliards de dollars de nouveaux dépôts arrivés en 5 jours, des examens réglementaires, et les situations ordinaires comme extraordinaires propres à un grand système financier

La fiabilité n’est pas la prévention des pannes, mais la capacité à absorber la variabilité

  • L’approche traditionnelle de la fiabilité se concentre sur l’énumération des défaillances, l’ajout de vérifications et de tests, et la recherche de bugs, mais cela ne suffit pas
  • Mercury traite la fiabilité comme la capacité d’un système à absorber la variabilité
    • Le système doit pouvoir se dégrader avec élégance
    • Les opérateurs doivent pouvoir comprendre et ajuster le système
    • L’architecture doit rendre les bonnes actions faciles et les mauvaises difficiles
  • Dans une organisation en forte croissance, les vraies questions d’exploitation sont de savoir si un ingénieur nouvellement arrivé peut lire et comprendre un module, si le service s’effondre avec la base de données quand celle-ci ralentit, et si le compilateur détecte les mauvais usages d’interface
  • Le système de types se rapproche davantage d’un outil d’assistance opérationnelle que d’une simple preuve de correction
    • Il exclut certaines classes d’erreurs
    • Il laisse le savoir institutionnel dans une forme lisible par le compilateur, même après le départ de son auteur
    • Il sert de documentation appliquée de manière plus cohérente qu’un wiki
  • Chez Mercury, le stability engineering n’est pas une police de la qualité qui ralentit le développement produit, mais une manière collaborative d’aborder dès la conception initiale l’impact d’une fonctionnalité lorsqu’elle casse
    • Le rayon d’explosion en cas d’échec
    • Les opérations qui exigent de l’idempotence et la manière de l’obtenir
    • La forme du rollback
    • Le traitement des travaux en cours
    • L’examen en amont de ce qui permet au système d’absorber l’échec ou au contraire de l’amplifier

La pureté n’est pas une propriété du langage, mais une frontière d’interface

  • La pureté en Haskell ne signifie pas l’absence totale d’effets de bord en interne, mais plutôt que l’interface crée une frontière qui empêche la fuite de ces effets de bord
  • Derrière les fonctions pures de bibliothèques comme bytestring, text ou vector, il existe des implémentations internes avec allocations mutables, écritures dans des buffers ou unsafe coercion
  • La monade ST utilise des modifications en place observables et des effets de bord à l’intérieur du calcul, mais le type rank-2 de runST empêche les références mutables créées en interne de s’échapper
    runST :: (forall s. ST s a) -> a  
    
  • En interne, un comportement impératif est possible, mais à l’extérieur seul le résultat est visible, et l’état mutable ne fuit pas au-delà de la frontière
  • Ce principe s’applique à l’ensemble des systèmes d’exploitation
    • La couche base de données peut utiliser en interne du pooling de connexions, des retries et de l’état mutable
    • Un cache peut utiliser une map mutable concurrente
    • Un client HTTP peut avoir un circuit breaker, un pool de connexions et beaucoup de logique de gestion interne
    • L’essentiel est d’envelopper les comportements risqués dans une interface étroite afin d’en rendre le mauvais usage difficile
  • Dans un système réel, l’objectif n’est pas d’éviter totalement le changement, mais de rendre clair où il se trouve et de limiter quelles parties de la base de code doivent en avoir connaissance

Rendre la bonne chose facile à faire

  • Dans une grande base de code, on voit souvent apparaître des patterns dont la correction dépend d’un ordre précis ou d’étapes supplémentaires invisibles
    • Il faut flush l’audit log après une transaction
    • Il faut vérifier le feature flag avant d’appeler un endpoint
    • Il faut mettre une notification en file d’attente à l’intérieur de la transaction de base de données
  • Si ce savoir opérationnel n’existe que dans un wiki, des documents d’onboarding, d’anciennes design reviews, des threads Slack ou la mémoire de quelques ingénieurs seniors, il disparaît vite
  • Haskell permet d’encoder ces procédures dans les types afin qu’elles ne puissent pas être oubliées
  • La mauvaise approche consiste à demander d’utiliser la bonne fonction tout en laissant un chemin de contournement
    -- 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 ()  
    
    • Une meilleure approche consiste à restructurer les types pour que le seul chemin d’exécution de l’opération inclue la publication des événements
    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  
    
  • Ici, le système de types ne sert pas tant à prouver un théorème profond sur les événements qu’à faire de la bonne procédure opérationnelle le chemin le plus simple
  • Lorsqu’un nouvel ingénieur demande comment écrire une transaction, la signature de type et l’API publique fournissent la réponse, et le savoir reste présent même si un ingénieur senior s’en va

Exécution durable et Temporal

  • Les workflows des systèmes financiers ne tiennent pas dans une transaction unique
    • envoi d’un paiement
    • attente de l’approbation d’un partenaire
    • mise à jour du grand livre
    • notification du client
    • gestion des annulations et des timeouts
    • cas où le partenaire a réussi, mais où le worker est mort avant d’enregistrer
    • cas où il n’y a pas de réponse à cause d’un problème réseau
  • De tels flux ont besoin d’état, de retries, de timeouts, d’idempotence et d’une exécution qui survive aux crashs de processus comme aux déploiements
  • Par le passé, Mercury orchestrait ces processus avec des machines à états basées sur la base de données, des tâches cron, des workers en arrière-plan, ainsi que des retries et timeouts gérés à divers endroits du code
    • cela fonctionnait, mais c’était fragile, difficile à comprendre, et une source disproportionnée d’incidents en production
  • Temporal est le framework de durable execution de Mercury, qui permet d’écrire des workflows comme du code séquentiel ordinaire, tandis que la plateforme enregistre chaque étape dans un historique d’événements
  • Si un worker plante au milieu d’un workflow, un autre worker rejoue le préfixe déterministe pour reconstituer l’état, puis reprend à partir du point d’interruption
  • Les retries, timeouts, annulations et la gestion des erreurs sont fournis par la plateforme au lieu d’être réimplémentés séparément par chaque équipe
  • Un workflow Temporal a une nature proche d’une fonction pure sur l’historique d’événements
    • un workflow rejoué doit produire la même séquence de commandes que l’original
    • cette exigence de déterminisme ressemble à la contrainte même entrée·même sortie du code pur
    • les effets de bord sont isolés dans des activities, qui correspondent au IO du workflow
  • Mercury a créé et open sourcé le SDK Haskell hs-temporal-sdk, qui encapsule le Core SDK officiel de Temporal via Rust FFI
  • Les schémas d’adoption de Temporal ont aussi été abordés dans la présentation Temporal Replay conference, et Mercury a obtenu des améliorations opérationnelles en remplaçant des chaînes fragiles de cron et de machines à états par des workflows durables

Le domaine doit être conçu dans le langage métier, pas dans la couche de transport

  • Une erreur fréquente dans les systèmes qui ont grandi est de laisser les concepts du système appelant fuiter dans le modèle de domaine
  • Quand du code écrit pour des handlers de requêtes HTTP est ensuite réutilisé dans des tâches cron, des workers en arrière-plan fondés sur des files, ou des workflows Temporal, des exceptions HTTP comme StatusCodeException 409 "Conflict" peuvent se propager dans des contextes non HTTP
  • Une tâche cron n’a pas d’appelant en attente d’une réponse 409, et les codes d’état entraînent une signification métier dans la mauvaise couche
  • La solution consiste à modéliser les erreurs de domaine avec des types de domaine
    • un solde insuffisant doit être InsufficientFunds
    • une requête en doublon doit être DuplicateRequest
    • un timeout côté partenaire doit être PartnerTimeout
  • On place à chaque frontière une fine couche de conversion
    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  
    
  • Les préoccupations de la couche de transport doivent rester en périphérie, et le modèle de domaine ne doit pas traîner des codes d’état HTTP, qu’il soit appelé depuis un handler web, une CLI, une tâche cron, un worker en arrière-plan ou un moteur de workflow

Le coût de l’encodage dans les types, et le bon niveau

  • Mettre des invariants dans les types est puissant, mais cela a un coût en charge cognitive, en rigidité et en difficulté lorsque les exigences changent
  • Si une violation peut entraîner une perte de données, une erreur financière, un problème réglementaire ou un incident lié à des appels bloqués en attente, alors le coût de l’encodage dans les types se justifie
  • Si c’est seulement parce que « c’est comme ça aujourd’hui » ou parce qu’on veut essayer une technique au niveau des types, il y a de fortes chances que cela rende la base de code plus difficile à faire évoluer
  • Côté trop d’encodage

    • les états illégaux deviennent impossibles à représenter et le domaine est fidèlement modélisé dans les types
    • un changement de règle métier entraîne des modifications de types à travers 50 modules, ce qui allonge le refactoring
    • les nouveaux ingénieurs ont du mal à comprendre les signatures de types
  • Côté aucun encodage

    • les types se rapprochent de String, IO (), voire au pire de Dynamic
    • le code est facile à modifier, mais il n’y a pas de contrat, et le sens dépend de la mémoire de ceux qui l’ont écrit
    • quand ces personnes partent, il devient difficile de comprendre pourquoi le système fonctionne
  • Quelques critères utiles

    • les invariants qui empêchent une corruption silencieuse gagnent à être encodés dans les types
      • transaction validée sans événement
      • paiement traité sans journal d’audit
      • transition d’état apparemment possible, mais sémantiquement impossible
    • les invariants qui échouent bruyamment peuvent se contenter de vérifications à l’exécution avec de bons messages d’erreur
      • réponse 500
      • échec d’assertion
      • incompatibilité de type à la frontière JSON
    • il faut résister à la tentation de modéliser tout le domaine dans les types
      • un domaine contient des exceptions, des règles de compatibilité héritées, des règles contradictoires et des comportements spéciaux pour certains clients
    • les types sont un outil pour l’équipe, pas seulement pour le compilateur
      • ils doivent former une couche de défense avec les tests, la documentation, les revues de code, les exemples et les playbooks
    • en interne, Mercury a aussi des bibliothèques qui utilisent des dispositifs complexes au niveau des types, comme les GADT, les type families et les phantom types qui suivent les transitions d’état
    • cette complexité est nécessaire dans les mécanismes où une erreur peut faire partir de l’argent au mauvais endroit ou briser des invariants réglementaires
    • l’essentiel est de l’encapsuler
    • le module qui implémente une machine à états au niveau des types doit avoir un petit nombre d’auteurs qui la comprennent en profondeur, ainsi que des tests suffisants
    • l’API côté usage doit ressembler à quelques fonctions aux types ordinaires
    • un product engineer doit pouvoir l’appeler en toute sécurité sans connaître les mécanismes internes de preuve au niveau des types
    • si, en revue de code, une PR qui touche d’autres modules est remplie d’annotations de type copiées pour apaiser le compilateur, c’est le signe que l’abstraction fuit au-delà de sa frontière

Concevoir pour l’introspectibilité

  • Si la fiabilité est une capacité d’adaptation, l’introspectibilité est l’un des moyens d’acquérir cette capacité
  • Les opérateurs ne peuvent pas exploiter ce qu’ils ne peuvent pas voir, et une équipe a du mal à s’adapter à un système dont l’intérieur est opaque
  • Haskell n’a pas de monkey patching, il est donc difficile de remplacer à l’exécution le client HTTP interne d’une bibliothèque ou de substituer les appels à la base de données par des fonctions qui émettent des spans OpenTelemetry
  • Rust a la même contrainte, mais son écosystème a convergé vers le pattern de middleware tower, alors que l’écosystème Haskell est réparti entre plusieurs approches
  • Si une bibliothèque n’expose qu’un ensemble de fonctions top-level concrètes, l’instrumenter oblige à l’envelopper dans un nouveau module et à espérer que les gens importent ce module à la place de l’original
  • Enregistrements de fonctions

    • La solution la plus courante consiste à exposer des enregistrements de fonctions plutôt que des fonctions concrètes
      -- 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  
      }  
      
    • Avec cette approche, on peut envelopper sendRequest avec une instrumentation de timing et renvoyer un nouveau HttpClient
    • Cela permet d’ajouter à l’exécution des préoccupations transverses comme l’injection de fautes pour les tests, le remplacement par des mocks, les retries, le tracing, la réécriture des requêtes ou des comportements spécifiques à chaque tenant
    • Comme type Middleware = Application -> Application dans WAI, ce pattern qui rend les transformations de comportement composables est très utile en exploitation
  • Des intercepteurs composés avec Monoid

    • Les types de middleware et d’interceptor peuvent généralement avoir des instances Semigroup et Monoid
    • Le Middleware de WAI est un endomorphisme, et les endomorphismes forment un monoïde sous la composition avec id
    • Les enregistrements de hooks d’interceptor peuvent être composés champ par champ, ce qui permet de combiner via mconcat des préoccupations comme le tracing, les timeouts ou la réécriture de task queues, sans plomberie supplémentaire
      appTemporalInterceptors =  
      mconcat  
        [ retargetingInterceptor  
        , otelInterceptor  
        , sentryInterceptor  
        , sqlApplicationNameInterceptor  
        , loggingContextInterceptor  
        , statementTimeoutInterceptor  
        , teamNameInterceptor  
        , clientExceptionInterceptor  
        , workflowTypeNameInterceptor  
        ]  
      
    • Chaque interceptor vit dans un module indépendant, ne traite qu’une seule préoccupation, ne redéfinit que les champs nécessaires à partir de mempty, et l’ordre est explicite dans la liste
  • Systèmes d’effets

    • Les effect systems comme effectful, polysemy, fused-effects et cleff offrent aussi une autre voie
    • On définit les opérations disponibles sous forme de types d’effet, puis on peut remplacer au point d’appel les interpréteurs pour la production, les tests ou le tracing
    • On peut intercepter un effet pour enregistrer des métriques ou injecter de la latence, puis le renvoyer vers le handler réel
    • L’inconvénient est qu’ils ajoutent de la machinerie, comme des listes d’effets au niveau des types, des piles de handlers et des erreurs de type délicates
    • Les enregistrements de fonctions restent assez simples pour qu’un nouvel ingénieur les comprenne en un après-midi
  • L’exemple positif de persistent

    • Le SqlBackend de persistent est un enregistrement de fonctions avec des éléments comme connPrepare, connInsertSql, connBegin, connCommit et connRollback
    • Lors de l’ajout de l’instrumentation OpenTelemetry, il a suffi d’envelopper les champs concernés pour attacher des spans de tracing à toutes les opérations de base de données
    • Cela a apporté de la visibilité sur la couche base de données sans fork, avec presque aucun changement de code source
  • Les bibliothèques difficiles à exploiter

    • Mercury utilise très peu les bindings de clients d’API web publiés sur Hackage
    • Quand des bindings tiers effectuent les appels HTTP via des fonctions concrètes, il devient difficile d’ajouter du tracing, des timeouts alignés sur les SLO, des simulations de panne de partenaires, ou d’expliquer un trou de 400 ms dans une trace
    • Mercury écrit donc ses propres clients et les rend observables dès le départ
  • Le coût d’un petit écosystème

    • Certaines bibliothèques Haskell ne sont pas abandonnées, mais restent comme une infrastructure publique sans acteur clairement responsable pour les faire évoluer rapidement
    • D’anciennes interfaces sont maintenues, et l’adoption de nouvelles conceptions autour de l’observabilité, de la conception des frontières ou de l’opérabilité peut être lente
    • http-client ne prend directement en charge que HTTP/1.1 ; c’est largement utilisable, mais certains moments peuvent nécessiter des contournements

Exigences opérationnelles pour les auteurs de packages

  • Les auteurs de bibliothèques doivent fournir des portes de sortie comme des enregistrements de fonctions, des types d’effet ou des callbacks, afin que les utilisateurs puissent injecter du comportement sans modifier le code source
  • Le simple fait d’ajouter hs-opentelemetry-api comme dépendance et de placer des spans autour des opérations IO critiques aide déjà les utilisateurs qui exploitent la bibliothèque en production
    • Le package d’API est conservateur sur les breaking changes, et il est conçu pour rester inerte si l’application n’initialise pas le SDK OpenTelemetry
    • Le surcoût en performance est minimal, et il ne provoque pas d’exceptions inattendues ni de logging dans l’application utilisatrice
    • L’empreinte de dépendances n’est pas encore aussi réduite que souhaité, et un travail d’amélioration est en cours
  • Il ne faut pas écrire directement dans les logs depuis le code d’une bibliothèque
    • Au lieu d’importer un framework de logging pour écrire directement sur stdout ou stderr, il faut fournir des callbacks, un paramètre de logger ou un type de données de message de log que l’appelant peut router
    • La destination des logs est une décision qui relève de l’environnement d’exploitation de l’application
    • Mercury envoie un pipeline de logs structurés vers son observability stack ; si une bibliothèque écrit directement sur stderr, cela impose une plomberie séparée du flux JSON lines
  • Exposer des modules .Internal peut aussi être envisagé
    • L’inquiétude selon laquelle les utilisateurs pourraient dépendre d’API internes et rendre les refactorings plus difficiles est légitime
    • Mais il est rare de pouvoir affirmer avec certitude que l’API publique couvre déjà tous les cas d’usage
    • Des modules .Internal avec un avertissement explicite sur leur stabilité peuvent valoir mieux que de forker un package et le vendoriser
    • containers, text et unordered-containers sont de bons exemples de cette approche dans l’écosystème Haskell
    • Cela dit, si les utilisateurs se contentent discrètement d’utiliser des modules internes pour obtenir ce dont ils ont besoin, le feedback sur les défauts de l’API publique peut diminuer

Ce que l’on ne met pas dans les types

  • Même en Haskell de production, il existe des parties peu élégantes
  • unsafePerformIO est utilisé au sein de bibliothèques dont on dépend au quotidien
    • bytestring et text allouent en interne des buffers mutables, y écrivent, puis les figent pour produire le résultat
    • Les types ne disent pas ce qui s’est passé pendant la construction
    • Les frontières sont maintenues par convention, par un raisonnement prudent et par la revue de code
  • Si une alternative type-safe rend le coût en performances ou en complexité excessif, on peut être amené à écrire soi-même ce genre de compromis
    • Il faut documenter les invariants que les types ne vérifient pas
    • Il faut conserver une certaine gêne face à cela et réévaluer périodiquement si une alternative type-safe est devenue pratique
    • Le Haskell de production n’est pas l’absence de compromis, mais leur isolement discipliné
  • De nombreuses bibliothèques Haskell sur Hackage ont peu de tests, voire aucun
    • L’idée que « si ça compile, alors ça marche » peut parfois être vraie pour du petit code pur avec des types forts
    • Elle est presque toujours fausse pour le code fortement orienté IO, les intégrations avec des systèmes externes et le code dont les bugs relèvent du sens plutôt que de la structure
  • Les types peuvent dire qu’on renvoie Either ParseError Transaction, mais ils ne peuvent pas dire :
    • si le champ amount est parsé en centimes ou en dollars
    • si l’API partenaire interprète différemment un champ omis et un champ null
    • si la logique de retry provoque une double facturation dans une fenêtre temporelle spécifique d’une année bissextile
  • En production, on construit des systèmes sur ces bibliothèques et on hérite donc d’hypothèses non vérifiées, qu’il faut compenser avec des tests d’intégration à son propre niveau
  • D’autres compromis s’accumulent aussi : orphan instances, fonctions partielles que l’on croit totales dans leur contexte, error promis comme inatteignable, wrappers FFI maladroits, hiérarchies d’exceptions faites à la main
  • L’objectif n’est pas la pureté morale, mais de faire en sorte qu’on puisse savoir, via la revue de code, la documentation, les exemples et les tests, où se trouve chaque compromis, pourquoi il a été fait et ce qui casserait si on l’enlevait

Pourquoi Haskell vaut la peine en production

  • Haskell n’est pas un choix rapide dès le premier jour
    • L’écosystème actuel ne fournit pas immédiatement un environnement de développement batteries-included avec hot reloading comme Next.js ou Rails
    • Il peut manquer la bibliothèque nécessaire, ou elle peut exister mais être maintenue par une seule personne sur son temps libre
    • Les messages d’erreur peuvent parfois être très obscurs
  • Les problèmes de recrutement sont exagérés
    • Max Tagher, CTO de Mercury, a déclaré publiquement qu’ingénieur backend Haskell était le poste le plus facile à pourvoir dans toute l’entreprise
    • La demande pour les postes Haskell dépasse l’offre, ce qui inverse la dynamique habituelle du recrutement
    • Mercury recrute à la fois des personnes ayant une forte expérience de Haskell et d’autres qui n’en ont aucune, ces dernières devenant productives grâce à un programme de formation de 6 à 8 semaines
    • Si vous avez besoin demain de 100 experts Haskell, le problème du vivier de recrutement est réel ; si vous êtes prêt à recruter de bons développeurs généralistes et à les former, il l’est beaucoup moins
  • Le plus grand risque de recrutement n’est pas la taille du vivier, mais les dispositions d’esprit
    • Haskell attire des idéalistes qui se soucient de précision et d’abstraction, aiment lire des articles de recherche et remettent en question les hypothèses établies
    • Si cette force n’est pas maîtrisée, elle peut devenir une responsabilité en production
    • Vouloir réécrire la couche base de données avec un nouvel encodage d’algèbre relationnelle au niveau des types, refuser une fusion parce qu’un script jetable utilise String au lieu de Text, ou pousser chaque design vers une réécriture totale inspirée du dernier article académique ralentit l’équipe
  • Le Haskell de production a besoin d’une culture du pragmatisme
    • Le système de types est un outil électrique, pas une religion
    • En production, il ne convient pas de transformer un problème qui a déjà une bonne solution en occasion d’inventer un nouveau mécanisme
  • Les bénéfices apparaissent avec le temps
    • Un refactoring qui prendrait des semaines dans une codebase à typage dynamique peut être terminé en quelques heures après un changement de type, parce que le compilateur signale tous les call sites
    • Un nouvel ingénieur peut lire les signatures de type et comprendre le contrat d’un module
    • Un incident de production peut ne jamais se produire parce que des états impossibles sont réellement impossibles à représenter
  • Mercury estime que le retour sur investissement se voit non pas en années, mais en quelques mois
    • En particulier dans les services financiers, le coût d’un bug d’intégrité des données ne se mesure pas en mécontentement utilisateur, mais en remarques des régulateurs et en argent appartenant à d’autres
    • Le système de types n’élimine pas le risque, mais il fournit des outils qui rendent plus difficile l’introduction accidentelle de risques dans une codebase en forte croissance
  • La valeur de Haskell en production n’est ni une balle magique ni un mouvement moral ; elle réside dans un ensemble d’outils puissants qui permet même à des équipes aux niveaux de maîtrise variés de Haskell de garder les dispositifs dangereux à l’intérieur de garde-fous, de préserver le savoir opérationnel et de faire du chemin sûr le chemin le plus facile

1 commentaires

 
GN⁺ 3 시간 전
Commentaires sur Hacker News
  • Il est vrai que Haskell fait partie des langages les plus puissants pour imposer ce genre de choses par le type, mais le même modèle fonctionne aussi plutôt bien en Rust et en TypeScript
    J’aime aussi la façon dont on peut empêcher les bugs d’autorisation évidents qui se répètent dans les applications web, avec des flux du type User -> LoggedInUser -> AccessControlledLoggedInUser
    J’ai l’impression que ce modèle est énormément sous-utilisé dans l’industrie

    • Ce n’est pas propre à Rust ou TypeScript, c’est en fait possible dans presque tous les langages
      S’il faut distinguer, pour des raisons de sécurité, une chaîne avant/après échappement, même dans un langage dynamiquement typé on peut l’envelopper dans une classe Escaped et avoir des fonctions comme escape(str)->Escaped, dangerouslyAssumeEscaped(str)->Escaped
      Il y a un coût en performance, donc il faut faire des compromis, mais c’est faisable
      Une autre approche est l’Application Hungarian, mais cela dépend davantage de la discipline du programmeur que du compilateur : https://www.joelonsoftware.com/2005/05/11/making-wrong-code-...
    • C’est moins une question de système de types en soi que d’affordance
      Par exemple, c’est tout à fait faisable en C#, mais on se retrouve souvent avec plus de bruit visuel que de vraie définition de type
    • Rust et TypeScript sont évidemment eux aussi très influencés par Haskell
      Mais ils évitent généralement de le dire explicitement et changent les noms pour éviter l’effet « les monades font peur, il faut écrire un tutoriel »
      L’influence est plus forte du côté des classes de types que des monades
    • Je ne suis pas certain que cela fonctionne vraiment si bien en TypeScript
      En l’absence de typage nominal, il faut retenir des incantations assez bricolées pour fabriquer l’équivalent d’un newtype autour d’un type primitif
      D’après mon expérience, OCaml était plus puissant que Rust pour imposer ce genre de sûreté de typage
      Son expressivité est plus grande avec les GADT, il est plus pratique grâce aux variantes polymorphes et aux types objets / row types d’enregistrements, et il a aussi un système de modules et des foncteurs
      Là où un ramasse-miettes suffit, cela évite aussi les contraintes d’abstraction et les difficultés causées par le borrow checker de Rust
    • C’est la même idée que « rendre les états invalides impossibles à représenter » : https://news.ycombinator.com/item?id=40150159
  • J’ai vraiment adoré travailler en Haskell pendant quelques années
    Je ne le cherchais pas spécialement, mais l’occasion s’est présentée par hasard, et c’était intéressant et intellectuellement stimulant
    Cela dit, malheureusement, même après 3 ans passés uniquement sur Haskell, ma productivité en Rust reste facilement deux fois supérieure
    Haskell a davantage de pièges qu’il faut connaître à l’avance pour les éviter, et selon l’auteur du code, cela peut presque devenir un langage en lecture seule tant il est difficile à assimiler
    La toolchain est souvent couplée à Nix, qui est lui-même un monstre complexe, les extensions de langage donnent l’impression d’être disséminées partout
    Les fichiers Cabal ne sont pas terribles non plus, et il faut du temps pour s’habituer aux erreurs du compilateur

    • Assez étonnamment, mon expérience a été presque l’exact opposé
      Sur mon dernier produit, on a commencé à migrer le backend de Typescript vers Rust, parce qu’on en avait assez des crashs
      Aujourd’hui, je considère cela comme l’une des plus grosses erreurs techniques que j’aie commises, tellement la productivité a ralenti
      Un exemple de temps perdu spécifique à Rust : écrire une fonction d’ordre supérieur qui ouvre une connexion à la base de données, fait quelque chose puis la ferme est trivial en Haskell, TypeScript, JavaScript, C++ ou PHP, mais en Rust c’était en pratique impossible au point que, même après avoir demandé à des amis experts Rust, on a fini par abandonner
      J’ai aussi plusieurs fois tenté des refactorings, passé la journée à corriger des erreurs de type, puis atteint une erreur dans le fichier de plus haut niveau avant de réaliser qu’à cause d’un élément fondamental de la conception, tout le refactoring était impossible et qu’il fallait tout annuler
      En plus, Rust est le seul langage moderne auquel je puisse penser où utiliser des valeurs via une interface plutôt qu’un type concret se situe, selon le contexte, quelque part entre technique avancée et impossibilité
      J’en suis donc arrivé à la conclusion qu’en gros, le code applicatif — par opposition au code système ou au code de bibliothèque — ne devrait pas être écrit en Rust
    • Je me demande si la productivité était globalement 2x meilleure, ou s’il y avait aussi des domaines où Rust était moins productif
      Et je me demande aussi ce que signifie « en lecture seule »
  • Contrairement à l’idée reçue, je pense qu’une part non négligeable du succès de Mercury vient peut-être du fait qu’ils ont choisi Haskell et que leurs premiers dirigeants avaient une solide expérience de Haskell
    Du point de vue d’un client de Mercury, cette entreprise est l’une des sociétés centrales de ma boîte à outils, et je n’arrive pas à me défaire de l’impression que le choix de Haskell a amélioré leur progression, leur développement et l’ensemble de leur parcours
    Bien sûr, on pourrait dire cela de la plupart des langages, et cela ne veut pas dire qu’un langage fonctionnel comme Haskell soit une recette du succès
    Mais avoir pris ce type de décision délibérée avant l’ère du « vibe coding » et des LLM paraît particulièrement clairvoyant, surtout combiné à la culture d’ingénierie détaillée dans l’article

    • Il est plus probable que les facteurs de succès soient surtout leur orientation fintech tournée startup et leur capacité d’exécution
      Moi aussi j’aime une bonne culture technique, mais j’ai vu des entreprises dotées d’une excellente culture technique mourir à cause d’un mauvais positionnement business
      On peut même aller plus loin et dire qu’une culture fintech de type startup a peut-être justement favorisé une bonne culture technique
      Comme ils n’ont pas commencé comme une banque, ils n’avaient pas besoin d’être aussi conservateurs ni d’intégrer une pile technique antique et horrible, contrairement par exemple à SVB
      Je suis content qu’ils aient réussi avec Haskell, mais comme pour Jane Street et OCaml, je pense que malgré ce que l’entreprise aimerait faire croire, le choix du langage relève presque de l’accident du point de vue business
      En revanche, je me demande ce qu’ils utilisent pour le frontend. J’imagine que tout ce Haskell est du backend
    • Le fait de recruter des généralistes sans expérience préalable du langage les a peut-être même aidés
      Cela leur permettait d’inculquer la culture et le style dès le départ aux nouveaux arrivants
      Avant l’ère du vibe coding, la plupart de ces gens ne se seraient probablement pas contentés de foncer tête baissée pour bricoler sans consignes
    • J’ai eu le sentiment que tout fonctionne simplement bien dans l’application
      Quand on vient d’un autre service, c’est vraiment appréciable
  • Un très bon ami travaille dans cette entreprise, et même de l’extérieur, leur culture d’ingénierie a l’air bonne
    Haskell me semble être le bon outil pour ce travail, et ils exploitent bien ses points forts, mais j’ai aussi l’impression qu’une grande part de leur succès vient peut-être simplement du fait que l’entreprise est globalement bien gérée

    • C’est aussi l’impression que j’ai eue en lisant l’article
      Cet auteur donnerait sans doute naissance à une organisation d’ingénierie performante quel que soit le langage utilisé
    • Cela ne contredit pas non plus l’idée répandue selon laquelle utiliser un langage de programmation fonctionnelle filtre vers un vivier de talents / de candidats de meilleure qualité
  • Je lis en ce moment Real-World OCaml, et même si je connaissais déjà quelques notions, j’en apprends davantage sur la programmation fonctionnelle
    On dirait qu’elle permet de construire des morceaux de logiciel étonnamment robustes
    Mais cela me fait aussi réfléchir
    Le backend du produit actuel tourne sur NiceGUI et remplit bien son rôle
    Le code est raisonnable, en MVVM, et la chose la plus importante consiste à se connecter à des websockets selon le client pour consommer les données et afficher les analyses
    Le nombre de clients ne sera pas énorme, et le site n’aura probablement que quelques dizaines, au mieux quelques centaines de visiteurs
    Je veux aussi un REPL ou du hot reload, mais je sais qu’à mesure que les fonctionnalités augmentent — panneau d’administration des utilisateurs, davantage d’analyses, etc. — la programmation fonctionnelle peut bien convenir aux transformations de pipelines de données
    Cela dit, Haskell et OCaml sont des langages statiques
    Si je veux quelque chose de dynamique plus tard, tout en grossissant et en passant à l’échelle, Clojure ou Elixir me semblent être de bons choix
    En même temps, j’ai peur qu’un futur besoin de refactoring casse tout
    Pour l’instant, j’utilise Python avec Mypy, et NiceGUI génère le frontend côté backend

    • Je ne connais pas OCaml, mais en Haskell on peut recharger très rapidement une appli web en cours de développement avec ghci / cabal repl
      Honnêtement, je pense que beaucoup d’utilisateurs de Haskell n’en tirent pas assez parti
  • J’ai déjà travaillé sur un système similaire avec un langage relativement marginal, Scheme puis plus tard Racket ; il a grossi, mais une petite équipe a pu le maintenir longtemps tout en gardant un bon rythme
    On n’introduisait pas beaucoup de bugs, et on pouvait en général ajouter des fonctionnalités très vite
    Par exemple, on a été les premiers à obtenir une certaine certification pour héberger des données sensibles sur AWS
    Parfois, l’ajout de fonctionnalités était plus lent parce qu’il fallait construire from scratch ce que, sur une plateforme populaire, on aurait résolu avec des composants existants
    Mais une fois construit, cela fonctionnait bien, on retrouvait notre vitesse d’avant, et on n’était pas ralentis par l’embonpoint et la complexité de dizaines de frameworks tout faits
    Comme on contrôlait entièrement une plateforme maîtrisable, on pouvait aussi migrer rapidement vers AWS quand le besoin s’est présenté
    Le système disposait aussi dès le départ d’une recette d’architecture pour les données complexes et les interactions web, ce qui a permis de développer rapidement beaucoup de fonctionnalités et a ensuite continué à pousser les choses dans une direction intelligente
    La différence avec cette fintech en Haskell, c’est que l’équipe était extrêmement petite
    Il n’y avait en général que 2 ou 3 ingénieurs logiciel à la fois, plus une personne qui gérait toute l’exploitation
    On n’avait donc pas à affronter la difficulté de coordonner des centaines de personnes tout en gardant un système cohérent
    En général, l’un s’occupait des changements de code plus techniques et architecturaux, et l’autre ajoutait rapidement des fonctionnalités pleines de logique métier volumineuse pour des processus complexes
    En utilisant prudemment les outils d’IA de type LLM actuels ou à venir, j’ai l’impression qu’on pourrait retrouver une partie de l’efficacité de très petites équipes extraordinairement performantes, même en développement logiciel
    Le modèle qui me vient à l’esprit n’est pas de produire une énorme lourdeur pour faire disparaître les story points et laisser la soutenabilité à la charge de quelqu’un d’autre, mais plutôt quelques penseurs très affûtés qui maintiennent le système sur une trajectoire à la fois puissante et maîtrisable

  • C’est une arme à double tranchant
    2 millions de lignes est un accomplissement remarquable, mais c’est aussi une charge de maintenance considérable
    Les avantages de Haskell sont théoriquement clairs, mais ses inconvénients sont plus difficiles à saisir intuitivement
    La tentation est de tout modéliser dans les types
    La base de code elle-même finit par devenir non pas l’application, mais la spécification métier
    Chaque changement de politique devient un gros refactoring, et grâce à la sûreté de Haskell cela peut demander étonnamment beaucoup de travail
    En fin de compte, on ne peut pas tout avoir, et tôt ou tard on finit prisonnier des types
    Haskell est vraiment impressionnant et puissant, surtout à cette échelle, mais il apporte aussi ses propres problèmes
    La tentation de modéliser la logique métier dans les types peut produire une structure rigide, et la sûreté qu’elle apporte peut empêcher de voir d’autres formes de risque

    • Si les parties centrales sont conçues par des ingénieurs expérimentés avec du goût, on peut assez bien marcher sur cette ligne de crête
      On ne peut pas tout avoir, mais on peut en avoir beaucoup
      J’ai fait un stage chez Jane Street il y a quelques années ; ce n’était pas Haskell mais OCaml, et ils semblaient très bien tenir cet équilibre
      C’est un domaine à la complexité intrinsèque élevée, où fiabilité et exactitude sont directement liées à la survie de l’entreprise, et pourtant ils avançaient étonnamment vite
      Avec le recul, le point clé chez Jane Street était d’avoir recruté des programmeurs OCaml expérimentés et dotés d’un excellent goût, comme Stephen Weeks, puis de leur avoir confié dès le départ la construction des bibliothèques centrales et l’orientation de toute la base de code
      Malheureusement, Mercury n’a pas aussi bien réussi sur cet aspect
    • C’est pareil en TypeScript : https://www.richard-towers.com/2023/03/11/typescripting-the-...
      Honnêtement, le plus gros défaut d’un système de types Turing-complet est qu’en théorie on peut implémenter une application qui se réduit en poussière à la compilation
  • Un cas similaire de succès de Haskell chez Bellroy sera justement le sujet d’un meetup Melbourne Compose à venir : https://luma.com/uhdgct1v

  • Le problème que je rencontre avec la programmation fonctionnelle, c’est le débogage
    Plus précisément, j’y vois la force de la programmation impérative, surtout dans sa forme procédurale
    Dans un style fonctionnel / déclaratif, on décrit généralement l’état dans lequel quelque chose doit se trouver plutôt que la façon dont il est produit, et le langage assemble tout pour donner le résultat final
    Tant que tout est bien fait, c’est très bien et peut-être même meilleur, mais quand ce n’est pas le cas et qu’on n’obtient pas le résultat attendu, la question est de savoir comment trouver le bug
    Dans un langage comme C, c’est relativement simple
    On suit ligne par ligne, on observe l’état d’exécution entre chaque étape — en gros la RAM — et si cela diffère de ce qu’on attend, alors il y a quelque chose de faux à cette ligne, on rentre dedans et on continue ainsi
    Plus le langage essaie de masquer l’état, comme en programmation fonctionnelle, plus cela devient difficile
    Il est d’ailleurs intéressant que la plus longue section de l’article porte précisément sur ce problème, à savoir « design for introspection »
    L’auteur a dû fournir volontairement beaucoup d’efforts pour rendre le code débogable, ce qui apporte un bon éclairage sur l’usage pratique, souvent négligé, de Haskell

    • Mon astuce de débogage consiste à faire en sorte que tout code ayant la moindre importance renvoie la même sortie pour la même entrée
      Même pour le code trivial
      Les autres langages grand public n’arrivent même pas à s’en approcher
      Dans les cas où cela ne marche pas, comme la concurrence à mémoire partagée, j’utilise des transactions
      Là encore, les autres langages grand public n’arrivent pas à s’en approcher
      Et je ne parle même pas d’avantages simples comme l’absence de null ou l’absence de conversions implicites d’entiers
      Il est tout à fait vrai que déboguer du code Haskell est plus difficile que dans d’autres langages
      Mais quand on élimine les 90 % inférieurs des pièges habituels, c’est forcément ce qui arrive
    • Le débogage en programmation fonctionnelle est souvent piloté par le REPL, contrairement à la programmation impérative
      Bien sûr, ce n’est pas propre au fonctionnel, et même dans des langages surtout impératifs comme Python ou JavaScript, on utilise souvent comme première couche de débogage le shell Python, la console du navigateur, le shell Node/Deno/Bun, les notebooks, etc.
      Le débogage centré sur le REPL implique des compromis intéressants
      Dans un langage comme C, on part souvent du programme entier et de points d’arrêt, en essayant de deviner l’endroit exact où le problème est susceptible de se trouver
      Dans un monde centré REPL, on cherche davantage à rendre les composants du programme directement testables depuis le REPL
      Les frontières entre modules / API / types finissent donc par ressembler aux frontières du débogage
      Il peut alors exister une pression plus forte qu’en langage impératif comme C/C++ pour que ces frontières soient bonnes et faciles à utiliser
      À l’inverse, par rapport à un débogage d’abord centré sur le programme entier, il devient parfois plus difficile d’isoler les problèmes complexes d’intégration entre unités dans des scénarios réels un peu tordus
      Mais l’approche REPL-first pousse souvent à minimiser la surface d’intégration, si bien que dans les langages fonctionnels on voit parfois moins ces effets d’intégration que dans les langages impératifs
      Dire que les langages fonctionnels cachent l’état n’est pas vraiment juste
      Eux aussi s’exécutent sur du matériel impératif et manipulent un état matériel réel
      À un certain niveau, il y a une traduction entre les deux mondes, et ce n’est probablement pas si différent qu’on pourrait le croire
      En cas de besoin, on peut toujours revenir à des points d’arrêt impératifs et à un débogueur impératif
      C’est pour cela que j’appelle cela du débogage « piloté par le REPL »
      Le REPL permet de réduire le problème jusqu’à l’unité fautive, c’est-à-dire le module / l’API / la fonction exacte et l’entrée qui produit une sortie surprenante
      Si le bug n’apparaît toujours pas en lisant le code source, on peut alors passer au débogueur impératif pour retrouver quasiment la même expérience de pas-à-pas ligne par ligne, avec en plus du contexte
      À ce stade, le REPL a déjà tellement réduit le périmètre que l’unité elle-même est petite et étroite, donc il n’est probablement même plus nécessaire de choisir de bons points d’arrêt
      Je pense que le message retenu de la section « design for introspection » est erroné
      Cette section ne parlait pas de débogabilité, mais d’observabilité
      Il était question de bien câbler les systèmes de logs / télémétrie, de mocker des faux pendant les tests et d’ajouter retries / coupe-circuits au niveau du système entier au lieu de laisser cela à chaque bibliothèque individuellement
      Dans le monde impératif aussi, ce n’est pas un problème de débogage mais de décomposition : injection de dépendances, installation de middleware, usage d’interfaces abstraites plutôt que de classes concrètes aux frontières de l’API publique
      Ces propositions de conception relèvent du refactoring, et affectent moins la débogabilité que la facilité avec laquelle on peut installer du middleware d’observabilité sur l’API publique de quelqu’un d’autre
  • J’ai du mal à imaginer ce que peuvent bien faire 2 millions de lignes de Haskell
    Cela fait vraiment beaucoup de code, alors que Haskell a la réputation d’être un langage « dense » capable de faire beaucoup avec peu
    Peut-être est-ce dû au grand nombre de bibliothèques pour la sérialisation / désérialisation JSON, les frameworks d’API REST, le logging, etc.

    • D’après l’article original, le problème est qu’un code qu’on ne peut pas instrumenter n’est pas digne de confiance
      Si un binding tiers fait des appels HTTP via des fonctions concrètes, il n’y a aucun moyen d’ajouter du tracing, aucun moyen d’injecter des timeouts alignés sur les SLO, aucun moyen de simuler une panne partenaire en test, et aucun moyen d’expliquer un trou de 400 ms dans une trace autrement qu’en échafaudant des théories
      Ils l’ont donc écrit eux-mêmes
      Cela demande plus de travail au départ, mais comme leurs clients maison ont été conçus ainsi dès l’origine, ils sont pensés pour l’observabilité
    • La propriété que vous appelez « dense » est généralement appelée forte expressivité
      Cela signifie qu’on peut exprimer des idées relativement très abstraites avec peu de caractères
      Certaines personnes appellent aussi cela un langage « de haut niveau »
      Cela dit, je ne pense pas que 2 millions de lignes soient tant que cela quand on l’entend pour la première fois
      Surtout pour une entreprise accumulant du code depuis des années dans un domaine fortement régulé comme la finance
    • Ce n’est pas une mesure objective du tout, mais j’ai eu l’impression que Haskell avait simplement un rapport hauteur/largeur différent
      Le nombre de lignes peut être un peu plus faible, mais le nombre de mots reste globalement proche de celui de langages OO plus impératifs
    • Je ne sais pas à quoi ressemble réellement leur base de code, mais la réputation de concision de Haskell vient en partie d’une surreprésentation du monde académique ou de la théorie des catégories
      Dans ces milieux, des expressions comme St M -> C T passent très bien, mais dans un logiciel réel, écrire TransactionState Debit -> Verified Transaction est bien plus utile
      Une autre part vient d’un facteur culturel qui remonte jusqu’au LISP
      Les gens ont tendance à vouloir être trop malins pour économiser des lignes avec des astuces ou des macros difficiles à comprendre
      Dans une entreprise financière comme Mercury, j’imagine qu’on encourage plutôt la clarté et la lisibilité que ce genre de pratique
      Par exemple, un linter peut imposer de découper le code monadique en expressions do détaillées sur plusieurs lignes au lieu de tout écrire sur une seule avec >> et >>=