Functional Programming: Sekilas tentang Type Signature sebuah Function
lagi dan lagi, dalam ES6
Belajar Functional Programming itu hukumnya haram kalau belum mengenal Type Signature. Karena memang nantinya Type Signature inilah yang akan membantu reasoning kita akan alur sebuah program. Ciri paling mencolok dari Type Signature yang sering digunakan adalah adanya tanda-tanda panah di atas sebuah function. Mungkin ada yang pernah lihat simbol macam ini?
Inilah yang sebentar lagi akan dibahas. Notasi di atas biasa disebut Type Signature, merujuk pada type system-nya Hindley-Milner yang banyak digunakan di berbagai bahasa FP, termasuk Haskell. Type Signature ini hanya salah satu bentuk standarisasi saja di antara bahasa-bahasa FP. Scala punya gaya Type Signature-nya sendiri. Namun di artikel ini (dan mungkin di artikel-artikel selanjutnya), saya akan mulai menggunakan Type Signature-nya si HM ini.
Penggunaan
Type Signature digunakan sebagai alat dokumentasi akan bagaimana sebuah pure function menerima input dan mengeluarkan output. Apa saja tipe data paramaternya? Apakah sebuah Integer? String? Atau array? Atau justru bisa semuanya?
Function Sederhana
// lima :: Number
const lima = 5// numToString :: Number → String
const numToString = num => num.toString()
Function numToString
di atas simple-nya adalah menerima input dengan tipe Number
dan mengembalikan output dengan tipe String
. Beberapa rule dasar yang mesti dipahami adalah:
- Nama function dengan tipe parameter dan/atau return-nya dipisah dengan simbol
::
- Nama function ditulis sebelum simbol
::
- Return type ditulis setelah simbol → (yang paling akhir) atau jika tidak memiliki pramater, setelah simbol
::
Function dengan Beberapa Parameter
// add :: Number → Number → Number
const add = (x, y) => x + y
Fungsi add
menerima 2 input, yang keduanya harus bertipe Number
, dan mengembalikan value dengan tipe Number
juga. Ingat kembali, tipe yang ditulis paling kanan selalu merupakan tipe return value-nya.
// split :: String → String → [String]
const split = (separator, str) => str.split(separator)
Juga seperti contoh split
ini, menerima 2 inputan bertipe String
, dan mengembalikan kumpulan value bertipe String
pula (array of strings).
Note: Best practice-nya, function yang memiliki paramater lebih dari satu dijadikan curried function. Contoh-contoh function yang memiliki lebih dari satu parameter di artikel ini saya asumsikan sudah curried semua agar penjelasan bisa lebih fokus pada esensi.
Higher Order Function
Bukan Functional Programming namanya kalau sebuah function tidak bisa menerima function juga dan/atau mengembalikan sebuah function.
// mathMsg :: (Number → Number → Number) → Number → Number → String
const mathMsg = (mathOp, x, y) => {
const result = mathOp(x, y) return `The result of ${x} and ${y} is ${result}`
}
Wah lumayan banyak ya type signature-nya. Tapi sederhananya adalah, mathMsg
yang mengembalikan String
ini menerima sebuah function yang mana function tersebut memiliki 2 parameter bertipe Number
dan mengembalikan Number
, lalu parameter kedua dan ketiga dari mathMsg
sendiri sama-sama menerima Number
. Mudah kan?
Nah function yang di-pass inilah (dalam hal ini mathOp
) jika dituliskan ke dalam Type Signature harus dibungkus dengan tanda kurung, sebagai pembeda function dengan tipe biasa.
Jika melihat type signature dari mathMsg
di atas, kita bisa mensubstitusi mathOp
dengan fungsi add
di contoh sebelumnya karena memiliki type signature yang sama (Number → Number → Number
).
add
bisa di-pass sebagai argument pertama mathMsg
Generic?
Bagaimana kalau kita memiliki function yang general, yang artinya tidak sebatas tipe tertentu saja? Seperti
const identity = value => value
Jika saya pass sebuah Number
ke dalam function identity
, maka return type-nya adalah Number
. Jika pass sebuah String
, maka tipe kembalian String
juga. Jika pass sebuah Array, maka tipe kembaliannya pun Array yang sama juga. Artinya, fungsi identity
ini fleksibel, parameternya dapat menerima lebih dari satu macam tipe. Lantas bagaimana Type Signature-nya?
// identity :: a → a
const identity = value => value
a
bisa apa saja, karena berlaku general. Contoh lain:
// head :: [a] → a
const head = xs => xs[0]console.log(head([5, 6, 7]))
// => 5, dalam hal ini a bertipe Intconsole.log(head('Kamu iya kamu'))
// => 'K', dalam hal ini a bertipe Stringconsole.log(head([[1, 2], [3, 4]]))
// => [1, 2], dalam hal ini a bertipe [Int]
Operasi map
pada sebuah array pun demikian. Kita tidak tahu tipe data di dalam array tersebut, pun tidak tahu hasil kembalian dari function yang ingin di-pass ke dalam fungsi map
. Oleh karena itu variable acak (seperti a
, b
, c
) dapat digunakan dalam kasus seperti ini.
Hal yang sama berlaku juga untuk operasi filter
dan reduce
pada array.
Perhatikan bagaimana tipe kembalian fungsi reduce
haruslah sama dengan tipe nilai init
.
Instance
Bagi kamu yang sudah mengerti konsep Functor, kita bisa bisa membuat Type Signature map dari sebuah Functor:
// fmap :: Functor f => (a → b) → f a → f b
const fmap = fn => functor => functor.map(fn)
Maksud dari notasi di atas adalah: semua f
yang ditulis di Type Signature fmap
, haruslah berupa Functor.
// (Int → String) → Maybe Int → Maybe String
fmap(x => x.toString(), Maybe(5))// anggap fmap itu singkatan functor-map
Begitupun Monad, kita dapat membuat notasi fungsi bind
yang general untuk Monad:
// mbind :: Monad m => (a → m b) → m a → m b
const mbind = fn => monad => monad.bind(fn)// anggap mbind itu singkatan monad-bind
Dan untuk kamu yang sudah mengerti konsep Applicative Functor, kita bisa sama-sama membuat Type Signature dari pure
, ap
dan liftA
:
Ingat bahwa Applicative itu subclass dari Functor
Kesimpulannya dari ketiga contoh instance di atas adalah bahwa simbol =>
berperan sebagai pemisah antara nama instance dan “the real type signature” dari sebuah fungsi.
Reasoning & Optimisasi
Seperti yang telah dijelsakan di atas, Type Signature ini dapat digunakan untuk reasoning alur sebuah program, terutama terhadap sebuah fungsi hasil komposisi dari beberapa pure function. Untuk memahami konsep komposisi dalam Functional Programming, kamu dapat membacanya disini.
Saya ingin membuktikan bahwa kedua fungsi dibawah ini adalah sama
// Ingat bahwa Type Signature dari head adalah
// head :: [a] → acompose(head, map(fn)) === compose(fn, head)
compose
yang sebelah kiri maksutnya adalah: Diberikan sebuah array lalu saya map terhadap function fn
, kemudian saya ambil elemen pertama dari hasil map tersebut. Sedangkan yang sebelah kanan adalah: Diberikan sebuah array, saya ambil elemen pertama terlebih dahulu, lalu saya proses dengan function fn
. Pertanyaannya adalah, apakah hasilnya sama?
Mari kita buktikan dengan Type Signature.
Note: Ingat compose mengevaluasi function dari kanan ke kiri.
// head :: [a] → a
// fn :: a → b
// map(fn) :: [a] → [b]// [a] → [b] → b === [a] → a → b
compose(head, map(fn)) === compose(fn, head)-- Lalu kita analisa INPUT dan OUTPUT nya saja// [a] → b === [a] → b
compose(head, map(fn)) === compose(fn, head)
Tadaa! Terbukti bahwa kedua fungsi tersebut menghasilkan OUTPUT yang sama. Yang lebih performan yang mana? Jelas yang sebelah kanan karena tidak perlu mengiterasi array-nya terlebih dahulu. Nah, jika ada teman programmer yang menulis fungsi seperti compose
yang sebelah kiri, kita langsung bisa refactor fungsi tersebut dengan fungsi yang lebih performan (compose
yang sebelah kanan) melalui pembuktian dari type signature-nya.
Kesimpulan
Dengan adanya Type Signature, pure function yang kita buat dapat terdokumentasikan dengan lebih baik. Pun ketika kita sedang menggunakan pure function yang dibuat oleh orang lain, kita jadi langsung paham apa yang harus kita lakukan dengan fungsi tersebut. Reasoning alur program pun menjadi lebih terbantu.
Untuk lebih lanjutnya, kamu bisa mampir-mampir ke dokumentasi Ramda, salah satu library Javascript terpopuler. Dokumentasinya sangat bagus untuk dipelajari.
Penutup
Terima kasih sudah bersedia membaca artikel ini. Jika terdapat penjelasan yang kurang mudah dicerna, mohon feedback-nya di kolom komentar. Harapannya artikel-artikel selanjutnya dapat menjadi lebih baik.
Happy coding! 😃