Dari Nol ke MonadEkspektasi dan SyaratEkspektasiTarget PembacaCode EditorPermulaanGHC dan GHCiGHCi promptPrelude
Menyimpan definisi ke dalam fileMengubah isi file dan :reload
Pesan ErrorTypes & FunctionsTypesClassesLebih lanjut tentang Types dan ClassesFunctionsBind dan Usecase ... of ...
Latihan#1Tipe DasarBoolean TypeLatihan#2Types & Functions, bagian 2Tipe-tipe sub-ekspresiLantas, apa tipe dari angka tersebut?Constrained polymorphismKelas Tipe Ord
Dengan catatan ini, pengguna pemula bahasa Haskell bisa memahami konsep dasar sampai dengan konsep Monad.
Meskipun catatan ini ditargetkan untuk level Pemula di Haskell, namun diharapkan pembaca sebelumnya telah berpengalaman di bahasa lain (entah bahasa tersebut menggunakan paradigma Prosedural atau OOP).
Pada materi awal ini, kita akan mendemonstrasikan bagaimana caranya menggunakan tools dasar dari Haskell, sehingga kita nantinya bisa mengikuti keseluruhan materi yang akan tertulis di sini.
Anda memiliki 3 opsi untuk menggunakan Haskell:
Menggunakan text editor berbasis CLI dan terminal (seperti tmux + vim),
Menggunakan code editor lainnya (seperti Visual Studio),
Menggunakan Repl.it
Di catatan ini, kita tidak lagi akan membahas tentang text editor apa yang akan kita gunakan. Anda bebas menggunakan apa yang bisa Anda gunakan. Hal ini berlaku apabila anda memilih salah satu dari dua opsi awal. Dan di dua opsi awal itu pula, diasumsikan bahwa code editor dan tools penunjang lainnya telah terinstall di komputer Anda. Namun sebagai alternatifnya, di opsi ke-3, Anda bisa menggunakan Repl.it melalui browser yang Anda gunakan.
GHC sendiri merupakan singkatan dari 'Glasgow Haskell Compiler'. Ia adalah compiler untuk Haskell. Sedangkan 'i' yang ada pada 'GHCi' adalah 'interactive'. Sehingga, bisa dikatakan bahwa GHCi adalah antarmuka interaktif dari compiler Haskell.
GHCi sendiri terkadang disebut sebagai REPL (Read-eval-print loop), yang mana REPL ini adalah tools yang memiliki kemampuan untuk mengevaluasi ekspresi yang diketikkan pada prompt.
Nantinya juga akan ada tulisan mengenai GHCi secara khusus. (---)
Di GHCi, kita bisa melakukan hal berikut:
Mendefinisikan variabel. Misal, kita mendefinisikan x
sebagai 43
.
ghci> x = 43
Mengevaluasi ekspresi. Jika kita meminta GHCi untuk menghitung nilai dari
x - 33 + 1 * 2
, maka ia akan memberitahukan kepada kita bahwa nilainya adalah 12
.
ghci> x - 33 + 1 * 2
12
Menjalankan fungsi. Semisal, kita ingin mencetak string "Halo" ke terminal, kita bisa menggunakan fungsi putStrLn
.
ghci> putStrLn "Halo"
Halo
Prelude
Sejauh ini, kita sudah menggunakan tiga fungsi, yaitu tanda tambah (+
), tanda kurang (-
) dan putStrLn
. Dan kita tidak perlu mengimport apapun untuk menggunakan ketiga fungsi tersebut. Ini dikarenakan tiga fungsi tersebut sudah ada di dalam standard library, yang mana ia adalah sebuah module yang disebut sebagai Prelude
, yang mana ia secara default telah terimport. Jika saja kita anggap bahwa Prelude
tidak secara default terimport, maka kita mesti menuliskan,
ghci> import Prelude
Kita bisa menuliskannya seperti itu, namun kita tidak perlu melakukannya. Karena sebenarnya Prelude
telah terimport secara otomatis.
Di materi selanjutnya, akan dijelaskan secara singkat mengenai apa itu module dan apa yang dimaksud dengan mengimport module.
Di GHCi, kita tidak bisa menyimpan definisi-definisi yang telah kita tuliskan. Dan juga, prompt bukanlah hal yang tepat untuk menuliskan ekspresi yang lebih panjang, terlebih apabila ekspresinya perlu dituliskan lebih dari satu baris. Untuk hal ini, kita memerlukan text editor.
Buatlah sebuah file bernama main.hs
. .hs
adalah file extension untuk source code dari Haskell. Di dalam file ini, kita bisa menuliskan definisi-definisi yang ingin kita simpan. Contohnya:
xxxxxxxxxx
31x = 50
2y = x + 10
3main = putStrLn "Halo"
Kali ini, kita mendefinisikan x
sebagai 50
, menambahkan variabel y
untuk mendefinisikan sebuah bilangan yang mana nilainya adalah x + 10
, dan juga menambahkan putStrLn "Halo"
yang mana kita tempatkan ke variabel bernama main
.
Kita bisa menuliskan definisi-definisi tersebut di dalam text editor, namun kita tidak bisa mengevaluasi ekspresi atau menjalankan fungsi yang dituliskan di file tersebut. Untuk hal itu, kita kembali ke GHCi. Setelah kita menyimpan file tersebut, file tersebut akan kita load ke GHCi. Gunakan perintah :load
atau :l
dan tuliskan nama file yang ingin kita load ke GHCi, yang mana adalah main.hs
.
ghci> :l main.hs
[1 of 1] Compiling Main ( main.hs, interpreted )
Ok, one module loaded.
GHCi memberi tahu kita bahwa file tersebut sudah di-load. Sekarang kita bisa menggunakan definisi-definisi yang berada di dalam file tersebut melalui GHCi. Sebagai contoh, kita akan melihat nilai dari x
, y
dan main
.
ghci> x
50
ghci> y
60
ghci> main
Halo
:reload
Ubah isi file dari main.hs
. Kita ubah x
menjadi 150
dan string yang ada di variabel main
menjadi "Halo semuanya!!!" Sehingga source codenya menjadi,
xxxxxxxxxx
31x = 150
2y = x + 10
3main = putStrLn "Halo semuanya!!!"
Simpan kembali file tersebut, dan lakukan reload ke GHCi menggunakan perintah :reload
atau :r
.
ghci> :r
[1 of 1] Compiling Main ( main.hs, interpreted )
Ok, one module loaded.
Lalu kita lihat kembali nilai dari x
, y
dan main
.
ghci> x
150
ghci> y
160
ghci> main
Halo semuanya!!
Terkadang kita melakukan kesalahan saat menulis sesuatu. Anggaplah kita tidak sengaja melakukan typo. Saat kita ingin menulis putStrLn
, kita justru menuliskannya putstrln
. Di skenario ini, kita ubah putStrLn
menjadi putstrln
, sehingga menjadi,
xxxxxxxxxx
31x = 150
2y = x + 10
3main = putstrln "Halo semuanya!!!"
Lalu reload. Maka Anda akan membaca pesan error seperti berikut.
ghci> :r
[1 of 1] Compiling Main ( main.hs, interpreted )
main.hs:3:8: error:
• Variable not in scope: putstrln :: String -> t
• Perhaps you meant ‘putStrLn’ (imported from Prelude)
|
3 | main = putstrln "Halo semuanya!!!"
| ^^^^^^^^
Failed, no modules loaded.
Bacalah pesan error tersebut secara perlahan-lahan, karena pesan error umumnya memiliki banyak informasi. Coba kita jabarkan pesan error tersebut.
main.hs:3
Ini memberitahukan kita bahwa errornya berada di file main.hs
pada baris ke-3
Variable not in scope: putstrln
Ini memberitahukan kita bahwa fungsi putstrln
tidak ditemukan
Perhaps you meant ‘putStrLn’
GHCi merekomendasikan putStrLn
sebagai fungsi yang mungkin ingin kita gunakan. Dari sini juga kita mengetahui bahwa penulisan nama fungsi di Haskell itu bersifat case-sensitive.
Pesan error itu juga menunjukkan dimana letak kesalahan dari file tersebut, yang mana kesalahannya ditandai dengan underline berupa simbol ^
secara berulang, dan ia mengingatkan kita kembali bahwa errornya berada di baris ke-3.
3 | main = putstrln "Halo semuanya!!!"
| ^^^^^^^^
Untuk memperbaikinya, ubah kembali putstrln
menjadi putStrLn
. Simpan, lalu reload. Maka ia akan kembali lagi menjadi normal.
Kita akan mulai membahas sedikit tentang grammar, atau dengan kata lain, struktur yang paling penting dan mendasar di bahasa Haskell. Ini artinya kita mulai membahas mengenai konsep tentang apa itu types dan apa itu functions, dan mempelajari hubungan di antara keduanya.
Kita bisa memikirkan types sebagaimana sets, atau himpunan yang pernah kita pelajari sewaktu SMP. Di saat kita berfikir tentang bilangan/angka, “bilangan” itu sendiri sebenarnya bukanlah sebuah himpunan. Karena ada berbagai macam himpunan bilangan yang berguna untuk beberapa hal yang berbeda. Misalnya:
Integer (bilangan bulat), yang mana beranggotakan bilangan bulat negatif, nol dan bilangan bulat positif.
Natural (bilangan asli), yang mana hanya beranggotakan bilangan bulat positif.
Bilangan Rasional, yang mana beranggotakan semua bilangan bulat dan juga semua bilangan pecahan.
Beberapa orang berpendapat bahwa nol adalah bagian dari bilangan rasional. Beberapa lainnya berpendapat sebaliknya. Natural
type pada Haskell memasukkan nol sebagai bagian dari type tersebut.
Bisa kita lihat, bahwa bilangan rasional merepresentasikan perluasan konsep tentang bilangan yang lebih banyak anggotanya daripada bilangan bulat. Dan bilangan bulat itu sendiri merupakan konsep bilangan yang lebih banyak anggotanya daripada bilangan asli. Bahkan, himpunan rasional sendiri sebenarnya tidak mencakup segala sesuatu yang kita sebut sebagai "bilangan". Masih ada lebih banyak himpunan bilangan dan masih banyak cara untuk mendeskripsikan banyak hal seperti menghitung dan mengukur menggunakan himpunan yang berbeda, tergantung dari keperluan apa yang kita butuhkan. Misalnya, untuk mengukur luas lingkaran, kita membutuhkan sebuah bilangan dari bilangan Real, yang mana itu adalah π, sebuah bilangan irasional.
Secara umum, apa yang biasa kita sebut sebagai "bilangan" bukanlah suatu himpunan yang tetap, namun ia adalah sebuah kelompok dari himpunan yang memiliki beberapa kesamaan. Contohnya, kita bisa melakukan penjumlahan terhadap sembarang anggota-anggota yang berada pada himpunan-himpunan tersebut. Kita bisa menjumlahkan bilangan bulat, kita bisa menjumlahkan bilangan natural, dan kita bisa menjumlahkan bilangan rasional. Kita bisa mengurangkan dan mengalikan sembarang angka yang berada di dalam semua himpunan tersebut. Sehingga, dalam beberapa hal, kita mengetahui "apa itu bilangan" bukan karena karena ia termasuk dalam himpunan bilangan tertentu, tetapi karena apa yang dapat kita lakukan dengan himpunan bilangan tersebut.
Di Haskell, konsep mengenai "apa saja yang menjadikan suatu hal bisa disebut sebagai angka" direpresentasikan melalui typeclass. Kita memiliki types di Haskell yang mana ia seperti sets, dan kita memiliki classes (yang mana ini merupakan konsep yang lebih besar) yang mendefinisikan beberapa operasi yang memiliki kesamaan dalam beberapa himpunan yang berbeda.
Kita memiliki typeclass bernama Num
, dan itu mencakup beberapa tipe bilangan, yang mana adalah Integer
, Natural
, Rational
, dan beberapa tipe bilangan lainnya. Dan typeclass ini memberikan operasi-operasi apa saja yang bisa dilakukan pada tipe-tipe tersebut, seperti penjumlahan, perkalian dan pengurangan.
Dan inilah cara kita merepresentasikan konsep dari sebuah bilangan. Kita memiliki himpunan-himpunan yang dimana anggotanya adalah nilai aktual seperti 1, 2 dan 3. Mereka adalah himpunan seperti Integer
dan Natural
, dan Rational
, dan kita akan menyebutnya sebagai Tipe (Type). Lalu kita mempunyai Kelas Tipe (typeclass) yang memberi tahu kita kesamaan apa yang dimiliki semua tipe tersebut, dan operasi-operasi apa saja yang bisa kita lakukan dengan tipe-tipe tersebut.
Ada beberapa tipe yang anggotanya bukanlah angka-angka, yang tentu saja, karena akan ada banyak tipe data yang kita gunakan. Ada pula himpunan yang anggotanya hanya berisi dua nilai saja, nama dari type tersebut adalah Bool
– hanya ada False
dan True
di dalam himpunannya. Kita juga memiliki tipe lain yang kita sebut dengan Char
, yang mana anggotanya adalah karakter alfabet dan karakter apapun yang bisa kita ketik pada standard keyboard, dan masih banyak tipe-tipe lainnya lagi.
Jika kita definisikan ke dalam bentuk notasi:
Single quotes diperlukan saat mendefinisikan sebuah value yang typenya adalah Char
.
Integer
, Natural
, dan Rasional
, secara konseptual, semuanya cukup mirip sehingga cukup mudah untuk mengetahui bahwa kita dapat melakukan penjumlahan ke seluruh anggota yang berada pada tipe-tipe tersebut. Tapi bisakah kita memikirkan sebuah typeclass yang bisa mencakup tipe karakter dan angka? Apa kesamaan yang dimiliki oleh himpunan karakter dan himpunan angka?
Ada satu kesamaan yang dimiliki oleh mereka, yaitu mereka dapat diurutkan (order). Ada typeclass tersendiri untuk hal ini. Namanya Ord
. Di situlah tempat dari operasi perbandingan seperti "lebih besar dari" (>
) dan "kurang dari" (<
). Suatu value lebih kecil dari value lainnya (x < y
) jika x
berada sebelum y
saat diurutkan.
Semua tipe yang telah kita sebutkan sejauh ini juga dapat diuji kesamaan valuenya. Misalnya,
Apakah ‘a’ == ‘a’
? Tentu saja.
Apakah ‘a’ == ‘b’
? Tidak. Karena keduanya adalah karakter yang berbeda.
Demikian pula dengan,
1 == 1
yang bernilai True
, dan
1 == 2
yang bernilai False
.
Di Haskell, typeclass untuk hal ini disebut dengan Eq
, yang mana ini adalah kependekan dari equality. Dan typeclass Eq
ini yang mendefinisikan operator ==
.
Function (fungsi) sebenarnya adalah mapping antar himpunan (dari himpunan ke himpunan), yang mana, fungsi ini mengikuti aturan di matematika. Atau dengan kata lain, sebenarnya ini sama saja seperti dengan fungsi yang pernah kita pelajari sewaktu SMP.
Atau bahkan, lebih sederhana dari itu, sebenarnya kita sudah mengenal bentuk fungsi yang paling dasar sewaktu kita masih SD. Sewaktu di kelas 1 SD, kita pernah mendapatkan soal seperti ini saat mata pelajaran matematika.
Di sebelah kiri, kita memiliki himpunan angka-angka. Di sebelah kanan, kita memiliki himpunan yang anggotanya adalah nama dari angka-angka. Dan kita diminta untuk menggambarkan garis dari setiap angka tersebut ke nama angka yang sesuai. Ya, persis seperti yang sering kita lakukan di kelas 1 SD.
Ini adalah gambaran tentang bagaimana mendefinisikan suatu fungsi, yang mana dengan cara menggambar garis dari suatu elemen (anggota himpunan) di kiri ke elemen di kanan, berdasarkan beberapa aturan yang memberi tahu kita apa yang membuat pencocokan tersebut sesuai. Seperti contoh di atas, sesuai dengan perintahnya (mencocokkan angka dengan nama dari angka tersebut), pencocokannya sesuai saat kita menarik garis dari 3
ke tiga
. Dengan begitu, kita bisa mengartikan bahwa Fungsi adalah deskripsi tentang bagaimana suatu elemen dari suatu himpunan dicocokkan dengan elemen yang ada di dalam himpunan lain.
Mungkin saja himpunan kiri (input) dan himpunan kanan (output) adalah himpunan yang sama, dan ini sama sekali tidak apa-apa.
Ingatlah hal ini dengan baik. Fungsi adalah suatu proses yang mana kita mencocokkan kolom di sebelah kiri dengan kolom di sebelah kanan. Sambil kita memikirkan konsep tersebut, secara bersamaan kita juga memikirkan cara mengekspresikan fungsi tersebut di Haskell. Tujuan kita adalah mencocokkan angka-angka tersebut dengan nama-nama mereka, namun melalui Haskell.
Kita akan menyebut fungsi tersebut dengan namaAngka
. Pertama-tama kita menuliskan deklarasi tipe (type declaration) untuk fungsi namaAngka
tersebut.
xxxxxxxxxx
11namaAngka :: Integer -> String
Kita bisa mengartikan baris di atas sebagai berikut:
namaAngka
adalah nama fungsinya.
Integer
adalah input type
String
adalah output type
Makna dari double-colon (::
) tersebut adalah "memiliki tipe". Sehingga apa yang kita tulis di atas itu adalah, "namaAngka
memiliki tipe Integer -> String
"
Sehingga bisa kita katakan bahwa input dari fungsi tersebut adalah Integer (1, 2, 3, 4, ...) dan outputnya adalah string ("satu", "dua", "tiga", ...).
Yang baru saja kita tulis tersebut hanyalah type declarationnya saja. Sekarang saatnya kita menambahkan definisi fungsinya.
xxxxxxxxxx
81namaAngka :: Integer -> String
2namaAngka x =
3 case x of
4 0 -> "nol"
5 1 -> "satu"
6 2 -> "dua"
7 3 -> "tiga"
8 4 -> "empat"
Sewaktu kita menuliskan definisi fungsinya, kita kembali menuliskan fungsi namaAngka
. Ingatlah, nama fungsi yang dituliskan di pendefinisian fungsi harus sama dengan nama fungsi yang dituliskan pada type declarationnya. Selanjutnya, x
, adalah variabel. Di dalam fungsi, variabel disebut juga dengan sebutan parameter. Kita bisa menyebutnya parameter, karena saat kita mengaplikasikan fungsi namaAngka
ke sebuah angka, maka angka tersebut akan menggantikan variabel yang kita beri nama x
tersebut. Sehingga, ditahap ini sudah jelas bahwa:
namaAngka
adalah nama fungsinya
x
adalah nama parameternya.
Istilah ini datang dari Kalkulus Lambda. Sebenarnya Bind dan Use ini sudah biasa kita temukan di bahasa pemrograman yang lain. Hanya saja, kita tetap perlu memahami istilah ini karena kemungkinan kedepannya istilah ini akan sering digunakan.
Untuk sejenak, kita perhatikan function berikut:
xxxxxxxxxx
21tambahSatu :: Integer -> Integer
2tambahSatu x = x + 1
Jika kita lihat di line 2, x
dituliskan dua kali, yaitu:
Sebelum tanda =
, yang mana x
dituliskan pada saat pendeklarasian variabel. Ini disebut dengan Bind.
Sesudah tanda =
, yang mana x
dituliskan pada saat penggunaan variabel. Ini disebeut dengan Use.
Dengan kata lain, Bind adalah tempat dimana untuk pertama kalinya variabel tersebut diperkenalkan kepada fungsi tersebut. Sedangkan Use adalah saat variabelnya digunakan, yang mana lebih tepatnya pada kasus ini, x
digunakan sebagai operand dalam penjumlahan.
Saatnya kita kembali ke fungsi namaAngka
yang sebelumnya sudah kita buat.
xxxxxxxxxx
81namaAngka :: Integer -> String
2namaAngka x =
3case x of
40 -> "nol"
51 -> "satu"
62 -> "dua"
73 -> "tiga"
84 -> "empat"
Setelah memahami apa itu Bind dan Use, sekarang kita mengetahui, bahwa x
yang ada pada line 2 (sebelum tanda =
) tersebut adalah bind. Sedangkan x
yang ada pada line 3 (sesudah tanda =
) adalah use.
Nantinya akan ada tulisan tersendiri mengenai Kalkulus Lambda. (---)
case ... of ...
Setelah dari baris ke-2, setiap baris memiliki angka, disertai dengan tanda panah dan juga setiap nama angka yang berada di sisi sebelah kanan dari tanda panah tersebut. Perhatikanlah, setiap output string tersebut berada di antara dua double quotes (eg."nol"
). Seperti itulah cara kita menuliskan string di Haskell. Angka-angka tersebut ditulis tanpa tambahan apapun.
Di saat kita mengaplikasikan fungsi namaAngka
ke sebuah angka, evaluator bahasa Haskell akan melihat apakah ada salah satu dari angka di sebelah kiri tersebut yang sama dengan angka yang diinput, apabila ada, maka kemudian evaluator akan mengembalikan output string di sebelah kanan yang sesuai pada angka yang diinputkan.
Masukkan type declaration dan function definition ke dalam file main.hs
, lalu ke REPL untuk mencoba mengaplikasikan fungsi yang telah kita buat tersebut ke suatu argumen.
ghci> :l main.hs
[1 of 1] Compiling Main ( main.hs, interpreted )
Ok, one module loaded.
Kita perlu mengaplikasikan fungsi namaAngka
ke suatu argumen, yang mana kita akan memilih salah satu angka diantara 0 sampai dengan 4.
ghci> spell 1
"satu"
Namun, adalah hal yang mustahil untuk menulis fungsi serupa yang isinya adalah seluruh anggota dari bilangan Integer
. Sehingga fungsi yang kita buat tersebut disebut sebagai fungsi parsial (partial function), karena fungsi yang kita buat tersebut tidak memetakan (mapping) setiap nilai yang memungkinkan dari himpunan input ke output. Di saat kita menuliskan tipe Integer -> String
, kita menyatakan bahwa untuk setiap nilai inputan Integer
, kita akan mendapatkan String
. Namun ini tidak sepenuhnya benar, karena meskipun kita menuliskan hal tersebut pada type declaration, namun pada function definitionnya kita menuliskan fungsinya hanya bekerja untuk angka 0, 1, 2, 3, 4. Untuk bilangan Integer
lainnya, fungsi tersebut akan gagal digunakan. Jadi, seperti itulah partial function, ia hanya mencakup beberapa part (bagian) dari input set.
ghci> namaAngka 43
*** Exception: main.hs:(3,3)-(8,16): Non-exhaustive patterns in case
Ada beberapa hal yang bisa kita lakukan untuk meng-improve hal ini. Salah satu hal yang bisa kita lakukan adalah menambahkan sebuah kondisi yang mana kondisi tersebut adalah representasi dari semua anggota Integer
yang bukan bagian dari Integer
yang masuk dalam daftar yang kita buat (dalam kasus ini adalah 0, 1, 2, 3, 4). Kita akan menggunakan underscore (_
) sebagai semacam wildcard, yang dalam kasus ini ia menandakan "apapun anggota Integer
yang bukan 0, 1, 2, 3, 4". Pada situasi ini, kita akan mengembalikan string "Saya tidak tahu nama dari angka ini!"
.
xxxxxxxxxx
91namaAngka :: Integer -> String
2namaAngka x =
3case x of
40 -> "nol"
51 -> "satu"
62 -> "dua"
73 -> "tiga"
84 -> "empat"
9_ -> "Saya tidak tahu nama dari angka ini!"
Di Haskell, underscore adalah hal yang sakral. Akan ada tulisan tersendiri terkait hal ini. (---)
Sekarang, kita kembali ke REPL dan lakukan :reload
, dan kita bisa mencoba mengaplikasikan nilai Integer
apapun ke fungsi tersebut.
ghci> :r
ghci> spell 43
Saya tidak tahu nama dari angka ini!
Seharusnya kita tidak lagi melihat exceptions apapun di GHCi, karena kita sekarang telah menulis fungsi namaAngka
sebagai fungsi yang bisa memberikan suatu output saat menerima inputan Integer
apapun. Ia bukan lagi fungsi parsial.
Cobalah untuk membuat fungsi yang mana saat kita menuliskan nama sebuah negara, maka akan memberikan output berupa tahun kemerdekaan dari negara tersebut. Misal, saat kita memberikan argumen "Indonesia"
pada fungsi tersebut, maka kita akan mendapatkan kembalian 1945
.
Perhatikan hal berikut:
Type declaration dari fungsi ini adalah kebalikan dari namaAngka
. (String -> Integer
)
Fungsi yang dibuat bukanlah fungsi parsial
String
, yang mana adalah nama negara, harus dituliskan menggunakan double quotes, contoh "Indonesia"
Kita telah melihat hubungan antara tipe dan fungsi, kita akan mulai mengeksplorasi tipe data lebih lanjut.
Sebelumnya, kita telah menyebut bahwa ada tipe data yang disebut dengan Bool
, meskipun kita belum mempelajarinya lebih lanjut. Bool
merepresentasikan dua nilai dari Logika Boolean, yang mana adalah True
dan False
. Ya, hanya dua nilai itu saja yang ada di dalam himpunan Bool
tersebut.
Tipe data Bool
dari nama George Boole.
Datatype declaration untuk tipe data Boolean di Haskell adalah seperti berikut:
xxxxxxxxxx
11data Bool = False | True
Anda tidak perlu lagi memasukkan definisi tersebut ke dalam file main.hs
. Definisi tersebut sudah didefinisikan di dalam standard library.
Anda mungkin memperhatikan bahwa terlihat bahwa datatype declaration mirip seperti function declaration, dengan nama fungsi di sisi kiri dari tanda =
, dan isi fungsinya yang berada di sisi kanan dari tanda =
. Sama halnya dengan datatype declaration, nama tipe diberikan di sebelah kiri dari tanda =
, dan isi dari himpunannya ada di sebelah kanan dari tanda =
.
Datatype declaration dituliskan dengan menggunakan keyword data
. Keyword data
ini memberitahukan kita bahwa apa yang kita lihat adalah type definition, bukan function definition. Perhatikan juga bahwa nama dari tipenya selalu diawali dengan sebuah huruf kapital, yang mana nama fungsi tidak pernah menggunakan awalan huruf kapital.
Tanda pipe/pipa (|
) memiliki arti "atau" ("or"). Sehingga nilai dari tipe Bool
adalah True
atau False
, hanya salah satunya saja, bukan sekaligus keduanya, dan tidak ada kemungkinan lainnya selain dua nilai tersebut. Bool
adalah himpunan yang kecil, namun ia sangatlah berguna. Sebagai contoh, ada banyak fungsi yang pada umumnya mengembalikan sebuah nilai Boolean sebagai outputnya. Ya, terkadang dalam suatu program yang kita perlukan hanyalah sesederhana jawaban ya-atau-tidak.
Kita akan melihat beberapa contohnya melalu REPL. Kita bisa menanyakan kepada GHCI, "apakah 5 kurang dari 3 ?"
ghci> 5 > 3
True
Dan GHCi memberitahu kita bahwa hal tersebut adalah benar. Tentu saja 5 lebih besar daripada 3.
Atau kita juga bisa membandingkan dua buah string, "apakah "abc" sama dengan "def" ?"
xxxxxxxxxx
21ghci> "abc" == "def"
2False
Dan GHCi akan memberitahukan bahwa itu adalah salah. Kedua string tersebut tidaklah sama.
Atau kita juga bisa menanyakan ke GHCi apakah huruf 'r'
adalah elemen dari string "kerupuk"
.
xxxxxxxxxx
11λ> elem 'r' "kerupuk"
Dan adalah benar bahwa huruf 'r'
ada di dalam string "kerupuk"
. Perhatikan pula bahwa saat kita mengekspresikan sebuah single character, kita perlu menuliskannya dengan diapit oleh dua single quotes, sehingga GHCi mengetahui bahwa kita sedang membicarakan tentang sebuah single character 'r'
. Adalah hal yang berbeda pula apabila kita menginginkan string yang isinya hanyalah single character, yang mana kita menuliskannya sebagai "j"
. Apabila yang kita inginkan adalah sebuah string, tipe yang dapat mewakili sekumpulan karakter secara bersamaan, maka kita bisa menggunakan tanda double quotation.
Untuk beberapa saat Anda bisa mencoba-coba menuliskan beberapa ekspresi menggunakan >
, <
, dan ==
di GHCi.
Pada bagian ini kita akan mempelajari lebih dalam tentang hubungan antara functions dengan types.
Anggaplah kita memiliki ekspresi pseudo-code berikut:
if (x < 43) then (negate x) else (x + 43)
Ekspresi di atas hanyalah pseudo-code. Anda tidak bisa menuliskan ekspresi tersebut langsung ke GHCi, karena ia memiliki variabel x
yang mana tidak ter-bind ke manapun.
Kita bisa membacanya seperti berikut:
Jika
x
kurang dari 43, makax * -1
, jika sebaliknya, makax + 43
.
Setiap bagian dari ekspresi disebut sebagai sub-ekspresi. Dan setiap sub-ekspresi tersebut memiliki tipenya masing-masing.
Dalam ekspresi tersebut, ada tiga sub-ekspresi:
(x < 43)
(negate x)
(x + 43)
Kita mulai dari (x < 43)
. Kita mengetahui bahwa 43 adalah angka. Dan kita membandingkan x
dengan 43. Ini membuat kita menyimpulkan bahwa x
juga adalah angka. Ya, tentu saja kita sebelumnya sudah mengetahui bahwa angka hanya bisa dibandingkan dengan angka.
Sehingga, jika x
adalah suatu angka, maka ekspresi (x < 43)
akan memberikan kembalian berupa nilai Bool
. Kembalian dari perbandingan tersebut adalah True
atau False
. Ini sama saja seperti pertanyaan "apakah x
lebih kecil dari 43 ? Ataukah sebaliknya?"
Jika (x < 43)
bernilai True
, maka ia akan mengambil percabangan then
dan menjalankan (negate x)
. Lalu, pertanyaan lanjutannya adalah "Tipe apa yang seharusnya menjadi tipe dari (negate x)
?"
Fungsi negate
mengubah tanda dari sebuah angka. Contohnya:
Jika x
adalah 1, maka negate x
adalah -1.
Jika x
adalah -1, maka negate x
adalah 1.
Dari sini, kita bisa mengetahui bahwa output dari menegasikan sebuah angka memiliki tipe yang sama dengan tipe inputnya. Fungsi negate
mengambil sebuah angka dan mengembalikan angka lainnya dengan tipe yang sama.
Begitu juga dengan (x + 43)
, ekspresi ini juga memiliki tipe.
Lagi-lagi, kita memiliki sebuah operasi matematika yang melibatkan angka 43. Sehingga kita mengetahui pula bahwa operasi ini melibatkan dua angka. Atau dengan kata lain, x
haruslah sebuah angka yang dijumlahkan dengan 43. namun tidak seperti (x < 43)
yang mana memberikan kembalian yang nilainya True
atau False
, (x < 43)
memberikan kembalian berupa angka. Tentu saja, kembalian angka tersebut juga memiliki tipe yang sama dengan x
.
Pada bagian sebelumnya (Types & Functions), kita sudah membahas tentang perbedaan tipe-tipe pada bilangan. Jadi, tipe bilangan apa yang akan kita gunakan untuk ekspresi tersebut?
Mungkin Anda akan berfikir, "43
ini 'kan bilangan bulat. Mungkin saja ini adalah Integer
". Namun jangan terkecoh dengan bagaimana angka tersebut dituliskan. Ada banyak cara untuk menuliskan 43
. Perhatikan hal berikut:
ghci> 43 == 43.0
True
ghci> 43 == 43/1
True
Meskipun kita menuliskan "empat puluh tiga" sebagai 43
(bukan 43.0
) pada ekspresi if-then-else, bisa saja x
tersebut bernilai 6.2
. Dan x
dengan nilai bilangan berkoma tersebut mestinya tetap berjalan dengan baik pada ekspresi tersebut.
Sehingga, apa sebenarnya tipe yang tepat untuk keseluruhan ekspresi ini?
if (x < 43) then (negate x) else (x + 43)
Apa pun tipe bilangan x
, itulah tipe dari keseluruhan ekspresinya. Namun kita masih belum menemukan jawaban yang lebih spesifik tentang apa tipe dari x
tersebut.
Kita tidak mengetahui apa tipe dari x
tersebut, dan sebenarnya kita memang tidak perlu untuk mengetahuinya. Hal seperti ini biasanya disebut sebagai ekspresi polimorfik (polymorphic expression), yang mana ini artinya adalah input dari suatu fungsi bisa saja mempunyai tipe yang berbeda, dan outputnya bisa mempunyai tipe yang berbeda (namun di kasus ini, input dan outputnya harus memiliki tipe yang sama satu sama lain).
Sebelumnya kita telah membahas tentang adanya typeclass yang disebut dengan Num
. Karena x
berupa bilangan, tetapi kita tidak ingin menentukan secara spesifik apa tipe bilangannya, maka kita dapat menuliskan deklarasi tipenya seperti berikut:
xxxxxxxxxx
31sebuahFungsi :: (Num a) => a -> a
2sebuahFungsi x =
3 if (x < 10) then (negate x) else (x + 10)
Di kode tersebut, sebenarnya kita telah "menuangkan" ekspresi yang sebelumnya dan menempatkannya ke dalam konteks sebuah fungsi yang memiliki nama (named function), yang mana nama dari fungsi tersebut kita beri nama sebuahFungsi
. Dengan diberikannya nama untuk fungsi, maka hal ini memungkinkan kita untuk melakukan bind terhadap variabel x
sebagai paramer fungsi, dan hal ini juga memberikan kita kesempatan untuk menuliskan deklarasi tipe untuk fungsi tersebut. (Ingatlah: Nama fungsi diperlukan untuk mendeklarasikan tipe)
Kita memiliki sebuah variabel dengan nama a
di dalam deklarasi tipe yang kita buat, dan variabel ini dibatasi (constrained) oleh typeclass Num
. Di dalam tipe tersebut, kita menyatakan:
Bahwa a
adalah tipe dari angka/bilangan (dengan dibatasi oleh typeclass Num a =>
)
Bahwa input dan output harus bertipe bilangan yang sama (karena variabel a
muncul dua kali pada a -> a
).
Atau, alasan lain mengapa ia harus bertipe bilangan yang sama, karena kita tidak menuliskannya (misal) a -> b
. Apabila yang dituliskan adalah a -> b
, maka ini akan memperbolehkan kedua tipe a
dan b
untuk memiliki tipe yang berbeda.
Namun, apa yang telah kita tuliskan pada deklarasi tipe tersebut masih belum tepat.
Ord
Fungsi +
dan negate
berasal dari kelas tipe Num
, dan dari sanalah kita menyimpulkan bahwa a
memerlukan constraint Num
. Namun fungsi <
berasal dari kelas tipe bernama Ord
.
Untuk mendapatkan informasi mengeni tipe dan kelas tipe, Anda bisa menggunakan perintah :info
pada GHCi. Saatnya kita mencoba perintah tersebut.
ghci> :info Ord
type Ord :: * -> Constraint
class Eq a => Ord a where
compare :: a -> a -> Ordering
(<) :: a -> a -> Bool
(<=) :: a -> a -> Bool
(>) :: a -> a -> Bool
(>=) :: a -> a -> Bool
max :: a -> a -> a
min :: a -> a -> a
...
Nantinya akan dibuatkan tulisan tersendiri mengenai perintah :info
pada topik yang membahas mengenai GHCi (---)
Perintah tersebut menunjukkan banyak informasi (bahkan lebih dari yang kita perlukan), sehingga Anda mungkin perlu untuk scroll terminal yang Anda gunakan ke posisi awal perintah :info
tersebut dituliskan untuk membacanya dari awal. Dan apa yang ditampilkan pada tulisan ini hanyalah potongan awalnya saja (agar terlihat singkat).
Kita abaikan terlebih dahulu baris pertama, type Ord :: * -> Constraint
.
Baris kedua, class Eq a => Ord a where
dan baris yang terindentasi dibawahnya berisi definisi dari kelas tipe. Dari baris yang terindentasi tersebut, kita bisa melihat semua fungsi-fungsi yang ada di dalam kelas tipe Ord
, yang mana adalah compare
, <
, <=
, >
, >=
, max
dan min
.
Jika kita ingat kembali terkait pada bagian "Types & Functions", ada banyak tipe yang sifatnya orderable (bisa diurutkan), seperti karakter ('a'
berada pada urutan sebelum 'b'
), yang mana karakter-karakter tersebut bukanlah angka. Jadinya, Ord
itu sediri adalah sebuah typeclass yang berbeda dari Num
, karena ada beberapa elemennya bisa diurutkan padahal anggota-anggotanya bukanlah angka.
Dengan ini, artinya kita perlu untuk menambahkan sebuah constraint kedua pada type variable a
. Apapun tipe dari bilangan x
, tipenya haruslah memiliki kedua kondisi berikut:
Sebuah angka, sehingga ia bisa dinegasikan, dibandingkan dengan angka 10, dan ditambahkan dengan angka 10,
Sesuatu yang bisa diurutkan, sehingga ia bisa dibandingkan.