Rancang tipe terlebih dahulu

Tanda tangan dan ADT sebagai kerangka desain sebelum implementasi ditulis.

Daftar isi

Proyek pengolah rekap nilai ujian dirancang mulai dari tepinya, yaitu tipe data dan tanda tangan fungsinya, sebelum satu baris implementasi ditulis. Topik ini menetapkan ADT untuk data siswa dan laporan rekap, mengunci tanda tangan tiga fungsi utama, lalu menutup dengan kerangka program yang sudah dapat dijalankan meskipun badan fungsinya masih berupa pengganti sementara. Hasil akhirnya adalah kerangka yang lolos kompilasi dan mencetak bentuk datanya, sehingga kesalahan desain terungkap pada tahap desain, bukan di tengah implementasi.

Tanda tangan fungsi sebagai kontrak#

Tanda tangan fungsi (function signature) menyatakan masukan dan keluaran sebuah fungsi tanpa menyentuh cara kerjanya, sehingga bentuknya cocok menjadi kontrak pertama sebuah desain. Kontrak ini ditulis lebih dahulu karena setiap keputusan implementasi, misalnya urutan pengolahan atau pemilihan metode koleksi, harus tunduk pada tipe masukan dan keluaran yang sudah disepakati. Apabila tanda tangan ditulis terakhir, implementasi yang berjalan sering kali menentukan bentuk tipe, dan keputusan desain pun berpindah ke tempat yang paling sulit diperiksa.

Tanda tangan tiga fungsi utama proyek ini dinyatakan sebagai berikut.

scala Contoh: tanda tangan fungsi pengolah rekap
def validasi(siswa: List[Siswa]): Either[String, List[Siswa]]
def rataRata(nilai: List[Int]): Option[Double]
def rekap(siswa: List[Siswa]): Either[String, Rekap]

Fungsi validasi menerima daftar siswa dan mengembalikan daftar yang sama apabila semua nilainya sah, sedangkan kegagalannya, misalnya ada nilai di luar jangkauan 0 sampai 100, dibawa oleh Left. Fungsi rataRata menerima kumpulan angka dan mengembalikan Option[Double], karena daftar kosong tidak memiliki rata-rata dan kondisi itu bukan error melainkan ketiadaan hasil. Fungsi rekap menyusun Rekap dari seluruh daftar siswa, dan kegagalannya dinyatakan dengan Either[String, Rekap]. Ketiganya membentuk rantai kecil: validasi membersihkan masukan, rataRata menghitung satu kumpulan angka, dan rekap menyusun hasilnya menjadi laporan.

Perhatikan bahwa tanda tangan di atas ditulis tanpa badan fungsi. Bentuk ini sengaja digunakan sebagai rencana, karena tipe Siswa dan Rekap pada tandanya menuntut ADT yang sama sekali belum ada, dan compiler Scala akan menolak program yang menyebut tipe tersebut sebelum tipe itu didefinisikan. Dengan kata lain, tanda tangan langsung membuat daftar pekerjaan yang harus diselesaikan.

ADT untuk siswa, nilai, dan rekap#

ADT mengubah kebutuhan lisan menjadi tipe yang diperiksa compiler, sehingga keputusan tentang data dituangkan sebelum logika ditulis. Kebutuhan data proyek ini terbaca dari cakupan minimumnya: setiap siswa memiliki nama dan sekumpulan nilai untuk tiga jenis evaluasi yang tetap, sedangkan rekap berisi jumlah siswa, rata-rata per jenis evaluasi, dan rata-rata keseluruhan. Tiga tipe berikut memenuhi kebutuhan itu.

scala Contoh: ADT data siswa dan rekap
enum Evaluasi:
  case Kuis, TengahSemester, AkhirSemester

case class Siswa(nama: String, nilai: Map[Evaluasi, Int])

case class Rekap(
  jumlahSiswa: Int,
  rataRataEvaluasi: Map[Evaluasi, Double],
  rataRataKeseluruhan: Double
)

