8 menit

Gagasan Kebenaran Tony Hoare: Dari Logika ke Kode yang Aman

Pelajari bagaimana karya Tony Hoare tentang logika Hoare, Quicksort, dan pemikiran keselamatan membentuk teknik praktis untuk menulis dan meninjau perangkat lunak yang benar.

Gagasan Kebenaran Tony Hoare: Dari Logika ke Kode yang Aman

Kenapa “kebenaran” lebih dari sekadar “tampaknya bekerja”

Ketika orang mengatakan sebuah program “benar”, mereka seringkali maksudnya: “Saya menjalankannya beberapa kali dan keluarannya terlihat benar.” Itu sinyal berguna—tetapi bukan kebenaran. Secara sederhana, kebenaran berarti program memenuhi spesifikasinya: untuk setiap input yang diizinkan, ia menghasilkan hasil yang diminta dan menghormati aturan tentang perubahan status, waktu, dan penanganan kesalahan.

Masalahnya, “memenuhi spesifikasi” lebih sulit daripada kelihatannya.

Kenapa kebenaran itu benar-benar sulit

Pertama, spesifikasi sering ambigu. Requirement produk mungkin mengatakan “urutkan daftar”, tetapi apakah itu berarti sorting stabil? Bagaimana dengan nilai duplikat, daftar kosong, atau item non-numerik? Jika spesifikasi tidak menyebutkan, orang berbeda akan mengasumsikan jawaban berbeda.

Kedua, kasus batas bukanlah langka—mereka hanya lebih jarang diuji. Nilai null, overflow, batas off-by-one, urutan pengguna yang tidak biasa, dan kegagalan eksternal yang tak terduga dapat mengubah “tampaknya bekerja” menjadi “gagal di produksi.”

Ketiga, requirements berubah. Sebuah program bisa benar relatif terhadap spesifikasi kemarin dan salah relatif terhadap spesifikasi hari ini.

Apa yang bisa Anda harapkan dari sisa artikel ini

Kontribusi besar Tony Hoare bukan klaim bahwa kita harus membuktikan semuanya sepanjang waktu. Kontribusinya adalah gagasan bahwa kita bisa lebih presisi tentang apa yang kode seharusnya lakukan—dan berpikir tentang itu dengan cara yang terdisiplin.

Di posting ini, kita akan mengikuti tiga benang yang saling terhubung:

  • Logika Hoare: penalaran ringan dan terstruktur menggunakan precondition dan postcondition.
  • Quicksort: sebuah algoritma yang familier yang memperlihatkan bagaimana langkah-langkah “jelas” kecil (seperti partisi) perlu pemikiran cermat.
  • Pola pikir keselamatan: kebenaran sebagai tanggung jawab praktis saat kegagalan punya konsekuensi nyata.

Kebanyakan tim tidak akan menulis bukti formal lengkap. Tapi bahkan pemikiran “gaya bukti” parsial dapat membuat bug lebih mudah ditemukan, tinjauan lebih tajam, dan perilaku lebih jelas sebelum kode dikirim.

Tony Hoare secara singkat: gagasan yang sampai ke kode sehari-hari

Tony Hoare adalah salah satu ilmuwan komputer langka yang karyanya tidak hanya tinggal di makalah atau ruang kelas. Ia bergerak antara akademia dan industri, dan peduli pada pertanyaan praktis yang masih dihadapi setiap tim: bagaimana kita tahu sebuah program melakukan apa yang kita kira—terutama ketika taruhannya tinggi?

Kontribusi yang relevan untuk posting ini

Artikel ini fokus pada beberapa gagasan Hoare yang terus muncul di basis kode nyata:

  • Logika Hoare: cara menggambarkan perilaku program menggunakan precondition, postcondition, dan tripel Hoare {P} C {Q}.
  • Invarian loop: kebiasaan disiplin untuk menalar tentang loop lebih dari sekadar “bekerja di mesin saya.”
  • Quicksort (dan khususnya langkah partisi): contoh terkenal di mana pernyataan kecil dan presisi tentang kebenaran sangat memperjelas.
  • Pemikiran keselamatan: pola pikir bahwa kebenaran bukan fitur mewah; ia bisa jadi pembeda antara ketidaknyamanan dan bahaya.

Apa yang tidak akan dilakukan posting ini

Anda tidak akan menemukan formalitas matematika yang mendalam di sini, dan kita tidak akan mencoba bukti Quicksort yang dapat diperiksa mesin sepenuhnya. Tujuannya adalah membuat konsep mudah didekati: struktur yang cukup untuk memperjelas penalaran Anda, tanpa mengubah tinjauan kode menjadi seminar pascasarjana.

Kenapa karyanya memengaruhi pemrograman sehari-hari

