Batiste Bieler
Batiste Bieler

Reputation: 309

Why does Ruby pollute the global namespace with Classes and Functions but not variable?

Having this piece of code in test1.rb

my_var = 42

def my_func()
  42
end

class MyCLS
  attr_accessor :prop
  def initialize()
    @prop = 42
  end
end

Then in the interpreter I require it in irb

> require './test1.rb'
> MyCLS.new().prop
  => 42
> my_func()
  => 42
> my_var
NameError: undefined local variable or method `my_var' for main:Object

I am confused, ruby seems quite happy to pollute the global namespace with classes and functions, but refuse to do the same with my_var? I imagine this is to avoid name collisions and bugs. But the problem is only partially solved as it is still present with Class and Function. Maybe just a little less prone to happen?

So now imagine this second file test2.rb

def my_func()
  43
end

class MyCLS
  attr_accessor :prop
  def initialize()
    @prop = 43
  end
end

And then execute it

> require './test1.rb'
> require './test2.rb'
> MyCLS.new().prop
  => 43
> my_func()
  => 43

Is that normal that the previous globals MyCLS and my_func get silently overwritten? Isn't this highly likely to break a software because a gem decided to add/rename a Class or a function somewhere? All of this seems very brittle and dangerous.

I am aware of modules and I tried them with little success (awkward, and once again they are globals)

Is there ways to prevent this or mitigate what seems like a language design flaw?

Edit: Another example

# test1.rb
def my_func()
  42
end

# test2.rb
puts my_func()

# test3.rb
class Example
  require './test1.rb'
end

class AnotherExample
  require './test2.rb'
end

# command line
$ ruby test3.rb
42

Upvotes: 1

Views: 3594

Answers (2)

Chris Heald
Chris Heald

Reputation: 62698

Constants in Ruby (things which start with an uppercase letter) are always created as constants on Object, unless they are explicitly a member of another constant (ie, module Foo::Bar creates a module under the Foo constant, which is itself under the Object constant).

Additionally, there is a special top-level Object instance named "main". Any methods defined at the top level are defined on Object as private methods, and thus accessible from main.

When you require a file, the mechanism is:

  • Create a new anonymous Module
  • Load the requested file into that module
  • Extend main with that module.

These rules are always obeyed; you can't define a top-level method in a file, then include that file into a namespace by cleverly placing the require statement. The file is parsed, Ruby finds a top-level method and extends main with it, without any consideration for where require is called from.

If you want a method that gets mixed into another class, then you typically would put it into a module, and then mix that module into your class.

# test1.rb
module Bar
  def my_func
    42
  end
end

# test2.rb
require 'test1'
class Foo
  include Bar
end

my_func => # NameError: undefined local variable or method `my_func' for main:Object
Foo.new.my_func # => 42

In Ruby, it is expected that each file will fully namespace the constants and methods that it intends to expose. You will practically never write top-level methods in most real Ruby projects; there's little fear of things being unintentionally overwritten, because someone would need to explicitly step into your namespace to overwrite things.

If you want to execute a file without extending the main object, then you can use Kernel#load with the wrap parameter, which wraps the load in an anonymous module (but makes its internals inaccessible, unless you were do do something in that file to expose the methods and constants in that file):

load "test1", true
MyCLS # => NameError: uninitialized constant MyCLS

You could get this scoped kind of loading via a custom loader:

# test1.rb
def foo
  42
end

# test2.rb
def relative_load(file)
  Module.new.tap {|m| m.module_eval open(file).read }
end

class Foo
  include relative_load("test1.rb")
end

Foo.new.foo  # => 42
foo          # => NameError: undefined local variable or method `foo' for main:Object

As an aside, in your first example, the MyCLS class isn't overwritten; it's merged with the existing MyCLS class. Because both declare initialize, the latter declaration takes precedence. For example:

# test1.rb
class MyCLS
  attr_accessor :prop

  # This definition will get thrown away when we overwrite it from test2.
  # It is test2's responsibility to make sure that behavior is preserved;
  # this can be done with reimplementation, or by saving a copy of this
  # method with `alias` or similar and invoking it.
  def initialize(prop)
    @prop = prop
  end
end

# test2.rb
class MyCLS
  attr_accessor :another_prop

  def initialize(prop, another_prop)
    @prop = prop
    @another_prop = prop
  end
end

# test3.rb
require 'test1'

c = MyCLS.new(1, 2) # => ArgumentError: wrong number of arguments (2 for 1)
c = MyCLS.new(1)
c.prop => 1
c.another_prop =>   # => NoMethodError: undefined method `another_prop'

require 'test2'

c = MyCLS.new(1)    # => ArgumentError: wrong number of arguments (1 for 2)
c = MyCLS.new(1, 2)
c.prop => 1
c.another_prop => 2

Upvotes: 7

Ruby_Pry
Ruby_Pry

Reputation: 237

Since you are using the global namespace, I am going to venture that you are diving into Ruby with a background in JavaScript, correct me if I am wrong.

Ruby is not polluting the global namespace because when you require a file such as test_1.rb or test_2.rb, their scope is then limited to the place where you required them. For instance, if you require 'test_1' in a Class called Example, that has no reach if you were to require 'test_2' in a Class called AnotherExample:

Class Example
  require 'test_1'
end

Class AnotherExample
  require 'test_2'
end

Your methods get overwritten because you required both files within the same scope, which you would not do within the context of a larger application. Namespaces prevent you from overriding similarly named variables, methods, etc.

my_var is a local variable whose context is bound to test_1.rb. As such, its scope is limited to within test_1.rb.

Upvotes: -3

Related Questions