Class vs Module in constant lookup

P

Peter McLain

Constant lookup behaves differently if there is a class versus a
module in the name:

XYZ = 10

module M
end

class C
end

p defined? M::XYZ # => nil
p defined? C::XYZ # => "constant"

Is this difference intended? If so, what is the rationale?
 
C

Caleb Clausen

Constant lookup behaves differently if there is a class versus a
module in the name:

Extending your example a little to actually look up the constants
instead of just checking if they're defined?:

XYZ = 10

module M
end

class C
end

p defined? M::XYZ # => nil
p defined? C::XYZ # => "constant"

C::XYZ
#(irb):13: warning: toplevel constant XYZ referenced by C::XYZ
#=> 10

M::XYZ
# raises NameError


Note the warning for C::XYZ. You really shouldn't look up constants
this way, so I think a little inconsistency like this is tolerable.
 
P

Peter McLain

Extending your example a little to actually look up the constants
instead of just checking if they're defined?:

XYZ = 10

module M
end

class C
end

p defined? M::XYZ # => nil
p defined? C::XYZ # => "constant"

C::XYZ
#(irb):13: warning: toplevel constant XYZ referenced by C::XYZ
#=> 10

M::XYZ
# raises NameError


Note the warning for C::XYZ. You really shouldn't look up constants
this way, so I think a little inconsistency like this is tolerable.

Well, the code isn't mine, but I'd still like to understand if
there is a rationale for the difference in behavior, or if this is
just another MRI implementation detail that's leaked out. Currently,
MagLev returns "constant" for both classes and modules, but that
breaks third party code. E.g,:

return unless defined? RDoc::VERSION # RDoc 1 does not have VERSION
# code that blows up if RDoc 1 being used...

One could argue that the proper test, which works for both classes and
modules in all ruby implementations, is:

return unless RDoc.const_defined? :VERSION

But I'd still like to understand if the difference in behavior is
intended or an implementation quirk, and if it is intended, what the
rationale is.
 
X

Xavier Noria

Constant lookup behaves differently if there is a class versus a
module in the name:

=C2=A0 =C2=A0XYZ =3D 10

=C2=A0 =C2=A0module M
=C2=A0 =C2=A0end

=C2=A0 =C2=A0class C
=C2=A0 =C2=A0end

=C2=A0 =C2=A0p defined? M::XYZ =C2=A0# =3D> nil
=C2=A0 =C2=A0p defined? C::XYZ =C2=A0# =3D> "constant"

Is this difference intended? If so, what is the rationale?

I believe C::XYZ works because C is a subclass of Object.
 
C

Caleb Clausen

Well, the code isn't mine, but I'd still like to understand if
there is a rationale for the difference in behavior, or if this is
just another MRI implementation detail that's leaked out. Currently,
MagLev returns "constant" for both classes and modules, but that
breaks third party code. E.g,:

return unless defined? RDoc::VERSION # RDoc 1 does not have VERSION
# code that blows up if RDoc 1 being used...

One could argue that the proper test, which works for both classes and
modules in all ruby implementations, is:

return unless RDoc.const_defined? :VERSION

But I'd still like to understand if the difference in behavior is
intended or an implementation quirk, and if it is intended, what the
rationale is.

Ok, I see your problem, and I sympathize.

Collisions of the toplevel constant VERSION with VERSIONs defined
inside classes are a repeated snafu. (It's bitten me.) This is
probably why MRI 1.9 no longer defines VERSION, but RUBY_VERSION
instead.

I'd suspect this is unintended, but for an authoritative answer you'll
have to get matz or another core maintainer to chime in. You might ask
on ruby-core; I suspect those guys don't pay a lot of attention to
this list nowadays.
 
P

Peter McLain

I believe C::XYZ works because C is a subclass of Object.

I think that will end up being part of the explanation, but I'm not
yet convinced that it *should* be part of the explanation.

p M.ancestors # => [M]
p C.ancestors # => [C, Object, Kernel]

The explanation for constant lookup in the Flanagan/Matz book, Section
7.9, starts off:

"When a constant is referenced without any qualifying namespace..."
and then goes on to explain constant lookup.

Under those rules, I would expect both M and C to find the constant,
and indeed, using unqualified names, they both do find it:

XYZ = 10

module M
p XYZ # => 10
p defined? XYZ # => "constant"
end

class C
p XYZ # => 10
p defined? XYZ # => "constant"
end

But the original example was a fully qualified path: M::XYZ and
C:XYZ. I didn't find any discussion of scoped constants. The draft
ruby spec, however, leads me to believe that the MRI behavior is a bug.

Section 11.4.3.2 of the spec discusses Scoped constant references.
Under my reading of that section, and our test case, then (following
the logic in that section):

(a) the primary-expression is M,
(b) M is a module
(c 1) XYZ is the constant-identifier
(c 2) XYZ is not one of the constants defined in M
(c 3 i) There are no included modules in M (skip)
(c 3 ii) N/A
(c 3 iii) Goto step e of section 11.4.3.1

11.4.3.1
(e) M is not a class
(e 1) Search Object for a binding for XYZ

So we *should* find it in Object and MRI is incorrect to return nil.

Did I miss something?
 
X

Xavier Noria

The explanation for constant lookup in the Flanagan/Matz book, Section 7.= 9,
starts off:

=C2=A0"When a constant is referenced without any qualifying namespace..."= and
then goes on to explain constant lookup.

Yes I also checked the book and albeit it is clear that the algorithm
applies to unqualified names, it is not that much clear what exactly
applies to qualified names.
 
X

Xavier Noria

Section 11.4.3.2 of the spec discusses Scoped constant references. =C2=A0= Under my
reading of that section, and our test case, then (following the logic in
that section):

=C2=A0(a) the primary-expression is M,
=C2=A0(b) M is a module
=C2=A0(c 1) XYZ is the constant-identifier
=C2=A0(c 2) XYZ is not one of the constants defined in M
=C2=A0(c 3 i) There are no included modules in M (skip)
=C2=A0(c 3 ii) N/A
=C2=A0(c 3 iii) Goto step e of section 11.4.3.1

11.4.3.1
=C2=A0(e) M is not a class
=C2=A0(e 1) Search Object for a binding for XYZ

So we *should* find it in Object and MRI is incorrect to return nil.

Or else the spec needs rewording.

Looking for Object for non-scoped constants in modules seems like a
lexical rule to me, akin to searching Module.nesting.

In a scoped constant reference I wouldn't expect lexical-like steps to
be followed, so it might be the case that M::XYZ does not have to look
in Object, while C::XYZ needs to because of the ancestors rule. That
would a posteriori explain MRI's behavior.

We need an authoritative answer, I saw the question was posted to
ruby-core, let's see.
 
P

Peter McLain

Since I didn't get a good answer here, I posted the question on ruby-
core, and it looks like I've caught the attention of the core team.
So, if you're interested in the resolution, you should follow:
[ruby-core:28482] Question on scoped constant resolution Class vs Module
 
X

Xavier Noria

Or else the spec needs rewording.

Looking for Object for non-scoped constants in modules seems like a
lexical rule to me, akin to searching Module.nesting.

In a scoped constant reference I wouldn't expect lexical-like steps to
be followed, so it might be the case that M::XYZ does not have to look
in Object, while C::XYZ needs to because of the ancestors rule. That
would a posteriori explain MRI's behavior.

We need an authoritative answer, I saw the question was posted to
ruby-core, let's see.

Just a followup, the spec has been indeed revised, M::X does not look in Object.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,582
Members
45,065
Latest member
OrderGreenAcreCBD

Latest Threads

Top