Perulangan #
Perulangan adalah salah satu konstruk paling fundamental dalam pemrograman — tapi cara Ruby mengekspresikannya sangat berbeda dari bahasa seperti C, Java, atau JavaScript. Di Ruby, loop berbasis kondisi (while, until) ada, tapi bukan yang paling umum digunakan. Yang lebih dominan adalah iterator berbasis blok dari modul Enumerable — each, map, select, reduce, dan puluhan method lainnya yang sudah terpasang pada Array, Hash, Range, dan semua koleksi. Memahami perbedaan antara “loop” dan “iterator” di Ruby, serta kapan menggunakan masing-masing, adalah kunci untuk menulis kode yang benar-benar idiomatik.
Loop Berbasis Kondisi #
Loop berbasis kondisi menjalankan blok kode selama kondisi tertentu terpenuhi. Ruby menyediakan while, until, dan loop untuk keperluan ini.
while #
while menjalankan blok kode selama kondisinya truthy:
hitung = 1
while hitung <= 5
puts "Iterasi ke-#{hitung}"
hitung += 1
end
# => Iterasi ke-1, ke-2, ... ke-5
while paling tepat digunakan ketika jumlah iterasi tidak diketahui di awal dan bergantung pada kondisi eksternal yang berubah:
# Contoh nyata: polling hingga kondisi terpenuhi
def tunggu_hingga_selesai(task, maks_detik: 30)
elapsed = 0
while !task.selesai? && elapsed < maks_detik
sleep 1
elapsed += 1
end
task.selesai? ? "Selesai dalam #{elapsed} detik" : "Timeout setelah #{maks_detik} detik"
end
until #
until adalah kebalikan while — menjalankan blok kode selama kondisinya falsy. Ia membaca lebih natural untuk kondisi yang dirumuskan secara negatif:
antrian = ["tugas A", "tugas B", "tugas C"]
until antrian.empty?
tugas = antrian.shift
puts "Memproses: #{tugas}"
end
# => Memproses: tugas A, tugas B, tugas C
while dan until juga mendukung bentuk modifier postfix — berguna untuk kondisi sederhana satu baris:
# Modifier postfix while
sleep 0.5 while server_belum_siap?
# Modifier postfix until
kirim_ulang until berhasil? || percobaan >= 3
loop — Perulangan Tak Terbatas #
loop menjalankan blok kode selamanya sampai dihentikan secara eksplisit dengan break. Ini adalah cara idiomatik Ruby untuk menulis infinite loop:
# ANTI-PATTERN: while true — gaya C, bukan Ruby
while true
input = gets.chomp
break if input == "keluar"
puts "Kamu ketik: #{input}"
end
# BENAR: gunakan loop
loop do
input = gets.chomp
break if input == "keluar"
puts "Kamu ketik: #{input}"
end
loop sangat berguna untuk implementasi REPL (Read-Eval-Print Loop) atau event loop sederhana:
def jalankan_repl
loop do
print "ruby> "
input = gets&.chomp
break if input.nil? || input == "exit"
begin
hasil = eval(input)
puts "=> #{hasil.inspect}"
rescue => e
puts "Error: #{e.message}"
end
end
end
for — Loop yang Jarang Digunakan #
for ada di Ruby, tapi hampir tidak pernah digunakan oleh developer Ruby berpengalaman. Ada alasan teknis di baliknya:
# for loop — valid tapi tidak idiomatik
for n in 1..5
puts n
end
# MASALAH: variabel yang didefinisikan di dalam for loop
# bocor ke scope luar!
for item in ["a", "b", "c"]
hasil = item.upcase
end
puts hasil # => "C" (masih bisa diakses di luar loop!)
# each tidak punya masalah ini
["a", "b", "c"].each do |item|
hasil_dalam = item.upcase
end
puts hasil_dalam # => NameError: variabel tidak bocor keluar blok
Kenapa for jarang digunakan di Ruby:
✗ Variabel lokal di dalam for bocor ke scope luar
✗ Tidak mendukung method chaining
✗ Tidak bisa dipakai dengan Enumerable lain (Hash, Range dengan step, dll)
✓ each mengatasi semua masalah ini dan lebih ekspresif
Iterator Enumerable — Cara Idiomatik Ruby #
Enumerable adalah modul yang menyediakan puluhan method iterasi untuk semua koleksi di Ruby — Array, Hash, Range, dan kelas apapun yang mengimplementasikan each. Inilah cara perulangan yang paling idiomatik di Ruby.
flowchart TD
A[Enumerable] --> B[Iterasi Dasar\neach, each_with_index\neach_with_object]
A --> C[Transformasi\nmap/collect\nflat_map\nzip]
A --> D[Filter\nselect/filter\nreject\nfind/detect]
A --> E[Akumulasi\nreduce/inject\nsum, count\nmin, max, minmax]
A --> F[Pengelompokan\ngroup_by\nchunk, tally\npartition]
A --> G[Pengecekan\nany?, all?\nnone?, one?]each — Iterasi Dasar #
each adalah iterator paling fundamental — ia menjalankan blok untuk setiap elemen tanpa mengubah atau mengumpulkan hasilnya:
buah = ["apel", "mangga", "jeruk", "nanas"]
# Sintaks blok do...end — untuk multi-baris
buah.each do |b|
puts b.upcase
end
# Sintaks { } — untuk satu baris
buah.each { |b| puts b.upcase }
# Hash — blok menerima kunci dan nilai
profil = { nama: "Rina", umur: 28, kota: "Bandung" }
profil.each do |kunci, nilai|
puts "#{kunci}: #{nilai}"
end
# Range
(1..5).each { |n| print "#{n} " } # => 1 2 3 4 5
each_with_index dan each_with_object #
Ketika kamu butuh indeks selama iterasi, gunakan each_with_index. Ketika butuh mengakumulasikan hasil ke satu objek, gunakan each_with_object:
daftar = ["laptop", "mouse", "keyboard", "monitor"]
# each_with_index — indeks mulai dari 0
daftar.each_with_index do |item, i|
puts "#{i + 1}. #{item}"
end
# => 1. laptop, 2. mouse, 3. keyboard, 4. monitor
# ANTI-PATTERN: membuat Hash secara manual dengan each
hasil = {}
daftar.each_with_index { |item, i| hasil[item] = i }
# BENAR: each_with_object lebih ringkas
hasil = daftar.each_with_object({}) do |item, hash|
hash[item] = item.length
end
puts hasil.inspect
# => {"laptop"=>6, "mouse"=>5, "keyboard"=>8, "monitor"=>7}
times, upto, downto, step #
Untuk perulangan berbasis angka sederhana, Ruby menyediakan method langsung pada Integer:
# times — ulangi N kali, indeks mulai 0
5.times { |i| print "#{i} " } # => 0 1 2 3 4
# upto — naik dari a ke b
1.upto(5) { |n| print "#{n} " } # => 1 2 3 4 5
# downto — turun dari a ke b
5.downto(1) { |n| print "#{n} " } # => 5 4 3 2 1
# step — dengan langkah tertentu
1.step(10, 2) { |n| print "#{n} " } # => 1 3 5 7 9
10.step(1, -3) { |n| print "#{n} " } # => 10 7 4 1
# Range juga punya step
(0.0..1.0).step(0.25) { |n| print "#{n} " }
# => 0.0 0.25 0.5 0.75 1.0
Transformasi Koleksi #
map / collect — Ubah Setiap Elemen #
map mengembalikan Array baru dengan hasil blok yang diterapkan pada setiap elemen. Ini adalah operator transformasi — tidak mengubah array asli:
angka = [1, 2, 3, 4, 5]
# Kuadratkan setiap elemen
kuadrat = angka.map { |n| n ** 2 }
puts kuadrat.inspect # => [1, 4, 9, 16, 25]
# Transformasi String
nama = ["rina", "budi", "citra"]
puts nama.map(&:capitalize).inspect # => ["Rina", "Budi", "Citra"]
puts nama.map(&:upcase).inspect # => ["RINA", "BUDI", "CITRA"]
# Transformasi objek kompleks
produk = [
{ nama: "Laptop", harga: 15_000_000 },
{ nama: "Mouse", harga: 350_000 },
{ nama: "Monitor", harga: 3_500_000 }
]
# Ambil hanya nama saja
produk.map { |p| p[:nama] }
# => ["Laptop", "Mouse", "Monitor"]
# Terapkan diskon 10%
produk.map { |p| p.merge(harga: (p[:harga] * 0.9).round) }
map! adalah versi destruktif yang memodifikasi array asli. Gunakan dengan hati-hati:
# ANTI-PATTERN: map! mengubah array asli — berbahaya jika tidak disengaja
data = [1, 2, 3]
data.map! { |n| n * 2 }
puts data.inspect # => [2, 4, 6] (array asli berubah!)
# BENAR: gunakan map (non-destructive) dan assign hasilnya
data = [1, 2, 3]
hasil = data.map { |n| n * 2 }
puts data.inspect # => [1, 2, 3] (tidak berubah)
puts hasil.inspect # => [2, 4, 6]
flat_map — Map dan Flatten Sekaligus #
flat_map menerapkan blok pada setiap elemen, lalu meratakan hasilnya satu tingkat:
kalimat = ["Ruby itu menyenangkan", "Python juga bagus"]
# map biasa menghasilkan array of arrays
kalimat.map { |k| k.split(" ") }
# => [["Ruby", "itu", "menyenangkan"], ["Python", "juga", "bagus"]]
# flat_map langsung meratakan
kalimat.flat_map { |k| k.split(" ") }
# => ["Ruby", "itu", "menyenangkan", "Python", "juga", "bagus"]
# Contoh lain: ambil semua tag dari banyak artikel
artikel = [
{ judul: "Ruby Dasar", tag: [:ruby, :pemula] },
{ judul: "Rails Guide", tag: [:ruby, :rails, :web] },
{ judul: "Python Tips", tag: [:python, :tips] }
]
semua_tag = artikel.flat_map { |a| a[:tag] }.uniq
puts semua_tag.inspect
# => [:ruby, :pemula, :rails, :web, :python, :tips]
Filter Koleksi #
select / filter — Pilih yang Memenuhi Kondisi #
select mengembalikan Array baru berisi elemen yang membuat bloknya bernilai true:
angka = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
genap = angka.select { |n| n.even? }
puts genap.inspect # => [2, 4, 6, 8, 10]
besar = angka.select { |n| n > 5 }
puts besar.inspect # => [6, 7, 8, 9, 10]
# Pada Hash — mengembalikan Hash baru
config = { host: "localhost", port: 5432, debug: false, verbose: true }
aktif = config.select { |_, v| v }
puts aktif.inspect # => {host: "localhost", port: 5432, verbose: true}
reject — Kebalikan select #
reject mengembalikan elemen yang membuat bloknya bernilai false:
angka = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# ANTI-PATTERN: select dengan negasi — menyulitkan pembacaan
ganjil = angka.select { |n| !n.even? }
# BENAR: reject lebih ekspresif
ganjil = angka.reject { |n| n.even? }
puts ganjil.inspect # => [1, 3, 5, 7, 9]
# Sangat berguna untuk filter nil
data = [1, nil, 2, nil, 3, nil]
bersih = data.reject(&:nil?) # sama dengan data.compact
puts bersih.inspect # => [1, 2, 3]
find / detect — Cari Satu Elemen #
find mengembalikan elemen pertama yang memenuhi kondisi, atau nil jika tidak ada:
angka = [3, 7, 12, 5, 18, 2]
pertama_genap = angka.find { |n| n.even? }
puts pertama_genap # => 12
# Dengan nilai default jika tidak ditemukan
tidak_ada = angka.find { |n| n > 100 }
puts tidak_ada.inspect # => nil
# find_index — kembalikan indeks, bukan nilai
puts angka.find_index { |n| n.even? } # => 2
Akumulasi dan Reduksi #
reduce / inject — Akumulasi ke Satu Nilai #
reduce (alias inject) menggabungkan semua elemen koleksi menjadi satu nilai melalui operasi yang berulang:
angka = [1, 2, 3, 4, 5]
# Sintaks dengan blok
jumlah = angka.reduce(0) { |akum, n| akum + n }
puts jumlah # => 15
# Sintaks dengan symbol — lebih ringkas
jumlah = angka.reduce(:+) # => 15
produk = angka.reduce(:*) # => 120
maks = angka.reduce { |a, b| a > b ? a : b } # => 5
# Contoh kompleks: membangun Hash dari Array
pasangan = [[:nama, "Budi"], [:umur, 30], [:kota, "Jakarta"]]
hash = pasangan.reduce({}) do |akum, (kunci, nilai)|
akum.merge(kunci => nilai)
end
puts hash.inspect # => {nama: "Budi", umur: 30, kota: "Jakarta"}
sum, count, min, max, minmax #
Untuk operasi agregasi umum, Enumerable menyediakan shortcut yang lebih ekspresif dari reduce:
angka = [5, 3, 8, 1, 9, 2, 7]
puts angka.sum # => 35
puts angka.count # => 7
puts angka.count { |n| n > 5 } # => 3 (berapa yang > 5)
puts angka.min # => 1
puts angka.max # => 9
puts angka.minmax.inspect # => [1, 9]
puts angka.sum { |n| n * 2 } # => 70 (sum dengan transformasi)
# min_by / max_by — untuk objek kompleks
produk = [
{ nama: "Laptop", harga: 15_000_000 },
{ nama: "Mouse", harga: 350_000 },
{ nama: "Monitor", harga: 3_500_000 }
]
termurah = produk.min_by { |p| p[:harga] }
termahal = produk.max_by { |p| p[:harga] }
puts termurah[:nama] # => "Mouse"
puts termahal[:nama] # => "Laptop"
Pengelompokan #
group_by — Kelompokkan Berdasarkan Kriteria #
group_by mengelompokkan elemen-elemen menjadi Hash di mana kuncinya adalah hasil blok:
angka = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
kelompok = angka.group_by { |n| n.even? ? :genap : :ganjil }
puts kelompok[:genap].inspect # => [2, 4, 6, 8, 10]
puts kelompok[:ganjil].inspect # => [1, 3, 5, 7, 9]
# Kelompokkan transaksi berdasarkan bulan
transaksi = [
{ tanggal: "2024-01-15", jumlah: 100_000 },
{ tanggal: "2024-01-28", jumlah: 50_000 },
{ tanggal: "2024-02-05", jumlah: 200_000 },
{ tanggal: "2024-02-20", jumlah: 75_000 },
]
per_bulan = transaksi.group_by { |t| t[:tanggal][0..6] }
per_bulan.each do |bulan, data|
total = data.sum { |t| t[:jumlah] }
puts "#{bulan}: Rp #{total}"
end
# => 2024-01: Rp 150000
# => 2024-02: Rp 275000
tally — Hitung Frekuensi #
tally menghitung berapa kali setiap nilai muncul dalam koleksi:
hasil_survey = [:puas, :tidak_puas, :puas, :netral, :puas, :tidak_puas, :puas]
frekuensi = hasil_survey.tally
puts frekuensi.inspect
# => {:puas=>4, :tidak_puas=>2, :netral=>1}
# Kata yang paling sering muncul
teks = "ruby adalah bahasa yang menyenangkan dan ruby sangat ekspresif"
kata = teks.split.tally.sort_by { |_, v| -v }
kata.first(3).each { |w, n| puts "#{w}: #{n}x" }
# => ruby: 2x, adalah: 1x, bahasa: 1x
partition — Pisah Menjadi Dua Grup #
partition mengembalikan dua array: elemen yang memenuhi kondisi dan yang tidak:
angka = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
genap, ganjil = angka.partition { |n| n.even? }
puts genap.inspect # => [2, 4, 6, 8, 10]
puts ganjil.inspect # => [1, 3, 5, 7, 9]
# Pisahkan user aktif dan nonaktif
aktif, nonaktif = users.partition { |u| u.aktif? }
Pengecekan Koleksi #
angka = [2, 4, 6, 8, 10]
puts angka.any? { |n| n > 8 } # => true (ada yang > 8)
puts angka.all? { |n| n.even? } # => true (semua genap)
puts angka.none? { |n| n > 20 } # => true (tidak ada yang > 20)
puts angka.one? { |n| n > 8 } # => true (tepat satu yang > 8)
# Tanpa blok — cek apakah ada elemen truthy
[nil, false].any? # => false
[nil, false, 1].any? # => true
[1, 2, 3].all? # => true
[1, nil, 3].all? # => false
Kontrol Alur dalam Perulangan #
break — Hentikan Loop #
break menghentikan loop sepenuhnya. Di iterator Enumerable, ia juga bisa mengembalikan nilai:
# break dari while
i = 0
while i < 10
break if i == 5
puts i
i += 1
end
# => 0 1 2 3 4
# break dengan nilai kembalian dari iterator
hasil = [1, 2, 3, 4, 5].each do |n|
break "ditemukan: #{n}" if n == 3
end
puts hasil # => "ditemukan: 3"
# Praktis untuk mencari dengan nilai balik
nomor = (1..Float::INFINITY).lazy.each do |n|
break n if n % 17 == 0 && n % 13 == 0
end
puts nomor # => 221 (bilangan pertama yang habis dibagi 17 dan 13)
next — Lewati Iterasi Ini #
next melewati sisa blok untuk iterasi saat ini dan melanjutkan ke elemen berikutnya:
(1..10).each do |n|
next if n.even? # lewati angka genap
puts n
end
# => 1 3 5 7 9
# Praktis untuk skip kondisi tidak valid tanpa nesting
data = [nil, 1, nil, 2, 3, nil, 4]
data.each do |item|
next if item.nil? # skip nil — hindari nesting
next if item < 2 # skip yang < 2
puts item * 10
end
# => 20 30 40
redo — Ulang Iterasi Saat Ini #
redo mengulang iterasi yang sedang berjalan dari awal tanpa berpindah ke elemen berikutnya. Jarang digunakan, tapi berguna untuk kasus retry dalam iterasi:
percobaan_per_item = Hash.new(0)
["url1", "url2", "url3"].each do |url|
percobaan_per_item[url] += 1
begin
# simulasi proses yang bisa gagal
raise "Koneksi gagal" if percobaan_per_item[url] < 3
puts "Berhasil memproses #{url} pada percobaan ke-#{percobaan_per_item[url]}"
rescue
redo if percobaan_per_item[url] < 3 # coba ulang max 3 kali
puts "Gagal memproses #{url} setelah 3 percobaan"
end
end
Lazy Enumerator — Efisiensi untuk Koleksi Besar #
Secara default, semua iterator Enumerable mengevaluasi seluruh koleksi sebelum mengembalikan hasil. Untuk koleksi yang sangat besar atau tak terbatas, ini tidak efisien. lazy menunda evaluasi hingga benar-benar dibutuhkan:
# ANTI-PATTERN: eager evaluation pada range besar
(1..Float::INFINITY).select { |n| n.odd? }.first(5)
# ← ini tidak akan selesai karena mencoba select semua bilangan tak terbatas!
# BENAR: lazy evaluation
(1..Float::INFINITY).lazy.select { |n| n.odd? }.first(5)
# => [1, 3, 5, 7, 9] ← hanya evaluasi sampai 5 elemen ditemukan
# Lazy chain
hasil = (1..Float::INFINITY)
.lazy
.select { |n| n % 3 == 0 } # habis dibagi 3
.map { |n| n ** 2 } # kuadratkan
.reject { |n| n.to_s.include?("9") } # tidak mengandung angka 9
.first(5)
puts hasil.inspect # => [36, 144, 225, 576, 1296]
# Lazy berguna juga untuk file besar — baca baris per baris tanpa load semua
File.foreach("data_besar.csv")
.lazy
.select { |baris| baris.include?("Jakarta") }
.map { |baris| baris.split(",").first }
.first(10)
Memilih Metode Perulangan yang Tepat #
flowchart TD
A[Perlu perulangan] --> B{Jenis operasi?}
B --> C["Jalankan efek samping\ntanpa hasil koleksi"]
B --> D["Ubah setiap elemen\nmenjadi nilai baru"]
B --> E["Pilih subset elemen"]
B --> F["Gabungkan ke satu nilai"]
B --> G["Kelompokkan elemen"]
B --> H["Kondisi berulang\nbukan koleksi"]
C --> C1["each\neach_with_index"]
D --> D1["map / flat_map"]
E --> E1["select / reject\nfind / find_index"]
F --> F1["reduce / inject\nsum, min, max, count"]
G --> G1["group_by / tally\npartition"]
H --> H1["while / until / loop"]Panduan cepat memilih metode iterasi:
each → iterasi, efek samping, tidak butuh hasil koleksi
map → ubah setiap elemen, kembalikan koleksi baru
flat_map → map + flatten satu tingkat
select / filter → filter elemen yang memenuhi kondisi
reject → filter elemen yang TIDAK memenuhi kondisi
find → cari satu elemen pertama yang memenuhi kondisi
reduce / inject → akumulasi ke satu nilai
sum → shortcut reduce(:+) dengan transformasi opsional
group_by → kelompokkan ke Hash berdasarkan kriteria
tally → hitung frekuensi kemunculan elemen
partition → pisah menjadi dua grup (memenuhi / tidak)
any? all? none? → pengecekan kondisi pada koleksi
each_with_index → each tapi butuh indeks
each_with_object → each tapi akumulasikan ke objek tertentu
times / upto → perulangan berbasis angka sederhana
while / until → perulangan berbasis kondisi (bukan koleksi)
loop → infinite loop dengan break eksplisit
.lazy → evaluasi malas untuk koleksi besar atau tak terbatas
Ringkasan #
forloop hampir tidak pernah digunakan — variabel lokal di dalamnya bocor ke scope luar, tidak ada keunggulan dibandingeach.eachuntuk efek samping,mapuntuk transformasi — jangan gunakaneachuntuk membangun koleksi baru dan jangan gunakanmapjika kamu membuang hasilnya.map!mengubah array asli — lebih aman menggunakanmap(non-destruktif) dan menyimpan hasilnya ke variabel baru.flat_map=map+flatten(1)— gunakan ketika blok mengembalikan Array dan kamu ingin hasilnya diratakan.reducepaling fleksibel — tapi untuk kasus umum (sum, min, max, count) gunakan shortcut yang lebih ekspresif.group_bydantallyuntuk analisis data — lebih ringkas dari membangun Hash secara manual denganeach.breakbisa mengembalikan nilai dari iterator — berguna untuk pencarian dengan early exit yang mengembalikan hasil temuan.nextlebih bersih dari nesting — gunakannext if kondisidi awal blok untuk skip elemen tidak valid daripada membungkus logika dalamif..lazywajib untuk koleksi tak terbatas — tanpalazy,selectataumappada(1..Float::INFINITY)tidak akan pernah selesai.whiledanloopuntuk kondisi non-koleksi — polling, retry, event loop, dan kondisi yang bergantung pada state eksternal yang berubah.