Riz Maulana

Bagian 9-b: Multi-arity dan Destrukturisasi Parameter

  |   Reading time: 12 minute(s).


Di bagian 9-a, kita sudah belajar cara membuat dan menggunakan fungsi di Clojure. Di bagian ini, kita akan mengulas fungsi di Clojure lebih dalam. Kita akan mengenal cara membuat fungsi yang dapat menerima jumlah argumen yang berbeda dan juga cara mengakses elemen dalam suatu argumen dalam bentuk koleksi.

Fungsi dengan multi-arity

Hal pertama yang akan kita pelajari adalah fungsi dengan multi-arity. Apa itu arity? Arity adalah jumlah argumen yang bisa diberikan kepada suatu fungsi. Sebagai contoh, fungsi inc yang bisa digunakan untuk increment memiliki arity 1, karena ia hanya menerima 1 argumen. Jika argumen yang diberikan kepada inc bukan 1, maka kita akan mendapatkan ArityException.

user> (inc 10)
;; => 11

;; mencoba arity 2
user> (inc 10 11)
;; Syntax error (ArityException) compiling inc at (*cider-repl ~:localhost:33537(clj)*:53:7).
;; Wrong number of args (2) passed to: clojure.core/inc--inliner--5601

;; arity 0
user> (inc)
;; Syntax error (ArityException) compiling inc at (*cider-repl ~:localhost:33537(clj)*:56:7).
;; Wrong number of args (0) passed to: clojure.core/inc--inliner--5601

Lalu, apa yang dimaksud dengan multi-arity? Multi-arity berarti sebuah fungsi dapat menerima jumlah argumen yang berbeda. Fungsi inc di atas bukanlah fungsi dengan multi-arity karena hanya menerima 1 argumen. Jika inc memiliki multi-arity, maka ia seharusnya bisa menerima 0 atau lebih dari 1 argumen dan contoh REPL di atas pasti tidak memunculkan error.

Multi-arity ini biasanya dipakai untuk mendefinisikan argumen default dari suatu fungsi. Misalkan kita ingin membuat sebuah fungsi yang menampilkan pesan selamat datang kepada pengunjung. Jika pengunjung tersebut sudah terdaftar, kita tampilkan namanya. Jika belum terdaftar, anggaplah ia sebagai “Pengunjung yang budiman”. Berikut adalah definisi fungsi dengan multi-arity.

user> (defn sambut
        ;; definisi untuk arity 0
        ;; "Pengunjung yang budiman" dianggap sebagai nilai default
        ([] (sambut "Pengunjung yang budiman"))
        ;; definisi untuk arity 1
        ([nama] (println (str "Selamat datang, " nama "."))))
;; => #'user/sambut

user> (sambut)
;; Selamat datang, Pengunjung yang budiman.
;; => nil

user> (sambut "Budi")
;; Selamat datang, Budi.
;; => nil

;; akan error karena arity 2 belum didefinisikan
user> (sambut "Ani" "Budi")
;; Execution error (ArityException) at user/eval9205 (REPL:102).
;; Wrong number of args (2) passed to: user/sambut

Dari contoh REPL di atas, kita bisa melihat bahwa badan fungsi untuk tiap arity didefinisikan satu kali. Jadi kita tidak perlu mengetik defn beberapa kali untuk tiap arity. Saat fungsi sambut tidak diberi argumen, maka ia akan memanggil dirinya sendiri namun dengan tambahan Pengunjung yang budiman sebagai nilai default. Dengan begitu, jika seorang pengunjung belum terdaftar, pesan sambutan tersebut masih tetap ditampilkan. Fungsi sambut di atas meskipun sudah multi-arity, tapi hanya bisa menangani 0 dan 1 argumen. Jika kita beri 2 argumen, ia akan error. Ini berarti kita harus mendefinisikan ulang dan menambahkan arity 2 kepada sambut.

