2

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>
Yogh
  • 591
  • 6
  • 17

1 Answers1

3

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.

Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
  • The reason for declaring it on the singleton class is to make it not accessible. `TEST` is a hidden container for the data that's accessed through `[]`. It's constant as a locally scoped variable that doesn't change rather than constant like a class or module name. – Yogh May 19 '17 at 04:37
  • “It's constant as a locally scoped variable”—I believe you misunderstand ruby idiomatic concepts. Try `class Dummy2 < Dummy; end; Dummy2.singleton_class::TEST`. There is literally _nothing_ inaccessible in ruby. Ruby drives on contracts. Need such a contract?—Issue `private_constant :Test` near `private :new`. The latter still _does not make it inaccessible_ though. As well as `private :new`: try `Dummy.send :new`. – Aleksei Matiushkin May 19 '17 at 04:53
  • Yes, I know you _can_ still get to it, but by putting it where I did I intended it to show that contract that it's not _supposed_ to be accessed. I didn't know about `private_constant` so maybe that's a better way. – Yogh May 22 '17 at 04:13