Tipe pada Haskell bukan sekadar label yang diperiksa compiler, melainkan alat rekayasa yang memindahkan pemeriksaan dari waktu berjalan ke waktu kompilasi. Topik ini membahas dua pola utamanya, yaitu smart constructor yang menjadikan satu fungsi sebagai satu-satunya pintu masuk menuju nilai yang valid, serta validasi data di batas sistem (system boundary) tempat masukan dari luar diterima. Setelah pola-pola tersebut diterapkan, nilai tidak valid dapat ditolak lebih dahulu sebelum menyusup ke dalam program.
Nilai mentah yang lolos pemeriksaan#
Masalah yang hendak diselesaikan paling jelas terlihat pada program yang tidak menggunakan pola apa pun. Tipe berikut menampung jam piket sebagai newtype sederhana, dan fungsinya menampilkan jadwal tanpa menanyakan asal-usul nilainya.
-- Jam sebagai newtype belum menyelesaikan masalah nilai tidak valid
newtype Jam = Jam Int deriving (Eq, Show)
aturPiket :: Jam -> String
aturPiket (Jam j) = "piket jam " ++ show j ++ ".00"
main :: IO ()
main = do
print (aturPiket (Jam 7))
print (aturPiket (Jam 99)) -- lolos tanpa peringatan dari compiler"piket jam 7.00"
"piket jam 99.00"Nilai Jam 99 tidak sah karena jam hanya berjalan dari 0 sampai 23, tetapi compiler tidak memiliki cara mengetahuinya. Rentang sah adalah informasi yang berada di luar tipe, sehingga setiap fungsi yang menerima Jam harus menerima kemungkinan nilai keliru itu. Program dengan lima fungsi semacam ini memerlukan lima pemeriksaan yang tersebar, dan satu yang terlupa cukup untuk membuat jadwal rusak. Pola smart constructor menyelesaikannya dengan memindahkan pemeriksaan tersebut ke satu tempat.
Smart constructor#
Smart constructor adalah fungsi biasa yang memeriksa masukannya dan menghasilkan tipe yang dijamin valid, sedangkan konstruktor aslinya disembunyikan dari luar modul. Modul berikut mengekspor tipe Jam beserta dua fungsinya, tetapi tidak mengekspor konstruktor Jam, sehingga satu-satunya cara memperoleh nilai Jam adalah melalui buatJam.
-- Modul jadwal piket: Jam yang selalu valid
module Jadwal (Jam, buatJam, nilaiJam) where
newtype Jam = Jam Int deriving (Eq, Show)
-- Konstruktor pintar: satu-satunya pintu masuk menuju Jam yang valid
buatJam :: Int -> Maybe Jam
buatJam n
| 0 <= n && n <= 23 = Just (Jam n)
| otherwise = Nothing
nilaiJam :: Jam -> Int
nilaiJam (Jam n) = n
main :: IO ()
main = do
print (buatJam 7)
print (buatJam 24)
print (buatJam 99)Just (Jam 7)
Nothing
NothingDaftar ekspor (Jam, buatJam, nilaiJam) memuat nama tipe Jam tanpa konstruktor di dalam tanda kurung, sehingga kode di luar modul dapat menggunakan dan membandingkan Jam, tetapi tidak dapat membangunnya secara langsung. Kasus sah menghasilkan Just berisi jam yang sudah terjaga rentangnya, sedangkan 24 dan 99 menghasilkan Nothing karena melewati batas atas rentangnya. Tipe Maybe membuat kemungkinan penolakan itu terbaca dari signature, sehingga pemanggilan yang tidak menangani Nothing ditolak compiler, keadaan yang tidak pernah terjadi pada tipe mentah Jam Int.
Validasi di batas sistem#
Masukan dari luar program, misalnya argumen baris perintah atau isi berkas, datang sebagai String yang belum dijamin apa pun. Pola smart constructor diterapkan tepat di titik masuk itu, yaitu teks dibaca, divalidasi, lalu hasilnya berupa tipe yang dijamin valid atau pesan penolakan yang menjelaskan alasannya. Fungsi reads mengurai teks menjadi nilai dan menghasilkan list hasil yang dapat kosong apabila teksnya tidak dapat diurai.
-- Validasi masukan teks di batas sistem
newtype Jam = Jam Int deriving (Eq, Show)
bacaJam :: String -> Either String Jam
bacaJam teks = case reads teks of
[(n, "")] | 0 <= n && n <= 23 -> Right (Jam n)
_ -> Left ("jam tidak sah: " ++ teks)
tampilkan :: Either String Jam -> String
tampilkan (Right (Jam n)) = "piket jam " ++ show n ++ ".00"
tampilkan (Left alasan) = "ditolak: " ++ alasan
main :: IO ()
main = do
putStrLn (tampilkan (bacaJam "7"))
putStrLn (tampilkan (bacaJam "24"))
putStrLn (tampilkan (bacaJam "tujuh"))piket jam 7.00
ditolak: jam tidak sah: 24
ditolak: jam tidak sah: tujuhTipe Either String Jam menyatakan dua kemungkinan secara eksplisit, yaitu berhasil dengan nilai Jam yang valid atau gagal dengan pesan penjelasan. Pola [(n, "")] menuntut penguraian yang menghabiskan seluruh teks, sehingga masukan seperti "7abc" juga tertolak. Setelah melewati titik ini, sisa program bekerja dengan Jam yang tak perlu diperiksa lagi, karena satu-satunya jalan menuju tipe itu sudah memeriksa rentangnya. Pembagian kerja inilah inti polanya, yaitu pemeriksaan dipusatkan di tepi dan bagian dalam bekerja tanpa pemeriksaan ulang.
Penggunaan modul dari berkas lain#
Modul Jadwal digunakan dari berkas lain melalui import Jadwal, dan percobaan pemanggilan konstruktor secara langsung ditolak compiler karena Jam tidak diekspor sebagai konstruktor.
-- tidak-dijalankan
-- Pemakaian modul Jadwal dari luar
import Jadwal
main :: IO ()
main = do
let piket = fmap nilaiJam (buatJam 21)
print piket
-- Jam 25 -- ditolak compiler karena konstruktor Jam tidak diekspor
print (fmap buatJam [7, 12, 24])Berkas tersebut diletakkan di samping Jadwal.hs, sehingga eksekusinya dilakukan dari direktori yang memuat kedua berkas agar modulnya ditemukan.
# jalankan program pemakai dari direktori yang memuat Jadwal.hs
runghc uji-jadwal.hsJust 21
[Just (Jam 7),Just (Jam 12),Nothing]Keluaran pertama memperlihatkan Just 21, yaitu nilai di dalam Jam yang diambil melalui fmap nilaiJam, sedangkan komentar pada program menandai Jam 25 yang ditolak compiler sebelum program sempat dijalankan. Keluaran kedua memetakan tiga angka dengan buatJam dan memperlihatkan dua nilai sah beserta satu Nothing.
Polanya tidak berhenti pada rentang angka. Fungsi buatJam dapat diperluas menjadi fungsi yang memeriksa bentuk teks, keunikan elemen, atau hubungan antar-field, dan bentuk kembaliannya tetap Maybe Jam atau Either String Jam, sehingga pemanggil tidak menanggung perubahan apa pun. Semakin banyak aturan domain yang ditaruh di dalam smart constructor, semakin sedikit keputusan yang ditinggalkan kepada pengguna modul, keadaan yang menjadikan tipe sebagai tempat kebijakan program.
Kesalahan umum#
Kesalahan pertama adalah mengekspor konstruktor, misalnya menulis daftar ekspor Jam (..) yang memuat konstruktor juga. Ekspor semacam itu membuka pintu kedua menuju Jam 99, sehingga jaminan validitasnya hilang meskipun smart constructor tetap ada. Perbaikannya berupa mengekspor nama tipe tanpa daftar konstruktor, sebagaimana Jam pada modul di atas.
Kesalahan kedua berupa memvalidasi terlambat, yaitu membiarkan Int mentah mengalir ke dalam program lalu memeriksa rentangnya di beberapa fungsi yang terpisah. Setiap titik pemeriksaan yang tersebar adalah peluang untuk terlupa, sedangkan validasi di batas sistem menyisakan satu titik yang harus dijaga. Ketika pola ini digabungkan dengan tipe data aljabar yang sudah dibahas sebelumnya, seluruh keadaan tidak sah dapat dinyatakan sebagai nilai Nothing atau Left yang wajib ditangani pemanggilnya.
Ringkasan#
- Smart constructor adalah satu fungsi yang memvalidasi masukan dan menjadi satu-satunya pintu masuk menuju nilai yang dijamin valid.
- Daftar ekspor
Jam, buatJam, nilaiJammenyembunyikan konstruktor, sehingga nilai tidak valid tidak dapat dibangun dari luar modul. - Validasi diterapkan di batas sistem melalui
Either String Jam, sehingga masukan mentah ditolak sebelum mengalir ke bagian dalam program. - Tipe hasil seperti
MaybedanEithermemindahkan penanganan kegagalan ke compiler, bukan ke ingatan pembaca kode.
Referensi#
- Data.Maybe — Hackage
— Dokumentasi
Maybebeserta kombinator yang menyertainya. - Data.Either — Hackage
— Dokumentasi
Eitheruntuk kegagalan yang membawa pesan. - Smart constructors — Haskell Wiki — Penjelasan pola konstruktor pintar beserta variasi penerapannya.