Destrukturisasi data

Case class dan pola bersarang yang mengikat field ke nama lokal.

Daftar isi

Destrukturisasi (destructuring) adalah pembongkaran satu nilai terstruktur menjadi beberapa nama lokal sekaligus, sehingga setiap bagiannya dapat digunakan tanpa mengakses field satu per satu. Topik ini membahas destrukturisasi case class pada ekspresi match, pola bersarang yang menjangkau field di dalamnya, pengikatan keseluruhan nilai dengan @, serta destrukturisasi pada pengikatan val beserta syarat keselamatannya. Setelah topik ini dibaca, pola yang mengikat data dapat ditulis lebih ringkas dan risiko kegagalannya dapat diantisipasi.

Destrukturisasi pada ekspresi match#

Destrukturisasi pada match dilakukan dengan pola yang meniru bentuk konstruktor case class. Apabila nilai cocok dengan bentuk tersebut, compiler memverifikasi strukturnya dan sekaligus mengikat setiap field ke nama baru yang hanya berlaku pada cabang tersebut. Penulisan ini menggantikan akses field manual seperti p.x dan p.y, dan compiler turut memeriksa kecocokan nama serta tipenya.

Program berikut mengikat field case class Titik ke x dan y dalam satu langkah.

scala Contoh: mengikat field case class ke nama lokal
val p = Titik(3, 4)

p match
  case Titik(x, y) => println(s"($x, $y)")

Pola tuple bekerja dengan cara yang sama terhadap pasangan nilai, misalnya case (a, b) => yang mengikat elemen pertama ke a dan elemen kedua ke b. Asalkan bentuk nilai yang dicocokkan sesuai, setiap posisi terikat secara berurutan tanpa pengaksesan indeks. Nama x dan y pada contoh di atas tidak tertulis di luar cabangnya, sehingga tidak menambah variabel baru pada lingkup yang lebih luas.

Apabila bentuknya tidak cocok, pencocokan berlanjut ke cabang berikutnya, sehingga beberapa pola dapat disusun berurutan, dimulai dari pola yang paling spesifik. Kombinasi semacam ini menjadi dasar pemrosesan data terstruktur, yaitu setiap cabang menguraikan bentuk yang dihadapinya tanpa pernyataan if bertingkat.

Pola bersarang#

Pola case class dapat bersarang karena setiap posisi pola menerima pola lain. Pola Garis(Titik(x1, y1), Titik(x2, y2)) mengikat empat koordinat ujung garis secara langsung, tanpa variabel perantara untuk objek Titik di dalamnya. Struktur seperti ini menyingkat kode yang seharusnya mengakses g.awal.x dan seterusnya secara berulang.

Simpan program berikut sebagai pola-geometri.scala. Programnya mengelompokkan garis menjadi vertikal, horizontal, dan diagonal, dengan guard sebagai syarat tambahan yang mempersempit kecocokan tiap cabang.

scala pola-geometri.scala
// Destrukturisasi case class pada pola bersarang
case class Titik(x: Int, y: Int)
case class Garis(awal: Titik, akhir: Titik)

@main def polaGeometri(): Unit =
  val kumpulanGaris = List(
    Garis(Titik(0, 0), Titik(4, 0)),
    Garis(Titik(1, 2), Titik(1, 7)),
    Garis(Titik(2, 3), Titik(6, 6))
  )

  for g <- kumpulanGaris do
    g match
      // pola bersarang: field dalam langsung terikat ke nama lokal
      case Garis(Titik(x1, y1), Titik(x2, y2)) if x1 == x2 =>
        println(s"vertikal   : dari ($x1, $y1) ke ($x2, $y2)")
      case Garis(Titik(x1, y1), Titik(x2, y2)) if y1 == y2 =>
        println(s"horizontal : dari ($x1, $y1) ke ($x2, $y2)")
      // binding @: garis utuh terikat ke `garis`, sekaligus bagiannya ke nama lokal
      case garis @ Garis(Titik(_, _), Titik(x2, y2)) =>
        println(s"diagonal   : $garis, ujung ($x2, $y2)")

Jalankan program tersebut dengan scala-cli.

bash
# mengompilasi dan menjalankan program pola bersarang
scala-cli run pola-geometri.scala
text Keluaran
horizontal : dari (0, 0) ke (4, 0)
vertikal   : dari (1, 2) ke (1, 7)
diagonal   : Garis(Titik(2,3),Titik(6,6)), ujung (6, 6)

