I think I expressed myself poorly in my original post. You're correct that the ultimate result of all useful code produces changes in the interpreter's internal state. Even a simple variable assignment creates an object in memory and a mapping from the variable to that object.

I meant to allude to the fact Ruby has a single global namespace that all Ruby code adds bindings to. In Ruby we have to manually namespace stuff just like we add prefixes to functions in C. Python on the other hand creates isolated namespaces for each module which makes it impossible for them to clobber each other. Importing creates explicit links between those isolated namespaces.

> So in this terminology, we can "load" a module and this has absolutely no benefits, except that we can subsequently "import" the same module and importing gets us what we want, the ability to refer to the imported code.

Yes. We load modules, and we import symbols from already loaded modules. In most if not all languages I've learned, importing symbols loads the referenced module implicitly but it most definitely is a distinct operation.

Loading means reading the module's code from somewhere, usually the file system, and placing it into memory, then evaluating the code to obtain the resulting bindings, code like:

module xf
x = 10
f = (y) { y + 10 }
z = x + f(80)

The result of loading a module is just a hash table binding variables to their values, just like Javascript's require.

modules["xf"] = {
name: "xf",
bindings: {
x: 10,
z: 100,
f: {
type: function,
arguments: ["y"],
code: "y + 10"
}
}
}

This is also true of native modules like ELF libraries, they contain the exact same table of symbols, the only difference is this evaluation is done ahead of time by the compiler. This hash table is almost always cached by the implementation so that it does not need to be loaded again.

Importing essentially does this:

xf = modules["xf"]
bindings = xf["bindings"]

x = bindings["x"]
f = bindings["f"]
z = bindings["z"]

Naturally, the modules object must contain a "xf" key pointing to a module object containing the bindings for "x" and "f" and "z". Loading a module puts the module object in the modules cache.

So loading a module without importing their symbols could present benefits. In most languages, they are lazily loaded, work is done only when they are referenced. You might prefer to load everything up front while the program is starting up though. I went through quite the adventure to embed lisp code into an ELF binary in such a way that Linux would automatically map the code in before the program even started executing. Having my interpreter import the symbols from the embedded modules which are already in memory was relatively trivial.

> The entire purpose of loading a module is to add things to the global interpreter state. Why is this an "alternative" to adding things to the global state?

Yes. The interpreter could maintain one namespace and make it so that all code modifies that namespace. It could also maintain a separate namespace for each logical module. In the latter case, changes are local rather than global, even though they're all part of the global state of an interpreter program.

It's analogous to git repositories being local and distributed even though everyone is using the same git implementation to operate on them.