Program Haskell disusun dari bahan yang sama sejak awal hingga akhir, yaitu ekspresi (expression) yang menghasilkan nilai. Topik ini membahas model tersebut, aturan penulisan definisi (definition) tingkat atas dan definisi lokal, serta perbandingan dua bentuk pengikatan yang disediakan Haskell, yaitu where dan let. Setelah membacanya, program sederhana dapat disusun dengan definisi yang rapi, dan pilihan antara where dan let dapat ditentukan dengan alasan yang jelas.
Segala sesuatu adalah ekspresi#
Ekspresi adalah bagian program yang menghasilkan nilai. Angka 7, penjumlahan 3 + 4, pemanggilan fungsi, hingga percabangan if-then-else semuanya merupakan ekspresi karena menghasilkan nilai. Model ini berbeda dari bahasa imperatif yang memisahkan ekspresi dari pernyataan (statement), yaitu instruksi yang dijalankan tanpa menghasilkan nilai. Dalam Haskell tidak ada pernyataan semacam itu, sehingga sebuah program dapat dibaca sebagai rangkaian penghitungan nilai dari masukan hingga keluaran.
Karena setiap ekspresi menghasilkan nilai, percabangan pun menghasilkan nilai. Program berikut menyimpan hasil if-then-else ke dalam sebuah definisi, sesuatu yang tidak mungkin dilakukan pada pernyataan if bahasa imperatif. Kedua cabangnya juga wajib menghasilkan nilai bertipe sama karena hasil akhirnya adalah nilai dari salah satu cabang.
-- Setiap bagian program adalah ekspresi yang menghasilkan nilai
rataRata :: Double
rataRata = (7 + 8 + 9) / 3
hasil :: String
hasil = if rataRata >= 8 then "lulus" else "berlatih"
main :: IO ()
main = do
print rataRata
putStrLn hasil8.0
lulusKedua baris keluaran memperlihatkan dua nilai yang dihitung dari ekspresi. Sifat ekspresional ini dapat dicoba cepat melalui perintah ghc -e, yaitu opsi compiler untuk mengevaluasi ekspresi langsung tanpa membuat berkas program.
# dua ekspresi dievaluasi langsung tanpa membuat berkas program
ghc -e '1 + 2 * 3'
ghc -e 'if 5 > 3 then "benar" else "salah"'7
"benar"Definisi tingkat atas#
Definisi mengikat satu nama ke satu nilai. Pada tingkat atas, yaitu bagian berkas program yang tidak berada di dalam definisi mana pun, definisi ditulis sebagai nama = ekspresi tanpa kata kunci. Anotasi tipe di atasnya seperti jumlahHari :: Int bersifat pilihan, tetapi lazim ditulis karena berfungsi sebagai dokumentasi yang diperiksa compiler. Nilai yang terikat tidak pernah berubah, sehingga pembacaan nama yang sama di mana pun menghasilkan nilai yang sama.
Program berikut menggunakan beberapa definisi tingkat atas yang saling merujuk.
-- Definisi tingkat atas mengikat satu nama ke satu nilai
namaKegiatan :: String
namaKegiatan = "jadwal piket kelas"
jumlahHari :: Int
jumlahHari = 6
jamPerHari :: Double
jamPerHari = 2.5
totalJam :: Double
totalJam = fromIntegral jumlahHari * jamPerHari
main :: IO ()
main = do
putStrLn namaKegiatan
print jumlahHari
print totalJamjadwal piket kelas
6
15.0Definisi totalJam menunjukkan bahwa definisi boleh merujuk definisi lain. Fungsi fromIntegral mengubah nilai Int menjadi Double agar perkaliannya dengan jamPerHari sah, sebab konversi antar tipe tidak dilakukan secara diam-diam. Urutan penulisan definisi tidak menentukan hasil, sehingga totalJam boleh ditulis sebelum definisi yang dirujuknya.
Anotasi tipe juga berperan sebagai alat berpikir sebelum tubuh definisi ditulis, karena satu baris tersebut merangkum bentuk masukan dan keluaran. Apabila anotasi dan tubuh tidak cocok, compiler menunjuk ketidakcocokannya dan program tidak jadi dibangun. Anotasi pada konstanta seperti jumlahHari :: Int memang terlihat jelas tanpa bantuan, tetapi kebiasaan menulisnya membangun pembiasaan yang berguna untuk tipe yang lebih rumit.
Definisi lokal dengan where#
Tidak semua nilai pendukung perlu menjadi definisi tingkat atas. Kata kunci where menyediakan definisi lokal yang hanya terlihat oleh definisi yang dilayaninya, sehingga nama pembantu tidak bocor ke luar. Program berikut menggunakan where untuk dua keperluan, yaitu nilai-nilai persamaan kuadrat dan konversi tinggi badan ke meter.
-- Definisi lokal where menaruh nilai pendukung di bawah tubuh definisi
akarPositif :: Double
akarPositif = (-b + sqrt diskriminan) / (2 * a)
where
a = 1.0
b = -3.0
c = 2.0
diskriminan = b * b - 4 * a * c
-- Definisi lokal where bebas menggunakan parameter fungsi
indeksMassa :: Double -> Double -> Double
indeksMassa beratKg tinggiCm = beratKg / (tinggiMeter * tinggiMeter)
where
tinggiMeter = tinggiCm / 100
main :: IO ()
main = do
print akarPositif
print (indeksMassa 80 200)2.0
20.0Definisi lokal tinggiMeter menggunakan parameter tinggiCm dengan bebas karena berada dalam lingkup fungsi indeksMassa. Satu hal yang wajib dijaga adalah indentasi, sebab Haskell menentukan batas blok dari kolom, bukan dari tanda kurung. Klausa where harus ditulis lebih menjorok daripada awal persamaan yang dilayaninya, dan seluruh definisi di dalamnya harus sejajar pada satu kolom. Contoh berikut ditolak compiler karena where berada di kolom yang sama dengan awal persamaannya.
-- tidak-dijalankan
akarPositif :: Double
akarPositif = (-b + sqrt diskriminan) / (2 * a)
where
a = 1.0
b = -3.0
c = 2.0
diskriminan = b * b - 4 * a * cCompiler menolak contoh tersebut dengan pesan parse error. Perbaikannya sederhana, yaitu menjorokkan where satu tingkat dari persamaannya sebagaimana program yang benar di atas, lalu menyelaraskan seluruh definisi lokalnya.
Ekspresi let in#
Bentuk pengikatan kedua adalah let ... in, yang mengikat nama di dalam satu ekspresi. Strukturnya let pengikatan in ekspresi, dan nama yang diikat hanya terlihat pada ekspresi setelah in. Karena berupa ekspresi penuh, bentuk ini dapat ditempatkan di mana pun ekspresi diperbolehkan, termasuk di dalam penjumlahan maupun argumen fungsi.
-- let mengikat nama lokal di dalam satu ekspresi
volumKubus :: Int
volumKubus =
let sisi = 4
in sisi * sisi * sisi
main :: IO ()
main = do
let pengali = 10
print volumKubus
print (volumKubus * pengali)64
640Program di atas memperlihatkan dua bentuk penggunaan. Blok let ... in mengikat sisi untuk tubuh volumKubus, sedangkan baris let pengali = 10 di dalam main adalah varian blok do yang mengikat nama untuk sisa blok tersebut tanpa kata kunci in. Keduanya menyelesaikan kebutuhan yang sama dengan where, yaitu memberi nama pada nilai antara, dengan lingkup yang lebih ketat.
Memilih where atau let#
Kedua bentuk sering dapat dipertukarkan, sehingga pilihannya lebih ditentukan oleh kejelasan daripada keharusan. Tabel berikut merangkum perbedaan yang menentukan.
| Aspek | where | let ... in |
|---|---|---|
| Bentuk | klausa milik satu definisi | ekspresi penuh |
| Letak | di bawah tubuh definisi | di mana pun ekspresi sah |
| Lingkup nama | seluruh tubuh definisi | mulai in hingga akhir ekspresi |
| Penggunaan lazim | nilai pembantu fungsi, termasuk bagi guard | pengikatan sesaat dan pengikatan di blok do |
Aturan praktisnya, gunakan where apabila definisi lokal melayani satu fungsi dan perlu terlihat oleh seluruh tubuhnya, terutama ketika fungsi tersebut memiliki banyak cabang. Gunakan let apabila pengikatan hanya berlaku pada satu ekspresi atau apabila kode berada di dalam blok do. Konsistensi pilihan di dalam satu proyek lebih penting daripada preferensi pribadi, karena pembaca mengandalkan pola yang tetap.
Ringkasan#
- Hampir semua bagian program Haskell adalah ekspresi yang menghasilkan nilai, termasuk percabangan
if-then-else. - Definisi mengikat satu nama ke satu nilai yang tidak berubah, dengan anotasi tipe pilihan yang berperan sebagai dokumentasi yang diperiksa compiler.
wheremenyediakan definisi lokal untuk satu definisi dan menuntut indentasi yang benar karena batas blok ditentukan dari kolom.let ... inadalah ekspresi pengikatan yang lingkupnya sebatas ekspresi setelahin, sedangkan bentukletdi blokdomengikat nama untuk sisa blok.
Referensi#
- Haskell 2010 Language Report, Bab 4 — haskell.org
— Spesifikasi resmi deklarasi dan pengikatan, termasuk klausa
wheredan ekspresilet. - GHC User’s Guide — GHC — Panduan resmi compiler GHC yang menerangkan aturan layout dan pemeriksaan tipe.
- base package — Hackage
— Dokumentasi library standar yang memuat fungsi dasar seperti
fromIntegral.