3 poin oleh GN⁺ 2025-05-18 | 1 komentar | Bagikan ke WhatsApp
  • 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 if di dalam fungsi, pertama-tama bisa ditinjau apakah kondisi itu dapat dipindahkan ke pemanggil
    • Daripada fungsi yang menerima Option<Walrus> lalu mengembalikan saat nilainya None, bentuk yang lebih sederhana adalah fungsi yang langsung menerima Walrus
    • Alih-alih fungsi memeriksa prasyarat lalu “tidak melakukan apa-apa”, pemanggil bisa memeriksanya lebih dulu lalu menjaminnya lewat tipe atau assert
  • Cara menaikkan pemeriksaan prasyarat ini dapat menyebar ke seluruh jalur pemanggilan, dan pada akhirnya menjadi dorongan untuk mengurangi jumlah pemeriksaan
  • Alur kontrol dan if cenderung 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() membuat E::Foo atau E::Bar berdasarkan kondisi, lalu g() me-match lagi untuk memanggil foo() atau bar(), maka percabangan yang sama muncul dua kali
    • Jika kondisinya ditarik ke atas, bentuknya menjadi main() langsung memanggil foo(x) atau bar(y) berdasarkan kondisi yang sama

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 dibanding frobnicate_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 if dan for bisa diterapkan bersama
    • Jika kondisi berada di luar loop, condition tidak akan dievaluasi ulang berulang kali
    • Percabangan di hot loop dihilangkan
    • Peluang untuk vektorisasi terbuka
  • 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 for memang performa, tetapi aturan ini juga bisa membantu daya ekspresi
    • jQuery bekerja 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

 
