Pattern matching

Pencocokan pola pada argumen fungsi dan binding beserta penggunaan wildcard.

Daftar isi

Pattern matching adalah mekanisme yang memeriksa bentuk sebuah nilai, lalu mengikat bagian-bagiannya ke nama. Hampir semua definisi Haskell menyentuh mekanisme ini, dari persamaan berganda hingga pengikatan di let dan where. Topik ini membahas pola pada argumen fungsi, pola pada list, penggunaan wildcard, pengikatan pola di luar argumen, serta kesalahan umum yang menyertainya.

Pola pada argumen fungsi#

Persamaan berganda bekerja dengan menguji pola (pattern) pada setiap argumen. Pola paling sederhana adalah nama variabel yang cocok dengan apa pun, sedangkan pola yang lebih spesifik menyasar bentuk nilai. Pola tuple misalnya mencocokkan pasangan nilai dan mengikat setiap komponennya ke nama sendiri, sehingga pembongkaran nilai gabungan menjadi ringkas.

haskell pola-tuple.hs
-- Pola tuple mengikat setiap komponen ke sebuah nama
jarakAsal :: (Double, Double) -> Double
jarakAsal (x, y) = sqrt (x * x + y * y)

main :: IO ()
main = do
  print (jarakAsal (3, 4))
  print (jarakAsal (0, 0))
  print (jarakAsal (5, 12))
text Keluaran
5.0
0.0
13.0

Fungsi jarakAsal menerima satu argumen berupa pasangan, dan pola (x, y) memecahnya menjadi dua nama sekaligus. Tanpa pola, penghitungan yang sama harus ditulis dengan fungsi pengambil komponen, misalnya sqrt (fst t * fst t + snd t * snd t), yang jelas lebih sulit diikuti. Pencocokan pola sekaligus memeriksa bentuk dan menyediakan pengikatan, itulah sebabnya mekanisme ini menjadi gaya utama untuk membongkar data pada Haskell.

Pola yang sah tidak berhenti pada tuple. Nilai literal seperti angka juga dapat menjadi pola, dan pola nama variabel tetap menjadi cara paling umum untuk menerima masukan apa pun. Pencocokan berlangsung dari atas ke bawah, sehingga pola yang lebih spesifik ditaruh lebih dahulu dan pola penangkap menutup daftar. Urutan semacam ini sama dengan aturan persamaan berganda, karena keduanya memang satu mekanisme yang sama.

Pola pada list#

List hanya memiliki dua bentuk, yaitu list kosong [] dan pasangan kepala-ekor yang ditulis x : xs, sehingga pola list selalu menangani dua kasus tersebut. Pola x : xs mengikat elemen pertama ke x dan sisa list ke xs. Karena ekornya berupa list yang lebih pendek, fungsi yang memanggil dirinya pada ekor akan memproses seluruh list langkah demi langkah.

haskell pola-list.hs
-- Pola list: list kosong dan pasangan kepala-ekor
totalNilai :: [Int] -> Int
totalNilai [] = 0
totalNilai (nilai : sisanya) = nilai + totalNilai sisanya

-- Garis bawah menyatakan elemen yang tidak dipakai
banyakNilai :: [Int] -> Int
banyakNilai [] = 0
banyakNilai (_ : sisanya) = 1 + banyakNilai sisanya

main :: IO ()
main = do
  let nilaiUjian = [8, 7, 9, 6]
  print (totalNilai nilaiUjian)
  print (banyakNilai nilaiUjian)
text Keluaran
30
4

Fungsi totalNilai mengjumlahkan seluruh elemen dengan pola dua kasus, sedangkan banyakNilai menghitung banyak elemen dengan pola yang serupa. Kasus [] menjadi titik henti rekursi (recursion), sedangkan kasus x : xs menambahkan satu langkah lalu melanjutkan ke ekor. Pola (_ : sisanya) pada banyakNilai menggunakan garis bawah karena nilai elemen pertama memang tidak diperlukan dalam penghitungan.

Nama xs pada pola tersebut merupakan konvensi yang lazim dijumpai di seluruh kode Haskell, sebab x menyatakan satu elemen dan xs menyatakan banyak elemen. Nama itu bukan aturan compiler, melainkan kebiasaan yang membuat persamaan list mudah dikenali. Apabila fungsi hanya memerlukan ekor tanpa kepalanya, wildcard dapat menempati posisi kepala sehingga maksud pengabaian itu tersampaikan tanpa penjelasan tambahan.

Wildcard dan pola as#

Garis bawah _ disebut wildcard, yaitu pola yang selalu cocok namun tidak mengikat nama. Pola ini digunakan apabila bagian tertentu dari nilai tidak diperlukan, misalnya komponen kedua pasangan atau ekor list. Beberapa wildcard boleh muncul dalam satu pola, seperti (_, y) yang hanya mengikat komponen kedua. Evaluasi berikut memperlihatkan pengikatan selektif tersebut.

