Milyonlarca Satır Haskell: Mercury’nin Prodüksiyon Mühendisliği
(blog.haskell.org)- Mercury, yorumlar vb. hariç yaklaşık 2 milyon satırlık Haskell kod tabanıyla 300 binden fazla şirkete bankacılık hizmeti sunuyor ve 2025'te 248 milyar dolar işlem hacmi ile yıllıklandırılmış 650 milyon dolar gelir işliyor
- Mercury’nin Haskell kullanımındaki değeri, saflığın kendisinden çok operasyonel bilgiyi API'lere ve tiplere yerleştirmekte, riskli davranışları dar sınırların arkasına koymakta ve güvenli yolu en kolay yol haline getirmekte yatıyor
- Güvenilirlik, tüm hataları engellemekten ziyade sistemin değişkenliği absorbe etme yeteneği olarak ele alınıyor; tip sistemi ise hata sınıflarını dışlayıp kurumsal bilgiyi derleyicinin zorunlu kıldığı bir dokümantasyon gibi bırakıyor
- Mercury, finansal iş akışlarında yeniden deneme, zaman aşımı, iptal ve çökme sonrası kurtarma için Temporal'ı durable execution çatısı olarak kullanıyor ve Haskell SDK'sı
hs-temporal-sdk'yı açık kaynak olarak yayımladı - Haskell’in prodüksiyondaki değeri her şeyi tiplere koymakta değil; veri kaybı, finansal hata ve düzenleyici sorunlara yol açabilecek değişmezleri tiplerle koruyup karmaşıklığı kapsülleyerek, bunu da test, dokümantasyon ve kod incelemesiyle birlikte işletmekte yatıyor
Mercury’nin Haskell operasyon ölçeği ve güvenilirlik yaklaşımı
- Mercury, yorumlar vb. hariç yaklaşık 2 milyon satır büyüklüğünde bir Haskell kod tabanı işletiyor
- Mercury, 300 binden fazla şirkete bankacılık hizmeti veren bir fintech şirketi ve 2025'te 248 milyar dolar işlem hacmi ile yıllıklandırılmış 650 milyon dolar gelir işliyor
- Şirketin yaklaşık 1.500 çalışanı var; mühendislik organizasyonu çoğunlukla genel amaçlı geliştiriciler işe alıyor ve bunların büyük kısmı işe girmeden önce Haskell kullanmamış oluyor
- Bu sistem, hızlı büyüme, SVB krizi sırasında 5 gün içinde 2 milyar dolar yeni mevduat girişi, düzenleyici incelemeler ve büyük ölçekli finansal sistemlerin olağan ve olağan dışı durumları boyunca yıllardır çalışıyor
Güvenilirlik, hata önleme değil değişkenliği absorbe etme yeteneğidir
- Geleneksel güvenilirlik yaklaşımı hataları listelemeye, kontroller ve testler eklemeye, bug bulmaya odaklanır; ancak bu tek başına yeterli değildir
- Mercury, güvenilirliği sistemin değişkenliği absorbe etme yeteneği olarak ele alıyor
- Sistem zarif biçimde performans düşüşü yaşayabilmeli
- Operatörler sistemi anlayıp ayarlayabilmeli
- Mimari, doğru işi kolay; yanlış işi zor hale getirmeli
- Hızla büyüyen organizasyonlarda, yeni katılan bir mühendisin modülü okuyup anlayabilmesi, veritabanı yavaşladığında servisin onunla birlikte çöküp çökmediği ve arayüzün yanlış kullanımını derleyicinin yakalayıp yakalamadığı gerçek operasyon sorularına dönüşür
- Tip sistemi, salt doğruluk ispatından çok bir operasyonel yardımcı mekanizmaya yakındır
- Belirli hata sınıflarını dışlar
- Yazarı ayrıldıktan sonra bile kurumsal bilgiyi derleyicinin okuyabileceği biçimde bırakır
- Wiki'den daha tutarlı biçimde zorunlu kılınan bir dokümantasyon işlevi görür
- Mercury’nin kararlılık mühendisliği, ürün geliştirmeyi yavaşlatan bir kalite polisi değil; bir özelliğin bozulduğunda yaratacağı etkiyi tasarımın başından itibaren ele alan işbirlikçi bir çalışma biçimidir
- Hata anındaki etki alanı
- İdempotent olması gereken işler ve yöntemleri
- Geri alma biçimi
- Devam eden işlerin nasıl ele alınacağı
- Hataları absorbe eden sistemlerle onları büyüten sistemler önceden değerlendirilir
Saflık, dilin özelliği değil arayüz sınırıdır
- Haskell’de saflık, içeride hiç yan etki olmadığı anlamına gelmez; daha çok arayüzün yan etkilerin sızmasını engelleyen bir sınır oluşturması anlamına gelir
bytestring,text,vectorgibi kütüphanelerin saf fonksiyonlarının arkasında değiştirilebilir tahsisler, buffer yazımı, unsafe coercion gibi iç uygulamalar bulunurSTmonadı, hesaplama içinde gözlemlenebilir yerinde değişiklikler ve yan etkiler kullanır; ancakrunST’nin rank-2 tipi içeride oluşturulan değiştirilebilir referansların dışarı kaçmasını engellerrunST :: (forall s. ST s a) -> a- İçeride emirsel davranış mümkün olsa da dışarıya yalnızca sonuç çıkar; değiştirilebilir durum sınırın dışına sızmaz
- Bu ilke, operasyonel sistemin geneline uygulanır
- Veritabanı katmanı içeride bağlantı havuzlama, yeniden deneme ve değiştirilebilir durum kullanabilir
- Cache, eşzamanlı değiştirilebilir map kullanabilir
- HTTP istemcisi circuit breaker, bağlantı havuzu ve çok sayıda muhasebe/izleme yönetimi içerebilir
- Asıl nokta, riskli davranışları dar arayüzlerle sarmalayıp yanlış kullanımı zorlaştırmaktır
- Gerçek sistemlerde amaç değişimi tamamen önlemek değil, değişimin nerede olduğunu açık hale getirmek ve kod tabanında bunu kimlerin bilmesi gerektiğini sınırlamaktır
Doğru işi kolay iş haline getirmek
- Büyük kod tabanlarında doğruluk, sık sık belirli bir sıraya ya da görünmeyen ek adımlara bağlı örüntülere dayanır
- Audit log, transaction sonrasında flush edilmelidir
- Endpoint çağrılmadan önce feature flag kontrol edilmelidir
- Bildirim enqueue işlemi veritabanı transaction'ı içinde yapılmalıdır
- Bu operasyonel bilgi yalnızca wiki'lerde, onboarding dokümanlarında, geçmiş tasarım incelemelerinde, Slack thread'lerinde ya da bazı kıdemli mühendislerin hafızasında kalırsa hızla kaybolur
- Haskell, bu tür prosedürleri tiplerle encode ederek unutulamaz hale getirebilir
- Kötü yaklaşım, doğru fonksiyonun kullanılmasını istemek ama baypas yollarını açık bırakmaktır
-- 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 ()- Daha iyi yaklaşım, işi yürütmenin tek yolunun event yayımlamayı içerecek şekilde tipleri yeniden yapılandırmaktır
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 - Burada tip sistemi, event'lerle ilgili derin teoremleri ispatlamaktan çok doğru operasyonel prosedürü en kolay yol haline getirir
- Yeni bir mühendis transaction'ın nasıl yazılacağını sorduğunda, tip imzaları ve açık API cevabı verir; kıdemli mühendis ayrılsa bile bilgi kalır
Dayanıklı yürütme ve Temporal
- Finansal sistemlerdeki iş akışları tek bir işlem içinde kalmaz
- ödeme gönderimi
- iş ortağı onayını bekleme
- defter güncelleme
- müşteri bildirimi
- iptal ve zaman aşımı işleme
- iş ortağı başarılı olmuş ama worker kaydetmeden önce ölmüş olabilir
- ağ sorunları nedeniyle yanıt gelmeyebilir
- Bu tür akışlar; durum, yeniden deneme, zaman aşımı, idempotensi ve süreç çökmesiyle dağıtımları aşan dayanıklı yürütme gerektirir
- Mercury geçmişte bu süreçleri veritabanı tabanlı durum makineleri, cron işleri, arka plan worker’ları ve kodun çeşitli yerlerine serpiştirilmiş yeniden deneme ile zaman aşımı işleme mantığıyla orkestre ediyordu
- Çalışıyordu ama kırılgandı, anlaşılması zordu ve operasyonel olayların orantısız bir nedeni haline gelmişti
- Temporal, Mercury’nin durable execution çerçevesidir; iş akışları sıradan sıralı kod gibi yazılır ve platform her adımı olay geçmişine kaydeder
- Bir worker iş akışının ortasında çökerse başka bir worker deterministik prefix’i replay ederek durumu yeniden kurar ve durduğu yerden devam eder
- Yeniden deneme, zaman aşımı, iptal ve hata işleme; her ekibin ayrı ayrı yeniden uygulaması yerine platform tarafından sağlanır
- Temporal workflow, olay geçmişi üzerinde çalışan saf bir fonksiyona benzer bir niteliğe sahiptir
- replay edilen workflow, orijinaliyle aynı komut dizisini üretmelidir
- Bu determinizm gereksinimi, saf kodun aynı girdi·aynı çıktı kısıtıyla benzerlik taşır
- Yan etkiler, workflow’un
IOkarşılığı olan activity içinde izole edilir
- Mercury, Temporal’ın resmi Core SDK’sını Rust FFI ile saran Haskell SDK’sı
hs-temporal-sdkgeliştirdi ve bunu açık kaynak olarak yayımladı - Temporal benimseme kalıbı Temporal Replay conference konuşmasında da ele alındı; Mercury kırılgan cron·durum makinesi zincirlerini dayanıklı workflow’larla değiştirerek operasyonel iyileşme sağladı
Alanı taşıma katmanıyla değil, iş diliyle tasarlamak
- Büyüyen sistemlerde sık görülen hata, çağıran sistemin kavramlarının alan modeline sızmasıdır
- HTTP istek işleyicileri için yazılmış kod daha sonra cron işleri, kuyruk tabanlı arka plan worker’ları ve Temporal workflow’larında yeniden kullanıldığında
StatusCodeException 409 "Conflict"gibi HTTP istisnaları HTTP dışı bağlamlara yayılabilir - Bir cron işinde 409 yanıtını bekleyen bir çağıran yoktur ve durum kodları iş anlamını yanlış katmana taşır
- Çözüm, alan hatalarını alan tipleri olarak modellemektir
- yetersiz bakiye
InsufficientFundsolmalıdır - yinelenen istek
DuplicateRequestolmalıdır - iş ortağı zaman aşımı
PartnerTimeoutolmalıdır
- yetersiz bakiye
- Her sınırda ince bir dönüştürme katmanı bulunur
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 - Taşıma katmanına ilişkin kaygılar uçta kalmalıdır; alan modeli web handler, CLI, cron işi, arka plan worker’ı veya workflow motoru tarafından nereden çağrılırsa çağrılsın HTTP durum kodlarını yanında taşımamalıdır
Tür seviyesinde kodlamanın maliyeti ve doğru denge
- Değişmezleri tiplere yerleştirmek güçlüdür ama bunun bilişsel maliyet, katılık ve gereksinimler değiştiğinde zorluk gibi bir bedeli vardır
- Bir ihlal veri kaybına, finansal hataya, düzenleyici soruna veya çağrı üzerine müdahale olayına yol açacaksa tür seviyesinde kodlama maliyeti haklı çıkar
- Sebep yalnızca şu anda işlerin böyle yapılması ya da tip-seviyesinde teknikleri denemek istemekse, sonuç kod tabanını değiştirmeyi zorlaştırmak olabilir
-
Çok fazla kodlama yapılan taraf
- yasadışı durumlar ifade edilemez hale gelir ve alan tiplere sadık biçimde modellenir
- iş kurallarındaki değişiklikler, 50 modülü etkileyen tip değişikliklerine dönüşür ve refactoring uzar
- yeni mühendislerin tip imzalarını anlaması zorlaşır
-
Hiçbir şeyin kodlanmadığı taraf
- tipler
String,IO (), en kötü durumdaDynamicseviyesine yaklaşır - kodu değiştirmek kolaydır ama sözleşme yoktur; anlam, mevcut yazarın hafızasına bağlıdır
- yazar ayrıldığında sistemin neden çalışmadığını anlamak zorlaşır
- tipler
-
Yararlı ölçütler
- sessiz bozulmayı engelleyen değişmezler genelde tiplere konmalıdır
- olay olmadan commit edilmiş işlem
- denetim kaydı olmadan işlenmiş ödeme
- görünüşte mümkün olsa da anlamsal olarak imkânsız durum geçişleri
- büyük ve görünür şekilde başarısız olan değişmezler için iyi hata mesajlarına sahip çalışma zamanı kontrolleri yeterli olabilir
- 500 yanıtı
- assertion hatası
- JSON sınırında tip uyumsuzluğu
- tüm alanı tiplerle modelleme dürtüsü bastırılmalıdır
- alanın içinde istisnalar, geriye dönük uyumluluk kuralları, birbiriyle çelişen kurallar ve belirli müşterilere özel davranışlar bulunur
- tipler yalnızca derleyici için değil, ekip için de bir araçtır
- testler, dokümantasyon, kod incelemeleri, örnekler ve playbook’larla birlikte bir savunma katmanı oluşturmalıdır
- Mercury içinde GADT, type family ve durum geçişlerini izleyen phantom type gibi karmaşık tip-seviyesi düzenekler kullanan kütüphaneler de vardır
- hata olduğunda paranın yanlış yere gitmesine ya da düzenleyici değişmezlerin bozulmasına yol açabilecek mekanizmalarda bu karmaşıklık gereklidir
- kilit nokta, karmaşıklığı kapsüllemektir
- tip-seviyesinde durum makineleri uygulayan modüller, konuyu derinlemesine anlayan az sayıda yazar ve yeterli testlere sahip olmalıdır
- kullanan taraftaki API, sıradan tiplere sahip birkaç fonksiyon gibi görünmelidir
- bir product engineer, içerideki tip-seviyesi ispat düzeneklerini bilmeden de bunları güvenle çağırabilmelidir
- kod incelemesinde başka modüllere dokunan bir PR, derleyiciyi yatıştırmak için kopyalanmış tip açıklamalarıyla doluysa bu, soyutlamanın sınırların ötesine sızdığının işaretidir
- sessiz bozulmayı engelleyen değişmezler genelde tiplere konmalıdır
İçgözlemlenebilirlik için tasarım
- Güvenilirlik uyum sağlama becerisiyse, içgözlemlenebilirlik bu beceriyi kazanmanın yollarından biridir
- Operatörler göremedikleri şeyi işletemez; ekiplerin de içi opak sistemlere uyum sağlaması zordur
- Haskell’de monkey patching olmadığından, çalışma anında kütüphane içindeki HTTP istemcisini değiştirmek ya da veritabanı çağrılarını OpenTelemetry span üreten işlevlerle değiştirmek zordur
- Rust da aynı kısıta sahiptir, ancak Rust ekosistemi
towermiddleware deseninde yakınsarken Haskell ekosistemi birden fazla yaklaşıma bölünmüştür - Bir kütüphane yalnızca somut üst düzey işlev kümelerini açığa çıkarıyorsa, enstrümantasyon için onu yeni bir modülle sarmak ve insanların özgün modül yerine onu import etmesini ummak gerekir
-
Fonksiyon kayıtları
- En sık kullanılan çözüm, somut işlevler yerine fonksiyon kayıtlarını açığa çıkarmaktır
-- 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 } - Bu yaklaşımla
sendRequestzamanlama enstrümantasyonuyla sarılıp yeni birHttpClientdöndürülebilir - Test için fault injection, mock değiştirme, retry, tracing, istek yeniden yazımı, kiracı bazlı davranış gibi çapraz kesen kaygılar çalışma anında eklenebilir
- WAI’deki
type Middleware = Application -> Applicationgibi davranış dönüşümlerini bileştirilebilir kılan desen, operasyon açısından çok faydalıdır
- En sık kullanılan çözüm, somut işlevler yerine fonksiyon kayıtlarını açığa çıkarmaktır
-
Monoidile bileştirilen interceptor’lar- Middleware ve interceptor türleri genellikle
SemigroupveMonoidinstance’larına sahip olabilir - WAI’nin
Middlewaretürü bir endomorphism’dir ve endomorphism’ler bileşim veidaltında bir monoid oluşturur - Interceptor hook kayıtları alan bazında bileştirilebilir; böylece tracing, timeout, task queue rewrite gibi kaygılar ek bir kablolama olmadan
mconcatile birleştirilebilirappTemporalInterceptors = mconcat [ retargetingInterceptor , otelInterceptor , sentryInterceptor , sqlApplicationNameInterceptor , loggingContextInterceptor , statementTimeoutInterceptor , teamNameInterceptor , clientExceptionInterceptor , workflowTypeNameInterceptor ] - Her interceptor bağımsız bir modülde yalnızca tek bir kaygıyla ilgilenir,
memptyüzerindeki gerekli alanları override eder ve sıra listede açıkça belirtilir
- Middleware ve interceptor türleri genellikle
-
Effect system’leri
effectful,polysemy,fused-effects,cleffgibi effect system’leri de başka bir yol sunar- Kullanılabilir işlemler effect türleriyle tanımlanır ve production, test ya da tracing için interpreter’lar çağrı noktasında değiştirilebilir
- Effect’ler yakalanıp metrik kaydı veya gecikme enjeksiyonu yapıldıktan sonra gerçek handler’a geri gönderilebilir
- Dezavantajı ise tür seviyesinde effect listesi, handler stack’i ve zorlayıcı tür hataları gibi ek düzenekler getirmesidir
- Fonksiyon kayıtları ise yeni bir mühendisin bir öğleden sonra içinde kavrayabileceği kadar basittir
-
persistentiçin olumlu bir örnekpersistentiçindekiSqlBackend,connPrepare,connInsertSql,connBegin,connCommit,connRollbackgibi işlevlerden oluşan bir fonksiyon kaydıdır- OpenTelemetry enstrümantasyonu eklenirken ilgili alanlar sarılarak tüm veritabanı işlemlerine tracing span’ları eklenebilmiştir
- Fork oluşturmadan, neredeyse hiç kaynak kod değişikliği yapmadan veritabanı katmanında görünürlük sağlanmıştır
-
Operasyon açısından zor kütüphaneler
- Mercury, Hackage’da açık olarak yayımlanan web API istemci binding’lerini neredeyse hiç kullanmaz
- Üçüncü taraf binding’ler HTTP çağrılarını somut işlevlerle yapıyorsa, tracing, SLO’ya uygun timeout’lar, partner arızası simülasyonu ya da trace içindeki 400 ms’lik boşluğu açıklamak zorlaşır
- Bu yüzden istemciler doğrudan kendileri tarafından yazılır ve en baştan gözlemlenebilir olacak şekilde tasarlanır
-
Küçük bir ekosistemin maliyeti
- Bazı Haskell kütüphaneleri terk edilmiş değildir, ancak onları açıkça sahiplenip hızlı biçimde iyileştiren bir özne olmadan, kamusal altyapı gibi ortada kalmıştır
- Eski arayüzler korunur ve gözlemlenebilirlik, sınır tasarımı ve işletilebilirlik konularındaki yeni tasarımların benimsenme hızı yavaş olabilir
http-clientdoğrudan yalnızca HTTP/1.1 destekler; yeterince kullanışlıdır ama bazı anlarda geçici çözümler gerekebilir
Paket yazarları için operasyonel gereksinimler
- Kütüphane yazarları, kullanıcıların kaynak kodu değiştirmeden davranış enjekte edebilmesi için fonksiyon kayıtları, effect türleri, callback’ler gibi kaçış kapıları sunmalıdır
hs-opentelemetry-apibağımlılığını eklemek ve temelIOişlemlerinin etrafına span yerleştirmek bile kütüphaneyi production’da işleten kullanıcılara yardımcı olur- API paketi breaking change’ler konusunda temkinlidir ve uygulama OpenTelemetry SDK’sını başlatmazsa etkisiz çalışacak şekilde tasarlanmıştır
- Performans ek yükü minimumdur; kullanıcı uygulamasında beklenmedik istisnalar ya da logging üretmez
- Bağımlılık footprint’i hâlâ istenildiği kadar küçük değildir ve bunu iyileştirme çalışmaları sürmektedir
- Kütüphane kodu içinden doğrudan log yazılmamalıdır
- Bir logging framework’ünü import edip doğrudan
stdoutya dastderre yazmak yerine callback, logger parametresi veya çağıranın yönlendirebileceği bir log mesajı veri türü sağlanmalıdır - Logların nereye gideceği, uygulamanın operasyon ortamına ait bir karardır
- Mercury, yapılandırılmış log pipeline’ını observability stack’ine gönderir; kütüphane doğrudan
stderre yazarsa JSON lines akışından ayrı ek kablolama gerekir
- Bir logging framework’ünü import edip doğrudan
.Internalmodüllerini açığa çıkarmak da düşünülebilir- Kullanıcıların iç API’lere bağımlanıp refactor’ı zorlaştırabileceği yönündeki kaygı geçerlidir
- Ancak açık API’nin zaten tüm kullanım senaryolarını karşıladığına dair güven çok nadiren haklı çıkar
- Kararlılık uyarısı açıkça belirtilmiş
.Internalmodülleri, kullanıcıların paketi fork edip vendor etmesinden daha iyi olabilir containers,text,unordered-containers, Haskell ekosisteminde bu yaklaşımı kullanan iyi örneklerdir- Ancak kullanıcılar iç modülleri sessizce kullanarak ihtiyaçlarını çözerse, açık API eksiklerine dair feedback azalabilir
Tiplere koyulmayan şeyler
- Prodüksiyon Haskell’de de zarif olmayan kısımlar vardır
unsafePerformIO, günlük olarak bağımlı olunan kütüphanelerin içinde kullanılırbytestringvetext, içeride değiştirilebilir buffer’lar ayırır, bunlara yazar ve sonucu üretmek için freeze eder- Tipler, oluşturma sırasında neler yaşandığını söylemez
- Sınırlar; teamül, dikkatli muhakeme ve kod incelemesiyle korunur
- Tip güvenli bir alternatifin performans ya da karmaşıklık maliyetini aşırı artırdığı durumlarda bu tür ödünleri doğrudan kendiniz de yazabilirsiniz
- Tiplerin denetlemediği invariant’lar belgelenmelidir
- Rahatsızlık hissi korunmalı ve tip güvenli alternatifin pratik hale gelip gelmediği periyodik olarak yeniden değerlendirilmelidir
- production Haskell, ödünlerin yokluğu değil, ödünlerin disiplinli biçimde izole edilmesidir
- Hackage üzerindeki birçok Haskell kütüphanesinde test azdır ya da hiç yoktur
- “Derleniyorsa çalışır” düşüncesi, küçük saf kodlarda ve güçlü tiplerde bazen doğru olabilir
- Ancak IO ağırlıklı kodlarda, dış sistem entegrasyonlarında ve hataların yapıdan çok anlamda olduğu kodlarda neredeyse hiç doğru değildir
- Tipler,
Either ParseError Transactiondöndürüldüğünü söyleyebilir ama şunları söyleyemezamountalanının cent olarak mı yoksa dolar olarak mı parse edildiğini- Partner API’nin atlanmış alanlarla null alanları farklı yorumlayıp yorumlamadığını
- Retry mantığının artık yıllardaki belirli bir zaman penceresinde çift tahsilata yol açıp açmadığını
- Prodüksiyonda bu tür kütüphanelerin üzerine sistemler kurulur; doğrulanmamış varsayımlar da miras alınır, bu yüzden bunlar kendi katmanınızın integration test’leriyle telafi edilmelidir
- orphan instance’lar, bağlam içinde total olduğuna inanılan partial function’lar, ulaşılamaz olduğu varsayılan
error, tuhaf FFI wrapper’ları ve elle yapılmış exception hierarchy’leri gibi ödünler de birikir - Hedef ahlaki saflık değil; her ödünün nerede olduğu, neden yapıldığı ve kaldırılırsa neyin bozulacağının kod incelemeleri, belgeler, örnekler ve testlerle görülebilmesidir
Haskell’i prodüksiyonda kullanmaya değer kılan şey
- Haskell, ilk günden hızlı bir tercih değildir
- Bugünkü ekosistem, Next.js veya Rails gibi batteries-included ve hot-reloading’li bir geliştirme ortamını hemen sunamaz
- Gerekli kütüphane olmayabilir ya da varsa bile boş zamanında bakım yapan tek bir kişi tarafından sürdürülüyor olabilir
- Hata mesajları zaman zaman son derece anlaşılmaz olabilir
- İşe alım sorunu abartılmaktadır
- Mercury CTO’su Max Tagher, backend Haskell engineer rolünün Mercury genelinde en kolay işe alım yapılan rol olduğunu açıkça söylemiştir
- Haskell işleri için talep arzdan yüksek olduğundan, alışılmış işe alım dinamikleri tersine döner
- Mercury, hem derin Haskell deneyimi olan hem de hiç olmayan kişileri işe alır; ikinci grup 6–8 haftalık bir eğitim programıyla üretken hale getirilir
- Yarın 100 Haskell uzmanına ihtiyacınız varsa yetenek havuzu sorunu gerçektir; ama iyi genel amaçlı geliştiricileri alıp eğitmeye istekliyseniz bu çok daha az gerçekçi bir sorundur
- Daha büyük işe alım riski havuzun büyüklüğü değil, mizacıdır
- Haskell; doğruluğa ve soyutlamaya önem veren, makale okumayı seven ve mevcut varsayımları sorgulayan idealistleri çeker
- Bu güç kontrol edilmezse prodüksiyonda bir sorumluluğa dönüşebilir
- Veritabanı katmanını yeni bir tip-seviyesi ilişkisel cebir kodlamasıyla baştan yazmaya çalışmak, geçici bir script’te
StringyerineTextkullanılmadı diye merge’ü reddetmek ya da her tasarımı son makale tarzı total rewrite’a çekmeye çalışmak ekibi yavaşlatır
- Prodüksiyon Haskell, pragmatizm kültürü gerektirir
- Tip sistemi bir elektrikli alettir, din değil
- Zaten iyi bir çözümü olan sorunları yeni mekanizmalar icat etme fırsatına çevirmek prodüksiyona uygun değildir
- Getiri zaman içinde ortaya çıkar
- Dinamik tipli bir kod tabanında haftalar sürebilecek bir refactoring, tip değişikliğinden sonra derleyicinin tüm call site’ları göstermesi sayesinde birkaç saatte bitebilir
- Yeni bir engineer, tip imzalarını okuyarak bir modülün sözleşmesini anlayabilir
- İmkânsız durumlar gerçekten ifade edilemez olduğundan bir production incident hiç yaşanmayabilir
- Mercury, yatırım geri dönüşünün yıllarla değil aylar içinde görüldüğünü düşünüyor
- Özellikle finansal hizmetlerde veri bütünlüğü hatalarının maliyeti, kullanıcı şikâyetiyle değil, düzenleyici uyarılarla ve başkasının parasıyla ölçülür
- Tip sistemi riski ortadan kaldırmaz, ama hızla büyüyen bir kod tabanında riski yanlışlıkla sisteme sokmayı zorlaştıran araçlar sağlar
- Haskell’in prodüksiyondaki değeri, sihirli değnek ya da ahlaki bir hareket olmasında değil; farklı Haskell uzmanlık seviyelerine sahip ekiplerin tehlikeli aygıtları sınırlar içinde tutmasına, operasyonel bilgiyi korumasına ve güvenli yolu kolay yol haline getirmesine imkân veren güçlü bir araç seti sunmasındadır
1 yorum
Hacker News yorumları
Haskell’in bu tür şeyleri tiplerle zorunlu kılma konusunda en güçlü dillerden biri olduğu doğru, ama aynı desen Rust ve TypeScript’te de gayet iyi çalışıyor.
Web uygulamalarında
User -> LoggedInUser -> AccessControlledLoggedInUsergibi akışlarla tekrar tekrar görülen bariz yetkilendirme hatalarını önleme yaklaşımını da seviyorum.Bana göre sektörde bu desen aşırı derecede az kullanılıyor.
Güvenlik için escape edilmemiş ve edilmiş string’leri ayırmanız gerekiyorsa, dinamik tipli bir dilde bile bunları
Escapedsınıfıyla sarabilir veescape(str)->Escaped,dangerouslyAssumeEscaped(str)->Escapedgibi fonksiyonlar tanımlayabilirsiniz.Performans maliyeti olduğu için bir denge kurmak gerekir ama yapılabilir.
Bir başka yaklaşım da Application Hungarian; ancak bunda derleyiciden çok programcının disiplinine güvenilir: https://www.joelonsoftware.com/2005/05/11/making-wrong-code-...
Örneğin C# ile de rahatça yapılabilir ama pratikte görsel gürültü, gerçek tip tanımından daha baskın hale gelir.
Ama “monad’lar korkutucu, o yüzden bir tutorial yazalım” etkisini önlemek için bunu özellikle söylemeyip isimleri de farklı koyma eğilimindeler.
Etki, monad’lardansa daha çok type class gibi alanlarda görülüyor.
Nominal typing olmadığı için primitive türleri saran
newtypebenzeri şeyler yapmak adına epey hacky numaralar hatırlamak gerekiyor.Benim deneyimimde bu tür tip güvenliğini zorunlu kılma konusunda OCaml, Rust’tan daha güçlüydü.
GADT sayesinde daha fazla ifade gücü var; polymorphic variant’lar ve object type/record row type’lar sayesinde de kullanışlılık sağlıyor, ayrıca module sistemi ve functor’ler de var.
Garbage collection’ın yeterli olduğu alanlarda Rust’un borrow checker’ının getirdiği soyutlama kısıtları ve zorluklardan da kaçınabiliyorsunuz.
Birkaç yıl boyunca Haskell ile çalışmayı gerçekten çok sevdim.
Özellikle aradığım bir şey değildi ama fırsat tesadüfen çıktı; ilginç ve entelektüel olarak uyarıcıydı.
Yine de ne yazık ki Haskell ile 3 yıl çalıştıktan sonra bile Rust’taki üretkenliğim Haskell’in rahatlıkla iki katı oldu.
Haskell’de önceden bilip kaçınmanız gereken daha fazla tuzak var ve yazana bağlı olarak neredeyse salt okunur bir dilmiş gibi sindirmesi zor olabiliyor.
Toolchain çoğu zaman Nix ile iç içe oluyor; Nix’in kendisi de karmaşık bir canavar ve dil eklentileri her yere dağılmış gibi hissettiriyor.
Cabal dosyaları da pek iyi değil, derleyici hatalarına alışmak zaman alıyor.
Son ürünümüzde backend’i Typescript’ten Rust’a taşımaya başlamıştık; sebebi crash’lerden bıkmış olmamızdı.
Şimdi bunu yaptığım en büyük teknik hatalardan biri olarak görüyorum; çünkü üretkenlik çok ciddi biçimde düştü.
Rust’ta yaşadığım zaman kayıplarına bir örnek: veritabanı bağlantısı açıp bir şey yapıp sonra kapatan bir yüksek dereceli fonksiyon yazmak Haskell, TypeScript, JavaScript, C++, PHP’de önemsiz bir işken Rust’ta, uzman Rust arkadaşlara sormama rağmen fiilen imkânsız olduğu için vazgeçtiğim bir şey oldu.
Bir de birkaç kez refactor girişiminde bulunup bütün gün tip hatalarını düzelttikten sonra en üst düzey dosyada bir hata ile karşılaşıp, bunun aslında tasarımın temel bir parçasından kaynaklandığını ve tüm refactor’un imkânsız olduğunu anlayıp her şeyi geri aldığım oldu.
Üstelik Rust, somut türler yerine değerleri arayüzler üzerinden kullanmanın duruma göre ileri seviye bir teknikle imkânsızlık arasında bir yerde olduğu aklıma gelen tek modern dil.
Bu yüzden, uygulama kodu yani sistem kodu veya kütüphane kodu olmayan şeylerin kabaca Rust ile yazılmaması gerektiği sonucuna vardım.
Bir de “salt okunur” derken neyi kastettiğinizi merak ediyorum.
Genel algının aksine, Mercury’nin Haskell’i seçmesi ve ilk liderlerinin Haskell’de derin deneyime sahip olması başarıda azımsanmayacak bir rol oynamış olabilir diye düşünüyorum.
Mercury müşterisi olarak bu şirket benim araç kutumdaki temel şirketlerden biri ve Haskell seçiminin onların ilerleyişini, geliştirme sürecini ve genel yolculuğunu daha iyi hale getirdiği hissinden kurtulamıyorum.
Elbette çoğu dil için benzer bir iddia kurulabilir ve bu, Haskell gibi fonksiyonel dillerin başarının formülü olduğu anlamına gelmez.
Yine de “vibe coding” ve LLM çağından önce böyle bilinçli bir karar verilmiş olması, özellikle yazıda ayrıntılı anlatılan mühendislik kültürüyle birleşince, oldukça ileri görüşlü görünüyor.
Ben de iyi teknik kültürü severim ama çok iyi teknik kültürü olup kötü iş odağı yüzünden batan şirketler gördüm.
Hatta startup tarzı fintech kültürü iyi teknik kültürü üretmiş de olabilir.
Banka olarak başlamadıkları için örneğin SVB’nin aksine o kadar muhafazakâr olmak ya da korkunç derecede eski teknoloji yığınlarıyla entegre olmak zorunda değillerdi.
Haskell ile başarıya ulaşmalarına sevindim ama Jane Street ve OCaml örneğinde olduğu gibi, şirketlerin sizi inandırmak istediğinin aksine dil seçiminin iş açısından neredeyse tesadüfi olduğunu düşünüyorum.
Yine de frontend’de ne kullandıklarını merak ediyorum. Muhtemelen bu Haskell’in tamamı backend’dedir.
Çünkü yeni gelenlere kültürü ve stili en baştan aşılayabiliyordunuz.
Vibe coding öncesi dönemde böyle insanların çoğu, yönlendirme olmadan doğrudan dalıp bir şeyler hack’lemeye kalkmazdı.
Başka servislerden geçince bu gerçekten tatmin edici oluyor.
En yakın arkadaşlarımdan biri bu şirkette çalışıyor ve dışarıdan bakınca bile mühendislik kültürü iyi görünüyor.
Haskell’in bu iş için doğru araç olduğunu ve güçlü yanlarının iyi kullanıldığını düşünüyorum ama başarının büyük kısmı belki de şirketin genel olarak iyi yönetilmesinden geliyor olabilir.
Bu yazar muhtemelen hangi dili kullanırsa kullansın başarılı bir mühendislik organizasyonu kurardı.
Şu sıralar Real-World OCaml okuyorum; bazı şeyleri zaten biliyordum ama fonksiyonel programlama hakkında daha çok şey öğreniyorum.
Fonksiyonel programlama ile şaşırtıcı derecede sağlam yazılım parçaları üretmek mümkün gibi görünüyor.
Ama kafamda soru işaretleri de var.
Şu an ürün backend’i NiceGUI üzerinde çalışıyor ve işini iyi yapıyor.
Kod makul, MVVM tarzında ve en önemli iş müşteri bazında websocket’lere bağlanıp veriyi tüketmek ve analizleri göstermek.
Müşteri sayımız çok fazla olmayacak ve site ziyaretçileri muhtemelen birkaç on kişi ile en fazla birkaç yüz kişi arasında kalacak.
REPL veya hot reload da istiyorum ama özellikler arttıkça kullanıcı yönetim paneli, daha fazla analiz gibi alanlarda fonksiyonel programlamanın veri hattı dönüşümlerine iyi uyduğunu da biliyorum.
Ama Haskell ya da OCaml statik diller.
Sonradan büyüyüp ölçeklenirken de dinamik kalmak istiyorsam Clojure veya Elixir iyi bir seçim gibi geliyor.
Aynı zamanda bir gün refactor gerektiğinde her şeyin kırılmasından korkuyorum.
Şu anda Python ve Mypy kullanıyoruz, frontend’i de NiceGUI backend tarafından üretiyor.
cabal replile geliştirme aşamasındaki bir web uygulamasını çok hızlı yeniden yükleyebilirsiniz.Açıkçası birçok Haskell kullanıcısının bundan yeterince yararlanmadığını düşünüyorum.
Benzer bir sistemi önce Scheme, sonra da Racket gibi nispeten niş bir dilde geliştirmiştim; sistem büyüdü ama küçük bir ekip uzun süre sürdürülebilir ve hızlı kalabildi.
Çok fazla bug üretmedik ve genellikle özellikleri çok hızlı ekleyebildik.
Örneğin hassas verileri AWS üzerinde barındırmak için gereken belirli bir sertifikasyonu ilk alanlardan olmuştuk.
Bazen, popüler platformlarda hazır bileşenlerle çözülecek şeyleri sıfırdan yapmak gerektiği için özellik ekleme yavaşlıyordu.
Ama bir kez yapıldı mı iyi çalışıyordu; sonra eski hızımıza geri dönüyor ve onlarca hazır framework’ün şişkinliği ve karmaşıklığıyla yavaşlamıyorduk.
Yönetilebilir platformu kendimiz kontrol ettiğimiz için ihtiyaç doğduğunda AWS’ye hızlıca geçebilmiştik.
Sistemin en başından beri karmaşık veri ve web etkileşimleri için mimari sihir denebilecek bir yapısı vardı; bu da pek çok özelliği hızlı geliştirmeyi sağladı ve sonrasında da akıllıca yön verici oldu.
Haskell tabanlı fintech örneğinden farkı, ekibin çok küçük olmasıydı.
Aynı anda sadece 2-3 yazılım mühendisi vardı ve operasyonları da yürüten biri bulunuyordu.
Bu yüzden yüzlerce kişinin koordinasyon içinde tutarlı bir sistem sürdürmesi gibi bir zorluk yaşanmadı.
Genelde bir kişi daha teknik ve mimari kod değişikliklerini yaparken, diğer kişi karmaşık süreçlere dair büyük miktarda iş mantığı özelliklerini hızla ekliyordu.
Bugünün ya da yakın geleceğin LLM benzeri yapay zeka araçları dikkatli kullanılırsa, yazılım geliştirmede çok küçük ama inanılmaz derecede etkili ekiplerin verimliliğinin bir kısmını yeniden yakalamamızı sağlayabilir.
Aklımdaki model, story point’leri ortadan kaldırmaya çalışırken devasa şişkinlik üretip sürdürülebilirliği başkalarının sorunu haline getirmek değil; az sayıda çok keskin düşünürün sistemi hem güçlendirip hem de yönetilebilir bir yolda tutması.
Bu iki ucu keskin bir kılıç.
2 milyon satır büyük bir başarı ama aynı zamanda ciddi bir bakım yükü de demek.
Haskell’in avantajları teorik olarak açık ama dezavantajlarını sezmek daha zor.
Baştan çıkarıcı olan, her şeyi tipler üzerinden modellemek.
Bu durumda kod tabanı uygulamanın kendisinden çok iş spesifikasyonu haline geliyor.
Her politika değişikliği büyük bir refactor’a dönüşüyor ve Haskell’in güvenliği sayesinde bu, şaşırtıcı derecede emek yoğun olabiliyor.
Sonunda ikisini birden elde edemezsiniz; bir noktada tiplerin içinde sıkışıp kalırsınız.
Haskell özellikle bu ölçekte gerçekten etkileyici ve güçlü ama kendine özgü sorunları da beraberinde getiriyor.
İş mantığını tipler üzerinden modelleme dürtüsü katı yapılar yaratabiliyor ve o yapıların sağladığı güvenlik, başka türden riskleri görmeyi zorlaştırabiliyor.
Her şeyi elde edemezsiniz ama çok şeyi elde edebilirsiniz.
Birkaç yıl önce Jane Street’te staj yapmıştım; Haskell değil OCaml kullanıyorlardı ama o dengeyi gerçekten iyi kuruyor gibi görünüyorlardı.
İçsel karmaşıklığın yüksek olduğu, güvenilirlik ve doğruluğun işin varlığı için kritik olduğu bir alanda olmalarına rağmen şaşırtıcı derecede hızlı ilerliyorlardı.
Geriye dönüp bakınca Jane Street’in anahtarı, Stephen Weeks gibi zevki çok iyi olan deneyimli OCaml programcılarını işe almaları ve onların en baştan çekirdek kütüphaneleri oluşturup tüm kod tabanına yön vermesiydi.
Ne yazık ki Mercury bu kısmı o kadar iyi yapamadı.
Dürüst olmak gerekirse, Turing-complete bir tip sisteminin en büyük dezavantajı, teoride derlendiğinde toza dönüşecek bir uygulamayı hayata geçirebilmenizdir.
Bellroy’un benzer bir Haskell başarı hikâyesi de yakında yapılacak Melbourne Compose buluşmasının konusu: https://luma.com/uhdgct1v
Fonksiyonel programlamada yaşadığım sorun debugging.
Daha doğrusu, bunu imperatif programlamanın özellikle de prosedürel tarzın güçlü yanı olarak görüyorum.
Fonksiyonel/deklaratif stilde genellikle bir şeyin nasıl oluşturulduğunu değil, hangi durumda olması gerektiğini anlatırsınız; dili de her şeyi birleştirip nihai sonucu üretmeye bırakırsınız.
Her şeyi doğru yaptıysanız güzel, hatta daha da iyi olabilir; ama olmadığında ve beklediğiniz sonuç çıkmadığında bug’ı nasıl bulacağınız mesele oluyor.
C gibi bir dilde bu nispeten basit.
Satır satır ilerler, her adım arasındaki çalışma durumuna yani fiilen RAM’e bakarsınız; beklentiden sapma varsa sorun o satırdadır, oradan girip devam edersiniz.
Dil durum bilgisini ne kadar saklamaya çalışırsa, fonksiyonel programlamada olduğu gibi, bu iş o kadar zorlaşıyor.
Yazıdaki en uzun bölümün tam da bu konu yani “design for introspection” olması da ilginç.
Yazar, kodu debug edilebilir hale getirmek için bilerek ciddi çaba harcamak zorunda kaldı ve bu da Haskell’in çoğu zaman gözden kaçan pratik kullanımı hakkında iyi bir içgörü veriyor.
Önemsiz kodlarda bile bunu yaparım.
Diğer ana akım diller buna yaklaşamıyor bile.
Shared-memory concurrency gibi bunun mümkün olmadığı durumlarda transaction kullanırım.
Buna da diğer ana akım diller yaklaşamıyor.
Buna null olmaması, örtük integer cast olmaması gibi kolay avantajları henüz eklemedim bile.
Haskell kodunda debugging’in diğer dillere göre daha zor olduğu tamamen doğru.
Ama ilk yüzde 90’lık ayak bağlarını ortadan kaldırınca bunun böyle olması kaçınılmaz.
Elbette bu yalnızca fonksiyonel dillere özgü değil; Python ya da JavaScript gibi büyük ölçüde imperatif dillerde de Python shell, tarayıcı konsolu, Node/Deno/Bun shell’leri, notebook’lar vb. ilk debugging katmanı olarak sık kullanılır.
REPL merkezli debugging’in ilginç ödünleşimleri var.
C gibi dillerde genelde tüm programı debug edip breakpoint’lerle başlayarak sorunun olabileceği doğru noktayı bulmaya çalışırsınız.
REPL merkezli dünyada ise programın bileşenlerini REPL içinde doğrudan daha fazla test edilebilir hale getirmeye çalışırsınız.
Böylece modül/API/tip sınırları debug edilebilirlikle daha çok örtüşür.
C/C++ gibi imperatif dillere kıyasla bu sınırları doğru ve kullanımı kolay kurma baskısı bazen daha yüksek olur.
Buna karşılık, tüm program öncelikli debugging’e göre gerçek hayattaki tuhaf senaryolarda birimler arası karmaşık entegrasyon sorunlarını izole etmek daha zorlaşabilir.
Ama REPL öncelikli yaklaşım çoğu zaman entegrasyonun yüzey alanını en aza indirmeye teşvik eder; bu yüzden fonksiyonel dillerde, imperatif dillerde görülen bazı entegrasyon etkileri daha az ortaya çıkar.
Fonksiyonel dillerin durumu sakladığını söylemek de tam doğru değil.
Bu diller de imperatif donanım üzerinde çalışır ve gerçek donanım durumuyla uğraşır.
Bir noktada bu iki dünya arasında bir çeviri vardır ve muhtemelen düşündüğünüz kadar farklı da değildir.
Gerekirse yine imperatif breakpoint’lere ve imperatif debugger’a dönebilirsiniz.
Bu yüzden buna “REPL odaklı” debugging diyorum.
REPL ile sorunlu birimi yani şaşırtıcı çıktı üreten doğru modülü/API’yi/fonksiyonu ve girdiyi daraltabilirsiniz.
Yalnızca kaynağa bakarak bug görünmüyorsa bunu imperatif debugger’a verip neredeyse aynı satır satır ilerleme deneyimini yaşayabilir, ek bağlam da elde edebilirsiniz.
O noktada REPL ile zaten yeterince daraltmış olacağınız için birimin kendisi küçük ve sınırlı olur; dolayısıyla iyi bir breakpoint seçme ihtiyacınız da büyük ihtimalle azalır.
Bence yazının “design for introspection” bölümünden çıkarılan mesaj biraz yanlış.
O bölüm debugging değil, observability ile ilgiliydi.
Loglama/telemetry sistemini doğru bağlamak, testlerde fake’ler/mock’lar kullanmak ve bunu tek tek kütüphanelere bırakmak yerine retry/circuit breaker mekanizmalarını sistem düzeyinde eklemekten söz ediyordu.
Imperatif dünyada da bu bir debugging sorunu değil; dependency injection, middleware kurma ve herkese açık API sınırlarında somut sınıflar yerine soyut arayüzler kullanma gibi ayrıştırma meseleleri.
Bu tür tasarım önerileri refactor niteliğindedir ve debugging’den çok, başkasının public API’sine observability middleware’ini ne kadar kolay yerleştirebildiğinizi etkiler.
2 milyon satır Haskell’in ne yaptığını hayal etmek bile zor.
Kod çok fazla ve Haskell, az kodla çok iş yapılabilen “yoğun” bir dil gibi bir izlenim bırakıyor.
JSON serileştirme/deserileştirme, REST API framework’leri, logging gibi şeyler için çok sayıda kütüphane olduğu için mi böyle diye düşündüm.
Bir third-party binding somut fonksiyonlarla HTTP çağrısı yapıyorsa ona trace eklemenin bir yolu yoktur, SLO’ya uygun timeout enjekte etmenin yolu yoktur, testte partner arızasını simüle etmenin yolu yoktur ve trace’teki 400 ms’lik boşluğu teoriler uydurmak dışında açıklamanın bir yolu yoktur.
Bu yüzden kendileri yazmışlar.
Başta daha çok iş çıkarıyor ama kendi yazdıkları client’lar en baştan bu amaçla tasarlandığı için observability göz önünde bulundurularak inşa edilmiş oluyor.
Yani görece çok soyut fikirleri az karakterle ifade edebilmek demektir.
Buna “yüksek seviye” diyenler de olur.
Ama bence 2 milyon satır ilk duyulduğu kadar da büyük bir sayı değil.
Özellikle finans gibi regülasyonun yoğun olduğu bir sektörde faaliyet gösteren ve yıllar içinde kod biriktirmiş bir şirket için.
Satır sayısı biraz daha düşük olabilir ama kelime sayısı, daha imperatif nesne yönelimli dillere genel olarak oldukça yakın.
Oralarda
St M -> C Tgibi bir yazım kabul edilebilir ama gerçek yazılımdaTransactionState Debit -> Verified Transactiongibi yazmak çok daha faydalıdır.Bir başka kısmı da LISP’e kadar giden kültürel bir etkiden kaynaklanıyor.
İnsanlar, anlaması zor numaralar veya makrolarla satır sayısını azaltmak için gereğinden fazla zekice davranma eğiliminde oluyor.
Mercury gibi bir finans şirketinde bunun yerine açıklık ve okunabilirliğin teşvik edildiğini tahmin ederim.
Örneğin linter, monadik kodu
>>ve>>=ile tek satıra yığmak yerine, özenli çok satırlıdoifadelerine bölmeye zorlayabilir.