user> (defn sambut
        ([] (sambut "Pengunjung yang budiman"))
        ([nama] (println (str "Selamat datang, " nama ".")))
        ([nama1 nama2] (println (str "Selamat datang, " nama1 " & " nama2 "."))))
;; => #'user/sambut

user> (sambut "Ani" "Budi")
;; Selamat datang, Ani & Budi.
;; => nil

Berikut adalah contoh selanjutnya untuk fungsi menghitung nilai kubik dengan multi-arity. Dengan multi-arity, kita bisa meng-handle beberapa user error (seperti lupa jumlah argumen atau salah ketik) saat mereka memakai fungsi kubik.

user> (defn kubik
        ([] (println "Tidak menerima 0 argumen"))
        ([x] (kubik x x x))
        ([x y] (when (= x y)
                 (kubik x x x)))
        ([x y z] (when (= x y z)
                   (* x y z))))
;; => #'user/kubik

;; arity 0
user> (kubik)
;; Tidak menerima 0 argumen
;; => nil

;; arity 1
user> (kubik 3)
;; => 27

;; arity 2 - valid
user> (kubik 3 3)
;; => 27

;; arity 2 - invalid
user> (kubik 3 2)
;; => nil

;; arity 3 - valid
user> (kubik 3 3 3)
;; => 27

;; arity 3 - invalid
user> (kubik 3 3 4)
;; => nil

Sebelum membuat fungsi dengan multi-arity, kita harus perhatikan skenario seperti apa saja yang perlu ditangani dari fungsi yang kita buat agar dapat bekerja sesuai yang diharapkan.

Destrukturisasi Parameter

Contoh fungsi yang sudah kita buat sejauh ini memiliki parameter nilai skalar seperti string, integer, boolean, dsb. Bagaimana jika kita ingin memberikan struktur data koleksi sebagai argumen kepada suatu fungsi? Bagaimana cara mengakses elemen dari koleksi tersebut? Kita bisa melakukan destrukturisasi atau pembongkaran parameter agar kita bisa mengakses nilainya secara lebih ringkas.

Selain pada parameter fungsi, proses destrukturisasi ini bisa dilakukan dengan fungsi let. Mari kita coba lakukan destrukturisasi dengan let terlebih dahulu. Setelah itu, kita terapkan destrukturisasi di let kepada parameter fungsi.

Misalkan kita punya struktur koleksi untuk menyimpan biodata dua orang mahasiswa beserta nilai hasil ujiannya sebagai berikut (keduanya disimpan struktur yang berbeda sebagai contoh).

;; biodata dalam vector
user> (def bio-budi ["Budi" 20 :laki-laki])
;; => #'user/bio-budi

;; biodata + nilai dalam vector bersarang
user> (def nilai-budi ["Budi" 20 :laki-laki [70 85 80 91]])
;; => #'user/nilai-budi

;; biodata dalam maps
user> (def bio-dina {:nama "Dina" :umur 21 
                     :jenis-kelamin :perempuan})
;; => #'user/bio-dina

;; biodata + nilai dalam maps bersarang
user> (def nilai-dina {:nama "Dina" :umur 21 
                       :jenis-kelamin :perempuan 
                       :hasil-ujian {:ujian-1 78
                                     :ujian-2 66
                                     :ujian-3 97
                                     :ujian-4 93}})
;; => #'user/nilai-dina

Destrukturisasi Vector

Baik di parameter fungsi ataupun di fungsi let, proses destrukturisasi terjadi di dalam sebuah vector. Seperti yang sudah dipelajari sebelumnya, tiap binding di dalam vector tersebut terdiri dari sepasang symbol dan nilai. Jika nilai yang akan kita proses disimpan dalam bentuk vector, maka symbol juga harus di dalam vector. Perhatikan contoh berikut.

;; tanpa destrukturisasi.
user> (let [data-utuh bio-budi]
        (println "Data utuh Budi" data-utuh))
;; Data utuh Budi [Budi 20 :laki-laki]
;; => nil

