3 puan yazan GN⁺ 2025-05-18 | 1 yorum | WhatsApp'ta paylaş
  • Bir fonksiyonun içindeki koşullu dallanmaları çağıran tarafa yukarı taşıyıp, tekrar edilen hedef işleri tek tek çağrılar yerine toplu işlemler olarak aşağı indirirseniz hem kod akışını hem de performansı sadeleştirebilirsiniz
  • if yukarı taşındığında önkoşulları tipler veya assert’lerle garanti etmek kolaylaşır; fonksiyon içindeki yinelenen kontroller ve gereksiz dallanmalar azalır
  • Dallanma mantığı birden çok fonksiyona dağılırsa ölü koşulları veya tekrarlanan kararları bulmak zorlaşır; enum oluşturup sonra tekrar match etmek de aynı dallanmayı veri yapısı olarak yineleyen bir biçime dönüşebilir
  • for aşağı indirildiğinde birden çok nesne tek seferde işlenerek başlangıç maliyeti amorti edilir; işleme sırasını yeniden düzenleme veya vektörleştirme gibi optimizasyon fırsatları doğar
  • İki kural birlikte kullanıldığında döngü içindeki koşul yeniden değerlendirmeleri ve sıcak döngülerdeki dallanmalar azalır; yapı, kontrol düzlemindeki karar verme maliyetini veri düzlemindeki toplu işlemeye devretmeye yaklaşır

if’i çağıran tarafa yukarı taşımak

  • Bir fonksiyonun içinde if koşulu varsa, önce bu koşulun çağıran tarafa taşınıp taşınamayacağını değerlendirebilirsiniz
    • Option<Walrus> alıp None ise dönen bir fonksiyon yerine, zaten Walrus alan bir fonksiyon daha basit bir biçimdir
    • Fonksiyonun önkoşulu kontrol edip “hiçbir şey yapmaması” yerine, çağıran taraf kontrol ettikten sonra bunu tip veya assert ile garanti edebilir
  • Önkoşul kontrollerini yukarı taşıma yaklaşımı tüm çağrı yolu boyunca yayılabilir ve sonuçta kontrol sayısını azaltmak için bir motivasyon oluşturur
  • Kontrol akışı ve if, kod karmaşıklığını ve hata olasılığını kolayca artırır
    • Karmaşık dallanma mantığı üst düzey tek bir fonksiyonda toplanıp, asıl işin doğrusal alt fonksiyonlara devredildiği yapı daha okunabilirdir
    • Karmaşık kontrol akışı dosyanın tamamına dağılmak yerine tek ekrandaki tek bir fonksiyonda toplandığında yinelenen koşulları ve ölü koşulları fark etmek daha kolaydır
  • “dissolving enum” refactoring’i, enum oluşturma ile match aynı dallanmayı tekrarladığında faydalıdır
    • f() koşula göre E::Foo veya E::Bar oluşturuyor, g() da tekrar match edip foo() veya bar() çağırıyorsa dallanma iki kez görünür
    • Koşul yukarı çekildiğinde main() içinde aynı koşulla doğrudan foo(x) veya bar(y) çağrılan bir biçime dönüşür

