3 poin oleh GN⁺ 2026-02-23 | 1 komentar | Bagikan ke WhatsApp
  • Menjelaskan pendekatan desain Rust yang memanfaatkan sistem tipe untuk menjamin invariant pada waktu kompilasi alih-alih validasi saat runtime
  • Mendefinisikan tipe baru (newtype) seperti NonZeroF32, NonEmptyVec untuk membuat status yang salah (0, vektor kosong, dan sebagainya) tidak bisa direpresentasikan
  • Alih-alih mengembalikan kegagalan dengan Option atau Result, pendekatan ini memperketat batasan pada argumen fungsi agar error dicegah sejak awal
  • Menunjukkan contoh seperti String::from_utf8 atau serde_json::from_str yang mengubah data menjadi tipe bermakna melalui parsing
  • Prinsip desain untuk membuat status ilegal tidak dapat direpresentasikan dan memajukan validasi sedini mungkin meningkatkan stabilitas serta keterbacaan kode

1. Mengekspresikan batasan dengan tipe, bukan validasi runtime

  • Pada fungsi divide(a, b), pembagian dengan 0 menyebabkan panic saat runtime
    • Kegagalan bisa direpresentasikan dengan mengembalikan Option, tetapi ini melemahkan tipe hasil
  • Dengan mendefinisikan tipe NonZeroF32, hanya nilai bukan 0 yang dapat dibuat
    • Konstruktor berbentuk fn new(n: f32) -> Option<NonZeroF32>, yang mengembalikan None saat gagal
    • Jika didefinisikan sebagai divide_floats(a: f32, b: NonZeroF32), validasi runtime tidak lagi diperlukan
  • Tanggung jawab validasi dipindahkan dari dalam fungsi ke sisi pemanggil, sehingga error dihilangkan lebih awal

2. Menghilangkan validasi duplikat dan menyederhanakan kode

  • Pada fungsi roots(a, b, c), jika pemeriksaan a == 0 ditangani dengan Option, validasi duplikat muncul baik di pemanggil maupun di fungsi
  • Dengan memakai NonZeroF32, validasi cukup dilakukan sekali, lalu logika setelahnya menjadi lebih sederhana
  • Prinsip yang sama bisa dipakai untuk mendefinisikan NonEmptyVec<T> agar vektor kosong tidak diizinkan
    • Jika get_cfg_dirs() mengembalikan NonEmptyVec<PathBuf>, maka validasi tambahan di main() tidak diperlukan lagi

3. Contoh nyata: String dan serde_json

  • String secara internal adalah tipe baru (newtype) dari Vec<u8>, dan String::from_utf8 melakukan pemeriksaan validitas
    • Setelah itu, nilainya bisa dipakai dengan aman sebagai string yang dijamin UTF-8
  • serde_json from_str::<Sample> mem-parse JSON menjadi struct dan menjamin keberadaan field serta konsistensi tipe pada waktu kompilasi
    • Semua batasan seperti keberadaan field foo, bar, kecocokan tipe, panjang array, dan lainnya diperiksa pada level tipe

4. Dua prinsip desain berbasis tipe

  • Membuat status ilegal tidak dapat direpresentasikan
    • NonZeroF32 tidak bisa merepresentasikan 0, dan NonEmptyVec tidak bisa merepresentasikan keadaan kosong
    • Fungsi validasi sederhana seperti is_nonzero tetap memungkinkan status yang salah direpresentasikan, sehingga tidak lengkap
  • Lakukan validasi sedini mungkin
    • Jika validasi tersebar di seluruh kode seperti pada ‘Shotgun Parsing’, hal itu bisa berujung pada celah keamanan (CVE-2016-0752, dan lain-lain)
    • Jika semua batasan diperiksa pada tahap parsing, logika setelahnya dapat berjalan dengan aman

5. Pembuktian berbasis tipe di Rust dan penerapannya

  • Menurut korespondensi Curry-Howard, tipe dapat dipandang sebagai proposisi logika, dan nilai sebagai buktinya
    • Dengan crate typenum, relasi matematis (3 + 4 = 8) dapat diverifikasi pada waktu kompilasi
  • Sistem tipe dapat digunakan untuk membuktikan kebenaran program pada saat kompilasi

