Manfaat Pemrograman Fungsional dalam Pengembangan Aplikasi: Python
Seiring dengan banyaknya kode program yang ditulis dan tingginya tuntutan market, semakin besar pula peluang masalah-masalah yang muncul selama pengembangan. Dengan banyaknya kemungkinan yang terjadi dalam program, bug pun terus bermunculan. Bagi pengembang baru, memahami kode program yang begitu banyak ditengah himpitan tenggat waktu yang ketat menjadi suatu masalah tersendiri. Belum lagi aplikasi yang diprogram mengandung banyak boilerplate code, sehingga menjadikan pengembang tidak produktif. Apalagi dengan berkembangnya aplikasi cerdas yang dibutuhkan masyarakat dewasa ini. Banyak sekali data yang diperlukan dan besar kemungkinan seorang pemrogram membuat aplikasi yang tidak efisien.
Konsekuensi negatif dari kode program yang gemuk dapat ditekan melalui paradigma pemrograman fungsional. Paradigma ini membentuk pemikiran pemrogram untuk membungkus setiap operasi dengan fungsi yang murni. Maksudnya adalah setiap fungsi yang dibuat tidak mengubah state yang ada atau tidak merubah apapun dari objek/argumen yang kita masukkan kedalam fungsi tersebut. Suatu objek masuk dan keluar dengan objek yang baru yang seutuhnya lepas dari objek sebelumnya. Artinya, setiap data yang diproses bersifat immutable atau tidak dapat dimutasi/diubah. Sehingga, kecil kemungkinan program yang dibuat memproduksi bug dalam konteks tersebut.
Lebih jauh lagi, fungsi-fungsi murni yang telah dibuat dapat digunakan kembali yang menyebabkan kode program lebih sedikit untuk ditulis. Sebut saja fungsi tingkat tinggi seperti map atau reduce yang terbukti mampu mempersingkat kode program dengan fungsionalitas yang sama jika dikerjakan melalui pendekatan imperatif. Dengan membuat fungsi sendiri layaknya map dan reduce, kita akan lebih produktif dalam menulis kode program yang berujung pada cepatnya implementasi fitur.
Masih banyak manfaat lain yang dapat dirasakan oleh pemrogram dengan memanfaatkan pemrograman fungsional. Lalu bagaimana secara teknis manfaat itu bisa kita rasakan ?
Berikut poin-poin yang dapat saya rangkum terkait manfaat teknis dari paradigma pemrograman fungsional.
Lazy Evaluation
Pada umumnya kode program akan dievaluasi seutuhnya. Sebagai contoh terdapat suatu task untuk mengkalikan dua bilangan yang ada pada list. Program akan melakukan perulangan pada tiap bilangan lalu dikalikan dengan dua dan hasilnya disimpan di variabel tertentu. Pendekatan ini disebut dengan eager evaluation karena tiap elemen akan langsung diproses (dikalikan 2). Solusi tersebut dapat dilihat pada program dibawah.
Dalam lazy evaluation, tidak semua operasi akan dievaluasi. Hanya saat operasi itu benar-benar dibutuhkan saju baru akan dievaluasi. Dengan kata lain, operasi dapat didefinisikan terlebih dahulu tanpa harus dieksekusi. Contohnya dapat dilihat dibawah:
Fungsi kali_dua_lazy diatas merupakan fungsi yang dievaluasi secara “malas” atau disebut juga sebagai generator. Fungsi def pada umumnya akan menggunakan return, namun disini diganti dengan yield. Return akan langsung menghentikan fungsi, sedangkan yield tidak akan menghentikan proses fungsi. Sintaksis ini akan mengembalikan nilai tahap demi tahap sesuai dengan kebutuhan. Sekarang mari kita cek hasil dari data_baru:
print(data_baru)<generator object kali_dua_lazy at 0x0000025ACF3A8D60>
Setelah dicetak, data baru yang dihasilkan tidak muncul seperti yang diharapkan melainkan berupa suatu objek. Untuk dapat mendapatkan hasilnya kita perlu melakukan perulangan pada objek tersebut (generator) atau dapat memanfaatkan fungsi list yang mentransformasi generator menjadi list. Lebih jelasnya dapat dilihat pada kode berikut:
for x in data_baru:
print(x)output:2
4
6
Atau
list(data_baru)output:[2, 4, 6]
Operasi diatas juga dapat disingkat dengan memanfaatkan fungsi map. Fungsi ini akan memetakan koleksi kedalam koleksi lain melalui fungsi tertentu (kita sebut sebagai lambda). Hasil dari map ini merupakan generator yang sifatnya lazy evaluation.
map(lambda x: x*2, data)
Melalui lazy evaluation, banyak sekali operasi yang dapat dilakukan tanpa harus mengevaluasi semua nilainya. Dapat dibayangkan, kita melakukan operasi matematis terhadap suatu variabel, lalu ketika kita butuhkan, variabel tersebut tinggal kita isi dengan suatu nilai. Sehingga evaluasi yang dilakukan tidak ketat yang mana tidak banyak memerlukan sumber daya dari sisi memori dan cpu karena hanya digunakan jika diperlukan.
Berkaitan dengan memori, karena dievaluasi dengan “malas” sudah jadi barang tentu memerlukan ukuran yang lebih kecil. Seperti terlihat pada kode program dibawah, saat tidak dievaluasi penuh, fungsi yang dipanggil hanya menghasilkan 96 bytes. Berbeda dengan lawannya yaitu eager evaluation atau koding seperti biasa akan menghasilkan 87600 bytes.
data = [_ for _ in range(10000)]
data_baru = kali_dua_lazy(data)
print(data_baru.__sizeof__())-> 96data_baru = kali_dua(data)
print(data_baru.__sizeof__())-> 87600
Bagaimana konsep ini dapat dimanfaatkan untuk aplikasi? Dalam teknologi big data spark, keputusan yang optimal dapat dihasilkan ketika data yang ditransformasikan tidak semerta-merta langsung dievaluasi. Tidak mungkin optimalitas dihasilkan jika menggunakan eager evaluation yang mana melibatkan network I/O yang besar.
Brevity
Brevity secara literal berati ringkas. Kode program yang banyak dapat dibungkus menjadi lebih singkat untuk mencapai tujuan yang sama. Melalui komposisi fungsi, pemrogram dapat menyusun program yang lebih ringkas dari sebelumnya. Sebaliknya bilamana tidak dimanfaatkan, pemrogram dapat menulis banyak sekali kode yang sama secara manual tanpa memanfaatkan penggunaan kembali fungsi yang telah didefinisikan sebelumnya.
Sebagai contoh kita akan mencoba membandingkan kedua pendekatan untuk menyelesaikan masalah jumlah huruf vokal. Solusi yang dihasilkan melalui pendekatan imperatif dapat dilihat dibawah. Mula-mula variabel untuk menampung jumlah huruf vokal didefinisikan sebanyak panjang list dengan nilai 0. Setelah itu, satu per satu data dikunjungi lalu diulang kembali berdasarkan karakternya (nested loop) dan dilakukan pengecekan terhadap huruf vokal lalu jumlah vokal dinaikan seperti terlihat pada baris 5–8.
Sudah jelas kan kode program diatas? Sekarang kita lihat penyelesaian masalah yang sama dengan pendekatan fungsional (kode program dapat dilihat dibawah). Pertama-tama, baris 2, fungsi dibuat untuk mengecek apakah karakter termasuk vokal atau bukan. Lalu jumlah vokal dihasilkan dari penjumlahan (sum) terhadap hasil pemetaan data kepada fungsi isVokal.
Hasil yang didapatkan lebih ringkas dari program sebelumnya serta banyak manfaat lain yang diperoleh. Secara jelas program diatas lebih mudah untuk dipahami bagi pengembang yang sudah memahami maksud dari tiap fungsinya. Bayangkan, program ini dibuat oleh orang lain atau beberapa bulan kemudian dibuka kembali. Dengan memahami secara fungsinya atau dengan kata lain “apa yang dilakukan” dari pada bagaimana masalah diselesaikan, lebih mudah bagi pemrogram untuk memahami kode program tersebut. Terlebih lagi, fungsi isVokal dapat digunakan berkali-kali di program lain tanpa harus diprogram ulang.
Manfaat keringkasan ini selama pengembangan tentu banyak. Pemrogram yang menulis kode dengan lebih singkat baik secara sintaksis ataupun penggunaan kembali fungsi akan mengimplementasikan fitur lebih cepat dari sebelumnya. Konsekuensinya adalah produk dengan fitur-fitur baru dapat diserahkan kepada pengguna lebih cepat yang akan berdampak positif bagi bisnis. Selain peningkatan layanan, perusahaan dapat lebih agile dalam menghadapi perubahan perilaku pelanggan yang mana biayanya dapat dikurangi.
Modularity
Fungsi-fungsi yang telah didefinisikan sebelumnya dapat digunakan kembali sehingga membentuk layaknya suatu modul. Fungsi dapat dijadikan sebagai argumen pada fungsi yang kita gunakan kembali. Dengan kata lain, fungsi menerima fungsi lainnya. Kita juga dapat mengembalikan suatu fungsi yang mendefinisikan perilaku tertentu untuk kita gunakan kembali di kasus yang lain (disebut dengan fungsi tingkat tinggi/higher-order function). Selain itu, komposisi fungsi yang dibentuk dari fungsi-fungsi lainnya dapat dengan mudah kita uji. Konsep ini yang membentuk modularitas selama pengembangan perangkat lunak.
Mari kita lihat program dibawah. Fungsi diskon_harga menunjukkan harga setelah didiskon sekian persen. Misal harga 100 dan diskon adalah 10% maka hasilnya adalah 90. Sedangkan fungsi is_boleh_diskon adalah fungsi boolean yang menunjukkan apakah harga boleh didiskon atau tidak berdasarkan nilai minimal tertentu. Sebut saja minimal 1000 namun harga 500 maka akan mengembalikan False yang menunjukkan bahwa harga tidak boleh didiskon.
Kedua fungsi ini dapat dimanfaatkan untuk digunakan sebagai penyaringan dari harga dan juga penentuan diskon. Sehingga terbentuklah fungsi hasil_diskon yang menerapkan tujuan tersebut. Pada ujungnya, final diskon adalah fungsi yang digunakan untuk mendapatkan hasil filter dan juga hasil akhir diskon tertentu.
Mari kita lihat kembali pada fungsi diskon_10_persen. Fungsi ini adalah fungsi yang dibuat sebagai fungsi yang memanggil fungsi lain namun dengan parameter yang tidak utuh. Maksudnya adalah kita mencoba menggunakan fungsi yang masih belum diketahui sebagian paramaternya. Teknik ini juga berlaku untuk fungsi diskon_min_1000 yang merupakan fungsi filter untuk harga dengan minimal 1000.
Modularitas itu juga berdampak pada proses pengujian perangkat lunak dalam tingkat unit. Karena basisnya adalah fungsi murni, maka mudah untuk kita uji. Seperti terlihat pada kode dibawah.
Less Bug
Banyak sekali kemungkinan sumber bug dalam aplikasi yang dikembangkan. Salah satunya adalah dengan merubah state selama fungsi itu dijalankan. Misal kita memiliki banyak sekali fungsi atau objek yang terlibat yang mana tugasnya memutasi suatu objek/data tertentu. Maka, besar kemungkinan akan terjadi bug karena banyak fungsi yang melakukan perubahan nilai. Jenis bug seperti ini tidak memungkinkan terjadi ketika pengembang menerapkan fungsi murni atau pure function.
Fungsi murni seperti layaknya fungsi yang kita pelajari dibangku sekolah bahwa fungsi akan menerima suatu nilai yang dipetakan pada nilai yang lain. Tidak ada perubahan data/objek selama fungsi murni dijalankan. Contoh misalnya fungsi perkalian antara dua bilangan. Kedua nilai akan diproses dan menghasilkan nilai yang baru tanpa merubah nilai yang ada sebelumnya atau tanpa merubah data/objek yang ada. Lebih jelasnya dapat dilihat pada program dibawah.
Kode program diatas menerangkan bahwa tidak ada data yang dimutasi. Misal saat fungsi ubahData dipanggil untuk merubah data (“a”, 10, -90) pada indeks ke 1 dengan nilai 99, akan menghasilkan tuple baru. Ketika dicek data aslinya tidak berubah sama sekali. Dengan mekanisme seperti ini akan meminimalkan bug dalam konteks yang sudah dipaparkan sebelummya.
Contoh kasus nyata yang lebih real dapat dilihat pada program dibawah. namedtuple digunakan sebagai immutable data pengganti kelas. Penggunaannya lebih ringkas apabila disandingkan dengan pendefinisian suatu kelas. Dalam namedtuple terdapat method _replace yang dapat menggantikan nilai pada nama tertentu tanpa merubah nilai asalnya. Selebihnya bisa dipahami sendiri ya.. :)
Multiprocessing
Manfaat yang satu ini pasti pernah kita rasakan walaupun tidak menggunakan paradigma fungsional. Eksekusi unit program dapat dipercepat dengan memanfaatkan dua atau lebih CPU secara bersamaan. Ibarat rumah, akan lebih cepat dibangun jika melibatkan lebih dari satu pekerja. Namun kenapa paradigma fungsional cocok dalam pemrosesan di core cpu yang banyak?
Fungsi murni yang sudah dijelaskan sebelumnya adalah penyebab kenapa multi processing menjadi lebih andal ketika menggunakan fungsi yang notabene tidak merubah state apapun. Melalui fungsi yang tak murni, ketika banyak proses dieksekusi untuk memutasi suatu data disaat yang bersamaan, disaat itulah kemungkinan kesalahan akan muncul. Sehingga perlu perlakuan khusus yang menyebabkan pendekatan biasa menjadi tidak biasa dan fungsi murni menjadi alternatif dalam multi processing.
Melalui fungsi yang hanya memetakan nilai, fungsi-fungsi yang dijalankan secara paralel tidak akan berpengaruh pada fungsi atau data yang lain. Contoh fungsi map yang memetakan suatu koleksi kedalam koleksi lain dengan jumlah data yang sama. Fungsi tersebut tidak bergantung pada faktor lain ditiap pemetaan datanya. Sebut saja map yang memangkatkan suatu koleksi berkali-kali. Ketika data x dipangkatkan dan data y dipangkatkan mereka sifatnya independen, sehingga teknik paralel pada fungsi tersebut tidak akan memicu banyak kesalahan.
Nah, teknik fungsional ini sudah tersedia di Python dengan memanfaatkan modul multiprocessing melalui fungsi map seperti terlihat pada kode program dibawah. Pada baris ke 10, fungsi denganMultiProcess menerapkan fungsi map yang dijalankan secara paralel, 6 pada Pool menunjukkan jumlah proses yang digunakan untuk mengeksekusi program yang berada diblok with.
Hasil waktu komputasi yang didapatkan dari kedua pendekatan menunjukan perbedaan yang cukup signifikan. Tanpa multi process, diperlukan waktu sekitar 5,62 detik untuk menyelesaikan tugas yang sama. Sedangkan setengah waktunya, 2,14 detik, dapat dihasilkan dengan menggunakan multi processing. Lumayan kan?
Lalu aplikasi apa yang bisa dimanfaatkan untuk multiprocessing? Aplikasi yang tentunya membutuhkan I/O yang berat atau komputasi yang kompleks. Yang mana memerlukan waktu yang cukup lama. Jika masalah sudah cukup terselesaikan dengan pemrograman biasa/sinkron kenapa repot-repot dibuat multi processing?
Seperti buah simalakama, masalah yang masih tergolong sederhana, memungkinkan multiprocessing akan malah menambah waktu komputasi karena dibungkus di process yang berbeda (takes time). Sangat memungkinkan jika dengan cara biasa, membutuhkan waktu 0,05 detik, justru multiprocessing bisa lebih parah. Oleh sebab itu gunakanlah teknik ini dengan bijak khususnya apabila pendekatan biasa memerlukan waktu lebih dari sekian detik (misal 1 detik keatas)
Sejatinya, performa aplikasi akan meningkat drastis jika pemrogram dapat menemukan kompleksitas algoritme dengan magnitude yang lebih kecil. Sehebat apapun multi processing dengan kompleksitas linear O(n) tidak akan lebih bagus dibandingkan dengan O(log n) apalagi O(1). Sehingga, teknik ini lebih baik digunakan jika memang tidak ada lagi algoritme yang mampu menyelesaikan masalah dengan waktu komputasi yang lebih kecil.
Rangkuman
Akhir kata, banyak manfaat yang dapat dirasakan dengan menggunakan pemrograman fungsional diantaranya adalah lazy evaluation, menulis kode program yang lebih ringkas, modularitas, bug yang lebih sedikit, dan multi processing. Apa yang sudah disajikan disini mengupas manfaat dari tiap-tiap aspek yang telah dipaparkan.
Tulisan ini tidak menganjurkan bagi pembaca untuk secara brutal menggunakan pemrograman fungsional. Perlu banyak latihan dan perlu adanya penyesuaian dengan masalah yang ada. Karena dibalik manfaat tersebut terdapat pula kekurangan yang perlu dipertimbangkan apabila pemrogram belum terbiasa. Apabila digunakan dengan baik akan menghasilkan aplikasi yang jauh dapat diandalkan. Selamat bereksperimen :)