Membaca tipe

Membaca panah fungsi, aplikasi, dan disiplin :t sebagai dokumen desain.

Daftar isi

Setiap fungsi dalam Haskell menyertakan tipe yang dapat dibaca seperti kalimat pendek, yaitu deretan masukan yang berakhir pada satu hasil. Topik ini membahas cara membaca anotasi tipe, cara menanyakan tipe melalui GHCi, dan cara memandang tipe sebagai kontrak desain program. Setelah membacanya, tipe fungsi dapat diterjemahkan menjadi bentuk pemanggilan yang benar, dan ketidaksesuaian tipe dapat dideteksi saat membaca kode, sebelum program sempat berjalan.

Anotasi tipe dan panahnya#

Anotasi tipe (type annotation) ditulis dengan tanda ::, yang dibaca “bertipe”. Baris durasiKeTeks :: Int -> String menyatakan bahwa durasiKeTeks menerima satu Int dan menghasilkan satu String. Panah -> dibaca “menghasilkan”, sehingga membaca tipe dari kiri ke kanan sama dengan mengikuti alur data. Tipe paling kanan selalu menjadi hasil, sedangkan tipe-tipe sebelumnya adalah masukan yang berurutan.

Program berikut memuat satu fungsi dengan satu masukan dan satu hasil. Fungsi durasiKeTeks menerima jumlah menit dan mengubahnya menjadi teks durasi. Fungsi div dan mod mengoperasikan bilangan bulat, show mengubah Int menjadi String, dan ++ menyambung dua String. Setiap bagian memiliki tipe yang dapat ditanyakan, sehingga penyambungannya dapat diperiksa bagian demi bagian.

haskell durasi.hs
-- Anotasi tipe: janji fungsi yang diperiksa compiler sebelum program berjalan
durasiKeTeks :: Int -> String
durasiKeTeks menit = show (menit `div` 60) ++ " jam " ++ show (menit `mod` 60) ++ " menit"

main :: IO ()
main = do
  putStrLn (durasiKeTeks 90)
  putStrLn (durasiKeTeks 125)
text Keluaran
1 jam 30 menit
2 jam 5 menit

Ketika dipanggil dengan durasiKeTeks 125, nilai 125 menggantikan parameter menit yang bertipe Int, sesuai janji pada anotasinya. Apabila pemanggilan menyertakan nilai yang tidak cocok, misalnya durasiKeTeks 1.5, compiler menolak program karena 1.5 bukan Int. Penolakan terjadi sebelum program berjalan, sehingga anotasi tipe berperan sebagai kontrak yang diperiksa compiler dan sekaligus sebagai dokumentasi yang tidak dapat kedaluwarsa.

Menanyakan tipe dengan :t#

GHCi menyediakan perintah :t untuk menampilkan tipe sebuah ekspresi tanpa mengeksekusinya sebagai program utuh. Bentuk noninteraktifnya menggunakan opsi -e, sehingga perintah dapat dijalankan langsung dari terminal tanpa memasuki sesi interaktif. Pola kerja ini berguna ketika perilaku sebuah fungsi bawaan belum dikenal, karena tipe sering kali sudah menerangkan cara penggunaannya. Perintah berikut menanyakan tipe fungsi predikat not, kemudian mengevaluasi fungsi tersebut untuk membandingkan janji tipenya dengan keluaran nyatanya.

bash
# menanyakan tipe fungsi predikat
ghci -e ':t not'
# mengevaluasi fungsi sesuai tipe yang dibaca
ghc -e 'not True'
text Keluaran
not :: Bool -> Bool
False

Tipe not :: Bool -> Bool menyatakan satu masukan Bool dan satu keluaran Bool, sesuai keluaran False untuk masukan True. Pemeriksaan seperti ini tidak mengubah apa pun dalam program karena :t hanya membaca tipe. Kebiasaan menanyakan tipe sebelum menulis kode membuat pemanggilan yang salah tertangkap di awal, bukan setelah program dikompilasi atau dijalankan.