6. Saran penerapan di dunia kerja

  • Meski API eksternal meminta tipe sederhana (bool, i32), di sisi internal sebaiknya tetap direpresentasikan sebagai enum atau newtype yang bermakna
    • Contoh: definisikan LightBulbState { On, Off } lalu implementasikan From<LightBulbState> for bool
  • Jika ada fungsi validasi sederhana seperti verify() atau do_something_fallible(), pertimbangkan transformasi ke tipe terstruktur melalui parsing
  • Untuk fungsi tanpa efek samping, status yang sengaja dibuat mustahil bisa diekspresikan lewat tipe seperti Result<Infallible, MyError>

7. Kesimpulan

  • Dengan memanfaatkan sistem tipe Rust sebagai alat validasi, kejelasan dan stabilitas kode dapat ditingkatkan
  • Berbagai alat di ekosistem Rust seperti Vec, sqlx, bon sudah memanfaatkan desain berbasis tipe
  • Tidak semua masalah bisa diselesaikan dengan tipe, tetapi pendekatan menaikkan logika validasi ke level tipe meningkatkan kemudahan pemeliharaan dan keamanan
  • Disarankan memanfaatkan sistem tipe Rust yang kuat semaksimal mungkin agar kompiler yang menangkap error lebih awal

1 komentar

 
GN⁺ 2026-02-23
Komentar Hacker News
  • Contoh pembagian dengan nol yang digunakan dalam tulisan ini kurang tepat untuk menjelaskan prinsip “Parse, Don’t Validate”
    Inti prinsip ini ada pada fungsi yang mengubah data tak tepercaya menjadi tipe yang secara struktural benar
    Dalam tulisan Alexis King "Names are not type safety", disebutkan juga bahwa pola newtype tidak dapat menjamin ‘correct by construction’ secara penuh
    Ketika sistem tipe tidak bisa mengekspresikan invariant secara langsung, pendekatan yang realistis adalah memakai tipe abstrak dengan smart constructor yang meniru parser
    Contoh kedua, yaitu non-empty vec, merupakan kasus yang jauh lebih baik karena menjamin di dalam sistem tipe bahwa “selalu ada setidaknya satu elemen”

    • “parse, don’t validate” berbasis newtype tetap sangat berguna dalam praktik
      Saat asal sebuah string tidak jelas, nilai yang dienkapsulasi sangat meningkatkan keandalan
      Untuk correctness-by-construction yang sepenuhnya, dibutuhkan sistem tipe dependen, tetapi ada juga alternatif ringan seperti pattern types di Rust
      Misalnya, bisa membatasi rentang seperti i8 is 0..100 atau mengekspresikan slice yang tidak kosong dengan [T] is [_, ..]
      Namun, non-empty list berbentuk (T, Vec<T>) adalah contoh benturan antara kepraktisan dan kemurnian teoretis, karena ada banyak batasan jika ingin memperlakukannya seperti vektor
    • ‘correct by construction’ adalah tujuan akhirnya
      Tipe seperti NonZeroU32 memang sederhana, tetapi kekuatan sebenarnya ada pada merancang seluruh logika domain sebagai tipe sehingga compiler bertindak sebagai penjaga gerbang
      Dengan begitu, beban debugging berpindah dari runtime ke tahap perancangan
    • Materi terkait juga bisa dicari dengan kata kunci “make invalid states impossible/unrepresentable”
      Sebagai contoh, "Domain Modeling Made Functional" dan video terkait layak dilihat
    • Contoh pembagian dengan nol adalah kasus pemisahan concern yang keliru
      Daripada mencoba membungkusnya pada level seperti itu, akan lebih jelas bila membungkus perilaku fungsi aritmetika seperti overflow
  • Saya merangkum tautan ke diskusi terkait terbaru
    Parse, Don't Validate (2019) (Februari 2026, 172 komentar)
    Parse, Don’t Validate – Some C Safety Tips (Juli 2025, 73 komentar)
    Parse, Don't Validate (2019) (Juli 2024, 102 komentar) dan lain-lain
    Ini dibagikan hanya sebagai referensi

  • Pendekatan parsing over validation punya keterbatasan ketika kita tidak bisa mengetahui semua kemungkinan kasus di dunia nyata
    Untuk hal seperti format file, membuatnya gagal sedini mungkin itu bagus, tetapi penerapannya pada logika bisnis atau pemodelan transisi status perlu hati-hati
    Jika kebutuhan di dunia nyata berubah, sistem bisa gagal mengakomodasinya dan pada akhirnya pengguna akan mencari jalan memutar

  • Dalam bahasa lain, pendekatan ini bisa dibawa lebih jauh dengan dependent typing
    Misalnya, get_elem_at_index(array, index) dapat menjamin rentang indeks pada waktu kompilasi meskipun panjang array tidak diketahui sebelumnya
    Tipe Vect n a dan Fin n di Idris adalah contohnya

    • Di Rust juga ada library berbasis makro yang meniru dependent type
      Contoh: anodized (video pengantar)
    • Jika panjang array dibaca dari stdin, maka nilainya tidak diketahui saat kompilasi, jadi verifikasi seperti ini terbatas pada kasus dengan informasi statis
    • Semoga fitur seperti ini menjadi lebih umum
  • Ada juga pendekatan dengan menaruh banyak fungsi pada satu tipe
    Ini seperti di Clojure, yang merepresentasikan semua data dengan satu map dan memungkinkan seluruh standard library memanipulasinya

    • Ada ketegangan antara ucapan Perlis tentang “100 fungsi untuk satu struktur data” dan “Parse, Don’t Validate”
      Invariant penting bisa dimasukkan ke dalam tipe, atau diekspresikan sebagai fungsi sederhana
      Bahkan dalam bahasa bertipe dinamis pun ada kebiasaan desain yang menghasilkan efek serupa
    • Ini bukan alternatif murni, melainkan sebuah trade-off
      Input eksternal tetap harus di-parse pada akhirnya, jadi ini bukan pengganti total
    • Kedengarannya mirip dengan kritik terhadap “stringly typed language”, tetapi sebenarnya ini adalah proses memurnikan bentuk data secara bertahap
    • Keseimbangan itu penting
      Dalam sistem tipe struktural, kita bisa meniru tipe nominal dengan branding, dan sebaliknya juga bisa, tetapi kurang ergonomis
      Pada akhirnya, yang realistis adalah mencampurkan kedua pendekatan dengan tepat
  • Diskusi ini mengingatkan pada fitur concepts di C++
    Dalam Concept-based Generic Programming karya Bjarne Stroustrup, ada contoh validasi otomatis untuk konversi integer
    Tipe seperti Number<unsigned int> atau Number<char> akan melempar exception jika nilainya berada di luar rentang

  • Contoh try_roots dalam tulisan itu sebenarnya adalah kontra-contoh
    Untuk mengekspresikan batasan b^2 - 4ac >= 0 sebagai tipe, hal itu menjadi sangat rumit di Rust
    Dalam kasus seperti ini, lebih masuk akal untuk cukup mengembalikan Option dan melakukan validasi di dalam fungsi
    Sebagian besar validasi menangani interaksi antara banyak nilai, sehingga kurang nyaman diselesaikan lewat “parsing”

    • Jika validitas input bergantung pada relasi antar beberapa argumen, pada akhirnya harus digabung ke bentuk seperti fn(abc: ValidABC)
  • Pola ini juga cocok untuk desain API
    Daripada memvalidasi request JSON, lebih baik langsung meng-parse-nya menjadi struct yang dijamin oleh tipe sehingga logika setelahnya tidak perlu validasi berulang
    Ini mudah diterapkan di Rust dengan kombinasi serde + custom deserializer
    Saya pernah melihat kasus nyata di mana pendekatan seperti ini mengurangi kode penanganan error sebesar 60%

    • Di Go juga ada upaya serupa, tetapi hasilnya agak bertele-tele karena penyalahgunaan pointer dan tidak adanya tipe aljabar
  • Filosofi yang sama juga diterapkan pada UI design system
    Alih-alih memeriksa CSS belakangan, didefinisikan tipe yang hanya mengizinkan penempatan berdasarkan satuan grid, sehingga margin arbitrer seperti 13px menjadi error saat kompilasi
    Dengan cara ini, desain tetap deterministik

    • Ada pertanyaan tentang tooling apa yang digunakan
  • records + pattern matching di C# mendekati pendekatan ini
    discriminated unions di F# lebih kuat lagi, sehingga dengan Result<'T,'Error> kita bisa membuat status tidak valid menjadi mustahil direpresentasikan
    Jika C# nantinya mengadopsi DU native, hasilnya akan jauh lebih rapi