Dari Nol ke Monad

Ekspektasi dan Syarat

Ekspektasi

Dengan catatan ini, pengguna pemula bahasa Haskell bisa memahami konsep dasar sampai dengan konsep Monad.

Target Pembaca

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).

Code Editor

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:

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.

Permulaan

GHC dan GHCi

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. (---)


GHCi prompt

Di GHCi, kita bisa melakukan hal berikut:

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.

Menyimpan definisi ke dalam file

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:

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

Mengubah isi file dan :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,

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!!

Pesan Error

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,

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.

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.

Types & Functions

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.

Types

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:


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.


g2332

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.

Classes

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.

Lebih lanjut tentang Types dan Classes

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:

Integer={2,1,0,1,2}Bool={True,False}Char={a,b,c,A,B,1,2}

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,

Demikian pula dengan,

Di Haskell, typeclass untuk hal ini disebut dengan Eq, yang mana ini adalah kependekan dari equality. Dan typeclass Eq ini yang mendefinisikan operator ==.

Functions

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.

g5309

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.

Kita bisa mengartikan baris di atas sebagai berikut:

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.

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:

Bind dan Use

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:

Jika kita lihat di line 2, x dituliskan dua kali, yaitu:

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.

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!".


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.

Latihan#1

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:

Tipe Dasar

Boolean Type

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:


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" ?"

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".

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.

Latihan#2

Untuk beberapa saat Anda bisa mencoba-coba menuliskan beberapa ekspresi menggunakan >, <, dan == di GHCi.

Types & Functions, bagian 2

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, maka x * -1, jika sebaliknya, maka x + 43.

Setiap bagian dari ekspresi disebut sebagai sub-ekspresi. Dan setiap sub-ekspresi tersebut memiliki tipenya masing-masing.

Tipe-tipe sub-ekspresi

Dalam ekspresi tersebut, ada tiga sub-ekspresi:

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:

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.

Lantas, apa tipe dari angka tersebut?

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.

Constrained polymorphism

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:


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:

Namun, apa yang telah kita tuliskan pada deklarasi tipe tersebut masih belum tepat.

Kelas Tipe 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: