Removing functionality the Ruby way - a look into SortedSet on Ruby 3
2020-12-30Ruby 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:
begin
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."
end
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)
end
# => 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!