Modules

As your programs or scripts get larger, you'll reach a point where it becomes simply too cumbersome to keep all your code in a single file. In order to split up your code, you use modules, a concept borrowed from many other languages as a means of large-scale program organization.

A MiniD module roughly corresponds to a source file, although due to its dynamic and embedded nature, modules can be generated in different ways. A module is just a namespace that contains objects and which sits in a hierarchy of namespaces rooted at the global namespace (_G). How modules are loaded into this hierarchy is defined by the modules standard library, though it defines a default behavior which should be pretty familiar if you're used to other languages' module systems.

The Namespace Hierarchy

As mentioned in the introduction, there is a hierarchy of namespaces rooted at _G which is used to represent modules. Here is a sample hierarchy:

Each node in this hierarchy is a namespace object. You'll notice _G at the top. It contains math, io, and game. game in turn contains game.input, game.drawing, and game.npcs. game.npcs further contains game.npcs.orc and game.npcs.troll.

MiniD doesn't make any distinction between modules and "packages" as D does. You are allowed to have both a module and a package of the same name. For instance, game.npcs might have some functions and classes in it as well as containing the sub-modules game.npcs.orc and game.npcs.troll.

In the namespace hierarchy, each namespace has a single parent namespace. The chain of parent namespaces is used in global lookup, which will be explained a little further down.

Making a Module

So the first thing you have to do when dealing with modules is to make one. You've probably been doing this all along, if you've been saving your code to MiniD source files.

To make a module, you just create a MiniD source file and place a module declaration at the top. Every source file must start with a module declaration.

module foo

You'd save this as foo.md somewhere where the interpreter or your host app would be able to see it. By default this is the same directory as the app which will load it. As an example, we'll use MDCL. Save that file to the same directory as MDCL. Congratulations, you now have a module! That wasn't terribly hard.

Now a module with nothing in it isn't very useful. So let's discuss how you add members to a module, as well as the concept of 'local' versus 'global' module members.

By default, when you declare a function, class, or namespace in a module, it is created as a global. That is, writing "class A {}" at module scope is the same as writing "global class A {}". When you declare any global symbol in a module (including normal global variables), they will be visible from outside the module. That is, they are inserted as key-value pairs in the module's namespace. But if you declare something at module scope as a local, it will only be visible from within the module. In this way, 'global' and 'local' at module scope behave something like 'public' and 'private' in other languages.

So knowing that, we can add some members to our module by declaring stuff at module scope:

module foo

// private to this module
local x = 0

// public to other modules
function getX() = x

function setX(val: int)
	x = val

There. Now we have x, which can only be used from within foo, and two functions getX and setX, which can be used from other modules. Now that we have a module, how do we use it?

Importing Modules and Symbols

In order to actually access the symbols within a module, you must first import it. This causes the interpreter to look for a source file of the given name, which it then compiles and runs. After that, the module has been loaded into the namespace hierarchy and can be used.

You import modules with the 'import' statement. After that, you access the symbols in the module as fields from it. Here's an example use from within MDCL:

$ ./mdcl
MiniD Command-Line interpreter 2.0 beta
Use the "exit()" function to end.
>>> import foo // import the module
>>> foo // let's see what members it has
 => namespace foo { getX, setX }
>>> foo.getX() // call a function
 => 0
>>> foo.setX(5)
>>> foo.getX()
 => 5
>>> foo.x // this won't work!
Error: stdin(1): Attempting to access nonexistent field 'x' from 'namespace foo'
Traceback: stdin(1)

>>>

Note that if you import a module at least once into your interpreter, any subsequent imports of it will immediately return successfully.

The import statement can be used just like a normal statement, i.e. you can put it in functions, conditionally execute it, etc. It also has some more interesting forms to do more than just a simple import.

Suppose you wanted to import a different module if one module isn't available.

try
	import foo
catch(e)
	import bar

In this case, if the import of foo failed for any reason, it'll just try importing bar instead.

