Noel Rappin Writes Here

Better Know A Ruby Thing #2: Constants

Posted on October 22, 2023


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.



Comments

comments powered by Disqus



Copyright 2024 Noel Rappin

All opinions and thoughts expressed or shared in this article or post are my own and are independent of and should not be attributed to my current employer, Chime Financial, Inc., or its subsidiaries.