Constant naming and lookup in Ruby

I thought I had a pretty good understanding of how constant lookup worked in Ruby, but I encountered a surprising piece of behavior recently and I wanted to share it.

We had a god model at work that contains thousands of lines of code, much of which is in methods that aren’t truly core to the model. In a long-term attempt to clean this up, we started by moving some of the methods in this model into dependency mixins, like so:

class User
  CONSTANT = 10
  include User::LoginDependency
  #lots of methods
end
module User::LoginDependency
  #login-specific methods go in this file
end

My pair and I started moving methods over, and it all started out easy enough.

Then we encountered something surprising: a method in one of the mixins referenced a constant from the god model, and we got the error uninitialized constant User::LoginDependency::CONSTANT.

This, to me, was surprising to see. Ruby, after all, is a dynamic language with late binding, and I expect things like this to be figured out at runtime. My first hand-wavy assumption was that constant lookup works in a manner similar to sending messages to objects, which made me expect that to work.

Turns out, how you name something makes a difference. If I want to have a module called User::LoginDependency, there are two ways I can define it:

module User
  module LoginDependency
  end
end

module User::LoginDependency
end

I can get to both by typing User::LoginDependency, but the key difference is that in the first case, Ruby understands it as “a module LoginDependency nested inside of the module User,” and in the second case, it’s “the module User::LoginDependency”.

For the nested version of User::LoginDependency, if Ruby doesn’t find the constant inside of LoginDependency, Ruby will work its way up the namespace chain and look for the constant in User. but in the latter case, there isn’t a next level up that you can look, because User::LoginDependency is a top-level defined constant.

For this refactoring, my pair and I made the decision that if the new module’s methods were the only ones to reference a given constant we’d move the constant over too, otherwise we’d leave the constant in the original model and update the new dependency mixin to reference the constant’s fully qualified name; e.g. User::CONSTANT instead of just CONSTANT.

Alternatively, we could have switched these mixins over to being defined with a nested namespace, but in our case User is a class and we didn’t want to be re-opening that class just to add these modules that would later be included. Besides, our approach seemed more appropriate, because it keeps the mixin-specific constants private to that mixin, and the shared stuff is all together in the model that those mixins are all included in.

Leave a Reply

Your email address will not be published. Required fields are marked *