Komposisi railway-oriented

Mengikat langkah-langkah rawan gagal menjadi satu pipeline dengan bind dan map.

Daftar isi

Topik ini membahas komposisi railway-oriented, yaitu pola menyusun fungsi-fungsi yang mengembalikan Result menjadi satu pipeline tanpa pencabangan yang menumpuk. Pembahasan mencakup metafora jalur utama dan jalur gagal, implementasi bind secara manual sebelum menggunakan fungsi bawaannya, serta pipeline validasi multi langkah yang tersusun dari Result.bind dan Result.map.

Dua jalur dalam satu aliran#

Fungsi biasa memiliki satu jalur masuk dan satu jalur keluar, sedangkan fungsi yang mengembalikan Result mengeluarkan dua kemungkinan, yaitu Ok pada jalur utama dan Error pada jalur gagal. Masalah muncul ketika langkah kedua perlu disambungkan, karena keluaran langkah pertama berupa dua jalur, sedangkan fungsi langkahnya hanya menerima satu nilai. Menyambungkannya dengan match pada setiap sambungan membuat kode kembali penuh pencabangan.

Tanpa bind, setiap sambungan menuntut pencabangan yang membongkar Ok dan meneruskan Error. Dua langkah saja sudah menghasilkan match bersarang, dan setiap langkah tambahan menambah satu tingkat lagi. Kedalaman itu tumbuh pada bagian yang justru paling tidak penting, karena bentuk pencabangannya sama untuk setiap sambungan.

Diagram berikut memperlihatkan susunan yang dianjurkan. Langkah-langkah terjajar pada jalur utama, sedangkan kegagalan pindah ke jalur gagal dan melaju melewati seluruh langkah yang tersisa sampai ujung pipeline.

masukan Resulthasil Resultjalur utama (Ok)jalur gagal (Error)langkah 1'a -> Result<'b,'e>langkah 2'a -> Result<'b,'e>Error pindah jalurOkError
Alur railway dengan dua jalur

Metafora kereta api menekankan satu hal, yaitu kegagalan tidak menghentikan kereta, melainkan memindahkannya ke rel yang lain. Seluruh langkah tetap ditulis sebagai fungsi satu jalur yang mengembalikan Result. Tugas penyambungan antar langkah diserahkan kepada satu fungsi kecil bernama bind.

Operator |> yang menjadi fondasi pipeline tetap berperan penuh di sini. Nilai Result mengalir melalui |> sama seperti nilai biasa, karena Result hanyalah data biasa. Yang mengubah aliran menjadi dua rel adalah fungsi penyambungnya, bukan operator pipeline itu sendiri. Dengan demikian, kemampuan operator pipeline tetap berlaku, dan bind hanya menambahkan aturan penyambungan untuk kegagalan.

Bind sebagai rel penghubung#

Bind adalah fungsi dengan dua masukan, yaitu hasil sebelumnya yang bertipe Result dan langkah berikutnya yang bertipe fungsi dari nilai ke Result. Apabila hasil sebelumnya berupa Ok, bind meneruskan nilai di dalamnya ke langkah berikutnya. Apabila berupa Error, bind membiarkan alasan kegagalan lewat tanpa memanggil langkahnya. Implementasinya hanya berupa match dengan dua cabang.

