- 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,textouvector, il existe des implémentations internes avec allocations mutables, écritures dans des buffers ou unsafe coercion - La monade
STutilise des modifications en place observables et des effets de bord à l’intérieur du calcul, mais le type rank-2 derunSTempêche les références mutables créées en interne de s’échapperrunST :: (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
IOdu 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
- un solde insuffisant doit être
- 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 deDynamic - 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
- les types se rapprochent de
-
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
- les invariants qui empêchent une corruption silencieuse gagnent à être encodés dans les types
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
sendRequestavec une instrumentation de timing et renvoyer un nouveauHttpClient - 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 -> Applicationdans WAI, ce pattern qui rend les transformations de comportement composables est très utile en exploitation
- La solution la plus courante consiste à exposer des enregistrements de fonctions plutôt que des fonctions concrètes
-
Des intercepteurs composés avec
Monoid- Les types de middleware et d’interceptor peuvent généralement avoir des instances
SemigroupetMonoid - Le
Middlewarede WAI est un endomorphisme, et les endomorphismes forment un monoïde sous la composition avecid - Les enregistrements de hooks d’interceptor peuvent être composés champ par champ, ce qui permet de combiner via
mconcatdes préoccupations comme le tracing, les timeouts ou la réécriture de task queues, sans plomberie supplémentaireappTemporalInterceptors = 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
- Les types de middleware et d’interceptor peuvent généralement avoir des instances
-
Systèmes d’effets
- Les effect systems comme
effectful,polysemy,fused-effectsetcleffoffrent 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
- Les effect systems comme
-
L’exemple positif de
persistent- Le
SqlBackenddepersistentest un enregistrement de fonctions avec des éléments commeconnPrepare,connInsertSql,connBegin,connCommitetconnRollback - 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
- Le
-
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-clientne 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-apicomme dépendance et de placer des spans autour des opérationsIOcritiques 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
stdoutoustderr, 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
- Au lieu d’importer un framework de logging pour écrire directement sur
- Exposer des modules
.Internalpeut 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
.Internalavec un avertissement explicite sur leur stabilité peuvent valoir mieux que de forker un package et le vendoriser containers,textetunordered-containerssont 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
unsafePerformIOest utilisé au sein de bibliothèques dont on dépend au quotidienbytestringettextallouent 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
amountest 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
- si le champ
- 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,
errorpromis 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
Stringau lieu deText, 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
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
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)->EscapedIl 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-...
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
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
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
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
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
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
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
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
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
Cet auteur donnerait sans doute naissance à une organisation d’ingénierie performante quel que soit le langage utilisé
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
cabal replHonnê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
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
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
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
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.
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é
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
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
Dans ces milieux, des expressions comme
St M -> C Tpassent très bien, mais dans un logiciel réel, écrireTransactionState Debit -> Verified Transactionest bien plus utileUne 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
dodétaillées sur plusieurs lignes au lieu de tout écrire sur une seule avec>>et>>=