Tipe Result

Keberhasilan atau kegagalan yang dibawa sebagai data biasa alih-alih lemparan exception.

Daftar isi

Topik ini membahas tipe Result, yaitu tipe yang membawa keberhasilan atau kegagalan beserta alasannya sebagai data biasa. Pembahasan mencakup konstruktor Ok dan Error, perbandingan Result dengan option, fungsi Result.map, Result.mapError, dan Result.bind, serta penguraian teks menjadi angka dengan Int32.TryParse. Contoh yang digunakan berupa konversi satuan suhu dan validasi masukan.

Keberhasilan dan kegagalan sebagai data#

Tipe Result adalah discriminated union dengan dua kasus, yaitu Ok yang membawa nilai hasil dan Error yang membawa alasan kegagalan. Bentuk lengkapnya adalah Result<'T,'TError>, sehingga tipe sukses dan tipe kegagalannya dinyatakan secara eksplisit. Fungsi yang dapat gagal mengembalikan Result, dan pemanggil menerima kegagalan sebagai data yang boleh dibaca, disimpan, atau diteruskan.

Konversi suhu Celsius ke Fahrenheit memuat aturan domain yang wajar, yaitu suhu di bawah nol mutlak tidak sah secara fisik. Alih-alih melempar exception, fungsi berikut mengembalikan alasan penolakan di dalam Error.

fsharp konversi-suhu.fsx
// Konversi suhu dengan alasan kegagalan yang dibawa sebagai data
let keFahrenheit (celsius: float) : Result<float, string> =
    if celsius < -273.15 then
        Error (sprintf "%.2f berada di bawah nol mutlak" celsius)
    else
        Ok (celsius * 9.0 / 5.0 + 32.0)

printfn "%A" (keFahrenheit 25.0)
printfn "%A" (keFahrenheit -40.0)
printfn "%A" (keFahrenheit -300.0)
text Keluaran
Ok 77.0
Ok -40.0
Error "-300.00 berada di bawah nol mutlak"

Nilai Ok 77.0 dan Error "-300.00 berada di bawah nol mutlak" bertipe sama, yaitu Result<float, string>, sehingga keduanya dapat disimpan dalam variabel yang sama, dimasukkan ke dalam list, atau diteruskan ke fungsi lain. Kegagalan bukan lagi peristiwa yang menghentikan aliran program, melainkan salah satu dari dua nilai yang mungkin dikembalikan. Kasus Error yang belum ditangani pemanggil tetap terlihat pada tipe hasil, dan compiler tidak membiarkan alasan kegagalan menghilang tanpa jejak.

Tipe alasan pada Error tidak harus string. Apabila kegagalan domain memiliki bentuk yang pasti, discriminated union dapat dideklarasikan sebagai tipe kegagalannya, misalnya Result<float, SuhuTidakSah>. Pemisahan tipe sukses dan tipe gagal membuat kedua jalurnya dapat berkembang tanpa saling mengganggu.

Result dibandingkan option#

Result dan option sama-sama menyatakan kemungkinan kegagalan dalam tipe, tetapi keduanya menyampaikan informasi yang berbeda. Option hanya menyatakan bahwa nilai dapat absen, sedangkan Result membawa alasan kegagalannya pada kasus Error. Perbedaan itu menentukan pilihan di antara keduanya.

SituasiPilihan yang tepat
Nilai memang dapat absen tanpa alasan yang perlu dibedakan, misalnya kolom opsionaloption
Kegagalan perlu dilaporkan bersama penyebabnya, misalnya validasi masukanResult
Kegagalan berasal dari pustaka .NET yang melempar exceptionResult hasil pembungkusan try-with

Pilihan yang keliru membuat informasi hilang. Fungsi validasi yang mengembalikan option memaksa pemanggil menebak penyebab penolakan, sedangkan fungsi yang sama dengan Result menyampaikannya langsung. Sebaliknya, option lebih ringkas untuk nilai yang ketidakhadirannya memang cukup dijelaskan dengan None, misalnya nama tengah pada data diri.

Tipe kembalian juga menjadi semacam kontrak yang terbaca dari signature fungsi. Fungsi bertipe string -> Result<int, string> mengumumkan bahwa kegagalan wajar beserta bentuk alasannya, sedangkan fungsi bertipe string -> int menjanjikan angka untuk setiap masukan. Pembacaan seperti ini memudahkan penilaian apakah pemanggil perlu menyiapkan penanganan kegagalan.

Memetakan dan mengikat Result#

Modul Result menyediakan fungsi transformasi yang sejajar dengan modul Option. Result.map mengubah nilai pada kasus Ok dan membiarkan Error lewat tanpa disentuh. Result.mapError mengubah alasan pada kasus Error, berguna ketika alasan kegagalan perlu diperkaya atau diberi konteks. Result.bind menyambungkan Result ke fungsi berikutnya yang juga mengembalikan Result, sehingga langkah-langkah rawan gagal dapat dirangkai.