fsharp bind-manual.fsx
// bind manual: rel penghubung dari keluaran dua jalur ke fungsi satu jalur
let bind (hasil: Result<'a, 'e>) (langkah: 'a -> Result<'b, 'e>) : Result<'b, 'e> =
    match hasil with
    | Ok nilai -> langkah nilai
    | Error e -> Error e

let uraiUmur (teks: string) : Result<int, string> =
    match System.Int32.TryParse teks with
    | true, n -> Ok n
    | false, _ -> Error (sprintf "%s bukan bilangan bulat" teks)

let cekRentang (n: int) : Result<int, string> =
    if n >= 10 && n <= 80 then Ok n
    else Error (sprintf "umur %d di luar rentang piket" n)

let validasi (teks: string) : Result<int, string> =
    bind (uraiUmur teks) cekRentang

printfn "%A" (validasi "17")
printfn "%A" (validasi "sembilan")
printfn "%A" (validasi "120")
text Keluaran
Ok 17
Error "sembilan bukan bilangan bulat"
Error "umur 120 di luar rentang piket"

Signature bind, yaitu Result<'a,'e> -> ('a -> Result<'b,'e>) -> Result<'b,'e>, menjelaskan seluruh perilakunya. Masukan pertama berupa dua jalur dari langkah sebelumnya, sedangkan masukan kedua berupa fungsi satu jalur yang hanya menangani nilai Ok. Hasil setiap sambungan kembali bertipe Result, sehingga bind dapat dipanggil beruntun tanpa batas, dan itulah bentuk dasar dari pipeline.

Keluaran memperlihatkan tiga kemungkinan yang berbeda. Masukan yang sah melewati kedua langkah dan berakhir sebagai Ok, masukan yang tidak dapat diurai berhenti pada langkah pertama, dan masukan di luar rentang berhenti pada langkah kedua. Ketiganya menghasilkan tipe yang sama, yaitu Result<int, string>, sehingga pemanggil memperlakukannya dengan cara yang sama.

Bentuk bind pada Result sejajar dengan Option.bind yang menyambungkan pencarian beruntun pada tipe Option. Perbedaannya ada pada kasus gagal, karena Error membawa alasan yang wajib diteruskan, sedangkan None tidak membawa apa-apa.

Pipeline validasi multi langkah#

Dengan bind, pipeline dibangun tanpa satu pun match di tempat pemanggilan. Operator pipeline menyusun langkah-langkahnya dari atas ke bawah, Result.bind menyambungkan langkah yang dapat gagal, dan Result.map menutup rangkaian dengan langkah yang tidak dapat gagal. Berkas berikut membandingkan kedua bentuk penyambungan pada rangkaian tiga langkah, yaitu penguraian teks, pemeriksaan rentang, dan penetapan kelompok piket.

fsharp pipeline-validasi.fsx
// Pipeline validasi tiga langkah: penyambungan manual dibandingkan bind
let uraiUmur (teks: string) : Result<int, string> =
    match System.Int32.TryParse teks with
    | true, n -> Ok n
    | false, _ -> Error (sprintf "%s bukan bilangan bulat" teks)

let cekRentang (n: int) : Result<int, string> =
    if n >= 10 && n <= 80 then Ok n
    else Error (sprintf "umur %d di luar rentang piket" n)

let kelompokPiket (n: int) : string =
    if n <= 15 then "piket pagi" else "piket sore"

// penyambungan manual: match bersarang pada setiap sambungan
let tetapkanManual (teks: string) : Result<string, string> =
    match uraiUmur teks with
    | Ok n ->
        match cekRentang n with
        | Ok nSah -> Ok (kelompokPiket nSah)
        | Error e -> Error e
    | Error e -> Error e

// penyambungan dengan bind: satu baris per langkah
let tetapkan (teks: string) : Result<string, string> =
    uraiUmur teks
    |> Result.bind cekRentang
    |> Result.map kelompokPiket

printfn "%A" (tetapkanManual "17")
printfn "%A" (tetapkanManual "sembilan")
printfn "%A" (tetapkan "17")
printfn "%A" (tetapkan "sembilan")
text Keluaran
Ok "piket sore"
Error "sembilan bukan bilangan bulat"
Ok "piket sore"
Error "sembilan bukan bilangan bulat"

Dua fungsi terakhir menghasilkan keluaran yang sama untuk masukan yang sama, dan perbedaannya ada pada bentuk penulisannya. Fungsi tetapkanManual menyambungkan langkah dengan match bersarang, dan setiap langkah tambahan menambah satu tingkat bersarang. Fungsi tetapkan menyambungkan langkah yang sama melalui Result.bind dan Result.map, sehingga menambah langkah berarti menyisipkan satu baris tanpa mengubah kedalaman apa pun. Tidak ada try-with pada kedua versi, karena kegagalan mengalir sebagai data.

Seluruh langkah pada contoh menggunakan string sebagai tipe kegagalannya, sehingga rangkaiannya langsung tersusun. Apabila salah satu langkah menggunakan tipe kegagalan berbeda, Result.mapError menerjemahkannya lebih dahulu sebelum masuk rangkaian. Disiplin kecil ini menjaga pipeline tetap dapat diperluas seiring bertambahnya aturan validasi.

Ringkasan#

  • Fungsi yang mengembalikan Result mengeluarkan dua jalur, yaitu Ok pada jalur utama dan Error pada jalur gagal.
  • bind menyambungkan keluaran dua jalur ke fungsi satu jalur, yaitu meneruskan nilai Ok dan membiarkan Error lewat.
  • Implementasi bind hanya berupa match dua cabang, dan modul Result menyediakannya sebagai Result.bind.
  • Pipeline multi langkah tersusun dari Result.bind dan Result.map tanpa match di tempat pemanggilan, dengan syarat tipe kegagalan yang seragam.

Referensi#