rubysequelruby-2.4

Why are singleton_class objects frozen in Ruby 2.4 but not in 2.3?


We have an app that uses the Sequel gem to connect to a data source, perform some work, and then return a result which has a number of convenience methods attached to the singleton_class of that object. In ruby 2.3, this code is working as expected:

result = EpulseDB::Employee.where(normalized_args)
result.singleton_class.include(EpulseNormalization)

And we can see using ruby 2.3.4 the singleton_class is not frozen:

[1] pry(main)> result = EpulseDB::Employee.where(employee_id: 2)
=> #<Sequel::Postgres::Dataset: "SELECT * FROM \"employee\" WHERE (\"employee_id\" = 2)">
[2] pry(main)> result.frozen?
=> true
[3] pry(main)> result.singleton_class.frozen?
=> false
[4] pry(main)> result.singleton_class.include(EpulseNormalization)
=> #<Class:#<Sequel::Postgres::Dataset:0x007feff0903660>>

But in Ruby 2.4.2 it appears the singleton_class is being returned as frozen and we can no longer extend it. Is there a new way of extending the singleton that I should be using??

[1] pry(main)> result = EpulseDB::Employee.where(employee_id: 2)
=> #<Sequel::Postgres::Dataset: "SELECT * FROM \"employee\" WHERE (\"employee_id\" = 2)">
[2] pry(main)> result.frozen?
=> true
[3] pry(main)> result.singleton_class.frozen?
=> true
[4] pry(main)> result.singleton_class.include(EpulseNormalization)
RuntimeError: can't modify frozen object
from (pry):4:in `append_features'

Solution

  • Use Dataset#with_extend to return a modified copy of the dataset extended with a module, instead of calling Dataset#extend to modify the dataset itself. This works on all versions of ruby that Sequel supports.

    Backstory: This is not related to Ruby itself, it's due to a workaround in Sequel for the lack of a feature in Ruby <2.4.

    In Ruby <2.4, Object#freeze can't handle cases where Object#clone is used to create modified copies of frozen objects (including copies of the object's singleton class). Ruby 2.4 added the freeze: false option to Object#clone to allow creating modified copies of frozen objects including their singleton class (see https://bugs.ruby-lang.org/issues/12300).

    Sequel::Dataset uses #clone internally to return modified datasets, and it's required the datasets include copies of any singleton classes used for proper functioning. Since I wanted Sequel::Dataset to be frozen, but still work on ruby < 2.4, it basically fakes being frozen on ruby <2.4. It's only truly frozen in ruby 2.4. See: