rubyruby-2.3

Class level constant broken in Ruby 2.3.4


I have some code that can be simplified to the following. It works on Ruby 2.3.3 and breaks on 2.3.4. It's slightly odd and I'd welcome suggestions on how to rewrite it as well as explanations as to why it breaks.

require 'forwardable'

class Dummy
  class << self
    TEST = {
      a: Dummy.new,
      b: Dummy.new
    }

    extend Forwardable

    def_delegators :TEST, :[]

    private :new
  end
end

puts Dummy[:a]

Ruby 2.3.3

#<Dummy:0x007fbd6d162380>

Ruby 2.3.4

NameError: uninitialized constant TEST

The goal was to only initialize TEST once and have .new be private. Memoizing a @test variable inside a [] method doesn't work because new is private by the point the hash is created.

EDIT

Removing Forwardable from the equation fixes things, but I'm still curious as to why and thoughts on how to improve it.

class Dummy
  class << self
    TEST = {
      a: Dummy.new,
      b: Dummy.new
    }

    def [](key)
      TEST[key]
    end

    private :new
  end
end

puts Dummy[:a]

Ruby 2.3.3 and 2.3.4

#<Dummy:0x007fbd6d162380>

Solution

  • How to fix

    require 'forwardable'
    
    class Dummy
      Test = {
        a: Dummy.new,
        b: Dummy.new
      }
    
      class << self
        extend Forwardable
    
        def_delegators :"::Dummy::Test", :[]
    
        private :new
      end
    end
    
    puts Dummy[:a]
    

    Why

    Ruby is open source. There was a bug #12478, fixed in that commit. The commit’s message explicitly states that it changes the behaviour while dealing with non-module objects.

    Symbols are not converted to Strings anymore and dealed separately, :TEST is not expanded on Dummy level and the constant could not be resolved in different context.

    Why it’s not a bug

    It does not make any sense to declare the constants on singleton classes (check with your old code):

    Dummy.constants
    #⇒ []
    Dummy.singleton_class.constants
    #⇒ [:TEST]
    

    The constant was successfully resolved in the legacy implementation exactly the same way as multiplying two negatives gives a positive result: the errors negated. The code was not working properly, it occasionally failed twice in unexpected, but appreciated way, producing the correct result.