17 puan yazan GN⁺ 2025-07-26 | 8 yorum | WhatsApp'ta paylaş
  • Programlama sırasında tip sistemi kullanılarak farklı veri anlamları açıkça birbirinden ayrılabilir
  • String veya integer gibi genel tipleri olduğu gibi kullanmak, bağlamın kaybolmasına yol açar ve hatalara neden olabilir
  • Aynı temel tipe dayansa bile amaca uygun yeni tipler tanımlanırsa, derleme zamanında hatalarla yanlışlar önlenebilir
  • Go kütüphanesi libwx, ölçü birimlerini açıkça ayıran tipler tanımlayarak float64 karışımından kaynaklanan hataları önler
  • Örnek kodda UUID tipi UserID ve AccountID olarak ayrılarak yanlış kullanım derleyici tarafından engellenir
  • Go gibi tip sistemi çok güçlü olmayan dillerde bile basit tip sarmalama ile hatalar önlenebilir

Tip sistemini aktif biçimde kullanalım

Sorunun başlangıç noktası: basit tiplerin karıştırılması

  • Programlamada string, int, UUID gibi temel tiplerle birçok değeri ifade etmek yaygındır
  • Ancak projenin ölçeği büyüdükçe bu basit tiplerin ayırt edilmeden birbirine karıştırılarak kullanılması sık görülür
    • Örnek: userID string’ini yanlışlıkla accountID olarak geçirmek ya da int parametresi 3 tane olan bir fonksiyonda sırayı yanlış vermek

Çözüm: niyeti ortaya koyan tip tanımları

  • int ve string yalnızca yapı taşlarıdır; bunları sistem genelinde olduğu gibi geçirmek anlamlı bağlamın kaybolmasına neden olur
  • Bunu önlemek için her role özgü benzersiz tipler tanımlanmalı ve kullanılmalıdır
    • Örnek:
      type AccountID uuid.UUID  
      type UserID uuid.UUID  
      
      func UUIDTypeMixup() {  
          {  
              userID := UserID(uuid.New())  
              DeleteUser(userID)  
              // hata yok  
          }  
      
          {  
              accountID := AccountID(uuid.New())  
              DeleteUser(accountID)  
              // hata: AccountID tipi UserID olarak kullanılamaz  
          }  
      
          {  
              accountID := uuid.New()  
              DeleteUserUntyped(accountID)  
              // derleme zamanında hata yok, çalışma zamanında sorun çıkma olasılığı yüksek  
          }  
      }  
      
  • Böylece yanlış tipte argümanlar derleme zamanında engellenebilir

Gerçek uygulama örneği: libwx kütüphanesi

  • Yazar, kendi Go kütüphanesi libwx içinde bu tekniği uyguluyor
  • Tüm ölçü birimleri için özel tipler tanımlanıyor ve birim dönüşüm metotları da bu tiplere bağlanıyor
    • Örnek: Km.Miles() metodu ile birimler açıkça ayrılıyor
  • Aşağıda, yanlış fonksiyon argüman sırası ile birim karışıklığını derleyicinin engellediği bir örnek yer alıyor:
    // Fahrenheit sıcaklığı tanımı  
    temp := libwx.TempF(84)  
    
    // Bağıl nem tanımı (yüzde)  
    humidity := libwx.RelHumidity(67)  
    
    // Fahrenheit yerine Celsius sıcaklığı bekleyen fonksiyona yanlış değer geçiliyor  
    fmt.Printf("Dew point: %.1fºF\n",  
      libwx.DewPointC(temp, humidity))  
    // derleyici tip uyuşmazlığı hatasını anında yakalar  
    // temp (TempF tipi) TempC olarak kullanılamaz  
    
    // Fonksiyona argüman sırası yanlış veriliyor  
    fmt.Printf("Dew point: %.1fºF\n",  
      libwx.DewPointF(humidity, temp))  
    // derleyici argüman tip hatasını engeller  
    
  • Yalnızca float64 kullanılsaydı ortaya çıkabilecek hataların tamamı önlenebilir