Gagasan Hoare diterjemahkan ke keputusan biasa: asumsi apa yang diandalkan sebuah fungsi, apa yang dijaminkan kepada pemanggil, apa yang harus tetap benar di tengah loop, dan bagaimana mengenali perubahan yang “hampir benar” selama tinjauan. Bahkan saat Anda tidak menulis {P} C {Q} secara eksplisit, berpikir dalam bentuk itu memperbaiki API, tes, dan kualitas diskusi tentang kode rumit.

Apa arti “kebenaran” dalam praktik

Pandangan Hoare lebih ketat daripada “lulus beberapa contoh”: kebenaran adalah tentang memenuhi janji yang disepakati, bukan tentang terlihat benar pada sampel kecil.

Requirements vs. spesifikasi vs. implementasi

  • Requirements adalah kebutuhan bisnis dalam bahasa biasa (apa yang diinginkan pemangku kepentingan).
  • Spesifikasi adalah versi kebutuhan yang tepat dan dapat diperiksa (apa yang harus dilakukan fungsi).
  • Implementasi adalah kode yang Anda buat (bagaimana ia melakukannya).

Bug sering terjadi ketika tim melewatkan langkah tengah: mereka lompat dari requirements langsung ke kode, membuat “janji” menjadi kabur.

Kebenaran parsial vs. kebenaran total

Dua klaim berbeda sering dicampur:

  • Kebenaran parsial: jika kodenya mengembalikan, hasilnya benar.
  • Kebenaran total: kodenya mengembalikan, dan hasilnya benar. (jadi terminasi termasuk dalam klaim)

Untuk sistem nyata, “tak pernah selesai” bisa sama berbahayanya dengan “selesai dengan jawaban yang salah.”

Kebenaran selalu bergantung pada asumsi

Pernyataan kebenaran tak pernah universal; mereka bergantung pada asumsi tentang:

  • Input (mis. daftar muat di memori, elemen dapat dibandingkan)
  • Batasan (mis. batas waktu, rentang integer)
  • Lingkungan (mis. konkurensi, kegagalan I/O, konfigurasi)

Menjadi eksplisit tentang asumsi mengubah “bekerja di mesin saya” menjadi sesuatu yang dapat dipahami orang lain.

Contoh spesifikasi kecil

Pertimbangkan fungsi sortedCopy(xs).

Spesifikasi berguna bisa: “Mengembalikan daftar baru ys sehingga (1) ys terurut menaik, dan (2) ys berisi tepat elemen yang sama dengan xs (dengan jumlah yang sama), dan (3) xs tidak berubah.”

Sekarang “benar” berarti kode memenuhi tiga poin itu di bawah asumsi yang dinyatakan—bukan sekadar keluaran terlihat terurut pada pengujian cepat.

Dasar logika Hoare: precondition, postcondition, tripel

Logika Hoare adalah cara berbicara tentang kode dengan kejelasan yang sama seperti Anda berbicara tentang kontrak: jika Anda mulai dalam keadaan yang memenuhi asumsi tertentu, dan menjalankan potongan kode ini, Anda akan berakhir dalam keadaan yang memenuhi jaminan tertentu.

Notasi inti adalah tripel Hoare:

{precondition} program {postcondition}

Precondition: apa yang Anda asumsikan

Sebuah precondition menyatakan apa yang harus benar sebelum fragmen program dijalankan. Ini bukan tentang apa yang Anda harapkan; ini tentang apa yang kode butuh agar janjinya valid.

Contoh: misalkan sebuah fungsi mengembalikan rata-rata dua angka tanpa pemeriksaan overflow.

  • Precondition: a + b muat dalam tipe integer
  • Program: avg = (a + b) / 2
  • Postcondition: avg sama dengan rata-rata matematis a dan b

Jika precondition tidak terpenuhi (overflow mungkin terjadi), janji postcondition tidak lagi berlaku. Tripel memaksa Anda menyatakannya secara eksplisit.

Postcondition: apa yang Anda jamin

Sebuah postcondition menyatakan apa yang akan benar setelah kode berjalan—dengan asumsi precondition terpenuhi. Postcondition yang baik konkret dan dapat diperiksa. Daripada mengatakan “hasil valid”, jelaskan apa arti “valid”: terurut, non-negatif, dalam batas, tak berubah kecuali pada bidang tertentu, dll.

Penugasan dan pengurutan (tanpa simbolisme berlebihan)

Logika Hoare bisa diterapkan dari pernyataan kecil ke kode multi-langkah:

  • Penugasan mengubah keadaan secara tepat. Penalaran menanyakan: setelah x = x + 1, fakta apa tentang x yang sekarang benar?
  • Pengurutan (“lakukan ini, lalu itu”) merangkai jaminan: jika langkah 1 memenuhi precondition untuk langkah 2, blok keseluruhan menjadi lebih mudah dipercaya.

