Protokol Enumerable

Membangun iterasi khusus dengan each, map, select, dan mixin Enumerable.

Daftar isi

Module Enumerable adalah kontrak iterasi pustaka standar Crystal. Apabila sebuah class melakukan include Enumerable dan mendefinisikan satu metode each, metode turunan seperti map, select, dan count dapat dipanggil pada objeknya tanpa satu baris kode tambahan. Topik ini membangun koleksi khusus bernama Kolam, membuktikan metode-metode turunan itu bekerja atas nama class tersebut, dan menelusuri pola generik yang membuat semuanya mungkin. Setelah membaca topik ini, class koleksi buatan sendiri dapat diperkaya dengan operasi iterasi standar tanpa menyalin satu pun implementasinya.

Satu metode each, seluruh operasi iterasi#

Enumerable(T) adalah module generik yang parameternya menyatakan tipe elemen koleksi. Kontrak yang dituntutnya berupa satu metode each: class yang menyertakan module ini wajib mendefinisikan each, umumnya dengan yield yang menyerahkan satu elemen per kunjungan. Kontrak ini berpola mixin, yaitu penyisipan perilaku module ke dalam class tanpa melewati pewarisan. Sesudah kontrak terpenuhi, compiler menyediakan puluhan metode turunan yang seluruh perhitungannya dibangun di atas each.

Pola ini juga mengubah cara data koleksi diakses. Variabel @isi bersifat privat bagi Kolam, dan pemakai class tersebut tidak perlu mengetahui bentuk penyimpanannya. Apa pun struktur di baliknya, Enumerable hanya menuntut satu jalan keluar bagi elemen, yaitu each yang menyerahkannya satu per satu. Perancang koleksi khusus dengan demikian bebas menentukan isi dan urutan kunjungan, asalkan each tetap disediakan.

Class Kolam pada program berikut mengiterasi kumpulan nama ikan. Metode each miliknya mengurutkan isi sebelum menyerahkannya satu per satu, sehingga urutan kunjungan tidak sama dengan urutan penyimpanan. Sisa program memanggil each secara langsung, kemudian deretan metode turunan yang tidak pernah didefinisikan di dalam Kolam.

crystal kolam.cr
class Kolam
  include Enumerable(String)

  def initialize(@isi : Array(String))
  end

  def each
    @isi.sort.each do |ikan|
      yield ikan
    end
  end
end

kolam = Kolam.new(["nila", "gurame", "lele"])

kolam.each do |ikan|
  puts ikan
end

puts kolam.to_a
puts kolam.map &.upcase
puts kolam.select { |ikan| ikan.size > 4 }
puts kolam.reject { |ikan| ikan.starts_with?("g") }
puts kolam.count { |ikan| ikan.size == 4 }
puts kolam.sum(&.size)
puts kolam.min
puts kolam.max
puts kolam.map(&.size).class
puts kolam.map(&.upcase).class
bash
# jalankan koleksi khusus dengan Enumerable
crystal run kolam.cr
text Keluaran
gurame
lele
nila
["gurame", "lele", "nila"]
["GURAME", "LELE", "NILA"]
["gurame"]
["lele", "nila"]
2
14
gurame
nila
Array(Int32)
Array(String)

Tiga baris pertama berasal dari pemanggilan each secara langsung, dan urutan kunjungannya ditentukan oleh each itu sendiri. to_a mengumpulkan hasil kunjungan menjadi array dengan urutan yang sama. select menyaring elemen yang memenuhi kondisi, sedangkan reject melakukan kebalikannya. count dengan block menghitung nama yang panjangnya empat huruf, sum(&.size) menjumlahkan panjang seluruh nama, dan min beserta max membandingkan isi secara leksikal karena String mencakup Comparable. Dua baris terakhir memperlihatkan tipe hasil map: map(&.size) menghasilkan Array(Int32), sedangkan map(&.upcase) menghasilkan Array(String).

Pola generik di balik metode turunan#

Metode turunan Enumerable ditulis sebagai metode generik terhadap tipe hasil block. Bentuknya dapat ditiru dengan module sendiri untuk melihat mekanismenya tanpa membaca pustaka standar. Program berikut mendefinisikan module IterasiKu dengan metode map_ku yang parameternya &block : T -> U disertai forall U. Anotasi itu menyatakan block mengubah elemen bertipe T menjadi nilai bertipe U, dan U tersimpulkan dari block pada setiap pemanggilan.

