Advanced Tricks with Import

The import function is not a standard part of the MiniScript language, but it is a feature common to many MiniScript environments, including Mini Micro, Farmtronics, command-line MiniScript, and Soda. In this post, we review how import works, and describe some techniques for advanced users.

Basic Usage

An import call looks like this:

import "stringUtil"

This searches several directories, defined by env.importPaths, looking for a file named stringUtil.ms. Finding it, it runs that code in its own call frame, then returns the locals of that call frame (i.e., anything that was assigned to at "file scope" within the module) as a map referenced by the name of the module, in this case stringUtil. So, if stringUtil.ms contains code like

TAB = char(9)

then after doing import "stringUtil", you can refer to stringUtil.TAB in your own code.

How it Works

The last section very briefly mentioned that the imported code is run "in its own call frame." What does that mean, exactly? It means that the entire contents of the imported code is wrapped up in an implicit function, and that function is called.

An example may help. Let's suppose your import module looks like this:

myModule.ms:
a = 42
b = function
  print "Hello from myModule!"
end function

Then your main program imports it like so:

main.ms:
import "myModule"
myModule.b  // prints "Hello from myModule!"

Under the hood, this is basically doing:

main.ms:
_temp = function
  a = 42
  b = function
    print "Hello from myModule!"
  end function
  return locals
end function
myModule = _temp
myModule.b  // prints "Hello from myModule!"

(Note that there isn't actually a variable called _temp; the function is unnamed and invoked via some behind-the-scenes magic. But the effect is otherwise the same.) So where the import statement was, we are actually assigning the result of this implicit function that contains all the module code plus a return locals line. This is how things assigned to in the module, like a and b in this example, end up in a map with the module name.

Assigning Directly to Globals

You can now see how code at the "file" level (i.e. not inside some other function) in the module is one level down from the scope of the import statement; it can read variables in that outer scope, but can't update them without using outer (or globals, in cases where the import was at global scope).

Usually this sort of encapsulation is a very good idea; it keeps the global scope from being cluttered with a lot of identifiers, and reduces the chance that one import module is going to step on another import module's toes.

However, if these are not general-purpose modules but just parts of a big program that you have divided into multiple files, then you may want to have your import modules directly write to the global scope. This is particularly true when what your module does is define some class, and you want the rest of your code to be able to refer to that class without the module prefix. Example:

animUtils.ms:
globals.AnimSprite = new Sprite
AnimSprite.frames = []
AnimSprite.update = function
    // ....
end function

This import module is called animUtils, but what it actually does (possibly among other things) is define a global class called AnimSprite. Now any file that imports this module can use AnimSprite directly, without having to say animUtils.AnimSprite.

Copying To Globals After Import

If you're using a module that doesn't do this, but you really hate having the module prefix for some functions or values, you can just copy them out of the module map after the import. For example, suppose your code works with angles a lot, and you're always converting between degrees and radians. You might want to copy the functions for these out of the mathUtil module, like so:

main.ms:
import "mathUtil"
radToDeg = @mathUtil.radToDeg
degToRad = @mathUtil.degToRad

Don't forget to use @ to reference the function without invoking it!

What if you wanted to copy all the identifiers out of an import module (similar to Python's from module import *)? I generally wouldn't recommend it, but if you really have the need, then you could just do (for example):

main.ms:
import "mathUtil"
for kv in mathUtil; globals[kv.key] = @kv.value; end for

This just iterates over all the key-value (kv) pairs in the module, and stuffs them into globals (again using @ to avoid invoking any functions).

Changing the Return Value

That brings us to the last and most advanced trick in today's post. Recall that the implicit function that wraps the module code always ends with return locals, to return all the identifiers assigned to in that module as a map. Well, that only happens if you don't explictly return yourself! It is quite possible to do that, and so have the result of your import be something other than all the assigned values in the module.

Here's an example. It's a little longer, to illustrate some of the benefits we'll go over below.

Test.ms:
count = 0

Test = {}
Test.testFunc = function
	outer.count = count + 1
	print "This is call " + count + " to Test.testFunc"
end function

if locals == globals then
	print "Running unit tests."
	Test.testFunc
	print "Done with unit tests.  Hooray!"
end if

return Test

The trick here is in the very last line, return Test. You won't see anything like that in most import modules. But because it's in this one, it means that in the calling code, the name of the module (Test in this case) will refer to this Test class, rather than to a map containing Test and count.

main.ms:
import "Test"
Test.testFunc  // prints "This is call 1 to Test.testFunc"
Test.testFunc  // prints "This is call 2 to Test.testFunc"

Why would you do this? There are a few benefits:

  1. Unit tests in the module can use it with the exact same syntax (including the prefix) that users of the module would use.
  2. You could potentially return a completely different class chosen at runtime, perhaps based on what platform you're running on.
  3. You can have truly private module variables, like count in this example.

That last one is probably the greatest benefit. There is no way that code outside this module can see or alter count in this example, which allows you to build more bullet-proof code that is hard to misuse. Such private variables are impossible without this trick, because normally everything assigned at the module scope is returned in the resulting map.

Credit Where Credit is Due

Although I wrote the import function (more than once!), that last trick never occurred to me until Discord user ThaCuber pointed it out. It's brilliant (and now officially documented), and I will probably start using it in my own MiniScript programming. I couldn't be more delighted to be part of a community where I can learn things about MiniScript from other people!

Conclusion

We looked at how MiniScript import works under the hood, and how you can use it in some advanced ways. It's a powerful tool for dividing and organizing your code, and I hope you'll put it to good use in your own projects.