Enumerable #
Hampir setiap program Ruby yang pernah kamu baca atau tulis menggunakan Enumerable — seringkali tanpa sadar. Ketika kamu memanggil .map, .select, atau .sort_on pada sebuah Array, kamu sedang menggunakan metode dari modul Enumerable. Modul ini adalah salah satu kontribusi terbesar Ruby terhadap cara kita menulis kode — ia mengubah operasi yang biasanya membutuhkan loop eksplisit menjadi ekspresi deklaratif yang terbaca seperti kalimat. Artikel ini membahas cara kerja Enumerable dari dalam, method-method yang paling sering dipakai, teknik chaining untuk pipeline data yang bersih, dan lazy enumeration untuk koleksi yang besar atau tak terbatas.
Apa Itu Enumerable? #
Enumerable adalah modul Ruby yang menyediakan puluhan method untuk bekerja dengan koleksi — iterasi, pencarian, transformasi, pengurutan, dan agregasi. Cara kerjanya simpel: kamu hanya perlu mengimplementasikan satu method, yaitu each, dan Enumerable secara otomatis memberikan semua method lainnya.
# Array dan Hash sudah include Enumerable secara default
[1, 2, 3].class.ancestors
# => [Array, Enumerable, Object, Kernel, BasicObject]
{ a: 1 }.class.ancestors
# => [Hash, Enumerable, Object, Kernel, BasicObject]
# Kamu bisa membuat kelas sendiri yang Enumerable
class DaftarBuku
include Enumerable
def initialize
@buku = []
end
def tambah(buku)
@buku << buku
self
end
# Satu-satunya method yang wajib diimplementasikan
def each(&block)
@buku.each(&block)
end
end
daftar = DaftarBuku.new
daftar.tambah("Sapiens").tambah("Atomic Habits").tambah("Deep Work")
# Setelah implementasi each, semua method Enumerable tersedia
daftar.map(&:upcase)
# => ["SAPIENS", "ATOMIC HABITS", "DEEP WORK"]
daftar.select { |b| b.length > 7 }
# => ["Sapiens", "Atomic Habits", "Deep Work"]
daftar.sort
# => ["Atomic Habits", "Deep Work", "Sapiens"]
flowchart TD
A[Kelas dengan include Enumerable] --> B[Implementasi method each]
B --> C[Enumerable menyediakan semua method lain]
C --> D[map / flat_map]
C --> E[select / reject / filter]
C --> F[reduce / inject]
C --> G[sort / sort_by / min / max]
C --> H[group_by / tally / chunk]
C --> I[find / detect / any? / all? / none?]
C --> J[each_with_object / each_with_index]
C --> K[lazy / take / first]Konsekuensi dari desain ini sangat elegan: semua kelas yang bisa di-iterate — Array, Hash, Range, Set, atau kelas buatanmu sendiri — berbagi API yang sama. Kamu belajar Enumerable sekali, dan kamu bisa menggunakannya di mana saja.
Iterasi Dasar #
Sebelum masuk ke method yang lebih kompleks, penting untuk memahami variasi iterasi dasar yang sering dipakai dan perbedaannya.
each adalah fondasi — ia mengiterasi koleksi dan mengeksekusi blok untuk setiap elemen, lalu mengembalikan koleksi aslinya. each_with_index menambahkan index ke dalam blok. each_with_object berguna ketika kamu ingin membangun objek selama iterasi.
nama = ["alice", "bob", "charlie"]
# each — iterasi murni, kembalikan koleksi asli
nama.each { |n| puts n.capitalize }
# Alice
# Bob
# Charlie
# each_with_index — ketika kamu butuh posisi elemen
nama.each_with_index do |n, i|
puts "#{i + 1}. #{n.capitalize}"
end
# 1. Alice
# 2. Bob
# 3. Charlie
# each_with_object — membangun objek baru selama iterasi
hasil = nama.each_with_object({}) do |n, hash|
hash[n] = n.length
end
# => {"alice"=>5, "bob"=>3, "charlie"=>7}
# each_slice — memproses elemen dalam kelompok N
(1..10).each_slice(3) { |kelompok| p kelompok }
# [1, 2, 3]
# [4, 5, 6]
# [7, 8, 9]
# [10]
# each_cons — sliding window ukuran N
(1..5).each_cons(3) { |window| p window }
# [1, 2, 3]
# [2, 3, 4]
# [3, 4, 5]
# ANTI-PATTERN: menggunakan each untuk membangun array baru
hasil = []
[1, 2, 3, 4, 5].each { |x| hasil << x * 2 }
# => [2, 4, 6, 8, 10]
# BENAR: gunakan map — lebih deklaratif, tidak ada state mutation
hasil = [1, 2, 3, 4, 5].map { |x| x * 2 }
# => [2, 4, 6, 8, 10]
Transformasi dengan map dan flat_map #
map (alias collect) adalah method transformasi paling sering dipakai. Ia mengambil setiap elemen, mengeksekusi blok, dan mengembalikan array baru berisi hasil transformasi. Koleksi asli tidak diubah.
angka = [1, 2, 3, 4, 5]
# map — transformasi satu-ke-satu
angka.map { |n| n ** 2 }
# => [1, 4, 9, 16, 25]
angka.map { |n| n.even? ? "genap" : "ganjil" }
# => ["ganjil", "genap", "ganjil", "genap", "ganjil"]
# map dengan symbol — shorthand untuk memanggil method
["hello", "world", "ruby"].map(&:upcase)
# => ["HELLO", "WORLD", "RUBY"]
["1", "2", "3"].map(&:to_i)
# => [1, 2, 3]
# map pada Hash — mengiterasi pasangan [key, value]
{ a: 1, b: 2, c: 3 }.map { |k, v| "#{k}=#{v}" }
# => ["a=1", "b=2", "c=3"]
# Jika ingin hasil berupa Hash, tambahkan .to_h
{ a: 1, b: 2, c: 3 }.map { |k, v| [k, v * 10] }.to_h
# => {:a=>10, :b=>20, :c=>30}
# Atau gunakan transform_values / transform_keys (Ruby 2.4+)
{ a: 1, b: 2, c: 3 }.transform_values { |v| v * 10 }
# => {:a=>10, :b=>20, :c=>30}
flat_map berguna ketika blok menghasilkan array dan kamu ingin hasilnya di-flatten satu level.
kata = ["hello world", "foo bar", "ruby enumerable"]
# ANTI-PATTERN: map lalu flatten terpisah
kata.map { |k| k.split(" ") }.flatten
# => ["hello", "world", "foo", "bar", "ruby", "enumerable"]
# BENAR: flat_map langsung
kata.flat_map { |k| k.split(" ") }
# => ["hello", "world", "foo", "bar", "ruby", "enumerable"]
# Contoh praktis: mengambil semua tag dari kumpulan artikel
artikel = [
{ judul: "Ruby Tips", tag: ["ruby", "programming"] },
{ judul: "Web Dev", tag: ["rails", "ruby", "web"] },
{ judul: "Testing", tag: ["rspec", "testing"] }
]
semua_tag = artikel.flat_map { |a| a[:tag] }
# => ["ruby", "programming", "rails", "ruby", "web", "rspec", "testing"]
semua_tag.uniq.sort
# => ["programming", "rails", "rspec", "ruby", "testing", "web"]
Filter dengan select, reject, dan filter_map #
select (alias filter) mengembalikan elemen-elemen yang membuat blok mengembalikan nilai truthy. reject adalah kebalikannya — mengembalikan elemen yang membuat blok mengembalikan falsy.
angka = (1..10).to_a
# select — ambil yang memenuhi kondisi
angka.select { |n| n.even? }
# => [2, 4, 6, 8, 10]
angka.select { |n| n > 5 }
# => [6, 7, 8, 9, 10]
# reject — buang yang memenuhi kondisi
angka.reject { |n| n.even? }
# => [1, 3, 5, 7, 9]
# Pada Hash
produk = { apel: 5000, mangga: 15000, jeruk: 8000, durian: 45000 }
produk.select { |nama, harga| harga < 10_000 }
# => {:apel=>5000, :jeruk=>8000}
produk.reject { |nama, harga| harga > 20_000 }
# => {:apel=>5000, :mangga=>15000, :jeruk=>8000}
filter_map adalah kombinasi select + map dalam satu operasi — berguna ketika kamu ingin memfilter sekaligus mentransformasi, dan tidak ingin nilai nil masuk ke hasil.
data = ["1", "abc", "2", nil, "3", "", "4"]
# ANTI-PATTERN: select lalu map, dua iterasi
data.select { |x| x&.match?(/\d+/) }.map(&:to_i)
# => [1, 2, 3, 4]
# BENAR: filter_map — satu iterasi, lebih bersih
data.filter_map { |x| x&.to_i if x&.match?(/\d+/) }
# => [1, 2, 3, 4]
# Contoh praktis: proses response API yang mungkin ada nil
response = [
{ id: 1, nilai: "42" },
{ id: 2, nilai: nil },
{ id: 3, nilai: "invalid" },
{ id: 4, nilai: "99" }
]
nilai_valid = response.filter_map do |item|
Integer(item[:nilai], exception: false)
end
# => [42, 99]
Pencarian dengan find, any?, all?, none?, dan count #
Ketika kamu perlu menemukan elemen tertentu atau memeriksa kondisi pada koleksi, Enumerable menyediakan method yang ekspresif dan terbaca seperti kalimat bahasa manusia.
pengguna = [
{ nama: "Alice", umur: 28, aktif: true },
{ nama: "Bob", umur: 17, aktif: false },
{ nama: "Charlie", umur: 35, aktif: true },
{ nama: "Diana", umur: 22, aktif: true }
]
# find / detect — kembalikan elemen pertama yang cocok
pengguna.find { |u| u[:umur] >= 18 && u[:aktif] }
# => {:nama=>"Alice", :umur=>28, :aktif=>true}
# find_index — kembalikan index elemen pertama yang cocok
pengguna.find_index { |u| u[:nama] == "Charlie" }
# => 2
# any? — apakah ada setidaknya satu yang cocok?
pengguna.any? { |u| u[:umur] < 18 }
# => true
# all? — apakah semua cocok?
pengguna.all? { |u| u[:nama].length > 0 }
# => true
# none? — apakah tidak ada yang cocok?
pengguna.none? { |u| u[:umur] > 100 }
# => true
# one? — apakah tepat satu yang cocok?
pengguna.one? { |u| !u[:aktif] }
# => true
# count — hitung yang cocok
pengguna.count { |u| u[:aktif] }
# => 3
pengguna.count # tanpa blok: hitung semua elemen
# => 4
findmengembalikanniljika tidak ada elemen yang cocok, bukan array kosong. Jika kamu memanggil method pada hasilfindtanpa pengecekan, kamu akan mendapatkanNoMethodError: undefined method '...' for nil. Gunakan safe navigation operator&.atau pastikan hasilnya tidak nil sebelum diproses lebih lanjut.# ANTI-PATTERN: langsung akses property tanpa cek nil admin = pengguna.find { |u| u[:role] == "admin" } admin[:nama] # => NoMethodError jika tidak ditemukan! # BENAR: gunakan safe navigation admin&.fetch(:nama, "tidak ditemukan")
Agregasi dengan reduce dan inject #
reduce (alias inject) adalah method yang paling powerful sekaligus paling sering disalahgunakan. Ia mengakumulasi nilai dari seluruh koleksi menjadi satu nilai tunggal — cocok untuk penjumlahan, perkalian, membangun string, atau konstruksi data struktur kompleks.
angka = [1, 2, 3, 4, 5]
# reduce dengan initial value dan blok
angka.reduce(0) { |akumulasi, n| akumulasi + n }
# => 15
# reduce dengan symbol — shorthand untuk operasi binary
angka.reduce(:+) # => 15
angka.reduce(:*) # => 120
angka.reduce(10, :+) # dengan initial value: 10 + 1 + 2 + 3 + 4 + 5 = 25
# Mencari nilai maksimum dan minimum
angka.reduce { |maks, n| n > maks ? n : maks } # => 5
angka.reduce { |min, n| n < min ? n : min } # => 1
# (Untuk ini, lebih baik pakai min/max langsung)
# Membangun Hash dari array
kata = ["ruby", "python", "golang"]
kata.reduce({}) { |hash, k| hash.merge(k => k.length) }
# => {"ruby"=>4, "python"=>6, "golang"=>6}
# Menggabungkan string
["Hello", "World", "Ruby"].reduce { |hasil, kata| "#{hasil} #{kata}" }
# => "Hello World Ruby"
# Contoh nyata: menghitung total harga dari keranjang belanja
keranjang = [
{ nama: "Buku", harga: 75_000, jumlah: 2 },
{ nama: "Pulpen", harga: 5_000, jumlah: 5 },
{ nama: "Tas", harga: 150_000, jumlah: 1 }
]
total = keranjang.reduce(0) { |sum, item| sum + (item[:harga] * item[:jumlah]) }
# => 375_000
# Dengan sum yang lebih expressif (Ruby 2.4+)
total = keranjang.sum { |item| item[:harga] * item[:jumlah] }
# => 375_000
# ANTI-PATTERN: reduce untuk tugas yang punya method khusus
angka.reduce(0) { |sum, n| sum + n } # ✗ terlalu verbose untuk penjumlahan
angka.reduce { |min, n| n < min ? n : n } # ✗ gunakan min saja
# BENAR: gunakan method yang lebih spesifik jika tersedia
angka.sum # ✓ penjumlahan
angka.min # ✓ nilai terkecil
angka.max # ✓ nilai terbesar
angka.minmax # ✓ [min, max] sekaligus
Pengurutan dengan sort, sort_by, min, max #
Enumerable menyediakan beberapa cara untuk mengurutkan dan menemukan nilai ekstrem dalam koleksi. Memilih method yang tepat berdampak pada keterbacaan dan juga performa.
angka = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3]
# sort — pengurutan default (ascending)
angka.sort
# => [1, 1, 2, 3, 3, 4, 5, 5, 6, 9]
# sort dengan custom comparator
angka.sort { |a, b| b <=> a } # descending
# => [9, 6, 5, 5, 4, 3, 3, 2, 1, 1]
# min dan max
angka.min # => 1
angka.max # => 9
angka.minmax # => [1, 9]
# min_by dan max_by — berdasarkan kriteria
kata = ["banana", "apple", "kiwi", "strawberry", "fig"]
kata.min_by(&:length) # => "fig"
kata.max_by(&:length) # => "strawberry"
kata.minmax_by(&:length) # => ["fig", "strawberry"]
sort_by adalah method yang lebih sering dipakai daripada sort dengan blok, karena menggunakan algoritma Schwartzian Transform secara internal — lebih efisien ketika kriteria pengurutan mahal dihitung.
pengguna = [
{ nama: "Charlie", skor: 85 },
{ nama: "Alice", skor: 92 },
{ nama: "Bob", skor: 78 },
{ nama: "Diana", skor: 95 }
]
# sort_by — lebih bersih dari sort dengan blok
pengguna.sort_by { |u| u[:skor] }
# => [{nama:"Bob", skor:78}, {nama:"Charlie", skor:85}, ...]
# Descending: negate nilainya
pengguna.sort_by { |u| -u[:skor] }
# => [{nama:"Diana", skor:95}, {nama:"Alice", skor:92}, ...]
# Multi-kriteria: sort by array
pengguna_extended = [
{ nama: "Alice", departemen: "Engineering", skor: 85 },
{ nama: "Bob", departemen: "Design", skor: 92 },
{ nama: "Charlie", departemen: "Engineering", skor: 78 },
{ nama: "Diana", departemen: "Design", skor: 85 }
]
# Sort by departemen, lalu by skor descending dalam departemen yang sama
pengguna_extended.sort_by { |u| [u[:departemen], -u[:skor]] }
# => [Diana/Design/85, Bob/Design/92 dibalik jadi...]
# Sort by [departemen asc, skor desc]
# ANTI-PATTERN: sort dengan blok perbandingan untuk kriteria sederhana
data.sort { |a, b| a[:nama] <=> b[:nama] }
# BENAR: sort_by lebih bersih dan lebih efisien
data.sort_by { |item| item[:nama] }
data.sort_by(&:nama) # jika nama adalah method/attr accessor
Pengelompokan dengan group_by, tally, dan chunk #
Ketika kamu perlu mengelompokkan data berdasarkan kriteria tertentu, Enumerable menyediakan method yang sangat berguna untuk kasus ini.
group_by mengembalikan Hash di mana key-nya adalah hasil blok dan value-nya adalah array elemen yang menghasilkan key tersebut.
transaksi = [
{ id: 1, jenis: "debit", jumlah: 50_000 },
{ id: 2, jenis: "kredit", jumlah: 200_000 },
{ id: 3, jenis: "debit", jumlah: 75_000 },
{ id: 4, jenis: "kredit", jumlah: 100_000 },
{ id: 5, jenis: "debit", jumlah: 30_000 }
]
# group_by — kelompokkan berdasarkan kriteria
per_jenis = transaksi.group_by { |t| t[:jenis] }
# => {
# "debit" => [{id:1,...}, {id:3,...}, {id:5,...}],
# "kredit" => [{id:2,...}, {id:4,...}]
# }
# Hitung total per jenis
per_jenis.transform_values { |trx| trx.sum { |t| t[:jumlah] } }
# => {"debit" => 155_000, "kredit" => 300_000}
# group_by dengan kriteria yang dihitung
(1..10).group_by { |n| n % 3 }
# => {1=>[1, 4, 7, 10], 2=>[2, 5, 8], 0=>[3, 6, 9]}
# group_by untuk mengelompokkan objek by class
data_campuran = [1, "hello", 2, "world", :sym, 3, :lain]
data_campuran.group_by(&:class)
# => {Integer=>[1, 2, 3], String=>["hello", "world"], Symbol=>[:sym, :lain]}
tally menghitung berapa kali setiap elemen muncul dalam koleksi.
# tally — hitung frekuensi kemunculan
["apel", "mangga", "apel", "jeruk", "mangga", "apel"].tally
# => {"apel"=>3, "mangga"=>2, "jeruk"=>1}
# Contoh: analisis log level
log_levels = [:info, :error, :info, :warn, :error, :info, :error]
log_levels.tally
# => {:info=>3, :error=>3, :warn=>1}
# Kombinasi dengan sort_by untuk menemukan yang paling sering muncul
log_levels.tally.max_by { |_, count| count }
# => [:info, 3] atau [:error, 3] — keduanya sama
# tally_by (Ruby 3.1+) — tally dengan transformasi
kata = ["Ruby", "ruby", "RUBY", "Python", "python"]
kata.tally_by(&:downcase)
# => {"ruby"=>3, "python"=>2}
chunk mengelompokkan elemen-elemen yang berurutan memiliki nilai blok yang sama — berbeda dari group_by yang mengelompokkan global.
# chunk — kelompokkan elemen berurutan yang sama
[1, 1, 2, 2, 3, 1, 1].chunk { |n| n }.map { |key, arr| [key, arr.length] }
# => [[1, 2], [2, 2], [3, 1], [1, 2]]
# chunk_while — kelompokkan selama kondisi terpenuhi
[1, 2, 4, 9, 10, 11, 12, 15, 16, 19, 20, 21].chunk_while { |i, j| j - i <= 1 }.to_a
# => [[1, 2], [4], [9, 10, 11, 12], [15, 16], [19, 20, 21]]
# slice_when — potong koleksi ketika kondisi terpenuhi (invers dari chunk_while)
[1, 2, 4, 9, 10, 11, 12, 15].slice_when { |i, j| j - i > 1 }.to_a
# => [[1, 2], [4], [9, 10, 11, 12], [15]]
Chaining dan Pipeline Data #
Salah satu kekuatan terbesar Enumerable adalah kemampuan untuk di-chain — menghubungkan beberapa method dalam satu ekspresi untuk membentuk pipeline data yang bersih.
# Skenario: dari data transaksi mentah, ambil transaksi kredit di atas 50rb,
# urutkan dari terbesar, dan ambil nama customernya
transaksi = [
{ customer: "Alice", jenis: :kredit, jumlah: 120_000 },
{ customer: "Bob", jenis: :debit, jumlah: 75_000 },
{ customer: "Charlie", jenis: :kredit, jumlah: 30_000 },
{ customer: "Diana", jenis: :kredit, jumlah: 200_000 },
{ customer: "Eve", jenis: :debit, jumlah: 90_000 },
{ customer: "Frank", jenis: :kredit, jumlah: 85_000 }
]
# ANTI-PATTERN: imperatif dengan state mutation
hasil = []
transaksi.each do |t|
if t[:jenis] == :kredit && t[:jumlah] > 50_000
hasil << t
end
end
hasil.sort_by! { |t| -t[:jumlah] }
nama = hasil.map { |t| t[:customer] }
# BENAR: pipeline deklaratif
nama = transaksi
.select { |t| t[:jenis] == :kredit && t[:jumlah] > 50_000 }
.sort_by { |t| -t[:jumlah] }
.map { |t| t[:customer] }
# => ["Diana", "Alice", "Frank"]
Chaining menciptakan pipeline di mana setiap step hanya tahu satu hal: “terima koleksi, lakukan satu transformasi, teruskan hasilnya.” Ini sejalan dengan prinsip Single Responsibility yang membuat kode mudah dipahami dan diuji.
# Pipeline yang lebih kompleks: laporan ringkasan
laporan = transaksi
.group_by { |t| t[:jenis] }
.transform_values { |list| list.sum { |t| t[:jumlah] } }
# => {:kredit=>435_000, :debit=>165_000}
# Zip — menggabungkan dua array elemen per elemen
nama = ["Alice", "Bob", "Charlie"]
skor = [85, 92, 78]
nama.zip(skor)
# => [["Alice", 85], ["Bob", 92], ["Charlie", 78]]
nama.zip(skor).map { |n, s| { nama: n, skor: s } }
# => [{:nama=>"Alice", :skor=>85}, ...]
# each_with_object sebagai alternatif reduce untuk membangun Hash
nama.each_with_object({}).with_index do |(n, hash), i|
hash[n] = skor[i]
end
# => {"Alice"=>85, "Bob"=>92, "Charlie"=>78}
flowchart LR
A["Array mentah\n[transaksi]"] --> B["select\n(filter jenis & jumlah)"]
B --> C["sort_by\n(urutkan descending)"]
C --> D["map\n(ambil nama)"]
D --> E["[Diana, Alice, Frank]"]
style A fill:#374151,color:#fff
style E fill:#374151,color:#fffLazy Enumeration #
Semua method Enumerable yang sudah dibahas bersifat eager — mereka memproses seluruh koleksi sebelum mengembalikan hasil. Ini bisa menjadi masalah ketika koleksinya sangat besar atau bahkan tak terbatas (infinite sequence).
lazy mengubah enumerator menjadi lazy enumerator — transformasi dan filter hanya dieksekusi ketika hasilnya benar-benar dibutuhkan (on-demand).
# Range tak terbatas — tidak bisa diproses dengan eager
# Ini akan hang selamanya:
# (1..Float::INFINITY).select { |n| n.odd? }.first(5)
# BENAR: gunakan lazy
(1..Float::INFINITY).lazy.select { |n| n.odd? }.first(5)
# => [1, 3, 5, 7, 9]
# Lazy pipeline — hanya memproses elemen seperlunya
(1..Float::INFINITY)
.lazy
.select { |n| n % 3 == 0 }
.map { |n| n ** 2 }
.first(5)
# => [9, 36, 81, 144, 225]
# Hanya memproses angka sampai 15 — tidak memproses jutaan angka!
# Perbandingan eager vs lazy untuk koleksi besar
require "benchmark"
data = (1..1_000_000).to_a
Benchmark.bm do |x|
# Eager: memproses semua 1 juta elemen, lalu ambil 10
x.report("eager:") do
data.select { |n| n.even? }.map { |n| n * 2 }.first(10)
end
# Lazy: hanya memproses sampai 10 elemen yang memenuhi syarat ditemukan
x.report("lazy: ") do
data.lazy.select { |n| n.even? }.map { |n| n * 2 }.first(10)
end
end
# eager: 0.087 detik
# lazy: 0.000 detik (jauh lebih cepat!)
# Contoh praktis: memproses file besar baris per baris tanpa load semua ke memori
File.foreach("log_besar.txt")
.lazy
.select { |baris| baris.include?("ERROR") }
.map { |baris| baris.strip }
.first(100)
# Hanya membaca file sampai 100 baris ERROR ditemukan
# Fibonacci tak terbatas dengan lazy
fibonacci = Enumerator.new do |y|
a, b = 0, 1
loop do
y << a
a, b = b, a + b
end
end
fibonacci.lazy.select { |n| n.even? }.first(10)
# => [0, 2, 8, 34, 144, 610, 2584, 10946, 46368, 196418]
lazymengubah return type dari Array menjadiEnumerator::Lazy. Kamu perlu memanggil.to_aatau.force(atau.first(n)) untuk mendapatkan hasilnya sebagai Array biasa. Gunakan lazy ketika:
- Koleksi sangat besar (>100rb elemen) dan kamu hanya butuh sebagian hasilnya
- Bekerja dengan infinite sequence atau stream data
- Pipeline-mu memiliki banyak step filter yang bisa memotong banyak elemen di awal
Enumerable vs Array: Kapan Pakai Apa #
Beberapa method tersedia di Array tapi tidak di Enumerable, dan sebaliknya. Memahami perbedaannya membantu kamu menulis kode yang tepat.
| Method | Enumerable | Array | Catatan |
|---|---|---|---|
map / collect | ✓ | ✓ | Identik |
select / filter | ✓ | ✓ | Identik |
find / detect | ✓ | ✓ | Identik |
sort | ✓ | ✓ | Array punya sort! (in-place) |
flatten | ✗ | ✓ | Hanya Array |
push / << | ✗ | ✓ | Hanya Array (mutation) |
unshift | ✗ | ✓ | Hanya Array (mutation) |
zip | ✓ | ✓ | Enumerable returns Array |
each_cons | ✓ | ✗ | Hanya Enumerable |
each_slice | ✓ | ✗ | Hanya Enumerable |
chunk | ✓ | ✗ | Hanya Enumerable |
tally | ✓ | ✓ | Ruby 2.7+ |
filter_map | ✓ | ✓ | Ruby 2.7+ |
lazy | ✓ | ✗ | Hanya Enumerable |
# Method yang ada di Enumerable tapi dikembalikan sebagai Enumerator
# jika dipanggil tanpa blok — berguna untuk chaining lanjutan
[1, 2, 3].each_with_object([])
# => #<Enumerator: ...> -- tanpa blok, kembalikan Enumerator
[1, 2, 3].each_with_object([]).with_index do |(n, arr), i|
arr << "#{i}:#{n}"
end
# => ["0:1", "1:2", "2:3"]
# Enum::Chain — menggabungkan beberapa enumerator (Ruby 2.6+)
kombinasi = [1, 2, 3].each + [4, 5, 6].each
kombinasi.to_a
# => [1, 2, 3, 4, 5, 6]
# Atau dengan chain method
[1, 2, 3].chain([4, 5, 6], [7, 8, 9]).to_a
# => [1, 2, 3, 4, 5, 6, 7, 8, 9]
Membuat Kelas Enumerable Sendiri #
Menambahkan Enumerable ke kelas buatanmu adalah salah satu pola paling elegan di Ruby. Kamu hanya butuh satu method — each — dan kamu mendapatkan puluhan method gratis.
class Suhu
include Comparable # bonus: bisa di-sort dan dibandingkan
include Enumerable
attr_reader :celsius
def initialize(celsius)
@celsius = celsius
end
def fahrenheit
@celsius * 9.0 / 5 + 32
end
def kelvin
@celsius + 273.15
end
def <=>(lain)
@celsius <=> lain.celsius
end
def to_s
"#{@celsius}°C"
end
end
class RangeSuhu
include Enumerable
def initialize(dari, ke, langkah = 1)
@dari = dari
@ke = ke
@langkah = langkah
end
def each
suhu = @dari
while suhu <= @ke
yield Suhu.new(suhu)
suhu += @langkah
end
end
end
# Setelah implementasi each, semua method Enumerable tersedia!
minggu_ini = RangeSuhu.new(22, 32, 2)
minggu_ini.map(&:fahrenheit)
# => [71.6, 75.2, 78.8, 82.4, 86.0, 89.6]
minggu_ini.select { |s| s.celsius > 27 }.map(&:to_s)
# => ["28°C", "30°C", "32°C"]
minggu_ini.min
# => 22°C
minggu_ini.max
# => 32°C
minggu_ini.sort.reverse.first(3).map(&:to_s)
# => ["32°C", "30°C", "28°C"]
flowchart TD
A[Kelas Custom] --> B{include Enumerable}
B --> C[Implementasi each]
C --> D[Enumerable otomatis menyediakan]
D --> E[map, flat_map]
D --> F[select, reject, filter_map]
D --> G[find, any?, all?, none?]
D --> H[sort, sort_by, min, max]
D --> I[group_by, tally, chunk]
D --> J[reduce, sum, count]
D --> K[lazy, each_cons, each_slice]
B2[include Comparable] --> L[Implementasi<br>spaceship operator <=>]
L --> M[Comparable menyediakan]
M --> N[< > <= >= between?]
M --> O[clamp]Ringkasan #
- Enumerable bekerja melalui
each— implementasikan satu method ini di kelasmu, dan kamu mendapatkan puluhan method iterasi, transformasi, pencarian, pengurutan, dan agregasi secara gratis.mapuntuk transformasi,select/rejectuntuk filter — keduanya tidak mengubah koleksi asli. Gunakan versi bang (map!,select!) jika kamu memang ingin mutasi in-place.filter_mapmenggabungkan filter dan transformasi — lebih efisien dariselect+mapdan otomatis menghapusnildari hasil.reduceadalah agregator universal — gunakan untuk kasus yang tidak punya method khusus; untuk penjumlahan, gunakansum; untuk ekstrem, gunakanmin/max.sort_bylebih disukai darisortdengan blok — lebih bersih dan lebih efisien karena menggunakan Schwartzian Transform internal.group_byuntuk pengelompokan global,chunkuntuk pengelompokan berurutan — keduanya berbeda secara fundamental:group_bymengelompokkan dari seluruh koleksi,chunkhanya mengelompokkan elemen yang bersebelahan.- Gunakan
lazyuntuk koleksi besar atau infinite sequence — eager enumeration memproses semua elemen terlebih dahulu; lazy enumeration hanya memproses seperlunya dan bisa menghemat memori serta waktu secara drastis.- Chaining adalah pola yang dianjurkan — rangkaikan
select,map,sort_by, dan method lain dalam satu pipeline deklaratif daripada menggunakan loop imperatif dengan state mutation.tallydanfilter_maptersedia sejak Ruby 2.7 — pastikan versi Ruby proyekmu mendukung sebelum menggunakannya dalam codebase yang perlu backward compatibility.