Modules can be implemented in any programming language, if the programmer is disciplined enough[1]. However, Modula-2 originated modules as language constructs, not only for grouping & namespacing, but also as compilation units.

Very important to repeat that namespacing bit; Modula-2 modules allowed for a way to group & scope names. Other block structured languages allowed for name shadowing within a block, but with modules, one has access to shadowed names in enclosing blocks.

At the implementation level, you can think of per-Module block structured languages as having a stack of hash-tables for environment/symbol-table. As each name is declared, a fresh entry is created in the hash-table and the initialization value for the variable stored as value for the key. When processing enters a new scope, say BLOCK, BEGIN, LET, new function declaration or similar name hiding construct, a fresh hash-table is created and pushed. When the block is exited, the stack is popped and we return to previous definitions.

Except the environment stack is actually implemented as a list, to allow non-shadowed names to be available without popping. The compiler can walk up and down the stack list to look up identifiers, often assigning an stack-depth number to each nested environment. Say, a top-level global variable might actually be internally represented as {env: 0, name: x, val:3.0, type: float}.

Programmers don't have access to that numeric environment ID. Once a variable is shadowed, we lose all access to it, if we don't keep a copy, and even that is useless with side-effects.

Modules change this in one important way. The hash-tables have names! They are not just anonymous values to be pushed around (eh? ;-) but named entities that we can look up. Why settle for environment IDs when you have glorious, descriptive, human readable names?

If you substitute a graph for the environment stack, you get yourself a more interesting structure. One that allows for module composition and structure sharing, so that two or more environments can have their common bits factored out.

To allow for separate compilation of modules, we need to know in advance what services they offer (i.e. what keys are in their environment table.) If we can find that out quickly, without processing the module itself, we can move along faster. This is very important. A common pattern in language compilation is name resolution. Some languages force programmers to declare all names before use. Other languages are more forgiving, and try to resolve the names themselves, often by processing input code in multiple passes, and only then signaling errors for yet still unbound names. For modules, we can help the compiler discover names by abstracting out the keys ahead of time. So break the module definition into signature declaration, and actual module body known as structure. The signature is a compact, high-level view of the map that tells code processors and other modules what names they can expect from the module. (A primitive form of signature/structure separation is C & C++'s header files, but those have nothing to offer us, intellectually.)

Like any form of cooperation, contractual agreements between modules will have to be in place. We need a certificate of authenticity of sorts. The addition of types to module definition is a pillar of modern software engineering.

Modules make software serious.

For the full story on modules, see Standard ML.

--

[1] David L.Parnas(1972). On the criteria to be used in decomposing systems into modules.