Sets and set operations

A set is a hash of keys

A Set is a collection with no duplicates and no order. Under the hood it is a hash whose values are all true, so a lookup costs the same as a hash lookup instead of a scan.

ruby -e 'require "set"; p Set[1, 2, 2, 3].size'

3

set is part of the standard library but not loaded by default; the require at the top of the file is expected. Ruby 3.2 and newer also has Set available without a require in some contexts, but writing it explicitly costs nothing.

Union, intersection, difference

ruby -e 'require "set"; p Set[1, 2] | Set[2, 3]'

#<Set: {1, 2, 3}>

ruby -e 'require "set"; p Set[1, 2] & Set[2, 3]'

#<Set: {2}>

ruby -e 'require "set"; p Set[1, 2, 3] - Set[2]'

#<Set: {1, 3}>

ruby -e 'require "set"; p Set[1].subset?(Set[1, 2])'

true

The four operations cover the questions a program asks about groups: which tags are on both lists, which are only on one, which are on all of them. superset? and intersect? are the other two predicates, and to_a turns the result back into an array for output.

When a set beats an array scan

array.include?(value) walks the array, so a loop over one list that checks another is quadratic. Turn the list that gets checked into a set once and the loop becomes linear:

require "set" allowed = Set.new(%w[admin editor]) users = ["admin", "guest", "editor"] puts users.select { |role| allowed.include?(role) }.inspect

ruby set_demo.rb

["admin", "editor"]

With three items the array would be fine; with thirty thousand the set is the difference between a page that loads and one that times out. The code reads the same either way, which is why the change is easy to make late.

What to watch for

A set uses eql? and hash to decide identity, the same as a hash key. A class with a custom == but no hash keeps two equal objects as two members.

A set has no order, so first is a request the data cannot answer. Call to_a and sort, or keep the array when the order carries meaning.

Next: Lazy enumerators

Article author: Arthur Isaev

Banner image