Polimorfisme

Variabel tipe dan penulisan fungsi generik yang bekerja untuk banyak tipe.

Daftar isi

Anotasi tipe tidak selalu menyebut tipe konkret. Huruf kecil seperti a pada [a] -> a adalah variabel tipe (type variable), yaitu tempat kosong yang diisi dengan tipe apa pun pada saat fungsi dipanggil. Mekanisme ini disebut polimorfisme, dan topik ini membahas cara membacanya, cara menulis fungsi generik sendiri, serta batasan yang menyertainya ketika variabel tipenya diberi syarat.

Variabel tipe pada fungsi bawaan#

Banyak fungsi bawaan memiliki tipe generik yang dapat ditanyakan langsung. Fungsi reverse membalik list dengan elemen tipe apa pun, sedangkan zip menyatukan dua list yang elemennya boleh berbeda tipe. Tidak ada satu pun tipe konkret yang disebut pada anotasi keduanya, sehingga satu definisi melayani pemanggilan dengan tipe yang berbeda-beda.

bash
# variabel tipe a muncul pada masukan dan keluaran
ghci -e ':t reverse'
# dua variabel tipe membedakan elemen dua list
ghci -e ':t zip'
text Keluaran
reverse :: [a] -> [a]
zip :: [a] -> [b] -> [(a, b)]

Pada reverse :: [a] -> [a], huruf a muncul di kedua sisi panah, sehingga elemen masukan dan elemen hasil pasti bertipe sama. Pada zip :: [a] -> [b] -> [(a, b)], terdapat dua huruf yang berbeda, sehingga dua list boleh berisi tipe yang berbeda dan hasilnya berupa pasangan. Satu pemanggilan menetapkan satu pemilihan tipe, misalnya reverse [3, 2, 1] menetapkan a menjadi Int untuk pemanggilan tersebut.

Satu nama fungsi generik juga dapat dipanggil dengan tipe yang berbeda pada baris yang sama tanpa penyalinan definisi apa pun. Compiler menyusun satu definisi dan menetapkan variabel tipenya pada setiap pemanggilan, sehingga reverse yang digunakan untuk list angka dan list huruf tetap satu fungsi yang sama. Perilaku yang dijanjikannya juga seragam untuk semua pilihan tipe, karena definisinya tidak dapat menyentuh isi elemen yang tipenya belum diketahui.

Fungsi generik ditulis dengan cara yang sama seperti fungsi biasa, yaitu memberi nama variabel tipe pada anotasinya, kemudian mengisi isinya tanpa menganggap tipe itu konkret. Program berikut memuat tiga fungsi generik, yaitu pengambilan elemen terakhir, penukaran isi pasangan, dan penerapan fungsi dua kali.

haskell generik.hs
-- a dan b adalah variabel tipe: singkatan tipe apa pun yang ditentukan pemanggil
terakhir :: [a] -> a
terakhir xs = xs !! (length xs - 1)

tukar :: (a, b) -> (b, a)
tukar (x, y) = (y, x)

terapkanDuaKali :: (a -> a) -> a -> a
terapkanDuaKali f x = f (f x)

main :: IO ()
main = do
  print (terakhir [1, 2, 3])
  print (terakhir "kalkulator")
  print (tukar (1, True))
  print (tukar ("api", 9))
  print (terapkanDuaKali (* 3) 2)
text Keluaran
3
'r'
(True,1)
(9,"api")
18

Fungsi terakhir [1, 2, 3] menghasilkan 3, sedangkan terakhir "kalkulator" menghasilkan karakter 'r', karena String adalah list dari Char dan keluaran Char ditampilkan dengan tanda kutip tunggal. Fungsi tukar bekerja untuk pasangan tipe campuran seperti (1, True) maupun ("api", 9), sedangkan terapkanDuaKali (* 3) 2 menghasilkan 18 karena penggandaan dilakukan dua kali. Satu definisi per fungsi sudah cukup untuk seluruh pemanggilan tersebut.

