If’i Yukarı, For’u Aşağı Taşımak
(matklad.github.io)- 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
ifyukarı 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
foraş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
ifkoşulu varsa, önce bu koşulun çağıran tarafa taşınıp taşınamayacağını değerlendirebilirsinizOption<Walrus>alıpNoneise dönen bir fonksiyon yerine, zatenWalrusalan 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öreE::FooveyaE::Baroluşturuyor,g()da tekrar match edipfoo()veyabar()çağırıyorsa dallanma iki kez görünür- Koşul yukarı çekildiğinde
main()içinde aynı koşulla doğrudanfoo(x)veyabar(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) }yerinefrobnicate_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
ifveforkuralları birlikte uygulanabilir- Koşul döngünün dışında olursa
conditiontekrar tekrar yeniden değerlendirilmez - Sıcak döngüden dallanma kaldırılır
- Vektörleştirme olasılığı açılır
- Koşul döngünün dışında olursa
- 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
forkuralının başlıca motivasyonu performans olsa da ifade gücüne de yardımcı olabilirjQueryöğ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
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.
İfadeler, indirgeme kurallarına göre tekrar tekrar “yeniden yazılarak” değerlendirilir. Örneğin
(1 + 2) + 4,3 + 4olur, sonra da7.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
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.
gninhnin 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
gilehnin en başta birleştirilmemiş olmasının bir nedeni vardı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.
“Bir fonksiyonun içinde
ifkoş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
ififadesini tekrar mı etmek gerekir?Bu fonksiyon
getaddrinfoveyaEnterCriticalSectionise API kullanıcısınaifi 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
ifgerektiren çağırıcılar iseifi 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
ifkararı, 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.consider.Bu yazı, özellikle tagged union gibi şeyler kullanırken ortaya çıkan oldukça belirli bir tasarım sorununu hedefliyor.
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,ifkoş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
ififadesi 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ülebilirAmaç, durum ayrımının yapıldığı noktayı, yani ilk
ifile buna bağlı olarakfooya dabarçalıştırılan noktayı birbirinden ayırmaktırDurum 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 öneriyorif’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...
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
Ö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
“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
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
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?
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ığı
PUTtarzı bir işlemdirDiğ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ğı itilmelidirPerformans izin veriyorsa
for’u yukarı taşımayı düşünmelisiniz. Böylecefilter/map/reduceve fonksiyon bileşimi kullanarak hangi nesneye hangi davranışın uygulanacağını seçebilir, fiilen kodu vektörleştirebilirsinizif’i aşağı itmek genelde vektörleştirmeyi engellerYazı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
Dallanmayı
fordöngüsünün içine koymak, “fordöngüsü kuraldır, dallanma ise davranıştır” diyen bir soyutlamadırAncak 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 iyidirDöngüler için de aynı şekilde
printInvoices(invoices),for(invoice of invoices){ printInvoice(invoice) }kullanımından daha iyidirSonuçta kod okunabilirliği çok önemlidir, ancak kapsülleme daha da önemlidir; bu yüzden ikisini uygun şekilde harmanlamak gerekir
printInvoicefonksiyonunun faturayı yazdırması gerekirPeki 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
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
Kâğıt yokken yazıcının kendisi asla tekerlekleri boşa döndürmemeli. O kontrolü fonksiyonun içine koyardım
maybe_print_invoicegibi bir ad verirdim; bu yaklaşım çok daha hoşuma gidiyor