Kebiasaan yang membedakan Haskell dari kebanyakan bahasa lain terletak pada urutan penulisan, yaitu tipe dan signature modul disusun lebih dahulu sebelum satu baris implementasi pun ditulis. Urutan ini bukan formalitas, karena compiler segera memeriksa kontrak tersebut, program pemakainya sudah dapat ditulis di atasnya, dan sisa pekerjaan berubah menjadi pengisian yang diarahkan oleh tipe. Topik ini memandu langkah itu pada satu modul kecil, dari kontrak, pengguna, sampai implementasi yang mengisinya.
Kontrak sebagai berkas yang dapat dikompilasi#
Berkas rancangan berisi nama fungsi beserta tipenya, sedangkan isinya sementara berupa error yang menyatakan bahwa pekerjaan belum dilakukan. Bentuk semacam ini tetap sah bagi compiler, sehingga kecocokan tipe diuji sejak hari pertama.
-- tidak-dijalankan
-- Rancangan awal: signature diserahkan dahulu, isi menyusul
rataRata :: [Int] -> Int
rataRata xs = error "belum diimplementasi"
main :: IO ()
main = print (rataRata [4, 6])# jalankan rancangan awal dari direktori berkasnya
runghc kontrak-awal.hskontrak-awal.hs: belum diimplementasi
CallStack (from HasCallStack):
error, called at kontrak-awal.hs:4:15 in main:MainProgram di atas lolos pemeriksaan tipe dan berjalan, kemudian berhenti tepat pada bagian yang memang belum ada, yaitu pemanggilan rataRata. Pesan errornya menunjuk baris error yang sengaja disisipkan pada definisi tersebut, sehingga keadaan belum selesai terbaca dari keluaran dan bukan dari ingatan. Perhatikan bahwa signature rataRata :: [Int] -> Int sudah mengunci dua hal, yaitu masukannya berupa list bilangan bulat dan hasilnya satu bilangan bulat, dua keputusan desain yang paling sering berubah apabila ditebak belakangan.
Keuntungan lainnya muncul saat berkas tersebut dikompilasi sendiri. Pemeriksaan tipe berjalan atas seluruh kontrak tanpa menunggu satu implementasi pun selesai, sehingga kekeliruan pada signature, seperti tipe masukan yang tidak sesuai rencana, tertangkap pada hari pertama. Menunda penulisan tipe sampai implementasi selesai berarti pemeriksaan itu ikut tertunda, dan keputusan yang keliru baru terlihat pada akhir pengerjaan, ketika perbaikannya jauh lebih mahal.
Modul dengan kontraknya#
Langkah berikutnya adalah menyusun fungsi-fungsi tersebut menjadi satu modul dengan daftar ekspornya, dan signature pada daftar itulah kontrak yang dijanjikan modul kepada pemakainya.
-- tidak-dijalankan
-- Modul Statistik: kontrak beserta implementasinya
module Statistik (rataRata, rentang) where
rataRata :: [Int] -> Int
rataRata xs = sum xs `div` length xs
rentang :: [Int] -> Int
rentang xs = maximum xs - minimum xsModul ini tidak memuat main karena perannya sebagai pustaka, sehingga eksekusinya dikerjakan melalui program pengguna yang mengimpornya. Daftar ekspor (rataRata, rentang) menyatakan seluruh kemampuan modul dalam satu baris, kebiasaan yang membuat pembaca menilai modul dari kontraknya tanpa membaca isi fungsinya lebih dahulu.
Daftar ekspor itu juga menjadi tempat menimbang pertumbuhan modul. Fungsi baru yang belum stabil tidak masuk daftar sampai bentuknya pasti, kebiasaan yang mencegah pengguna lain bergantung pada kontrak yang belum selesai dipikirkan. Antarmuka yang dijaga tetap kecil lebih mudah dipertahankan daripada antarmuka yang tumbuh mengikuti isi berkasnya.
Program pengguna di atas kontrak#
Program pengguna dapat ditulis sebelum implementasinya selesai, karena yang dibutuhkannya hanyalah tipe. Berkas berikut mengimpor Statistik dan memanggil kedua fungsinya, dan program tersebut berjalan penuh setelah implementasi terisi.
-- tidak-dijalankan
-- Program pemakai modul Statistik
import Statistik
main :: IO ()
main = do
print (rataRata [7, 8, 9])
print (rentang [2, 5, 11])Berkas modul dan program pemakainya berada di satu direktori yang sama, sehingga eksekusinya dilakukan dari direktori tersebut agar modulnya ditemukan.
# jalankan program pemakai dari direktori yang memuat Statistik.hs
runghc uji-statistik.hs8
9Keluaran memperlihatkan kedua fungsi bekerja sesuai kontraknya, yaitu rataRata [7, 8, 9] menghasilkan 8 dan rentang [2, 5, 11] menghasilkan 9. Pada saat kontraknya baru selesai dan implementasinya masih kosong, program yang sama ini sudah dapat ditulis lebih dahulu tanpa perubahan apa pun, karena yang dibutuhkannya hanyalah tipe. Inilah manfaat praktis urutan tersebut, yaitu antarmuka dan program pemakainya dikerjakan lebih dahulu, sedangkan isi fungsi menyusul tanpa menyentuh bagian lain.
Pemisahan antara kontrak dan isi juga membagi pekerjaan tanpa menimbulkan kekacauan. Satu orang dapat mengisi implementasi Statistik sementara orang lain menulis program pengguna atau ujinya, dan satu-satunya kesepakatan yang dibutuhkan berupa teks signature yang sudah tertulis. Ketika implementasinya keliru, program pengguna tidak berubah, karena yang dijanjikan tipe tetap sama dan hanya isinya yang diperbaiki.
Kesalahan umum#
Kesalahan pertama adalah membalik urutannya, yaitu menulis implementasi lalu menyimpulkan tipenya dari kode yang sudah ada. Tipe yang disusun belakangan cenderung mengikuti bentuk implementasi pertama, keadaan yang mengunci desain pada pilihan yang belum diperiksa, sedangkan tipe yang ditulis dahulu menjadi alasan bagi implementasinya. Ketika implementasi terasa bertentangan dengan tipenya, pertanyaan yang tepat adalah apakah tipenya perlu disesuaikan, bukan membiarkan keduanya begitu saja.
Kesalahan kedua berupa kontrak yang terlalu spesifik, misalnya rataRata :: [Int] -> Int yang membatasi masukannya pada bilangan bulat padahal rata-rata bermakna untuk angka apa pun. Kontrak yang lebih umum seperti tipe polimorfik atau kelas tipe dapat menunggu sampai kebutuhannya nyata, tetapi keputusannya perlu disadari pada saat menulis signature, bukan ditemukan setelah pengguna lain bergantung pada bentuk yang sempit. Kesalahan ketiga berupa membiarkan error "belum diimplementasi" bertahan sampai program dikirim, keadaan yang dicegah oleh properti dan pengujian yang menuntut setiap kontrak terisi.
Ringkasan#
- Rancangan berupa tipe dan signature ditulis dahulu, dan bentuk
error "belum diimplementasi"membuatnya tetap dapat dikompilasi. - Pesan error yang muncul saat evaluasi dipaksa menunjuk definisi yang belum diisi, sehingga keadaan belum selesai terbaca dari keluaran program.
- Program pengguna ditulis di atas kontrak tanpa menunggu implementasi, dan eksekusinya dilakukan dari direktori yang memuat modulnya.
- Implementasi yang mengisi kontrak tidak mengubah bagian lain, keadaan yang menjadi manfaat utama urutan tipe lebih dahulu.
Referensi#
- GHC Users Guide: Separate compilation — haskell.org — Cara GHC menemukan modul yang diimpor dari direktori yang sama.
- Haskell 2010 Language Report, Bab 5 — haskell.org — Spesifikasi deklarasi modul dan daftar ekspor.
- Control.Exception — Hackage
— Dokumentasi exception yang menyertai
errorbesertaCallStackpada pesannya.