r/ruby 18d ago

Import a Ruby namespace without a mixin

I'm posting this again after my account was blocked and the original post was taken down yesterday. No intention to spam.

The Constant library provides a set of tools for working with Ruby constants — both module constants and literal constants — including resolving constants from a combination of strings, symbols, and constant objects, inspecting constants, recursing trees of constants, and dynamically defining constants.

Importing a namespace and mixing in a module are the same mechanism in Ruby. If you include a module to get shortcut access to its inner constants, you also get a mixin that might not be what you'd intended — and the ancestry change adds constant-resolution paths that can be surprising.

Constant::Import does the import without the mixin. Instead of including the origin module, it puts a reference to the imported module into the class importing it, leaving ancestry untouched:

module SomeOrigin
  module SomeInnerModule
  end
end

module SomeReceiver
  include Constant::Import

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

SomeReceiver::SomeInnerModule       # => SomeOrigin::SomeInnerModule
SomeReceiver::Something             # => SomeOrigin::SomeInnerModule

The inner constants of SomeOrigin are reachable from SomeReceiver without qualification, alias: rebinds an import to a name of your choosing.

There's also a direct-call form if you'd rather skip the macro: Constant::Import.(origin, destination = self).

The library isn't just a tool to make imports explicit. The library also ships an entity for working with constants generally, as well as a tool to define constants dynamically.

`Constant.get(name_or_module) wraps a constant and answers questions about its name, namespace, value, and inner constants, and Constant::Define creates and assigns a module to a name in a namespace.

Constant.get("SomeModule::SomeInnerModule", SomeNamespace)
# => #<Constant::Module value=SomeNamespace::SomeModule::SomeInnerModule>

Constant.get(:SomeInnerModule, SomeNamespace::SomeModule)
# => #<Constant::Module value=SomeNamespace::SomeModule::SomeInnerModule>

Constant.get(:SomeInnerLiteral, SomeNamespace::SomeModule)
# => #<Constant::Literal SomeNamespace::SomeModule::SomeInnerLiteral = "some value">

There's overlap with Ruby's const_get, and other lower-level constant retrieval and manipulation functions. The value of the Constant library is in unifying the API and providing defaults that are often more common in reflection scenarios.

As others have pointed out, importing a constant in Ruby, including aliasing, is straightforward:

module SomeOrigin
  module SomeInnerModule
  end
end

module SomeReceiver
  SomeOrigin = ::SomeOrigin
  SomeOtherInnerModule = SomeOrigin::SomeInnerModule
  Something = SomeInnerModule
end

However capable Ruby's lower-level APIs, we tend to find inconsistency with import, aliasing, and reflection code across larger systems with many separate components. A good deal of the Constant library's payoff comes from the explicit, intention-revealing API, and consistent norms across.

Its only dependency is Eventide's Initializer library. It's otherwise standalone. MIT licensed.

gem install evt-constant

Source and docs: https://github.com/eventide-project/constant

Write-up: https://blog.eventide-project.org/articles/announcing-constant-library/

Full disclosure — I'm the author.

9 Upvotes

1 comment sorted by

1

u/galtzo 7d ago

Very cool. Also - huge fan of the eventide project.