7 days of WordPress plugins, themes & templates - for free!* Unlimited asset downloads! Start 7-Day Free Trial
Advertisement
  1. Code
  2. Elixir

Polymorphism Dengan Protocols di Elixir

Scroll to top
Read Time: 11 mins

Indonesian (Bahasa Indonesia) translation by Husain Ali Yahya (you can also view the original English article)

Polymorphism adalah sebuah konsep penting dalam pemograman, dan programmer pemula biasa-nya mempelajari-nya selama bulan pertama dari pembelajarannya. Polymorphism pada dasarnya berarti bahwa kamu bisa menerapkan operasi semisal ke aneka entitas yang berbeda. Contohnya; fungsi count/1 bisa diterapkan baik ke sebuah rentang maupun daftar.

Bagaimana itu mungkin? Dalam Elixir, polymorphism dicapai dengan menggunakan sebuah fitur menarik yang disebut protocol, yang bertingkah seperti sebuah kontrak. Untuk tiap data yang ingin kamu dukung, protocol-nya harus diimplementasikan.

Seutuhnya, pendekatan ini tidaklah revolusioner, karena ini juga ada di bahasa llainnya (di Ruby contohnya). Tetap saja, protocol sangatlah berguna, sehingga pada artikel ini kita kan membahas cara mendefinsikan, mengimplementasikan dan bekerja dengan mereka sambil menelusuri beberapa contoh. Mari mulai!

Pengantar Singkat ke Protocol

Jadi, seperti yang sudah disebutkan sebelumnya, sebuah protokol memiliki beberapa kode generik yang bergantung pada tipe data spesifik untuk mengimplementasikan logika-nya. Ini logis, karena tipe data yang berbeda membutuhkan implementasi yang berbeda. Sebuah tipe data bisa dikirlm pada sebuah protocol tanpa mengkhawatirkan internal-nya.

Elixir memiliki banyak protocol bawaan, termasuk Enumerable, Collectable, Inspect, List.Chars dan String.Chars. Beberapa dari mereka akan dibahas nanti dalam artikel ini. Kamu bisa menerapkan protokol ini dalam modul kostum-mu dan mendapatkan aneka fungsi secara gratis. Contohnya, setelah mengimplementasikan Enumerable, kamu akan bisa mengakses semua fungsi yang didefinisikan dalam Enum module yang cukup keren.

Jika kamu berasal dari dunia Ruby yang menakjubkan dan penuh dengan objek, class, peri dan naga, kamu akan mendapatkan konsep yang mirp dengan mixins. Contohnya, jika kamu harus membuat objek-objek-mu dapat dibandingkan, cukup camput sebuah modul dengan nama yang berhubungan ke dalam class. Lalu, cukup implementasikan metode spaceship <=> dan semua contoh dari kelasnya akan mendapatkan metode-metode seperti > dan < secara gratis. Mekanisme ini mirip dengan protocol di Elixir, Bahkan jika kamu belum pernah bertemu dengan konsep ini sebelumnya, percayalah, ini tidak serumit itu.

Oke, ke hal yang pertama dahulu: Protocol-nya harus didefinisikan, jadi mari lihat cara melakukannya di bagian berikutnya.

Mendefinisikan sebuah Protokol

Mendefinisikan sebuah protokol tidak menggunakan sihir hitam apapun-pada kenyataannya ini mirip dengan pendefinisian modul. Gunakan defprotocol/2 untuk melakukannya:

Di dalam definisi protokol kamu meletakkan fungsi-fungsi, sama seperti modul. Perbedaannya hanyalah bahwa fungsi ini tidak memiliki body. Ini berarti bahwa protokol-nya hanya mendefinisikan sebuah antarmuka, sebuah cetak biru yang harus diimplementasikan oleh semua tipe data yang ingin dikirim melalui protokol ini:

Pada contoh ini, seorang programmer harus mengimplementasikan fungsi my_func/1 untuk menggunakan MyProtocol dengan sukses.