Intinya bukan menaburkan kurung kurawal di mana-mana. Intinya membuat maksud dapat dibaca: asumsi jelas, hasil jelas, dan lebih sedikit percakapan “tampaknya bekerja” di tinjauan.

Invarian loop yang bisa ditulis tim nyata

Sebuah invarian loop adalah pernyataan yang benar sebelum loop mulai, tetap benar setelah setiap iterasi, dan masih benar ketika loop selesai. Ide sederhana dengan keuntungan besar: ia menggantikan “tampaknya bekerja” dengan klaim yang bisa Anda periksa setiap langkah.

Kenapa invarian menghentikan penalaran yang kabur

Tanpa invarian, sebuah tinjauan sering terdengar seperti: “Kita iterasi daftar dan perlahan memperbaiki hal.” Invarian memaksa presisi: apa yang sudah benar tepat saat ini, meskipun loop belum selesai? Setelah Anda bisa mengatakannya jelas, bug off-by-one dan kasus terlewat jadi lebih mudah terlihat, karena mereka muncul sebagai titik di mana invarian akan dilanggar.

Template invarian yang bisa Anda pakai ulang

Sebagian besar kode sehari-hari bisa memakai beberapa template andal.

1) Batas / keselamatan indeks

Menjaga indeks dalam rentang aman.

  • 0 <= i <= n
  • low <= left <= right <= high

Jenis invarian ini bagus untuk mencegah akses di luar rentang dan membuat penalaran array menjadi konkret.

2) Item yang diproses vs belum diproses

Membagi data menjadi wilayah “selesai” dan “belum”.

  • “Semua elemen di a[0..i) telah diperiksa.”
  • “Setiap item yang dipindah ke result memenuhi predikat filter.”

Ini mengubah progres kabur menjadi kontrak jelas tentang apa arti “diproses”.

3) Prefix terurut (atau prefix terpartisi)

Umum pada pengurutan, penggabungan, dan partisi.

  • a[0..i) terurut.”
  • “Semua item di a[0..i) <= pivot, dan semua item di a[j..n) >= pivot.”

Bahkan jika seluruh array belum terurut, Anda sudah menetapkan bagian yang terikat.

Terminasi dalam kata-kata sederhana: ukuran yang mengecil

Kebenaran bukan hanya tentang benar; loop juga harus selesai. Cara sederhana untuk berargumen adalah memberi nama sebuah ukuran (variant) yang mengecil setiap iterasi dan tidak bisa mengecil selamanya.

Contoh:

  • n - i mengecil 1 setiap kali.”
  • “Jumlah item yang belum diproses berkurang.”

Jika Anda tidak bisa menemukan ukuran yang mengecil, Anda mungkin menemukan risiko nyata: loop tak berujung pada beberapa input.

Quicksort sebagai studi kasus penalaran tentang kode

Tambahkan Kontrak pada Fungsi Kunci
Minta Koder.ai menyarankan komentar kontrak untuk fungsi kunci, termasuk perilaku saat gagal.

Quicksort punya janji sederhana: diberikan sebuah potongan (slice) atau segmen array, susun elemen-elemennya sehingga menjadi tidak menurun, tanpa kehilangan atau menambah nilai. Bentuk algoritma pada tingkat tinggi mudah diringkas:

  1. Pilih sebuah pivot.
  2. Partisi rentang sehingga elemen “kurang dari pivot” berpindah ke satu sisi dan “lebih besar dari pivot” ke sisi lain (dengan aturan untuk “sama” tertentu).
  3. Rekursi pada subrange kiri dan kanan.

Ini contoh pengajaran yang bagus karena cukup kecil untuk diingat, tetapi cukup kaya untuk menunjukkan di mana penalaran informal gagal. Quicksort yang “tampaknya bekerja” pada beberapa tes acak masih bisa salah pada input tertentu atau kondisi batas.

Perangkap yang merusak implementasi “jelas”

Beberapa isu yang menyebabkan kebanyakan bug:

  • Duplikat: jika partisi memperlakukan “sama dengan pivot” secara tidak konsisten, Anda bisa mengalami rekursi tak berujung (subrange tidak menyusut) atau partisi yang melanggar aturan sendiri.
  • Rentang kosong atau satu elemen: kasus dasar harus tepat; jika tidak Anda bisa mengakses di luar batas atau rekursi tak berujung.
  • Off-by-one pada indeks: algoritma partisi sering memakai dua pointer; satu perbandingan atau increment yang salah bisa melewatkan elemen atau menukar di luar rentang.

Apa yang sebenarnya harus dibuktikan

Untuk berargumen kebenaran secara gaya Hoare, biasanya Anda pisahkan bukti menjadi dua bagian:

  • Kebenaran partisi: setelah partisi, setiap elemen di kiri memenuhi relasi terhadap pivot, setiap elemen di kanan memenuhi relasi sebaliknya, dan hasil adalah permutasi dari elemen asli.
  • Kebenaran rekursi: panggilan rekursif bekerja pada rentang yang benar-benar lebih kecil (terminasi) dan, dengan asumsi mereka mengurutkan subrange, seluruh rentang menjadi terurut.