Membaca tipe generik sekaligus membatasi apa yang boleh ditulis. Fungsi terakhir :: [a] -> a hanya dapat mengembalikan elemen dari listnya, karena tidak ada sumber nilai a lain yang tersedia di dalam fungsinya, sedangkan fungsi yang harus menyediakan nilai sendiri memerlukan syarat tambahan pada tipenya. Bentuk demikian disebut tipe yang mengarahkan implementasi. Setelah anotasinya ditetapkan, compiler menolak isi yang membutuhkan informasi lebih daripada yang dijanjikan tipe.

Keterikatan variabel tipe yang sama#

Huruf yang sama pada satu anotasi tipe berarti tipe yang sama, dan huruf yang berbeda bebas berbeda tipe. Pada terapkanDuaKali :: (a -> a) -> a -> a, semua a terikat bersama, sehingga fungsi yang diteruskan harus menerima dan menghasilkan tipe yang sama dengan nilai yang diberikan. Pemanggilan terapkanDuaKali (* 3) 2 menetapkan a menjadi Int pada seluruh posisi sekaligus.

Keterikatan ini memberi jaminan perilaku yang kuat. Fungsi bertipe [a] -> Int tidak dapat menginspeksi isi elemennya, karena a dapat berupa tipe apa pun yang tidak diketahui, sehingga satu-satunya informasi yang tersedia hanyalah bentuk listnya. Fungsi bertipe (a -> b) -> a -> a pun tidak dapat ditulis secara sah, sebab hasil f bertipe b, sedangkan posisi hasil menuntut a, dan tidak ada cara menyamakan keduanya tanpa informasi tambahan. Pola seperti ini disebut polimorfisme parametrik (parametric polymorphism), yaitu polimorfisme yang perilakunya ditentukan oleh bentuk tipenya, bukan oleh isi tipenya.

Polimorfisme dengan syarat#

Tidak semua fungsi generik dapat menerima tipe apa pun. Fungsi abs membutuhkan operasi aritmetika, sehingga tipe yang mengisinya wajib menyediakannya. Syarat semacam ini ditulis di depan tipe dengan tanda => dan disebut constraint, yaitu persyaratan class yang melekat pada variabel tipe.

bash
# constraint Num a menemani variabel tipe pada abs
ghci -e ':t abs'
text Keluaran
abs :: Num a => a -> a

Pembacaan tipe di atas adalah “untuk setiap a yang mengikuti class Num, fungsi ini menerima a dan menghasilkan a”. Variabel tipe tetap fleksibel, tetapi pilihannya dibatasi oleh class yang disyaratkan, dan compiler memeriksa syarat itu pada setiap pemanggilan. Num adalah salah satu dari banyak class, yaitu kontrak perilaku yang menyertai sekelompok tipe. Mekanisme class beserta contoh lengkapnya dibahas pada topik Type class.

Syarat pada tipe terlihat paling jelas ketika pemanggilannya keliru. Ekspresi abs True ditolak compiler dengan pesan yang menyatakan tidak ada instans Num Bool. Dari pesan itu terlihat bahwa constraint diperiksa pada titik pemanggilan dan bukan pada definisi fungsinya. Pesan error yang menyebut nama class selalu dapat ditelusuri ke syarat pada tipe fungsinya, sehingga pembacaan tipe menjadi alat diagnosis pertama sebelum membaca isi fungsinya.

Ringkasan#

  • Huruf kecil pada tipe, seperti a dan b, adalah variabel tipe yang diisi tipe konkret saat pemanggilan.
  • Huruf yang sama terikat pada tipe yang sama, sedangkan huruf yang berbeda bebas berbeda tipe.
  • Fungsi generik hanya dapat menggunakan bentuk nilainya, sehingga [a] -> a menjamin elemen hasil bertipe sama dengan elemen masukan.
  • Tanda => menyatakan constraint, yaitu syarat class yang harus diikuti variabel tipe.
  • Polimorfisme parametrik membuat satu definisi aman digunakan untuk banyak tipe tanpa penyesuaian.

Referensi#