Ekspresi match

Case, guard, dan pemeriksaan exhaustiveness yang dijamin compiler.

Daftar isi

Ekspresi match adalah struktur pencocokan pola (pattern matching) Scala yang menguji satu nilai terhadap sejumlah cabang case dan menghasilkan nilai dari cabang pertama yang cocok. Topik ini membahas match sebagai ekspresi bernilai, guard if untuk menyaring cabang, pemeriksaan exhaustiveness (exhaustiveness checking) yang dijamin compiler pada tipe tertutup (sealed), serta peran case _ dan case x sebagai cabang cadangan. Setelah membacanya, match dapat digunakan bukan hanya sebagai pengganti rantai if-else, tetapi juga sebagai alat yang membuat compiler memeriksa kelengkapan percabangan pada waktu kompilasi.

Match sebagai ekspresi yang bernilai#

Compiler mengolah match sebagai satu ekspresi, sehingga hasilnya dapat diikat ke val, dikirim ke fungsi, atau dibandingkan seperti nilai lain. Nilai yang diuji ditulis di sebelah kiri kata kunci match, sedangkan setiap cabang case memuat satu pola beserta hasilnya. Pencocokan berjalan dari atas ke bawah, dan cabang pertama yang polanya cocok menentukan nilai keseluruhan. Tipe hasilnya diambil dari tipe hasil seluruh cabangnya, sehingga cabang-cabang yang semuanya menghasilkan String membuat ekspresi match bertipe String. Program berikut mengelompokkan angka ke dalam keterangan yang sesuai, lalu mengikat hasilnya ke val keterangan sebelum dicetak.

scala klasifikasi.scala
// match adalah ekspresi: hasil setiap case menjadi nilai ekspresinya
@main def klasifikasi(): Unit =
  val angka: List[Int] = List(0, 7, 42)

  for a <- angka do
    val keterangan =
      a match
        case 0 => "nol"
        case 1 => "satu"
        case n => s"bukan nol atau satu, yaitu $n"

    println(s"$a -> $keterangan")
bash
# menjalankan program klasifikasi.scala
scala-cli run klasifikasi.scala
text Keluaran
0 -> nol
7 -> bukan nol atau satu, yaitu 7
42 -> bukan nol atau satu, yaitu 42

Nilai 0 ditangani oleh pola literal case 0, sedangkan 42 jatuh ke case n karena dua cabang sebelumnya sudah gagal. Pola pada case n adalah pola variabel, yaitu pola yang cocok dengan nilai apa pun sekaligus mengikat nilai itu ke pengenal n agar dapat digunakan pada hasil cabang. Interpolasi s"bukan nol atau satu, yaitu $n" menggunakan ikatan tersebut, sehingga keterangan untuk 42 memuat angkanya sendiri. Urutan cabang berperan penting di sini: apabila case n diletakkan paling atas, dua pola literal di bawahnya tidak akan pernah terjangkau.

Guard if pada case#

Guard (guard) adalah syarat boolean yang ditempelkan pada sebuah cabang case dengan kata kunci if setelah polanya. Cabang semacam ini diuji dalam dua tahap: pola harus cocok terlebih dahulu, kemudian guard dievaluasi menggunakan ikatan yang dihasilkan pola. Apabila guard menghasilkan false, pencocokan berlanjut ke cabang berikutnya seolah-olah pola tadi tidak cocok. Guard berguna ketika satu pola perlu dibedakan lebih rinci, misalnya memisahkan angka negatif dari angka kecil tanpa menambah pola baru. Program berikut membagi angka menjadi empat kelompok dengan tiga cabang yang memuat guard dan satu cabang cadangan.

scala guard.scala
// guard if pada case menyaring nilai yang sudah cocok dengan polanya
@main def kelompokkan(): Unit =
  val angka: List[Int] = List(-5, 0, 34, 78, 120)

  for x <- angka do
    val kategori =
      x match
        case 0             => "nol"
        case n if n < 0    => "negatif"
        case n if n <= 100 => "normal"
        case _             => "di atas 100"

    println(s"$x -> $kategori")
bash
# menjalankan program guard.scala
scala-cli run guard.scala
text Keluaran
-5 -> negatif
0 -> nol
34 -> normal
78 -> normal
120 -> di atas 100

Cabang case 0 menangkap angka nol sebelum guard mana pun diuji. Angka -5 cocok dengan pola variabel case n dan lolos karena guard n < 0 benar, sedangkan 34 dan 78 memenuhi guard n <= 100. Nilai 120 gagal pada kedua guard, sehingga satu-satunya jalan tersisa adalah case _. Perhatikan bahwa guard pada cabang kedua dan ketiga sama-sama menggunakan pola variabel n, karena guard hanya dapat merujuk ikatan yang dibuat oleh polanya sendiri. Tanpa pola variabel, kondisi seperti n < 0 tidak memiliki n yang dapat dirujuk.

Pemeriksaan exhaustiveness yang dijamin compiler#

Pemeriksaan exhaustiveness adalah jaminan compiler bahwa match telah menangani setiap nilai yang mungkin, dan jaminan ini hanya dapat diberikan apabila compiler mengetahui seluruh kemungkinan itu. Syaratnya terpenuhi pada tipe tertutup (sealed), yaitu trait yang seluruh subtipenya dideklarasikan dalam berkas yang sama, sehingga daftar kemungkinannya tertutup dan dapat dihitung compiler. Match yang salah satu subtipenya belum ditangani tidak menggagalkan kompilasi, tetapi memunculkan peringatan yang menyebut subtipe yang terlewat. Berkas berikut sengaja hanya menangani Merah padahal StatusLampu juga memiliki Hijau.