Pemecahan ini membuat penalaran lebih mudah dikelola: perbaiki partisi dulu, lalu bangun kebenaran pengurutan di atasnya.

Kebenaran partisi: inti Quicksort

Kecepatan Quicksort bergantung pada satu rutinitas yang tampak sepele: partisi. Jika partisi sedikit pun salah, Quicksort bisa salah urut, berputar selamanya, atau crash pada kasus batas.

Kontrak partisi (apa yang harus dijamin)

Kita gunakan skema Hoare partition klasik (dua pointer bergerak ke dalam).

Input: sebuah potongan array A[lo..hi] dan nilai pivot yang dipilih (sering A[lo]).

Output: sebuah indeks p sedemikian sehingga:

  • setiap elemen di A[lo..p] <= pivot
  • setiap elemen di A[p+1..hi] >= pivot

Perhatikan apa yang tidak dijanjikan: pivot tidak mesti berakhir di posisi p, dan elemen yang sama dengan pivot bisa muncul di kedua sisi. Itu tidak apa-apa—Quicksort hanya butuh pemisahan yang benar.

Invarian kunci saat memindai dan menukar

Saat algoritma memajukan dua indeks—i dari kiri, j dari kanan—penalaran baik fokus pada apa yang sudah “terkunci”. Sekumpulan invarian praktis:

  • semua item di A[lo..i-1] adalah <= pivot (sisi kiri bersih)
  • semua item di A[j+1..hi] adalah >= pivot (sisi kanan bersih)
  • semuanya di A[i..j] belum diklasifikasikan (masih harus diperiksa)

Ketika kita menemukan A[i] >= pivot dan A[j] <= pivot, menukarnya mempertahankan invarian itu dan mengecilkan bagian yang belum diklasifikasikan.

Kasus batas yang harus ditutup kebenaran

  • Semua lebih kecil dari pivot: i bergerak ke kanan; partisi harus tetap berhenti dan mengembalikan p yang masuk akal.
  • Semua lebih besar dari pivot: j bergerak ke kiri; kekhawatiran terminasi sama.
  • Banyak yang sama: jika perbandingan tidak konsisten (< vs <=), pointer bisa macet. Skema Hoare bergantung pada aturan konsisten agar progres berlangsung.
  • Sudah terurut / terbalik urut: tidak boleh mematahkan kontrak, meski kinerja menurun.

Berbagai skema partisi ada (Lomuto, Hoare, partisi tiga-arah). Kuncinya adalah memilih satu, menyatakan kontraknya, dan meninjau kode terhadap kontrak itu secara konsisten.

Menalar tentang rekursi: kasus dasar dan terminasi

Buat Kerangka API Go dan Postgres
Ubah spesifikasi jelas menjadi API Go dan skema PostgreSQL yang bisa Anda sempurnakan bersama tim.

Rekursi paling mudah dipercaya ketika Anda dapat menjawab dua pertanyaan dengan jelas: kapan berhenti? dan mengapa setiap langkah valid? Pemikiran ala Hoare membantu karena memaksa Anda menyatakan apa yang harus benar sebelum pemanggilan, dan apa yang akan benar setelahnya.

Kasus dasar harus benar

Sebuah fungsi rekursif butuh setidaknya satu kasus dasar di mana tidak ada pemanggilan rekursif lebih lanjut dan tetap memenuhi hasil yang dijanjikan.

Untuk pengurutan, kasus dasar tipikal adalah “array dengan panjang 0 atau 1 sudah terurut.” Di sini, “terurut” harus eksplisit: untuk relasi ≤, output terurut jika untuk setiap indeks i < j, kita punya a[i] ≤ a[j]. (Stabilitas—apakah elemen sama mempertahankan urutan awal—adalah properti terpisah; Quicksort biasanya tidak stabil kecuali didesain demikian.)

Submasalah harus menyusut

Setiap langkah rekursif harus memanggil dirinya pada input yang secara ketat lebih kecil. “Menyusut” ini adalah argumen terminasi: jika ukuran mengecil dan tidak bisa kurang dari 0, Anda tidak bisa rekursi selamanya.

Menyusut juga penting untuk keselamatan stack. Kode yang benar pun bisa crash jika kedalaman rekursi terlalu besar. Dalam Quicksort, partisi yang sangat tidak seimbang bisa menghasilkan kedalaman rekursi besar. Itu pengingat terminasi sekaligus pengingat praktis untuk mempertimbangkan kedalaman terburuk.

Kebenaran dulu, kinerja kemudian