for’u toplu işlem tarafına aşağı indirmek

  • Veri odaklı düşüncede programlar genellikle birden çok nesneden oluşan kümelerle çalışır; sıcak yollar da çoğu zaman çok sayıda entity işlediği için sıcak hâle gelir
    • Nesneler için toplu işlem kavramını tanıtmak ve skaler sürümü toplu sürümün özel bir durumu olarak görmek faydalıdır
    • for walrus in walruses { frobnicate(walrus) } yerine frobnicate_batch(walruses) daha iyi bir biçimdir
  • Toplu işlemenin başlıca avantajı performanstır
    • Birden çok hedefi tek seferde ele almak başlangıç maliyetini amorti eder
    • İşleme sırasını esnek biçimde değiştirebilirsiniz; entity’leri belirli bir sırayla işleme gereği de azalır
    • Tüm entity’lerin önce bir alanını işleyip sonra başka bir alana geçmek gibi vektörleştirme veya struct-of-array teknikleri kullanılabilir
  • Uç bir performans örneği olarak Vectorized Interpreters Talk bağlantısı veriliyor
  • İlginç bir örnek olarak FFT tabanlı polinom çarpımı var
    • Polinomu birden çok noktada aynı anda değerlendirme yöntemi, tek tek nokta değerlendirmelerini defalarca yapmaktan daha hızlı olabilir
  • if ve for kuralları birlikte uygulanabilir
    • Koşul döngünün dışında olursa condition tekrar tekrar yeniden değerlendirilmez
    • Sıcak döngüden dallanma kaldırılır
    • Vektörleştirme olasılığı açılır
  • Aynı örüntü hem mikro hem de makro düzeyde çalışır
    • TigerBeetle’ın yapısı, veri düzleminde nesne topluluklarını aynı anda işleyerek kontrol düzlemindeki karar verme maliyetini amorti eden bir yaklaşımdır
  • for kuralının başlıca motivasyonu performans olsa da ifade gücüne de yardımcı olabilir
    • jQuery öğe koleksiyonları üzerinde çalışıyordu ve geçmişte başarılıydı
    • Soyut vektör uzayı dili, koordinat bazlı denklem kümelerine göre kimi zaman daha iyi bir düşünme aracı olabilir

