Topik ini mengerjakan layanan jadwal piket dari lingkup yang sudah ditetapkan sampai menjadi berkas yang dapat dijalankan. Urutan kerjanya membalik kebiasaan yang lazim, yaitu tipe dan signature disusun lebih dahulu, baru badan fungsi diisi satu per satu beserta ujinya. Hasil akhirnya berupa satu berkas layanan yang memuat definisi tipe, empat fungsi domain, dan pengujian yang keluarannya ditampilkan apa adanya.
Urutan pengerjaan#
Urutan tipe lebih dahulu dikerjakan dalam empat langkah berurutan.
- Menuliskan seluruh tipe domain sampai berkas dapat dikompilasi tanpa satu pun fungsi.
- Merancang signature setiap fungsi sebagai kontrak, termasuk kanal penolakannya.
- Mengisi badan satu fungsi, lalu langsung mengujinya sebelum berpindah ke fungsi berikutnya.
- Menjalankan skenario uji yang mencakup penerimaan dan penolakan.
Urutan ini memindahkan sebagian besar kesalahan dari waktu uji ke waktu kompilasi. Bentuk data yang salah tertangkap compiler saat definisi tipe sedang ditulis, bukan saat program dijalankan dengan masukan tertentu. Kesalahan yang tersisa pada waktu uji hanya kesalahan perilaku, bukan lagi kesalahan bentuk data. Signature yang sudah tertulis menjadi acuan tetap ketika badan diisi, sehingga perubahan kontrak tidak terjadi secara diam-diam.
Kebiasaan yang dibalik di sini adalah menulis badan terlebih dahulu. Apabila badan ditulis lebih awal, keputusan tipe sering berubah di tengah pengerjaan, dan pemeriksaan kesalahan bergeser ke pengujian manual yang mudah terlewat. Dengan urutan terbalik, badan fungsi hanya mengisi kontrak yang bentuknya sudah final.
Tipe domain jadwal piket#
Definisi tipe ditulis lebih dahulu sampai dapat dikompilasi tanpa perilaku apa pun. Tipe IdPeserta dan NamaPeserta dibungkus single-case DU, yaitu discriminated union dengan satu kasus, sehingga nomor urut dan nama tidak dapat saling dipertukarkan ketika urutan argumen tertukar. Status tiap hari dinyatakan sebagai discriminated union Kosong dan Terisi yang kasus terisinya membawa IdPeserta, sedangkan JadwalPiket berupa record yang menggabungkan daftar anggota dan Map hari ke status.
// tipe domain layanan jadwal piket, ditulis sebelum badan fungsi mana pun
type IdPeserta = IdPeserta of int
type NamaPeserta = NamaPeserta of string
type Peserta = { Id: IdPeserta; Nama: NamaPeserta }
type HariPiket = Senin | Selasa | Rabu | Kamis | Jumat
type StatusHari = Kosong | Terisi of IdPeserta
type JadwalPiket =
{ Anggota: Peserta list
Penugasan: Map<HariPiket, StatusHari> }
// titik awal satu pekan: lima hari kosong, belum ada anggota terdaftar
let jadwalKosong : JadwalPiket =
{ Anggota = []
Penugasan =
[ Senin; Selasa; Rabu; Kamis; Jumat ]
|> List.map (fun hari -> (hari, Kosong))
|> Map.ofList }
printfn "%A" jadwalKosong{ Anggota = []
Penugasan =
map
[(Senin, Kosong); (Selasa, Kosong); (Rabu, Kosong); (Kamis, Kosong);
(Jumat, Kosong)] }Keluaran memperlihatkan keadaan awal satu pekan sebelum satu pun fungsi domain ditulis. Map Penugasan berisi lima kunci yang seluruhnya Kosong, sedangkan daftar Anggota masih kosong. Bentuk seperti ini dapat dicetak dan diperiksa tanpa perilaku, dan bentuk inilah yang membatasi keputusan saat badan fungsi diisi.
Signature sebelum badan#
Signature adalah baris tipe yang menyatakan masukan dan keluaran sebuah fungsi tanpa menampakkan badannya. Empat signature berikut dirancang sebagai kontrak sebelum satu pun badan ditulis, dan bentuknya masih berupa rancangan yang berdiri sendiri. Bentuk lengkapnya dituliskan sebagai komentar agar tetap terbaca sebagai rancangan, bukan sebagai kode yang mengaku selesai.
// tambahPeserta : NamaPeserta -> JadwalPiket -> Result<JadwalPiket, string>
// tugaskan : IdPeserta -> HariPiket -> JadwalPiket -> Result<JadwalPiket, string>
// tukar : HariPiket -> HariPiket -> JadwalPiket -> Result<JadwalPiket, string>
// cekBentrok : JadwalPiket -> Peserta listTiga fungsi pertama mengembalikan Result karena dapat menolak, dan kanal Error-nya berupa string yang membawa alasan penolakan. cekBentrok tidak mengembalikan Result karena pemeriksaan tidak dapat gagal, dan cekBentrok hanya melaporkan daftar peserta yang tercatat pada lebih dari satu hari. Jadwal selalu diletakkan sebagai parameter terakhir agar setiap fungsi dapat dirangkai dengan operator pipeline.
Signature yang sudah tertulis juga menetapkan apa yang tidak boleh berubah. Apabila pengisian badan menuntut perubahan signature, perubahan itu adalah perubahan rancangan yang dinyatakan ulang, bukan penyimpangan yang diselipkan di tengah kode. Kontrak yang stabil membuat badan fungsi dapat diisi dan diuji satu per satu tanpa menimbulkan kejutan bagi fungsi lain.
Mengisi badan satu per satu#
Badan pertama yang diisi adalah tambahPeserta. Aturan domainnya berupa dua pemeriksaan, yaitu nama tidak boleh kembar dan tim maksimal lima anggota. Kedua pemeriksaan mengembalikan Error dengan alasan yang terbaca, sedangkan jalur sukses membuat nomor urut dari panjang daftar lalu menambahkan peserta baru ke daftar Anggota. Nomor urut dibuat dari panjang daftar karena layanan ini tidak menyediakan penghapusan peserta.
// definisi tipe dan jadwalKosong dari bagian sebelumnya disertakan ulang
// agar skrip uji ini dapat dijalankan sendiri
type IdPeserta = IdPeserta of int
type NamaPeserta = NamaPeserta of string
type Peserta = { Id: IdPeserta; Nama: NamaPeserta }
type HariPiket = Senin | Selasa | Rabu | Kamis | Jumat
type StatusHari = Kosong | Terisi of IdPeserta
type JadwalPiket =
{ Anggota: Peserta list
Penugasan: Map<HariPiket, StatusHari> }
let jadwalKosong : JadwalPiket =
{ Anggota = []
Penugasan =
[ Senin; Selasa; Rabu; Kamis; Jumat ]
|> List.map (fun hari -> (hari, Kosong))
|> Map.ofList }
// badan pertama: mendaftarkan peserta baru dengan dua aturan
let tambahPeserta (nama: NamaPeserta) (jadwal: JadwalPiket) : Result<JadwalPiket, string> =
let namaSudahAda =
jadwal.Anggota |> List.exists (fun p -> p.Nama = nama)
if namaSudahAda then Error "nama sudah terdaftar"
elif List.length jadwal.Anggota >= 5 then Error "tim penuh"
else
let idBaru = IdPeserta (List.length jadwal.Anggota + 1)
Ok { jadwal with Anggota = jadwal.Anggota @ [ { Id = idBaru; Nama = nama } ] }
// uji penerimaan dan penolakan nama kembar
let langkahPertama = tambahPeserta (NamaPeserta "Ali") jadwalKosong
match langkahPertama with
| Ok j1 ->
printfn "Ali diterima, anggota kini %d" (List.length j1.Anggota)
match tambahPeserta (NamaPeserta "Ali") j1 with
| Ok j2 -> printfn "Ali diterima lagi, anggota kini %d" (List.length j2.Anggota)
| Error e -> printfn "ditolak: %s" e
| Error e -> printfn "ditolak: %s" e
// uji batas anggota: lima diterima, keenam ditolak
let penuh =
jadwalKosong
|> tambahPeserta (NamaPeserta "Ali")
|> Result.bind (tambahPeserta (NamaPeserta "Budi"))
|> Result.bind (tambahPeserta (NamaPeserta "Citra"))
|> Result.bind (tambahPeserta (NamaPeserta "Dewi"))
|> Result.bind (tambahPeserta (NamaPeserta "Eka"))
match penuh with
| Ok j ->
printfn "tim berisi lima anggota"
match tambahPeserta (NamaPeserta "Fajar") j with
| Ok j6 -> printfn "anggota keenam diterima, anggota kini %d" (List.length j6.Anggota)
| Error e -> printfn "ditolak: %s" e
| Error e -> printfn "ditolak: %s" eAli diterima, anggota kini 1
ditolak: nama sudah terdaftar
tim berisi lima anggota
ditolak: tim penuhUji di atas membaca hasil setiap pemanggilan dengan match, dan keempat baris keluarannya memperlihatkan satu penerimaan, satu penolakan nama kembar, satu keadaan penuh, dan satu penolakan anggota keenam. Rangkaian lima pendaftaran dirangkai dengan Result.bind, yang langkah pertamanya diterapkan langsung pada jadwalKosong karena masukannya belum berupa Result. Setiap cabang Error menggunakan teks yang sama dengan daftar aturan pada lingkup minimum, sehingga keluaran penolakan langsung dapat dibandingkan dengan rencananya. Pola isi lalu uji ini diulang untuk tiga fungsi berikutnya, yaitu badan diisi, uji kecil ditambahkan, dan berkas dijalankan.
Layanan lengkap dan pengujian#
Tiga badan berikutnya mengikuti pola yang sama. tugaskan menguji tiga aturan secara berurutan, yaitu peserta harus terdaftar, hari harus masih Kosong, dan peserta tidak boleh sudah tercatat pada hari lain. tukar menolak dua hari yang sama serta penukaran pada hari yang masih Kosong, sedangkan cekBentrok menghitung peserta yang tercatat pada lebih dari satu hari. Berkas lengkap berikut memuat seluruh tipe, empat badan fungsi, dua pembantu tampilan, dan skenario ujinya.
// layanan jadwal piket: tipe, jadwal awal, badan fungsi, lalu pengujian
type IdPeserta = IdPeserta of int
type NamaPeserta = NamaPeserta of string
type Peserta = { Id: IdPeserta; Nama: NamaPeserta }
type HariPiket = Senin | Selasa | Rabu | Kamis | Jumat
type StatusHari = Kosong | Terisi of IdPeserta
type JadwalPiket =
{ Anggota: Peserta list
Penugasan: Map<HariPiket, StatusHari> }
let jadwalKosong : JadwalPiket =
{ Anggota = []
Penugasan =
[ Senin; Selasa; Rabu; Kamis; Jumat ]
|> List.map (fun hari -> (hari, Kosong))
|> Map.ofList }
// mendaftarkan peserta baru; nama harus unik dan tim maksimal lima orang
let tambahPeserta (nama: NamaPeserta) (jadwal: JadwalPiket) : Result<JadwalPiket, string> =
let namaSudahAda =
jadwal.Anggota |> List.exists (fun p -> p.Nama = nama)
if namaSudahAda then Error "nama sudah terdaftar"
elif List.length jadwal.Anggota >= 5 then Error "tim penuh"
else
let idBaru = IdPeserta (List.length jadwal.Anggota + 1)
Ok { jadwal with Anggota = jadwal.Anggota @ [ { Id = idBaru; Nama = nama } ] }
// menugaskan peserta pada satu hari; tiga aturan diuji berurutan
let tugaskan (pid: IdPeserta) (hari: HariPiket) (jadwal: JadwalPiket) : Result<JadwalPiket, string> =
let terdaftar = jadwal.Anggota |> List.exists (fun p -> p.Id = pid)
if not terdaftar then Error "peserta tidak terdaftar"
elif Map.find hari jadwal.Penugasan <> Kosong then Error "hari sudah terisi"
elif jadwal.Penugasan |> Map.exists (fun _ status -> status = Terisi pid) then Error "dobel tugas"
else Ok { jadwal with Penugasan = Map.add hari (Terisi pid) jadwal.Penugasan }
// menukar isi dua hari; kedua hari harus sudah terisi
let tukar (hariA: HariPiket) (hariB: HariPiket) (jadwal: JadwalPiket) : Result<JadwalPiket, string> =
if hariA = hariB then Error "dua hari yang sama"
else
match Map.find hariA jadwal.Penugasan, Map.find hariB jadwal.Penugasan with
| Terisi _, Terisi _ ->
let isiA = Map.find hariA jadwal.Penugasan
let isiB = Map.find hariB jadwal.Penugasan
Ok { jadwal with
Penugasan =
jadwal.Penugasan
|> Map.add hariA isiB
|> Map.add hariB isiA }
| _ -> Error "salah satu hari belum terisi"
// mengembalikan peserta yang tercatat pada lebih dari satu hari
let cekBentrok (jadwal: JadwalPiket) : Peserta list =
let terisi =
jadwal.Penugasan
|> Map.toList
|> List.choose (fun (_, status) ->
match status with
| Terisi pid -> Some pid
| Kosong -> None)
let dobel =
terisi
|> List.countBy (fun pid -> pid)
|> List.filter (fun (_, jumlah) -> jumlah > 1)
|> List.map fst
jadwal.Anggota
|> List.filter (fun p -> List.contains p.Id dobel)
// pembantu tampilan: teks nama hari dan ringkasan satu hari
let namaHari hari =
match hari with
| Senin -> "Senin"
| Selasa -> "Selasa"
| Rabu -> "Rabu"
| Kamis -> "Kamis"
| Jumat -> "Jumat"
let ringkasHari (hari: HariPiket) (jadwal: JadwalPiket) =
let namaDari pid =
let peserta = jadwal.Anggota |> List.find (fun p -> p.Id = pid)
match peserta.Nama with
| NamaPeserta n -> n
match Map.find hari jadwal.Penugasan with
| Kosong -> sprintf "%s: kosong" (namaHari hari)
| Terisi pid -> sprintf "%s: %s" (namaHari hari) (namaDari pid)
// pembantu pengujian: mencetak keberhasilan atau alasan penolakan
let laporkan (pesan: string) (hasil: Result<JadwalPiket, string>) : unit =
match hasil with
| Ok _ -> printfn "%s: diterima" pesan
| Error e -> printfn "%s: ditolak, %s" pesan e
// skenario: satu pekan untuk tim tiga orang
let pekan =
jadwalKosong
|> tambahPeserta (NamaPeserta "Ali")
|> Result.bind (tambahPeserta (NamaPeserta "Budi"))
|> Result.bind (tambahPeserta (NamaPeserta "Citra"))
let terisi =
match pekan with
| Error e -> Error e
| Ok j ->
j
|> tugaskan (IdPeserta 1) Senin
|> Result.bind (tugaskan (IdPeserta 2) Rabu)
|> Result.bind (tugaskan (IdPeserta 3) Jumat)
match terisi with
| Error e -> printfn "skenario gagal: %s" e
| Ok j ->
printfn "laporan pekan:"
for hari in [ Senin; Selasa; Rabu; Kamis; Jumat ] do
printfn "%s" (ringkasHari hari j)
// penolakan yang diharapkan pada jadwal yang sudah berisi
j |> tugaskan (IdPeserta 2) Senin |> laporkan "tugaskan Budi pada Senin"
j |> tugaskan (IdPeserta 3) Selasa |> laporkan "tugaskan Citra pada Selasa"
j |> tugaskan (IdPeserta 9) Selasa |> laporkan "tugaskan id 9 pada Selasa"
j |> tukar Senin Selasa |> laporkan "tukar Senin dengan Selasa"
// penukaran sah antara dua hari yang sudah terisi
match tukar Senin Rabu j with
| Ok j2 ->
printfn "setelah tukar Senin dan Rabu:"
printfn "%s" (ringkasHari Senin j2)
printfn "%s" (ringkasHari Rabu j2)
| Error e -> printfn "tukar gagal: %s" e
// jadwal yang dibangun langsung tanpa lewat tugaskan, untuk menguji cekBentrok
let ilegal : JadwalPiket =
{ Anggota = [ { Id = IdPeserta 1; Nama = NamaPeserta "Ali" } ]
Penugasan =
[ (Senin, Terisi (IdPeserta 1))
(Selasa, Terisi (IdPeserta 1))
(Rabu, Kosong)
(Kamis, Kosong)
(Jumat, Kosong) ]
|> Map.ofList }
match cekBentrok ilegal with
| [] -> printfn "tidak ada peserta bentrok"
| daftar -> printfn "bentrok: %d peserta" (List.length daftar)laporan pekan:
Senin: Ali
Selasa: kosong
Rabu: Budi
Kamis: kosong
Jumat: Citra
tugaskan Budi pada Senin: ditolak, hari sudah terisi
tugaskan Citra pada Selasa: ditolak, dobel tugas
tugaskan id 9 pada Selasa: ditolak, peserta tidak terdaftar
tukar Senin dengan Selasa: ditolak, salah satu hari belum terisi
setelah tukar Senin dan Rabu:
Senin: Budi
Rabu: Ali
bentrok: 1 pesertaBerkas yang sama dijalankan dari terminal untuk memastikan salinan yang ada di folder benar-benar berjalan.
# jalankan dari folder tempat berkas layanan berada
dotnet fsi jadwal-piket.fsxlaporan pekan:
Senin: Ali
Selasa: kosong
Rabu: Budi
Kamis: kosong
Jumat: Citra
tugaskan Budi pada Senin: ditolak, hari sudah terisi
tugaskan Citra pada Selasa: ditolak, dobel tugas
tugaskan id 9 pada Selasa: ditolak, peserta tidak terdaftar
tukar Senin dengan Selasa: ditolak, salah satu hari belum terisi
setelah tukar Senin dan Rabu:
Senin: Budi
Rabu: Ali
bentrok: 1 pesertaEmpat baris penolakan pada keluaran menunjukkan bahwa masing-masing aturan bekerja, yaitu hari sudah terisi, dobel tugas, peserta tidak terdaftar, dan penukaran pada hari yang masih kosong. Penukaran Senin dan Rabu yang sah menukar Ali dan Budi tanpa mengubah jumlah penugasan. Baris terakhir berasal dari jadwal yang dibangun langsung tanpa lewat tugaskan, dan cekBentrok tetap menemukan peserta yang tercatat dua kali.
Ringkasan#
- Tipe domain ditulis dan dikompilasi lebih dahulu, sehingga kesalahan bentuk data tertangkap compiler sebelum perilaku ada.
- Signature menjadi kontrak tertulis, dan tiga dari empat fungsi mengumumkan penolakan melalui kanal
Errorpada Result. - Badan diisi satu per satu, dan setiap pengisian langsung diuji dengan
matchsebelum melanjutkan. - Aturan domain yang semula tersebar menjadi daftar alasan penolakan yang eksplisit, sedangkan
cekBentrokmenjaga jadwal yang tidak dibuat melalui layanan.
Referensi#
- Results — Microsoft Learn — Dokumentasi resmi tipe Result yang menjadi kanal penolakan aturan domain.
- Discriminated Unions — Microsoft Learn — Dokumentasi resmi discriminated union, termasuk bentuk satu kasus yang membungkus tipe primitif.
- Records — Microsoft Learn
— Dokumentasi resmi record yang digunakan untuk
PesertadanJadwalPiket.