Kasus terburuk waktu Quicksort bisa memburuk menjadi O(n²) saat partisi sangat tidak seimbang, tetapi itu masalah kinerja—bukan kegagalan kebenaran. Tujuan penalaran di sini adalah: dengan asumsi langkah partisi mempertahankan elemen dan memisahkannya menurut pivot, pengurutan rekursif pada bagian yang lebih kecil berarti seluruh rentang memenuhi definisi terurut.

Pemikiran gaya-bukti dan testing: bagaimana mereka cocok

Testing dan penalaran gaya-bukti bertujuan pada tujuan yang sama—keyakinan—tetapi mencapai itu secara berbeda.

Testing menemukan bug; penalaran menyingkirkan kelas bug

Tes sangat bagus untuk menangkap kesalahan konkret: off-by-one, kasus batas terlewat, regresi. Tapi suite tes hanya bisa mengambil sampel ruang input. Bahkan “100% coverage” tidak berarti “semua perilaku diperiksa”; biasanya itu berarti “semua baris dieksekusi.”

Penalaran gaya-bukti (khususnya logika Hoare) mulai dari spesifikasi dan bertanya: jika precondition ini terpenuhi, apakah kode selalu menegakkan postcondition? Ketika Anda melakukan itu dengan baik, Anda tidak hanya menemukan bug—Anda sering bisa mengeliminasi seluruh kategori bug (mis. “akses array tetap dalam batas” atau “loop tidak merusak properti partisi”).

Spesifikasi menghasilkan kasus uji yang lebih baik

Spesifikasi yang jelas adalah generator tes.

Jika postcondition Anda mengatakan “output terurut dan adalah permutasi dari input”, Anda otomatis mendapat ide tes:

  • Batas: daftar kosong, satu elemen, sudah terurut, terbalik urut.
  • Invarian: properti antara langkah (mis. partisi mempertahankan elemen <= pivot di kiri).
  • Input tak valid: null, NaN, indeks di luar rentang, comparator yang inkonsisten.

Spesifikasi memberi tahu apa arti “benar”, dan tes memeriksa apakah kenyataan cocok dengannya.

Tes berbasis properti sebagai jembatan praktis

Testing berbasis properti berada di antara bukti dan contoh. Alih-alih memilih beberapa kasus, Anda menyatakan properti dan membiarkan alat menghasilkan banyak input.

Untuk pengurutan, dua properti sederhana sangat berguna:

  • Terurut: hasil dalam urutan non-decreasing.
  • Permutasi: hasil mengandung tepat elemen yang sama seperti input.

Properti ini pada dasarnya adalah postcondition yang ditulis sebagai pemeriksaan yang dapat dieksekusi.

Alur kerja ringan yang bisa dipakai tim

Rutin ringan yang skalabel:

  1. Tulis spes terlebih dahulu (preconditions, postconditions, invarian kunci).
  2. Pikirkan bagian rumit (loop, partisi, batas rekursi).
  3. Ubah spes menjadi tes (kasus batas + cek berbasis properti).
  4. Simpan bersama spes, penalaran, dan tes dalam PR sehingga perubahan di masa depan tidak diam-diam melanggar maksud awal.

Jika Anda ingin menginstitusikan ini, buat “spes + catatan penalaran + tes” sebagai bagian dari template PR atau checklist tinjauan kode (lihat juga /blog/code-review-checklist).

Jika Anda menggunakan alur kerja pembuatan kode cepat dari antarmuka chat, disiplin yang sama berlaku—bahkan lebih penting. Di Koder.ai, misalnya, Anda bisa mulai di Planning Mode untuk menetapkan precondition/postcondition sebelum kode digenerasi, lalu iterasi dengan snapshot dan rollback sambil menambahkan tes berbasis properti. Alat mempercepat implementasi, tapi spes masih menjaga agar “cepat” tidak berubah jadi “rapuh.”

Pemikiran keselamatan: kebenaran dengan konsekuensi dunia nyata

Kebenaran bukan hanya soal “program mengembalikan nilai yang benar.” Pemikiran keselamatan menanyakan: hasil apa yang tidak dapat diterima, dan bagaimana kita mencegahnya—bahkan ketika kode mendapat tekanan, disalahgunakan, atau sebagian gagal? Dalam praktik, keselamatan adalah kebenaran dengan sistem prioritas: beberapa kegagalan sekadar menyebalkan, yang lain bisa menyebabkan kerugian finansial, pelanggaran privasi, atau bahaya fisik.

Bahaya vs bug: kenapa dampak penting

Sebuah bug adalah cacat dalam kode atau desain. Sebuah bahaya (hazard) adalah situasi yang bisa mengarah pada hasil yang tidak dapat diterima. Satu bug bisa jadi tidak berbahaya di satu konteks dan berbahaya di konteks lain.

Contoh: off-by-one pada galeri foto mungkin salah memberi label gambar; kesalahan yang sama pada kalkulator dosis obat bisa mencelakai pasien. Pemikiran keselamatan memaksa Anda menghubungkan perilaku kode ke konsekuensi, bukan hanya ke “kepatuhan spes.”

