Modules
Once you start writing programs that are more than little toys, you quickly run into two problems:
- You want to break them down into multiple smaller files to make it easier to find your way around them.
- You want to reuse pieces of them across different programs.
To address those, Magpie has a simple module system. A file containing Magpie code saved with a .mag
extension defines a module. A module can use the code defined in another module by importing it. You can break big programs into smaller modules that import each other, and you can reuse code by having multiple programs share the use of a single module.
Importing a Module
One module can import another using an import
expression. Like any expression, these can appear anywhere, but the convention is to place them at the top of the file before any other code.
import io.path
Here, we're importing the io.path
module. Importing a module does two things: it runs the module's code, then it binds name in the importing scope.
Qualified Imports
By default, when you import a module you get everything it exports just as it appears in that module. That works most of the time, but sometimes you may want finer-grained control. Maybe you only want to import a few things, or you need to rename something to deal with a name collision.
To handle those, Magpie has a few additional qualifiers you can provide when you import. The first is a prefix:
import io.path as path
When you add as
followed by a name after the module being imported, it prefixes every imported name with that name followed by a dot (.
). So if io.path
defines a baseName
method, we will import it as path.baseName
. This is useful if you want to bring in every name, but make it very clear where they are coming from.
If you want more precise control over specific names, you can add a with
block to the import:
import io.path with ... end
Each line inside that block lets you do a few things with a name. First is renaming:
import io.path with baseName as base end
Using as
lets you provide a new name for a name that you're importing. With the above example, this module would be able able to invoke the baseName
method defined in io.path
by calling base
.
If a line starts with export
it lets you re-export an imported name.
import io.path with export baseName end
By default, imported names are not in turn exported for a module. The idea is that you import things to use yourself, but you only export the behavior you define in your module.
If you do actually want to make something you import part of the exported set of names in your module you can add export
as shown. Here, anyone importing your module will be able to call the baseName
method that's defined in io.path
as if it were defined in your module.
It isn't implemented yet, but at some point you'll also be able to exclude imported names and indicate that you only want to import a select set of names.
Module Loading
When a module is imported for the first time, Magpie needs to find the file that corresponds to that module's name. This is called the lookup process. It works like so:
- Dots in the name are turned into directory separators. So if we're loading
io.path
, we'll turn that intoio/path
. - A
.mag
file extension is added, so now we haveio/path.mag
. - We look for a file at that path relative to the current working directory. If found there, we're done.
- Otherwise, we look for it inside the standard Magpie
lib/
directory which lives where the main executable is.
If Magpie couldn't find a module at either of those paths, an error is thrown.
If it succeeds, Magpie creates a new empty module scope and runs the loaded code. Magpie has no single global scope where names are defined. Instead, each module has its own top level scope. When you define a variable or method in a module, that name is only visible to the module where it's defined.
When that's done, the module is successfully loaded. Note that Magpie only does this the first time a given module is imported. If a module is imported by three other modules, its code will only be run once, and it will only have a single top-level scope in memory.
Once a module has been loaded in response to an import
, it proceeds to the next step, importing names.
Importing Names
When one module imports another, it usually does so because it wants to use some of the methods or variables defined in that module. But, since there is no single global scope, it doesn't have a way to get at them.
To solve that, an import
expression will also define names in the importing module that reference methods and variables defined in the imported module. By default, when you import a module, any methods and variables that that module declares at its top level (i.e. not inside some nested block scope or in a method) will be imported into yours. For example:
// dessert.mag var pie = "apple" def eatPie() print("You eat a delicious " + pie + " pie") end // hungry.mag import dessert print("I imported " + pie) eatPie()
When hungry.mag
imports dessert.mag
it gets a variable named pie
defined in its scope that references the same value that it has in dessert.mag
. Likewise, it gets a top-level eatPie()
defined in its scope that it can then call.
It's important to realize that when you import a variable, you get your own variable declared in your module that points to the same value that the exported variable had when you imported it. If either module assigns a different value to it, the other won't see that change. Consider:
// dessert.mag var pie = "apple" def eatPie() print("You eat a delicious " + pie + " pie") end def changePie() pie = "chocolate" end // hungry.mag import dessert changePie() print(pie) // Still prints "apple" eatPie() // Prints "chocolate"
Here, dessert.mag
is changing the value of pie
after hungry.mag
imports it. We won't see that change reflected in the pie
variable defined in hungry.mag
, but we will see it when it calls eatPie()
since that method looks it up in dessert.mag
where its defined.
Public and Private Names
By default, any variable or method defined at the top level of a module is considered "public" and can be exported into other modules. Sometimes, though, you want to define code that the module can use itself but that isn't visible to the outside world.
To address that, Magpie supports private names. Any name that starts with an underscore will not be exported from a module.
// secret.mag val _hidden = "you can't see me!" // main.mag import secret print(_hidden) // ERROR! _hidden is not defined
This applies to methods, variables, as well as classes and their fields. With a class, you can even make some fields private and others public.
// secret.mag var _nextKey = 12345 defclass Lock val _key = _nextKey = _nextKey + 1 end // main.mag import secret val lock = Lock new() lock _key // ERROR: _key getter isn't defined here
TODO: circular dependencies, _init.mag, relative imports.