Better Know A Ruby Thing #2: Constants
A fun thing about learning Ruby is that sometimes concepts that have the same name as in other languages have different behavior and purpose in Ruby.
Today: constants
They aren't actually constant.
They aren't only used for small strings or magic literals. They aren't even mostly used for that in most Ruby programs.
Constants are one of the core pieces of Ruby and they aren't super-well documented in the official site, so here we go...
Hi -- we've gotten some comments that the code snippets don't look good on Apple Mail in dark mode. Buttondown is working on this, but if you need to, you can also find this newsletter on the web at https://noelrappin.com/blog/2023/10/better-know-a-ruby-thing-22-constants
If you like this and want to see more of it in your mailbox, you can sign up at http://buttondown.email/noelrap. If you really like this and would like to support it financially, thanks, and you can sign up for a Monthly Subscription for $3/month or an Annual Subscription for $30/year.
Also, you can buy Programming Ruby 3.2 in ebook from Pragmatic or as a pre-order from Amazon, coming sometime in November.
Thanks!
How are constants normally demonstrated?
Constants are usually presented as a special kind of identifier that you use to represent a value that you don't expect to change. Often you are giving a logical name to a magical literal value that is used multiple times in your code:
class HasAConstant
MAX_THINGS = 12
end
That constant is accessible within the class HasAConstant
as MAX_THINGS
and outside the class as HasAConstant::MAX_THINGS
.
And you can use constants to do that in Ruby, but there's something important you should know...
* class HasAConstant
* MAX_THINGS = 12
> end
=> 12
> HasAConstant::MAX_THINGS
=> 12
> HasAConstant::MAX_THINGS = 15
(irb):5: warning: already initialized constant HasAConstant::MAX_THINGS
(irb):2: warning: previous definition of MAX_THINGS was here
=> 15
irb(main):006> HasAConstant::MAX_THINGS
=> 15
Yes, you can change the value of "constants" in Ruby, and all you get is a mild rebuke. You literally get let off with a warning.
If even the mild rebuke is too much for you, you can even turn it off with the -W0
CLI option:
RUBYOPT="-W0" irb
irb(main):001> FRED = 3
=> 3
irb(main):002> FRED = 5
=> 5
So, if constants aren't constant, why are they called constants?
Probably because they are similar to constants in other languages, and because "variables whose names start with capital letters" is kind of verbose.
Okay then, what's a constant in Ruby, actually?
Any identifier that begins with a capital letter in Ruby is a "constant". That'll be the only time I use the sarcasm quotes, but you can imagine they are there every time.
The convention is that value constants like MAX_LIMIT
are written in all-caps with words separated by underscores, but that's just a community convention, the Ruby parser doesn't care.
Constants can, as you've seen, be used as the left hand side of an assignment statement.
Constants behave differently then variables:
- Local variables belong to local bindings, instance variables belong to instances, constants belong to the module or class in which they are declared. All the standard library API functionality pertaining to constants is defined in the class
Module
. - Constants have different lookup logic than variables, more on that in a second.
- Constants can not be defined inside a method, presumably because they are meant to be attached to a module or class and not to an instance.
You might have a few questions.
For example: "If constants start with capital letters, and class and module names also start with capital letters, then what are class and module names."
Class and module names are just constants, stored internally and with identical behavior to something like MAX_TRIES
. When you say class Foo
, Ruby creates a constant Foo
whose value is an instance of the class Class
and whose name is Foo
.
A follow up question, might be. "You said constants belong to modules or classes. But when I define class Foo
at the top level there is no module or class. Where is Foo
defined?"
Great question. You might not like the answer.
The "implicit receiver" (or self
) at the top level in Ruby is a special object named main
. The main
object is an instance of Object
. Top-level constants, including classes and modules, are added by Ruby on to the list of constants that Object
knows about.
"But that means that Object
has a list of constants that includes all the top-level classes that have been loaded?"
Yep.
> Object.constants.sort.take(5)
=> [:ARGF, :ARGV, :ArgumentError, :Array, :BasicObject]
Three of those are top-level classes.
"But that means that if I define a new class then Object gets a new constant? So every time I define a new class I'm technically monkey-patching Object
?
Afraid so.
> Object.constants.include?(:Banana)
=> false
> class Banana; end
=> nil
> Object.constants.include?(:Banana)
=> true
Do other languages have constants?
Most compiled languages have constants in one form or another, Java does, C# does. Dynamic languages tend to be hit or miss. JavaScript has the const
keyword, which prevents you from changing the object, but the object is mutable so you can, for example, add elements to an array declared as const
. Python constants are enforced by community standards, the language doesn't support them. Smalltalk doesn't have them as values, unless I'm really not remembering something.
Let's talk about constant lookup.
Constant lookup in Ruby is related to, but different from, method lookup.
If you reference a constant, Ruby will first look to see if the constant is defined in the class or module where the reference occurs. Unlike methods, constants don't have receivers -- they do have namespaces, and we'll get to that -- so constant lookup always starts with the place where the constant is referenced.
If the constant is not defined in the place of reference itself, Ruby checks to see if that class or module is nested inside another class or module and looks for the constant in that outer class or module, and so on until you get to the top level.
So:
TOP_MAX = 3
class Outer
OUTER_MAX = 4
class Inner
INNER_MAX = 5
end
end
Inside this snippet, if you are inside class Inner
, you can reference INNER_MAX
, but you can also reference OUTER_MAX
as Ruby walks up the nesting, and you can also reference TOP_MAX
.
If for some reason it's not clear what the nesting hierarchy is at a given point, you can find out what Ruby thinks it is by inserting a call to a class method of Module called Module.nesting
.
If Ruby can't find the constant in the nested namespaces, it will continue to look through the normal object hierarchy.
class Parent
PARENT_LIMIT = 10
end
class Child < Parent
end
Inside Child
, you can reference PARENT_LIMIT
, even though the two classes aren't nested, because Ruby will walk the ancestor chain if the nesting chain doesn't find anything. (At each step in the ancestor chain, it will also walk the nesting chain, if necessary). Eventually, it will get to Object
which references all the top-level classes and modules.
Constants, no matter how deeply nested, are available anywhere in your Ruby code through the scope resolution operator, ::
. The ::
can be placed between two constants, in which case it means "look for the second constant only inside the first". In our initial example, the Inner
class is accessible anywhere inside the program with Outer::Inner
, and the INNER_MAX
constant is accessible as Outer::Inner::INNER_MAX
.
The thing to remember here is that Ruby resolves a compound reference like Outer::Inner::INNER_MAX
as three different lookups, not one lookup, which means you can get in trouble if constant names are re-used in different places:
module Utils
DEFAULT_NAME = "default"
end
module Network
module Utils
DEFAULT_TIMEOUT = 10
def self.network_name
"#{Utils::DEFAULT_NAME} network"
end
end
end
> Network::Utils.network_name
uninitialized constant Network::Utils::DEFAULT_NAME (NameError)
What happened?
Inside the method network_name
, Ruby is asked to look up Utils::DEFAULT_NAME
. First, Ruby looks up Utils
and that happens to be the module we're inside. Great! Then Ruby goes to lookup DEFAULT_NAME
inside that scope, which fails, because DEFAULT_NAME
is defined inside a different Utils
module.
You might argue that Ruby should try to look up the entire constant name chain as a whole thing before giving up, but sadly it does not. The way to get to the constant you are looking for, is to refer to it as ::Utils::DEFAULT_NAME
. The leading ::
forces Ruby to start the constant lookup at the top level, rather than the place where constant is referenced. In this case ::Utils
finds the top level Utils
module with the DEFAULT_NAME
defined inside it and all is well.
As to why Ruby has a completely different lookup mechanism for constants I'm not 100% sure, but I'd guess that like a lot of things in Ruby, it's a way to reconcile Ruby's "everything is an object" semantics with the expectations of programmers that namespacing will follow nesting. In this case, the idea that a module is an instance of the class Module
has to co-exist with the possibility of nesting relationship between modules.
Are there any bonkers metaprogramming things you can do with constants?
This is Ruby, what do you think?
There are some ways you can get at constants programmatically. Within a module, const_get
and const_set
both take the constant name as a symbol, as in Math.const_get(:PI)
or Math.const_set(:PI, 3)
. If you can't find a constant, const_source_location
will return the file name and line number.
In the past section, I mentioned that Ruby constant lookup typically ends at Object
, but didn't mention what happens if Ruby doesn't find the constant. Ruby has two different mechanisms here.
If you liked the Roman numeral example from last time, but were saddened that the API was limited to lower case letters, and you really want to do RomanNumeral::XXIII
in honor of the SuperBowl, you can do that:
class Roman
def self.const_missing(const)
Roman.new.to_i(const.to_s)
end
def to_i(string)
# Algorithm redacted
end
end
Roman::XXII # => 22
Yes, there is a const_missing
method in Module
that you can use to capture any constant that is not found and do something with it.
In this case, all we do is convert the constant to a string and translate the string, but you can do way more. The classic Rails autoloader, which was the default until Rails 5.2, used const_missing
to trigger loading a file based on the constant's name and then looking up the constant again after the file was loaded.
It turned out that const_missing
was not quite powerful enough to handle autoloading perfectly, and the replacement autoloader (Zeitwerk), uses a different mechanism, Ruby's autoload
method.
The autoload
method takes a constant and a file name and tells Ruby that whenever the constant is invoked, Ruby should automatically require the associated file. I'm probably over simplifying, or am just wrong, but Zeitwerk basically parses your file system and creates a bunch of autoload
calls based on the file names and their assumed related constants.
It looks like if you define both an autoload
and a const_missing
, Ruby will try the autoload first:
> autoload("Fred", "/fred.rb")
> class Testing
* def self.const_missing(arg)
* p "In const missing with #{arg}"
* end
> end
> Testing::Foo
"In const missing with Foo"
=> "In const missing with Foo"
> Testing::Fred
<internal:/Users/noel/.rbenv/versions/3.2.2/lib/ruby/site_ruby/3.2.0/rubygems/core_ext/kernel_require.rb>:86:in `require': cannot load such file -- /fred.rb (LoadError)
I didn't bother to actually create a file fred.rb
, but when searching for Testing::Fred
, Ruby throws an error that it can't find the file before it gets to const_missing
.
Do I have a hot take?
I have two hot takes about value constants in Ruby.
The one I'm less attached to is the idea that the readability value of a constant can be overrated, especially if the constant represents a short string. I'll often see things like:
class Person
DEFAULT_ROLE = "user"
end
class User
def name
"#{role || Person::DEFAULT_ROLE}_#{count}
end
end
This is simplified to make a point, but I'm not clear that Person::DEFAULT_ROLE
is adding a lot there versus #{role || "user"}
-- I do get that if the string value was longer than the constant is useful shorthand, and I also get that if the constant is used in a lot of places then it's easier to define it once. But on the other side, you sometimes get things like SEVEN = 7
and that can make the code harder to understand. My point is only that "constants are useful in many contexts" does not mean "constants are useful in all contexts".
But my real point is that I think you should be defining all these value constants as methods:
class Person
def self.default_role = "user"
end
class User
def name
"#{role || Person.default_role}_#{count}
end
end
My reasons include:
- With the "endless" method definition syntax, there's no longer much of a syntax penalty for the method definition versus the context.
- Method lookup is more consistent than module lookup, though I grant that if the class is in a namespace you'll still be doing module lookup to get to the class name.
- I find the method lookup to be marginally more readable then the constant lookup -- the
::
always makes me pause, and the all-caps makes the constant name seem more important than it is. - If the constant becomes not so constant, the method body can be changed to have logic without any users of the method changing syntax.
That last one is the real winner for me. One of the consistent banes of my existence in designing software is things that you initially think are 100% always true that turn out in the real world to be, like, 99.9% true. Usually this comes up in database validation, but it's true here, too. My max timeout or default name or error string or whatever --- those things have a tendency to be more complicated than you think they are so it seems like a reasonable defensive move to code them as a method since there's basically no cost to doing so.
What Next?
If you've made it this far, thanks! If you have a Ruby Thing you'd like me to tackle, please let me know by replying to this email or commenting on the post at https://noelrappin.com/blog/2023/10/better-know-a-ruby-thing-22-constants.
Dynamic Ruby is brought to you by Noel Rappin.
Comments and archive at noelrappin.com, or contact me at noelrap@ruby.social on Mastodon or @noelrappin.com on Bluesky.
To support this newsletter, subscribe by following one of these two links: