Announcing the Constant Library: Importing Ruby Namespaces Without Mixing Them In

Announcing the Constant library. A new release from the Eventide Project: github.com/eventide-project/constant.

Purpose

In Ruby, importing a namespace and mixing in a module are the same mechanism. include is the only tool for either, so you can’t do one without the other: reach for include to get shortcut access to another module’s inner constants — an import — and you also get a mixin.

A mixin modifies the receiver’s ancestry. When the module being included brings modules of its own, that change adds new paths by which a constant can be resolved — paths that can be surprising and unexpected. They can lead to subtle, frustrating bugs in code that uses reflection, which is common when building general-purpose tools and frameworks.

The quirk can be demonstrated in a few lines:

module SomeModule
  module SomeInnerModule
  end
end

module SomeReceiver
  include SomeModule
end

Object.const_defined?(:SomeModule) # => true

SomeModule was included into SomeReceiver, yet it’s now reachable through Object as well — one of the added lookup paths. The repository’s proof.rb demonstrates the full extent.

Import, without the mixin

Constant::Import performs the import without the mixin. Rather than including the origin module, it creates within the receiver a reference to the imported namespace, so the origin’s inner constants are reachable without qualification — and the receiver’s ancestry is left untouched:

module SomeReceiver
  include Constant::Import

  import SomeOrigin
  import SomeOrigin::SomeInnerModule, alias: :Something
end

The inner constants of SomeOrigin are now reachable from SomeReceiver without qualification, and the optional alias: rebinds the imported namespace to a name of your choosing. The import macro is activated by including Constant::Import. There’s also a direct call form if you’d rather not use the macro: Constant::Import.(origin, destination=self).

A domain object for constants

Import is the headline, but the library also includes tools for working with constants in general, built around a small domain object. A Constant wraps a Ruby constant and answers questions about it — its name, its namespace, its value, whether a name is defined within it, and what inner constants it contains.

constant = Constant.get(SomeNamespace)

constant.name                        # => "SomeNamespace"
constant.constant_names              # => ["SomeInnerModule"]
constant.defined?(:SomeInnerModule)  # => true

constant.constants(include_literal_constants: true)
# => [#<Constant::Module value=SomeNamespace::SomeInnerModule>,
#     #<Constant::Literal SomeNamespace::SomeLiteralConstant = "some value">]

Constant.get takes a module, or a name resolved within a namespace. Constant::Define creates a new module and assigns it to a name within a namespace; it’s the same mechanism Import uses to build an alias target, and it’s useful on its own.

Where it fits

Constant is a foundation utility. It’s the sort of thing that quietly underpins protocol discovery and reflection work in the Eventide ecosystem, but it carries no dependency on the rest of Eventide and is useful anywhere Ruby constants are handled programmatically. If you’ve used include purely for its import side effect, this is a tool built to do exactly that, and nothing more.

Get it

evt-constant is released under the MIT license.

gem install evt-constant

Source and full documentation are at github.com/eventide-project/constant.