Sonuç: tip sistemini aktif kullanın

  • Tip sistemi yalnızca sözdizimi denetimi için değil, hata önleme aracıdır
  • Her model için ID tipleri ayrı tanımlanmalı, fonksiyon argümanları da float veya int yerine açık tiplerle sarılmalıdır
  • Bu yaklaşım, Go gibi tip sistemi çok güçlü olmayan dillerde bile çok etkilidir ve uygulanması da basittir
  • Gerçek dünyada UUID veya string tiplerinin karıştırılmasından kaynaklanan hatalar gerçekten çok yaygındır
  • Yazar, bu basit yaklaşımın üretim kodunda yaygın biçimde kullanılmamasının şaşırtıcı olduğunu vurguluyor

İlgili kod

8 yorum

 
vk8520 2025-07-29

Kotlin'de kullanmaya çalıştığınızda, primitive'lerin wrapper'larla sarılması nedeniyle stack yerine heap'te saklanmasından kaynaklanan bir performans sorunu olabileceğini biliyorum. Elbette çoğu kullanım senaryosunda bakım kolaylığı önceliklidir. Ayrıca, performans sorununu en aza indirmek için value class kullanılabilir.

 
regentag 2025-07-28

Ada dili bu açıdan gerçekten çok iyi bir tip sistemine sahip. Türü farklı olan değerler kolayca ayrı tipler olarak tanımlanabiliyor ve karıştırıldıklarında derleyici bunu iyi yakalıyor.

 
roxie 2025-07-28

Merakımdan soruyorum. Diğer yaygın tipli dillerden farklı başka avantajları da var mı? (kotlin, rust, typescript, ... )

 
regentag 2025-07-28

Ada'nın avantajları genel olarak "C'den daha iyi" tarafında. C'de geliştiriciye güvenilip izin verilen şeylerin kısıtlanmaması büyük bir etken; örtük tür dönüşümleri gibi şeyler de buna dahil. Ama çoğu geliştirici alışkın oldukları için sanırım yine de C'yi daha çok seviyor gibi görünüyor...

Bu, üzerinde çalıştığım kod tabanının bir özelliği olabilir ama neredeyse her şeyi ayrı bir tür olarak tanımlayıp kullanıyoruz. Temel türleri kullandığımız tek yer neredeyse dizi indeksleri.

 
roxie 2025-07-28