1 yorum

 
GN⁺ 2025-05-18
Hacker News yorumları
  • Benim tuhaf düşünme modelim şu: Olası durumların ve program akışının bir ağacı var; koşul ifadeleri de bu ağacı buduyor.
    Mümkün olduğunca erken budayıp yalnızca daha az dal üzerinde çalışmak daha iyi.
    Sonuçta bir fonksiyonun ya program ağacında yürümeye ya da gerçek işi yapmaya odaklanmasını isterim.

    • Bu bakış açısı, programlama dili kuramında veya lambda hesabının küçük adım semantiğinde görülen yapıyla iyi örtüşüyor.
      İfadeler, indirgeme kurallarına göre tekrar tekrar “yeniden yazılarak” değerlendirilir. Örneğin (1 + 2) + 4, 3 + 4 olur, sonra da 7.
      Burada bir sonraki hangi alt ifadenin değerlendirileceğini belirleyen uyum kuralları ve ifadeyi gerçekten değiştirerek program durumunu değiştiren hesaplama kuralları vardır.
      Katı, yani tembel olmayan dillerin çoğu tüm alt ifadeleri üst ifadeden önce değerlendirir; ancak koşul ifadeleri ve sonsuz döngüler gibi özel yapılar istisnadır.
      Koşul ifadelerinde, uyum kuralları tüm alt ifadelerin değerlendirilmesini emretmeden önce hesaplama kuralı uygulanır ve kelimenin tam anlamıyla ifade ağacını budar.
      [1]: Benjamin C. Pierce, Types and Programming Languages önerilir
    • Benim düşünme modelim, yazmakta olduğum kodun içinde bulunduğu somut dünyaya uyum sağlamak.
      Alanın özelliklerine, mevcut kod tabanındaki kalıplara, veri hattının hangi aşamasında olduğuna, performans özelliklerine vb. bakmak gerekir.
      Eskiden böyle kod düzenleme kuralları ve sezgisel yöntemler oluşturmaya çalışırdım; ama yeterince kod yazınca soyutlama düzeyinin yanlış olduğunu ve üzerinde uzun süre durmaya değmediğini kabul ediyorsunuz.
      Bu tür tartışmaların sahte fonksiyon adlarına veya tek harfli değişkenlere dayanması da manidar. Çünkü dış bağlamı olmayan bir “kod adasında” neredeyse her kural makul görünebilir.
      gnin hnin tek çağırıcısı olduğu ve gelecekte de öyle kalacağı yönündeki rahat varsayımı yapmadan, bu kuralla ölü bir dalı ortaya çıkardığınızı söyleyemezsiniz.
      Gerçek kod tabanlarında genellikle g ile hnin en başta birleştirilmemiş olmasının bir nedeni vardır.
    • Yakın bir model ortaya atmak gerekirse, sınıflar isimdir, fonksiyonlar ise fiildir.
    • O kadar da tuhaf bir model değil; sonuna kadar götürürseniz fiilen Prolog’un yürütme modeline yaklaşır.
  • Daha genel kural, ifi girdinin kaynağına yakın tutmaktır: https://gieseanw.wordpress.com/2024/06/24/dont-push-ifs-up-p...
    Dışarıdan programa girilen giriş noktası, başka servislerden alınan veriler de dahil olmak üzere bu noktayı bulmak ve özellikle kaynak yoğun kısımlar olmak üzere çekirdek mantığa ulaşmadan önce mümkün olduğunca çok güvence oluşturacak şekilde düzenlemek esastır.
    Mümkünse bu güvenceleri tipe kodlamak daha iyidir.

    • Bu neredeyse doğrulama yapma, parse et ile aynı şey: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
    • O zaman çekirdek mantığı anlamaya çalışırken hangi varsayımları yapabileceğiniz daha da belirsizleşmez mi? Tüm çağrı zincirlerini tek tek takip edip kontrol etmeyi mi tercih ediyorsunuz?
  • “Bir fonksiyonun içinde if koşulu varsa, bunu çağıran tarafa taşıyıp taşıyamayacağını düşün” türünden gevşek bir tahminin karşı örneği çok fazla.
    Fonksiyon 37 yerden çağrılıyorsa hepsinde if ifadesini tekrar mı etmek gerekir?
    Bu fonksiyon getaddrinfo veya EnterCriticalSection ise API kullanıcısına ifi dışarı itmesi mi söylenmeli?
    Bence bu dönüşüm ancak en fazla iki yerden çağrılan dahili bir fonksiyon olduğunda ve o karar fonksiyonun ilgi alanının dışında kaldığında düşünülebilir.
    Başka bir yöntem, fonksiyonun yalnızca ifi çalıştırıp iki yardımcı fonksiyonu çağırmasını sağlamaktır.
    Çağıran tarafın döngü içinde koşulu dışarı çekmesi gerekiyorsa, düşük seviyeli bir “koşul yorumlama yardımcısı” kullanabilir. Döngü içi/dışı ayrımı değil, yalnızca tek bir if gerektiren çağırıcılar ise ifi gizleyen bir kolaylık fonksiyonu kullanabilir.
    Ancak bu optimizasyon içindir; optimizasyon da iyi program yapısıyla sık sık çatışır.
    Nesne yönelimli programlamada, çağrılan tarafın içindeki if kararı, hangi metodun çağrılacağını seçen metot dispatch olarak ortaya çıkar.
    Metot dispatch’i döngünün dışına alma tekniği de tasarımın akışına ters düşebilir.
    Örneğin bir canvas nesnesini raster bir görüntüyle doldurmak için görüntü pikselleri üzerinde dolaşıp canvas.putpixel(x, y, color) çağırmak istemezsiniz. Görüntüyü canvas’a veya onun dikdörtgen bir bölgesine blit eden bir metot olmalıdır.

    • Fonksiyon 37 yerden çağrılıyorsa, burada kastedilen daha çok fonksiyonu doğru/yanlış dallarını uygulayan iki fonksiyona bölüp bunların sırasıyla 21 ve 16 yerden çağrılmasını sağlayabileceğinizdir.
    • Buradaki kilit kelime consider.
      Bu yazı, özellikle tagged union gibi şeyler kullanırken ortaya çıkan oldukça belirli bir tasarım sorununu hedefliyor.
    • Fonksiyon 37 yerden çağrılıyorsa kodu refactor etmek gerekir; ama yine de cevap “duruma bağlı”dır.
      DRY doğru cevap gibi hissettirse de karar verebilmek için gerçek kod örneğini görmek gerekir.
      Bir kütüphane fonksiyonuysa konumu özeldir. Sahiplik sınırındadır, veri alanlar arasında dolaşır ve DDD terimleriyle söylersek sınırlandırılmış bağlamı aşar. Bu yüzden kendi alanını kendisi korumalıdır.
      EnterCriticalSection, girişte güçlü doğrulamanın, if koşulunu da içeren doğrulamanın, geçerli bir kod yoluna işaret ettiği ve alan sınırı olarak görülmesi gereken bir örnektir.
      Buna karşılık bir uygulama yazarken sıradan uygulama fonksiyonunda if ifadesi varsa güvenle dışarı itilebilir.
      Kütüphane veya kritik kod bölümlerinde bile ifi derin içeride değil kenara taşımak güvenlidir.
      Kendi alanını yönet, başkasının alanından talepte bulunma ve o alanın içinde kontrol akışını kenara taşı tavsiyesi kulağa makul geliyor.
      Elbette deyim deyimdir; gerçek dünyada ise bağlamı anlayıp makul karar verebilen birinin değerlendirmesi gerekir.
  • “Enum ayrıştırma refactoring’i” örneği özünde polimorfizmdir
    match, enum üzerinde polimorfik bir metot çağrısına dönüştürülebilir
    Amaç, durum ayrımının yapıldığı noktayı, yani ilk if ile buna bağlı olarak foo ya da bar çalıştırılan noktayı birbirinden ayırmaktır
    Durum ayrımını nesne — burada enum değeri ya da closure — yanında taşır; çağrı noktasında bunu tekrar etmek gerekmez
    Yani durum ayrımı değişirse, yalnızca bu ayrımın oluşturulduğu noktayı değiştirmek yeterli olur; her bir duruma özgü davranışı tetikleyen noktaları değiştirmek gerekmez
    Ancak bir ödünleşim var. Davranışın çalıştırıldığı noktada dikkate alınacak tekil durumları doğrudan görmek faydalı olabilir; ama bunun karşılığında tekil durumlar listesine kod düzeyinde bir bağımlılık eklenir

  • Kod karmaşıklığı tarayıcıları sonunda if’i aşağı itmeye zorlar. Yazı ise bunun tersini öneriyor
    if’i yukarı almak çoğu zaman kontrol akışını tek bir fonksiyonda merkezileştirir; bu fonksiyon karmaşık dallanma mantığına sahiptir, ama asıl işi doğrusal alt rutinlere devreder
    https://docs.sonarsource.com/sonarqube-server/latest/user-gu...

    • Çözüm, karar verme ile yürütmeyi ayırmaktır; bu kavramı Bertrand Meyer’den aldım
      if (weShouldDoThis()) { doThis(); }
      Bu, fonksiyonel çekirdeği ve imperatif kabuğu tamamlar ya da onun bir parçasıdır
      Kontrolleri ayrı tutmak test etmeyi kolaylaştırır; karmaşıklık dert oluyorsa kontroldeki her bir koşulu fonksiyona çıkarabilirsiniz
    • Kod tarayıcı raporları kutsal metin gibi kabul edilmemeli, sorgulanmalı
      Özellikle Sonar, gerçek hatalar değil “kod kokuları” raporlar
      Bu “hata olmayan” maddeleri düzeltirken yeni hata çıkarma riski 0’dan 0’dan büyük bir değere yükselir ve gerçek üretim sorunlarıyla ilgilenecek geliştirici zamanını boşa harcayabilir
    • Deneyimlerime göre bu çoğu zaman yerel optimumdur
      “Yerel” derken, gereksinimler değişene ya da istisnai durumlar keşfedilip döngünün dışında da dallanma gerekene kadar böyle olduğunu kastediyorum
      Hem döngünün içinde hem de dışında dallanma olduğunda akıl yürütmek zorlaşır
      Koşulun yalnızca döngünün içini etkileyeceğinden oldukça eminseniz, orada bırakabilirsiniz
      Ama döngünün dışında da dallanma gerektirecek bir gereksinimi hayal etmek zor değilse, yapıyı baştan buna göre tasarlamak daha iyi olabilir
      Kod daha uzun olabilir ama takip etmesi kolaylaşır ve ileride spagettiye dönüşme olasılığı da azalır
      Haskell’i bırakmamın nedeni de buydu. Haskell, en özlü ve “yerel optimum” mantığı yazma isteği uyandırıyor; ama bu, mantığın niyetinden çok mantığın kendisini ifade etmeye kayıyor. Küçük gereksinim değişikliklerinde korkunç biçimde söküp yeniden düzenlemek gerekebiliyordu
    • Tamamen okunabilir büyük bir fonksiyondan şikâyet edildiğini gördüğümden beri kod karmaşıklığı tarayıcılarından hep hoşlanmadım
      Mantığın tek yerde olması çok daha okunabilir; yalnızca ayrıntılar yüzünden büyük resmi kaçırmaya başladığınızda bölmeye çalışmalısınız
    • Dün bir LLM başlığında biri “insanların kodlamada benimsediği bir başka güvenilmez araç nedir” diye sordu; artık cevabım var
  • Bazen koşul mantığını çağrılan fonksiyonun içinde tutmayı tercih ederim. Çünkü çağıranın işleri yanlışlıkla yanlış sırada yapmasını engeller
    Örneğin idempotent bir işlem yapmak istiyorsanız, önce o işin zaten tamamlanıp tamamlanmadığını kontrol eder, tamamlanmadıysa çalıştırabilirsiniz
    Bu koşulu çağıranın dışına iterseniz, fonksiyonun tüm çağıranları idempotentlik garantisini elde etmek için doğru şekilde çağırdıklarını ayrı ayrı doğrulamak zorunda kalır; bu garantiyi soyutlayamazsınız
    Veritabanı transaction’ı içinde bir iş yapmadan önce bir dizi kontrol çalıştırmanız gereken durumlar da vardır. Bu felsefeyi uygularken kontrolleri transaction sınırının içinde nasıl tutmak gerekir?

    • Kontrol içermeyen bir fonksiyon yazıp, yalnızca kontrol yapan ve iç fonksiyonu çağıran bir wrapper fonksiyon koymak mümkün olabilir
    • Aslında cevap sorunun içinde
      Koşulu çağıranın dışına iterseniz o fonksiyon artık idempotent değildir; dolayısıyla elbette garanti sağlayamaz
      Yine de tekil bir fonksiyonun durum yönetimini uygulayıp idempotentlik sağlaması gerekiyorsa, bu oldukça tuhaf bir yapı olabilir ve tek bir fonksiyonun içinde fazla fazla mantık işliyor gibi görünür
      İdempotent kod genelde iki sınıfa ayrılır
      İlki, veri modelinin ve gerçekleştirilen işlemin kendisinin doğası gereği idempotent olduğu durumdur. Durumsuz bir işlem ya da giriş verisinin kaydedilmesi gereken tüm durumu taşıdığı PUT tarzı bir işlemdir
      Diğeri daha karmaşık bir iş işlemidir; rollback yaparak ya da kısmi başarısızlığın durumu bozmamasını garanti eden atomik uygulama soyutlaması sağlayarak idempotent bir soyutlama oluşturur
      İlk durumda iş sırasını kontrol etmeye gerek yoktur. Doğası gereği idempotenttir, bu yüzden tekrar çalıştırmanız yeterlidir
      İkinci durumda basit bir soyutlama uygulanamaz. Yapmak istediğiniz işi kaydetmeli, bunun tamamlanmasını ya da başarısız olmasını garanti etmeli, ardından bu tamamlanma ya da başarısızlığın kalıcı olmasını sağlamalısınız
      Bu tür bir mantık tek bir fonksiyona konup başka işlemlerle kolayca birleştirilecek bir şey değildir
  • Bu öneriler oldukça keskin görüşlü; bu yüzden pratik kural gibi ele alınmamalı
    Burada pratik kuralın kendisi bile yok bence. İlla bir tane çıkarılacaksa muhtemelen tersini söylerdim
    if, DRY nedeniyle aşağı itilmelidir
    Performans izin veriyorsa for’u yukarı taşımayı düşünmelisiniz. Böylece filter/map/reduce ve fonksiyon bileşimi kullanarak hangi nesneye hangi davranışın uygulanacağını seçebilir, fiilen kodu vektörleştirebilirsiniz

    • İsimleri ters çevirmişsiniz ya da verilen gerekçe sonucu desteklemiyor gibi görünüyor
      if’i aşağı itmek genelde vektörleştirmeyi engeller
      Yazıda anlatılan örnekler DRY olmayan durumlar; özellikle de türün içeride etiketlendiği ve benzer dallanmaların çağrı yığınının altındaki birçok fonksiyonda çoğalmak zorunda kaldığı durumlar
  • Bunun uyulmaya değer “iyi” bir kural olduğu fikrinden emin değilim
    Bazı durumlarda doğru olabilir, ama fazla bağlama bağımlı olduğu için sonuç çıkarmak zor
    Bana “c’den sonra gelmedikçe i, e’den önce gelir” gibi bir kuralı hatırlatıyor. O kadar çok istisnası var ki fiilen kural yokmuş gibi

  • Sandi Metz’in 99 Bottles of OOP kitabında buna benzer bir versiyona rastladım
    Genel olarak benim tarzım değil, ancak çok sayıda flag’i birkaç katman aşağıya aktaran bir codebase üzerinde çalışırken mantıktaki yol ayrımlarını çağrı yığınının yukarısına taşıma fikri son derece ikna ediciydi
    https://sandimetz.com/99bottles

    • Hemen aynı yazarın The Wrong Abstraction yazısı aklıma geldi
      Dallanmayı for döngüsünün içine koymak, “for döngüsü kuraldır, dallanma ise davranıştır” diyen bir soyutlamadır
      Ancak yeni gereksinimlerin bu soyutlamayı bozması çok yaygındır
      O zaman etrafından dolaşmak gerekir; ortaya çıkan kod da ya bazı durumlarda geçerli olup bazılarında geçerli olmayan bir soyutlamaya sahip olur ya da her yerde geçerli kılmaya çalışırken soyutlamaya bir sürü parametre tıkıştırıldığı için takip etmesi zorlaşır
      En başta o soyutlamayı yapmamış olsaydınız, ortaya çıkan kodu değiştirmek ve anlamak daha kolay olabilirdi
      https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction
  • Kod okunabilirliği açısından her şeyi aşağıya itmek daha iyidir
    printInvoice(invoice, options), if(printerReady){ if(printerHasInk){ if(printerHasPaper){ if(invoiceFormatIsPortrait){ ... }}}} ifadesinden çok daha iyidir
    Döngüler için de aynı şekilde printInvoices(invoices), for(invoice of invoices){ printInvoice(invoice) } kullanımından daha iyidir
    Sonuçta kod okunabilirliği çok önemlidir, ancak kapsülleme daha da önemlidir; bu yüzden ikisini uygun şekilde harmanlamak gerekir

    • printInvoice fonksiyonunun faturayı yazdırması gerekir
      Peki adında ima edilen koşullardan biri yanlış olduğu için faturayı yazdıramazsa ne olur?
      Exception fırlatabilir ya da bir sentinel değer veya hata tipi döndürebilir; ancak bu durumda ne yapılması gerektiği hemen açık değildir
      Özellikle normal kontrol akışı için exception kullanmaktan kaçınılan ve monadik hataların yaygın olmadığı Java ya da C++ gibi dillerde, yapıyı ikinci stile daha yakın kurmak daha iyi olabilir
      Öte yandan dikey biçim, bir hatayı ifade etmiyorsa fatura yazıcısının işlemesi gereken bir şeydir
      Kapsülleme daha çok uzun vadeli kod okunabilirliği, yerel refactoring ve değiştirilebilirlik, ayrıca yalnızca yerel nesnelere odaklanarak küresel davranışı çıkarımlayabilme yeteneği için bir araç gibi görünüyor
      Bu yüzden okunabilirlik ile kapsüllemeyi karşılaştırıp birinin diğerinden daha önemli olduğunu söylemek bana bir kategori hatası gibi geliyor
    • “Okunabilirliği artırmak için her şeyi aşağıya itmek” derken ok anti-pattern göstermek pek iyi değil
      Bunun yerine şöyle yapılmalı
      if(!printerReady){ return; }
      if(!printerHasInk){ return; }
      if(!printerHasPaper){ return; }
      if(!invoiceFormatIsPortrait){ return; }
      Büyüyen oktan çok daha okunabilir
      Yine de döngüyü kendi fonksiyonuna koyup kalan varsayımları da önceden ele almak iyi olur
    • Bu, PC’deki yazıcı sürücüsü de olabilir, yazıcının iç devresi de; bu yüzden görüşler kişiden kişiye değişebilir
      Kâğıt yokken yazıcının kendisi asla tekerlekleri boşa döndürmemeli. O kontrolü fonksiyonun içine koyardım
    • Elixir tarafında olsaydı, o fonksiyona muhtemelen maybe_print_invoice gibi bir ad verirdim; bu yaklaşım çok daha hoşuma gidiyor