Ein paar Millionen Zeilen Haskell: Production Engineering bei Mercury
(blog.haskell.org)- Mercury betreibt mit einer Haskell-Codebasis von rund 2 Millionen Zeilen ohne Kommentare u. Ä. Banking-Services für mehr als 300.000 Unternehmen und verarbeitete 2025 ein Transaktionsvolumen von 248 Milliarden US-Dollar sowie einen annualisierten Umsatz von 650 Millionen US-Dollar
- Der Wert von Haskell bei Mercury liegt weniger in der Purität an sich, sondern darin, Betriebswissen in APIs und Typen zu verankern, riskante Operationen hinter schmale Grenzen zu stellen und den sicheren Weg zum einfachsten Weg zu machen
- Zuverlässigkeit wird nicht als vollständige Verhinderung von Ausfällen verstanden, sondern als Fähigkeit des Systems, Variabilität zu absorbieren; das Typsystem schließt Fehlerklassen aus und hinterlässt institutionelles Wissen als Dokumentation, die der Compiler durchsetzt
- Mercury nutzt Temporal als Framework für durable execution bei Retries, Timeouts, Abbrüchen und Crash-Recovery in Finanz-Workflows und hat das Haskell-SDK
hs-temporal-sdkals Open Source veröffentlicht - Der Produktionswert von Haskell besteht nicht darin, alles in Typen zu pressen, sondern Invarianten, die zu Datenverlust, Finanzfehlern oder regulatorischen Problemen führen könnten, per Typen zu schützen, dabei aber Komplexität zu kapseln und das Ganze zusammen mit Tests, Dokumentation und Code-Reviews zu betreiben
Haskells Betriebsgröße bei Mercury und die Perspektive auf Zuverlässigkeit
- Mercury betreibt eine Haskell-Codebasis von rund 2 Millionen Zeilen ohne Kommentare u. Ä.
- Mercury ist ein Fintech-Unternehmen, das Banking-Services für mehr als 300.000 Unternehmen anbietet, und verarbeitete 2025 ein Transaktionsvolumen von 248 Milliarden US-Dollar sowie einen annualisierten Umsatz von 650 Millionen US-Dollar
- Das Unternehmen hat etwa 1.500 Mitarbeitende, und die Engineering-Organisation stellt überwiegend Generalist:innen ein; die meisten hatten vor ihrem Einstieg noch nie mit Haskell gearbeitet
- Dieses System läuft seit Jahren durch schnelles Wachstum, die SVB-Krise mit 2 Milliarden US-Dollar an neuen Einlagen innerhalb von fünf Tagen, regulatorische Prüfungen sowie gewöhnliche und ungewöhnliche Situationen eines großen Finanzsystems
Zuverlässigkeit ist nicht Ausfallvermeidung, sondern die Fähigkeit, Variabilität zu absorbieren
- Traditionelle Ansätze zur Zuverlässigkeit konzentrieren sich darauf, Ausfälle aufzulisten, zusätzliche Prüfungen und Tests einzubauen und Bugs zu finden, doch das allein reicht nicht aus
- Mercury behandelt Zuverlässigkeit als die Fähigkeit eines Systems, Variabilität zu absorbieren
- Das System muss sich kontrolliert verschlechtern können
- Operatoren müssen das System verstehen und anpassen können
- Die Architektur muss das Richtige einfach und das Falsche schwer machen
- In schnell wachsenden Organisationen werden reale Betriebsfragen daraus, ob neu hinzugekommene Engineers ein Modul lesen und verstehen können, ob ein langsamer werdender Datenbankdienst andere Services mit in den Zusammenbruch zieht und ob der Compiler Fehlgebrauch von Schnittstellen erkennt
- Das Typsystem ist eher eine Betriebshilfe als ein bloßer Korrektheitsbeweis
- Es schließt bestimmte Fehlerklassen aus
- Es hinterlässt institutionelles Wissen in einer Form, die der Compiler lesen kann, auch nachdem die Autorin oder der Autor das Unternehmen verlassen hat
- Es dient als Dokumentation, die konsistenter durchgesetzt wird als ein Wiki
- Stability Engineering bei Mercury ist keine Qualitäts-Polizei, die die Produktentwicklung verlangsamt, sondern eine Form der Zusammenarbeit, bei der die Auswirkungen kaputter Funktionen von Beginn des Designs an berücksichtigt werden
- Blast Radius im Fehlerfall
- Welche Arbeiten Idempotenz benötigen und auf welche Weise
- Die Form des Rollbacks
- Die Behandlung laufender Arbeiten
- Die frühzeitige Unterscheidung zwischen Systemen, die Fehler absorbieren, und solchen, die sie verstärken
Purität ist keine Eigenschaft der Sprache, sondern eine Schnittstellengrenze
- Haskells Purität bedeutet nicht, dass es im Inneren überhaupt keine Seiteneffekte gibt, sondern eher, dass die Schnittstelle eine Grenze bildet, die das Auslaufen von Seiteneffekten verhindert
- Hinter den reinen Funktionen von Bibliotheken wie
bytestring,textundvectorstehen interne Implementierungen mit veränderbaren Allokationen, Buffer-Schreibvorgängen und unsafe coercion - Die
ST-Monad verwendet beobachtbare In-place-Mutationen und Seiteneffekte innerhalb einer Berechnung, aber der Rank-2-Typ vonrunSTverhindert, dass intern erzeugte veränderbare Referenzen nach außen entkommenrunST :: (forall s. ST s a) -> a - Intern ist imperatives Verhalten möglich, nach außen kommt jedoch nur das Ergebnis, und veränderbarer Zustand tritt nicht über die Grenze hinaus aus
- Dieses Prinzip wird auf das gesamte Betriebssystem angewandt
- Die Datenbankschicht kann intern Connection Pooling, Retries und veränderbaren Zustand verwenden
- Caches können nebenläufige veränderbare Maps verwenden
- HTTP-Clients können Circuit Breaker, Connection Pools und viel Bookkeeping haben
- Entscheidend ist, riskante Operationen in schmale Schnittstellen zu kapseln und Fehlgebrauch schwierig zu machen
- In realen Systemen besteht das Ziel nicht darin, Veränderung vollständig zu vermeiden, sondern klarzumachen, wo Veränderung stattfindet, und zu begrenzen, wer in der Codebasis davon wissen muss
Das Richtige zum Einfachen machen
- In großen Codebasen entstehen häufig Muster, bei denen Korrektheit von einer bestimmten Reihenfolge oder unsichtbaren zusätzlichen Schritten abhängt
- Nach einer Transaktion muss das Audit-Log geflusht werden
- Vor dem Aufruf eines Endpoints muss ein Feature Flag geprüft werden
- Das Enqueueing einer Benachrichtigung muss innerhalb der Datenbanktransaktion erfolgen
- Wenn dieses Betriebswissen nur in Wikis, Onboarding-Dokumenten, alten Design-Reviews, Slack-Threads oder in der Erinnerung einiger Senior-Engineers steckt, verschwindet es schnell
- Haskell kann solche Abläufe in Typen kodieren, damit man sie nicht vergessen kann
- Der schlechte Weg ist, darum zu bitten, die richtige Funktion zu verwenden, aber Umgehungswege offen zu lassen
-- 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 ()- Der bessere Weg ist, die Typen so umzubauen, dass der einzige Pfad zur Ausführung der Arbeit auch die Veröffentlichung von Events einschließt
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 - Hier beweist das Typsystem weniger tiefe Aussagen über Events, sondern macht das richtige Betriebsverfahren zum einfachsten Weg
- Wenn neue Engineers fragen, wie man eine Transaktion schreibt, geben Typsignaturen und die öffentliche API die Antwort, und das Wissen bleibt erhalten, auch wenn Senior-Engineers das Unternehmen verlassen
Dauerhafte Ausführung und Temporal
- Workflows in Finanzsystemen bleiben nicht innerhalb einer einzelnen Transaktion
- Zahlungsübermittlung
- Warten auf Freigabe durch Partner
- Aktualisierung des Hauptbuchs
- Benachrichtigung der Kunden
- Behandlung von Stornierungen und Timeouts
- Fälle, in denen der Partner erfolgreich war, der Worker aber vor dem Protokollieren starb
- Fälle ohne Antwort aufgrund von Netzwerkproblemen
- Solche Abläufe benötigen Zustand, Retries, Timeouts, Idempotenz und eine Ausführung, die Prozess-Crashes und Deployments überdauert
- Mercury koordinierte solche Prozesse früher mit datenbankbasierten Zustandsmaschinen, cron-Jobs, Background-Workern sowie über den Code verteilten Retries und Timeout-Behandlungen
- Das funktionierte, war aber fragil, schwer zu verstehen und eine überproportionale Ursache für Betriebsstörungen
- Temporal ist bei Mercury das Framework für durable execution, in dem Workflows wie gewöhnlicher sequentieller Code geschrieben werden und die Plattform jeden Schritt in einer Event-Historie aufzeichnet
- Wenn ein Worker mitten im Workflow crasht, kann ein anderer Worker den deterministischen Prefix replayen, den Zustand rekonstruieren und ab dem Unterbrechungspunkt fortfahren
- Retries, Timeouts, Stornierungen und Fehlerbehandlung werden von der Plattform bereitgestellt, statt von jedem Team separat neu implementiert zu werden
- Ein Temporal-Workflow hat einen Charakter ähnlich einer reinen Funktion über einer Event-Historie
- Ein gereplayter Workflow muss dieselbe Befehlssequenz erzeugen wie der ursprüngliche
- Diese Determinismus-Anforderung ähnelt der Einschränkung reinen Codes: gleicher Input, gleicher Output
- Seiteneffekte werden in Activities isoliert, die dem
IOdes Workflows entsprechen
- Mercury entwickelte das Haskell-SDK
hs-temporal-sdk, das das offizielle Core SDK von Temporal über Rust FFI kapselt, und veröffentlichte es als Open Source - Das Einführungsmuster von Temporal wurde auch im Temporal-Replay-Konferenzvortrag behandelt; Mercury erzielte operative Verbesserungen, indem es fragile Ketten aus cron-Jobs und Zustandsmaschinen durch durable Workflows ersetzte
Die Domäne in Geschäftssprache entwerfen, nicht in der Sprache der Transportebene
- Ein häufiger Fehler in gewachsenen Systemen ist, dass Konzepte des aufrufenden Systems in das Domänenmodell durchsickern
- Wenn Code, der für HTTP-Request-Handler geschrieben wurde, später in cron-Jobs, queue-basierten Background-Workern oder Temporal-Workflows wiederverwendet wird, können HTTP-Ausnahmen wie
StatusCodeException 409 "Conflict"in Nicht-HTTP-Kontexte propagiert werden - Bei einem cron-Job gibt es keinen Aufrufer, der auf eine 409-Antwort wartet, und der Statuscode zieht geschäftliche Bedeutung auf die falsche Ebene
- Die Lösung besteht darin, Domänenfehler als Domänentypen zu modellieren
- Unzureichendes Guthaben sollte
InsufficientFundssein - Eine doppelte Anfrage sollte
DuplicateRequestsein - Ein Partner-Timeout sollte
PartnerTimeoutsein
- Unzureichendes Guthaben sollte
- An jeder Grenze gibt es eine dünne Transformationsschicht
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 - Belange der Transportebene sollten am Rand bleiben, und das Domänenmodell sollte keine HTTP-Statuscodes mit sich herumtragen, egal ob es von Web-Handlern, CLI, cron-Jobs, Background-Workern oder einer Workflow-Engine aufgerufen wird
Kosten und sinnvolles Maß bei der Typkodierung
- Invarianten in Typen abzubilden ist mächtig, bringt aber Kosten in Form von kognitiver Last, Starrheit und Schwierigkeiten bei geänderten Anforderungen mit sich
- Wenn ein Verstoß zu Datenverlust, Finanzfehlern, regulatorischen Problemen oder Bereitschaftsvorfällen führt, sind die Kosten der Typkodierung gerechtfertigt
- Wenn der einzige Grund ist, dass man es gerade so macht oder Techniken auf Type-Level ausprobieren möchte, wird die Codebasis wahrscheinlich schwerer veränderbar
-
Die Seite mit zu viel Kodierung
- Illegale Zustände sind nicht darstellbar, und die Domäne ist in den Typen präzise modelliert
- Änderungen an Geschäftsregeln führen zu Typänderungen quer durch 50 Module, wodurch Refactorings langwierig werden
- Für neue Engineers werden Typsignaturen schwerer verständlich
-
Die Seite ohne jede Kodierung
- Typen nähern sich
String,IO ()oder im schlimmsten FallDynamican - Der Code lässt sich leicht ändern, aber es gibt keine Verträge, und die Bedeutung hängt von der Erinnerung der ursprünglichen Autoren ab
- Wenn diese Autoren gehen, ist schwer zu verstehen, warum das System nicht mehr funktioniert
- Typen nähern sich
-
Nützliche Maßstäbe
- Invarianten, die stille Beschädigung verhindern, sollten eher in Typen kodiert werden
- Transaktionen, die ohne Event committet wurden
- Zahlungen, die ohne Audit-Log verarbeitet wurden
- Zustandsübergänge, die oberflächlich möglich, semantisch aber unmöglich sind
- Invarianten, die laut scheitern, können oft ausreichend durch Laufzeitprüfungen mit guten Fehlermeldungen abgedeckt werden
- 500-Antworten
- fehlgeschlagene Assertions
- Typinkompatibilität an einer JSON-Grenze
- Der Impuls, die gesamte Domäne in Typen zu modellieren, sollte gebremst werden
- Eine Domäne enthält Ausnahmen, Regeln zur Abwärtskompatibilität, einander widersprechende Regeln und Sonderverhalten für bestimmte Kunden
- Typen sind ein Werkzeug nicht nur für den Compiler, sondern für das Team
- Sie sollten zusammen mit Tests, Dokumentation, Code-Reviews, Beispielen und Playbooks eine Verteidigungsschicht bilden
- Innerhalb von Mercury gibt es auch Bibliotheken, die komplexe Type-Level-Konstrukte wie GADTs, Type Families und Phantom-Typen zur Verfolgung von Zustandsübergängen verwenden
- Diese Komplexität ist dort notwendig, wo bei Fehlern Geld falsch bewegt wird oder regulatorische Invarianten verletzt werden
- Entscheidend ist, die Komplexität zu kapseln
- Ein Modul, das eine Type-Level-Zustandsmaschine implementiert, sollte nur wenige Autoren haben, die es tief verstehen, sowie ausreichend Tests
- Die API für die Nutzerseite sollte wie einige wenige Funktionen mit gewöhnlichen Typen aussehen
- Ein Product Engineer sollte es sicher aufrufen können, ohne die internen Type-Level-Beweiskonstrukte zu kennen
- Wenn ein PR, der andere Module anfasst, im Code-Review voller kopierter Typannotationen ist, die nur den Compiler besänftigen sollen, ist das ein Zeichen dafür, dass die Abstraktion über ihre Grenze hinaus durchsickert
- Invarianten, die stille Beschädigung verhindern, sollten eher in Typen kodiert werden
Für Introspektionsfähigkeit entwerfen
- Wenn Zuverlässigkeit Anpassungsfähigkeit ist, dann ist Introspektionsfähigkeit eine der Methoden, um diese Fähigkeit zu erlangen
- Betreiber können nicht betreiben, was sie nicht sehen können, und Teams fällt es schwer, sich an Systeme anzupassen, deren Inneres undurchsichtig ist
- In Haskell gibt es kein Monkey Patching, daher ist es schwierig, zur Laufzeit den internen HTTP-Client einer Bibliothek auszutauschen oder Datenbankaufrufe durch Funktionen zu ersetzen, die OpenTelemetry-Spans erzeugen
- Rust hat dieselbe Einschränkung, aber das Rust-Ökosystem hat sich auf das
tower-Middleware-Muster angenähert, während das Haskell-Ökosystem in mehrere Ansätze aufgeteilt ist - Wenn eine Bibliothek nur ein Bündel konkreter Top-Level-Funktionen offenlegt, muss man zur Instrumentierung ein neues Modul darum herum bauen und darauf hoffen, dass Leute dieses Modul statt des ursprünglichen importieren
-
Funktions-Records
- Die am häufigsten verwendete Lösung ist, statt konkreter Funktionen Funktions-Records offenzulegen
-- 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 } - Auf diese Weise kann man
sendRequestmit Timing-Instrumentierung umhüllen und einen neuenHttpClientzurückgeben - Querschnittsthemen wie Fault Injection für Tests, Austausch durch Mocks, Retries, Tracing, Request-Rewrites oder mandantenspezifisches Verhalten lassen sich zur Laufzeit hinzufügen
- Ein Muster wie bei WAI mit
type Middleware = Application -> Application, das Verhaltensumwandlungen komponierbar macht, ist operativ sehr nützlich
- Die am häufigsten verwendete Lösung ist, statt konkreter Funktionen Funktions-Records offenzulegen
-
Mit
Monoidkomponierbare Interceptors- Middleware- und Interceptor-Typen können in der Regel
Semigroup- undMonoid-Instanzen haben Middlewarein WAI ist ein Endomorphismus, und Endomorphismen bilden unter Komposition undidein Monoid- Interceptor-Hook-Records lassen sich feldweise komponieren, sodass sich Anliegen wie Tracing, Timeouts oder Task-Queue-Rewrites ohne separate Verdrahtung mit
mconcatzusammenführen lassenappTemporalInterceptors = mconcat [ retargetingInterceptor , otelInterceptor , sentryInterceptor , sqlApplicationNameInterceptor , loggingContextInterceptor , statementTimeoutInterceptor , teamNameInterceptor , clientExceptionInterceptor , workflowTypeNameInterceptor ] - Jeder Interceptor behandelt in einem unabhängigen Modul nur ein einziges Anliegen, überschreibt von
memptyaus nur die benötigten Felder, und die Reihenfolge wird in der Liste festgelegt
- Middleware- und Interceptor-Typen können in der Regel
-
Effekt-Systeme
- Effekt-Systeme wie
effectful,polysemy,fused-effectsundcleffbieten ebenfalls einen anderen Weg - Verfügbare Operationen werden als Effekt-Typen definiert, und Interpreter für Production, Tests und Tracing lassen sich am Aufrufpunkt austauschen
- Effekte können abgefangen werden, um Metriken zu erfassen oder Verzögerungen einzuspeisen, und dann wieder an den eigentlichen Handler weitergeleitet werden
- Der Nachteil ist zusätzlicher Apparat wie Effekt-Listen auf Typebene, Handler-Stacks und heikle Typfehler
- Funktions-Records sind so einfach, dass neue Engineers sie an einem Nachmittag verstehen können
- Effekt-Systeme wie
-
Ein positives Beispiel aus
persistentSqlBackendinpersistentist ein Funktions-Record mit Funktionen wieconnPrepare,connInsertSql,connBegin,connCommitundconnRollback- Beim Hinzufügen von OpenTelemetry-Instrumentierung konnten relevante Felder umhüllt werden, um allen Datenbankoperationen Tracing-Spans hinzuzufügen
- So wurde Sichtbarkeit in die Datenbankschicht ohne Fork und fast ohne Änderungen am Quellcode erreicht
-
Bibliotheken, die im Betrieb schwierig sind
- Mercury verwendet fast keine auf Hackage veröffentlichten Web-API-Client-Bindings
- Wenn Third-Party-Bindings HTTP-Aufrufe mit konkreten Funktionen ausführen, wird es schwierig, Tracing, SLO-gerechte Timeouts, die Simulation von Partnerausfällen oder 400-ms-Lücken in Traces zu erklären
- Deshalb schreiben sie Clients selbst und machen sie von Anfang an beobachtbar
-
Die Kosten eines kleinen Ökosystems
- Manche Haskell-Bibliotheken sind nicht aufgegeben, bleiben aber wie öffentliche Infrastruktur zurück, für die niemand klar verantwortlich ist und die niemand schnell verbessert
- Alte Interfaces bleiben bestehen, und die Aufnahme neuer Entwürfe für Beobachtbarkeit, Grenzflächendesign und Betriebsfähigkeit kann langsam sein
http-clientunterstützt unmittelbar nur HTTP/1.1; das ist brauchbar genug, aber zu bestimmten Zeitpunkten können Workarounds nötig sein
Operative Anforderungen für Paketautoren
- Bibliotheksautoren sollten Escape Hatches wie Funktions-Records, Effekt-Typen oder Callbacks bereitstellen, damit Nutzer Verhalten ohne Änderungen am Quellcode injizieren können
- Bereits das Hinzufügen von
hs-opentelemetry-apials Abhängigkeit und das Platzieren von Spans um zentraleIO-Operationen hilft Nutzern, die die Bibliothek in Production betreiben- Das API-Paket ist bei Breaking Changes konservativ und so entworfen, dass es inert arbeitet, wenn die Anwendung das OpenTelemetry-SDK nicht initialisiert
- Der Performance-Overhead ist minimal und verursacht in Benutzeranwendungen weder unerwartete Ausnahmen noch Logging
- Der Dependency-Footprint ist noch nicht so klein, wie gewünscht, und wird derzeit verbessert
- In Bibliothekscode sollte nicht direkt geloggt werden
- Statt ein Logging-Framework zu importieren und direkt nach
stdoutoderstderrzu schreiben, sollten Callbacks, Logger-Parameter oder ein Log-Nachrichten-Datentyp bereitgestellt werden, den der Aufrufer weiterleiten kann - Wohin Logs gehen, ist eine Entscheidung, die zur Betriebsumgebung der Anwendung gehört
- Mercury schickt strukturierte Log-Pipelines an den Observability-Stack; wenn eine Bibliothek direkt nach
stderrschreibt, erfordert das neben dem JSON-Lines-Stream eine separate Verdrahtung
- Statt ein Logging-Framework zu importieren und direkt nach
- Auch das Offenlegen von
.Internal-Modulen kann erwogen werden- Die Sorge ist berechtigt, dass Nutzer von internen APIs abhängen und Refactorings dadurch erschwert werden
- Aber die Gewissheit, dass eine öffentliche API bereits alle Anwendungsfälle abdeckt, ist nur selten gerechtfertigt
- Ein
.Internal-Modul mit expliziter Stabilitätswarnung kann besser sein, als Nutzer das Paket forken und vendorisieren zu lassen containers,textundunordered-containerssind gute Beispiele für diesen Ansatz im Haskell-Ökosystem- Andererseits kann das Feedback zu Mängeln der öffentlichen API abnehmen, wenn Nutzer stillschweigend interne Module verwenden, um das Nötige zu lösen
Dinge, die man nicht in Typen abbildet
- Auch in Production-Haskell gibt es unschöne Stellen
unsafePerformIOwird innerhalb von Bibliotheken verwendet, auf die man sich im Alltag verlässtbytestringundtextallokieren intern veränderbare Buffer, schreiben hinein und frieren sie dann ein, um das Ergebnis zu erzeugen- Der Typ sagt nichts darüber aus, was während der Erzeugung passiert ist
- Die Grenze wird durch Konventionen, sorgfältiges Reasoning und Code-Reviews aufrechterhalten
- Wenn typsichere Alternativen die Kosten bei Performance oder Komplexität zu hoch treiben, kann man solche Kompromisse auch selbst schreiben
- Man muss die Invarianten dokumentieren, die der Typ nicht überprüft
- Man sollte die Unbequemlichkeit beibehalten und regelmäßig neu bewerten, ob eine typsichere Alternative praktikabel geworden ist
- Production Haskell bedeutet nicht das Fehlen von Kompromissen, sondern deren disziplinierte Isolierung
- Viele Haskell-Bibliotheken auf Hackage haben nur wenige oder gar keine Tests
- Die Vorstellung „Wenn es kompiliert, funktioniert es“ kann bei kleinem, purem Code und starken Typen manchmal zutreffen
- Auf IO-lastigen Code, Integrationen mit externen Systemen und Code, dessen Bugs eher in der Semantik als in der Struktur liegen, trifft das fast nie zu
- Typen können zwar ausdrücken, dass etwas
Either ParseError Transactionzurückgibt, aber nicht Folgendes- ob das Feld
amountin Cent oder in Dollar geparst wird - ob eine Partner-API ein ausgelassenes Feld anders interpretiert als ein Null-Feld
- ob Retry-Logik in einem bestimmten Zeitfenster eines Schaltjahrs Doppelbelastungen verursacht
- ob das Feld
- In Production baut man Systeme auf solchen Bibliotheken auf und erbt unvalidierte Annahmen, daher muss man das mit Integrationstests auf der eigenen Schicht ergänzen
- Weitere Kompromisse summieren sich ebenfalls: orphan instances, partielle Funktionen, die im Kontext als total gelten,
error, das als unerreichbar versprochen wird, unbeholfene FFI-Wrapper und manuell gepflegte Exception-Hierarchien - Das Ziel ist nicht moralische Reinheit, sondern dass durch Code-Reviews, Dokumentation, Beispiele und Tests klar ist, wo jeder Kompromiss liegt, warum er gemacht wurde und was kaputtgeht, wenn man ihn entfernt
Warum sich Haskell in Production lohnt
- Haskell ist nicht vom ersten Tag an die schnelle Wahl
- Das heutige Ökosystem liefert nicht sofort eine batteries-included Entwicklungsumgebung mit Hot Reloading wie Next.js oder Rails
- Die benötigte Bibliothek kann fehlen, oder sie existiert, wird aber vielleicht von einer einzelnen Person in ihrer spare time gepflegt
- Fehlermeldungen können mitunter äußerst kryptisch sein
- Das Hiring-Problem wird übertrieben
- Mercury-CTO Max Tagher hat öffentlich gesagt, dass Backend-Haskell-Engineer bei Mercury die mit Abstand am leichtesten zu besetzende Rolle im ganzen Unternehmen ist
- Da die Nachfrage nach Haskell-Jobs größer ist als das Angebot, sind die üblichen Hiring-Dynamiken umgekehrt
- Mercury stellt sowohl Menschen mit viel Haskell-Erfahrung als auch ohne jede Erfahrung ein; Letztere werden durch ein 6- bis 8-wöchiges Trainingsprogramm produktiv
- Wenn man morgen 100 Haskell-Experten bräuchte, wäre die Größe des Hiring-Pools tatsächlich ein Problem; wenn man aber bereit ist, gute Generalisten einzustellen und auszubilden, ist es weit weniger realistisch
- Das größere Hiring-Risiko ist nicht die Größe des Pools, sondern die Veranlagung
- Haskell zieht Idealisten an, denen Genauigkeit und Abstraktion wichtig sind, die gern Papers lesen und bestehende Annahmen infrage stellen
- Wenn diese Stärke nicht gesteuert wird, kann sie in Production zur Belastung werden
- Wenn jemand die Datenbankschicht in eine neue relationale Algebra-Kodierung auf Type-Level umschreiben will, einen Merge ablehnt, weil in einem Wegwerfskript statt
TextStringverwendet wurde, oder jedes Design in Richtung eines total rewrite nach dem neuesten Paper ziehen will, verlangsamt das das Team
- Production Haskell braucht eine pragmatische Kultur
- Das Typsystem ist ein Power-Tool, keine Religion
- Probleme, für die es bereits gute Lösungen gibt, als Gelegenheit zur Erfindung neuer Mechanismen zu behandeln, passt nicht zu Production
- Der Ertrag zeigt sich mit der Zeit
- Ein Refactoring, das in einer dynamisch typisierten Codebase Wochen dauern würde, kann nach einer Typänderung in wenigen Stunden erledigt sein, weil der Compiler alle Call Sites zeigt
- Neue Engineers können Typsignaturen lesen und den Vertrag eines Moduls verstehen
- Production-Incidents können ausbleiben, weil unmögliche Zustände tatsächlich nicht darstellbar sind
- Mercury sieht den Return on Investment nicht erst nach Jahren, sondern im Bereich von Monaten
- Gerade in Finanzdienstleistungen werden die Kosten von Datenintegritäts-Bugs nicht in Nutzerbeschwerden gemessen, sondern in regulatorischen Beanstandungen und dem Geld anderer Leute
- Das Typsystem eliminiert Risiken nicht, stellt aber Werkzeuge bereit, die es in einer schnell wachsenden Codebase schwerer machen, Risiken versehentlich einzuführen
- Der Production-Wert von Haskell liegt weder in einer Silver Bullet noch in einer moralischen Bewegung, sondern in einem starken Werkzeugkasten, mit dem Teams mit sehr unterschiedlichen Haskell-Kenntnissen gefährliche Konstruktionen innerhalb von Leitplanken halten, Betriebswissen bewahren und den sicheren Weg zum einfachen Weg machen können
1 Kommentare
Hacker-News-Kommentare
Haskell gehört definitiv zu den stärkeren Sprachen, wenn es darum geht, so etwas über das Typsystem zu erzwingen, aber dasselbe Muster funktioniert auch in Rust und TypeScript ziemlich gut
Ich mag auch die Art, wie man damit offensichtliche Autorisierungs-Bugs verhindert, die sich in Web-Apps ständig wiederholen, etwa mit einem Ablauf wie User -> LoggedInUser -> AccessControlledLoggedInUser
Ich glaube, in der Branche wird dieses Muster viel zu wenig genutzt
Wenn man aus Sicherheitsgründen zwischen Strings vor und nach dem Escaping unterscheiden muss, kann man selbst in dynamisch typisierten Sprachen eine Escaped-Klasse darumlegen und Funktionen wie
escape(str)->EscapedunddangerouslyAssumeEscaped(str)->EscapedbereitstellenDas hat zwar Performance-Kosten, also braucht man Abwägungen, aber es ist möglich
Ein anderer Ansatz ist Application Hungarian, wobei das allerdings stärker von der Disziplin der Programmierer abhängt als vom Compiler: https://www.joelonsoftware.com/2005/05/11/making-wrong-code-...
In C# kann man das zum Beispiel völlig ausreichend machen, aber dort entsteht oft mehr visuelles Rauschen als eigentliche Typdefinition
Sie vermeiden es nur eher, das ausdrücklich zu sagen oder dieselben Begriffe zu verwenden, um Effekte wie „Monaden sind gruselig, also schreibe ich lieber ein Tutorial“ zu vermeiden
Der Einfluss ist stärker bei Dingen wie Typklassen als bei Monaden
Es gibt keine nominalen Typen, daher muss man sich für so etwas wie newtypes um Primitive herum ziemlich hackige Tricks merken
Meiner Erfahrung nach war OCaml beim Erzwingen solcher Typsicherheit sogar mächtiger als Rust
Mit GADTs ist es ausdrucksstärker, mit polymorphen Varianten und Objekt-/Record-Row-Typen bequemer, und es hat außerdem ein Modulsystem und Funktoren
In Bereichen, in denen Garbage Collection völlig ausreicht, vermeidet man außerdem die Abstraktionsbeschränkungen und Schwierigkeiten, die Rusts Borrow Checker mit sich bringt
Ich habe es wirklich geliebt, einige Jahre lang mit Haskell zu arbeiten
Ich hatte nicht absichtlich danach gesucht, aber die Gelegenheit ergab sich zufällig, und es war interessant und intellektuell anregend
Leider war meine Produktivität in Rust selbst nach 3 Jahren nur mit Haskell immer noch leicht doppelt so hoch wie in Haskell
In Haskell gibt es mehr Fallstricke, die man vorher kennen und vermeiden muss, und je nach Autor kann der Code fast wie eine schreibgeschützte Sprache wirken, also schwer verdaulich sein
Die Toolchain ist oft mit Nix verbunden, und Nix selbst ist ein komplexes Monster, während Sprachextensionen überall verstreut scheinen
Cabal-Dateien sind auch nicht toll, und es dauert, bis man sich an Compiler-Fehlermeldungen gewöhnt hat
Beim letzten Produkt habe ich begonnen, das Backend von Typescript auf Rust umzustellen, weil ich die Abstürze leid war
Inzwischen halte ich das für einen der größten technischen Fehler, die ich je gemacht habe, weil die Produktivität massiv eingebrochen ist
Ein Beispiel für Zeitverschwendung, die nur in Rust auftrat: Eine Higher-Order Function zu schreiben, die eine Datenbankverbindung öffnet, etwas damit macht und sie dann wieder schließt, ist in Haskell, TypeScript, JavaScript, C++ und PHP trivial, aber in Rust war es praktisch unmöglich, selbst nachdem ich Rust-experten im Freundeskreis gefragt hatte, sodass ich aufgegeben habe
Außerdem habe ich mehrfach Refactorings angefangen, dann den ganzen Tag Typfehler behoben, schließlich in einer Top-Level-Datei einen Fehler erreicht und festgestellt, dass das gesamte Refactoring wegen eines grundlegenden Teils des Designs unmöglich war, woraufhin ich alles zurückgerollt habe
Darüber hinaus ist Rust die einzige moderne Sprache, die mir einfällt, in der Werte über Interfaces statt über konkrete Typen zu verwenden je nach Situation irgendwo zwischen einer fortgeschrittenen Technik und unmöglich liegt
Daher bin ich zu dem Schluss gekommen, dass man Anwendungscode, also Code, der weder Systemcode noch Bibliothekscode ist, im Wesentlichen nicht in Rust schreiben sollte
Und mich würde auch interessieren, was genau du mit „schreibgeschützt“ meinst
Entgegen der allgemeinen Wahrnehmung könnte es einen nicht unerheblichen Anteil am Erfolg gehabt haben, dass Mercury Haskell gewählt hat und die frühen Führungskräfte reichlich Erfahrung mit Haskell hatten
Aus Sicht eines Mercury-Kunden ist dieses Unternehmen eine der Kernfirmen in meinem Werkzeugkasten, und ich kann das Gefühl nicht abschütteln, dass die Entscheidung für Haskell ihren Fortschritt, ihre Entwicklung und ihre gesamte Reise verbessert hat
Natürlich kann man so etwas über die meisten Sprachen behaupten, und das bedeutet nicht, dass funktionale Sprachen wie Haskell eine Erfolgsformel wären
Aber eine so bewusste Entscheidung noch vor „vibe coding“ und der LLM-Ära wirkt besonders weitsichtig, vor allem in Kombination mit der im Artikel ausführlich beschriebenen Engineering-Kultur
Ich mag gute Technikkultur auch, aber ich habe Firmen mit großartiger Technikkultur an einem schlechten Business-Fokus scheitern sehen
Mehr noch: Eine Startup-artige Fintech-Kultur könnte erst die gute Technikkultur hervorgebracht haben
Da sie nicht als Bank gestartet sind, mussten sie zum Beispiel im Gegensatz zur SVB nicht so konservativ sein und auch nicht mit einem schrecklichen uralten Tech-Stack integrieren
Ich freue mich über den Erfolg mit Haskell, aber wie bei Jane Street und OCaml glaube ich, dass die Sprachwahl aus geschäftlicher Sicht fast zufällig ist, anders als das, woran die Firma einen gern glauben lassen möchte
Ich frage mich allerdings, was sie im Frontend verwenden. Vermutlich ist dieses Haskell alles Backend
So konnte man Kultur und Stil neuen Leuten von Anfang an einprägen
Vor vibe coding hätten die meisten von ihnen vermutlich nicht einfach ohne jede Anleitung losgelegt und herumgehackt
Wenn man von anderen Diensten kommt, ist das wirklich angenehm
Ein enger Freund arbeitet bei dieser Firma, und selbst von außen wirkt die Engineering-Kultur gut
Ich denke, Haskell ist hier das richtige Werkzeug und sie nutzen seine Stärken gut aus, aber ein großer Teil des Erfolgs könnte auch einfach daran liegen, dass die Firma insgesamt gut geführt ist
Ich habe den Eindruck, dass dieser Autor unabhängig von der verwendeten Sprache eine erfolgreiche Engineering-Organisation aufgebaut hätte
Ich lese gerade Real-World OCaml und lerne mehr funktionale Programmierung, auch wenn mir einiges schon bekannt war
Es scheint, als könne man damit erstaunlich robuste Softwarebausteine bauen
Gleichzeitig grüble ich aber
Das aktuelle Produkt-Backend läuft mit NiceGUI und erfüllt seine Rolle gut
Der Code ist vernünftig und MVVM, und die wichtigste Aufgabe ist, sich pro Kunde mit WebSockets zu verbinden, Daten zu konsumieren und Analysen anzuzeigen
Es wird nicht viele Kunden geben, und die Website dürfte wohl nur einige Dutzend bis höchstens ein paar Hundert Besucher haben
Ich hätte gern auch ein REPL oder Hot Reload, aber ich sehe, dass funktionale Programmierung bei mehr Features wie Benutzerverwaltungs-Panels und zusätzlichen Analysen gut zu Datenpipeline-Transformationen passen könnte
Allerdings sind Haskell und OCaml statische Sprachen
Wenn ich später beim Wachstum und Skalieren trotzdem etwas Dynamischeres will, scheinen Clojure oder Elixir gute Optionen zu sein
Gleichzeitig habe ich Angst, dass später einmal ein Refactoring nötig wird und dann alles kaputtgeht
Im Moment nutze ich Python mit Mypy, und das Frontend wird von NiceGUI im Backend erzeugt
cabal repleine Web-App in Entwicklung sehr schnell neu ladenEhrlich gesagt glaube ich, dass viele Haskell-Nutzer das nicht gut genug ausnutzen
Ich habe einmal an einem ähnlichen System in einer relativ obskuren Sprache gearbeitet, zuerst Scheme und später Racket, und obwohl es größer wurde, konnte ein kleines Team es lange pflegen und dabei hohes Tempo halten
Wir haben nicht viele Bugs gebaut und konnten Funktionen meistens sehr schnell hinzufügen
Zum Beispiel haben wir als Erste irgendeine Zertifizierung erreicht, um sensible Daten bei AWS zu hosten
Manchmal war die Feature-Entwicklung langsamer, weil man Dinge von Grund auf selbst bauen musste, die man auf populären Plattformen mit Standardkomponenten erledigt hätte
Aber wenn es einmal gebaut war, funktionierte es gut, wir waren wieder auf dem alten Tempo und wurden nicht von der Aufblähung und Komplexität Dutzender Standard-Frameworks ausgebremst
Weil wir die beherrschbare Plattform selbst kontrollierten, konnten wir auch schnell zu AWS wechseln, als es nötig wurde
Das System hatte von Anfang an auch eine architektonische Geheimsoße für komplexe Daten und Web-Interaktionen, was die schnelle Entwicklung vieler Features ermöglichte und auch später in eine kluge Richtung Schub gab
Anders als bei dem Haskell-Fintech war das Team aber sehr klein
Es gab jeweils nur 2 oder 3 Softwareingenieure gleichzeitig und eine Person, die den gesamten Betrieb machte
Daher hatten wir nicht die Schwierigkeit, dass Hunderte Leute koordiniert arbeiten und ein konsistentes System erhalten müssen
Meistens kümmerte sich eine Person um technischere, architektonische Codeänderungen, während eine andere schnell Features mit viel Business-Logik für komplexe Prozesse ergänzte
Wenn man aktuelle oder nahe zukünftige LLM-artige KI-Werkzeuge vorsichtig nutzt, könnte man vielleicht einen Teil der Effizienz sehr kleiner und extrem wirksamer Teams auch in der Softwareentwicklung wiedererlangen
Das aufkommende Modell ist für mich nicht, riesige Aufblähung zu produzieren, um Story Points verschwinden zu lassen und Nachhaltigkeit zum Problem anderer zu machen, sondern dass einige wenige sehr scharfe Denker das System auf einem Weg halten, der zugleich kraftvoll und beherrschbar ist
Das ist ein zweischneidiges Schwert
2 Millionen Zeilen sind eine beeindruckende Leistung, aber zugleich auch eine erhebliche Wartungslast
Die Vorteile von Haskell sind theoretisch klar, aber die Nachteile sind intuitiv schwerer zu erfassen
Die Versuchung besteht darin, alles in Typen zu modellieren
Die Codebasis selbst wird dann nicht zur Anwendung, sondern zur Business-Spezifikation
Jede Richtlinienänderung wird zu einem großen Refactoring, und dank der Sicherheit von Haskell kann das erstaunlich arbeitsintensiv werden
Am Ende kann man nicht beides haben, und irgendwann ist man in den Typen gefangen
Haskell ist gerade in dieser Größenordnung wirklich beeindruckend und mächtig, bringt aber auch eigene Probleme mit sich
Die Versuchung, Business-Logik in Typen zu modellieren, kann zu starren Strukturen führen, und die Sicherheit, die diese Strukturen geben, kann einen für andere Arten von Risiken blind machen
Man kann nicht alles haben, aber vieles schon
Ich war vor ein paar Jahren Praktikant bei Jane Street, und obwohl dort nicht Haskell, sondern OCaml verwendet wurde, schienen sie diese Balance wirklich gut hinzubekommen
Es ist ein Bereich mit hoher inhärenter Komplexität, in dem Zuverlässigkeit und Korrektheit direkt mit dem Fortbestand des Geschäfts verknüpft sind, und trotzdem bewegten sie sich erstaunlich schnell
Rückblickend lag ein wesentlicher Teil von Jane Street darin, erfahrene OCaml-Programmierer mit ausgezeichnetem Geschmack wie Stephen Weeks einzustellen und sie von Anfang an die Kernbibliotheken bauen und die gesamte Codebasis prägen zu lassen
Leider hat Mercury das in diesem Punkt wohl nicht ganz so gut geschafft
Ehrlich gesagt ist der größte Nachteil eines Turing-vollständigen Typsystems, dass man theoretisch Anwendungen bauen kann, die beim Kompilieren zu Staub zerfallen
Ein ähnlicher Haskell-Erfolgsfall von Bellroy ist Thema eines kommenden Melbourne Compose-Meetups: https://luma.com/uhdgct1v
Das Problem, das ich mit funktionaler Programmierung habe, ist das Debugging
Genauer gesagt halte ich das für eine Stärke imperativer Programmierung, besonders prozeduraler Ansätze
In funktionalen/deklarativen Stilen beschreibt man meist nicht, wie etwas aufgebaut wird, sondern in welchem Zustand es sein soll, und die Sprache setzt alles zusammen und liefert das Endergebnis
Wenn man alles richtig gemacht hat, ist das schön und vielleicht sogar besser, aber wenn nicht und nicht das erwartete Ergebnis herauskommt, stellt sich die Frage, wie man den Bug findet
In einer Sprache wie C ist das relativ einfach
Man geht Zeile für Zeile durch, betrachtet den Ausführungszustand zwischen den einzelnen Schritten, im Grunde den RAM, und wenn das nicht dem Erwarteten entspricht, ist in dieser Zeile etwas falsch, also steigt man dort ein und arbeitet sich so weiter vor
Je stärker die Sprache versucht, den Zustand zu verbergen, wie in funktionaler Programmierung, desto schwieriger wird das
Interessant ist auch, dass der längste Abschnitt im Artikel genau diesem Problem gewidmet ist, nämlich „design for introspection“
Der Autor musste bewusst viel Aufwand treiben, um den Code debuggbar zu machen, und das gibt gute Einblicke in die oft übersehene praktische Nutzung von Haskell
Das gilt auch für trivialen Code
Andere Mainstream-Sprachen kommen da nicht einmal annähernd heran
In Situationen, in denen das nicht geht, etwa bei Concurrency mit Shared Memory, verwendet man Transaktionen
Auch da kommen andere Mainstream-Sprachen nicht annähernd heran
Von den einfachen Vorteilen wie kein null und keine impliziten Integer-Casts habe ich da noch gar nicht gesprochen
Dass Haskell-Code schwerer zu debuggen ist als Code in anderen Sprachen, stimmt völlig
Aber wenn man die unteren 90 % der Stolperfallen beseitigt, ist das natürlich zu erwarten
Natürlich ist das nicht nur der funktionalen Welt eigen; auch in eher imperativen Sprachen wie Python oder JavaScript nutzt man häufig die Python-Shell, die Browser-Konsole, Node/Deno/Bun-Shells oder Notebooks als erste Debugging-Schicht
REPL-zentriertes Debugging bringt interessante Trade-offs mit sich
In einer Sprache wie C debuggt man oft das ganze Programm und startet an einem Breakpoint, wobei man versucht, genau die Stelle zu treffen, an der das Problem vermutlich liegt
In einer REPL-zentrierten Welt versucht man eher, die Bausteine des Programms so zu gestalten, dass man sie direkter im REPL testen kann
Dadurch beginnen Modul-/API-/Typgrenzen, der Debuggbarkeit zu ähneln
Es gibt dann teils mehr Druck als in imperativen Sprachen wie C/C++, solche Grenzen sauber und benutzbar zu machen
Umgekehrt kann es im Vergleich zum Whole-Program-First-Debugging schwieriger werden, komplexe Integrationsprobleme zwischen Einheiten in seltsamen realen Szenarien zu isolieren
Allerdings fördert ein REPL-first-Ansatz oft auch, die Integrationsoberfläche so klein wie möglich zu halten, weshalb manche Integrationseffekte in funktionalen Sprachen schwächer ausfallen als in imperativen
Die Aussage, funktionale Sprachen würden Zustand verstecken, trifft nicht wirklich zu
Auch diese Sprachen laufen auf imperativer Hardware und arbeiten mit realem Hardwarezustand
Irgendwo gibt es eine Übersetzung zwischen beiden Welten, und vermutlich sind sie gar nicht so verschieden, wie man denkt
Bei Bedarf kann man weiterhin zu imperativen Breakpoints und imperativen Debuggern zurückkehren
Deshalb nenne ich es „REPL-getriebenes“ Debugging
Mit dem REPL kann man die fehlerhafte Einheit, also das genaue Modul, die API oder Funktion samt den Eingaben, die zu überraschender Ausgabe führen, eingrenzen
Wenn der Bug im Quelltext dann noch nicht sichtbar ist, kann man ihn an einen imperativen Debugger übergeben und fast dieselbe Erfahrung des schrittweisen Durchgehens Zeile für Zeile bekommen, oft sogar mit zusätzlichem Kontext
Zu diesem Zeitpunkt hat das REPL das Problem meist schon so weit eingegrenzt, dass die Einheit selbst klein und schmal ist und man gar keinen guten Breakpoint mehr auswählen muss
Ich glaube, die Botschaft aus dem Abschnitt „design for introspection“ wurde falsch verstanden
Dieser Abschnitt handelte nicht von Debuggbarkeit, sondern von Observability
Es ging darum, Logging-/Telemetry-Systeme korrekt anzuschließen, im Test Fakes zu mocken und Dinge wie Retries oder Circuit Breaker auf Systemebene hinzuzufügen, statt sie einzelnen Bibliotheken zu überlassen
Auch in der imperativen Welt ist das kein Debugging-Problem, sondern eine Frage der Zerlegung, etwa Dependency Injection, Middleware-Installation oder die Nutzung abstrakter Interfaces statt konkreter Klassen an öffentlichen API-Grenzen
Solche Designvorschläge sind Refactorings und beeinflussen weniger die Debuggbarkeit als vielmehr, wie leicht sich Observability-Middleware in die öffentlichen APIs anderer einbauen lässt
Ich kann mir schwer vorstellen, was 2 Millionen Zeilen Haskell eigentlich alles machen sollen
Das ist wirklich sehr viel Code, und ich hatte immer den Eindruck, Haskell sei eine „dichte“ Sprache, mit der man mit wenig Code sehr viel erreicht
Vielleicht liegt es an den vielen Bibliotheken für Dinge wie JSON-Serialisierung/-Deserialisierung, REST-API-Frameworks oder Logging
Wenn Third-Party-Bindings HTTP-Aufrufe über konkrete Funktionen machen, gibt es keine Möglichkeit, Tracing hinzuzufügen, keine Möglichkeit, Timeouts passend zu den SLOs zu injizieren, keine Möglichkeit, Ausfälle eines Partners im Test zu simulieren, und keine Möglichkeit, die 400-ms-Lücke im Trace anders zu erklären, als nur zu raten
Also schreiben sie es selbst
Am Anfang ist das mehr Arbeit, aber die selbstgebauten Clients wurden von Beginn an so entworfen, dass sie für Observability aufgebaut sind
Das heißt, man kann relativ abstrakte Gedanken mit wenigen Zeichen ausdrücken
Manche nennen das auch „High-Level“
Ich glaube aber nicht, dass 2 Millionen Zeilen so viel sind, wie es zunächst klingt
Besonders nicht für ein Unternehmen in einem regulierten Bereich wie Finanzen und bei einer über Jahre gewachsenen Codebasis
Die Zeilenzahl mag etwas geringer sein, aber die Wortanzahl ist in etwa ähnlich wie bei stärker imperativen objektorientierten Sprachen
Dort ist so etwas wie
St M -> C Tvielleicht akzeptabel, aber in echter Software istTransactionState Debit -> Verified Transactionviel nützlicherEin anderer Teil ist ein kultureller Faktor, der bis zu LISP zurückreicht
Es gibt eine Tendenz, sich mit schwer verständlichen Tricks oder Makros übermäßig clever anzustellen, nur um Zeilen zu sparen
In einer Finanzfirma wie Mercury werden stattdessen vermutlich Klarheit und Lesbarkeit gefördert
Zum Beispiel könnte ein Linter erzwingen, monadischen Code nicht mit
>>und>>=in eine einzige Zeile zu pressen, sondern in sorgfältige mehrzeiligedo-Ausdrücke aufzuteilen