Why don't I see these module constants?

L

Lloyd Zusman

Suppose I have a file named 'modtest' which contains the following two
lines:

X = 1
Y = 2

Then, I execute this ruby code:

mod = Module.new {
load('modtest')
}
p mod.constants

When this code runs, I see this: []

In other words, it's an empty array, which means that I'm not seeing the
constants X and Y.

Why is this? Is there any way to load the file 'modtest' so that the
resulting anonymous module indeed contains these constants?

Thanks in advance.
 
J

Joel VanderWerf

Lloyd said:
Suppose I have a file named 'modtest' which contains the following two
lines:

X = 1
Y = 2

Then, I execute this ruby code:

mod = Module.new {
load('modtest')
}
p mod.constants

When this code runs, I see this: []

In other words, it's an empty array, which means that I'm not seeing the
constants X and Y.

Why is this? Is there any way to load the file 'modtest' so that the
resulting anonymous module indeed contains these constants?

It's not particularly loading that's causing the problem. The same
behavior occurs in

mod = Module.new {
XYZ = 1
}
p mod.constants

We can find out where that constant goes with this bit of code:

ObjectSpace.each_object(Module) do |obj|
if obj.constants.include?("XYZ")
puts "#{obj}::XYZ = #{obj::XYZ.inspect}"
end
end

The output:

[]
t.rb:8: warning: toplevel constant XYZ referenced by Class::XYZ
Class::XYZ = 1
t.rb:8: warning: toplevel constant XYZ referenced by Module::XYZ
Module::XYZ = 1
Object::XYZ = 1

The output says XYZ is toplevel. The constant is added in the top level
scope because that is the scope of the block in which the constant
assignment happens. Here's another example:

module Foo; end

module Bar
$block = proc { XYZ = 1 }
end

Foo.module_eval(&$block)

p Bar::XYZ # ==> 1
p Foo::XYZ # ==> uninitialized constant

Looks weird at first, but it's because constants are statically
(lexically) scoped. XYZ occurs in a block whose scope is Bar, so that's
where the constant is defined when the block is called.

You can get around this by using the string form of module_eval. A
string is not associated with its lexical scope in the way a proc is.

mod2 = Module.new
mod2.module_eval "X2 = 12"
p mod2.constants # ==> ["X2"]

So just use #read instead of #load, and pass that string to module_eval.
 
P

Pit Capitain

Lloyd said:
Suppose I have a file named 'modtest' which contains the following two
lines:

X = 1
Y = 2

Then, I execute this ruby code:

mod = Module.new {
load('modtest')
}
p mod.constants

When this code runs, I see this: []

In other words, it's an empty array, which means that I'm not seeing the
constants X and Y.

In addition to what Joel already said, note that

mod = Module.new {
load('modtest')
}

is the same as

mod = Module.new
load('modtest')

Kernel#load doesn't load the code in the context where it is called, but in the
context of the toplevel object (main) or in the context of an anonymous module,
if you set the second parameter to true. You can test this if you put a "p self"
into the file modtest.rb.

Regards,
Pit
 
L

Lloyd Zusman

Joel VanderWerf said:
[ ... ]

The output says XYZ is toplevel. The constant is added in the top level
scope because that is the scope of the block in which the constant
assignment happens. Here's another example:

module Foo; end

module Bar
$block = proc { XYZ = 1 }
end

Foo.module_eval(&$block)

p Bar::XYZ # ==> 1
p Foo::XYZ # ==> uninitialized constant

Looks weird at first, but it's because constants are statically
(lexically) scoped. XYZ occurs in a block whose scope is Bar, so that's
where the constant is defined when the block is called.

Thanks. I get it. It indeed seems counter-intuitive, but if that's the
way that constants are scoped in ruby, then so be it.

[ ... ]

So just use #read instead of #load, and pass that string to module_eval.

Yep. That works fine. Thanks.
 
L

Lloyd Zusman

Pit Capitain said:
[ ... ]

In addition to what Joel already said, note that

mod = Module.new {
load('modtest')
}

is the same as

mod = Module.new
load('modtest')

Kernel#load doesn't load the code in the context where it is called, but
in the context of the toplevel object (main) or in the context of an
anonymous module, if you set the second parameter to true. You can test
this if you put a "p self" into the file modtest.rb.

Yes, I see. Thanks.

I think that it would be nice if in a future ruby release, the
two-argument form of load() would return the anonymous module. Then,
with the following modtest file ...

X = 1
Y = 2

... I could then do this:

mod = load(modtest, true)
p mod.constants

... which should print this: ["X", "Y"]

The 'mod' return value would still be considered 'true', which is what
that call returns today if the load succeeds. In that case, we could at
least access that anonymous module if we wish.

Or here's another possibility. The second argument would get returned
to the caller, and if that argument was a module, the load would take
place within its context; otherwise, the load would just take place in
an anonymous module like today:

mod0 = Module.new
mod1 = load(modtest, mod0)
p mod0
p mod1

In this case, the load could take place within the mod0 module, which
would be returned; therefore, mod0 and mod1 would be equivalent after
this call. This would then print ["X", "Y"] twice.

Thoughts?
 
J

Joel VanderWerf

Lloyd said:
I think that it would be nice if in a future ruby release, the
two-argument form of load() would return the anonymous module. Then,

Ruby being ruby, it's easy enough to add a feature like this.

---------
class Module
def load_in_module(file)
module_eval(IO.read(file), File.expand_path(file))
end
end

m = Module.new
m.load_in_module("some-file.rb")
p m::X
---------

where some-file.rb is:
---------
X = 1
---------

I wrapped this up in a little library called "script" (a little small
for RAA, but it's there), which adds a few other niceties:

* instantiates the module and loads the file in one method call (whoopdeedo)
* defines #require so that the loaded file can require files relative to
its own directory
* makes all methods defined in the file "module_functions" so that they
are available in the form M.foo
* autoload equivalent
* can pass input to loaded files:
script = Script.load("my-script.rb") { |script| script::INPUT = 3 }
 

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,580
Members
45,055
Latest member
SlimSparkKetoACVReview

Latest Threads

Top