Later constant definitions

210 views

What's the return value of the following Ruby 3.2 code?

require 'net/http'

[Net::HTTP, Net::HTTP.class] # => [Net::HTTP, Class]

include Net

[HTTP, HTTP.class] # => [Net::HTTP, Class]

module HTTP
end

[HTTP, HTTP.class] # => ???

The correct answer is

It raise TypeError

[Net::HTTP, Class]

[HTTP, Module]

[HTTP, Class]

Unlock Your Ruby Potential

Subscribe to RubyCademy and get free access to all our courses, plus hundreds of fun Ruby cards, quizzes, guides, and tutorials!

Explanation

In Ruby 3.2, there was a significant change in how the language handles module reopening and redefinition.

If you’ve ever defined a module or class with a name already used in an included module, you might have encountered confusing errors.

In this post, we’ll dive deep into this new behavior, how it differs from earlier versions, and why it was introduced.

The Issue in Ruby < 3.2

Before Ruby 3.2, if a module or class was included, and you tried to define a new module or class with the same name, Ruby would assume you were reopening the existing class or module.

This was especially problematic when working with external libraries. Let’s look at a concrete example using Ruby’s net/http library.

require 'net/http'

include Net

# HTTP refers to Net::HTTP because Net is included
p HTTP  # => Net::HTTP

# Let's try to define a new module called HTTP
module HTTP
end

# Ruby 3.1 raises a TypeError here:
# => TypeError (HTTP is not a module)

In Ruby 3.1, we get a TypeError because Net::HTTP is a class, and Ruby tries to reopen it as a module.

Since classes and modules are different entities, this results in an error. Ruby assumed we were trying to modify Net::HTTP instead of creating a new HTTP module.

Ruby 3.2: A Safer Approach

In Ruby 3.2, this behavior was changed to avoid these cryptic errors. When you define a new module or class with a name that’s already been included from another module (like HTTP in the example above), Ruby 3.2 creates a new module rather than trying to reopen the existing class or module. This makes it easier to safely define new constants in your code without worrying about accidental conflicts.

Here’s an example showing how Ruby 3.2 behaves:

require 'net/http'

[Net::HTTP, Net::HTTP.class] # => [Net::HTTP, Class]

include Net

[HTTP, HTTP.class] # => [Net::HTTP, Class]

module HTTP
end

[HTTP, HTTP.class] # => [HTTP, Module]

Step-by-Step Explanation

  1. Loading the net/http Library:
require 'net/http'

  • This makes the Net::HTTP class available. At this point, Net::HTTP is a class defined in the Net module.
  1. Including the Net Module:
include Net

  • The include statement brings all constants from the Net module into the current namespace, making HTTP refer to Net::HTTP.
  1. Redefining HTTP in Ruby 3.2:
module HTTP
end

  • In Ruby 3.2, this defines a completely new HTTP module, separate from Net::HTTP. The previous behavior of trying to reopen Net::HTTP no longer occurs.
  1. Return Values:
  • Initially, [Net::HTTP, Net::HTTP.class] returns [Net::HTTP, Class] because Net::HTTP is a class.
  • After including Net, [HTTP, HTTP.class] also returns [Net::HTTP, Class], since HTTP refers to Net::HTTP.
  • After defining the new HTTP module, the last expression [HTTP, HTTP.class] returns [HTTP, Module] because Ruby 3.2 now treats HTTP as a new, unrelated module.

Why Was This Change Introduced?

The reasoning behind this change is that Ruby’s previous behavior could lead to hard-to-debug issues.

When including a module that brings in external constants (like Net::HTTP), it wasn’t always obvious whether a new module or class definition was reopening an existing constant or creating a new one.

This could lead to cryptic errors like the TypeError seen in the Ruby 3.1 example.

By changing this behavior in Ruby 3.2, developers can define new classes and modules without worrying about inadvertently modifying included constants.

This ensures safer and more predictable code, especially when working with third-party libraries.

Final Thoughts

The changes in Ruby 3.2 make it much easier to define new constants without worrying about conflicts with included modules.

No longer will you encounter cryptic TypeError messages when you accidentally redefine a constant from an included module.

Instead, Ruby 3.2 will simply create a new, unrelated module or class, making your code safer and more modular.

This is a welcome improvement, especially for large applications that include many third-party libraries.

By avoiding unintended reopening of classes or modules, Ruby 3.2 helps you avoid unexpected errors and keeps your code more predictable.

Unlock Your Ruby Potential

Subscribe to RubyCademy and get free access to all our courses, plus hundreds of fun Ruby cards, quizzes, guides, and tutorials!

RubyCademy ©