Higher-order Function — Paradigma Fungsional Praktis, Part 4
Artikel ini adalah bagian dari sebuah seri artikel tentang Paradigma Fungsional Praktis. Silakan lompat ke akhir artikel untuk melihat navigasi keseluruhan seri ini.

Higher-order function dan currying adalah salah satu konsep terpenting pada paradigma pemrograman fungsional. Bahkan bisa dibilang fitur ini yang mendefinisikan apakah sebuah bahasa pemrograman bisa disebut fungsional. Mari cari tahu tentangnya.
Higher-order Function
Dalam berbagai macam bahasa pemrograman, kita kenal dengan istilah value — nilai. Value adalah segala hal yang bisa di-assign ke variabel, diterima sebagai parameter fungsi, dikembalikan oleh suatu fungsi. Beberapa contoh sederhana misalnya 10, "Halo", true, atau suatu objek dari class Animal.
Di bahasa pemrograman “fungsional” atau beberapa bahasa yang multiparadigm (misalnya Javascript), fungsi juga merupakan sebuah value. Seperti value lain, ia dapat di-assign ke variabel, diterima sebagai parameter fungsi, dan dikembalikan oleh suatu fungsi layaknya string, integer, boolean, dan sebagainya. Dengan penjelasan tersebut, kita sampai pada definisi higher-order function:
Higher-order function atau fungsi orde tinggi adalah fungsi yang 1) menerima fungsi lain sebagai parameternya, dan/atau 2) mengembalikan fungsi lain sebagai keluarannya.
Kalau Anda mengikuti seri artikel ini dari pertama, mungkin Anda jeli memperhatikan bahwa fungsi filter yang kita definisikan pada Part 1: Pemrograman Deklaratif adalah sebuah higher-order function! Agar Anda tidak perlu berpindah lagi ke artikel tersebut; mari kita lihat lagi definisinya di sini:
/**
* Filters elements of a list based on predicate fn.
*/
function filter(list, fn) {
let result = [];
for (let i = 0; i < list.length; i++) {
let currentEl = list[i];
if (fn(currentEl)) {
result.push(currentEl);
}
}
return result;
}Ia menerima sebuah fungsi lain (fn) sebagai predikatnya. Andaikan kita memberi tipe pada fungsi filter, ia akan terlihat seperti ini (dengan notasi Flow):
function filter<T>(list: Array<T>, fn: (T => boolean)): Array<T>Tipe itu artinya filter menerima sebuah array bernama list dengan elemen bertipe T (apa pun) dan sebuah fungsi fn yang menerima value bertipe T yang sama dan mengembalikan boolean. Fungsi filter akan mengembalikan sebuah array dengan tipe T juga.
Kita waktu itu menggunakannya sebagai berikut:
let evenNumbers = filter(numbers, el => el % 2 == 0);Karena fungsi adalah value dan dia bisa di-assign ke suatu variabel, kita bisa menuliskannya sebagai:
let isEven = x => x % 2 == 0;let evenNumbers = filter(numbers, isEven);
…yang memberikan kita kode yang bahkan lebih deklaratif dan mudah dibaca!
Mengapa kita membutuhkan higher-order function?
Fungsi pada umumnya merupakan sebuah abstraksi atas value. Kita tahu bahwa 2 + 2 menghasilkan 4. Bagaimana kita bisa mengabstraksikan komputasi tersebut untuk semua bilangan? Ya, kita buat fungsi add yang menerima dua parameter bilangan dan menghasilkan
Higher-order function memberikan kita kemampuan untuk mengabstraksi atas sebuah action — aksi. Abstraksi adalah fundamental dari pemrograman. Pengabstraksian atas aksi memungkinkan kita menulis fungsi seperti filter. Fungsi filter memungkinkan kita menyaring elemen-elemen dari sebuah list, tapi bagaimana cara menyaringnya diabstraksi melalui fungsi yang bisa kita pass melalui parameternya.
Contoh fungsi lain yang juga bekerja pada array di Javascript misalnya forEach, sebagai berikut:
numbers.forEach((el) => {
console.log(el);
});forEach merupakan sebuah fungsi yang memungkinkan kita untuk melakukan sesuatu terhadap tiap-tiap elemen pada array. Kita dapat menspesifikasikan apa yang dilakukan dengan memberikan fungsinya.
Note: istilah “higher-order function” cukup panjang, dan orang-orang banyak menggunakan singkatan HOF untuk mengacu padanya. Mulai sekarang, ketika ada orang yang menyebut HOF, Anda akan tahu apa maksudnya!
HOF yang mengembalikan fungsi
Berdasarkan definisi higher-order function di atas, kita baru membahas tentang satu karakteristiknya saja, yaitu menerima fungsi sebagai parameternya. Kali ini, kita akan membahas karakteristik yang lain, yaitu fungsi yang mampu mengembalikan fungsi lain sebagai keluarannya.
Contoh fungsi yang bisa mengembalikan fungsi lain adalah sebagai berikut (menggunakan Javascript):
function createGreeter(greeting) {
return function greet(name) {
console.log(greeting, name);
};
}let greetInEnglish = createGreeter("Hello");
greetInEnglish("Bobby"); // mencetak "Hello Bobby"let greetInIndonesian = createGreeter("Halo");
greetInIndonesian("Bobby"); // mencetak "Halo Bobby"
Pada potongan di atas, fungsi createGreeter menerima parameter untuk menspesifikasikan sapaan yang akan digunakan, lalu mengembalikan fungsi yang menerima nama untuk menyampaikan sapaan tersebut.
Anda bisa melihat bahwa manfaat dari pendekatan ini adalah spesialisasi. Ketika mendefinisikan fungsi createGreeter, kita memberikan keleluasaan pada pemanggil fungsi untuk membuat sebuah fungsi yang terspesialisasi (greetInEnglish dan greetInIndonesian).
Jadi, bagaimana cara menggunakannya?
Kalau Anda menggunakan bahasa pemrograman “fungsional”, kapabilitas higher-order function sudah Anda dapat dengan cuma-cuma. Di Javascript juga demikian, namun secara spesifik, hal itu diperbolehkan karena di Javascript, everything is an object — semua hal adalah objek, termasuk fungsi. Beberapa bahasa lain seperti Ruby, Python, Go, dan Rust juga mendukung higher-order function.
Di Java bisa juga, namun sedikit lebih rumit: pada versi Java 8, kita mendapatkan sintaks lambda untuk menuliskan fungsi dan bisa mendapatkan reference ke method dengan operator ::, tapi bahkan sebelum itu, Java memiliki workaround untuk higher-order function dalam bentuk functional interface, yaitu interface yang hanya memiliki satu method, misalnya Runnable dan Comparator.
Meskipun tidak terlalu sederhana (dan melibatkan banyak sintaks yang tidak perlu), namun pendekatan itu bekerja. Malah, karena dinilai banyak digunakan, IDE seperti Intellij IDEA mulai menampilkan implementasi functional interface sebagai lambda, dan pada Java 8 akhirnya kita diberikan syntastic sugar dalam bentuk ekspresi lambda dan method reference. Keputusan Java mengadopsi sintaks tersebut tentu memberitahu kita sesuatu tentang praktikalitas paradigma fungsional.
Currying
Jika bicara tentang HOF, kurang lengkap kalau kita tidak membahas currying.
Currying adalah teknik yang memungkinkan kita melakukan pemberian argumen-argumen secara sebagian (aplikasi parsial) pada suatu fungsi. Pada beberapa bahasa pemrograman seperti Haskell, OCaml, dan F#, teknik ini didukung secara out-of-the-box. Pada bahasa-bahasa ini, sebuah fungsi dengan banyak argumen sebenarnya adalah komposisi fungsi dengan satu argumen. Untuk lebih jelasnya, perhatikan contoh fungsi di OCaml berikut.
let add x y = x + y
(* val add : int -> int -> int *)Tipe data dari fungsi add adalah int -> int -> int. Apa artinya? Jika Anda akrab dengan tipe data untuk fungsi pada OCaml dan Haskell, Anda akan tau bahwa notasi tersebut menjelaskan bahwa fungsi add menerima dua buah integer dan mengembalikan sebuah integer.
Namun, mengapa harus int -> int -> int? Mengapa tidak int, int -> int untuk membedakan mana argumen dan mana return value?
Jawabannya adalah, seperti yang saya bilang tadi, setiap fungsi di OCaml sejatinya adalah fungsi dengan satu argumen. Kita bisa melihat bentuk ekivalen dari fungsi tersebut sebagai berikut:
let add1 = fun x y -> x + y
(* val add1 : int -> int -> int *)let add2 = fun x -> (fun y -> x + y)
(* val add2 : int -> int -> int *)
Pada contoh di atas add, add1, dan add2 adalah fungsi yang ekivalen. Semuanya merupakan fungsi penjumlahan dua buah integer. Bentuk add2 bisa dibilang merupakan bentuk yang paling dasar dari fungsi tersebut. add2 adalah fungsi yang menerima satu argumen x, yang kemudian mengembalikan sebuah fungsi lain dengan satu argumen y, yang kemudian akan mengembalikan nilai x + y. Anda tentu dapat melihat betapa rumitnya menuliskannya seperti add2, maka pada OCaml kita memiliki bentuk add dan add1 sebagai pemanis sintaks untuk penulisan fungsi.
Untuk menjawab kenapa int -> int -> int, kalau kita menggunakan tanda kurung untuk mengelompokkannya, tipe data tersebut dapat dituliskan sebagai int -> (int -> int). Dengan bentuk ini, lebih jelas bahwa notasi tersebut menjelaskan sebuah fungsi yang menerima int dan mengembalikan (int -> int), yang mana merupakan fungsi lain!
Untuk melihat kegunaannya, perhatikan potongan kode berikut:
let addTen = add 10
(* val addTen: int -> int *)let numbers = [2; 3; 4; 5; 6; 7]let numbersPlusTen = List.map ~f:addTen numbers
(* numbersPlusTen akan berisi [12; 13; 14; 15; 16; 17] *)
Pada kode tersebut, kita melakukan aplikasi parsial pada fungsi add dengan memberikan nilai x sebagai 10. Hasilnya kita mendapatkan sebuah fungsi baru, addTen, yang sekarang hanya butuh satu argumen lagi. Kita kemudian menggunakannya untuk menambahkan semua elemen pada list numbers mengunakan List.map. Keren!
Lalu, untuk apa ada currying? Currying pada dasarnya hanyalah bentuk penyederhanaan HOF yang mengembalikan fungsi. Karena itu, ia memiliki manfaat yang sama, yaitu untuk spesialisasi, seperti ditunjukkan pada fungsi addTen di atas.
Oke, saya mau! Bagaimana caranya?
Penerapan currying pada bahasa pemrograman yang tidak “fungsional” sedikit lebih rumit; bahkan pada bahasa “fungsional” pun ada beberapa (misalnya Elixir) yang tidak mendukung currying secara out-of-the-box. Di Javascript, kita memiliki fungsi bind. Misalnya kita tulis ulang fungsi createGreeter sebagai sebuah fungsi biasa (bukan HOF) dengan nama greet:
function greet(greeting, name) {
console.log(greeting, name);
}Kita bisa melakukan aplikasi parsial kepada fungsi greet menggunakan bind sebagai berikut:
let greetInEnglish = greet.bind(null, "Hello");
greetInEnglish("Bobby"); // mencetak Hello Bobbylet greetInIndonesian = greet.bind(null, "Halo");
greetInIndonesian("Bobby"); // mencetak Halo Bobby
Di Java, sayangnya kita tidak memiliki kemampuan ini. Kita bisa secara eksplisit menuliskan fungsi yang mendukung aplikasi parsial, misalnya x -> y -> x + y, tapi definisi umumnya ((x, y) -> x + y) tidak otomatis mendapat kemampuan tersebut.
Sampai sini bahasan kita tentang higher-order function, dan itu adalah karakteristik keempat dan terakhir yang ingin saya bagikan kepada Anda. Abstraksi dan generalisasi operasi yang Anda gunakan! Dengan demikian, Anda dapat lebih leluasa menggunakan kembali (reuse) kode yang Anda tulis di bagian program Anda yang lain.
Artikel berikutnya adalah artikel terakhir seri ini, dan kita akan membahas tentang penerapan praktis paradigma fungsional di industri. Klik tautan berikut membacanya:
Atau gunakan navigasi di bawah ini untuk melihat artikel lainnya:
- Perkenalan Paradigma Pemrograman Fungsional Praktis
- Pemrograman Deklaratif — Paradigma Fungsional Praktis, Part 1
- Pure Functions dan Efek Samping — Paradigma Fungsional Praktis, Part 2
- Transformasi Data dan Immutability — Paradigma Fungsional Praktis, Part 3
- Higher-order Function — Paradigma Fungsional Praktis, Part 4
- Penerapan Paradigma Fungsional di Industri — Paradigma Fungsional Praktis, Part 5
Apakah Anda menyukai yang Anda baca? Apa saya melewatkan sesuatu? Beritahu saya melalui kolom komentar!
Paradigma Fungsional adalah sebuah blog tentang berbagai hal yang berkaitan dengan paradigma pemrograman fungsional (functional programming) di dunia pengembangan software. Tulisan-tulisan di blog ini akan dimuat dalam Bahasa Indonesia, karena menurut saya resource untuk belajar paradigma fungsional dalam bahasa kita masih sangat kurang.
Apabila Anda tertarik untuk mengetahui lebih lanjut tentang dunia paradigma fungsional, silakan ikuti blog ini!

