Membahas ulang ekspresi bawaan

async, seq, dan result dipahami kembali sebagai builder dengan mekanisme yang sama.

Daftar isi

Topik ini membaca kembali tiga workflow yang sudah digunakan sepanjang jalur, yaitu seq { }, async { }, dan result { }, dari sudut pandang pola builder. Masing-masing berdiri di atas kelas builder dengan member yang mekanismenya sama, sehingga perbedaan di antara ketiganya terletak pada isi member, bukan pada polanya. Pembahasan mencakup builder sekuens dengan yield, builder async dengan let! beruntun, serta builder Result yang didefinisikan sendiri dan dibandingkan dengan pipeline Result.bind pada masalah yang sama.

Seq sebagai builder#

Workflow seq { } membentuk nilai seq<'T>, yaitu sekuens yang elemennya baru disusun ketika diambil. Di baliknya berdiri kelas builder dengan member Yield untuk yield, YieldFrom untuk yield!, dan For untuk perulangan for. Member Yield menerima satu elemen dan meneruskannya ke sekuens, sedangkan YieldFrom menggabungkan seluruh sekuens lain ke dalam sekuens yang sedang disusun. Berkas berikut menyusun daftar nama piket dengan ketiga bentuk tersebut.

fsharp seq-builder.fsx
// seq { } sebagai builder dengan yield, for, dan yield!
let nomorPiket =
    seq {
        yield "Ali"
        yield "Budi"
        for nama in ["Citra"; "Dewi"] do
            yield nama
        yield! ["Eka"; "Fajar"]
    }

printfn "sekuens sudah dibentuk, belum diambil"

printfn "%A" (List.ofSeq nomorPiket)
text Keluaran
sekuens sudah dibentuk, belum diambil
["Ali"; "Budi"; "Citra"; "Dewi"; "Eka"; "Fajar"]

Pesan sekuens sudah dibentuk, belum diambil tercetak lebih dulu, dan baris itu menegaskan sifat lazy builder sekuens. Badan workflow tidak berjalan pada saat workflow dibentuk, karena yang dibentuk hanyalah deskripsi cara menyusun elemen. Elemen baru benar-benar disusun ketika List.ofSeq mengambil seluruh isinya, dan urutan keluarannya mengikuti urutan yield di dalam badan.

Sifat itu juga berarti satu nilai sekuens dapat diambil lebih dari satu kali, dan setiap pengambilan menyusun ulang elemennya dari awal. Badan workflow dieksekusi ulang pada setiap pengambilan, sehingga efek samping di dalamnya, misalnya baris cetak, ikut terulang. Perilaku ini berbeda dari array yang isinya sekali disusun dan dapat dibaca ulang tanpa biaya penyusunan kembali, sehingga pilihan di antara keduanya menyangkut pola pengaksesan datanya.

Async sebagai builder#

Workflow async { } berdiri di atas kelas AsyncBuilder yang member Bind-nya menunggu workflow lain selesai sebelum melanjutkan. Sifat ini membuat rangkaian let! menjadi langkah berurutan, dan urutan cetak pada contoh berikut pasti karena setiap langkah baru dimulai setelah langkah sebelumnya selesai. Selain let!, workflow async lazim menggunakan do! untuk langkah yang tidak menghasilkan nilai, dan return untuk menutup workflow.

fsharp async-builder.fsx
// async { } dengan let! beruntun: setiap langkah menunggu langkah sebelumnya
let muatSatu (nama: string) : Async<string> =
    async {
        printfn "memuat %s" nama
        do! Async.Sleep 30
        return sprintf "%s (ok)" nama
    }

let hasil =
    async {
        let! a = muatSatu "catatan-1"
        let! b = muatSatu "catatan-2"
        return [ a; b ]
    }
    |> Async.RunSynchronously

printfn "%A" hasil
text Keluaran
memuat catatan-1
memuat catatan-2
["catatan-1 (ok)"; "catatan-2 (ok)"]

Keluaran memperlihatkan pesan memuat sesuai urutan penulisan let!, karena AsyncBuilder.Bind baru menjalankan fungsi lanjutannya setelah workflow pertama menghasilkan nilai. Tipe Async<string> pada anotasi muatSatu menunjukkan bahwa fungsi tersebut mengembalikan deskripsi pekerjaan, bukan string biasa, dan hanya Bind di dalam workflow yang mampu membukanya. Perbandingan dengan builder sekuens pada bagian sebelumnya menunjukkan kontras isi member yang khas, yaitu Bind async memindahkan pekerjaan ke thread pool, sedangkan For sekuens sekadar menyusun elemen.

