Set #
Array menyimpan elemen dalam urutan dan mengizinkan duplikat. Kadang kamu tidak butuh keduanya — kamu hanya butuh koleksi di mana setiap elemen dijamin unik dan kamu sering perlu mengecek apakah sebuah nilai ada di dalamnya. Di sinilah Set masuk. Set adalah struktur data yang mengimplementasikan konsep himpunan matematika: tidak ada duplikat, operasi seperti union dan intersection tersedia secara native, dan pengecekan keberadaan elemen jauh lebih cepat dibanding Array. Set tersedia sebagai bagian dari Ruby standard library — kamu perlu require "set" sebelum menggunakannya, tapi tidak perlu menginstall gem apapun. Artikel ini membahas cara kerja Set, kapan memilihnya atas Array, semua operasi himpunan yang tersedia, dan integrasi Set dengan Enumerable.
Mengapa Set, Bukan Array? #
Sebelum membahas API-nya, penting untuk memahami masalah yang dipecahkan Set — karena tanpa konteks ini, Set terasa seperti Array yang lebih terbatas.
Masalah pertama: duplikat yang tidak diinginkan. Ketika kamu membangun koleksi dari berbagai sumber, memastikan tidak ada duplikat di Array membutuhkan pengecekan manual atau memanggil .uniq setiap kali.
Masalah kedua: pengecekan keberadaan yang lambat. Array#include? harus menelusuri elemen satu per satu dari awal — O(n). Untuk array besar yang sering dicek, ini menjadi bottleneck.
require "set"
# Masalah duplikat di Array
tag_array = []
tag_array << "ruby"
tag_array << "programming"
tag_array << "ruby" # duplikat masuk begitu saja
tag_array << "ruby"
# => ["ruby", "programming", "ruby", "ruby"]
tag_array.uniq # harus memanggil uniq setiap kali
# => ["ruby", "programming"]
# Set: duplikat otomatis diabaikan
tag_set = Set.new
tag_set << "ruby"
tag_set << "programming"
tag_set << "ruby" # diabaikan secara diam-diam
tag_set << "ruby"
# => #<Set: {"ruby", "programming"}>
# Tidak perlu uniq — Set selalu dalam kondisi unik
# Pengecekan keberadaan
data_besar = (1..100_000).to_a
data_set = Set.new(data_besar)
# Array include? — O(n), menelusuri satu per satu
data_besar.include?(99_999) # lambat untuk array besar
# Set include? — O(1), lookup langsung via hash
data_set.include?(99_999) # sangat cepat
flowchart TD
A{Butuh koleksi unik?} -- Tidak --> B[Gunakan Array]
A -- Ya --> C{Sering cek include?}
C -- Tidak --> D[Array + uniq]
C -- Ya --> E{Butuh urutan?}
E -- Ya --> F[Array + uniq + sort]
E -- Tidak --> G[Gunakan Set ✓]
G --> H[include? O-1]
G --> I[Otomatis unik]
G --> J[Operasi himpunan]Secara internal, Set menggunakan Hash sebagai backing store. Setiap elemen Set menjadi key di Hash, yang memberikan lookup O(1) — sama seperti Hash. Ini berarti elemen Set harus bisa di-hash, artinya memiliki method hash dan eql? yang konsisten.
Membuat Set #
Ada beberapa cara membuat Set, masing-masing untuk konteks yang berbeda.
require "set"
# Set kosong
kosong = Set.new
# => #<Set: {}>
# Dari array
dari_array = Set.new([1, 2, 3, 2, 1])
# => #<Set: {1, 2, 3}> -- duplikat otomatis dihapus
# Dengan blok transformasi — seperti map sebelum masuk Set
dari_blok = Set.new([1, 2, 3, 4, 5]) { |n| n ** 2 }
# => #<Set: {1, 4, 9, 16, 25}>
# Shorthand dengan Kernel#Set (Ruby 2.5+)
# Sama seperti Set.new tapi lebih ringkas
huruf = Set["a", "b", "c", "b", "a"]
# => #<Set: {"a", "b", "c"}>
# Dari Range
Set.new(1..5)
# => #<Set: {1, 2, 3, 4, 5}>
# Konversi dari Array
["apel", "mangga", "apel", "jeruk"].to_set
# => #<Set: {"apel", "mangga", "jeruk"}>
# Set bersarang (Set of Sets)
Set.new([Set.new([1, 2]), Set.new([3, 4])])
# => #<Set: {#<Set: {1, 2}>, #<Set: {3, 4}>}>
Menambah dan Menghapus Elemen #
Set menyediakan interface yang familiar untuk menambah dan menghapus elemen, dengan perilaku yang konsisten — operasi penambahan tidak pernah menghasilkan duplikat.
s = Set.new
# Menambah satu elemen — << atau add
s << "ruby"
s.add("python")
s << "ruby" # tidak ada efek, "ruby" sudah ada
# => #<Set: {"ruby", "python"}>
# add? — mengembalikan nil jika elemen sudah ada (berguna untuk kondisional)
s.add?("golang") # => #<Set: {"ruby", "python", "golang"}>
s.add?("ruby") # => nil (sudah ada)
# Menambah banyak sekaligus — merge
s.merge(["rust", "kotlin", "ruby"]) # "ruby" diabaikan
# => #<Set: {"ruby", "python", "golang", "rust", "kotlin"}>
# Menghapus elemen — delete
s.delete("kotlin")
# => #<Set: {"ruby", "python", "golang", "rust"}>
# delete? — mengembalikan nil jika elemen tidak ada
s.delete?("haskell") # => nil (tidak ada)
s.delete?("rust") # => #<Set: {"ruby", "python", "golang"}>
# Menghapus dengan kondisi — delete_if
s.delete_if { |bahasa| bahasa.start_with?("p") }
# => #<Set: {"ruby", "golang"}>
# Mengosongkan Set
s.clear
# => #<Set: {}>
Operasi Himpunan #
Inilah keunggulan utama Set atas Array — operasi himpunan matematika tersedia langsung sebagai method. Kamu bisa melakukan union, intersection, difference, dan subset check tanpa loop manual.
Union — Gabungan Dua Himpunan #
Union menghasilkan Set baru berisi semua elemen dari kedua Set, tanpa duplikat.
backend = Set.new(["ruby", "python", "golang", "rust"])
frontend = Set.new(["javascript", "typescript", "ruby"])
# Union — | atau union
semua = backend | frontend
# => #<Set: {"ruby", "python", "golang", "rust", "javascript", "typescript"}>
# Equivalent methods
backend.union(frontend) # identik dengan |
# merge untuk union in-place (modifikasi Set asli)
backend_copy = backend.dup
backend_copy.merge(frontend)
# backend_copy sekarang berisi union dari keduanya
Intersection — Irisan Dua Himpunan #
Intersection menghasilkan Set berisi hanya elemen yang ada di kedua Set.
backend = Set.new(["ruby", "python", "golang", "rust"])
fullstack = Set.new(["ruby", "javascript", "python", "sql"])
# Intersection — & atau intersection
keduanya = backend & fullstack
# => #<Set: {"ruby", "python"}>
backend.intersection(fullstack) # identik dengan &
# Contoh praktis: mencari tag yang sama antara dua artikel
artikel_a = Set.new(["ruby", "programming", "web", "api"])
artikel_b = Set.new(["python", "programming", "api", "data"])
tag_bersama = artikel_a & artikel_b
# => #<Set: {"programming", "api"}>
Difference — Selisih Himpunan #
Difference menghasilkan Set berisi elemen yang ada di Set pertama tapi tidak di Set kedua.
semua_bahasa = Set.new(["ruby", "python", "golang", "rust", "javascript"])
diketahui = Set.new(["ruby", "python"])
# Difference — - atau difference
perlu_belajar = semua_bahasa - diketahui
# => #<Set: {"golang", "rust", "javascript"}>
semua_bahasa.difference(diketahui) # identik dengan -
# Symmetric difference — elemen yang ada di salah satu tapi tidak keduanya
a = Set.new([1, 2, 3, 4])
b = Set.new([3, 4, 5, 6])
# Symmetric difference: (a | b) - (a & b)
(a | b) - (a & b)
# => #<Set: {1, 2, 5, 6}>
# Atau dengan XOR (^) — Ruby 2.x keatas
a ^ b
# => #<Set: {1, 2, 5, 6}>
Subset dan Superset #
Pengecekan apakah satu himpunan adalah bagian dari himpunan lain.
ruby_fitur = Set.new(["oop", "functional", "dynamic", "scripting"])
oop_fitur = Set.new(["oop", "dynamic"])
web_fitur = Set.new(["oop", "web", "framework"])
# subset? — apakah semua elemen Set ini ada di Set lain?
oop_fitur.subset?(ruby_fitur) # => true
web_fitur.subset?(ruby_fitur) # => false ("web" tidak ada di ruby_fitur)
# superset? — apakah Set ini mengandung semua elemen Set lain?
ruby_fitur.superset?(oop_fitur) # => true
ruby_fitur.superset?(web_fitur) # => false
# proper_subset? — subset tapi tidak sama persis
oop_fitur.proper_subset?(ruby_fitur) # => true
ruby_fitur.proper_subset?(ruby_fitur) # => false (sama persis, bukan proper subset)
# Pengecekan dengan operator
oop_fitur < ruby_fitur # proper subset
oop_fitur <= ruby_fitur # subset (bisa sama)
ruby_fitur > oop_fitur # proper superset
ruby_fitur >= oop_fitur # superset (bisa sama)
flowchart LR
subgraph Union["A | B — semua elemen"]
U1["A: {1,2,3}"]
U2["B: {3,4,5}"]
U3["Result: {1,2,3,4,5}"]
U1 --> U3
U2 --> U3
end
subgraph Intersection["A & B — irisan"]
I1["A: {1,2,3}"]
I2["B: {3,4,5}"]
I3["Result: {3}"]
I1 --> I3
I2 --> I3
end
subgraph Difference["A - B — selisih"]
D1["A: {1,2,3}"]
D2["B: {3,4,5}"]
D3["Result: {1,2}"]
D1 --> D3
D2 --> D3
endIterasi dan Enumerable #
Set meng-include modul Enumerable, sehingga semua method yang sudah kamu pelajari di artikel Enumerable tersedia langsung pada Set.
bahasa = Set.new(["ruby", "python", "golang", "rust", "kotlin"])
# Semua method Enumerable tersedia
bahasa.map(&:upcase)
# => ["RUBY", "PYTHON", "GOLANG", "RUST", "KOTLIN"] -- returns Array!
bahasa.select { |b| b.length > 4 }
# => ["python", "golang", "kotlin"] -- returns Array!
bahasa.sort
# => ["golang", "kotlin", "python", "ruby", "rust"]
bahasa.min_by(&:length)
# => "ruby"
bahasa.any? { |b| b.start_with?("r") }
# => true
bahasa.count { |b| b.include?("o") }
# => 2 ("golang", "kotlin" tidak, tapi "python" dan "golang" ya...)
Perhatikan bahwa method Enumerable seperti
mapdanselectpada Set mengembalikan Array, bukan Set baru. Ini berbeda dari perilaku Array di manamapmengembalikan Array danselectmengembalikan Array. Jika kamu butuh hasil sebagai Set, gunakanto_setatau gunakan method Set-specific sepertikeep_if.bahasa = Set.new(["ruby", "python", "golang"]) # map mengembalikan Array bahasa.map(&:upcase).class # => Array # Jika butuh Set, tambahkan to_set bahasa.map(&:upcase).to_set # => #<Set: {"RUBY", "PYTHON", "GOLANG"}> # keep_if — filter in-place, mengembalikan Set bahasa.keep_if { |b| b.length > 4 } # => #<Set: {"python", "golang"}>
# each — iterasi seperti biasa
bahasa.each { |b| puts b }
# each_with_object — membangun struktur data dari Set
bahasa = Set.new(["ruby", "python", "golang"])
panjang = bahasa.each_with_object({}) do |b, hash|
hash[b] = b.length
end
# => {"ruby"=>4, "python"=>6, "golang"=>6}
# group_by pada Set
bahasa.group_by(&:length)
# => {4=>["ruby"], 6=>["python", "golang"]}
# flat_map pada Set
Set.new(["a b", "c d", "e f"]).flat_map { |s| s.split }
# => ["a", "b", "c", "d", "e", "f"]
Pengecekan dan Perbandingan #
Set menyediakan method pengecekan yang ekspresif dan intuitif.
s = Set.new([1, 2, 3, 4, 5])
# include? / member? — O(1), jauh lebih cepat dari Array
s.include?(3) # => true
s.include?(9) # => false
s.member?(3) # => true (alias dari include?)
# empty? dan size / length
s.empty? # => false
Set.new.empty? # => true
s.size # => 5
s.length # => 5 (alias)
# Perbandingan dua Set — == memeriksa kesamaan elemen, bukan urutan
Set.new([1, 2, 3]) == Set.new([3, 1, 2]) # => true
Set.new([1, 2, 3]) == Set.new([1, 2]) # => false
# disjoint? — apakah dua Set tidak punya elemen yang sama?
a = Set.new([1, 2, 3])
b = Set.new([4, 5, 6])
c = Set.new([3, 4, 5])
a.disjoint?(b) # => true (tidak ada irisan)
a.disjoint?(c) # => false (ada 3 yang sama)
# intersect? — kebalikan disjoint?
a.intersect?(b) # => false
a.intersect?(c) # => true
Pola Penggunaan Umum #
Beberapa pola nyata di mana Set secara signifikan lebih tepat dari Array.
Deduplication yang Efisien #
# ANTI-PATTERN: menggunakan Array + uniq berulang kali
def kumpulkan_permission(pengguna_list)
semua = []
pengguna_list.each do |pengguna|
semua += pengguna.permissions
end
semua.uniq # uniq di akhir, tapi array bisa sangat besar sebelumnya
end
# BENAR: gunakan Set sejak awal
def kumpulkan_permission(pengguna_list)
pengguna_list.each_with_object(Set.new) do |pengguna, set|
pengguna.permissions.each { |p| set << p }
end
end
# Atau lebih ringkas
def kumpulkan_permission(pengguna_list)
pengguna_list.flat_map(&:permissions).to_set
end
Tracking yang Sudah Diproses #
# Skenario umum: proses item, jangan proses yang sudah diproses
def proses_antrian(antrian)
sudah_diproses = Set.new
antrian.each do |item|
# include? pada Set adalah O(1) — aman untuk loop besar
next if sudah_diproses.include?(item.id)
proses(item)
sudah_diproses << item.id
end
end
# Crawling web: jangan kunjungi URL yang sama dua kali
def crawl(url_awal)
dikunjungi = Set.new
antrian = [url_awal]
until antrian.empty?
url = antrian.pop
next if dikunjungi.include?(url)
dikunjungi << url
link_baru = ambil_link(url)
antrian.concat(link_baru - dikunjungi.to_a)
end
end
Validasi Nilai yang Diizinkan #
# ANTI-PATTERN: Array untuk validasi — include? O(n)
STATUS_VALID = ["pending", "active", "suspended", "deleted"]
def valid_status?(status)
STATUS_VALID.include?(status) # menelusuri array setiap kali
end
# BENAR: Set untuk validasi — include? O(1)
STATUS_VALID = Set.new(["pending", "active", "suspended", "deleted"]).freeze
def valid_status?(status)
STATUS_VALID.include?(status) # lookup langsung
end
# Pola serupa untuk permission, role, dll.
ROLE_ADMIN = Set.new([:create, :read, :update, :delete]).freeze
ROLE_EDITOR = Set.new([:create, :read, :update]).freeze
ROLE_VIEWER = Set.new([:read]).freeze
def boleh?(role, aksi)
case role
when :admin then ROLE_ADMIN.include?(aksi)
when :editor then ROLE_EDITOR.include?(aksi)
when :viewer then ROLE_VIEWER.include?(aksi)
else false
end
end
Analisis Perbandingan Data #
# Membandingkan dua versi konfigurasi
konfigurasi_lama = Set.new(["feature_a", "feature_b", "feature_c", "feature_d"])
konfigurasi_baru = Set.new(["feature_b", "feature_c", "feature_e", "feature_f"])
ditambahkan = konfigurasi_baru - konfigurasi_lama
# => #<Set: {"feature_e", "feature_f"}>
dihapus = konfigurasi_lama - konfigurasi_baru
# => #<Set: {"feature_a", "feature_d"}>
tetap_ada = konfigurasi_lama & konfigurasi_baru
# => #<Set: {"feature_b", "feature_c"}>
puts "Ditambahkan: #{ditambahkan.to_a.join(', ')}"
puts "Dihapus: #{dihapus.to_a.join(', ')}"
puts "Tidak berubah: #{tetap_ada.to_a.join(', ')}"
Perbandingan Set vs Array #
Memahami kapan Set lebih tepat dari Array (dan sebaliknya) adalah kunci menggunakannya dengan efektif.
| Aspek | Array | Set |
|---|---|---|
| Duplikat | Diizinkan | Tidak diizinkan |
| Urutan | Dipertahankan | Tidak dijamin |
| include? complexity | O(n) | O(1) |
| Operasi himpunan | Manual / gem | Native |
| Memory | Lebih efisien untuk data kecil | Lebih besar (backing hash) |
| Indexing (arr[0]) | ✓ | ✗ |
| sort | In-place (sort!) | Mengembalikan Array |
| Terbaik untuk | Ordered data, sequential access | Unique values, membership test |
# Kapan Array lebih tepat
daftar_belanja = ["susu", "roti", "susu", "telur"]
# ✓ duplikat "susu" memang disengaja (beli 2)
# ✓ urutan mungkin penting
# ✓ perlu akses by index: daftar_belanja[0]
# Kapan Set lebih tepat
bahasa_dikuasai = Set.new(["ruby", "python", "golang"])
# ✓ tidak perlu duplikat — tidak masuk akal menguasai Ruby dua kali
# ✓ sering cek: bahasa_dikuasai.include?("rust")
# ✓ operasi: bahasa_dibutuhkan - bahasa_dikuasai (perlu belajar apa)
# Konversi bolak-balik
array = [1, 2, 3, 2, 1]
set = array.to_set # => #<Set: {1, 2, 3}>
kembali = set.to_a # => [1, 2, 3] (urutan tidak dijamin)
kembali_urut = set.sort # => [1, 2, 3] (jika perlu urutan)
Set dan Thread Safety #
Set tidak thread-safe secara default. Jika beberapa thread mengakses Set yang sama secara bersamaan, kamu bisa mendapat race condition.
require "set"
require "monitor"
# ANTI-PATTERN: Set diakses dari banyak thread tanpa proteksi
cache = Set.new
threads = 10.times.map do |i|
Thread.new { cache << i } # race condition!
end
threads.each(&:join)
# BENAR: gunakan Mutex atau Monitor untuk proteksi
cache = Set.new
mutex = Mutex.new
threads = 10.times.map do |i|
Thread.new do
mutex.synchronize { cache << i }
end
end
threads.each(&:join)
# cache sekarang aman diakses dari banyak thread
# Alternatif: gunakan SizedQueue atau struktur data thread-safe lainnya
# untuk kasus yang lebih kompleks
Jika kamu menggunakan Ruby 3.x dengan Ractor, perlu diperhatikan bahwa Set tidak bisa di-share antar Ractor secara langsung karena tidak immutable. Untuk Ractor, gunakan frozen Set atau kirimkan copy-nya.
KONSTANTA = Set.new(["a", "b", "c"]).freeze # Frozen Set aman untuk dibaca dari mana saja # tapi tidak bisa dimodifikasi KONSTANTA << "d" # => FrozenError!
Ringkasan #
- Set untuk koleksi unik dengan lookup cepat — jika kamu sering memanggil
include?pada koleksi besar, Set jauh lebih efisien dari Array karena lookup O(1) vs O(n).require "set"dulu — Set bukan built-in seperti Array dan Hash; kamu perlu require sebelum menggunakannya.- Operasi himpunan tersedia native — union (
|), intersection (&), difference (-), dan symmetric difference (^) tersedia langsung tanpa loop manual.- Pengecekan subset/superset —
subset?,superset?,proper_subset?, dan operator<,<=,>,>=untuk membandingkan relasi antar himpunan.- Enumerable tersedia — karena Set meng-include Enumerable, semua method seperti
map,select,group_by, dan lainnya bisa digunakan, tapi hasilnya Array bukan Set baru.- Gunakan
freezeuntuk konstanta — Set yang dipakai sebagai daftar validasi atau permission harus di-freeze agar tidak bisa dimodifikasi secara tidak sengaja.- Set tidak thread-safe — gunakan Mutex saat mengakses Set dari banyak thread secara bersamaan.
- Konversi mudah —
array.to_setdanset.to_auntuk konversi bolak-balik; duplikat otomatis hilang saat konversi ke Set.