Scope resolution works differently in ruby when written in different ways
Asked Answered
S

2

7

I faced this issue while developing a feature.Lets say there is following code:

case 1:
module Person
  module Employee 
    class Officer
      def self.print_class(obj)
        obj.is_a? Employee
      end
    end
  end
end

case 2:
class Person::Employee::Officer
  def self.print_class(obj)
       puts obj.is_a? Employee
  end
end

class Employee < ActiveRecord::Base
end

emp = Employee.last

and we have model Employee . Now

For case 1: Person::Employee::Officer.print_class(emp) gives "false"

For case 2: Person::Employee::Officer.print_class(emp) gives "true"

Why is this happening?

Storfer answered 16/7, 2020 at 12:0 Comment(2)
Here is a very good and complete article that can explain how constants lookup works in Ruby: cirw.in/blog/constant-lookup.html. Just keep in mind that a class in ruby is an object of the class Class assigned to a constant (most of time at least)Oldline
In case two, you're printing obj.is_a? Employee. The return of a puts is nil. Is that the problem?Paralipomena
H
1

:: is the scope resolution operator. Unlike the class and module keywords it does not reopen the module and properly set the module nesting.

For example:

TEST = "I'm in the global scope"

module Foo
  TEST = "I'm scoped to foo"
end


module Foo::Bar
  def self.test
    TEST 
  end

  def self.nesting
    Module.nesting
  end
end

puts Foo::Bar.test # "I'm in the global scope"
puts Foo::Bar.nesting.inspect [Foo::Bar]

This is because the module nesting is resolved lexically at the point of definition. When you do module Foo::Bar that module nesting is the global scope - when you reference TEST it is not resolved to Foo::TEST since Foo is not in the module nesting.

In your case 2 Employee is resolved to ::Employee not Person::Employee.

Therefore you should always explicitly nest classes and modules as it will set the correct module nesting and avoid these very unexpected module lookups.

TEST = "I'm in the global scope"

module Foo
  TEST = "I'm scoped to foo"
  module Bar
    def self.test
      TEST 
    end

    def self.nesting
      Module.nesting
    end
  end
end

puts Foo::Bar.test # "I'm scoped to foo"
puts Foo::Bar.nesting.inspect [Foo::Bar, Foo]
Henriettahenriette answered 17/7, 2020 at 14:56 Comment(0)
O
0

At first I will try to make clearer what is the context in which the class method Emplyee::print_class is defined:

# top-level context

class Employee
end

module Person
  module Employee 
    class Officer
      # context_a the context of case 1 

      ## here the constant Employee is a Module
      p (Employee.class) # -> Module
      # ## If you want a reference to a constant defined at the top-level(the Employee class)
      # # you may preceded it with ::
      p ::Employee.class # ->  class
      p ::Employee == Employee # -> false
    end
  end
end

class Person::Employee::Officer
  #context_b the context of case 2

  # here The constant Employee is the class Employee
  # There are not Employee Constant defined in this context
  # the constant look up will reach the top-level context
  # and Employee will reference to ::Employee
  p (Employee.class) # -> Class
  p ::Employee == Employee # -> true
end

Now we can take into consideration the method Emplyee::print_class definition and execution.

When you defined the method Emplyee::print_class you used the constant Employee:

When and in which context this constant is evaluated to be a class or a module or a string?

def self.print_class(obj)
  obj.is_a? Employee
end

The answer to when is: when the method is executed.

The answer to in which context is: the context where you defined the method, not the one you execute it. For the Constant is the outer scope of the method definition (If you try to create a constant inside the method you will get an error 'dynamic constant assignment').

In your example case 1 the context will be context_a.

In your example case 2 the context will be context_b, but the constant look-up will reach the top-level context.

Oldline answered 17/7, 2020 at 14:55 Comment(2)
Your answer should be able to stand on its own without reference to a comment. This is a Q&A site. Not a discussion forum.Henriettahenriette
@Henriettahenriette I deleted the reference to the comment and the answer stands on its own now, thanks for the suggestion.Oldline

© 2022 - 2024 — McMap. All rights reserved.