GN⁺ 2025-05-18
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

    • Sudut pandang ini cocok dengan apa yang terlihat dalam teori bahasa pemrograman atau small-step semantics pada kalkulus lambda
      Ekspresi dievaluasi dengan berulang kali “ditulis ulang” menurut aturan reduksi. Misalnya, (1 + 2) + 4 berubah menjadi 3 + 4, lalu menjadi 7
      Di 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
    • Model berpikir saya adalah menyesuaikan dengan dunia konkret tempat kode yang sedang ditulis berada
      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 g adalah satu-satunya pemanggil h dan akan tetap begitu, barulah bisa dikatakan bahwa aturan ini mengungkap cabang mati
      Dalam codebase nyata, biasanya ada alasan mengapa g dan h tidak digabung sejak awal
    • Jika melempar satu model yang berdekatan: kelas adalah nomina, dan fungsi adalah verba
    • Itu bukan model yang terlalu aneh; kalau didorong sampai akhir, pada dasarnya menjadi dekat dengan model eksekusi Prolog
  • Aturan yang lebih umum adalah menempatkan if dekat 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

    • Ini hampir sama dengan gagasan jangan memvalidasi, parsing-lah: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
    • Bukankah itu malah membuat asumsi apa saja yang boleh diambil saat memahami logika inti menjadi lebih kabur? Apakah Anda lebih suka menelusuri dan memeriksa setiap call chain satu per satu?
  • Dugaan longgar seperti “jika ada kondisi if di dalam fungsi, pertimbangkan apakah bisa dipindahkan ke pihak caller” punya terlalu banyak kontra-contoh
    Jika fungsi dipanggil di 37 tempat, apakah semua tempat harus mengulang pernyataan if?
    Jika fungsi itu getaddrinfo atau EnterCriticalSection, apakah pengguna API harus mendorong if ke 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 if dan memanggil dua fungsi helper
    Jika 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 menyembunyikan if
    Namun ini untuk optimisasi, dan optimisasi sering berbenturan dengan struktur program yang baik
    Dalam object-oriented, penilaian if di dalam callee muncul sebagai method dispatch yang memilih metode mana yang akan dipanggil
    Teknik 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 panjangnya

    • Jika fungsi dipanggil di 37 tempat, dalam kasus ini maksudnya lebih dekat ke: fungsi itu bisa dipecah menjadi dua fungsi yang mengimplementasikan cabang true/false, lalu masing-masing dipanggil dari 21 dan 16 tempat
    • Kata kunci di sini adalah consider
      Tulisan ini membidik masalah desain yang cukup spesifik, terutama yang muncul saat memakai sesuatu seperti tagged union
    • Jika fungsi dipanggil di 37 tempat, kode memang harus direfaktor, tetapi jawabannya tetap “tergantung situasi”
      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
      EnterCriticalSection menyiratkan jalur kode yang masuk akal untuk validasi kuat saat masuk, termasuk validasi kondisi if, dan harus dilihat sebagai batas domain
      Sebaliknya, saat menulis aplikasi, jika ada pernyataan if dalam fungsi aplikasi biasa, biasanya aman untuk mendorongnya keluar
      Bahkan di dalam library atau bagian kode penting, mengangkat if ke tepi, bukan membiarkannya jauh di dalam, itu aman
      Saran 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
    match bisa diganti dengan pemanggilan metode polimorfik pada enum
    Tujuannya adalah memisahkan titik tempat pembedaan kasus dibuat, yaitu if pertama, dari titik yang menjalankan foo atau bar berdasarkan pembedaan itu
    Pembedaan 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 if didorong ke bawah. Tulisan ini menyarankan kebalikannya
    Jika if dinaikkan 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...

    • Solusinya adalah memisahkan penilaian dan eksekusi, dan konsep ini saya dapatkan dari Bertrand Meyer
      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
    • Laporan pemindai kode jangan diterima seperti kitab suci; harus diragukan
      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
    • Berdasarkan pengalaman saya, ini sering kali merupakan optimum lokal
      “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
    • Saya selalu tidak suka pemindai kompleksitas kode sejak melihatnya mengeluhkan sebuah fungsi besar yang sepenuhnya mudah dibaca
      Logika yang berada di satu tempat jauh lebih mudah dibaca, dan kita seharusnya baru memecahnya ketika detail-detail mulai membuat gambaran besarnya terlewat
    • Kemarin di thread LLM ada yang bertanya “apa lagi alat tidak dapat dipercaya yang orang-orang terima dalam coding?”, dan sekarang saya punya jawabannya
  • 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?

    • Bisa saja menulis fungsi tanpa pemeriksaan, lalu membuat fungsi wrapper yang hanya melakukan pemeriksaan dan memanggil fungsi internal
    • Sebenarnya jawabannya ada di dalam pertanyaannya
      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 PUT di mana data input berisi semua state yang perlu dicatat
      Yang 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
    if sebaiknya didorong ke bawah karena DRY
    Jika performa memungkinkan, for sebaiknya dipertimbangkan untuk dinaikkan ke atas. Dengan begitu, kita bisa memakai filter/map/reduce dan komposisi fungsi untuk memilih objek mana yang dikenai perilaku apa, dan pada dasarnya bisa memvektorisasi kode

    • Sepertinya namanya tertukar, atau alasan yang diberikan tidak mendukung kesimpulannya
      Mendorong if ke bawah biasanya menghalangi vektorisasi
      Contoh-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

    • Saya langsung teringat The Wrong Abstraction dari penulis yang sama
      Menaruh percabangan di dalam loop for adalah abstraksi yang mengatakan “loop for adalah 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 daripada if(printerReady){ if(printerHasInk){ if(printerHasPaper){ if(invoiceFormatIsPortrait){ ... }}}}
    Loop juga sama: printInvoices(invoices) lebih baik daripada for(invoice of invoices){ printInvoice(invoice) }
    Pada akhirnya keterbacaan kode memang sangat penting, tetapi enkapsulasi lebih penting, jadi keduanya perlu dicampur dengan tepat

    • Fungsi printInvoice harus mencetak faktur
      Apa 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
    • Mengatakan “mendorong semuanya ke bawah untuk meningkatkan keterbacaan” sambil menunjukkan arrow anti-pattern itu kurang bagus
      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
    • Ini bisa saja driver printer di PC atau rangkaian internal printer, jadi pendapat tiap orang bisa berbeda
      Saat tidak ada kertas, printer itu sendiri sama sekali tidak boleh membiarkan rodanya berputar sia-sia. Pemeriksaan itu akan saya taruh di dalam fungsi
    • Kalau di Elixir, nama fungsi itu mungkin akan dibuat sekitar maybe_print_invoice, dan saya jauh lebih suka yang ini