Rails unable to autoload constant from file despite being defined in that file
Asked Answered
A

3

20

This is a tricky one to explain. I have a module in another module namespace like so:

# app/models/points/calculator.rb
module Points
  module Calculator
    def self.included(base)
      base.send(:include, CommonMethods)
      base.send(:include, "Points::Calculator::#{base}Methods".constantize)
    end
  end
end

So then in other classes all I need to do is:

class User
  include Points::Calculator
end

I've specified this directory in application.rb to be autoloadable...(even though i think rails recurses through models...)

config.autoload_paths += Dir[ Rails.root.join('app', 'models', "points") ]

In development env, everything works fine. When running tests(and production env), I get the following error:

Unable to autoload constant Points::Calculator, expected /Users/pete/work/recognize/app/models/points/calculator.rb to define it (LoadError)

I actually followed the advice here to fix the problem: Stop Rails from unloading a module in development mode by explicitly requiring calculator.rb in application.rb.

However, why is this happening??

I stuck some debug output in ActiveSupport's dependencies.rb file and noticed that this file is being required twice. The first time its required I can see that the constant is indeed loaded.

But the 2nd time its required the constant has been unloaded as far as Rails can tell, but when the actual require is called, ruby returns false because ruby knows its already required it. Then Rails throws the "unable to autoload constant" error because the constant still isn't present and ruby didn't "re-require" the file.

Can anyone shed light on why this might be happening?

Almoner answered 3/7, 2014 at 5:29 Comment(4)
Does removing points from the lost of autoloadable paths help? It shouldn't be necessaryChinese
Yes, I've tried it with and without in the autoload path. Same problem.Almoner
Seem to be having the same problem. Very annoying to have to require the class, since I have about 30+ such classes.Mashie
you need to define the missing variable may be controller name or anything in zetwerk mode.Hogshead
H
11

Rails augments the constant lookup mechanism of ruby.

Constant lookup in Ruby:

Similar to method missing, a Module#constant-missing is invoked when a reference to a constant fails to be resolved. When we refer to a constant in a given lexical scope, that constant is searched for in:

Each entry in Module.nesting 
Each entry in Module.nesting.first.ancestors
Each entry in Object.ancestors if Module.nesting.first is nil or a module.

When we refer to a constant, Ruby first attempts to find it according to this built-in lookup rules.

When ruby fails to find... rails kicks in, and using its own lookup convention and its knowledge about which constants have already been loaded (by ruby), Rails overrides Module#const_missing to load missing constants without the need for explicit require calls by the programmer.

Its own lookup convention?

Contrasting Ruby’s autoload (which requires the location of each autoloaded constant to be specified in advance) rails following a convention that maps constants to file names.

Points::Calculator # =>points/calculator.rb

Now for the constant Points::Calculator, rails searches this file path (ie 'points/calculator.rb') within the autoload paths, defined by the autoload_paths configuration.

In this case, rails searched for file path points/calculator in its autoloaded paths, but fails to find file and hence this error/warning is shown.

This answer is an abstract from this Urbanautomation blog.

Edit: I wrote a blog about Zeitwerk, the new code reloader in Rails. Check it out at -> https://blog.bigbinary.com/2019/10/08/rails-6-introduces-new-code-loader-called-zeitwerk.html

Hussey answered 3/7, 2014 at 8:0 Comment(1)
indeed, I've seen this post and thanks for your explanation. But, the file exists and is loaded correctly once, but as stated above, I can see it required twice and on the 2nd go around, the constant has been unloaded. I'm unclear how your explanation addresses that fact.Almoner
C
3

If someone is having this issue in rails 6 which has zeitwerk autoloader,

Change ruby constant lookup back to classic in your application.rb

# config/application.rb
#...
config.autoloader = :classic
#...

Read more details here Rails Official Guides

Companionable answered 1/9, 2020 at 5:2 Comment(3)
But what if you specifically want to fix this while using zeitwerk?Cottontail
Read @MIdhun Krishna answer, Points::Calculator # =>points/calculator.rbCompanionable
WOW this totally worked as 2021 on rails 6.0.3. Could not find any other suitable solution.. GawdDamnable
G
0

Calculator should be a class to be autoloaded correctly

module Points
  class Calculator
  ...
  end
end
Gadgetry answered 3/7, 2014 at 6:41 Comment(5)
sorry, I described it wrong, its a module in a module...I've updated the description above to be more accurate...does the fact that its a module affect autoloading?Almoner
In that case I recommend putting the file in the lib directory instead of app/modelsGadgetry
lib is for non-application specific libraries, hence me putting this app/. I could, however, see an argument for putting this in an app/lib or perhaps app/concerns or app/models/concerns directoryAlmoner
Yeah, app/models/concerns is probably the best place for it.Gadgetry
Whether a module or class, it doesn't matter with autoloading. Rails by convention should autoload the models directory. Whether the placement is ideal or not is not part of the question. The point is, if Points::Calculator is defined within app/models/points/calculator.rb it should be autoloaded.Flit

© 2022 - 2024 — McMap. All rights reserved.