Teknik sederhana yang mencegah hasil terburuk

Anda tidak perlu metode formal berat untuk mendapatkan manfaat keselamatan segera. Tim bisa mengadopsi praktik kecil dan bisa diulang:

  • Fail-safe defaults: jika sistem tidak yakin, pilih perilaku yang lebih aman. Mis. tolak akses saat pemeriksaan otorisasi gagal daripada “izinkan saat error.”
  • Validasi input di batas: anggap input pengguna, isi file, dan data jaringan sebagai tidak tepercaya. Validasi tipe, rentang, format, dan invarian lebih awal.
  • Batas dan timeout: batasi penggunaan memori, ukuran permintaan, kedalaman rekursi, retry, dan waktu eksekusi. Banyak insiden adalah kode “benar” yang berjalan dengan input yang tidak wajar.

Teknik ini berpadu alami dengan penalaran gaya Hoare: Anda buat precondition eksplisit (input yang diterima) dan pastikan postcondition mencakup properti keselamatan (apa yang tidak boleh terjadi).

Trade-off: pengecekan tidak gratis

Pengecekan berorientasi keselamatan punya biaya—waktu CPU, kompleksitas, atau penolakan palsu sesekali.

  • Kinerja vs pengecekan: jalur cepat berguna, tetapi batas kritis pantas divalidasi, dibatasi, dan diberi timeout.
  • Ketat vs kegunaan: menolak semua input yang tak sempurna bisa membuat pengguna frustrasi; menerima semua mengundang ambiguitas dan eksploitasi. Kompromi praktis: “ketat di inti, lunak di tepi,” sambil mencatat dan mengukur seberapa sering kasus tepi terjadi.

Pemikiran keselamatan lebih sedikit tentang membuktikan keindahan dan lebih banyak tentang mencegah mode kegagalan yang tidak bisa Anda tanggung.

Menerapkan penalaran gaya Hoare dalam tinjauan kode

Buat Review Lebih Konkret
Ubah pertanyaan review menjadi checklist singkat: asumsi, jaminan, dan terminasi.

Tinjauan kode adalah tempat di mana pemikiran kebenaran memberi hasil paling cepat, karena Anda bisa menemukan asumsi yang hilang jauh sebelum bug mencapai produksi. Gerakan inti Hoare—menyatakan apa yang harus benar sebelum dan apa yang akan benar setelah—terjemahannya mudah menjadi pertanyaan tinjauan.

Ubah gagasan Hoare menjadi pertanyaan tinjauan

Saat membaca perubahan, cobalah merangkai setiap fungsi kunci sebagai janji kecil:

  • Asumsi (preconditions): Apa yang harus benar tentang input, status, dan lingkungan? (mis. “list tidak kosong”, “user terautentikasi”, “lock dipegang”).
  • Jaminan (postconditions): Apa yang benar setelahnya, termasuk nilai balik dan efek samping? (mis. “saldo berkurang sebesar jumlah”, “record disisipkan persis sekali”).
  • Invarian: Apa yang harus tetap benar sepanjang loop, retry, atau workflow multi-langkah? (mis. “processed_count ≤ total”, “jumlah debit sama dengan jumlah kredit sejauh ini”).
  • Perilaku saat gagal: Apa yang terjadi saat error—apakah kita meninggalkan sistem dalam keadaan aman? Apakah update parsial di-rollback?

Kebiasaan pemeriksa yang sederhana: jika Anda tidak bisa mengatakan pre/post dalam satu kalimat, kode kemungkinan perlu struktur yang lebih jelas.

“Komentar kontrak” untuk fungsi kritis

Untuk fungsi berisiko atau sentral, tambahkan komentar kontrak kecil di atas tanda tangan. Buat konkret: input, output, efek samping, dan error.

def withdraw(account, amount):
    """Contract:
    Pre: amount is an integer > 0; account is active.
    Post (success): returns new_balance; account.balance decreased by amount.
    Post (failure): raises InsufficientFunds; account.balance unchanged.
    """
    ...

Komentar ini bukan bukti formal, tetapi memberi pemeriksa sesuatu yang konkret untuk dicocokkan.

Checklist ringan untuk kode berisiko

Jadilah ekstra eksplisit saat meninjau kode yang menangani:

  • Parsing/validasi (jalur input rusak, kasus batas)
  • Konkurensi (lock, race, idempotenitas, retry)
  • Uang/kuota (pembulatan, double-charge, overflow)
  • Izin (siapa bisa melakukan apa, dan mengapa)

Jika perubahan menyentuh hal-hal ini, tanyakan: “Apa preconditions-nya, dan di mana dipaksa?” dan “Jaminan apa yang kita berikan bahkan saat terjadi kegagalan?”