scala kurang.scala
// match yang belum menangani semua subtipe StatusLampu
sealed trait StatusLampu
case object Merah extends StatusLampu
case object Hijau extends StatusLampu

@main def lupa(): Unit =
  val lampu: StatusLampu = Merah
  val tindakan = lampu match
    case Merah => "berhenti"
  println(tindakan)
bash
# mengompilasi kurang.scala untuk melihat peringatan compiler
scala-cli compile kurang.scala
text Keluaran
[warn] ./kurang.scala:8:18
[warn] match may not be exhaustive.
[warn]
[warn] It would fail on pattern case: Hijau
[warn]   val tindakan = lampu match
[warn]                  ^^^^^

Peringatan match may not be exhaustive menunjuk ke baris match dan ditutup dengan kalimat It would fail on pattern case: Hijau, yang menyebut nilai yang akan membuat match gagal. Compiler mengetahuinya tanpa menjalankan program karena StatusLampu bertanda sealed, sehingga Merah dan Hijau adalah satu-satunya subtipenya. Perbaikannya berupa penambahan cabang untuk setiap subtipe yang tersisa, sebagaimana ditunjukkan berkas berikut.

scala lengkap.scala
// case untuk setiap subtipe membuat match lengkap tanpa case cadangan
sealed trait Lampu
case object Merah extends Lampu
case object Hijau extends Lampu

@main def aksiLampu(): Unit =
  val urutan: List[Lampu] = List(Merah, Hijau, Merah)

  for lampu <- urutan do
    val aksi =
      lampu match
        case Merah => "berhenti"
        case Hijau => "jalan"

    println(aksi)
bash
# menjalankan program lengkap.scala
scala-cli run lengkap.scala
text Keluaran
berhenti
jalan
berhenti

Dengan cabang case Hijau yang menutup daftar subtipe, peringatan hilang dan match dapat ditulis tanpa cabang cadangan. Manfaat jaminan ini baru terasa ketika subtipe baru ditambahkan: satu case object tambahan langsung memunculkan peringatan pada setiap match yang belum menanganinya, sehingga subtipe yang terlewat ditemukan melalui kompilasi dan bukan melalui error saat program berjalan. case object sendiri adalah objek tunggal tanpa data yang lazim digunakan sebagai subtipe sederhana.

case _ dan case x sebagai cabang cadangan#

Dua pola yang paling sering menutup sebuah match adalah case _ dan case x. Keduanya cocok dengan nilai apa pun, tetapi berbeda dalam hal ikatan: case _ mengabaikan nilai sepenuhnya, sedangkan case x mengikat nilai itu ke pengenal x agar dapat digunakan pada hasil cabang. Pilihan di antara keduanya ditentukan oleh satu hal teknis, yaitu apakah nilai yang tak tertangani cabang-cabang sebelumnya masih diperlukan.

Kedua program di bagian sebelumnya telah memperlihatkan bentuk tersebut. Program guard.scala menutup match dengan case _ karena kategori di luar rentang tidak memerlukan angkanya, sedangkan klasifikasi.scala menggunakan case n agar angka tetap dapat ditampilkan. Bentuk keduanya dirangkum sebagai berikut.

scala Contoh: case x mengikat nilai yang tidak tertangani cabang lain
case n => s"bukan nol atau satu, yaitu $n"
scala Contoh: case _ mengabaikan nilai sepenuhnya
case _ => "di atas 100"

Sebagai cabang terakhir, keduanya berfungsi sama dan dipilih menurut kebutuhan akan ikatannya. Sebagai cabang sebelum akhir, keduanya menyerap nilai yang seharusnya dijangkau cabang-cabang di bawahnya, sehingga letak yang tepat selalu berada di bagian paling bawah. Pola variabel dan urutan pencocokannya dibedah lebih rinci pada topik bentuk pola.

Kesalahan umum#

Kesalahan yang paling sering terjadi pada match adalah membiarkan sebuah nilai tanpa cabang yang cocok. Pada tipe yang tidak tertutup, seperti Int atau String, compiler tidak memunculkan peringatan apa pun karena daftar nilai yang mungkin tidak terbatas, sehingga program tetap lolos kompilasi dan baru gagal ketika input yang tidak tertangani benar-benar datang.

scala Contoh: match tanpa cabang cadangan
val n = 5
val keterangan =
  n match
    case 0 => "nol"
    case 1 => "satu"

Apabila potongan ini dijalankan, match melempar scala.MatchError dengan pesan 5 (of class java.lang.Integer) dan program berhenti dengan status gagal. Perbaikannya berupa satu cabang cadangan yang menutup match, misalnya case _ => "bukan nol atau satu" atau case x => s"nilai tidak dikenal: $x". Pada tipe tertutup, kelalaian serupa sudah terdeteksi lebih dulu oleh peringatan exhaustiveness, sehingga kebiasaan menyediakan cabang cadangan terutama penting untuk tipe yang tidak tertutup.

Ringkasan#

  • Match adalah ekspresi: cabang case pertama yang cocok menentukan nilai keseluruhannya, dan hasilnya dapat diikat ke val seperti nilai lain.
  • Guard if menyaring cabang setelah polanya cocok, dan guard yang gagal membuat pencocokan berlanjut ke cabang berikutnya.
  • Pada tipe sealed, compiler memeriksa kelengkapan match dan memperingatkan subtipe yang belum ditangani sebelum program dijalankan.
  • case _ dan case x menutup match sebagai cadangan, tanpa ikatan atau dengan ikatan, dan match tanpa cabang yang cocok melempar scala.MatchError.

Referensi#