fsharp fungsi-result.fsx
// Transformasi Result dengan map, mapError, dan bind
let keFahrenheit (celsius: float) : Result<float, string> =
    if celsius < -273.15 then Error "di bawah nol mutlak"
    else Ok (celsius * 9.0 / 5.0 + 32.0)

// map mengubah nilai pada jalur sukses
printfn "%A" (Result.map (fun f -> sprintf "%.1f F" f) (keFahrenheit 25.0))

// mapError memperkaya alasan pada jalur gagal
printfn "%A" (Result.mapError (fun e -> "validasi suhu: " + e) (keFahrenheit -300.0))

// bind menyambungkan ke langkah berikutnya yang juga menghasilkan Result
let amanKerja (fahrenheit: float) : Result<float, string> =
    if fahrenheit > 212.0 then Error "melebihi titik didih air"
    else Ok fahrenheit

printfn "%A" (Result.bind amanKerja (keFahrenheit 60.0))
printfn "%A" (Result.bind amanKerja (keFahrenheit 120.0))
text Keluaran
Ok "77.0 F"
Error "validasi suhu: di bawah nol mutlak"
Ok 140.0
Error "melebihi titik didih air"

Keempat baris keluaran memperlihatkan pembagian peran ketiga fungsi. Result.map membentuk teks dari nilai sukses, Result.mapError menambahkan konteks pada alasan kegagalan, dan Result.bind menilai hasil konversi pertama dengan aturan berikutnya. Kegagalan pada langkah mana pun menghentikan rangkaian tanpa pencabangan tambahan, dan pola inilah yang menjadi dasar komposisi railway-oriented.

Fungsi-fungsi tersebut dapat dirangkai karena keluaran satu transformasi tetap bertipe Result. Rangkaian yang lazim adalah Result.bind untuk langkah yang dapat gagal, disusul Result.map untuk pembentukan akhirnya. Urutan lain juga sah, misalnya Result.mapError diletakkan paling akhir agar seluruh alasan kegagalan memperoleh bentuk akhir di satu tempat.

Mengurai teks menjadi angka#

Penguraian teks menjadi bilangan adalah sumber kegagalan yang lazim, misalnya pada masukan yang diketik pengguna. Metode .NET Int32.TryParse tidak melempar exception, melainkan mengembalikan pasangan bool dan angka. Pola true, angka menandakan keberhasilan, sedangkan false, _ menandakan kegagalan. Pasangan itu diterjemahkan menjadi Result sehingga alasan kegagalannya menyatu dengan tipe hasil.

fsharp urai-angka.fsx
// Penguraian teks menjadi bilangan bulat dengan Int32.TryParse
let urai (teks: string) : Result<int, string> =
    match System.Int32.TryParse teks with
    | true, angka -> Ok angka
    | false, _ -> Error (sprintf "%s bukan bilangan bulat" teks)

let tampilkan (teks: string) =
    match urai teks with
    | Ok angka -> printfn "%s terbaca sebagai %d" teks angka
    | Error pesan -> printfn "%s ditolak: %s" teks pesan

tampilkan "42"
tampilkan "3.14"
tampilkan "tujuh"
text Keluaran
42 terbaca sebagai 42
3.14 ditolak: 3.14 bukan bilangan bulat
tujuh ditolak: tujuh bukan bilangan bulat

Fungsi urai tidak memeriksa isi teks dengan percabangan yang rapat karena TryParse sudah menguji formatnya. Perhatikan bahwa 3.14 ditolak meskipun tampak seperti bilangan, karena Int32.TryParse hanya menerima bilangan bulat. Kegagalan penguraian semacam ini bukan kegagalan sistem, melainkan data masukan yang tidak sesuai, sehingga tempatnya ada pada Result dan bukan pada exception.

Pengembalian bool oleh TryParse memang cukup untuk memutuskan lulus atau gagal, tetapi bool menghilangkan sebabnya. Apabila masukan ditolak, pesan yang menjelaskan penyebabnya jauh lebih berguna daripada sekadar nilai false. Result menjembatani keduanya, yaitu memanfaatkan pengujian dari TryParse sekaligus menyimpan alasan yang dapat ditampilkan.

Ringkasan#

  • Result<'T,'TError> membawa keberhasilan pada Ok dan kegagalan beserta alasannya pada Error, keduanya sebagai data biasa.
  • Result.map memetakan jalur sukses, Result.mapError memetakan alasan kegagalan, dan Result.bind merangkai langkah berikutnya.
  • option cukup untuk nilai yang dapat absen tanpa alasan yang perlu dibedakan, sedangkan Result tepat ketika penyebab kegagalan perlu dibawa.
  • Int32.TryParse mengembalikan pasangan bool dan angka yang diterjemahkan menjadi Result agar kegagalan penguraian tidak melempar exception.

Referensi#