Kapan menggunakan alat formal—dan checklist praktis

Penalaran formal tidak harus berarti mengubah seluruh basis kode menjadi makalah matematika. Tujuannya adalah menghabiskan kepastian ekstra di tempat yang memberi manfaat: lokasi di mana “terlihat baik di tes” tidak cukup.

Di mana metode formal paling membantu

Mereka cocok ketika Anda punya modul kecil dan kritis yang menjadi dasar banyak hal (auth, aturan pembayaran, izin, interlock keselamatan), atau algoritma rumit di mana kesalahan off-by-one bersembunyi lama (parser, scheduler, caching/eviction, primitif konkurensi, kode bergaya partisi/batas).

Aturan berguna: jika bug bisa menyebabkan bahaya nyata, kerugian besar, atau kerusakan data diam-diam, Anda ingin lebih dari sekadar tinjauan + tes biasa.

Alat yang bisa dipertimbangkan (tingkat tinggi)

Anda bisa memilih dari “ringan” hingga “berat”, dan sering hasil terbaik datang dari kombinasi:

  • Tipe (termasuk sistem tipe lebih kuat, non-null, unit/quantity): mencegah kategori keadaan tidak valid.
  • Analisis statis: menemukan jalur mencurigakan, penyalahgunaan API, race, aliran input yang terkontaminasi.
  • Kontrak (pre/post, assertion): versi yang dapat dieksekusi dari pernyataan gaya Hoare.
  • Model checking: menjelajah mesin status (bagus untuk protokol, konkurensi, urutan “bagaimana kalau”).
  • Verifikasi formal: bukti yang diperiksa mesin untuk bagian yang memerlukan jaminan tertinggi.

Seberapa dalam harus melangkah?

Tentukan kedalaman formalisasi dengan menimbang:

  • Risiko: dampak × kemungkinan. Risiko tinggi membenarkan jaminan lebih kuat.
  • Biaya: waktu untuk menspesifikasi, membuktikan, dan memelihara.
  • Tingkat perubahan: kode yang cepat berubah sulit dijaga secara formal; stabilkan antarmuka dulu.
  • Keterampilan tim: mulai dengan kontrak dan analisis statis jika bukti lengkap akan memperlambat pengiriman.

Dalam praktik, Anda juga bisa memperlakukan “formalitas” sebagai sesuatu yang ditambahkan bertahap: mulai dengan kontrak eksplisit dan invarian, lalu biarkan otomasi menjaga konsistensi. Untuk tim yang membangun cepat dengan Koder.ai—di mana menghasilkan front end React, backend Go, dan skema Postgres bisa terjadi cepat—snapshot/rollback dan ekspor kode memudahkan iterasi sambil tetap menegakkan kontrak lewat tes dan analisis statis di CI.

Checklist praktis

Gunakan ini sebagai gerbang singkat “haruskah kita memformalkan lebih?” saat perencanaan atau tinjauan:

  1. Apa kegagalan terburuk yang kredibel, dan siapa yang dirugikan (pengguna, ops, regulator)?
  2. Bisakah tes secara realistis menutup kasus batas dan keadaan penting?
  3. Apakah logika bersifat stateful, concurrent, atau banyak bergantung invarians/batas?
  4. Bisakah kita menulis preconditions/postconditions yang jelas untuk titik masuk publik?
  5. Apakah ada inti kecil yang bisa kita isolasi dan verifikasi lebih mendalam?
  6. Alat mana yang memberi nilai terbaik: tipe lebih kuat, analisis statis, kontrak, model checking, atau bukti?
  7. Apa yang akan berubah kuartal depan, dan bagaimana kita menjaga jaminan dari pergeseran?

Bacaan lanjutan: design-by-contract, testing berbasis properti, model checking untuk mesin status, analyzer statis untuk bahasa Anda, dan materi pengantar tentang proof assistant dan spesifikasi formal.

Pertanyaan umum

Apa yang dimaksud dengan “kebenaran” selain “itu bekerja saat saya mencobanya”?

Kebenaran berarti program memenuhi spesifikasi yang disepakati: untuk setiap input yang diizinkan dan kondisi sistem yang relevan, program menghasilkan keluaran dan efek samping yang diminta (dan menangani kesalahan sesuai janji). “Tampaknya bekerja” biasanya berarti Anda hanya memeriksa beberapa contoh, bukan seluruh ruang input atau kondisi batas yang rumit.

Apa perbedaan antara requirements, spesifikasi, dan implementasi?

Requirements adalah tujuan bisnis dalam bahasa biasa (mis. “urutkan daftar untuk ditampilkan”). Spesifikasi adalah janji yang tepat dan dapat diperiksa (mis. “mengembalikan daftar baru yang diurutkan menaik, memiliki multiset elemen yang sama, input tidak berubah”). Implementasi adalah kode yang Anda tulis. Bug sering terjadi ketika tim lompat langsung dari requirements ke implementasi tanpa menulis janji yang dapat diperiksa.

