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.
Enum.count(1..3) Enum.count([1,2,3])
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:
defprotocol MyProtocol do end
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:
defprotocol MyProtocol do def my_func(arg) end
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:
Enum.count 1 # ** (Protocol.UndefinedError) protocol Enumerable not implemented for 1 # (elixir) lib/enum.ex:1: Enumerable.impl_for!/1 # (elixir) lib/enum.ex:146: Enumerable.count/1 # (elixir) lib/enum.ex:467: Enum.count/1
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.
defimpl MyProtocol, for: Integer def my_func(arg) do IO.puts(arg) end end
Sekarang kamu bisa membuat integer-mu dapat dihitung dengan mengimplementasikan sebagian protocol Enumerable
-nya:
defimpl Enumerable, for: Integer do def count(_arg) do {:ok, 1} # integers always contain one element end end Enum.count(100) |> IO.puts # => 1
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:
defimpl MyProtocol, for: [Integer, List] do end
Selain itu, kamu bisa menggunakan Any
:
defimpl MyProtocol, for: Any def my_func(_) do IO.puts "Not implemented!" end end
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)
defprotocol MyProtocol do @fallback_to_any true def my_func(arg) end
Sekarang kamu bisa menggunakan protocol pada tipe apapun yang didukung:
MyProtocol.my_func(5) # simply prints out 5 MyProtocol.my_func("test") # prints "Not implemented!"
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
:
defmodule Product do defstruct title: "", price: 0 defimpl MyProtocol do def my_func(%Product{title: title, price: price}) do IO.puts "Title #{title}, price #{price}" end end end
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:
IO.puts 5 IO.puts "test" IO.puts :my_atom
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:
defmodule Product do defstruct title: "", price: 0 end defmodule Main do def run do %Product{title: "Test", price: 5} |> IO.puts end end Main.run
Setelah menjalankan kodeini, kamu akan mendapatkan sebuah error:
(Protocol.UndefinedError) protocol String.Chars not implemented for %Product{price: 5, title: "Test"}
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?
defmodule Product do defstruct title: "", price: 0 defimpl String.Chars do def to_string(%Product{title: title, price: price}) do "#{title}, $#{price}" end end end
Setelah kode-nya diletakkan, programnya akan mengeluarkan string berikut:
Test, $5
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:
%Product{title: "Test", price: 5} |> IO.inspect # or: %Product{title: "Test", price: 5} |> inspect |> IO.puts
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:
defmodule Product do defstruct title: "", price: 0 defimpl Inspect do def inspect(%Product{title: title, price: price}, _) do "That's a Product struct. It has a title of #{title} and a price of #{price}. Yay!" end end end
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:
defmodule Zoo do defstruct title: "", animals: [] end
Setiap hewan juga akan diwakili oleh sebuah struct:
defmodule Animal do defstruct species: "", name: "", age: 0 end
Sekaring mari buat contoh sebuah zoo baru:
defmodule Main do def run do my_zoo = %Zoo{ title: "Demo Zoo", animals: [ %Animal{species: "tiger", name: "Tigga", age: 5}, %Animal{species: "horse", name: "Amazing", age: 3}, %Animal{species: "deer", name: "Bambi", age: 2} ] } end end Main.run
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:
Enum.count(my_zoo) |> IO.inspect
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:
defmodule Zoo do defstruct title: "", animals: [] defimpl Enumerable do def count(%Zoo{animals: animals}) do {:ok, Enum.count(animals)} end end end
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:
defmodule Zoo do defstruct title: "", animals: [] defimpl Enumerable do # ... def member?(%Zoo{title: _, animals: animals}, animal) do {:ok, Enum.member?(animals, animal)} end end end
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:
Enum.member?(my_zoo, %Animal{species: "tiger", name: "Tigga", age: 5}) |> IO.inspect
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)
def reduce(_, {:halt, acc}, _fun), do: {:halted, acc} def reduce(list, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(list, &1, fun)} def reduce([], {:cont, acc}, _fun), do: {:done, acc} def reduce([h | t], {:cont, acc}, fun), do: reduce(t, fun.(h, acc), fun)
Kita bisa menggunakan kode ini sebagai sebuah contoh dan kode kita sebagai implementasi dari struct Zoo
:
defmodule Zoo do defstruct title: "", animals: [] defimpl Enumerable do def reduce(_, {:halt, acc}, _fun), do: {:halted, acc} def reduce(%Zoo{animals: animals}, {:suspend, acc}, fun) do {:suspended, acc, &reduce(%Zoo{animals: animals}, &1, fun)} end def reduce(%Zoo{animals: []}, {:cont, acc}, _fun), do: {:done, acc} def reduce(%Zoo{animals: [head | tail]}, {:cont, acc}, fun) do reduce(%Zoo{animals: tail}, fun.(head, acc), fun) end end end
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:
Enum.reduce(my_zoo, 0, fn(animal, total_age) -> animal.age + total_age end) |> IO.puts
Pada dasarnya, sekarang kita memiliki akses ke semua fungsi yang disediakan oleh modul Enum
. Mari coba gunakan join/2:
Enum.join(my_zoo) |> IO.inspect
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:
defmodule Animal do defstruct species: "", name: "", age: 0 defimpl String.Chars do def to_string(%Animal{species: species, name: name, age: age}) do "#{name} (#{species}), aged #{age}" end end end
Sekarang seharusnya semua berjalan baik. Dan juga kamu bisa coba menjalankan each/2 untuk menampilkan tiap hewan:
Enum.each(my_zoo, &(IO.puts(&1)))
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!