rubyguides

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

ParameterTypeDefaultDescription
enumEnumerablenilInitial elements
comparatorProcnilCustom 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

MethodDescription
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

MethodDescription
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

MethodOperatorDescription
union(other)&#124;Combine sets
intersection(other)&Common elements
difference(other)-Elements in self but not other
^XORElements 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.

See Also

  • Array — ordered collections with duplicates allowed
  • Hash — key-value pairs
  • YAML — serialization format