Tipe Evaluasi berupa enum dengan tiga varian, dan sifat enum yang tertutup menjadi pilihan penting di sini. Himpunan jenis evaluasi memang tidak akan berubah selama proyek berjalan, dan setiap pencocokan pola terhadap Evaluasi wajib menangani ketiga variannya karena compiler memeriksa kelengkapan itu. Penggunaan Map[Evaluasi, Int] pada Siswa mengikuti keputusan yang sama: kunci peta bertipe enum, sehingga kesalahan pengetikan nama evaluasi yang biasanya muncul pada String menjadi mustahil karena compiler memeriksa kunci tersebut.

Tipe Siswa dan Rekap berupa case class yang membawa data tanpa perilaku. Bidang rataRataEvaluasi bertipe Map[Evaluasi, Double] karena rata-rata per jenis evaluasi menghasilkan bilangan pecahan, sedangkan jumlah siswa cukup bertipe Int. Satu kebutuhan tetap berada di luar kemampuan tipe: jangkauan nilai 0 sampai 100 tidak dapat dinyatakan pada tipe Int, sehingga aturan itu menjadi pekerjaan validasi dan sengaja diperlihatkan pada tanda tangannya.

Keputusan tipe yang menyertai ADT#

Setiap tipe di atas membawa keputusan desain yang perlu dinyatakan secara eksplisit agar konsisten dengan bahan jalur ini. Keputusan pertama menyangkut ketidakberubahan. Ketiga tipe dinyatakan sebagai nilai yang tidak diubah, dan setiap fungsi menerima struktur data lama lalu mengembalikan struktur data baru sebagai hasilnya. Gaya ini sesuai dengan fungsi murni yang menjadi kebiasaan jalur ini, dan struktur yang tidak berubah juga aman dibaca lintas fungsi tanpa perlu memikirkan urutan perubahan.

Keputusan kedua menempatkan kegagalan pada tipe keluaran, bukan pada eksepsi. Either[String, Rekap] dipilih untuk rekap dan validasi karena kegagalan prosesnya bermakna dan perlu diberi pesan, misalnya daftar siswa yang kosong atau nilai yang tidak sah. Satu String sebagai tipe kegagalan sudah cukup untuk proyek seukuran ini, meskipun proyek yang lebih besar biasanya menggunakan enum kesalahan agar setiap jenis kegagalan bertipe sendiri. Option[Double] dipilih untuk rataRata dengan pertimbangan yang berbeda: daftar kosong tidak melewati aturan apa pun, tetapi rata-ratanya memang tidak ada, dan None menyatakan ketiadaan itu tanpa menganggapnya error.

Keputusan ketiga menggunakan Map alih-alih tiga bidang terpisah seperti nilaiKuis dan nilaiAkhir. Dengan peta berkunci Evaluasi, penambahan satu varian pada enum otomatis memperluas data yang dapat disimpan, dan fungsi yang menelusuri Evaluasi.values tetap bekerja tanpa disunting. Struktur data pun tumbuh mengikuti tipe, bukan sebaliknya.

Kerangka program yang sudah berjalan#

Tipe dan tanda tangan yang sudah dikunci dapat langsung dinyatakan sebagai program yang lolos kompilasi, dengan badan fungsi berupa stub, yaitu pengganti sementara yang menyerahkan implementasi penuh pada tahap pembangunan. Stub pada kerangka ini tidak berupa ??? yang memicu eksepsi, melainkan nilai pengganti yang sah menurut tipe keluarannya, sehingga program benar-benar berjalan dan bentuk datanya terlihat.

scala rekap-nilai.scala
// Kerangka pengolah rekap nilai ujian: tipe sudah final, implementasi masih stub
enum Evaluasi:
  case Kuis, TengahSemester, AkhirSemester

case class Siswa(nama: String, nilai: Map[Evaluasi, Int])

