Memindahkan `if` ke Atas, `for` ke Bawah
(matklad.github.io)- Jika percabangan kondisi di dalam fungsi dinaikkan ke sisi pemanggil, dan pekerjaan atas target berulang diturunkan menjadi operasi batch alih-alih pemanggilan satu per satu, alur kode dan performa dapat disederhanakan sekaligus
- Dengan menaikkan
if, prasyarat lebih mudah dijamin lewat tipe atau assert, dan pemeriksaan duplikat serta percabangan yang tidak perlu di dalam fungsi berkurang - Jika logika percabangan tersebar di beberapa fungsi, kondisi mati atau penilaian yang berulang menjadi sulit ditemukan; struktur yang membuat enum lalu me-match-nya lagi juga bisa menjadi bentuk mengulang percabangan dalam struktur data
- Dengan menurunkan
for, banyak objek bisa diproses sekaligus sehingga biaya awal bisa diamortisasi, dan muncul ruang untuk optimisasi seperti penataan ulang urutan pemrosesan atau vektorisasi - Jika dua aturan ini dipakai bersama, evaluasi ulang kondisi di dalam loop dan percabangan di hot loop berkurang, sehingga strukturnya makin mendekati pemindahan biaya pengambilan keputusan di control plane ke pemrosesan batch di data plane
Naikkan if ke sisi pemanggil
- Jika ada kondisi
ifdi dalam fungsi, pertama-tama bisa ditinjau apakah kondisi itu dapat dipindahkan ke pemanggil- Daripada fungsi yang menerima
Option<Walrus>lalu mengembalikan saat nilainyaNone, bentuk yang lebih sederhana adalah fungsi yang langsung menerimaWalrus - Alih-alih fungsi memeriksa prasyarat lalu “tidak melakukan apa-apa”, pemanggil bisa memeriksanya lebih dulu lalu menjaminnya lewat tipe atau assert
- Daripada fungsi yang menerima
- Cara menaikkan pemeriksaan prasyarat ini dapat menyebar ke seluruh jalur pemanggilan, dan pada akhirnya menjadi dorongan untuk mengurangi jumlah pemeriksaan
- Alur kontrol dan
ifcenderung mudah menambah kompleksitas kode serta kemungkinan bug- Struktur yang menempatkan logika percabangan rumit di satu fungsi tingkat atas, lalu mendelegasikan pekerjaan sebenarnya ke fungsi bawah yang lurus, lebih mudah dibaca
- Dibanding alur kontrol rumit yang tersebar di seluruh berkas, kondisi duplikat dan kondisi mati lebih mudah disadari ketika semuanya terkumpul dalam satu fungsi yang muat di satu layar
- Refaktoring “dissolving enum” berguna ketika pembuatan enum dan match mengulang percabangan yang sama
- Jika
f()membuatE::FooatauE::Barberdasarkan kondisi, lalug()me-match lagi untuk memanggilfoo()ataubar(), maka percabangan yang sama muncul dua kali - Jika kondisinya ditarik ke atas, bentuknya menjadi
main()langsung memanggilfoo(x)ataubar(y)berdasarkan kondisi yang sama
- Jika
Turunkan for ke operasi batch
- Dalam cara berpikir berorientasi data, program biasanya menangani kumpulan banyak objek, dan hot path juga sering menjadi panas karena memproses banyak entitas
- Memperkenalkan konsep batch objek, lalu menjadikan versi skalar sebagai kasus khusus dari versi batch, adalah pendekatan yang berguna
for walrus in walruses { frobnicate(walrus) }adalah bentuk yang kurang baik dibandingfrobnicate_batch(walruses)
- Manfaat utama pemrosesan batch adalah performa
- Saat banyak target ditangani sekaligus, biaya awal bisa diamortisasi
- Urutan pemrosesan bisa diubah dengan lebih fleksibel, dan kebutuhan untuk memproses entitas dalam urutan tertentu pun berkurang
- Kita bisa memakai vektorisasi atau teknik struct-of-array, seperti memproses satu field dari semua entitas terlebih dahulu sebelum pindah ke field lain
- Sebagai contoh performa yang ekstrem, ditautkan Vectorized Interpreters Talk
- Contoh menarik lainnya adalah perkalian polinomial berbasis FFT
- Mengevaluasi polinomial di banyak titik secara bersamaan bisa lebih cepat daripada mengevaluasi titik-titik individual berulang kali
- Aturan
ifdanforbisa diterapkan bersama- Jika kondisi berada di luar loop,
conditiontidak akan dievaluasi ulang berulang kali - Percabangan di hot loop dihilangkan
- Peluang untuk vektorisasi terbuka
- Jika kondisi berada di luar loop,
- Pola yang sama bekerja baik pada tingkat mikro maupun makro
- Struktur TigerBeetle memproses batch objek secara bersamaan di data plane, sehingga biaya pengambilan keputusan di control plane dapat diamortisasi
- Motivasi utama aturan
formemang performa, tetapi aturan ini juga bisa membantu daya ekspresijQuerybekerja terhadap koleksi elemen dan pernah sangat sukses di masa lalu- Bahasa ruang vektor abstrak kadang menjadi alat berpikir yang lebih baik daripada sekumpulan persamaan per-koordinat
1 komentar
Komentar Hacker News
Model berpikir saya yang agak aneh adalah bahwa ada pohon berisi state yang mungkin dan alur program, lalu conditional memangkas pohon itu
Sebaiknya pangkas sedini mungkin agar pekerjaan hanya dilakukan pada cabang yang lebih sedikit
Pada akhirnya, saya ingin sebuah fungsi berfokus pada salah satu dari dua hal: menelusuri pohon program atau melakukan pekerjaan nyata
Ekspresi dievaluasi dengan berulang kali “ditulis ulang” menurut aturan reduksi. Misalnya,
(1 + 2) + 4berubah menjadi3 + 4, lalu menjadi7Di sini ada aturan kongruensi yang menentukan subekspresi mana yang akan dievaluasi berikutnya, dan aturan komputasi yang benar-benar mengubah ekspresi sehingga mengubah state program
Bahasa yang strict, yakni non-lazy, umumnya mengevaluasi semua subekspresi sebelum ekspresi induknya, tetapi konstruksi khusus seperti conditional dan infinite loop merupakan pengecualian
Dalam conditional, aturan komputasi diterapkan lebih dulu sebelum aturan kongruensi memerintahkan evaluasi semua subekspresi, sehingga secara harfiah memangkas pohon ekspresi
[1]: Direkomendasikan: Benjamin C. Pierce, Types and Programming Languages
Kita harus melihat karakteristik domain, pola codebase yang sudah ada, tahap mana dalam pipeline data, karakteristik performa, dan sebagainya
Dulu saya mencoba membuat aturan dan heuristik pengorganisasian kode semacam ini, tetapi setelah menulis cukup banyak kode, saya menerima bahwa tingkat abstraksinya keliru dan tidak layak dipegang terlalu lama
Fakta bahwa diskusi seperti ini bergantung pada nama fungsi palsu atau variabel satu huruf juga cukup memberi petunjuk. Sebab di “pulau kode” tanpa konteks eksternal, hampir aturan apa pun bisa terdengar masuk akal
Hanya dengan membuat asumsi nyaman bahwa
gadalah satu-satunya pemanggilhdan akan tetap begitu, barulah bisa dikatakan bahwa aturan ini mengungkap cabang matiDalam codebase nyata, biasanya ada alasan mengapa
gdanhtidak digabung sejak awalAturan yang lebih umum adalah menempatkan
ifdekat dengan sumber input: https://gieseanw.wordpress.com/2024/06/24/dont-push-ifs-up-p...Intinya adalah menemukan titik itu, termasuk entry point dari luar ke dalam program dan data yang diambil dari layanan lain, lalu merapikannya agar sebanyak mungkin jaminan dibuat sebelum mencapai logika inti, terutama bagian yang banyak memakai resource
Jika memungkinkan, sebaiknya jaminan itu dienkode ke dalam type
Dugaan longgar seperti “jika ada kondisi
ifdi dalam fungsi, pertimbangkan apakah bisa dipindahkan ke pihak caller” punya terlalu banyak kontra-contohJika fungsi dipanggil di 37 tempat, apakah semua tempat harus mengulang pernyataan
if?Jika fungsi itu
getaddrinfoatauEnterCriticalSection, apakah pengguna API harus mendorongifke luar?Menurut saya transformasi ini hanya bisa dipertimbangkan bila itu fungsi internal yang dipanggil paling banyak dari dua tempat, dan keputusan tersebut berada di luar concern fungsi itu
Cara lain adalah membuat fungsi hanya menjalankan
ifdan memanggil dua fungsi helperJika caller perlu mengangkat kondisi keluar dari loop, gunakan “helper interpretasi kondisi” tingkat rendah. Caller yang hanya membutuhkan satu
if, bukan di dalam atau di luar loop, bisa memakai fungsi praktis yang menyembunyikanifNamun ini untuk optimisasi, dan optimisasi sering berbenturan dengan struktur program yang baik
Dalam object-oriented, penilaian
ifdi dalam callee muncul sebagai method dispatch yang memilih metode mana yang akan dipanggilTeknik mengeluarkan method dispatch dari loop juga bisa melawan alur desain
Misalnya, saat ingin mengisi objek canvas dengan gambar raster, kita tidak ingin mengiterasi piksel gambar sambil memanggil
canvas.putpixel(x, y, color). Harus ada metode untuk melakukan blit gambar ke canvas atau ke area persegi panjangnyaconsiderTulisan ini membidik masalah desain yang cukup spesifik, terutama yang muncul saat memakai sesuatu seperti tagged union
DRY terasa seperti jawaban yang benar, tetapi perlu melihat contoh kode nyata untuk bisa menilai
Jika itu fungsi library, posisinya khusus. Ia berada di batas kepemilikan, data melintasi domain, dan dalam istilah DDD, melewati bounded context. Jadi ia harus menjaga wilayahnya sendiri
EnterCriticalSectionmenyiratkan jalur kode yang masuk akal untuk validasi kuat saat masuk, termasuk validasi kondisiif, dan harus dilihat sebagai batas domainSebaliknya, saat menulis aplikasi, jika ada pernyataan
ifdalam fungsi aplikasi biasa, biasanya aman untuk mendorongnya keluarBahkan di dalam library atau bagian kode penting, mengangkat
ifke tepi, bukan membiarkannya jauh di dalam, itu amanSaran untuk mengelola domain sendiri, tidak menuntut domain orang lain, dan di dalam domain itu memindahkan control flow ke tepi terdengar masuk akal
Tentu saja idiom hanyalah idiom, dan di dunia nyata hal ini harus dievaluasi oleh orang yang memahami konteks dan bisa menilai secara rasional
Contoh “refactoring dekomposisi enum” pada dasarnya adalah polimorfisme
matchbisa diganti dengan pemanggilan metode polimorfik pada enumTujuannya adalah memisahkan titik tempat pembedaan kasus dibuat, yaitu
ifpertama, dari titik yang menjalankanfooataubarberdasarkan pembedaan ituPembedaan kasus dibawa oleh objek—dalam hal ini nilai enum atau closure—sehingga tidak perlu diulang lagi di titik pemanggilan
Artinya, jika pembedaan kasus berubah, cukup ubah titik tempat pembedaan itu dibuat, dan titik-titik yang memicu perilaku untuk tiap kasus tidak perlu diubah
Namun ada trade-off. Di titik tempat perilaku dijalankan, melihat langsung kasus-kasus individual yang perlu dipertimbangkan bisa membantu, tetapi sebagai gantinya ada dependensi tingkat kode tambahan terhadap daftar kasus individual
Pemindai kompleksitas kode pada akhirnya memaksa
ifdidorong ke bawah. Tulisan ini menyarankan kebalikannyaJika
ifdinaikkan ke atas, alur kontrol sering kali tersentralisasi dalam satu fungsi; fungsi itu memiliki logika percabangan yang kompleks, tetapi pekerjaan nyatanya didelegasikan ke subrutin yang linear⁰ https://docs.sonarsource.com/sonarqube-server/latest/user-gu...
if (weShouldDoThis()) { doThis(); }Ini melengkapi, atau merupakan bagian dari, functional core dan imperative shell
Jika pemeriksaan dipisahkan, pengujiannya menjadi lebih mudah; dan jika kompleksitas mengganggu, setiap klausa di dalam pemeriksaan bisa diekstrak menjadi fungsi
Khususnya Sonar melaporkan “code smell”, bukan bug nyata
Saat memperbaiki item yang “bukan bug” seperti ini, risiko munculnya error baru naik dari 0 menjadi lebih dari 0, dan waktu developer yang seharusnya menangani isu produksi nyata bisa terbuang
“Lokal” berarti hanya berlaku sampai requirement berubah atau situasi pengecualian ditemukan sehingga percabangan juga diperlukan di luar loop
Jika percabangan muncul baik di dalam maupun di luar loop, penalarannya menjadi lebih sulit
Jika Anda cukup yakin bahwa kondisi hanya akan memengaruhi bagian dalam loop, tidak apa-apa meletakkannya di sana
Namun jika tidak sulit membayangkan requirement yang akan membutuhkan percabangan di luar loop juga, mungkin lebih baik merancang struktur seperti itu sejak awal
Kodenya bisa jadi lebih panjang, tetapi lebih mudah diikuti dan lebih kecil kemungkinannya berubah menjadi spaghetti di kemudian hari
Inilah alasan saya berhenti memakai Haskell. Haskell membuat kita ingin menulis logika yang paling ringkas dan “optimal secara lokal”, tetapi itu lebih condong pada mengekspresikan logika itu sendiri daripada maksud logikanya. Perubahan requirement kecil bisa membuatnya harus diurai dengan sangat menyakitkan
Logika yang berada di satu tempat jauh lebih mudah dibaca, dan kita seharusnya baru memecahnya ketika detail-detail mulai membuat gambaran besarnya terlewat
Kadang saya lebih suka menaruh logika kondisi di dalam callee. Karena itu mencegah caller secara tidak sengaja melakukan pekerjaan dalam urutan yang salah
Misalnya, jika ingin membuat operasi idempoten, kita bisa terlebih dahulu memeriksa apakah pekerjaan itu sudah selesai, dan jika belum, menjalankannya
Jika kondisi itu didorong keluar dari caller, setiap caller fungsi harus memastikan sendiri bahwa ia memanggilnya dengan cara yang benar untuk mendapatkan jaminan idempotensi, dan jaminan itu tidak bisa diabstraksikan
Ada juga kasus ketika serangkaian pemeriksaan harus dijalankan sebelum melakukan suatu pekerjaan di dalam transaksi database. Jika menerapkan filosofi ini, bagaimana cara menjaga pemeriksaan tetap berada di dalam batas transaksi?
Jika kondisi didorong keluar dari caller, fungsi itu tidak lagi idempoten, jadi tentu saja tidak bisa memberikan jaminan tersebut
Namun jika sebuah fungsi individual harus mengimplementasikan manajemen state untuk menyediakan idempotensi, besar kemungkinan strukturnya cukup aneh, dan sepertinya terlalu banyak logika terjadi di dalam satu fungsi
Kode idempoten biasanya terbagi menjadi dua kategori
Yang pertama adalah ketika model data dan operasi yang dilakukan itu sendiri pada dasarnya idempoten. Entah operasi tanpa state, atau operasi bergaya
PUTdi mana data input berisi semua state yang perlu dicatatYang kedua adalah operasi bisnis yang lebih kompleks, yang membangun abstraksi idempoten dengan menyediakan abstraksi penerapan atomik yang melakukan rollback atau menjamin kegagalan parsial tidak merusak state
Pada kasus pertama, tidak perlu memeriksa urutan pekerjaan. Karena memang idempoten secara inheren, cukup jalankan lagi
Pada kasus kedua, abstraksi sederhana tidak bisa diterapkan. Anda perlu mencatat pekerjaan yang diinginkan, memastikan pekerjaan itu selesai atau gagal, lalu memastikan status selesai atau gagal itu bertahan secara permanen
Logika semacam itu bukan sesuatu yang layak dimasukkan ke satu fungsi lalu dikomposisikan dengan operasi lain
Saran-saran ini sangat opinionated, jadi tidak boleh diperlakukan seperti aturan praktis
Menurut saya tidak ada aturan praktis di sini. Kalau harus membuatnya, mungkin saya justru akan mengatakan kebalikannya
ifsebaiknya didorong ke bawah karena DRYJika performa memungkinkan,
forsebaiknya dipertimbangkan untuk dinaikkan ke atas. Dengan begitu, kita bisa memakaifilter/map/reducedan komposisi fungsi untuk memilih objek mana yang dikenai perilaku apa, dan pada dasarnya bisa memvektorisasi kodeMendorong
ifke bawah biasanya menghalangi vektorisasiContoh-contoh dalam tulisan itu adalah kasus yang tidak DRY, khususnya ketika tipe diberi tag secara internal sehingga percabangan serupa harus berkembang biak di banyak fungsi di bawah stack
Saya tidak yakin ini adalah aturan “baik” yang layak diikuti
Kadang mungkin begitu, tetapi terlalu bergantung pada konteks sehingga sulit menarik kesimpulan
Rasanya mirip aturan “i sebelum e kecuali setelah c”. Terlalu banyak pengecualian sehingga pada dasarnya sama saja seperti tidak ada aturan
Saya menemukan versi yang mirip dengan ini di 99 Bottles of OOP karya Sandi Metz
Secara keseluruhan bukan gaya saya, tetapi ketika bekerja pada codebase yang meneruskan banyak flag ke beberapa lapisan di bawah, poin untuk memindahkan percabangan logika ke atas call stack terasa sangat meyakinkan
https://sandimetz.com/99bottles
Menaruh percabangan di dalam loop
foradalah abstraksi yang mengatakan “loopforadalah aturannya, dan percabangan adalah perilakunya”Namun kebutuhan baru sangat sering merusak abstraksi itu
Lalu kita harus mencari jalan memutar, dan kode akhirnya memiliki abstraksi yang berlaku pada beberapa kasus tetapi tidak pada kasus lain, atau menjadi sulit diikuti karena kita menjejalkan banyak parameter ke dalam abstraksi agar bisa diterapkan di mana-mana
Jika sejak awal abstraksi itu tidak dibuat, kode akhirnya mungkin akan lebih mudah diubah dan dipahami
https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction
Demi keterbacaan kode, lebih baik mendorong semuanya ke bawah
printInvoice(invoice, options)jauh lebih baik daripadaif(printerReady){ if(printerHasInk){ if(printerHasPaper){ if(invoiceFormatIsPortrait){ ... }}}}Loop juga sama:
printInvoices(invoices)lebih baik daripadafor(invoice of invoices){ printInvoice(invoice) }Pada akhirnya keterbacaan kode memang sangat penting, tetapi enkapsulasi lebih penting, jadi keduanya perlu dicampur dengan tepat
printInvoiceharus mencetak fakturApa yang terjadi jika faktur tidak bisa dicetak karena salah satu kondisi yang disebutkan dalam namanya bernilai false?
Kita bisa melempar exception, atau mengembalikan nilai sentinel atau tipe error, tetapi dalam kasus itu tidak langsung jelas apa yang harus dilakukan
Terutama dalam bahasa seperti Java atau C++, yang cenderung enggan memakai exception untuk alur kontrol umum dan error bergaya monad tidak lazim, mungkin lebih baik menyusunnya mendekati gaya kedua
Namun jika format orientasi vertikal bukan menunjukkan error, itu seharusnya ditangani oleh printer faktur
Enkapsulasi terutama tampak sebagai alat untuk keterbacaan kode jangka panjang, refactoring dan perubahan yang bersifat lokal, serta kemampuan menalar perilaku global hanya dengan memperhatikan objek lokal
Jadi membandingkan keterbacaan dan enkapsulasi lalu menganggap salah satunya lebih penting terasa seperti kesalahan kategori
Sebaiknya lakukan seperti ini
if(!printerReady){ return; }if(!printerHasInk){ return; }if(!printerHasPaper){ return; }if(!invoiceFormatIsPortrait){ return; }Ini jauh lebih mudah dibaca daripada panah yang makin membesar
Meski begitu, memasukkan loop ke fungsi tersendiri dan memastikan asumsi-asumsi lain sudah ditangani itu bagus
Saat tidak ada kertas, printer itu sendiri sama sekali tidak boleh membiarkan rodanya berputar sia-sia. Pemeriksaan itu akan saya taruh di dalam fungsi
maybe_print_invoice, dan saya jauh lebih suka yang ini