Jika protokolnya tidak diimplementasikan, sebuah error akan muncul, Mari kembali ke contoh dengan fungsi count/1 yang didefinisikan di dalam modul Enum. Menjalankan kode berikut akan berakhir dengan sebuah error:

Ini berarti bahwa Integer-nya tidak mengimplementasikan protocol Enumerable (sebuah kejutan) karenanya kita tidak bisa menghitung integers. Tapi sebenarnya protocol-nya bisa diimplementasikan dan ini mudah untuk dicapai.

Mengimpelementasikan sebuah Protokol

Protokol diimplementasikan menggunakan makro defimpl/3. Kamu spesifikasikan protokol mana yang diimplementasikan dan untuk tipe apa.

Sekarang kamu bisa membuat integer-mu dapat dihitung dengan mengimplementasikan sebagian protocol Enumerable-nya:

Kita akan membahas protocol Enumerable secara lebih detil nanti dalam artikel ini dan mengimplementasikan fungsi lainnya juga.

Seperti pada type (dilewatkan ke for -nya), kam bisa menspesifikasikan tipe bawaan apapun, alias milikmu sendiri atau daftar dari alias-alias:

Selain itu, kamu bisa menggunakan Any:

Ini akan bertingkah seperti implementasi fallback, dan sebuah error tidak akan muncul jika protocolnya tidak diimplementasikan pada beberapa tipe. Untuk membuatnya bekerja, atur atribut @fallback_to_any menjadi true di dalam protocol-mu (jika tidak errornya tetap akan muncul)

Sekarang kamu bisa menggunakan protocol pada tipe apapun yang didukung:

Sebuah Catatan Mengenai Structs

Implementasi dari sebuah protokol bisa disarangkan ke dalam sebuah modul. Jika modul ini mendefinisikan sebuah struct, kamu bahkan tidak perlu menspesifikasikan for ketika memanggil defimpl:

Pada contoh ini, kita mendefinisikan sebuah struct baru bernama Product dan mengimplementasikan protocol demo kita. Di dalamnya, cukup cocokkan pola judul dan harga-nya lalu mengeluarkan sebuah string.

Namun ingat, bahwa sebuah implementasi harus disarangkan dalam sebuah modul-ini berarti kamu bisa memperluas dengan mudah modul apapun tanpa mengakses kode sumber-nya.

Contoh: Protocol String.Chars

Oke, cukup dengan teori abstrak:mari perhatikan beberapa contoh. Saya yakin kamu sudah cukup sering menggunakan fungsi IO.puts/2 untuk mengeluarkan info debugging ke konsol ketika menggunakan Elixir. Tentu saja, kita bisa mengeluarkan aneka tipe bawaan secara mudah:

Tapi apa yang terjadi jika kita mencoba untuk mengeluarkan struct Product kita yang dibuat pada bagian sebelumnya? Saya akan meletakkan kode yang berhubungan di dalam modul Main karena jika tidak kamu akan mendapati error yang mengatakan bahwa struct-nya tidak didefinisikan atau diakses dalam lingkup yang sama:

Setelah menjalankan kodeini, kamu akan mendapatkan sebuah error:

Aha! Ini berarti fungsi puts bergantung pada protocol String.Charts bawaan. Sepanjang dia tidak diimplementasikan pada Product kita, error akan muncul.

String.Chars bertangung jawab untuk mengonversi aneka struktur ke binary, dan satu-satunya fungsi yang kamu harus terapkan adalah to_string/1 seperti yang dinyatakan oleh dokumentasinya. Kenapa kita tidak implementasikan itu sekarang?

Setelah kode-nya diletakkan, programnya akan mengeluarkan string berikut:

Yang berarti semua-nya berjalan dengan baik!

Contoh: Protocol Inspect

Salah satu fungsi lain yang sangat umum adalah IO.inspect/2 untuk mendapatkan informasi mengenai sebuah construct. Ada juga fungsi inspect/2 yang didefinisikan di dalam modul Kernel-dia menjalankan inspeksi berdasarkan protocol inspect bawaan.

