Modules are units of compilation. They are single files of Monte source code which can be compiled on a per-file basis. Modules are relatively self-contained, declaring both their imported and exported names with special module syntax.
Some languages don’t have modules. Instead, they have inclusion, where source code is literally or semantically transposed from one source file into another. Our primary goal in providing modules is encapsulation, allowing modules to keep some of their design and layout private.
Some other languages have modules that alter global state. These languages typically evaluate when importing, applying each module’s code to the global state. Our module system abstracts this behavior, parameterizing inputs to modules and allowing for isolated modules that can be evaluated multiple times without side effects.
Module Declaration Syntax¶
Module files start with a module header, which is a declaration of the form:
import "namespace/name" =~ [=> first, => second] exports (maker, main)
with zero or more
import lines and exactly one
import line declares that the module depends on a named
dependency, which is known inside the module by its pet name. In
this example, the pet name is “namespace/name”. The dependency is matched
against the pattern on the right-hand side of the
=~ operator, called the
import pattern, and the resulting names are available for use
throughout the body of the module.
By convention, pet names have two pieces: The module namespace and the module’s name.
When new packaging efforts are ready, update this to mention that module namespaces are either the stdlib or a package name.
As a convenience, if the import pattern is a map-pattern, then an automatic ignore-pattern tail will be attached by the expander. This makes forward compatibility easier, as unknown names in imported modules will not throw exceptions.
exports line follows the import declarations. This line declares a
list of nouns which will be exported from the module’s scope. Exported names
will be available to other modules which import this module.
All exports must pass
exports (f) def f() as DeepFrozen: return 42
Which means that exports can only depend on
import "unittest" =~ [=> unittest :Any] # not DeepFrozen! exports (f) def f() as DeepFrozen: # Exception: `unittest` in the scope of `f` isn't DeepFrozen! return unittest
Each import pattern, by convention, should be a named parameter mapping a
Str key to a noun. This mirrors exported names, so that a name exported
from one module can be imported by another easily.
Imports can have guards on them:
import "fries/victor" =~ [=> diamonds :DeepFrozen] exports (freezeRay, oneLiners)
In fact, by default, imported names are automatically guarded with
DeepFrozen. This allows those imported names to be used in exported
Module Syntax Expansion¶
Under the hood, modules are compiled to be singleton objects which accept a mapping of imported objects, and return a mapping of exported names.
The export name “main”, when present, denotes the entrypoint of
the module. The entrypoint should take named parameters corresponding
to unsafe capabilities from the unsafe scope, and return an
a promise for an
exports (main) def main(_argv, => currentProcess) :Int as DeepFrozen: traceln(`Current process: $currentProcess`) return 0
Unit Testing and Benchmarking¶
The package loader provides a few Miranda import pet names to all modules.
A unit test collector. It is not
DeepFrozen, so unit tests are confined to their module:
import "unittest" =~ [=> unittest :Any]
A benchmark collector. It is not
import "bench" =~ [=> bench :Any]