Member AsyncBuilder juga tidak berhenti pada Bind dan Return, karena kelas itu menyediakan Combine, Delay, For, While, dan TryFinally, sehingga badan workflow async dapat memuat percabangan, perulangan, dan penanganan exception tanpa keluar dari bentuk workflow. Kerangka member semacam itu dapat dibangun sendiri dengan kelas sederhana, dan definisinya ditelusuri satu per satu pada topik workflow khusus.

Result sebagai builder#

FSharp.Core belum menyediakan builder untuk Result, sehingga kelasnya didefinisikan sendiri dengan tiga member, yaitu Bind yang meneruskan Error beserta alasannya, Return yang membungkus nilai menjadi Ok, dan ReturnFrom untuk return!. Dengan builder ini, pipeline yang biasanya disusun lewat Result.bind dapat ditulis sebagai urutan let!. Berkas berikut membandingkan kedua bentuk pada masalah yang sama, yaitu mengurai teks nilai ujian lalu menghitung bobotnya.

fsharp result-builder.fsx
// Builder Result sendiri dibandingkan dengan pipeline Result.bind
type ResultBuilder() =
    member _.Bind(m: Result<'a, 'e>, langkah: 'a -> Result<'b, 'e>) : Result<'b, 'e> =
        match m with
        | Ok v -> langkah v
        | Error e -> Error e
    member _.Return(nilai: 'a) : Result<'a, 'e> = Ok nilai
    member _.ReturnFrom(m: Result<'a, 'e>) : Result<'a, 'e> = m

let result = ResultBuilder()

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

let bobotDari (n: int) : Result<float, string> =
    if n >= 0 && n <= 100 then Ok (float n / 100.0)
    else Error (sprintf "%d di luar rentang 0..100" n)

// pipeline dengan Result.bind
let bobotPipeline (teks: string) : Result<float, string> =
    uraiNilai teks |> Result.bind bobotDari

// workflow result dengan let! dan return!
let bobotWorkflow (teks: string) : Result<float, string> =
    result {
        let! n = uraiNilai teks
        return! bobotDari n
    }

for teks in ["85"; "abc"; "150"] do
    printfn "%A" (bobotPipeline teks, bobotWorkflow teks)
text Keluaran
(Ok 0.85, Ok 0.85)
(Error "abc bukan bilangan", Error "abc bukan bilangan")
(Error "150 di luar rentang 0..100", Error "150 di luar rentang 0..100")

Setiap baris keluaran berupa pasangan yang kedua elemennya identik, sehingga pipeline dan workflow terbukti menghasilkan nilai yang sama pada ketiga masukan. Teks yang sah mengalir dua langkah menjadi Ok 0.85, teks yang tidak dapat diurai berhenti pada langkah pertama, dan angka 150 berhenti pada langkah kedua dengan alasan kegagalannya sendiri. Perbedaannya murni bentuk penyajian, yaitu pipeline menonjolkan aliran data melalui operator, sedangkan workflow menonjolkan urutan langkah seperti kode imperatif.

Workflow menjadi pilihan yang tepat ketika langkahnya banyak dan terus bertambah. Menambah satu langkah berarti menambah satu baris let!, sedangkan pada pipeline yang sama berarti menambah satu operator dengan rangkaian yang semakin panjang. Kedua bentuk menghasilkan tipe yang sama, sehingga pilihannya murni soal kejelasan pembacaan, dan keduanya dapat digunakan bergantian dalam satu berkas tanpa konversi apa pun.

Tabel berikut merangkum ketiga builder beserta kata kunci dan member utamanya. Ketiganya mengikuti satu pola penerjemahan yang sama, sehingga member baru pada satu builder dapat ditelusuri dengan cara yang sama pada builder lainnya.

WorkflowKata kunci khasMember utama di baliknya
seq { }yield, yield!, forYield, YieldFrom, For
async { }let!, do!, returnBind, Return
result { }let!, return!Bind, Return, ReturnFrom

Ringkasan#

  • seq { }, async { }, dan result { } berdiri di atas kelas builder dengan pola penerjemahan yang sama.
  • Sifat lazy sekuens, urutan langkah workflow async, dan penerusan kegagalan Result semuanya lahir dari isi member masing-masing kelas builder.
  • Builder Result tidak disediakan FSharp.Core dan didefinisikan sendiri dengan Bind, Return, dan ReturnFrom.
  • Pipeline Result.bind dan workflow result { } menghasilkan nilai yang identik untuk masukan yang sama.

Referensi#