Not-so-private constants in Ruby

TLDR; Putting a constant inside private block is not enough to make it private. To achieve the desired effect you should use Module#private_constant method.

Using constants is idiomatic for Ruby. We use them to store something meaningful (a well-suited name is very important), connected with a class that holds it. Consequently, we make our code easier to reason about, avoid duplication and, very often, more performant. Let’s back to the Language class from the previous blog post holding all languages supported by an imaginary Ruby application:

require 'set'

class Language
  def self.all
    SortedSet[
     :en,
     :pl,
     # ...another locales supported by the app
    ]
  end
end

It’s fairly common, that English is a default language for an application. To underline this fact, we can introduce a constant:

require 'set'

class Language
  DEFAULT = :en
  
  def self.all
    SortedSet[
     DEFAULT,
     :pl,
     # ...another locales supported by the app
    ].freeze
  end
end
Language::DEFAULT
=> :en
Language.constants
=> [:DEFAULT]
Language::DEFAULT.object_id
=> 1287708
Language::DEFAULT.object_id
=> 1287708 # the same object is returned always

How to make a constant private?

Let’s assume that the class is one of the first ones inside a fresh Ruby application and others are not interested in the fact that the app has a default language. Language class encapsulates all the logic and that’s totally fine. To hide the constant we can limit its visibility:

require 'set'

class Language  
  def self.all
    SortedSet[
     DEFAULT,
     :pl,
     # ...another locales supported by the app
    ].freeze
  end

  private

  DEFAULT = :en
end

…and we are done for today, aren’t we?

Language::DEFAULT
=> :en

Ruby, you bastard 😉 Even though putting a constant inside private block is very intuitive due to the fact that we define private methods in that way, it does not do the same with constants.

Luckily, private_constant method was added to Module class in Ruby 1.9.3 to address this particular issue:

require 'set'

class Language
  DEFAULT = :en 
  private_constant :DEFAULT  
  
  def self.all
    SortedSet[
     DEFAULT,
     :pl,
     # ...another locales supported by the app
    ].freeze
  end
end
Language.constants
 => []
Language::DEFAULT
NameError (private constant Language::DEFAULT referenced)

Outside word is not aware of the private constant, checkmate 🙂

Summary

Our intuition is not always right and it is totally normal. Some time ago I was sure that putting a constant inside private block to make it hidden was enough when it wasn’t. You live you learn.

P.S. Do you find something else misleading in Ruby world? Share your thoughts in comments.

 

Igor Springer

I build web apps. From time to time I put my thoughts on paper. I hope that some of them will be valuable for you. To teach is to learn twice.