Class_eval doesn't find const_missing

I am trying to write a DSL which uses method_missing and const_missing
to catch upper and lowercase names (specified in isolation) and fetch
the asscoated (already initialized) objects. This works great until I
need to use class_eval which seemingly doesn’t follow the custom
const_missing method.

Here’s a more simple example of a class with a constant and
#method_missing and #const_missing methods.

class DummyClass

NAME = ‘dummy constant’

def self.method_missing(method,*args,&block)
return “this #{method} was captured by #method_missing
end

def self.const_missing(constant)
return “this #{constant} was captured by #const_missing
end

end

Accessing the constant is no problem

DummyClass::NAME
=> “dummy constant”

Unknown methods and constants are handled as expected

DummyClass::ANOTHER_CONSTANT
=> “this ANOTHER_CONSTANT was captured by #const_missing
DummyClass.some_method
=> “this some_method was captured by #method_missing

Accessing the constant via class_eval is variable - it works when
evaluating strings but not blocks.

DummyClass.class_eval “NAME”
=> “dummy constant”
DummyClass.class_eval {NAME}
NameError: uninitialized constant NAME
from (irb):14
from (irb):14:in `class_eval’
from (irb):14
from :0

This is partly understood since blocks are scoped to the context in
which they were created. But even if the explicit constant is not
recognised, why isn’t the class #const_missing method invoked?

Does the class_eval method allow the method_missing method to be used?

DummyClass.class_eval “dummy_method”
=> “this dummy_method was captured by #method_missing
DummyClass.class_eval {dummy_method}
=> “this dummy_method was captured by #method_missing

Yes. #method_missing works as expected, in both string and block
formats. So only #const_missing is causing a problem.

Interestingly, an explicit call to #const_missing inside a class_eval
block DOES work.

DummyClass.class_eval {const_missing :ANOTHER_CONSTANT}
=> “this ANOTHER_CONSTANT was captured by #const_missing

Anybody understand this, and is there a way to get around it?

Cheers

If you add this code:

class Object
def self.const_missing(constant)
return “this #{constant} found in obj!”
end
end

Then you get the following:

DummyClass.class_eval “NAME”
=> “dummy constant”

DummyClass.class_eval {NAME}
=> “this NAME found in obj!”

NAME
=> “this NAME found in obj!”

Not sure if that helps you much, but it seems that class_eval doesn’t
affect the constant lookup in the way that you’d like.

Const lookup varies between 1.8, 1.9.1, and 1.9.2.

Basically, constants are lexically scoped.

You can use “const_get” I believe to lookup your constant in the
current binding rather than in the lexical scope.

stephen

Andrew Berkeley wrote in post #994369:

Interestingly, an explicit call to #const_missing inside a class_eval
block DOES work.

DummyClass.class_eval {const_missing :ANOTHER_CONSTANT}
=> “this ANOTHER_CONSTANT was captured by #const_missing

Anybody understand this, and is there a way to get around it?

This line:

DummyClass.class_eval {const_missing :ANOTHER_CONSTANT}

is equivalent to:

DummyClass.class_eval {
self.const_missing(:ANOTHER_CONSTANT)
}

And because class_eval() changes self inside the block to be equal to
the receiver, the above is equivalent to:

DummyClass.class_eval {
DummyClass.const_missing(:ANOTHER_CONSTANT)
}

…producing the output you see, which is to be expected.

Also, note the output here:

DummyClass.class_eval do
puts self::NAME
end

–output:–
dummy constant

In “The Well-Grounded Rubyist” David Black explains that constants are
like files in a file system, and depending on what “directory” you are
currently in, it will determine the “path name” to the constant that you
are interested in retrieving. It seems that class_eval does not affect
the path name to a constant. But then I don’t understand how
class_eval’ing a string changes that.

DummyClass.class_eval {NAME}
=> “this NAME found in obj!”
NAME
=> “this NAME found in obj!”

Not sure if that helps you much, but it seems that class_eval doesn’t
affect the constant lookup in the way that you’d like.

That tells me that NAME is being looked up in the current scope, which
is the block plus everything outside the block. Outside the block self
is equal to main, and the current class is equal to Object. So when
NAME isn’t found, Object.const_missing is called.

Stephen Prater wrote in post #994391:

Const lookup varies between 1.8, 1.9.1, and 1.9.2.

Basically, constants are lexically scoped.

You can use “const_get” I believe to lookup your constant in the
current binding rather than in the lexical scope.

That works for me in ruby 1.9.2:

DummyClass.class_eval do
puts const_get(:NAME)
end

–output:–
dummy constant

That makes sense because the call is really:

puts self.const_get(:NAME)

and because self is equal to DummyClass, that is equivalent to:

puts DummyClass.const_get(:NAME)

which like class_eval’ing a string serves to change the “directory” for
the lookup to DummyClass.

7stud – wrote in post #994396:

In “The Well-Grounded Rubyist” David Black explains that constants are
like files in a file system, and depending on what “directory” you are
currently in, it will determine the “path name” to the constant that you
are interested in retrieving. It seems that class_eval does not affect
the path name to a constant.

But then I don’t understand how
class_eval’ing a string changes that.

Here is a blog post from 2007 asking the exact same question as the op:

http://www.pgrs.net/2007/9/12/ruby-constants-have-weird-behavior-in-class_eval

As one person replied, a constant is looked up by the parser, so the
constant’s lexical scope determines the proper ‘path’ to the constant
(which is the path from the ‘directory’ where the constant is referenced
to the ‘directory’ in which the constant is defined)–while eval’ing a
string happens in the ‘dynamic scope’ when the code is executed, i.e.
inside the class.

Thanks People - very helpful and seems pretty clear what’s going on
then. A lesson learned for me!

The Object monkeypatch appears to work well for what I was wanting to
do…