Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mraaroncruz/9ae9c740c7b7ef4cacd2a6f5c3de88f3 to your computer and use it in GitHub Desktop.
Save mraaroncruz/9ae9c740c7b7ef4cacd2a6f5c3de88f3 to your computer and use it in GitHub Desktop.

Constant lookup in Ruby can happen lexically or through the ancestry tree of the receiver(a class or module). You can identify which lookup rules are being applied by the context you're in or by the syntax being used to define a class or module.

A class body that is defined as class A::B::C; …; end will lookup constants through the ancestry tree when a constant is evaluated in its class body. Anytime you see A::B::C being used as syntax to define a class or lookup the value of a constant the ancestry tree is being used for the lookup.

How would we apply lexical lookup in the definition of A::B::C if we know how to apply ancestry lookup? The syntax you use for the definition is what makes the difference. The first example defines a module with lexical constant lookup while the second example is defined to use the ancestry tree for lookup.

The examples come with comments that explain what is happening and what it means for a constant to be looked up 'lexically' or through the ancestry tree.

LEXICAL LOOKUP

require "test/unit"
include Test::Unit::Assertions

module A
  D = 5
  module B
    module C
     # C is asked if its constant table has 'D'. It's not.
     # The next scope is B. it also does not have 'D' in its
     # constant table. The next scope is A. A::D is found and
     # the lookup stops.
     #
     # the pattern to see here is that lookup is happening by
     # traversing back through the nested scopes. this is said
     # to be a lexical constant lookup. Anytime a class or module
     # body is defined like this constant lookup happens lexically.
     #
     assert D == 5
    end
  end
end

ANCESTRY LOOKUP

class D
  X = 5
end

class A::B::C < D
  # A constant lookup through the ancestry tree is implied
  # from the syntax we used to define the class. Anytime you
  # see the syntax A::B::C (in a class definition or expression)
  # you know constant lookup is going through the ancestry tree.
  #
  # C is asked does 'X' exist in its constant table, it does not.
  # Next the superclass of C is asked, which is D. D finds 'X' in
  # its constant table with a value of 5.
  #
  # neither B or A are asked for the constant like in a lexical
  # lookup. If X were not defined, C would be asked first, then
  # D(the superclass of C), then its superclass(Object) is asked,
  # at which point a NameError is raised.
  #
  X # => 5
end

What about the A::B::C syntax to read the value of a constant? we know the lookup will go through the ancestry tree but lets take a closer look:

require "test/unit"
include Test::Unit::Assertions

class A
  X = 42
  class B
    X = 5
  end

  class C < B
  end
end

# A::C is asked if its constant table has 'X', It's not.
# Next its superclass is asked: A::B. it does own the constant 'X'
# and returns a value of 5. A lexical lookup(starting at A::C) would 
# have found 42(A::X)..
#
assert A::C::X == 5 # => true

LEXICAL LOOKUP AND CLASS/INSTANCE_EVAL

A class eval or instance eval will look for constants lexically. The difference about a lexical lookup in a class body definition and a block passed to class_eval is that class_eval will lookup from the calling scope and not the scope of 'self'(a Class or Module). An example(with comments) to illustrate:

require "test/unit"
include Test::Unit::Assertions

X = 42

class A
  X = 5
  class B
    class C
    end
  end
end


A::B::C.class_eval do
  # A value of 42 for X. C is asked for X, it is not found, so
  # the calling scope(main/Object) is asked next. It is found
  # as Object::X and returns a value of 42. The lexical scope does
  # start at C but traverses into the calling scope and not the
  # definition of A::B::C.
  #
  assert X == 42 # => true
end

FINISHING UP

I think it is useful to know how constant lookup happens and in what contexts. You can follow the codepath much easier. To finish up an example of how those codepaths can be followed:

require "test/unit"
include Test::Unit::Assertions

class A
  class B
    # The lookup path for X (lexical lookup):
    # C -> B -> A -> calling scope(main/Object).
    class C
      assert_raise(NameError) { X }
    end
  end
end

class A; end
class D < A; end
class A::B::C < D
  # The lookup path for X (ancestor lookup):
  # C -> D -> A -> Object
  assert_raise(NameError) { X }
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment