Greg
Greg

Reputation: 6628

How different ways to define a class influence the way include works?

I have a simple module that defines a constant and makes it private:

module Foo
  Bar = "Bar"
  private_constant :Bar
end

I can include it in a class like this, and it works as expected:

class User
  include Foo

  def self.test
    Bar
  end
end

puts User.test
# => Bar

begin
  User::Bar
rescue => exception
  puts "#{exception} as expected"
  # => private constant Foo::Bar referenced as expected
end

(let's call it "typical class" definition)

Then I tried the Class.new approach, but this failed miserably:

X = Class.new do
  include Foo

  def self.test
    Bar # Line 28 pointed in the stack trace
  end
end

begin
  X::Bar
rescue => exception
  puts "#{exception}"
  # => private constant Foo::Bar
end

puts X.test
# test.rb:28:in `test': uninitialized constant Bar (NameError)
#         from test.rb:28:in `<main>'

Why? I always though class Something and Something = Class.new are equivalent. What's the actual difference?

Then I had a strike of inspiration, and recalled there's alternative way to define class methods, which actually worked:

X = Class.new do
  class << self
    include Foo
    def test
      Bar
    end
  end
end

begin
  X::Bar
rescue => exception
  puts "#{exception}"
  # => uninitialized constant X::Bar
end

puts X.test
# Bar

Again - why this one work, and why the exception is now different: private constant Foo::Bar vs uninitialized constant X::Bar?

It seems like those 3 ways of initializing classes differ in a nuanced way.

  1. does exactly what I want: Bar is accessible internally, and accessing it gives exception about referencing private constant.
  2. second gives "ok" exception, but has no access to Bar itself
  3. third has access, but now gives slightly different exception

What is exactly going on in here?

Upvotes: 3

Views: 58

Answers (1)

Max
Max

Reputation: 22325

This is one of the biggest gotchas in Ruby: constant definition scope is partially syntactic, that is, it depends on how the code around it is structured.

module Foo
  Bar = "Bar"
end

Bar is inside a module definition, so it is defined in that module.

class << self
  include Foo
end

Bar gets included inside a class definition, so it is defined in that class.

Class.new do
  include Foo
end

There is no enclosing class or module (this is a normal method call with a block), so the constant is defined at top level.

As for your third error, I believe that is because the constant got defined in the singleton class (that's what class << self is) versus the class itself. They are two separate class objects.

Upvotes: 3

Related Questions