;; dengan  destrukturisasi.
user> (let [[nama usia jenis-kelamin] bio-budi]
        (println "Nama: " nama)
        (println "Usia: " usia)
        (println "Jenis kelamin: " jenis-kelamin))
;; Nama:  Budi
;; Usia:  20
;; Jenis kelamin:  :laki-laki
;; => nil

Di contoh tanpa destrukturisasi, kita menganggap bio-budi sebagai satu kesatuan dan dianggap sebagai skalar, sehingga kita bind dengan data-utuh secara langsung. Elemen dari data-utuh nantinya bisa diakses dengan fungsi get. Di contoh dengan destrukturisasi, kita ingin mengakses elemen dari bio-budi. Oleh karena itu, data-utuh diganti dengan sebuah vector yang mana elemennya adalah symbol yang merepresentasikan posisi tiap elemen. Symbol-symbol tersebut bersifat lokal dan arbitrer (artinya kita bisa menamai mereka apapun). Dari contoh di atas, kita bind bio-budi dengan vector [nama usia jenis-kelamin]. Dengan proses destrukturisasi seperti ini, kita bisa mengakses elemen dari bio-budi dengan nama yang sudah di-bind, yaitu nama, usia, dan jenis-kelamin.

Perlu diperhatikan jika ada symbol yang tidak memiliki pasangan nilai, maka secara default symbol tersebut akan di-bind ke nil. Sebaliknya, bila jumlah symbol lebih sedikit dari jumlah elemen vector, maka elemen yang tidak di-bind akan diabaikan. Selain itu, proses destrukturisasi sebuah list sama seperti dengan vector.

;; sekolah adalah symbol yang tidak memiliki pasangan nilai
;; sehingga dianggap sebagai extra binding dan
;; secara default bernilai nil
user> (let [[nama usia jkelamin sekolah] bio-budi]
        (println nama usia jkelamin sekolah))
;; Budi 20 :laki-laki nil
;; => nil

;; elemen ketiga dari bio-budi diabaikan
user> (let [[nama usia] bio-budi]
        (println nama usia))
;; Budi 20
;; => nil

