Sebagian parameter tipe tidak menampung nilai apa pun, tetapi tetap membedakan satu tipe dari tipe lainnya bagi compiler. Parameter semacam itu disebut phantom type, yaitu label tipe yang menempel pada tipe data tanpa pernah muncul di dalam nilainya. Topik ini memperkenalkan pola tersebut melalui dua contoh, yaitu identitas yang tidak tertukar antarjenis dan dokumen yang membawa status, beserta penjelasan mengapa biayanya nol pada waktu berjalan.
Parameter tipe tanpa wakil#
Tipe newtype ID entitas = ID Int menerima satu parameter tipe bernama entitas, sedangkan nilai di dalamnya hanya Int. Parameter itu tidak pernah diisi nilai karena tidak ada konstruktor yang memintanya, tetapi dua tipe dengan parameter berbeda menjadi dua tipe yang tidak dapat dipertukarkan. Dua tipe kosong Pengguna dan Artikel menyediakan label yang diperlukan.
-- Identitas yang tidak dapat tertukar antarentitas
newtype ID entitas = ID Int deriving (Eq, Show)
data Pengguna
data Artikel
cariPengguna :: ID Pengguna -> String
cariPengguna (ID n) = "pengguna nomor " ++ show n
nomorID :: ID entitas -> Int
nomorID (ID n) = n
main :: IO ()
main = do
print (cariPengguna (ID 7 :: ID Pengguna))
-- print (cariPengguna (ID 7 :: ID Artikel)) -- ditolak compiler
print (nomorID (ID 7 :: ID Artikel))"pengguna nomor 7"
7Tipe data Pengguna tidak memiliki konstruktor sama sekali, sehingga tidak ada nilai Pengguna yang dapat dibangun dan nilainya tidak pernah ada pada waktu berjalan. Tipe itu hanya digunakan sebagai argumen untuk ID, yaitu mengisi parameter entitas pada tipe ID Pengguna atau ID Artikel. Baris cariPengguna (ID 7 :: ID Artikel) yang dikomentari ditolak compiler karena tipenya tidak cocok, meskipun isi Int-nya sama persis. Fungsi nomorID justru polimorfik pada parameter phantom-nya, sehingga label apa pun diterima dan angka di dalamnya tetap terbaca.
Kekuatan yang tampak istimewa ini berasal dari mekanisme biasa, yaitu pemeriksaan tipe biasa yang juga menolak pemanggilan length terhadap angka. Karena ID Pengguna dan ID Artikel berbeda pada bagian tipenya, seluruh alat yang sudah dikenal bekerja tanpa tambahan apa pun, mulai dari polimorfisme, type class, sampai pencocokan pola. Fungsi yang mengembalikan ID Pengguna tidak dapat langsung disambungkan ke fungsi yang menuntut ID Artikel, dan pesan errornya menunjuk tipe yang tidak cocok, bukan logika yang keliru.
Label status pada dokumen#
Pola yang sama menyimpan status sebuah nilai di dalam tipenya. Dokumen pada contoh berikut berada pada keadaan draf atau terbit, dan perpindahannya dinyatakan oleh signature, yaitu terbitkan menerima Dokumen Draf dan menghasilkan Dokumen Terbit.
-- Dokumen dengan label status pada parameter tipe
data Draf
data Terbit
newtype Dokumen status = Dokumen String deriving Show
terbitkan :: Dokumen Draf -> Dokumen Terbit
terbitkan (Dokumen teks) = Dokumen teks
judul :: Dokumen status -> String
judul (Dokumen teks) = teks
main :: IO ()
main = do
let draf :: Dokumen Draf
draf = Dokumen "catatan pagi"
print draf
print (terbitkan draf)
putStrLn (judul (terbitkan draf))Dokumen "catatan pagi"
Dokumen "catatan pagi"
catatan pagiSignature terbitkan :: Dokumen Draf -> Dokumen Terbit membuat aturan alur status menjadi bagian dari tipe. Dokumen terbit tidak dapat dilewatkan kepada terbitkan lagi karena tipenya sudah berbeda, sehingga operasi yang hanya sah untuk draf tidak mungkin terpanggil dua kali pada dokumen yang sama. Fungsi judul bersifat polimorfik pada statusnya, sehingga teks dokumen tetap dapat dibaca tanpa memandang keadaannya. Pada program yang lebih besar, kebutuhan semacam ini muncul pada proses persetujuan berjenjang, dan setiap jenjang memiliki operasi yang berbeda.
Label status juga dapat digabung dengan mekanisme tipe lain. Fungsi yang meneruskan dokumen berjenjang dapat menuntut label tertentu pada parameternya, sedangkan fungsi yang polimorfik pada statusnya tetap bekerja untuk semuanya, pola yang sama dengan judul. Instans type class pun dapat diberikan hanya untuk status tertentu, misalnya instans Eq yang hanya dimiliki dokumen terbit, karena membandingkan identitas dokumen yang belum selesai dianggap tidak bermakna bagi domainnya.
Biaya pada waktu berjalan#
Parameter phantom tidak menampung nilai apa pun, sehingga tidak ada yang perlu disimpan di memori. Nilai ID Pengguna dan ID Artikel sama-sama hanya satu Int pada waktu berjalan, dan pembedaannya hilang sepenuhnya setelah program dikompilasi. Seluruh pemeriksaan terjadi pada waktu kompilasi, yaitu compiler mencocokkan label pada setiap pemanggilan fungsi dan menolak yang tidak cocok. Pemeriksaan semacam ini memberi jaminan tanpa membuat program lebih lambat, dan itulah sebabnya pola ini sering digunakan untuk memperketat antarmuka yang sudah ada.
Karena labelnya tidak ada pada waktu berjalan, label tidak boleh diambil apa adanya dari data luar, misalnya angka status yang dibaca dari berkas konfigurasi. Konversi dari representasi luar menuju tipe berlabel tetap melalui fungsi yang memeriksa keabsahannya, dan di sinilah pola smart constructor berperan, sehingga label yang menempel selalu dijamin oleh program dan bukan oleh isi berkas.
Kesalahan umum#
Kesalahan pertama adalah membiarkan konstruktor terbuka. Konstruktor ID :: Int -> ID entitas dapat menghasilkan label apa pun yang diinginkan pemanggilnya, sehingga ID 7 :: ID Pengguna dan ID 7 :: ID Artikel sama-sama sah untuk dibangun. Jaminan ketertukaran hanya berguna apabila nilai diperoleh melalui pintu yang benar, sehingga label yang benar-benar kritis harus dikawal dengan menyembunyikan konstruktor seperti pada pola smart constructor.
Kesalahan kedua berupa memaksakan pola ini pada label yang sering berubah. Label status Draf dan Terbit bekerja karena perpindahannya sedikit dan pasti, sedangkan keadaan dengan belasan perpindahan membuat tipe dan fungsinya bertambah banyak dengan cepat. Apabila perpindahan antarkeadaan mulai memerlukan daftar fungsi yang panjang, keadaan tersebut lebih cocok dinyatakan sebagai data biasa yang menyimpan statusnya, bukan sebagai parameter tipe.
Ringkasan#
- Phantom type adalah parameter tipe yang tidak muncul pada nilai, tetapi membuat dua pengisian berbeda menjadi dua tipe yang tidak tertukar.
- Tipe kosong seperti
data Penggunaberfungsi sebagai label, dan pemeriksaannya seluruhnya terjadi pada waktu kompilasi. - Signature seperti
Dokumen Draf -> Dokumen Terbitmenjadikan aturan perpindahan keadaan bagian dari tipe. - Biaya pada waktu berjalan nol karena label hilang setelah kompilasi, dan label yang kritis dikawal dengan menyembunyikan konstruktor.
Referensi#
- Phantom type — Haskell Wiki — Penjelasan pola parameter tipe tanpa wakil beserta contohnya.
- Data.Proxy — Hackage — Tipe yang seluruhnya berupa parameter tanpa wakil, contoh bawaan pola phantom.