Map (Hash) #
Hash adalah struktur data pasangan kunci-nilai yang menjadi pondasi hampir semua representasi data terstruktur di Ruby — konfigurasi aplikasi, parameter request HTTP, hasil query database, objek JSON yang di-parse, hingga opsi method. Di Ruby, Hash diimplementasikan sebagai hash table yang memberikan akses O(1) ke nilai berdasarkan kunci. Yang membuat Hash Ruby istimewa dibanding dictionary di bahasa lain adalah fleksibilitas kuncinya: kunci bisa berupa objek apapun — Symbol, String, Integer, bahkan objek kustom — meski Symbol adalah pilihan yang paling umum dan efisien. Artikel ini membahas semua aspek Hash dari pembuatan, akses yang aman, transformasi idiomatik, hingga pola penggunaan di codebase profesional.
Membuat Hash #
Ada beberapa cara membuat Hash, masing-masing cocok untuk situasi yang berbeda:
# Sintaks modern dengan symbol key (paling umum)
profil = { nama: "Rina", umur: 28, kota: "Bandung" }
# Sintaks hash rocket — untuk key non-symbol atau key campuran
config = { "host" => "localhost", "port" => 5432 }
campuran = { :sym => 1, "str" => 2, 42 => "angka" }
# Hash kosong
kosong = {}
kosong = Hash.new
# Hash dengan default value — nilai kembalian jika key tidak ada
counter = Hash.new(0) # default: 0
kelompok = Hash.new { |h, k| h[k] = [] } # default: array kosong baru
# Membangun Hash dari Array
kunci = [:nama, :umur, :kota]
nilai = ["Budi", 30, "Jakarta"]
dari_array = kunci.zip(nilai).to_h
puts dari_array.inspect
# => {nama: "Budi", umur: 30, kota: "Jakarta"}
# Dari array pasangan
dari_pasangan = [[:a, 1], [:b, 2], [:c, 3]].to_h
puts dari_pasangan.inspect # => {a: 1, b: 2, c: 3}
# Dengan transform — build hash dari koleksi
produk = ["laptop", "mouse", "keyboard"]
panjang_nama = produk.index_by { |p| p }.transform_values(&:length)
# => {"laptop"=>6, "mouse"=>5, "keyboard"=>8}
# index_by — Array menjadi Hash dengan key dari blok
orang = [
{ id: 1, nama: "Rina" },
{ id: 2, nama: "Budi" },
{ id: 3, nama: "Citra" }
]
by_id = orang.index_by { |p| p[:id] }
puts by_id[2].inspect # => {id: 2, nama: "Budi"}
Mengakses Nilai #
config = { host: "localhost", port: 5432, database: "app_db", ssl: false }
# Akses biasa — mengembalikan nil jika tidak ada (tidak error)
puts config[:host] # => "localhost"
puts config[:password] # => nil (diam-diam, bisa menyembunyikan bug)
# fetch — raise KeyError jika tidak ada (lebih aman untuk key wajib)
puts config.fetch(:host) # => "localhost"
puts config.fetch(:password) # => KeyError: key not found: :password
puts config.fetch(:password, "") # => "" (default jika tidak ada)
puts config.fetch(:timeout) { 30 } # => 30 (blok sebagai default dinamis)
# values_at — ambil banyak nilai sekaligus
host, port, db = config.values_at(:host, :port, :database)
puts "#{host}:#{port}/#{db}" # => localhost:5432/app_db
# slice — ambil subset Hash dengan key tertentu (Ruby 2.5+)
koneksi = config.slice(:host, :port, :database)
puts koneksi.inspect
# => {host: "localhost", port: 5432, database: "app_db"}
dig — Akses Hash Bersarang yang Aman #
data = {
pengguna: {
profil: {
nama: "Rina",
alamat: {
kota: "Bandung",
kode_pos: "40111"
}
}
}
}
# Akses bersarang biasa — crash jika ada level yang nil
puts data[:pengguna][:profil][:alamat][:kota] # => "Bandung"
puts data[:pengguna][:profil][:kontak][:telepon] # => NoMethodError!
# dig — aman, mengembalikan nil jika ada level yang nil
puts data.dig(:pengguna, :profil, :alamat, :kota) # => "Bandung"
puts data.dig(:pengguna, :profil, :kontak, :telepon) # => nil (tidak crash)
# Kombinasi dig dengan safe navigation
kota = data.dig(:pengguna, :profil, :alamat, :kota) || "Tidak diketahui"
puts kota # => "Bandung"
Default Value dan Hash.new #
Default value adalah fitur yang sangat berguna untuk menghindari pola hash[key] ||= nilai_default yang berulang:
# Default value statis
counter = Hash.new(0)
["apel", "mangga", "apel", "jeruk", "mangga", "apel"].each do |buah|
counter[buah] += 1 # tidak perlu cek apakah key sudah ada
end
puts counter.inspect
# => {"apel"=>3, "mangga"=>2, "jeruk"=>1}
# ANTI-PATTERN: tanpa default value — verbose dan rawan lupa
counter = {}
["apel", "mangga", "apel"].each do |buah|
counter[buah] ||= 0 # perlu inisialisasi manual setiap kali
counter[buah] += 1
end
# Default value dengan blok — objek baru setiap key
kelompok = Hash.new { |hash, key| hash[key] = [] }
["Rina", "Budi", "Citra", "Deni"].each_with_index do |nama, i|
kelompok[i % 2 == 0 ? :genap : :ganjil] << nama
end
puts kelompok.inspect
# => {genap: ["Rina", "Citra"], ganjil: ["Budi", "Deni"]}
# Hash bersarang otomatis dengan default blok
nested = Hash.new { |h, k| h[k] = Hash.new(0) }
nested[:jan][:pendapatan] += 1_000_000
nested[:jan][:pengeluaran] += 750_000
nested[:feb][:pendapatan] += 1_200_000
puts nested[:jan].inspect # => {pendapatan: 1000000, pengeluaran: 750000}
puts nested[:mar].inspect # => {} (dibuat otomatis, tidak crash)
Hati-hati denganHash.new(objek_mutable)— semua key yang tidak ada akan mengembalikan objek yang sama. Gunakan blokHash.new { |h, k| h[k] = [] }untuk memastikan setiap key mendapat objek baru yang terpisah.
Menambah, Mengubah, dan Menghapus #
config = { host: "localhost", port: 5432 }
# Tambah key baru
config[:database] = "app_db"
config[:ssl] = false
# Ubah nilai yang ada
config[:port] = 5433
config[:host] = "db.production.com"
# Hapus key
nilai_terhapus = config.delete(:ssl) # kembalikan nilai yang dihapus
puts nilai_terhapus # => false
puts config.inspect # ssl sudah tidak ada
# Hapus berdasarkan kondisi
config = { a: 1, b: nil, c: 3, d: nil, e: 5 }
config.delete_if { |_, v| v.nil? } # hapus semua yang nilainya nil
puts config.inspect # => {a: 1, c: 3, e: 5}
# Hapus dan kembalikan hash baru (non-destruktif)
bersih = config.reject { |_, v| v.nil? }
Merge — Menggabungkan Hash #
merge adalah salah satu operasi Hash yang paling sering digunakan, terutama untuk menggabungkan konfigurasi atau opsi:
defaults = { timeout: 30, retries: 3, ssl: false, port: 80 }
override = { timeout: 60, ssl: true }
# merge — kembalikan Hash baru, hash asli tidak berubah
hasil = defaults.merge(override)
puts hasil.inspect
# => {timeout: 60, retries: 3, ssl: true, port: 80}
puts defaults.inspect # tidak berubah
# merge! / update — modifikasi hash asli
defaults.merge!(override)
puts defaults.inspect # defaults sekarang sudah diperbarui
# merge dengan blok — kontrol konflik key
h1 = { a: 1, b: 2, c: 3 }
h2 = { b: 20, c: 30, d: 40 }
hasil = h1.merge(h2) { |key, lama, baru| lama + baru }
puts hasil.inspect
# => {a: 1, b: 22, c: 33, d: 40} (nilai konflik dijumlahkan)
# Merge banyak hash sekaligus
a = { x: 1 }
b = { y: 2 }
c = { z: 3 }
gabungan = [a, b, c].reduce(:merge)
puts gabungan.inspect # => {x: 1, y: 2, z: 3}
Transformasi Hash #
Ruby menyediakan method yang sangat ekspresif untuk mentransformasi Hash tanpa harus membangun Hash baru secara manual:
transform_keys dan transform_values #
config = { host: "localhost", port: 5432, database: "app_db" }
# Ubah semua key — misalnya dari symbol ke string
string_keys = config.transform_keys(&:to_s)
puts string_keys.inspect
# => {"host"=>"localhost", "port"=>5432, "database"=>"app_db"}
# Ubah semua key ke uppercase
upper_keys = config.transform_keys { |k| k.to_s.upcase }
puts upper_keys.inspect
# => {"HOST"=>"localhost", "PORT"=>5432, "DATABASE"=>"app_db"}
# Ubah semua nilai — misalnya konversi ke string
string_values = config.transform_values(&:to_s)
puts string_values.inspect
# => {host: "localhost", port: "5432", database: "app_db"}
# Ubah nilai dengan logika kustom
harga = { laptop: 15_000_000, mouse: 350_000, keyboard: 450_000 }
setelah_diskon = harga.transform_values { |h| (h * 0.9).round }
puts setelah_diskon.inspect
# => {laptop: 13500000, mouse: 315000, keyboard: 405000}
# Normalisasi data dari API yang mungkin berupa string atau simbol
raw = { "nama" => "Rina", "umur" => "28", "aktif" => "true" }
normalized = raw
.transform_keys(&:to_sym)
.transform_values { |v| v == "true" ? true : v == "false" ? false : v }
puts normalized.inspect
# => {nama: "Rina", umur: "28", aktif: true}
map dan filter pada Hash #
skor = { Rina: 85, Budi: 92, Citra: 78, Deni: 60, Eko: 95 }
# select — filter pasangan yang memenuhi kondisi
lulus = skor.select { |_, v| v >= 75 }
puts lulus.inspect # => {Rina: 85, Budi: 92, Citra: 78, Eko: 95}
# reject — filter pasangan yang TIDAK memenuhi kondisi
tidak_lulus = skor.reject { |_, v| v >= 75 }
puts tidak_lulus.inspect # => {Deni: 60}
# map pada Hash — mengembalikan Array of pairs, perlu .to_h
dengan_grade = skor.map do |nama, nilai|
grade = nilai >= 90 ? "A" : nilai >= 80 ? "B" : nilai >= 70 ? "C" : "D"
[nama, "#{nilai} (#{grade})"]
end.to_h
puts dengan_grade.inspect
# => {Rina: "85 (B)", Budi: "92 (A)", Citra: "78 (C)", Deni: "60 (D)", Eko: "95 (A)"}
# min_by dan max_by — temukan pasangan dengan nilai min/max
puts skor.min_by { |_, v| v }.inspect # => [:Deni, 60]
puts skor.max_by { |_, v| v }.inspect # => [:Eko, 95]
# sort_by — urutkan berdasarkan nilai
skor.sort_by { |_, v| -v }.each { |nama, v| puts "#{nama}: #{v}" }
# => Eko: 95, Budi: 92, Rina: 85, Citra: 78, Deni: 60
# any? dan all? pada Hash
puts skor.any? { |_, v| v >= 90 } # => true
puts skor.all? { |_, v| v >= 60 } # => true
puts skor.none? { |_, v| v >= 100 } # => true
Iterasi #
profil = { nama: "Budi", umur: 30, kota: "Jakarta", aktif: true }
# each — iterasi semua pasangan
profil.each { |k, v| puts "#{k}: #{v}" }
# each_key / each_value — iterasi hanya key atau value
profil.each_key { |k| print "#{k} " } # => nama umur kota aktif
profil.each_value { |v| print "#{v} " } # => Budi 30 Jakarta true
# keys dan values — sebagai Array
puts profil.keys.inspect # => [:nama, :umur, :kota, :aktif]
puts profil.values.inspect # => ["Budi", 30, "Jakarta", true]
# each_with_object — akumulasikan hasil iterasi ke objek tertentu
hasil = profil.each_with_object([]) do |(k, v), arr|
arr << "#{k}=#{v}" if v.is_a?(String)
end
puts hasil.inspect # => ["nama=Budi", "kota=Jakarta"]
Hash Bersarang #
Hash bersarang sangat umum digunakan untuk merepresentasikan data hierarkis — konfigurasi multi-level, respons API, atau dokumen JSON:
konfigurasi = {
database: {
primary: { host: "db1.example.com", port: 5432, pool: 10 },
replica: { host: "db2.example.com", port: 5432, pool: 5 }
},
cache: {
driver: :redis,
host: "cache.example.com",
port: 6379,
ttl: 3_600
},
mailer: {
smtp: { host: "smtp.example.com", port: 587, tls: true },
from: "[email protected]"
}
}
# Akses dengan dig
puts konfigurasi.dig(:database, :primary, :host) # => "db1.example.com"
puts konfigurasi.dig(:cache, :ttl) # => 3600
puts konfigurasi.dig(:database, :sharding, :host) # => nil (aman)
# Modifikasi bersarang
konfigurasi[:database][:primary][:pool] = 20
konfigurasi.dig(:cache)[:ttl] = 7_200
# Deep merge — merge yang masuk ke level bersarang
def deep_merge(hash1, hash2)
hash1.merge(hash2) do |_, val1, val2|
if val1.is_a?(Hash) && val2.is_a?(Hash)
deep_merge(val1, val2)
else
val2
end
end
end
defaults = { db: { host: "localhost", port: 5432, pool: 5 } }
overrides = { db: { host: "production.db.com", pool: 20 } }
puts deep_merge(defaults, overrides).inspect
# => {db: {host: "production.db.com", port: 5432, pool: 20}}
Method Pengecekan dan Utilitas #
h = { a: 1, b: 2, c: nil, d: false }
# Pengecekan keberadaan key
puts h.key?(:a) # => true
puts h.key?(:z) # => false
puts h.has_key?(:a) # => true (alias)
puts h.include?(:a) # => true (alias)
puts h.member?(:a) # => true (alias)
# Pengecekan nilai
puts h.value?(2) # => true
puts h.has_value?(nil) # => true
# Pengecekan ukuran
puts h.size # => 4
puts h.length # => 4 (alias)
puts h.empty? # => false
puts {}.empty? # => true
puts h.any? # => true (ada elemen truthy)
puts h.count { |_, v| v } # => 2 (a dan b — nil dan false tidak truthy)
# invert — balik key dan value
kode_negara = { ID: "Indonesia", MY: "Malaysia", SG: "Singapura" }
nama_ke_kode = kode_negara.invert
puts nama_ke_kode["Indonesia"] # => :ID
# flatten — ratakan Hash menjadi Array
puts { a: 1, b: 2 }.flatten.inspect # => [:a, 1, :b, 2]
puts { a: 1, b: 2 }.flatten(2).inspect
# to_a — konversi ke array pasangan
puts { a: 1, b: 2 }.to_a.inspect # => [[:a, 1], [:b, 2]]
# assoc — cari pasangan berdasarkan key
puts h.assoc(:b).inspect # => [:b, 2]
puts h.assoc(:z).inspect # => nil
# rassoc — cari pasangan berdasarkan value
puts kode_negara.rassoc("Malaysia").inspect # => [:MY, "Malaysia"]
Pola Penggunaan Hash di Aplikasi Nyata #
Hash sebagai Opsi Method #
# ANTI-PATTERN: banyak parameter posisional — urutan membingungkan
def buat_pengguna(nama, email, umur, aktif, role, kota)
# 6 argumen — mudah tertukar urutannya
end
buat_pengguna("Rina", "[email protected]", 28, true, :admin, "Bandung")
# BENAR: keyword argument (Hash di balik layar)
def buat_pengguna(nama:, email:, umur:, aktif: true, role: :user, kota: nil)
{ nama: nama, email: email, umur: umur, aktif: aktif, role: role, kota: kota }
end
buat_pengguna(nama: "Rina", email: "[email protected]", umur: 28, role: :admin)
Hash sebagai Registry #
# Registry pattern — daftarkan handler berdasarkan key
HANDLER = {
:json => ->(data) { JSON.parse(data) },
:csv => ->(data) { data.split("\n").map { |r| r.split(",") } },
:yaml => ->(data) { YAML.safe_load(data) }
}.freeze
def parse(data, format)
handler = HANDLER.fetch(format) do
raise ArgumentError, "Format tidak didukung: #{format}. Pilihan: #{HANDLER.keys.join(', ')}"
end
handler.call(data)
end
Hash sebagai Konfigurasi Bertingkat #
module Config
DEFAULTS = {
server: { host: "0.0.0.0", port: 3000, workers: 4 },
log: { level: :info, format: :json, output: :stdout },
cache: { enabled: true, ttl: 3_600, max_size: 100 }
}.freeze
def self.load(overrides = {})
deep_merge(DEFAULTS, overrides)
end
def self.deep_merge(base, override)
base.merge(override) do |_, v1, v2|
v1.is_a?(Hash) && v2.is_a?(Hash) ? deep_merge(v1, v2) : v2
end
end
end
config = Config.load(
server: { port: 8080, workers: 8 },
log: { level: :debug }
)
puts config.dig(:server, :port) # => 8080 (overridden)
puts config.dig(:server, :host) # => "0.0.0.0" (dari default)
puts config.dig(:log, :level) # => :debug
puts config.dig(:log, :format) # => :json (dari default)
Memoization dengan Hash #
class Fibonacci
def initialize
@memo = { 0 => 0, 1 => 1 }
end
def hitung(n)
@memo[n] ||= hitung(n - 1) + hitung(n - 2)
end
end
fib = Fibonacci.new
puts fib.hitung(50) # => 12586269025 (cepat berkat memoization)
# Memoization dengan Hash.new yang lebih elegan
fib_memo = Hash.new { |h, n| h[n] = n < 2 ? n : h[n-1] + h[n-2] }
puts fib_memo[40] # => 102334155
Memilih antara Hash dan Struct #
# Hash — untuk data dinamis atau konfigurasi
opsi = { timeout: 30, retries: 3, verbose: false }
opsi[:cache] = true # mudah tambah key baru
# Struct — untuk data terstruktur yang lebih kaku dan punya method
Titik = Struct.new(:x, :y) do
def jarak_ke_origin
Math.sqrt(x**2 + y**2)
end
end
p = Titik.new(3, 4)
puts p.jarak_ke_origin # => 5.0
puts p == Titik.new(3, 4) # => true (perbandingan by value)
Kapan gunakan Hash vs Struct vs Kelas:
Hash:
✓ Data konfigurasi dan opsi yang mungkin berubah
✓ Representasi JSON atau data eksternal
✓ Key dinamis yang tidak diketahui sebelumnya
✓ Akumulasi data dalam iterasi
Struct:
✓ Data container sederhana dengan field yang tetap
✓ Butuh perbandingan == by value secara otomatis
✓ Ingin method helper tanpa boilerplate penuh
Kelas biasa:
✓ Ada validasi, logika bisnis, atau inheritance
✓ Butuh enkapsulasi dan visibility control
✓ Objek domain dengan identitas (bukan hanya data)
Ringkasan #
- Symbol key lebih efisien dari String key —
:namaadalah singleton di memori,"nama"membuat objek baru setiap kali ditulis.fetchlebih aman dari[]—fetchmelemparKeyErrorjika key tidak ada, sedangkan[]diam-diam mengembalikannilyang bisa menyembunyikan bug.diguntuk hash bersarang — lebih aman dari chaining[][]karena mengembalikannilalih-alih crash ketika ada level yangnil.slice(Ruby 2.5+) untuk mengambil subset Hash dengan key tertentu — lebih ringkas dariselect { |k, _| [:a, :b].include?(k) }.Hash.new { |h, k| h[k] = [] }bukanHash.new([])— blok memastikan setiap key mendapat objek baru, bukan shared reference.transform_keysdantransform_valuesuntuk mentransformasi key atau value tanpa loop manual — idiomatik dan ekspresif.mergedengan blok untuk menangani konflik key secara kustom —h1.merge(h2) { |k, v1, v2| v1 + v2 }.index_by(dari Enumerable) untuk mengonversi Array of objects menjadi Hash dengan key yang ditentukan blok.- Deep merge perlu implementasi manual —
mergebawaan hanya satu level, tapi pola rekursif sederhana untuk menangani Hash bersarang.- Hash adalah pilihan tepat untuk opsi method — gunakan keyword argument (yang secara teknis adalah Hash) untuk method dengan banyak parameter opsional.