Topik ini membangun workflow dari nol untuk menunjukkan bahwa satu kelas dengan beberapa member sudah cukup membentuk computation expression yang berguna. Dua workflow dibangun secara berurutan, yaitu LoggingBuilder yang mencatat setiap langkahnya dan workflow validasi yang menumpuk error alih-alih berhenti pada error pertama. Member tambahan di luar Bind dan Return, yaitu Zero, Combine, Delay, For, While, dan Run, dijelaskan bersama contoh yang benar-benar dijalankan agar kontraknya tidak menjadi tebakan.
LoggingBuilder dari nol#
LoggingBuilder mengubah workflow biasa menjadi urutan langkah yang mencatat nilai yang lewat. Bind-nya mencetak nilai yang diterima sebelum meneruskannya ke fungsi lanjutan, sedangkan Return-nya mencetak nilai akhir sebelum mengembalikannya. Kelasnya hanya berisi dua member dan tidak membungkus nilai apa pun, sehingga tipe hasil workflow tetap tipe aslinya. Berkas berikut menghitung nilai akhir dari bobot tugas dan bobot ujian, dengan setiap langkahnya tercatat.
// LoggingBuilder: setiap langkah mencatat nilai yang diterimanya
type LoggingBuilder() =
member _.Bind(nilai: 'a, langkah: 'a -> 'b) : 'b =
printfn "bind menerima %A" nilai
langkah nilai
member _.Return(nilai: 'a) : 'a =
printfn "return menghasilkan %A" nilai
nilai
let log = LoggingBuilder()
let rekap =
log {
let! tugas = 82
let! ujian = 74
let! total = tugas * 40 / 100 + ujian * 60 / 100
return total
}
printfn "nilai akhir: %d" rekapbind menerima 82
bind menerima 74
bind menerima 76
return menghasilkan 76
nilai akhir: 76Urutan cetaknya pasti karena setiap let! dievaluasi dari atas ke bawah sebelum return menutup workflow. Baris bind menerima 82 dan bind menerima 74 menunjukkan dua nilai mentah yang diikat, sedangkan bind menerima 76 menunjukkan hasil perhitungan pada langkah ketiga. Workflow seperti ini berguna pada pencatatan (log) proses yang setiap langkahnya perlu diaudit, dan tambahannya hanya berupa baris cetak di dalam Bind tanpa mengubah logika perhitungan sama sekali.
Builder semacam ini menempati posisi di antara pencatatan biasa dan workflow nyata. Nilai yang mengalir tidak dibungkus ulang, sehingga tidak ada lapisan tipe tambahan, sedangkan pencatatan tetap terjadi pada setiap sambungan. Karena urutan cetak mengikuti urutan let!, keluarannya dapat dibandingkan baris demi baris dengan badan workflow, dan itu mempermudah penelusuran nilai pada proses yang hasil akhirnya tidak sesuai dugaan.
Member tambahan Zero, Delay, For, dan While#
Workflow yang isinya lebih kaya menuntut member tambahan. Zero menyediakan nilai untuk badan yang tidak menghasilkan apa pun, misalnya badan perulangan yang hanya mencetak. Delay menerima fungsi tanpa parameter dan menyimpannya tanpa menjalankannya, sehingga badan workflow dapat diulang atau ditunda. For menerjemahkan for ... in, sedangkan While menerjemahkan while ... do dan membutuhkan badan berbentuk fungsi agar dapat dipanggil ulang pada setiap putaran. Combine menyambungkan dua bagian badan yang berurutan, dan Run menjalankan badan workflow yang telah tersusun.
Berkas berikut melengkapi LogBuilder dengan keenam member tersebut dan menggunakannya untuk perulangan for serta while. Delay-nya mengembalikan fungsi apa adanya, sehingga While menerima badan berupa fungsi dan Combine memanggil fungsi kelanjutannya.
// Builder log dengan member Zero, Delay, Combine, For, dan While
type LogBuilder() =
member _.Bind(x: 'a, f: 'a -> 'b) : 'b = f x
member _.Return(x: 'a) : 'a = x
member _.Zero() : unit = ()
member _.For(xs: seq<'a>, f: 'a -> unit) : unit =
for x in xs do
f x
member _.While(guard: unit -> bool, body: unit -> unit) : unit =
while guard () do
body ()
member _.Delay(f: unit -> unit) : unit -> unit = f
member _.Combine(a: unit, b: unit -> unit) : unit = a; b ()
member _.Run(f: unit -> unit) : unit = f ()
let log = LogBuilder()
// for..in di dalam workflow diterjemahkan menjadi pemanggilan For
let tandaiPiket () =
log {
for nama in ["Ali"; "Budi"; "Citra"] do
printfn "menandai piket: %s" nama
}
printfn "== for =="
tandaiPiket ()
// while..do diterjemahkan menjadi While dengan badan berbentuk fungsi
let hitungMundur () =
log {
let mutable sisa = 3
while sisa > 0 do
printfn "menunggu %d langkah" sisa
sisa <- sisa - 1
printfn "selesai"
}
printfn "== while =="
hitungMundur ()== for ==
menandai piket: Ali
menandai piket: Budi
menandai piket: Citra
== while ==
menunggu 3 langkah
menunggu 2 langkah
menunggu 1 langkah
selesaiKeluaran kedua bagian menunjukkan bahwa perulangan di dalam workflow berjalan sama seperti perulangan biasa, dan pemanggilan member-nya tidak terlihat oleh pembaca workflow. Apabila salah satu member dihapus, compiler menolak workflow dengan pesan yang menyebut member yang kurang, misalnya workflow dengan for tanpa member For. Pesan error tersebut menjadi cara cepat untuk memetakan kata kunci mana yang menuntut member mana.
Peran Run pada berkas di atas terlihat sedikit berbeda karena Delay-nya mengembalikan fungsi, bukan nilai jadi. Compiler membungkus badan workflow ke dalam fungsi melalui Delay, lalu Run memanggil fungsi tersebut satu kali di ujung. Bentuk fungsi inilah yang digunakan While untuk mengulang badannya dan Combine untuk mengeksekusi kelanjutannya, sehingga ketiga member itu saling menopang dan kontraknya perlu dipahami sebagai satu paket.
Workflow validasi yang menumpuk error#
Workflow validasi menghadapi pilihan desain, yaitu berhenti pada error pertama atau menumpuk seluruh error sekaligus. Bind pada ValidasiBuilder berperilaku seperti biasa, yaitu meneruskan Error tanpa memanggil langkah berikutnya, sehingga rangkaian let! atau do! beruntun tetap berhenti pada kegagalan pertama. Penumpukan justru lahir dari Combine, yaitu member yang menyambungkan dua bagian badan workflow. Apabila kedua sisi Combine sama-sama gagal, daftar error dari keduanya digabungkan menjadi satu.
Cabang if tanpa else adalah bentuk yang diterjemahkan melalui Zero dan Combine, sehingga setiap pemeriksaan ditulis sebagai cabang tersendiri. Berkas berikut menerapkan pola itu pada validasi pendaftaran piket dengan tiga pemeriksaan mandiri.
// Workflow validasi yang menumpuk error melalui member Combine
type ValidasiBuilder() =
member _.Bind(m: Result<'a, string list>, langkah: 'a -> Result<'b, string list>) : Result<'b, string list> =
match m with
| Ok v -> langkah v
| Error e -> Error e
member _.Return(nilai: 'a) : Result<'a, string list> = Ok nilai
member _.Zero() : Result<unit, string list> = Ok ()
member _.Combine(a: Result<unit, string list>, b: Result<'b, string list>) : Result<'b, string list> =
match a, b with
| Error e1, Error e2 -> Error (e1 @ e2)
| Error e1, Ok _ -> Error e1
| Ok _, Error e2 -> Error e2
| Ok _, Ok v -> Ok v
member _.Delay(f: unit -> Result<'a, string list>) : Result<'a, string list> = f ()
member _.Run(m: Result<'a, string list>) : Result<'a, string list> = m
let validasi = ValidasiBuilder()
let namaKosong (nama: string) = System.String.IsNullOrWhiteSpace nama
let diLuarRentang (umur: int) = umur < 10 || umur > 80
let melebihiKuota (tugas: int) = tugas > 5
// setiap pemeriksaan menjadi cabang if tanpa else yang disambung Combine
let daftarPiket (nama: string, umur: int, tugas: int) : Result<string, string list> =
validasi {
if namaKosong nama then do! Error ["nama kosong"]
if diLuarRentang umur then do! Error [sprintf "umur %d di luar rentang" umur]
if melebihiKuota tugas then do! Error [sprintf "tugas %d melebihi kuota" tugas]
return sprintf "%s terdaftar" nama
}
printfn "%A" (daftarPiket ("Ali", 16, 3))
printfn "%A" (daftarPiket (" ", 90, 7))
printfn "%A" (daftarPiket ("Budi", 85, 3))
printfn "%A" (daftarPiket ("Citra", 14, 2))Ok "Ali terdaftar"
Error ["nama kosong"; "umur 90 di luar rentang"; "tugas 7 melebihi kuota"]
Error ["umur 85 di luar rentang"]
Ok "Citra terdaftar"Baris kedua keluaran memperlihatkan tiga error yang bertumpuk dalam satu Error, padahal tiga pemeriksaannya berdiri sendiri. Baris ketiga menunjukkan penumpukan yang hanya membawa satu pesan karena satu-satunya pemeriksaan yang gagal, dan dua baris sisanya menunjukkan jalur sukses tanpa pesan apa pun. Tipe kegagalannya sengaja berupa string list agar Combine memiliki sesuatu untuk digabungkan, dan bentuk Error [pesan] pada tiap pemeriksaan menghasilkan daftar berisi satu pesan yang siap disambung.
Pilihan daftar string sebagai tipe kegagalan menuntut penyesuaian kecil pada setiap pemeriksaan, yaitu membungkus pesan tunggal ke dalam Error [pesan]. Keuntungan dari penyesuaian itu adalah penumpukan yang tetap teratur, karena Combine hanya perlu menyambungkan dua daftar. Apabila kegagalan dimodelkan sebagai tipe data sendiri, pola yang sama berlaku dengan menggantikan penyambungan daftar sesuai bentuk datanya.
Ringkasan#
- LoggingBuilder hanya membutuhkan Bind dan Return untuk mencatat setiap langkah beserta urutan cetak yang pasti.
- Zero menyediakan nilai badan kosong, Delay menunda eksekusi badan, Combine menyambungkan dua bagian badan, sedangkan For dan While menerjemahkan perulangan.
- Penumpukan error pada workflow validasi lahir dari Combine yang menggabungkan daftar kegagalan cabang
iftanpaelse. - Rangkaian
do!beruntun tetap berhenti pada error pertama karena tersusun sebagai Bind bersarang.
Referensi#
- Computation expressions — Microsoft Learn — kontrak lengkap member builder, termasuk Zero, Combine, Delay, For, dan While.
- Language reference F# — Microsoft Learn — rujukan sintaks workflow, perulangan, dan tipe Result.