Anladım, teşekkürler

 
GN⁺ 2025-07-26
Hacker News yorumu
  • Bu yaklaşımı seviyorum; “kötü durumu ifade düzeyinde imkânsız kılmak” fikrine dayanıyor. Ancak bu kalıpta sık görülen sorun, geliştiricilerin tip uygulamasının yalnızca ilk aşamasında kalması. Her şey bir tipe dönüşüyor, birbirleriyle iyi uyum sağlamıyor ve hafifçe değiştirilmiş çok sayıda tip ortaya çıktığı için kodu takip etmek ve anlamak zorlaşıyor. Böyle bir durumda, zayıf tip uygulanmış dinamik bir dilde (JS) ya da güçlü tipli dinamik bir dilde (Elixir) yazmayı tercih ederim. Ama geliştiriciler koşullu mantığı pattern matching yapılabilen union type’lara taşımaya devam eder, delegation’ı iyi kullanır ve tip güdümlü akışı ileri götürürse, geliştirme deneyimi yeniden keyifli hale geliyor. Örneğin DewPoint fonksiyonu, birden çok tipi kabul etse de doğal biçimde çalışacak şekilde tasarlanabilir.

    • Bu yüzden daha fazla dilin bounded (Integer aralığı kısıtlı) tipleri yerleşik olarak desteklemesini isterdim. Örneğin x: u32 yerine, tip sisteminin x için yalnızca [0,10) aralığını zorunlu kılabilmesini isterdim. Böylece dizi indekslemede bound check gerekmezdi. Option gibi durumlarda da peephole optimizasyonu çok daha kolay olurdu. Rust’ta fonksiyon içinde LLVM sayesinde bunun bir kısmı var, ama fonksiyonlar arasında değişken aktarımında yok.

    • Bu arada Ruby zayıf tipli değil, güçlü tipli bir dildir. 1 + "1" gibi bir işlem yaparsanız TypeError: String can't be coerced into Integer benzeri bir hata alırsınız.

    • “Tip uygulamasının ilk aşamasında kalmak” başarısızlığın nedeni. Örneğin int’i struct ile sarıp UUID olarak kullanmaya başlamak iyi bir başlangıçtır; ama biri elindeki herhangi bir int’i sarıp geçirirse, gerçekte benzersiz olması gereken UUID özelliği bozulabilir. Sonuçta önemli olan “Correct by construction” yani daha inşa edilirken doğruluğun garanti edilmesi. UUID gibi benzersiz olması gereken tipler, bir fonksiyon ya da kurucu içinde istisna fırlatmak dâhil gerçekten kanıtlanmadıkça üretilememeli. Bu kavram yalnızca UUID için değil, her tip ve invariant için geçerli.

    • Son zamanlarda Red-Green-Refactor kalıbını izliyorum; ama başarısız test yazmak yerine tip sistemini daha katı hale getirip hataların type checker tarafından yakalanmasını sağlıyorum. Yeni özellikler, edge case’ler ya da tiplerin hata üretmesini sağlayamadığı bug’lar için hâlâ test kullanıyorum; ama tip sisteminden yararlanan red-green-refactor genelde daha hızlı ve büyük bir hata sınıfını tamamen engelleyebiliyor.

    • Structural type’larla sorunların çoğu hafifletilebilir. Gerçekten gerekirse nominal type’larla zorunlu da kılınabilir.

  • İstisnalar ve tiplerle komşu bir konu olarak, checked exception’ların iyi kullanıldığında tipe göre uygun işlemeyi sağladığını düşünüyorum. Java’nın checked exception’larının neden bu kadar eleştirildiğini hiç anlamadım. Sorumlu olduğum bir projede checked exception kullanımını zorunlu kıldığımda başlangıçta herkes nefret etti; ama kod akışındaki tüm istisna durumlarını düşünmeye alışınca herkes sevmeye başladı. Unit testlerde o kadar katı değildik ama proje çok sağlam hale geldi.

    • Java checked exception’larına yönelik şikâyet, istisna işlemenin çok zahmetli olması. Kütüphane yazarı checked exception’ları net biçimde belirleyemiyor ve istemci tarafında her fonksiyon çağrısında gereksiz yere exception handling yapmak gerekiyor; bu da insanı soğutuyor. İstisnaları başka tiplere ya da runtime exception’a kolayca dönüştürebilmek veya modül/uygulama düzeyinde sadece bildirmek mümkün olsa bu sorun azalırdı, ama fazlasıyla zahmetli. Ayrıca imzaları kolay bozduğu için alan-özel istisnalar kullanmak gerekiyor; Java da exception dönüştürmeyi rahatsız edici hale getiriyor. Checked exception iyi bir fikir, ama Java’daki exception handling kullanılabilirliğini sevmiyorum.

    • Checked exception’ların eleştirilmesinin nedeni kötüye kullanımlarıydı. Java’nın hem checked hem unchecked exception desteklemesi iyi bir tercih. Ama Eric Lippert’in dediği gibi yalnızca “exogenous” türü istisnalarda checked exception kullanıp çoğunu unchecked’e çevirmek daha doğru. Örneğin veritabanı bağlantısı her an kopabilir; ama throws SQLException ifadesini tüm call stack boyunca taşımak fazla zahmetli. En üst seviyede catch-all ile yakalayıp HTTP 500 dönmek yeterli. İlgili yazı

    • Checked exception’lar (unchecked olanlara kıyasla), call stack’in derinindeki bir fonksiyon istisna fırlatacak şekilde değişirse yalnızca handler’ı değil aradaki tüm fonksiyonları da değiştirmeyi gerektirebilir. Yani sistem değişikliğinde esnekliği azaltır. Async function coloring tartışması da benzer bağlamda. Bir fonksiyon exception fırlatabiliyorsa ya try/catch ile sarmalamanız ya da çağıranın da exception fırlattığını beyan etmesi gerekir.

    • C# tipler konusunda açık ama unchecked exception yaklaşımını benimsemiş. Hata stack’i temiz kalıyor ve sorun çıkmıyor. Her seviyede özel özel iş yapan pattern matched exception handler’lardan daha temiz. Sağlam bir unwrap edilebilir error result varsa onun da benzer olduğunu düşünüyorum.

    • Java’da checked type’ların kullanılabilirliği zayıf; örneğin stream API kullanırken map/filter fonksiyonlarında checked exception fırlatmak gerçekten can sıkıcı. Birden fazla servis çağrısının her birinin kendi checked exception’ı varsa, sonunda ya Exception yakalamak ya da absürt derecede uzun exception listeleri yazmak zorunda kalıyorsunuz.

  • Genel olarak “özgün tipler oluşturma” yaklaşımına katılıyorum; ama her şeyin özgün tip olduğu sistemlerde çok zorlandığım oldu. Özellikle sadece byte taşıyan kodla alan hesaplaması yapan kod birbirine karıştığında iş gerçekten zorlaşıyor.

    • O hissi anlıyorum. Elinizde zaten gerekli veri var ama önce bir tip oluşturmanın ya da bir instance üretmenin yolunu bulmanız gerekiyor; ortada tarif yoksa dokümantasyonla boğuşuyormuşsunuz gibi geliyor. Örneğin elinizde {x, y, z} nesnesi var ama önce createVector(x, y, z): Vector kullanmanız gerekiyor; sonra bir Face oluşturmak için createFace(vertices: Vector[]): Face gibi şeyler çağırıyorsunuz, bu da gereksizce prosedürel hissettiriyor. BouncyCastle gibi durumlarda da byte array hazır olsa bile, istediğiniz işleve ulaşmak için önce birden fazla tip oluşturup bunların method’ları üzerinden ilerlemeniz gerekiyor.

    • Go’da type alias’ı asıl tipe geri çevirmek (AccountID → int gibi) oldukça kolay. Yapıyı düzgün kurarsanız, alan mantığı tarafı type alias’ları kullanırken alanı umursamayan kütüphane tarafı higher/lower tipler arasında çeviri yaparak clean architecture tarzı bir düzen de kurabilir. Ama bunun için çok fazla dönüşüm kodu gerekiyor.

    • Phantom type’lar bu durumda kullanışlı. Tip parametresi yani generic ekliyorsunuz, ama o parametreyi gerçekte hiçbir yerde kullanmıyorsunuz. Eskiden Scala’da kriptografi kodu yazarken dizilerin hepsi byte idi; ama phantom type’larla bunların birbirine karışmasını önlüyordum. İlgili örnek

    • İdeal durumda derleyicinin yalnızca tipleri denetlemesini, geri kalan alan mantığını da basit byte kopyalamaya indirmesini isterdim. Tabii seni doğru anladıysam.

  • Bence tip sistemlerinde de 80/20 kuralı geçerli. Aşırıya kaçınca kütüphane kullanımı külfetli hale geliyor ve gerçek getirisi çok az oluyor. UUID ya da String gibi şeyler tanıdık, ama AccountID, UserID gibi şeyleri bilmediğim için önce öğrenmem gerekiyor; bunun da maliyeti var. Gelişmiş tip sistemleri değerli olabilir de olmayabilir de, özellikle yeterince testiniz varsa. İlgili not

    • Sonuçta yazılımı kullanmak için Account ya da Userın ne olduğunu zaten bilmek zorundasınız. O yüzden getAccountById gibi AccountId alan bir fonksiyonu anlamak, UUID alan bir fonksiyonu anlamaktan daha zor değil bence.

    • Aslında String yalnızca bir byte kümesi, tek başına hiçbir anlamı yok. AccountID ise çoğu durumda “hesabın kimliği” demek. İç temsili gerçekten merak ediyorsanız tip tanımına bakarsınız; ama çoğu bağlamda AccountID’nin ne olduğunu bilmek yeterli. Tip denen şey, üstüne açık bir ad konunca kullanımı daha az kafa karıştırıcı oluyor. grugbrain.dev bağlantısı bence tersine fazla temel düzeyde; bir grug brain olsaydı bu kadar tip ayrımını muhtemelen desteklerdi.

    • foo(UUID, UUID) yerine foo(AccountId, UserId) çok daha iyi. Kendi kendini açıklıyor ve çağrıda sırayı yanlışlıkla değiştirirseniz derleyici bunu yakalayabiliyor. Karmaşık veri yapılarında da yeni tip oluşturmadan daha açık yazabiliyorsunuz.

      Map<UUID, List<UUID>>
      Map<AccountId, List<UserId>>
      
    • “UUID ya da String zaten tanıdık” denmesine karşılık, gerçekte UUID’nin GUIDv1, UUIDv4, UUIDv7 gibi hangi biçimde saklandığını/dönüştürüldüğünü anlamak her zaman kolay değil. Deneyimime göre Java + MS SQL kombinasyonunda UUID ile uniqueidentifier arasında dönüşüm yaparken endianness dönüşümü nedeniyle elle müdahale etmem gerekmişti. Tahminen veritabanlarının timezone’u otomatik çevirirken çıkardığı sorunlara benzer bir şey.

    • Aslında bu tipleri anlamak zaten yapılması gereken işti; yoksa yanlış veriyi olduğu gibi fonksiyona geçebilirdiniz.

  • Yakın zamanda bizim ekip de C++ kodunda farklı sayısal değerlerin birbirine karıştığı yerlere tip uyguladı. Bir hatayı bulup düzeltirken güvenli tipler eklemeye başladık ve sonra benzer yanlış değer kullanımının üç yerde daha olduğunu gördük.

  • mp-units(mp-units resmi dokümanı) kütüphanesi bu tür fiziksel birim sorunlarına odaklanan güzel bir örnek. Güçlü birim tipleri kullanınca güvenlik sağlanıyor, karmaşık birim dönüştürme mantığı otomatikleşiyor ve generic kodla farklı birimler işlenebiliyor. Bunu Prolog dünyasına taşımayı denemiştim ama çevremde pek ilgi görmedi. Prolog örneği

    • Bir zamanlar mesafe, hız, sıcaklık, basınç gibi farklı fiziksel niceliklerle çalışan bir projede bulundum; her şey sadece float olarak aktarılıyordu, bu yüzden mesafeyi hız yerine verseniz derleme geçiyor ve hata ancak çalışma anında ortaya çıkıyordu. Birimlerin (km/h ile miles/h gibi) yanlış aktarılmasında da aynı sorun vardı. Tipleri artırarak bu tür hataları geliştirme aşamasında yakalamak istiyordum ama o zaman junior’dım ve insanları ikna etmek zordu.

    • Fiziksel birimler için tip uygulamasının fazla karmaşık olacağından vazgeçmiştim ama mp-units’e bakmayı planlıyorum. Özellikle değişkenin hangi birimde olduğunu açıkça göstermemek sık sık sorun çıkarıyor. Dış veriler ya da standart fonksiyonlar gibi yerlerde birim belirtilmemesi çok yaygın.

  • C#’ta şu şekilde tip tanımlıyorum:

    readonly struct Id32<M> {
      public readonly int Value { get; }
    }
    

    Sonra

    public sealed class MFoo { }
    public sealed class MBar { }
    Id32<MFoo> x;
    Id32<MBar> y;
    

    böylece farklı integer ID’leri birbirinden ayırabiliyorum. Bunu IdGuid ya da IdString gibi türlere de genişletebilirsiniz ve yeni bir marker type (M) eklemek yalnızca bir satır sürer. TypeScript ve Rust’ta da benzer varyasyonlar kullanıyorum.

    • Benzer bir kalıp kullanmıştım. int ID söz konusuysa friction’ı en düşük şey aslında enum; ama fazla kafa karıştırıcı olabileceğini düşündüğüm için gerçek koda koymadım. İlgili tartışma

    • Bu kalıba “phantom type” denir; çünkü MFoo ya da MBar değerleri runtime’da gerçekten var olmaz.

    • Bu amaç için Vogen gibi kütüphaneler de var. Vogen, Value Object Generator anlamına geliyor ve source code generation ile value object tipleri eklemeyi destekliyor. README’sinde benzer kütüphaneler ve bağlantılar da bulunuyor.

  • Bu yaklaşımı daha önce de görmüştüm ama amacını bilmiyordum. Bugün de üç string argümanı alan bir fonksiyon yazarken tipi dışarıda parse etmeyi mi zorunlu kılsam, yoksa fonksiyon içinde mi yapsam diye düşünüyordum. Aslında parse edilmiş değere ihtiyacım yokmuş; demek ki aradığım cevap tam olarak buymuş. Muhtemelen bu yıl kodlama tarzımı en çok etkileyecek şey olacak.

  • Arkadaşım Lukas bu fikri “Safety Through Incompatibility” diye özetlemişti. Ben de bu kalıbı golang kodunun her yerine uyguladım ve çok faydalı buldum. Yanlış ID geçirilmesini en baştan engelliyor.
    İlgili yazı 1
    İlgili yazı 2

  • Swift’te typealias anahtar sözcüğü var ama temel tip aynıysa birbirine serbestçe dönüştürülebildikleri için pratikte bu amaç için uygun değil. Wrapper struct Swift’te daha deyimsel ve ExpressibleByStringLiteral ile birlikte kullanıldığında fena değil. Yine de “güçlü tip takma adı” gibi yeni bir anahtar sözcük (typecopy gibi) olsa ve “bu aslında sadece bir String ama özel anlam taşıyan bir String; diğer String’lerle karıştırma” demeyi mümkün kılsa güzel olurdu.

    • Aslında çoğu dil bu şekilde davranıyor; örneğin rust/c/c++ da öyle. Go örneğinde olduğu gibi wrapper type yazmak zorunda olmadığınız durumlar hoş oluyor. C++’ta kurucuyu explicit olarak işaretlemezseniz int’i Foo beklenen yere serbestçe verebildiğiniz için ekstra dikkat gerekiyor.

    • Teoride zarif görünse de pratikte uygulaması karmaşık olabilir. C++’ta std::cout ile yazdırmak ya da eskiden String alan üçüncü taraf fonksiyonlar veya extension point’lerle uyumluluk gibi konular gerçek hayatta düşünülmesi gereken şeyler.

    • Haskell’de bunun karşılığı newtype. OOP dillerinde tip final değilse, kolayca alt sınıf üretip istediğiniz davranışı ekleyebilir ya da özelleştirebilirsiniz. Ekstra wrapper ya da boxing olmadan ucuz ve basit olur. Ama Java’da String final olduğu için bu yol zor; String’i özelleştirmek kolay değil.

    • Somut olarak, bunun wrapper struct’tan hangi açıdan farklı çalışmasını isterdiniz?

 
brain1401 2025-07-28

Rust da zaten bu şekilde kullanılıyor; kesinlikle iyi bir yaklaşım gibi görünüyor.

 
regentag 2025-07-28

Tip sistemi iyi olan bir dil kullansaydınız, bunun gibi şeyleri de önleyemez miydiniz?..
1999 Eylül'ünde NASA Mars Climate Orbiter'ın kaybolması

  • Kuvvet büyüklüğünü ifade ederken pound birimini kullanan modülle newton birimini kullanan modül arasındaki veri entegrasyonu sorunu nedeniyle sondanın yanlış yönlendirilip düşmesi.