3

According to this answer, one can get a constant into the global namespace with an include at the top level, at least in IRB. Naively, I thought I could do the same trick inside a module:

module Foo
  class Bar
    def frob(val)
      "frobbed #{val}"
    end
  end
end

module Baz
  include Foo
  class Qux
    def initialize(val)
      @val = val
      @bar = Bar.new
    end
    def twiddle
      @bar.frob(@val)
    end
  end
end

However, attempting to use this produces:

NameError: uninitialized constant Baz::Qux::Bar

I then tried overriding Baz::const_missing to forward to Foo, but it doesn't get called. Baz::Qux::const_missing does, but I suppose by that time the interpreter has already given up on looking in Baz.

How can I import Foo constants into Baz such that classes under Baz don't have to qualify them?

(Note that I don't just want to declare Qux etc. in Foo instead of Baz, for reasons of consistency between directory structure and module names.)


ETA: If I create another class Baz::Corge, Baz::Qux can refer to it without qualification. So clearly there is some sense of "peer scope". I'm aware there are several ways to make it possible for Qux to access Bar, but I'm looking for a way for all classes in Baz to access Bar (and all other classes in Foo) without qualification.

Community
  • 1
  • 1
David Moles
  • 48,006
  • 27
  • 136
  • 235
  • 1
    I'd say Ruby *unexpectedly?* fails at resolving of `Bar` constant, although it resides in the nearest outer scope, because it is an *inherited* constant, not a constant of the receiver - module `Baz`. I can't find source for the definitive answer, deciding if this is an intended behavior or a bug in implementation. Same issue in latest CRuby 2.2.3, also in 2.0.0 version. – joanbm Oct 06 '15 at 03:14
  • Did meant, whether inherited constants can be directly accessed only in the same scope as the point of call, otherwise only fully qualified ? Not sure about this, maybe some veteran Ruby expert could make it clear ? – joanbm Oct 06 '15 at 03:20

4 Answers4

0

Ok.. The problems is that the constant are (for some reason) not defined inside the Quz class, but in the upper scope, the module Baz scope. If only we could somehow delegate (current word?) the constants calls in Quz to the upper (Baz) scope- We can:

Using Module#const_missing

I'll show it twice: once for your private case, and once for a more general approach.

1) solve for private case:

module Foo
  class Bar
    def frob(val)
      "frobbed #{val}"
    end
  end
end

module Baz
  include Foo
  class Qux
    def self.const_missing(c)
      #const_get("#{self.to_s.split('::')[-2]}::#{c}")
      const_get("Baz::#{c}")
    end

    def initialize(val)
      @val = val
      @bar = Bar.new
    end
    def twiddle
      @bar.frob(@val)
    end
  end
end

puts Baz::Qux.new('My Value').twiddle

So what happens here? almost same thing- only that when the error is received- it's kinda rescued and arriving (or fall back to) the const_missing function to receive a new value- This apply for both Constants and Classes (apparently they are the same type).

But that mean we have to add the self.const_missing(c) method to every class inside module Baz- Or we can just iterate every classes in module Baz and add it (There are probably better ways to do it, but it works)


2) A more automated approach:

module Foo
  class Bar
    def frob(val)
      "frobbed #{val}"
    end
  end
end

module Baz

  class Qux
    def initialize(val)
      @val = val
      @bar = Bar.new
    end

    def twiddle
      @bar.frob(@val)
    end
  end

  def self.add_const_missing_to_classes
    module_name = self.to_s

    #note 1
    classes_arr = constants.select{|x| const_get(x).instance_of? Class} #since constants get also constants we only take classes

    #iterate classes inside module Baz and for each adding the const_missing method
    classes_arr.each do |klass| #for example klass is Qux
      const_get(klass).define_singleton_method(:const_missing) do |c|
        const_get("#{module_name}::#{c}")
      end
    end
  end

  add_const_missing_to_classes

  #we moved the include Foo to the end so that (see note1) constants doesn't return Foo's classes as well
  include Foo
end

puts Baz::Qux.new('My Value').twiddle

Here at the end of module Baz, after all classes were defined. we iterate them (inside the add_const_missing_to_classes method). to select them we use the Module#constants method which returns an array of a module constants- meaning both CONSTANTS and CLASSES, so we use select method to only work on classes.

Then we iterate the found classes and add the const_missing class method to the classes.

Notice we moved the include Foo method to the end- because we wanted the constants method not to include constants from module Foo.

