Aaron Christiansen

Removing functionality the Ruby way - a look into SortedSet on Ruby 3


Ruby 3 has just released - hooray! But naturally, some of my code hit problems with Ruby 3’s breaking changes.

Almost all of these were related to Ruby 3’s changes to how keyword arguments are handled, but I also had an intriguing issue reported on my Sord type generation project.

Ruby used to include a SortedSet class, but it’s been removed from Ruby 3 for “dependency and performance reasons”. Suppose we try to use it…

irb(main)> SortedSet
RuntimeError (The `SortedSet` class has been extracted from the `set` library.You must use the `sorted_set` gem or other alternatives.)

Woah, what’s that error message!? That’s not usually what we see when we try to use a constant which doesn’t exist. You’d expect to get something like:

NameError (uninitialized constant SortedSet)

SortedSet gives us a much more useful error message.

How does Ruby do this? Let’s take a look!

My first guess was that Ruby 3 replaced the SortedSet class with a SortedSet method which throws an exception. It’s not unusual for the Ruby Kernel to have methods which look like classes, so I thought this was a pretty good guess. But nope, there isn’t any SortedSet method:

irb(main)> method(:SortedSet)
NameError (undefined method `SortedSet' for class `#<Class:#<Object:0x000056382e0a84c8>>')

To dig a bit deeper, I took a look at the Ruby interpreter source code. The lib/set.rb file revealed the answer:

autoload :SortedSet, "#{__dir__}/set/sorted_set"

SortedSet is now defined with autoload. Referencing SortedSet will require the given file to try and resolve that constant, acting as a kind of lazy-loading within a Ruby program.

Sure enough, the useful exception we were seeing is thrown by the sorted_set file which autoload points to:

  require 'sorted_set'
rescue ::LoadError
  raise "The `SortedSet` class has been extracted from the `set` library." \
        "You must use the `sorted_set` gem or other alternatives."

This is a really clever way of providing useful error messages when functionality is broken out or removed, which I personally haven’t seen before!

There is one quirk with it though, and it’s the cause of the bug which made me look into this in the first place.

SortedSet is still a constant, so Object.constants contains it. But accessing this constant isn’t always safe if you don’t have the sorted_set gem. This means that iterating over all available constants can throw an exception!

Object.constants.each do |c|
  p Object.const_get(c)

# => KeyError
# => Thread::Queue
# => IndexError
# ...etc, until boom!
# => RuntimeError (The `SortedSet` class has been extracted from the `set` library.You must use the `sorted_set` gem or other alternatives.)

I can see this being a problem in some heavily-metaprogrammed environments, just like it was for Sord.

For what’s basically just a custom error message, I found this approach to the removal of SortedSet interesting to think about.

Is breaking a common assumption like “reading a constant which exists won’t throw an exception” worthwhile when it might help out the developer?

Or, on the other hand, is consistency key to maximise the effectiveness of Ruby’s metaprogramming? Do edge cases like this introduce complexity which makes the internals of Ruby more difficult to understand?

Personally I think this minor touch is a great design decision in Ruby 3, even if it did introduce a bug that had to be solved. If you have any other thoughts, I’d be interested to hear them!