Tahap pembangunan proyek dimulai ketika setiap stub pada kerangka desain diganti dengan implementasi yang sesungguhnya. Topik ini menuntaskan fungsi validasi, rataRata, dan rekap beserta laporan teksnya, kemudian menetapkan titik pemeriksaan pada setiap tahapnya, yaitu menjalankan program utuh, menguji perilaku fungsi melalui REPL, memformat kode dengan scala-cli fmt, dan meninjau hasil kerja secara mandiri. Hasil akhirnya adalah program pengolah rekap nilai yang mencetak angka final dari data Ali, Budi, dan Citra, serta kebiasaan kerja yang membuat setiap perubahan kecil dapat diverifikasi segera setelah ditulis.
Implementasi penuh dari tanda tangan yang terkunci#
Tanda tangan yang dikunci pada tahap desain tidak berubah sama sekali, sehingga seluruh pekerjaan implementasi tinggal mengisi badan fungsinya. Fungsi validasi menyaring siswa yang memiliki setidaknya satu nilai di luar jangkauan 0 sampai 100, lalu mengembalikan seluruh daftar dalam Right apabila tidak ada yang bermasalah, atau Left yang membawa pesan berisi nama siswa yang keliru. Fungsi rataRata mengembalikan None untuk daftar kosong dan Some berisi hasil pembagian jumlah nilai dengan banyaknya data untuk kasus lainnya. Fungsi rekap mengalirkan hasil validasi melalui for-comprehension, menghitung rata-rata per jenis evaluasi dari Evaluasi.values, serta menjumlahkan seluruh nilai untuk rata-rata keseluruhan. Fungsi tambahan bernama laporan menyusun teks rekap dengan pencocokan pola terhadap Either, sehingga data yang gagal validasi menghasilkan pesan singkat, sedangkan data yang sah menghasilkan laporan lengkap.
Compiler menjadi titik pemeriksaan pertama pada tahap ini. Ketika laporan pertama kali disusun, program sempat gagal dikompilasi karena Evaluasi.values menghasilkan Array, sedangkan baris-baris laporan disusun sebagai List, dan pesan errornya menunjuk persis ekspresi yang keliru. Perbaikannya hanya berupa satu pemanggilan toList, dan program lolos kompilasi segera sesudahnya. Pola kecil ini merupakan siklus kerja yang sama dengan yang digunakan sepanjang pembangunan, yaitu tulis sedikit, jalankan, periksa, lalu perbaiki.
// Pengolah rekap nilai ujian: implementasi penuh dari tanda tangan yang sudah dikunci
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
)
// Menolak nilai di luar jangkauan 0 sampai 100
def validasi(siswa: List[Siswa]): Either[String, List[Siswa]] =
val bermasalah =
siswa.filter(s => s.nilai.values.exists(n => n < 0 || n > 100))
if bermasalah.isEmpty then Right(siswa)
else
Left(
s"nilai di luar jangkauan 0-100: ${bermasalah.map(_.nama).mkString(", ")}"
)
// Daftar kosong tidak memiliki rata-rata, sehingga hasilnya None
def rataRata(nilai: List[Int]): Option[Double] =
if nilai.isEmpty then None
else Some(nilai.sum.toDouble / nilai.length)
// Menyusun rekap setelah validasi lolos
def rekap(siswa: List[Siswa]): Either[String, Rekap] =
for sah <- validasi(siswa)
yield
val perEvaluasi = Evaluasi.values
.map(e => e -> rataRata(sah.map(_.nilai.getOrElse(e, 0))).getOrElse(0.0))
.toMap
val keseluruhan =
rataRata(sah.flatMap(_.nilai.values.toList)).getOrElse(0.0)
Rekap(sah.length, perEvaluasi, keseluruhan)
// Laporan teks yang siap dicetak
def laporan(siswa: List[Siswa]): String =
rekap(siswa) match
case Left(pesan) =>
s"rekap gagal: $pesan"
case Right(r) =>
val barisSiswa = siswa.map { s =>
val angka =
Evaluasi.values.map(e => s"${s.nilai.getOrElse(e, 0)}").mkString("/")
s"- ${s.nama}: $angka"
}
val barisRata = Evaluasi.values.toList.map(e =>
s"- $e: ${r.rataRataEvaluasi.getOrElse(e, 0.0)}"
)
val barisAkhir = f"- keseluruhan: ${r.rataRataKeseluruhan}%.2f"
val baris = "Rekap nilai ujian" :: s"jumlah siswa: ${r.jumlahSiswa}" ::
"nilai per siswa:" :: barisSiswa ::: "rata-rata per evaluasi:" :: barisRata ::: barisAkhir :: Nil
baris.mkString("\n")
@main def rekapUtuh(): Unit =
import Evaluasi.*
val kelas = List(
Siswa("Ali", Map(Kuis -> 80, TengahSemester -> 75, AkhirSemester -> 90)),
Siswa("Budi", Map(Kuis -> 70, TengahSemester -> 85, AkhirSemester -> 80)),
Siswa("Citra", Map(Kuis -> 90, TengahSemester -> 95, AkhirSemester -> 85))
)
println(laporan(kelas))# menjalankan program pengolah rekap yang sudah lengkap
scala-cli run rekap-utuh.scalaRekap nilai ujian
jumlah siswa: 3
nilai per siswa:
- Ali: 80/75/90
- Budi: 70/85/80
- Citra: 90/95/85
rata-rata per evaluasi:
- Kuis: 80.0
- TengahSemester: 85.0
- AkhirSemester: 85.0
- keseluruhan: 83.33Keluaran di atas dapat dihitung ulang dengan tangan sebagai verifikasi kedua setelah kompilasi. Rata-rata Kuis adalah 240 dibagi 3, yaitu 80.0, rata-rata TengahSemester dan AkhirSemester masing-masing 255 dibagi 3, yaitu 85.0, sedangkan rata-rata keseluruhan adalah 750 dibagi 9 yang ditampilkan dengan dua angka di belakang koma sebagai 83.33. Kesesuaian seluruh angka dengan perhitungan manual menandakan bahwa implementasi benar untuk data kelas ini, sehingga titik pemeriksaan tahap implementasi dinyatakan lolos.
Titik pemeriksaan setelah setiap perubahan#
Pembangunan selanjutnya bergerak dalam perubahan kecil yang selalu diikuti pemeriksaan. Satu perubahan dikerjakan, program dijalankan, keluarannya dibandingkan dengan keluaran pada titik sebelumnya, lalu perbedaan yang tak terduga diperbaiki sebelum pekerjaan berlanjut. Dengan disiplin ini, penyebab perbedaan selalu berada di antara dua titik pemeriksaan terakhir, sehingga pencarian kesalahan tetap sempit walaupun proyeknya membesar. Keluaran yang deterministik membuat perbandingan semacam itu dapat diandalkan, karena keluaran yang sama selalu berarti perilaku yang sama.
Pemeriksaan tidak selalu perlu menjalankan seluruh program. Perintah scala-cli repl rekap-utuh.scala membuka sesi REPL yang memuat seluruh definisi dalam berkas terlebih dahulu, sehingga perilaku satu fungsi dapat diuji secara terisolasi beserta kasus tepi yang tidak tercakup pada data kelas utama. Sesi berikut memeriksa daftar kosong pada rataRata serta nilai 120 pada validasi dan rekap.
scala> rataRata(List(80, 70, 90))
val res0: Option[Double] = Some(80.0)
scala> rataRata(List())
val res1: Option[Double] = None
scala> validasi(List(Siswa("Dewi", Map(Evaluasi.Kuis -> 120))))
val res2: Either[String, List[Siswa]] = Left("nilai di luar jangkauan 0-100: Dewi")
scala> rekap(List(Siswa("Dewi", Map(Evaluasi.Kuis -> 120))))
val res3: Either[String, Rekap] = Left("nilai di luar jangkauan 0-100: Dewi")Hasil sesi tersebut mengonfirmasi dua hal. Pertama, daftar kosong menghasilkan None, bukan error, karena rata-rata daftar kosong memang tidak ada dan kondisi itu sudah dinyatakan pada tipe keluarannya. Kedua, nilai 120 ditolak validasi dengan pesan yang menyebut siswanya, sedangkan rekap meneruskan Left itu tanpa menghitung apa pun karena for-comprehension berhenti pada hasil pertama yang gagal. Kedua perilaku sesuai dengan keputusan desain yang terkunci pada tanda tangan, sehingga titik pemeriksaan perilaku dinyatakan lolos.
Pemformatan dan peninjauan mandiri#
Titik pemeriksaan berikutnya menyasar bentuk kode, bukan perilakunya. Perintah scala-cli fmt menjalankan scalafmt, yaitu formatter kode Scala, dan menulis hasilnya langsung ke berkas yang diberikan tanpa konfirmasi. Karena itu, perintahnya dijalankan atas salinan kerja, bukan atas berkas proyek itu sendiri, dan hasilnya dibandingkan dengan berkas asalnya.
# salin berkas proyek, format salinannya, lalu tampilkan hasilnya
cp rekap-utuh.scala kerja.scala && scala-cli fmt kerja.scala 2>/dev/null && cat kerja.scala// Pengolah rekap nilai ujian: implementasi penuh dari tanda tangan yang sudah dikunci
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
)
// Menolak nilai di luar jangkauan 0 sampai 100
def validasi(siswa: List[Siswa]): Either[String, List[Siswa]] =
val bermasalah =
siswa.filter(s => s.nilai.values.exists(n => n < 0 || n > 100))
if bermasalah.isEmpty then Right(siswa)
else
Left(
s"nilai di luar jangkauan 0-100: ${bermasalah.map(_.nama).mkString(", ")}"
)
// Daftar kosong tidak memiliki rata-rata, sehingga hasilnya None
def rataRata(nilai: List[Int]): Option[Double] =
if nilai.isEmpty then None
else Some(nilai.sum.toDouble / nilai.length)
// Menyusun rekap setelah validasi lolos
def rekap(siswa: List[Siswa]): Either[String, Rekap] =
for sah <- validasi(siswa)
yield
val perEvaluasi = Evaluasi.values
.map(e => e -> rataRata(sah.map(_.nilai.getOrElse(e, 0))).getOrElse(0.0))
.toMap
val keseluruhan =
rataRata(sah.flatMap(_.nilai.values.toList)).getOrElse(0.0)
Rekap(sah.length, perEvaluasi, keseluruhan)
// Laporan teks yang siap dicetak
def laporan(siswa: List[Siswa]): String =
rekap(siswa) match
case Left(pesan) =>
s"rekap gagal: $pesan"
case Right(r) =>
val barisSiswa = siswa.map { s =>
val angka =
Evaluasi.values.map(e => s"${s.nilai.getOrElse(e, 0)}").mkString("/")
s"- ${s.nama}: $angka"
}
val barisRata = Evaluasi.values.toList.map(e =>
s"- $e: ${r.rataRataEvaluasi.getOrElse(e, 0.0)}"
)
val barisAkhir = f"- keseluruhan: ${r.rataRataKeseluruhan}%.2f"
val baris = "Rekap nilai ujian" :: s"jumlah siswa: ${r.jumlahSiswa}" ::
"nilai per siswa:" :: barisSiswa ::: "rata-rata per evaluasi:" :: barisRata ::: barisAkhir :: Nil
baris.mkString("\n")
@main def rekapUtuh(): Unit =
import Evaluasi.*
val kelas = List(
Siswa("Ali", Map(Kuis -> 80, TengahSemester -> 75, AkhirSemester -> 90)),
Siswa("Budi", Map(Kuis -> 70, TengahSemester -> 85, AkhirSemester -> 80)),
Siswa("Citra", Map(Kuis -> 90, TengahSemester -> 95, AkhirSemester -> 85))
)
println(laporan(kelas))Keluaran di atas identik dengan berkas rekap-utuh.scala baris demi baris, dan hasil yang tidak mengubah apa pun memang hasil yang diharapkan dari titik pemeriksaan ini, karena berkas proyek ditulis mengikuti aturan bawaan scalafmt. Scala CLI menggunakan aturan bawaan tersebut apabila proyek tidak menyediakan berkas konfigurasi .scalafmt.conf. Apabila salinan berbeda dengan asalnya, perbedaan itu diperiksa terlebih dahulu, lalu diterapkan pada berkas sumber agar tahap berikutnya selalu dimulai dari kode yang rapi.
Peninjauan mandiri menutup rangkaian titik pemeriksaan. Tipe Evaluasi, Siswa, dan Rekap serta nama validasi, rataRata, dan rekap dibaca ulang lalu dicocokkan dengan tanda tangan yang terkunci pada tahap desain, dan compiler akan menolak penyimpangan apa pun apabila ada nama yang berubah. Kasus tepi jangkauan ditinjau pada kondisi validasi, yaitu nilai 0 dan 100 diterima, sedangkan nilai negatif dan nilai lebih dari 100 ditolak. Cabang Left pada laporan juga sudah menutup kemungkinan data gagal, sehingga program tidak pernah mencetak angka dari data yang tidak sah.
Ringkasan#
- Implementasi mengganti setiap stub dengan badan fungsi yang sesungguhnya pada tanda tangan yang tetap, dan compiler menjadi titik pemeriksaan pertama sebelum program dijalankan.
- Siklus tulis, jalankan, periksa, dan perbaiki dijalankan atas perubahan kecil, sedangkan sesi REPL menguji perilaku satu fungsi beserta kasus tepinya secara terisolasi.
- Pemformatan dijalankan dengan
scala-cli fmtatas salinan kerja, dan salinan yang identik dengan berkas sumber menandakan titik pemeriksaan pemformatan lolos. - Peninjauan mandiri memastikan tipe, nama, dan kasus tepi tetap sesuai kontrak desain hingga proyek selesai.
Referensi#
- Scala CLI fmt — Scala CLI documentation — Perintah pemformatan yang menjalankan scalafmt atas berkas yang diberikan.
- Scala CLI repl — Scala CLI documentation — Sesi REPL yang dapat diberi berkas sumber agar definisinya dimuat lebih dahulu.
- scalafmt — Scalameta
— Formatter kode Scala yang menjadi mesin di balik
scala-cli fmt.