Surly there are better ways to do it. But I believe the OP's question:

How can I import Foo constants into Baz such that classes under Baz don't have to qualify them?

Is answered

Roko
  • 1,233
  • 1
  • 11
  • 22
  • This will only work if all classes in `Baz` are defined before `add_const_missing_to_classes` is called, correct? – David Moles Oct 06 '15 at 21:55
  • Yes, that's why it's called at the end. But that was my way of adding `const_missing` to classes- you can do it however you want – Roko Oct 06 '15 at 21:58
0

Here's what I eventually came up with. In the same file that initially declares Baz:

module Foo
  def self.included(base)
    constants.each { |c| base.const_set(c, const_get("#{self}::#{c}")) }
  end
end

module Baz
  include Foo
  #... etc.
end

When Baz includes Foo, it will set a corresponding constant Baz::Whatever for every Foo::Whatever, with Foo::Whatever as the value.

If you're worried Foo may already define self.included, you can use alias_method to adjust for that:

module Foo
  alias_method :old_included, :included if self.method_defined? :included
  def self.included(base)
    old_included(base) if method_defined? :old_included
    constants.each { |c| base.const_set(c, const_get("#{self}::#{c}")) }
  end
end

This approach has two limitations --

  1. All the constants (including classes) in Foo that we care about must be defined at the time include Foo is evaluated -- extensions added to Foo later will not be captured.
  2. The file that defines Foo.included here must be required before any file in Baz that uses any of those constants -- simple enough if clients are just using require 'baz' to pull in a baz.rb that in turn uses Dir.glob or similar to load all the other Baz files, but it's important not to require those files directly.

Roko's answer gets around problem (1) above using const_missing, but it still has an analogous problem to (2), in that one has to ensure add_const_missing_to_classes is called after all classes in Baz are defined. It's a shame there's no const_added hook.

I suspect the const_missing approach also suffers performance-wise by depending on const_missing and const_get for every constant reference. This might be mitigated by a hybrid that caches the results, i.e. by calling const_set in const_missing, but I haven't explored that since trying to figure out scoping inside define_singleton_method always gives me a headache.

Community
  • 1
  • 1
David Moles
  • 48,006
  • 27
  • 136
  • 235
-1

As you can see from the error given: NameError: uninitialized constant Baz::Qux::Bar

It tries to find the Bar class inside Qux class scope- But can't find it there- why is that?

Because it's not in this scope- It's in the Baz modlue scope, where you used include Foo

So you have two options: 1) address the correct scope when calling Bar class, so change this:

@bar = Bar.new

into this:

@bar = Baz::Bar.new

Like that:

module Baz
  include Foo
  class Qux
    def initialize(val)
      @val = val
      @bar = Baz::Bar.new
    end
    def twiddle
      @bar.frob(@val)
    end
  end
end

Or,

2) insert the include Foo into the class Qux itself:

module Baz
  class Qux
    include Foo
    def initialize(val)
      @val = val
      @bar = Bar.new
    end
    def twiddle
      @bar.frob(@val)
    end
  end
end

--EDIT--

As stated by joanbm this doesn't explain this behavior. You might want to take a look at Scope of Constants in Ruby Modules. Though that post is about constants (not classes) the principles are the same.

Community
  • 1
  • 1
Roko
  • 1,233
  • 1
  • 11
  • 22
  • This however does not explain `Module#include` behavior. Try in the `module Baz` scope define a constant in another way like `Bang = 123` or `const_set :Bang, 123` or define a class and unqualified access from `Qux#initialize` would be properly resolved. – joanbm Oct 06 '15 at 01:17
  • Yes, I'm aware there are several ways to make it possible for `Qux` to access `Bar`, but I'm looking for a way for _all_ classes in `Baz` to access `Bar` (and all other classes in `Foo`) without qualification. – David Moles Oct 06 '15 at 18:52
-1

Since Foo is included in Baz, that's where Bar is found -- it isn't found in Qux. So you can change

@bar = Bar.new

to

@bar = Baz::Bar.new

or move include Foo inside class Qux

Brian
  • 967
  • 7
  • 12
  • As noted in the question, I'm aware there are several ways to make it possible for `Qux` to access `Bar`. I'm looking for ways for _all_ classes in `Baz` to access `Bar` (and all other classes in `Foo`) _without_ qualification. – David Moles Oct 07 '15 at 17:30