bash
# komponen pertama dibiarkan tanpa nama
ghc -e 'let (_, y) = (10, 20) in y'
text Keluaran
20

Wildcard juga memberi pesan yang jelas kepada pembaca, karena keberadaannya menegaskan bahwa nilai tersebut sengaja diabaikan dan bukan terlupakan. Sebaliknya, apabila bagian keseluruhan tetap diperlukan, pola as mengikat keduanya sekaligus. Bentuknya nama@pola, sehingga semua@(awal : _) menyediakan nama untuk seluruh list sekaligus nama untuk elemen pertamanya.

haskell pola-as.hs
-- Pola as mengikat keseluruhan nilai sekaligus bagian pertamanya
labelDaftar :: [Int] -> String
labelDaftar [] = "daftar kosong"
labelDaftar semua@(awal : _) =
  "daftar berawal " ++ show awal ++ " dan memuat " ++ show (length semua) ++ " elemen"

main :: IO ()
main = do
  putStrLn (labelDaftar [])
  putStrLn (labelDaftar [4, 2, 7])
text Keluaran
daftar kosong
daftar berawal 4 dan memuat 3 elemen

Tanpa pola as, fungsi labelDaftar harus menghitung panjang list secara terpisah atau menerima masukan dalam bentuk lain. Satu pola as menjaga kedua informasi itu tetap tersedia, sehingga kode tetap ringkas tanpa penghitungan ulang.

Pengikatan pola pada let dan where#

Pola tidak terbatas pada argumen fungsi. Pengikatan let dan where juga menerima pola, sehingga nilai gabungan dapat dibongkar pada saat didefinisikan. Bentuk ini digunakan ketika struktur nilai sudah pasti, misalnya hasil fungsi yang selalu mengembalikan pasangan jam dan menit.

haskell pola-pengikatan.hs
-- Pola mengikat nilai di let dan where, bukan hanya pada argumen fungsi
pecahMenit :: Int -> (Int, Int)
pecahMenit m = (m `div` 60, m `mod` 60)

deskripsiWaktu :: Int -> String
deskripsiWaktu total = nama
  where
    (jam, sisa) = pecahMenit total
    nama = show jam ++ " jam " ++ show sisa ++ " menit"

main :: IO ()
main = do
  let (jam, menit) = pecahMenit 130
  print jam
  print menit
  putStrLn (deskripsiWaktu 130)
text Keluaran
2
10
2 jam 10 menit

Baris let (jam, menit) = pecahMenit 130 mengikat dua nama dari satu nilai tuple, dan klausa where melakukan hal yang sama untuk fungsi deskripsiWaktu. Apabila bentuk nilai dapat bervariasi, misalnya list yang bisa kosong, pencocokan melalui persamaan berganda tetap menjadi pilihan yang aman karena setiap kemungkinan bentuk dapat ditangani satu per satu.

Kesalahan umum#

Kesalahan yang paling sering muncul adalah fungsi yang polanya tidak mencakup semua kemungkinan masukan. Contoh berikut hanya menangani list yang memuat minimal satu elemen.

haskell Contoh: fungsi tanpa pola untuk list kosong
-- tidak-dijalankan
kepala :: [Int] -> Int
kepala (x : _) = x

Contoh itu lolos kompilasi karena polanya sah, tetapi panggilan kepala [] menghentikan program dengan exception non-exhaustive patterns. Compiler biasanya memberi peringatan pola tidak lengkap pada waktu kompilasi, dan peringatan itu pantas ditangani segera. Perbaikannya dengan menambah persamaan untuk kasus yang tertinggal, misalnya kepala [] = 0 apabila 0 merupakan nilai yang masuk akal, atau dengan mengubah tipe keluaran agar kekosongan dapat dinyatakan secara eksplisit.

Kesalahan kedua adalah menggunakan pola variabel yang nilainya tidak pernah digunakan, yang memunculkan peringatan pada waktu kompilasi tanpa memberi manfaat apa pun. Gunakan _ pada tempat semacam itu agar maksud kode tegas, karena wildcard menyatakan pengabaian yang disengaja dan compiler dapat berhenti memperingatkan bagian tersebut.

Ringkasan#

  • Pattern matching menguji bentuk nilai dan mengikat bagian-bagiannya ke nama, dengan pola pertama yang cocok dipilih dari atas ke bawah.
  • Pola tuple dan pola list membongkar struktur data langsung pada argumen, termasuk pola x : xs untuk kepala dan ekor list.
  • Wildcard _ selalu cocok tanpa mengikat nilai, sedangkan pola as nama@pola mengikat keseluruhan nilai sekaligus bagian dalamnya.
  • Pola juga sah pada let dan where, sedangkan pola yang tidak lengkap berujung exception non-exhaustive patterns pada waktu berjalan.

Referensi#