case class Rekap(
  jumlahSiswa: Int,
  rataRataEvaluasi: Map[Evaluasi, Double],
  rataRataKeseluruhan: Double
)

// Tanda tangan sudah dikunci; badan stub masih berupa placeholder
def validasi(siswa: List[Siswa]): Either[String, List[Siswa]] =
  Right(siswa)

def rataRata(nilai: List[Int]): Option[Double] =
  None

def rekap(siswa: List[Siswa]): Either[String, Rekap] =
  for
    sah <- validasi(siswa)
  yield Rekap(sah.length, Map.empty, 0.0)

@main def rekapNilai(): Unit =
  val kelas = List(
    Siswa("Ali", Map(Evaluasi.Kuis -> 80, Evaluasi.TengahSemester -> 75, Evaluasi.AkhirSemester -> 90)),
    Siswa("Budi", Map(Evaluasi.Kuis -> 70, Evaluasi.TengahSemester -> 85, Evaluasi.AkhirSemester -> 80)),
    Siswa("Citra", Map(Evaluasi.Kuis -> 90, Evaluasi.TengahSemester -> 95, Evaluasi.AkhirSemester -> 85))
  )

  println(s"evaluasi: ${Evaluasi.values.mkString(", ")}")
  println(s"jumlah siswa: ${kelas.length}")
  for s <- kelas do
    val baris = Evaluasi.values.map(e => s"${e}=${s.nilai.getOrElse(e, 0)}").mkString(", ")
    println(s"- ${s.nama}: $baris")

  println(rekap(kelas))
bash
# menjalankan kerangka program rekap-nilai.scala
scala-cli run rekap-nilai.scala
text Keluaran
evaluasi: Kuis, TengahSemester, AkhirSemester
jumlah siswa: 3
- Ali: Kuis=80, TengahSemester=75, AkhirSemester=90
- Budi: Kuis=70, TengahSemester=85, AkhirSemester=80
- Citra: Kuis=90, TengahSemester=95, AkhirSemester=85
Right(Rekap(3,Map(),0.0))

Keluaran di atas adalah bukti bahwa tipe mendahului logika. Baris-baris pertama menunjukkan data kelas yang tersusun dari ADT, yaitu tiga jenis evaluasi sesuai deklarasi enum dan nilai setiap siswa yang dibaca dari peta tanpa menyentuh urutan internal Map. Baris terakhir menunjukkan bentuk Either, yaitu Right yang membungkus Rekap sementara dengan jumlahSiswa yang sudah nyata, sedangkan dua bidang rata-ratanya masih 0 karena stub belum menghitung apa pun. Stub validasi mengembalikan Right tanpa penyaringan, stub rataRata mengembalikan None, dan keduanya sah menurut tipe keluarannya.

Pada titik ini, desain sudah memperoleh verifikasi pertamanya, yaitu kompilasi. Compiler memastikan tanda tangan dan ADT saling cocok, stub menghormati tipe keluarannya, dan bagian main berhasil mengomposisi semuanya. Implementasi penuh setiap stub kemudian menggantikan nilai pengganti satu per satu, dan setiap penggantian dapat diverifikasi dengan membandingkan keluaran program dengan hasil perhitungan manual.

Ringkasan#

  • Tanda tangan validasi, rataRata, dan rekap dinyatakan sebelum badan implementasi, sehingga bentuk masukan dan keluaran menjadi kesepakatan yang tetap.
  • ADT Evaluasi, Siswa, dan Rekap mengunci bentuk data: jenis evaluasi tertutup pada enum, data siswa dan rekap berupa case class immutable dengan peta berkunci enum.
  • Kegagalan dinyatakan pada tipe, yaitu Either[String, Rekap] untuk kegagalan proses rekap dan Option[Double] untuk rata-rata daftar kosong.
  • Kerangka program dengan stub berjalan dan mencetak bentuk data secara deterministik, sehingga desain sudah diverifikasi compiler sebelum logika ditulis.

Referensi#