;; proses destrukturisasi list sama seperti vector
user> (let [[a b c] '(1 2 3)]
        (println a b c))
;; 1 2 3
;; => nil

Jika kita ingin mengakses elemen di vector bersarang seperti nilai-budi di atas, tinggal disesuaikan saja struktur vectornya. Selain itu, jika ingin mengabaikan suatu elemen dalam vector, nama dari elemen tersebut bisa diganti dengan karakter _. Sebagai contoh, kita ingin mengakses elemen nama, usia, nilai ujian 2, dan ujian 3 saja dari nilai-budi. Proses destrukturisasinya adalah sebagai berikut.

;; tanpa destrukturisasi
user> (let [data-utuh nilai-budi]
        (println "Data utuh Budi" data-utuh))
;; Data utuh Budi [Budi 20 :laki-laki [70 85 80 91]]
;; => nil
 
;; dengan destrukturisasi
;; _ berarti elemen diabaikan
user> (let [[nama usia _ [_ u2 u3 _]] nilai-budi]
        (println nama "yang berusia" usia "tahun mendapatkan nilai" u2 "dan" u3))
;; Budi yang berusia 20 tahun mendapatkan nilai 85 dan 80
;; => nil

Destrukturisasi Maps

Untuk mendestruktur maps, prosesnya mirip seperti dengan destrukturisasi vector di atas namun kita harus spesifikasikan keyword-nya. Keyword tersebut harus ada di dalam maps yang ingin kita proses. Selain itu, di dalam vector, kita memakai kurung kurawal {}, bukan kurung siku [].

;; tanpa destrukturisasi
user> (let [data-utuh bio-dina]
        (println "Data utuh Dina" data-utuh))
;; Data utuh Dina {:nama Dina, :umur 21, :jenis-kelamin :perempuan}
;; nil

;; nilai yang terasosiasi dengan tiap keyword di bio-dina 
;; (:nama, :umur, :jenis-kelamin) akan di-bind 
;; ke nama, usia, dan kel
user> (let [{nama :nama
             usia :umur
             kel :jenis-kelamin} bio-dina]
        (println "Nama: " nama)
        (println "Usia: " usia)
        (println "Jenis kelamin: " kel))
;; Nama:  Dina
;; Usia:  21
;; Jenis kelamin:  :perempuan
;; => nil

Selain cara di atas, ada cara alternatif yang lebih singkat untuk mengakses nilai dari maps. Apabila kita tahu keyword apa saja yang ada di suatu maps, kita bisa memakai :keys untuk mengakses nilai di dalamnya. Dengan :keys, keyword yang ada di dalam maps ditempatkan di dalam sebuah vector dan dihilangkan tanda titik dua-nya : seperti contoh di bawah.

;; perhatikan bahwa symbol di dalam vector :keys tidak harus berurutan.
;; selain itu, tanda titik dua : dihilangkan dari tiap keyword.
user> (let [{:keys [jenis-kelamin nama umur]} bio-dina]
        (println "Nama: " nama)
        (println "Usia: " umur)
        (println "Jenis kelamin: " jenis-kelamin))
;; Nama:  Dina
;; Usia:  21
;; Jenis kelamin:  :perempuan
;; => nil

Untuk mendestruktur maps bersarang seperti nilai-dina, kita bisa menggunakan kombinasi destrukturisasi di atas seperti contoh berikut. Perlu diingat bahwa :hasil-ujian terasosiasi dengan sebuah maps yang berisi nilai, sehingga kita bisa memakai :keys untuk mengakses tiap nilai.

user> (let [{nama :nama
             usia :umur
             {:keys [ujian-2 ujian-4]} :hasil-ujian} nilai-dina]
        (println "Name: " nama)
        (println "Usia: " usia)
        (println "Nilai ujian 2 dan ujian 4 adalah " ujian-2 "dan" ujian-4))
;; Name:  Dina
;; Usia:  21
;; Nilai ujian 2 dan ujian 4 adalah  66 dan 93
;; => nil

Setelah mendestruktur koleksi dengan let, kita bisa menerapkan teknik tersebut terhadap parameter fungsi. Dengan destrukturisasi parameter, kita bisa mengakses elemen suatu koleksi secara lebih ringkas. Berikut adalah contoh fungsi dengan destrukturisasi parameter vector (hasil konversi dari contoh let di atas).

user> (defn fn-tanpa-destruk [data]
        (println "Parameter tanpa destrukturisasi:" data))
;; => #'user/fn-tanpa-destruk

user> (defn fn-destruk-vec [[nama usia jkelamin]]
        (println "Nama:" nama)
        (println "Usia:" usia)
        (println "Jenis kelamin:" jkelamin))
;; => #'user/fn-destruk-vec

user> (defn fn-destruk-vec-sarang [[nama usia _ [_ u2 u3 _]]]
        (println "Nama:" nama)
        (println "Usia:" usia)
        (println "Hasil ujian 2 dan 3 adalah:" u2 "dan" u3))
;; => #'user/fn-destruk-vec-sarang

Perhatikan bahwa secara struktur, definisi fungsi dan contoh let di atas sangat mirip. Perbedaannya hanya di nama fungsi dan vector untuk binding yang tidak memiliki nilai yang akan di-bind (bio-budi, nilai-budi, dsb).

Berikut adalah contoh saat kita memakai fungsi di atas untuk memproses bio-budi dan nilai-budi. Perhatikan bahwa fungsi fn-tanpa-destruk memiliki 1 parameter skalar, yang berarti ia bisa menerima berbagai macam struktur koleksi (ataupun skalar) tanpa harus melakukan destrukturisasi.

user> (fn-tanpa-destruk bio-budi)
;; Parameter tanpa destrukturisasi: [Budi 20 :laki-laki]
;; => nil

user> (fn-tanpa-destruk bio-dina)
;; Parameter tanpa destrukturisasi: {:nama Dina, :umur 21, :jenis-kelamin :perempuan}
;; => nil

user> (fn-destruk-vec bio-budi)
;; Nama: Budi
;; Usia: 20
;; Jenis kelamin: :laki-laki
;; => nil

user> (fn-destruk-vec-sarang nilai-budi)
;; Nama: Budi
;; Usia: 20
;; Hasil ujian 2 dan 3 adalah: 85 dan 80
;; => nil

Misalkan data yang kita punya adalah dalam bentuk list (bukan vector), fungsi yang sudah kita buat di atas masih bisa memprosesnya juga.

;; buat list terlebih dahulu
user> (def list-budi '("Budi" 20 :laki-laki))
;; => #'user/list-budi

user> (def list-bersarang-budi '("Budi" 20 :laki-laki (70 85 80 91)))
;; => #'user/list-bersarang-budi

user> (fn-tanpa-destruk list-budi)
;; Parameter tanpa destrukturisasi: (Budi 20 :laki-laki)
;; => nil

user> (fn-destruk-vec list-budi)
;; Nama: Budi
;; Usia: 20
;; Jenis kelamin: :laki-laki
;; => nil

user> (fn-destruk-vec-sarang list-bersarang-budi)
;; Nama: Budi
;; Usia: 20
;; Hasil ujian 2 dan 3 adalah: 85 dan 80
;; => nil

Selanjutnya, mari kita buat fungsi yang dapat melakukan destrukturisasi terhadap parameter dalam bentuk maps. Perhatikan pula bahwa strukturnya tidak jauh berbeda dengan contoh let di atas.

user> (defn fn-destruk-maps [{nama :nama
                              usia :umur
                              kel :jenis-kelamin}]
        (println "Nama: " nama)
        (println "Usia: " usia)
        (println "Jenis kelamin: " kel))
;; => #'user/fn-destruk-maps

user> (defn fn-destruk-maps-keys [{:keys [jenis-kelamin nama umur]}]
        (println "Nama: " nama)
        (println "Usia: " umur)
        (println "Jenis kelamin: " jenis-kelamin))
;; => #'user/fn-destruk-maps-keys

user> (defn fn-destruk-maps-sarang [{nama :nama
                                     usia :umur
                                     {:keys [ujian-2 ujian-4]} :hasil-ujian}]
        (println "Name: " nama)
        (println "Usia: " usia)
        (println "Nilai ujian 2 dan ujian 4 adalah " ujian-2 "dan" ujian-4))
;; => #'user/fn-destruk-maps-sarang

Mari kita coba pakai fungsi di atas untuk memproses maps bio-dina dan nilai-dina.

user> (fn-destruk-maps bio-dina)
;; Nama:  Dina
;; Usia:  21
;; Jenis kelamin:  :perempuan
;; => nil

user> (fn-destruk-maps-keys bio-dina)
;; Nama:  Dina
;; Usia:  21
;; Jenis kelamin:  :perempuan
;; => nil

user> (fn-destruk-maps-sarang nilai-dina)
;; Name:  Dina
;; Usia:  21
;; Nilai ujian 2 dan ujian 4 adalah  66 dan 93
;; => nil

Dari contoh-contoh di atas, kita bisa melihat bahwa fungsi yang sudah kita buat bisa memproses argumen dalam bentuk koleksi.

Penutup

Di bagian ini kita sudah belajar tentang multi-arity dan juga destrukturisasi parameter. Dengan multi-arity, kita bisa mendefinisikan sebuah fungsi yang bisa menerima jumlah argumen yang berbeda. Biasanya digunakan untuk membuat fungsi yang memiliki nilai default. Destrukturisasi parameter membantu kita untuk mengakses elemen dari suatu argumen secara ringkas. Kita bisa memilih elemen mana saja yang kita perlukan di badan fungsi dan mana saja yang bisa diabaikan.

#Clojure #tutorial