Pemrograman Berorientasi Railway
(fsharpforfunandprofit.com)- Railway Oriented Programming adalah pendekatan untuk menangani jalur kegagalan yang berulang dalam aplikasi nyata—seperti validasi, logging, dan error jaringan—di dalam pipeline fungsional
- Intinya adalah menjelaskan keterbatasan contoh yang hanya menyambung jalur sukses dengan analogi railway, sehingga pengguna F# pun lebih mudah memahami alur error secara visual
- Dari sudut pandang Haskell, ini dekat dengan tipe
Eitherdan **komposisi Kleisli`, tetapi fokusnya adalah resep penanganan error dan pola implementasi, bukan teori abstrak - Untuk penerapan nyata, disediakan pustaka F# untuk NuGet Chessie, contoh GitHub yang membandingkan C# dan F#, serta contoh penerapan pada FizzBuzz
- Ini adalah pendekatan yang berguna, tetapi tidak boleh diterapkan secara berlebihan ke semua masalah; karena F# tidak memiliki type class, pustaka contohnya mendefinisikan sendiri fungsi-fungsi yang dibutuhkan
Materi presentasi dan kode contoh
- Halaman Railway Oriented Programming adalah kumpulan slide dan kode presentasi di satu tempat
- Titik berangkatnya adalah masalah bahwa hanya dengan “happy path” yang sering diasumsikan oleh contoh pemrograman fungsional, sulit membangun aplikasi nyata
- Aplikasi yang tangguh harus menangani validasi, logging, error jaringan, error layanan, dan berbagai situasi pengecualian lainnya bersama-sama
- Tujuannya adalah memperkenalkan secara singkat dan mudah dipahami cara menangani error seperti ini dengan gaya fungsional yang bersih melalui analogi railway
- Topik serupa juga dilanjutkan dalam seri recipe for a functional app
- Kode nyatanya bisa dilihat di proyek GitHub yang membandingkan pendekatan ROP di C# dan F#
- Meski berguna untuk penanganan error, ada juga peringatan agar tidak menerapkannya secara ekstrem, dan “Against Railway-Oriented Programming” direkomendasikan sebagai bacaan pendamping
Video dan slide
- Topik ini dipresentasikan di NDC London 2014
- Video lain dari presentasi yang sama juga tersedia
- Tersedia slide presentasi Functional Programming eXchange pada 14 Maret 2014
- Slide PowerPoint juga dapat diunduh dari GitHub, dan disebutkan bahwa slide tersebut bebas dipinjam dan digunakan
Monad Either dan komposisi Kleisli
- Bagi pengguna Haskell, pendekatan ini mungkin terlihat seperti tipe
Either - Di sini pendekatan tersebut dispesialisasikan ke bentuk yang memakai daftar tipe error kustom pada kasus
Left- Contoh tipenya adalah
type TwoTrack a b = Either [a] (b,[a])
- Contoh tipenya adalah
- Hal baru yang dibuat bukanlah konsep penanganan error itu sendiri, melainkan lebih dekat pada analogi railway untuk menjelaskannya
- Alasannya jelas mengapa istilah standar Haskell tidak ditempatkan di depan
- Materi ini bukan tutorial monad, melainkan berfokus pada pemecahan masalah penanganan error
- Banyak orang yang baru mengenal F# tidak akrab dengan monad, sehingga penjelasan yang visual dan tidak terlalu mengintimidasi bisa terasa lebih intuitif
- Menyebut tipe two-track yang memiliki
bindlangsung sebagai monad itu tidak akurat, dan hukum monad juga tidak dibahas Eitheradalah alat yang terlalu umum, jadi di sini yang ingin diberikan adalah resep, bukan sekadar alatnya
Komponen resep penanganan error
- Pendekatan ini adalah kumpulan teknik yang lebih luas daripada sekadar “pakai
Eitherdanbind” - Teknik yang termasuk di dalamnya adalah sebagai berikut
- Menggunakan daftar tipe error kustom di sisi kiri dan kanan, alih-alih bentuk sederhana seperti
Either String a - Mengintegrasikan fungsi monadik ke dalam pipeline dengan
bind(>>=) - Mengomposisikan fungsi monadik dengan komposisi Kleisli (
>=>) - Mengintegrasikan fungsi non-monadik ke dalam pipeline dengan
map(fmap) - Karena F# tidak menggunakan monad IO,
teedipakai untuk mengintegrasikan fungsi unit ke dalam pipeline - Memetakan exception ke kasus error
- Menggabungkan fungsi monadik secara paralel dengan
&&&dalam situasi seperti validasi - Memanfaatkan kelebihan tipe error kustom dalam domain-driven design
- Perluasan seperti logging, domain event, dan transaksi kompensasi
- Menggunakan daftar tipe error kustom di sisi kiri dan kanan, alih-alih bentuk sederhana seperti
- Tujuannya adalah membuat template yang cukup serbaguna untuk dipakai hampir di semua situasi, tetapi juga cukup membatasi untuk memaksakan gaya yang konsisten
- Jika cara menulis kode pada akhirnya nyaris selalu mengerucut ke satu bentuk, orang yang memelihara kode itu nanti akan lebih mudah cepat memahami strukturnya
- Pendekatan ini bukan satu-satunya solusi, tetapi bisa menjadi titik awal yang baik
- Bahkan di komunitas Haskell sendiri, cara menangani error tidak konsisten sehingga bisa membingungkan bagi pemula
Cara menerapkannya ke kode saya
- Jika membutuhkan pustaka F# yang sudah siap, Anda bisa melihat Chessie project yang dapat digunakan bersama NuGet
- Contoh web service yang menerapkan teknik ini disediakan sebagai proyek GitHub
- Contoh penerapan pendekatan ROP pada FizzBuzz dapat dilihat di Railway Oriented Programming: carbonated
- F# tidak memiliki type class, jadi tidak ada cara umum untuk menggunakan kembali monad
- FSharpX library menawarkan pendekatan yang berguna, tetapi pustaka
Rop.fsmendefinisikan semua fungsi langsung dari awal - Isolasi seperti ini punya keuntungan karena sama sekali tidak memiliki dependensi eksternal
Bacaan lanjutan
- Untuk mendefinisikan monad dengan benar, tidak cukup hanya mengimplementasikan
binddanreturn - Monad adalah struktur aljabar yang harus memenuhi hukum monad, dan hukum ini berkaitan dengan hukum monoid dalam situasi tertentu
- Jika Anda tertarik pada
Eitherdan komposisi Kleisli, Anda bisa melihat materi berikut- Monads in general
- The
Eithermonad - Kleisli categories and composition
- Comprehensive error handling approaches
2 komentar
Railway-Oriented Programming untuk Membuat Program Tanpa Cacat
Komentar Hacker News
Elixir menangani masalah ini dengan cukup rapi lewat keyword/makro
withSeperti
with {:ok, file_handle} <- File.open(filename), ... do {:ok, parsed} end, fungsi dijalankan dari atas ke bawah, dan jika setiap nilai balik tidak cocok dengan pola di sebelah kiri, ia mengembalikan lebih awal dengan nilai tersebutJadi tanpa perlu membuat setiap fungsi menerima baik
{:ok, value}maupun{:error, reason}, fungsi cukup menerima nilai yang menjadi perhatiannya, sementara pattern matching pada blokwithmenangani propagasi errorMisalnya, jika
File.openmengembalikan{:error, reason},IO.readtidak dijalankan dan hasil keseluruhanwithmenjadi{:error, reason}. Pada akhirnya, kita cukup menulis jalur sukses, dan jalur gagal bisa dicocokkan oleh pemanggil jika diinginkanRasanya seperti menciptakan ulang exception handling setengah jalan; kalau saja dinamai
elsealih-alihcatch, itu hanya membuatnya tampak tidak terlalu seperti exception handlingwheredi Haskell danletdi LispIni seperti menetapkan precondition untuk sebuah fungsi, dan di Elixir kita juga bisa memakai blok
elseIO.readgagal, siapa yang menutup file handle juga menjadi masalahBeberapa tahun kemudian, penulis juga menulis artikel lanjutan berjudul Against Railway Oriented Programming: https://fsharpforfunandprofit.com/posts/against-railway-orie...
Pemrograman berorientasi rel memang menarik dan ada tempat untuk menggunakannya, tetapi menurut saya perlu disertai peringatan besar. Dalam praktiknya, saya sering melihatnya dipakai untuk menciptakan ulang penanganan exception dengan cara yang buruk; jika exception dipahami dan digunakan dengan benar, sebagian besar kondisi error bisa ditangani dengan lebih rapi dan efektif
Keunggulan exception adalah, dalam kebanyakan kasus, pilihan yang aman menjadi default. Kondisi error adalah sinyal bahwa kode tidak bisa melakukan tugasnya sesuai spesifikasi, dan jika tetap dilanjutkan dalam keadaan itu, data bisa rusak karena asumsi yang keliru. Secara default, exception akan melakukan pembersihan lalu dipropagasikan ke pemanggil, sedangkan pendekatan seperti pemrograman berorientasi rel membutuhkan banyak boilerplate yang mudah terlupa dan mudah salah
Namun ada dua kasus ketika pendekatan ini berguna. Pertama, ketika error yang diharapkan bersifat spesifik dan terdefinisi dengan baik, seperti validasi, harus ditangani di titik kemunculannya. Kedua, ketika berada di lingkungan yang tidak bisa memakai penanganan exception. Kode asinkron berbasis Promise pada jQuery lama hampir menyerupai implementasi pemrograman berorientasi rel, tetapi di JavaScript modern dengan
async/await, penanganan exception bisa dipakai lagiImbalan besar dari pemrograman berorientasi rel adalah hanya dengan melihat signature fungsi, kita bisa langsung tahu error apa saja yang mungkin muncul
Boilerplate bisa dikurangi tergantung bahasanya. Haskell punya notasi
do, dan di F# kita bisa menulisnya dengan sangat rapi memakai result computation expression. Misalnya, kita bisa mendefinisikanLoginError = InvalidUser | InvalidPwd | Unauthorized of AuthError, lalu menyusun alurnya denganResult.requireSome,Result.requireTrue, danResult.mapErrorSebaliknya, saya juga penasaran apakah kelemahan exception bisa dikurangi. Saya ingin tahu apakah ada linter atau alat analisis statis yang terintegrasi dengan Java IDE dan secara otomatis menampilkan exception yang tidak tertangkap yang bisa dilempar dari baris kode tertentu
https://demystifyfp.gitbook.io/fstoolkit-errorhandling/fstoo...
Pertama, banyak bahasa memaksa scope yang canggung dan tidak perlu hanya untuk menangkap exception. Misalnya, ketika ingin mendeklarasikan dan menginisialisasi variabel dari hasil fungsi yang bisa melempar exception, lalu memberi nilai lain saat gagal, deklarasi dan inisialisasi harus dipisahkan
Masalah yang lebih besar adalah kondisi error seharusnya ditangani pada level terendah yang memiliki konteks cukup untuk menanganinya dengan benar, sementara default exception adalah “lempar terus sampai paling atas”. Tambahkan satu cache file saja, lalu fungsi
HandleRequestdi atas tiba-tiba bisa melempar exception I/O; abstraksi jadi mudah bocorMenurut saya, semua hal yang bisa dikembalikan oleh sebuah fungsi harus menjadi bagian eksplisit dari signature, dan pemanggil harus menanganinya secara eksplisit atau setidaknya menandai bahwa ia meneruskannya ke atas. Bahasa tidak perlu menuntut banyak boilerplate untuk itu
Resultdi Rust benar-benar berhasil menangani error dengan baikSaat file atau record tidak ada pun, jawabannya bergantung pada apakah itu diharapkan. Jika permintaan melakukan lookup memakai ID yang diberikan pengguna, lookup itu sendiri adalah validasi input pengguna. Namun jika kita melakukan lookup ke storage memakai nama blob yang tersimpan di record database dan blob-nya tidak ada, itu situasi yang cocok ditangani sebagai exception
Batas antara error dan exception memang kabur dan bergantung pada tim serta ranah masalahnya, tetapi menurut saya ini aturan praktis yang cukup baik
Dalam kode C++, kita bisa memeriksa data di awal, membuat aplikasi crash jika pemeriksaan gagal, lalu men-debug dan memperbaiki kode pemeriksaan awal berdasarkan kondisi error yang ditemukan
Karena data yang ditangani adalah data yang relatif jelas, mengikuti format data CAD atau topologi geometri yang diketahui, kondisi error terasa “cukup mudah” ditangani dalam arti kita bisa lebih dulu memahami seperti apa data yang benar
Sejujurnya, seluruh situs ini adalah gudang harta. Meski tidak berniat memakai bahasa fungsional, situs ini memberi sudut pandang lain, dan itu sangat membantu saya secara pribadi. Saya juga merekomendasikan tulisan-tulisan lainnya
Meski begitu, sepertinya saya tetap tidak akan pernah benar-benar paham apa itu monad
https://fsharpforfunandprofit.com/series/understanding-parse...
Saya penasaran apakah klaim bahwa “mendorong penanganan error jauh dari titik pemanggilan itu buruk” memang valid. Karena caller-lah yang paling tahu cara menangani error, rasanya seharusnya ditangani langsung, bukan diteruskan ke bawah
Misalnya dalam alur
validate and-then update-db and-then send-email, kegagalanvalidatemungkin tidak masalah jika dikembalikan ke caller. Namun jikaupdate-dbgagal, harus diputuskan apakah akan mencoba lagi, memakai layanan lain, memasukkannya kembali ke queue, atau memberi tahu pengguna. Kegagalansend-emailjuga samaPada akhirnya, bukankah ini menjadi struktur percabangan yang lebih kompleks, seperti
ValidationErrordiberitahukan ke pengguna,DBErrordicoba ulang 5 menit kemudian atau diberi tahu setelah retry jarak jauh, danEmailFailedmemasukkan email kembali ke queue lalu dianggap berhasil. Mungkin saja itu tidak masalahOrang yang tidak suka Go mengeluh bahwa kondisi error harus terlalu sering diperiksa, dan checked exception di Java juga mirip. Dengan
Either, Anda tidak dipaksa untuk memeriksa; Anda bisa memeriksanya langsung atau meneruskannya ke callerOrang yang tidak suka unchecked exception di Java mengeluh bahwa mereka tidak tahu apa yang akan dilempar dan kapan. Dengan
Either, hal ini menjadi eksplisitIntinya adalah kita tidak tahu apa yang harus dilakukan ketika terjadi error, dan
Eithercocok dengan ketidaktahuan itu. Dalam codebase Java saat ini, ketikaLimit getUserLimit(User)tidak ada, ia bisa melempar exceptionNotFound, mengembalikannull, atau mengembalikan nilai default, tetapi kita tidak akan tahu sebelum menggali kodenyaJika memakai
Either, caller bisa mempercayainya tanpa membaca kode internal, dan bisa dengan mudah mengubahnya menjadi kondisi kegagalan lain menggunakan tool bawaan sepertiorElse(null)atauorElseThrow(...). Selain itu, karenaEitheradalah ekspresi, ia bagus untuk diabstraksikan, sehingga kita bisa menulis logika retry umum alih-alih kode retry khusus di dalamDbUpdatertry-catchJava, tetapi hasilnya terlihat jauh lebih buruk, dengantrybertumpuk,catch(DBError),sleep(5), retry jarak jauh, re-queueEmailFailedError, dan notifikasiValidationErroryang saling bercampurhttps://gist.github.com/Andrewp2/9d97bd213b061166d6df565ce26...
Mungkin ada orang yang bisa menulis versi
try-catchyang lebih baik, tetapi menurut saya tetap jauh lebih buruk daripada versi railway-style yang disajikanSaya suka DDD, tetapi programmer biasa seperti saya membutuhkan siklus maintenance yang masuk akal dan bisa dikelola. TDD bagus pada kode yang dapat berubah karena mengurangi beban kognitif dalam maintenance
Railway-oriented programming bisa membawa banyak konteks melalui banyak transformasi, dan saya ingin menghindari situasi menerima kereta di ujung rel yang penuh dengan konteks bisnis dan sistem yang belum siap
Menurut saya DDD dan onion architecture memberikan keunggulan kedua sisi. Pada akhirnya railway tetap muncul, tetapi berada di luar, bukan tertanam di dalam mikro-app. Isu railway-style lebih baik dikodekan secara eksplisit dan sedikit merepotkan; jika dilakukan dengan baik, sebagian besar keputusan railway-style adalah keputusan bisnis yang tidak boleh dihindari dengan struktur coding yang pintar
Pemrograman bergaya monad memang luar biasa, tetapi ada godaan untuk memakainya demi menghindari keputusan bisnis yang diperlukan. Memasukkan ambiguitas ke dalam type system dapat menyebabkan situasi yang sulit atau mustahil di kemudian hari
try/catchkhusus di dalamtry/catchbesarMisalnya, di dalam fungsi
send-email, Anda bisa memilih untuk menangani error sementara yang sangat cepat. Jika berhasil terhubung dalam waktu yang diperbolehkan, kembali ke jalur sukses; jika tidak, kembalikan ke callerJika ada aturan yang jelas, penanganan yang membutuhkan intervensi manusia harus selalu dibungkus dan diteruskan ke atas. Meski kembali ke jalur sukses, sebaiknya tetap tinggalkan log peringatan
notify-usermenangani notifikasi pengguna untukValidationErrordanDBError,try-update-dbmembungkusupdate-db, retry 5 menit kemudian, dan retry jarak jauh, sementaratry-send-emailbisa memasukkan email kembali ke queue saat gagal lalu mengubahnya menjadi suksesMaka level teratas tetap seperti
validate and-then try-update-db and-then try-send-email or-then notify-user. Sepertinyaand/or/thentelah dipetakan dengan cukup baik kemap/bindSitus ini adalah salah satu situs edukasi pemrograman terbaik yang pernah saya temui dalam hal mengajarkan konsep yang nyata dan praktis. Saya terutama menyukai konsep membuat state yang salah menjadi tidak dapat direpresentasikan, dan berusaha menerapkannya apa pun bahasanya. Tentu saja, beberapa bahasa membuatnya lebih mudah
[https://fsharpforfunandprofit.com/posts/designing-with-types...](https://fsharpforfunandprofit.com/posts/designing-with-types-making-illegal-states-unrepresentable/
Jika memakai alur data alih-alih panggilan/pengembalian, masalah ini terselesaikan dengan lebih baik, dan sebagian besar hilang dengan sendirinya
Dalam model panggilan/pengembalian, karena harus mengembalikan sesuatu, jika tidak ada nilai yang bisa dikembalikan akibat error, kita harus mengembalikan error, atau seperti Go, mengembalikan error bersama nilai normal. Ini mencemari jalur sukses. Meneruskan sisa pemrosesan lewat container polimorfik memang sedikit memperbaiki keadaan, tetapi masalahnya tetap ada
Dalam alur data, cukup tidak mengirim apa pun ke tahap filter berikutnya, sehingga jalur sukses sama sekali tidak terpengaruh. Error bisa dikirim ke tempat seperti standard error output, lalu dipusatkan di level aplikasi
Ini mungkin terdengar terlalu bagus, tetapi kami benar-benar memakainya di Wunderlist dan berjalan dengan baik. Teknik yang sama untuk saat nilai tidak ada juga langsung berlaku ketika nilainya belum ada, yaitu pemrosesan asinkron
Kalau error terjadi pada stream data sederhana, tidak ada data untuk tahap berikutnya sehingga berhenti itu tidak masalah. Namun jika pada
sqrt(-1)langsung “crash” lalu melompat ke output error atau logging, pada use case tertentu akan menjadi sulit ketika kita ingin memperluas domain agar memberikan nilai lain untuk bilangan negatifDi sini tipe error eksplisit seperti
Maybeberguna. Jalur sukses ditulis seolah-olah tidak ada error, tetapi komposisi fungsi memakai operator lain. Jika ingin pulih dari error, tangani kedua jalur di tempat itu, dan setelahnya bisa saja tidak lagi mengizinkan error. Yang terutama bagus adalah, untuk mengambil nilai nyata dari abstraksi “nilai yang mungkin ada”, kita harus menangani kedua jalur dengan eleganDengan tipe error eksplisit, kita juga bisa keluar dari dikotomi sukses/error. Status seperti “tidak ada nilai, tetapi” atau “ada nilai, tetapi” juga bisa direpresentasikan, dan error dari beberapa pekerjaan yang dapat diproses paralel bisa diakumulasikan lalu dikumpulkan sebagai hasil. Lebih jauh lagi, nondeterminisme bisa dienkode untuk hal-hal seperti penelusuran graf
Seperti yang dilakukan programmer Haskell, state juga bisa dibawa dengan cara yang murni, dan monad
IOHaskell yang terkenal itu lebih mirip perangkat untuk membatasi titik kontak dengan dunia luar kemain, bukan benar-benar merepresentasikan jalur aktual. Itu demi kemurnian, tetapi menurut saya kemurnian tidak selalu merupakan nilai yang harus dikejar secara absolutTeknik ini jauh lebih beragam daripada rel kereta sederhana, dan menyediakan cara yang rapi untuk hanya menulis jalur sukses sambil mempertahankan kemurnian. Ini juga menguntungkan untuk analisis statis berbasis persamaan dan unit test tanpa mock. Saya berharap lebih banyak bahasa arus utama menyediakan kotak alat monad seperti ini dengan lebih baik
Berikut tautan-tautan terkait: Railway Oriented Programming - https://news.ycombinator.com/item?id=31404643 - Mei 2022, Railway-Oriented Programming (2015) - https://news.ycombinator.com/item?id=17337155 - Juni 2018, Railway oriented programming - https://news.ycombinator.com/item?id=11955917 - Juni 2016, Railway Oriented Programming - https://news.ycombinator.com/item?id=9166943 - Maret 2015, Railway-oriented programming - https://news.ycombinator.com/item?id=7887134 - Juni 2014
Selain itu, What is railway oriented programming? (2020) - https://news.ycombinator.com/item?id=34245639 - Januari 2023, Railway Oriented Programming in Elixir (2015) - https://news.ycombinator.com/item?id=11958578 - Juni 2016
Gaya ini menular seperti monad lain, sehingga semua logika bisnis menjadi berbentuk seperti ini. Jika itu tidak masalah bagi Anda, silakan lanjut membaca
Exception, sesuai namanya, adalah error yang tidak terduga, dan stack akan di-unwind sampai ke tempat yang menangkapnya sambil secara otomatis menjalankan pembersihan yang diperlukan. Ini cocok untuk error yang terjadi di luar cakupan fungsi bisnis, seperti error I/O atau kehabisan memori
Jika ingin memvalidasi data, tidak perlu bergantung pada exception handler atau monad. Jadikan hasil pipeline fungsional sebagai data. Jika ada error validasi, kumpulkan ke dalam sebuah set, lalu periksa setelah pipeline selesai. Biasanya besar kemungkinan kita tidak ingin gagal pada error pertama
Jika pipeline membutuhkan efek samping, jangan lakukan; deskripsikan efek itu sebagai data lalu jalankan nanti. Jika perlu keluar lebih awal di tengah jalan, pisahkan pipeline pada titik itu, proses hasilnya, lalu masukkan ke pipeline lain
Atau bisa juga memakai rantai interceptor agar memutuskan apakah jalur sukses dilanjutkan. Jika bahasa mendukungnya, gunakan pattern matching. Yang penting adalah keputusan apakah akan melewati sebagian pipeline atau tidak dibuat di luar fungsi bisnis; dengan begitu fungsi bisnis hanya perlu memikirkan transformasi data yang murni, sehingga kemungkinan untuk digunakan ulang menjadi lebih tinggi
Senang saja topik seperti ini muncul lagi dan didiskusikan. Saya membaca situs ini selama setahun dan mengikuti contoh-contohnya; ini salah satu materi F# terbaik
Konsep-konsep yang dibahas di situs ini saja sudah membantu untuk berpikir secara fungsional dalam bahasa apa pun
Situsnya sudah lama, jadi kalau sekarang mendapat perhatian, saya penasaran apakah itu berarti F# sedang mendapatkan momentum. Dan saya juga penasaran apakah programmer bahasa lain memakai penanganan error ala railway seperti di Rust
Meski begitu, alasan saya belum bisa pindah ke F# adalah karena sebagian besar waktu saya dihabiskan untuk menyesuaikan frontend desktop. Di area ini, F# sepertinya tidak memberi banyak keuntungan. Frontend desktop yang siap produksi kebanyakan berupa kode berorientasi objek berbasis C#, bukan F# native, dan hanya diikat dengan F# lewat kode glue yang tipis
Begitu perlu kustomisasi UI yang sedikit saja kompleks, kita harus kembali ke dunia berorientasi objek, dan kalau begitu menurut saya lebih baik langsung melakukannya di C#. Secara pribadi, agar F# mendapat momentum, ia membutuhkan framework UI native
Tdan enum tertentu yang mendefinisikan kondisi hasilKalau terburu-buru, bisa juga memakai
std::pair, atau mengembalikan kalimat error dalam string lalu keluar lebih awal jika string itu tidak kosong. Jika semua fungsi mengembalikan tipe seperti itu, kalimat error bisa di-bubble up sampai level teratasSaya senang tulisan ini muncul lagi setiap beberapa tahun. Ini benar-benar tulisan sekaligus presentasi yang sangat bagus
Setiap kali membacanya lagi, posisi saya dalam perjalanan teknis—baik dari sisi kemampuan maupun filosofi—sudah berbeda, jadi menarik juga karena selalu terlihat berbeda