Membaca panah bertingkat#

Anotasi take :: Int -> [a] -> [a] memuat dua panah sehingga tampak seperti tiga tipe yang berbaris. Pembacaannya dimulai dari kiri, yaitu masukan pertama bertipe Int, masukan kedua bertipe [a], dan hasil terakhir bertipe [a]. Aturannya konsisten, yaitu tipe paling kanan selalu menjadi hasil, sedangkan tipe-tipe di depannya adalah masukan yang harus disediakan berurutan.

Pemanggilan take 3 [9, 8, 7, 6] menerapkan argumen dari kiri ke kanan. Fungsi menerima 3 lebih dahulu, kemudian menerima list, lalu menghasilkan [9,8,7]. Huruf kecil a adalah variabel tipe (type variable), yaitu tempat kosong yang menyesuaikan diri dengan tipe pemanggil. Variabel tipe dibahas sebagai mekanisme tersendiri pada bagian selanjutnya, dan untuk saat ini cukup dibaca sebagai “elemen dengan tipe apa pun”.

Panah yang bertingkat juga menjadi alur berpikir saat merancang fungsi baru. Fungsi yang memotong teks menurut indeks awal, indeks akhir, dan teks sumbernya dapat dirancang dengan menuliskan tipenya terlebih dahulu, misalnya potong :: Int -> Int -> [Char] -> [Char], dan baris itu sudah memuat tiga keputusan desain, yaitu masukan yang dibutuhkan, urutannya, serta bentuk hasilnya. Implementasinya menyusul setelah kontraknya disepakati. Kebiasaan ini menjadi inti pengerjaan proyek pada jalur ini.

Tipe sebagai kontrak desain#

Anotasi tipe menyatakan janji, bukan isi. Dua fungsi bertipe sama boleh memiliki implementasi yang berbeda, dan pemanggil yang membaca tipenya tidak perlu mengetahui detail keduanya. Disiplin desain yang lazim dalam Haskell menuliskan anotasi tipe terlebih dahulu, kemudian mengisi isinya, sehingga tipe menjadi dokumen desain yang diperiksa compiler.

Program berikut memuat dua fungsi bertipe sama, yaitu Int -> String, dengan isi yang berbeda.

haskell label-nilai.hs
-- Dua fungsi bertipe sama, isi berbeda: tipe menyatakan kontrak, bukan isi
labelLulus :: Int -> String
labelLulus n
  | n >= 75   = "lulus"
  | otherwise = "belum lulus"

ringkasNilai :: Int -> String
ringkasNilai n = "nilai " ++ show n

main :: IO ()
main = do
  putStrLn (labelLulus 80)
  putStrLn (labelLulus 60)
  putStrLn (ringkasNilai 80)
text Keluaran
lulus
belum lulus
nilai 80

Fungsi labelLulus dan ringkasNilai sama-sama menerima Int dan menghasilkan String, tetapi isinya berbeda. Pemanggil kedua fungsi bergantung hanya pada janji tipenya, sehingga isi masing-masing fungsi boleh diubah tanpa menyeret perubahan pada pemanggil, selama makna hasilnya tetap. Kebiasaan menulis tipe lebih dahulu juga membuat fungsi yang sedang dirancang dapat diperiksa kesesuaiannya melalui :t sebelum isinya selesai ditulis.

Ringkasan#

  • Tanda :: dibaca “bertipe”, dan deretan panah dibaca dari kiri ke kanan dengan tipe paling kanan sebagai hasil.
  • Perintah ghci -e ':t ekspresi' menampilkan tipe ekspresi tanpa menjalankan program utuh.
  • Tipe dengan beberapa panah, seperti Int -> [a] -> [a], menyatakan masukan berurutan dan satu hasil.
  • Huruf kecil pada tipe, seperti a, adalah variabel tipe yang menyesuaikan diri dengan tipe pemanggil.
  • Anotasi tipe berperan sebagai kontrak yang diperiksa compiler sebelum program berjalan.

Referensi#