What if you had a ton of modules you wanted to import all at once? Even worse, what if you didn't know in advance what their names were? This might happen if you have a plugins directory, for instance. No problem; the import statement allows you to use the form "import(name)", where name is any arbitrary expression that evaluates to a string.

foreach(file; io.listFiles("plugins", "*.md"))
	import("plugins." ~ io.name(file)[.. -3]) // chop off extension

You could of course add a try-catch on each import to see if the module loaded correctly.

Often when you import a module, typing the fully-qualified names of its symbols can become cumbersome, especially if the module name is deeply-nested. It can even become a performance concern, if a long name like "game.npcs.orc.Orc.getImage()" is accessed in a tight loop or something. In these situations, you can use import renaming and selective imports to make your life easier.

Import renaming is the act of declaring a local which is simply an alias for the longer module name. Selective imports are just locals which are aliases for members of the imported module. You can rename selective imports too.

A very common use of selective importing will probably be importing functions from the math library, since they've got pretty distinctive names and you'd probably like them to perform well. For example:

import math: sin, cos // selectively import sin and cos as locals

local x = sin(math.pi / 2)
local y = math.cos(math.pi / 2) // this is still fine

A selective import is really just a local variable declaration, and has the same restrictions (i.e. no shadowing allowed). The above import is the same as writing:

import math
local sin = math.sin
local cos = math.cos

Since the selectively-imported symbols are locals, they of course will not be visible from other modules.

Sometimes you don't mind writing qualified names, but the name of the module is just.too.freaking.long. In that case, you can use a renamed import to create a local alias to the module:

import short = some.long.module_.name
short.foo() // same as some.long.module_.name.foo()

Like selective imports, renamed imports are just a local variable declaration:

import some.long.module_.name
local short = some.long.module_.name

One last thing is a renamed selective import, which you might not use that much, but hey, it's there:

import math: sine = sin
local x = sine(0) // same as math.sin(0)

You cannot import all the symbols of another module into the current module. The number of local variables in a function cannot be modified at runtime, so it's just not possible. You could just do a brute force member copy from the other module's namespace to this module's namespace, but that would make all those symbols accessible from other modules, and might cause name clashes. In any case, importing all of another module's symbols has been determined a Bad Thing in languages that allow it, so you're not really missing that much.

Global Lookup

Something that should be mentioned along with imports and modules is how global lookup is performed and how it fits in with modules. This is where the parent namespaces come into play.

When you use a variable name in your MiniD program, the compiler first looks for a local variable declared in the current function or in any enclosing functions. If it finds one, it uses that. If it doesn't find any local variables by that name, it assumes it must be a global variable and emits a reference to it.

When your program runs and a global variable access is encountered, the following happens:

  1. It looks in the function's environment namespace (usually the module in which it was declared) for a slot with that name. If it finds it, it stops here.
  2. It follows the environment namespace's chain of parent namespaces up and up until it reaches the root namespace (the namespace with no parent). It looks for a slot with that name in that namespace. If it doesn't find it, you get an error telling you that you're trying to access an undefined global.

So for instance, let's say that we're in the game.npcs.orc module as demonstrated in the diagram at the beginning of this section. We attempt to access a global named "foo". First it looks in the current module for "foo", that is, it sees if game.npcs.orc.foo is defined. If it isn't, it then follows the chain of parent namespaces up until it can't go any further - to game.npcs, then game, and finally to _G. It looks in that root namespace for "foo", that is, it sees if _G.foo is defined. If it isn't, we get an error.

You'll notice that it skips the intermediate namespaces when looking up the variables. So for instance, it's not possible to access something like npcs.troll from game.npcs.orc, since npcs is defined neither in game.npcs.orc nor in _G.

If you create custom namespaces, you need to keep this global lookup mechanism in mind. By creating a namespace with a null parent, you can cause global lookup to halt when it reaches that namespace. In this way you can create sandboxes and such.

The modules Library and Custom Module Loaders

These are some more advanced features of the module system. I won't talk about them here, but there's an explanation of them in the advanced topics section.

The End

The next page is on coroutines.