A class is identified by the pair (module, short name). Two modules may each
define Parser, and the importing side decides which one a bare Parser means.
All of it is resolved at compile time. The VM dispatches on class ids and knows nothing about modules, so namespacing costs nothing at run time.
Every file is a module
A file belongs to exactly one module — there is no file that belongs to none.
| File | Module |
|---|---|
declares Module name: 'X' | X |
| a script with no declaration | a module of its own, named after the file |
the preloaded library (lib/Core/Core.st, lib/Collections.st) | Core |
Core holds the builtins and the preloaded library, and is every module's
prelude. Two consequences worth knowing:
- A script's class names cannot collide with a library's. Define
Parserinreport.stand it isreport.Parser, distinct from any otherParser. - The names
Coreexports —Association, theExceptionhierarchy — need no import.
A module file is declarations only. Its top-level statements are not executed, so a module cannot run work as a side effect of being imported.
Resolution order
A bare name resolves against, in order:
- the classes of the current module
- its imports — exported names only
Core, the prelude- the builtins
There is no step 5. A name that is none of these fails to compile, rather
than surviving to a doesNotUnderstand: somewhere far from the cause.
This includes the class an exception handler filters on. on: Errs.MyError do:
with a name that does not resolve is a compile error — it used to compile to a
catch-all, so a typo quietly swallowed every exception:
Module import: 'Errs'.
[ ... ] on: MyError do: [:ex | ... ]. "exported, so the short name works"
[ ... ] on: Errs.MyError do: [:ex | ... ]. "qualified works too"
[ ... ] on: Errs.NoSuchError do: [:ex | ... ].
"compile error: cannot resolve Errs.NoSuchError - module or class not found"
Declaring a module
"lib/Shapes/Shapes.st"
Module name: 'Shapes'.
Module export: #(Circle Square).
Object subclass: #Circle instanceVariableNames: 'r'.
Object subclass: #Square instanceVariableNames: 'side'.
Object subclass: #Helper instanceVariableNames: ''. "not exported: private"
Using it from another file:
Module import: 'Shapes'.
Circle new. "exported, so the short name is bound"
Shapes.Circle new. "the same class, reached qualified"
Shapes.Helper new. "compile error: cannot resolve Shapes.Helper"
Helper new. "compile error: undefined identifier 'Helper'"
export: is enforced. A name left off the list is reachable only inside its own
module — by its short name or qualified, it makes no difference.
Where a module lives
A module is located by its name, and a dot is a directory separator:
| Import | File |
|---|---|
Module import: 'Shapes' | lib/Shapes/Shapes.st |
Module import: 'Maybe' | lib/Maybe.st |
Module import: 'Http.Router' | lib/Http/Router.st |
Module import: './local/Thing.st' | that path |
Which directories are searched, and in what order, is covered under library and plugin search paths.
A qualified name splits at its last segment, so a module name may itself be
dotted: Http.Router.WebApp is the class WebApp of the module Http.Router.
One file, one module
A module is identified by the file it resolves to, canonicalized — not by the
string used to import it. Importing the same file as 'Shapes' and as
'lib/Shapes/Shapes.st' names one module and compiles it once.
Its name comes from its own Module name: declaration, so a module imported by
path is still reachable as Shapes.Circle.
Re-export: the facade pattern
Module export: lists a module's public names, which need not be names it
defines. An exported name is resolved in the exporting module's own scope, so a
module can pass on what it imported. The standard library's HTTP facade is
exactly this:
"lib/Http/Http.st"
Module name: 'Http'.
Module import: 'Http.Server'.
Module import: 'Http.Router'.
Module export: #(Route WebApp HttpRequest HttpResponse HttpServer).
One import binds all five:
Module import: 'Http'.
WebApp new. "bound through the facade"
Http.WebApp fullName. "=> 'Http.Router.WebApp'"
Note what fullName reports: the module that defines the class, not the
facade that re-exported it.
Extending an existing class
Two forms reach a class that already exists — including a builtin — instead of defining a new one:
StringStream >> myMethod "attach a method; no declaration needed"
Template instanceVariableNames: 'pattern' "give it state"
The existing class is what receives the methods, so every user of that class
sees them. This is how a module adds to a builtin, and it is why a builtin name
is never qualified by a module: String is String everywhere.
By contrast, Object subclass: #Parser in a module whose scope already has
Parser from another module is legal and defines a separate class. Shadowing
is a namespace's job.
When two imports export the same name
Importing two modules that both export Thing is fine. Using the bare name
is not — it fails to compile, and the error names the candidates:
Module import: 'ModA'. "exports Thing"
Module import: 'ModB'. "exports Thing too"
ModA.Thing new. "fine — say which one you mean"
ModB.Thing new. "fine"
Thing new.
"compile error: class name 'Thing' is ambiguous - imported modules export it
as ModA.Thing and ModB.Thing. Use the qualified name."
The collision is reported where the bare name is used, not at the import, so adding an import cannot break a file that qualifies what it reaches for.
A name the current module defines is never ambiguous: step 1 of the
resolution order wins outright, so Object subclass: #Thing alongside an
imported Thing shadows it deliberately.
Reflection
| Expression | Result |
|---|---|
Foo name | 'Circle' — the short name. Not unique |
Foo fullName | 'Shapes.Circle'; the plain name for a builtin or a Core class |
System allClasses | the qualified name of every class, exported or not |
System classNamed: | takes a qualified name; a short name works only when exactly one class carries it, otherwise nil |
export: governs name resolution at compile time, not reflection:
System allClasses lists a private class too.
Diagnostics
A method body that fails to compile fails the compile, and all such errors
are reported together. A method that does not compile is not installed, so
letting the build succeed would leave the program one doesNotUnderstand: away
from the real cause — or passing, if nothing happened to call it.
fileIn: is not an import
Smalltalk fileIn: 'helpers.st'.
fileIn: compiles a file into the current module, as if its text were pasted
in place. It is a source-inclusion mechanism, not a module import: the included
classes land in the including file's module, and no export: list applies.