Set
The Set class provides a collection of unordered values with no duplicates. It’s part of Ruby’s standard library and is useful when you need to track unique items or perform set operations.
Overview
A Set is like an Array but automatically ensures all elements are unique:
- No duplicates: Adding the same element twice has no effect
- Unordered: Elements have no specific order
- Fast lookups: Efficient membership testing
Creating a Set
require 'set'
# Create from an array
set = Set.new([1, 2, 3])
# Create empty and add items
set = Set.new
set.add(1)
set.add(2)
# Using the shortcut (Ruby 3.0+)
set = Set[1, 2, 3, 4, 5]
That first constructor block covers the most common ways to create a set from scratch. It is a good starting point because it shows the core idea before the page moves into transformations and comparison rules. Once the collection exists, the rest of the API becomes easier to follow.
Set.new parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
enum | Enumerable | nil | Initial elements |
comparator | Proc | nil | Custom comparison block |
# With a block for transformation
Set.new([1, 2, 3]) { |x| x * 2 }
# => Set{2, 4, 6}
# With custom comparison
require 'set'
Set.new([1, 2, 3], ->(a, b) { a.abs == b.abs })
This constructor section shows that a set can start from existing data or be built with custom comparison logic. That flexibility is useful when the unique-item rule needs a little help from the caller. In day-to-day code, the plain array form is the one people reach for most often because it is easy to read at a glance.
That first constructor block covers the most common ways to create a set from scratch. It is a good starting point because it shows the core idea before the page moves into transformations and comparison rules. Once the collection exists, the rest of the API becomes easier to follow.
From there, the next examples make more sense because they build on the same unique-value rule. You can see how items are added, checked, and compared without having to rethink the data structure each time.
Adding and removing elements
set = Set.new
# Add single element
set.add(1)
set.add(2)
# Add multiple elements
set.merge([3, 4, 5])
# Remove element
set.delete(2)
# Check if empty
set.empty? # => false
# Clear all elements
set.clear
The add-and-remove API is where the uniqueness rule becomes visible in practice. Adding the same value twice does not create duplicates, and that means the code can focus on the set’s contents instead of on defensive cleanup. When the collection is meant to stay unique, that behavior is exactly what you want.
Add methods
| Method | Description |
|---|---|
add(element) | Add one element |
<< | Alias for add |
add?(element) | Add only if not present (returns nil if existed) |
merge(enumerable) | Add multiple elements |
replace(enumerable) | Replace all elements |
set = Set.new([1, 2, 3])
# add? returns nil if already present
result = set.add?(2) # => nil (already there)
result = set.add?(4) # => Set{1, 2, 3, 4}
# << is an alias for add
set << 5
The table above is the part that ties the operator forms back to the named methods. It is useful when you are reading code and want to understand whether a set operation is combining, intersecting, or subtracting items. The method names can be easier to search for, while the operators can be easier to spot in a short expression.
Membership Testing
set = Set.new([1, 2, 3])
set.include?(2) # => true
set.member?(2) # => true (alias)
set.include?(99) # => false
Membership checks are one of the biggest reasons to choose Set in the first place. The code stays small, and the intent is obvious: ask whether one item is present without caring about duplicates or order. That makes the type a strong fit for tags, permissions, and other unique collections.
Set membership methods
| Method | Description |
|---|---|
include?(obj) | Check if element exists |
member?(obj) | Alias for include? |
subset?(other) | Check if all elements in another set |
superset?(other) | Check if superset of another set |
proper_subset?(other) | Check if proper subset |
proper_superset?(other) | Check if proper superset |
set1 = Set[1, 2, 3]
set2 = Set[1, 2, 3, 4]
set1.subset?(set2) # => true
set2.superset?(set1) # => true
set1.proper_subset?(set2) # => true
set1 == Set[1, 2, 3] # => true
These comparison methods make it easier to talk about relationships between unique collections. They are especially helpful when one set should fit inside another, such as a role inside a larger permission group. The method names are very direct, which makes the business rule easier to read in code reviews.
Set Operations
set1 = Set[1, 2, 3, 4]
set2 = Set[3, 4, 5, 6]
# Union (combine)
set1 | set2 # => Set{1, 2, 3, 4, 5, 6}
set1 + set2 # => Set{1, 2, 3, 4, 5, 6}
set1.merge(set2) # => Set{1, 2, 3, 4, 5, 6}
# Intersection (common)
set1 & set2 # => Set{3, 4}
set1.intersection(set2) # => Set{3, 4}
# Difference
set1 - set2 # => Set{1, 2}
set1.difference(set2) # => Set{1, 2}
# Symmetric difference
set1 ^ set2 # => Set{1, 2, 5, 6}
Set operations are what make the type feel different from an array. Union, intersection, and difference each describe a distinct mathematical relationship, so the code can match the shape of the problem instead of spelling out the whole algorithm by hand. That can make the intent much clearer.
Set operations methods
| Method | Operator | Description |
|---|---|---|
union(other) | | | Combine sets |
intersection(other) | & | Common elements |
difference(other) | - | Elements in self but not other |
^ | XOR | Elements in either but not both |
# Practical example: find unique items
authors = ["alice", "bob", "alice", "carol", "bob"]
unique_authors = Set.new(authors)
# => Set{"alice", "bob", "carol"}
This small example is a good reminder that set operations are not only for abstract math. They show up in ordinary cleanup tasks whenever duplicates need to be removed or when a collection should collapse down to unique values. The result can then feed reporting, validation, or display code.
Enumerable methods
Since Set includes Enumerable, you can use many iteration methods:
set = Set.new([1, 2, 3, 4, 5])
# Map
set.map { |x| x * 2 } # => [2, 4, 6, 8, 10]
# Select
set.select { |x| x > 2 } # => Set{3, 4, 5}
# Reject
set.reject { |x| x.even? } # => Set{1, 3, 5}
# Each
set.each { |x| puts x }
# Count
set.count # => 5
set.count { |x| x > 3 } # => 2
Because Set includes Enumerable, you can still map, select, reject, and count in the usual Ruby way. That means you do not lose the everyday convenience of collection methods just because you chose uniqueness over duplication. It is a nice balance between structure and flexibility.
Practical Examples
Removing Duplicates
# From an array with duplicates
items = [1, 2, 2, 3, 3, 3, 4]
unique = Set.new(items).to_a
# => [1, 2, 3, 4]
# Or use Array#uniq (built-in)
items.uniq
Removing duplicates is the most familiar use case for a set. It is often the quickest way to turn a noisy array into a list of unique items, especially when the array has no meaningful ordering requirement. In many scripts, this one step is enough to clean up the data.
Tracking seen items
def find_duplicates(items)
seen = Set.new
duplicates = Set.new
items.each do |item|
if seen.include?(item)
duplicates.add(item)
else
seen.add(item)
end
end
duplicates.to_a
end
find_duplicates([1, 2, 3, 2, 4, 3, 5])
# => [2, 3]
Tracking what has already appeared is another strong use for a set because the membership check is the whole point of the collection. The code reads plainly: if the item has been seen, mark it as a duplicate; otherwise, remember it for later. That pattern is common in validation and deduplication.
Tag system
class Article
attr_reader :tags
def initialize
@tags = Set.new
end
def add_tag(tag)
@tags.add(tag.downcase)
end
def has_tag?(tag)
@tags.include?(tag.downcase)
end
end
article = Article.new
article.add_tag("Ruby")
article.add_tag("rails")
article.add_tag("Ruby") # Duplicate - no effect
article.tags # => Set{"ruby", "rails"}
article.has_tag?("RUBY") # => true
Tag lists are a natural fit because tags should usually stay unique, and the set makes that rule automatic. The example also shows how a little normalization, like downcasing, can keep the collection stable even when input casing varies. That keeps the tag API easy to use from the outside.
Choosing set over array
Use Set when membership checks matter more than order. A set makes it easy to ask whether something exists without scanning through duplicates, which is handy for tags, permissions, and de-duplicated collections. If you need the original order or repeated values, an array still fits better.
Another practical benefit is that Set makes intent obvious. Readers can tell at a glance that the collection should stay unique, which can prevent accidental duplicates from sneaking into later steps of the program.
User permissions
class User
attr_reader :permissions
def initialize
@permissions = Set.new
end
def grant(permission)
@permissions.add(permission)
end
def revoke(permission)
@permissions.delete(permission)
end
def can?(permission)
@permissions.include?(permission)
end
end
user = User.new
user.grant(:read)
user.grant(:write)
user.can?(:read) # => true
user.can?(:delete) # => false
Permission checks are a clean match for Set because the allowed values are naturally unique. The code stays compact, and the set semantics make it clear that duplicates are not part of the model. That can be a nice fit for roles, feature flags, and other access-control data.