Struct Product kita bisa diinspeksi, dan kamu akan mendapatkan informasi singkat mengenai-nya:

Dia akan mengembalikan %Product{price: 5, title: "Test"}. Tapi sekali lagi, kita bisa dengan mudah mengimplementasikan protocol Inspect yang hanya membutuhkan fungsi inspect/2 diketik:

Argumen kedua yang dilewatkan ke fungsi ini adalah daftar dari opsi-opsi, tapi kita tidak terarik pada mereka.

Contoh: Protocol Enumerable

Sekarang mari lihat, contoh yang sedikit lebih rumit sambil membahas protocol Enumerable. Protocol ini dikerjakan oleh modul Enum yang menghadirkan fungsi seperti each/2 dan count/1 (tanpa dia, kamu akan tetap menggunakan rekursi yang tua).

Enumerable mendefinisikan tiga fungsi yang harus kamu sempurnakan untuk mengimplementasikan protocol-nya:

  • count/1 mengembalikan ukuran enumerable.
  • member?/2 mengecek apakah enumerable-nya mengandung sebuah elemen.
  • reduce/3 menerapkan sebuah fungsi ke tiap elemen dari enumerable.

Setelah semua fungsi tersebut diletakkan, kamu akan mendaptkan akses ke semua yang disediakan oleh modul Enum yang sangat bagus.

Sebagai contoh, mari buat sebuah struct baru bernama Zoo. Dia akan memiliki sebuah judul dan daftar dari hewan-hewan:

Setiap hewan juga akan diwakili oleh sebuah struct:

Sekaring mari buat contoh sebuah zoo baru:

Sekarang kita memiliki sebuah "Demo Zoo" dengan tiga hewan: macan, kuda, dan rusa. Apa yang ingin saya lakukan sekarang adalah menambahkan dukungan fungsi count/1 yang akan digunakan seperti ini:

Mari terapkan fungsionalitas ini sekarang!

Mengimplementasikan Fungsi Count

Apa yang kita maksud ketika mengatakan "count my zoo"? Ini terdengar sedikit asing. Tapi ini mungkin berarti menghitung semua hewan yang hidup di sana. Jadi implementasi dari fungsi yang mendasari-nya akan cukup sederhana:

Semua yang kita lakukan di sini adalah bergantung pada fungsi count/1 sambil melewatkan daftar dari hewan-hewan ke dalamnya (karena fungsi ini mendukung list) Hal yang sangat penting untuk disebutkan adalah bahwa fungsi count/1 harus mengembalikan hasil ke bentuk sebuah tuple {:ok, result} seperti yang dikatakan oleh dokumentasinya. Jika kamu hanya mengembalikan angka, sebuah error ** (CaseClauseError) no case clause matching akan muncul.

Cukup. Sekarang kamu bisa mengetik Enum.count(my_zoo) di dalam Main.run dan seharusnya dia mengembalikan angka 3 sebagai hasilnya. Kerja bagus!

Mengimplementasikan Member? Fungsi

Fungsi berikutnya yang protokol definisikan adalah member?/2. Dia seharusnya mengembalikan sebuah tuple {:ok, boolean} sebagai hasil yang mengatakan apakah sebuah enumerable (yang dilewati argumen pertama) mengandung sebuah elemen (argumen kedua).

Saya ingin fungsi baru ini untuk mengatakan apakah sebuah hewan ada di dalam kebun binatang atau tidak. Maka, implementasi-nya cukup sederhana juga:

Sekali lagi, ingat, bahwa fungsi-nya menerima dua argumen: sebuah enumerable dan sebuah elemen. Di dalamnya kita menggunakan fungsi member?/2 untuk mencari sebuah hewan dari daftar semua hewan.

Jadi sekarang kita menjalankan:

Dan ini seharusnya mengembalikan nilai true karena kita memiliki hewan tersebut dalam daftar!

Mengimplementasikan Fungsi Reduce