Apa itu kebenaran parsial vs. kebenaran total, dan kenapa penting?

Kebenaran parsial: jika kode selesai (return), hasilnya benar. Kebenaran total: kode pasti selesai dan hasilnya benar — jadi terminasi termasuk dalam klaim.

Secara praktis, kebenaran total penting ketika “terjebak selamanya” adalah kegagalan yang terlihat pengguna, kebocoran sumber daya, atau risiko keselamatan.

Apa itu tripel Hoare, dengan bahasa yang mudah dimengerti?

Tripel Hoare {P} C {Q} dibaca seperti kontrak:

  • P (precondition): apa yang harus benar sebelum menjalankan C
  • C: fragmen kode
  • Q (postcondition): apa yang akan benar setelah C selesai, dengan asumsi P terpenuhi

Anda tidak perlu menulis notasi itu dalam kode—menggunakan strukturnya dalam tinjauan kode (“asumsi masuk, jaminan keluar”) adalah keuntungan praktis.

Bagaimana memilih preconditions yang baik untuk sebuah fungsi?

Precondition adalah apa yang dibutuhkan kode (mis. “indeks berada dalam rentang”, “elemen bisa dibandingkan”, “lock dipegang”). Jika precondition bisa dilanggar oleh pemanggil, maka opsi Anda:

  • tegakkan (validasi, pengecekan, return awal), atau
  • buat eksplisit (dokumentasi/komentar kontrak), atau
  • desain ulang API sehingga keadaan tidak valid sulit direpresentasikan.

Kalau tidak, postcondition Anda menjadi sekadar harapan.

Apa itu invarian loop, dan contoh yang bisa saya pakai ulang?

Invarian loop adalah pernyataan yang benar sebelum loop dimulai, tetap benar setelah setiap iterasi, dan masih benar saat loop selesai. Contoh template yang bisa dipakai:

  • keselamatan indeks/batas (mis. 0 <= i <= n)
  • pemisahan diproses vs belum diproses (apa yang “sudah selesai” sekarang)
  • klaim prefix yang terurut/terpartisi

Jika Anda tidak bisa mengartikulasikan invarian, itu tanda loop melakukan terlalu banyak hal atau batasannya tidak jelas.

Bagaimana Anda berargumen bahwa sebuah loop atau rekursi akan berhenti?

Biasanya Anda memberi nama sebuah ukuran (varian) yang berkurang setiap iterasi dan tidak bisa berkurang selamanya, mis.:

  • n - i menyusut satu per langkah
  • “jumlah item yang belum diproses” menurun
  • jarak antara dua pointer menyusut

Jika Anda tidak menemukan ukuran yang berkurang, mungkin ada risiko non-terminasi (khususnya dengan duplikat atau pointer yang macet).

Mengapa langkah partisi adalah “inti” kebenaran Quicksort?

Dalam Quicksort, partisi adalah rutinitas kecil yang menjadi pondasi. Jika partisi sedikit pun salah, Anda bisa mendapatkan:

  • urutan salah (output tidak terurut)
  • subrange yang tidak menyusut (rekursi tak berujung)
  • akses di luar batas (crash)

Karena itu, bantuannya menyatakan kontrak partisi secara eksplisit: apa yang harus benar di sisi kiri, di sisi kanan, dan bahwa elemen hanya diubah urutannya (permutasi).

Bagaimana duplikat bisa merusak implementasi Quicksort, dan bagaimana mencegahnya?

Duplikat dan penanganan “sama dengan pivot” adalah titik kegagalan umum. Aturan praktis:

  • pilih satu skema partisi (Hoare, Lomuto, three-way) dan ikuti perbandingan yang konsisten
  • pastikan pointer selalu bergerak pada nilai yang sama (hindari i/j yang macet)
  • pastikan panggilan rekursif menyusut (jangan terus memanggil pada rentang yang sama)

Jika duplikat sering terjadi, pertimbangkan partisi tiga-arah untuk mengurangi bug dan kedalaman rekursi.

Bagaimana reasoning ala-bukti dan testing bekerja bersama di tim nyata?

Testing menemukan bug konkret; reasoning gaya-bukti dapat menyingkirkan kategori bug secara keseluruhan (keselamatan batas, pelestarian invarian, terminasi). Alur kerja hibrida yang praktis:

  1. tulis spesifikasi kecil (pre/postconditions, invarian kunci)
  2. pikirkan bagian rumit (loop, partisi, batas rekursi)
  3. ubah spesifikasi jadi tes, terutama tes berbasis properti

Untuk pengurutan, dua properti bernilai tinggi adalah:

  • terurut (non-decreasing) dan
  • permutasi (memiliki elemen yang sama dengan jumlah yang sama).

Related posts