Urutan cabang menentukan hasil. Guard yang tidak terpenuhi tidak menghentikan pencocokan, melainkan meneruskan nilai ke cabang berikutnya. Karena cabang terakhir tidak bersyarat, ketiga garis pada daftar selalu menemukan cabangnya, dan keluarannya tetap sama setiap kali program dijalankan.

Mengikat keseluruhan nilai dengan @#

Terkadang keseluruhan objek dan sebagian fieldnya diperlukan bersamaan. Pengikatan (binding) dengan @ menjawab kebutuhan itu, yaitu pola yang mengikat satu nama ke keseluruhan nilai sekaligus melanjutkan destrukturisasi di sisi kanannya.

scala Contoh: mengikat objek utuh dan bagiannya sekaligus
garis match
  case g @ Garis(Titik(x1, y1), _) =>
    println(s"$g bermula pada ($x1, $y1)")

Nama g merujuk objek Garis utuh, sedangkan x1 dan y1 merujuk koordinat ujung awalnya. Pengikatan dengan @ juga dapat bersarang, misalnya a @ Titik(ax, ay) di dalam pola Garis, sehingga objek Titik yang utuh dan koordinatnya terikat bersamaan. Pola _ pada ujung akhir menyatakan bagian yang diabaikan, sehingga tidak ada nama perantara yang tidak terpakai. Program utuh pada bagian sebelumnya menggunakan bentuk yang sama pada cabang diagonal, yaitu garis @ Garis(Titik(_, _), Titik(x2, y2)), untuk mencetak garis utuh sekaligus koordinat ujungnya.

Destrukturisasi pada val#

Pola juga dapat ditulis di sebelah kiri pengikatan val, sehingga destrukturisasi terjadi tanpa match. Bentuk ini lazim untuk tuple, misalnya val (lebar, tinggi) = (8, 6), dan juga sah untuk case class.

Simpan program berikut sebagai destrukturisasi-val.scala.

scala destrukturisasi-val.scala
// Destrukturisasi pada pengikatan val
case class Titik(x: Int, y: Int)

@main def destrukturisasiVal(): Unit =
  val pusat = Titik(3, 4)

  // pola tuple: dua nilai diikat sekaligus
  val (lebar, tinggi) = (8, 6)
  println(s"ukuran kanvas: $lebar x $tinggi")

  // tipe pusat persis Titik, sehingga polanya tidak mungkin gagal
  val Titik(px, py) = pusat
  println(s"pusat: ($px, $py)")

Jalankan program tersebut dengan scala-cli.

bash
# mengompilasi dan menjalankan program destrukturisasi val
scala-cli run destrukturisasi-val.scala
text Keluaran
ukuran kanvas: 8 x 6
pusat: (3, 4)

Keselamatan bentuk ini bergantung pada kemungkinan polanya gagal. Tipe pusat persis Titik, dan case class pada Scala 3 bersifat final secara bawaan, sehingga compiler membuktikan pola Titik(px, py) pasti cocok dan menerimanya tanpa peringatan. Pola tuple pada sisi kanan bertipe tuple juga pasti cocok, karena jumlah dan posisi elemennya merupakan bagian dari tipe itu sendiri. Sebaliknya, apabila tipe sisi kanan lebih luas daripada polanya, misalnya Any, compiler memberi peringatan karena kecocokan hanya dapat dipastikan saat program berjalan, dan kegagalan itu melempar MatchError.

Kesalahan umum#

Menggunakan nama yang sama pada dua posisi pola. Pola case Titik(x, x) ditolak compiler dengan pesan error duplicate pattern variable: x, karena satu cabang tidak boleh mengikat dua field ke nama yang sama. Beri nama berbeda untuk setiap posisi, lalu bandingkan nilainya di badan cabang apabila kesamaan memang diinginkan.

Berharap guard yang gagal menghentikan pencocokan. Guard yang tidak terpenuhi tidak melempar error, melainkan meneruskan nilai ke cabang berikutnya. Apabila tidak ada cabang lagi yang cocok, program berhenti dengan MatchError. Pastikan selalu ada cabang tanpa syarat di akhir, atau pastikan guard mencakup semua kemungkinan.

Ringkasan#

  • Pola pada match memverifikasi bentuk case class dan mengikat fieldnya ke nama lokal cabang tersebut.
  • Pola bersarang mengikat field di dalam field lain, sedangkan pengikatan @ menangkap keseluruhan nilai sekaligus bagiannya.
  • Destrukturisasi val aman hanya apabila polanya pasti cocok, yaitu ketika tipe sisi kanan persis sama dengan pola, dan melempar MatchError apabila kecocokan gagal saat program berjalan.

Referensi#