Hal-hal menjadi sedikit lebih rumit dengan fungsi reduce/3. Dia menerima argumen berikut:

  • Sebuah enumerable untuk menerapkan fungsi ke sana
  • Sebuah accumulator untuk menyimpan hasil-nya
  • Fungsi reducer sebenarnya untuk diterapkan

Apa yang menarik adalah bahwa sebenarnya accumulator hanya berisi satu tuple dengan dua nilai: sebuah verb dan sebuah value: {verb, value}. Verbnya adalah sebuah atom dan bisa memiliki salah satu dari tiga nilai berikut:

  • :cont (melanjutkan)
  • :halt (mengakhiri)
  • :suspend (penangguhan sementara)

Nilai yang dikembalikan oleh fungsi reduce/3 juga sebuah tuple yang mengandung state dan sebuah hasil. State-nya juga sebuah atom dan bisa memiliki nilai-nilai berikut:

  • :done (prosesnya selesai, itu adalah hasil akhir-nya)
  • :halted (proses-nya terhenti karena accumulatornya mengandung verb :halt)
  • :suspended (proses-nya ditangguhkan)

Jika prosesnya ditangguhkan, kita harus mengembalikan sebuah fungsi yang mewakili state saat ini dari prosesnya.

Semua kebutuhan ini didemonstrasikan dengan baik oleh implementasi fungsi reduce/3 untuk daftar-daftarnya (diambil dari dokumentasi)

Kita bisa menggunakan kode ini sebagai sebuah contoh dan kode kita sebagai implementasi dari struct Zoo:

Dalam klausa fungsi terakhir, kita mengambil kepala0nya yang mengandung semua hewan, menerapkan fungsi kepadana, dan menjalankan reduce yang melawan ekornya. Ketika tidak ada lagi hewan yang tersisa (klause ketiga), kita mengembalikan sebuah tuple dengan state :done dan hasil akhirnya. Klause pertama mengembalikan sebuah hasil jika prosesnya dihentikan. Klause kedua mengembalikan sebuah fungsi jika verb :suspend dilewatkan.

Sekarang, sebagai contoh, kita bisa menghitung total umur dari semua hewan kita dengan mudah:

Pada dasarnya, sekarang kita memiliki akses ke semua fungsi yang disediakan oleh modul Enum. Mari coba gunakan join/2:

Namun, kamu akan mendapatkan sebuah error yang mengatakan bahwa protocol String.Chars tidak diimplementasikan untuk struct Animal. Ini terjadi karena join berusaha untuk mengonversi tiap elemen ke sebuah string namun tidak bisa melakukannya untuk Animal. Maka, mari implementasikan protocol String.Chars sekarang:

Sekarang seharusnya semua berjalan baik. Dan juga kamu bisa coba menjalankan each/2 untuk menampilkan tiap hewan:

Sekali lagi, ini bekerja karena kita telah mengimpelemntasikan dua protocol: Enumerable (untuk Zoo) dan String.Chars (untuk Animal)

Kesimpulan

Dalam artikel ini, kita telah membahas cara polymorphism diterapkan menggunakan protocol Elixir. Kamu telah belajar cara mendefinisikan dan mengimplementasikan protocol dan juga menggunakan protocol bawaan: Enumerable, Inspect, dan String.Chars.

Sebagai latihan, kamu bisa perkuat modul Zoo kita dengan Colectible protocol sehingga fungsi Enum.into/2 bisa digunakan dengan benar. Protocol ini memerlukan implementasi hanya dari satu fungsi: into/2 yang mengumpulkan nilai-nilai dan mengembalikan hasilnya (ingat bahwa dia juga harus mendukung verb :done, :halt, dan :cont; state-nya tidak perlu dilaporkan) Bagikan solusi-mu dalam komentar!

Saya harap kamu menikmati artikel ini. Jika kamu memiliki pertanyaan jangan ragu untuk menghubungi saya. terima kasih untuk kesabarannya, sampai bertemu lagi!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.