crystal map-sendiri.cr
module IterasiKu(T)
  def map_ku(&block : T -> U) forall U
    hasil = [] of U
    each do |elemen|
      hasil << block.call(elemen)
    end
    hasil
  end
end

class Kolam
  include Enumerable(String)
  include IterasiKu(String)

  def initialize(@isi : Array(String))
  end

  def each
    @isi.sort.each do |ikan|
      yield ikan
    end
  end
end

kolam = Kolam.new(["nila", "gurame", "lele"])
puts kolam.map_ku &.size
puts kolam.map_ku &.upcase
puts kolam.map_ku { |ikan| ikan.size > 4 }
bash
# jalankan tiruan pola map dengan module sendiri
crystal run map-sendiri.cr
text Keluaran
[6, 4, 4]
["GURAME", "LELE", "NILA"]
[true, false, false]

Metode map_ku tidak pernah menyebut tipe elemen hasil secara langsung. Pada pemanggilan pertama block menghasilkan Int32, sehingga hasilnya Array(Int32). Pada pemanggilan kedua block menghasilkan String, sehingga hasilnya Array(String). Pada pemanggilan ketiga block menghasilkan Bool. Metode map, select, dan reject milik Enumerable memiliki bentuk yang sama, yaitu satu definisi generik yang memanggil each dan menampung hasil block. Karena itu satu implementasi each pada Kolam langsung melayani seluruh metode turunan dengan tipe hasil apa pun, tanpa perlu menuliskan ulang satu pun metode tersebut untuk tiap tipe.

Manfaat pola ini terasa ketika koleksi berkembang. Apabila Kolam kelak menyimpan elemen bertipe lain, parameter generiknya cukup diganti, misalnya Enumerable(Int32), tanpa menyentuh satu pun metode turunannya. Sebaliknya, tanpa mixin, setiap class harus menuliskan ulang map, select, dan metode lainnya sendiri untuk tiap tipe elemen. Satu definisi each yang melayani puluhan metode dengan tipe hasil bebas adalah titik keseimbangan yang ditawarkan Enumerable.

Kesalahan umum#

Kesalahan yang lazim terjadi adalah menyertakan Enumerable tanpa mendefinisikan each, misalnya karena menyangka parameter generik saja sudah cukup. Compiler menolak class seperti ini pada saat kompilasi, bukan saat program berjalan.

crystal Contoh: menyertakan Enumerable tanpa mendefinisikan each
class Terkunci
  include Enumerable(String)

  def initialize(@isi : Array(String))
  end
end
text Keluaran
Showing last frame. Use --error-trace for full trace.

In each-hilang.cr:1:1

 1 | class Terkunci
     ^
Error: abstract `def Enumerable(T)#each(& : (T ->))` must be implemented by Terkunci

Pesan error menyatakan bahwa each adalah abstract def milik Enumerable yang wajib diimplementasikan oleh Terkunci. Perbaikannya sederhana: definisikan each yang menyerahkan elemen satu per satu, dan seluruh metode turunan langsung tersedia.

Detail kontraknya dapat dibaca langsung dari pesan yang sama. Bagian def Enumerable(T)#each(& : (T ->)) menyatakan each yang diharapkan menerima block dengan satu parameter bertipe T, sedangkan baris In each-hilang.cr:1:1 menunjuk baris pertama definisi class dan bukan baris pemanggilan. Dengan demikian kekurangan kontrak ketahuan sejak class ditulis, jauh sebelum ada objek yang dibuat. Syarat tunggal inilah yang membuat penyertaan module Enumerable ringan diikuti sekaligus sulit dilanggar, karena kekurangan kontraknya tidak pernah lolos dari kompilasi.

Ringkasan#

  • include Enumerable(T) mengikat kontrak berupa satu metode each yang menyerahkan elemen satu per satu pada block pemanggilnya.
  • Sesudah each terpenuhi, metode turunan seperti map, select, reject, count, sum, min, max, dan to_a tersedia tanpa definisi tambahan, dan urutannya mengikuti each.
  • Metode turunan ditulis sebagai metode generik terhadap tipe hasil block, sehingga tipe elemen hasil mengikuti block yang diberikan pada tiap pemanggilan.
  • Class yang menyertakan Enumerable tanpa each ditolak compiler karena each dinyatakan sebagai abstract def.

Referensi#