CHAPTER 9

Module

Up to this point we have learned how to define types, functions and put some code to set them into motion. With these knowledge, we probably can sit down and solve some real-world problems. But as long as that real-world problem can be solved in under 100 lines of code.

If you plan to do anything serious with Julian, however, you will want to spread your code into files. In the first chapter we covered the two major approaches of Julian programming: loose scripting v.s. structured programming. A real-world product is most likely built on top of so many types that cramming them into a single file becomes unrealistic. But when you spread them out, the next problem arises: how each can see and refer to others? This is accomplished through Julian's module system.

Default module path

In Julian, a module is a self-contained file-system directory. Consider a module of name A.B.C. It's expected that this module is laid out on the file system in this manner:

\Root
    +- \A
        +- \B
            +- \C
                +- file1.jul
                +- file2.jul
                +- \D

Then, if Root is set to be one of the module paths (to explain below), all the .jul files under subfolder C become part of A.B.C. These files must start with a module declaration.

  // file1.jul
  module A.B.C;

The implication of this requirement is twofold. First, the module name must be consistent with the physical file system structure. If the engine loads a file under A/B/C that has a different module name than A.B.C, it's going to throw. Second, the engine doesn't load modules from directory Root automatically. This is to be specified, with the exception of default module path, through a special argument to Julian engine called module paths. If the module path is wrong, then the module file will still be considered illegal due to its mismatching against the search root.

By default, the directory of name jse_modules located alongside the invoked script is used as the sole module path for the current running session. Therefore, if you have

\Path
    +- main.jul
    +- \jse_modules
                  +- \A
                  |   +- \B
                  |       +- \C
                  |           +- file3.jul
                  +- \X
                      +- \Y
                          +- file4.jul

Then invoking main.jul from Java JSE API will cause \Path\jse_modules to be added as a module path, so that main.jul can import module A.B.C or X.Y to use types defined in file3.jul and file4.jul, respectively. Similarly, file3.jul can import X.Y, and file4.jul A.B.C.

For a module file that is placed under the default module path, the module author may use the simplified form of module file declaration, which contains a single statement of module;

  // file3.jul
  module;

Customized module paths

Sometimes you may want to use modules defined somewhere else. Let's use the example from earlier again:

\Root
    +- \A
        +- \B
            +- \C
                +- file1.jul
                +- file2.jul
                +- \D

To add a module path in interactive console, use -mp option:

bin> .\jse.sh -mp \Root -f main.jul

The above correctly specifies the module path as the search will start from underneath \Root. So the engine will see a module A, a module A.B and a module A.B.C. If we change the module path,

bin> .\jse.sh -mp \Root\A -f main.jul

It will cause errors as the engine will now see a module B and a module B.C, which is inconsistent with the module declaration seen in file1.jul.

Th -mp option can be specified more than once to provide multiple module paths for the engine to search. The searching will end once the module is found, and the order in which this search happens is undefined. Therefore you should not try to place a module of the same name in different module path. Also, when -mp is used, the default module path, jse_modules, is replaced. You must explicitly specify it using another -mp option if you want to keep using it as a module path.

\Root
|   +- \A
|       +- \B
|           +- \C
|               +- file.jul (defines class Item)
\Lib
   +- \Root2
           +- \X
               +- \Y
                   +- file.jul (defines class Entity)

To use class A.B.C.Item and X.Y.Entity, one would call with:

bin> .\jse.sh -mp \Root -mp \Lib\Root2 -f main.jul

To add module paths in Java script engine, use an engine argument.

  // (Java)
  @SuppressWarnings("rawtypes")
  List list = new ArrayList(); 
  for(String path : paths){
    list.add(path);
  }

  ScriptContext context = new SimpleScriptContext();
  context.setAttribute(
    "JSE_MODULE_PATHS",
    list, 
    ScriptContext.ENGINE_SCOPE);				

The attribute JSE_MODULE_PATHS can take either a List for a bunch of paths, or a string for a single path. In the same way as the command line argument -mp, this attribute would replace the default module path, jse_modules.

While supported, we wouldn't recommend using additional module paths. First, such practice introduces unpredictability, as we have explained on how the search would terminate abruptly a path matching the module name is found. Second, managing multiple module paths, potentially located outside the folder where the main script resides, could bring a lot of headache to code maintenance, and is certainly detrimental to portability. In future, Julian may introduce package system, which would use the default path as the place to store the downloaded packages, in a similar arrangement as npm for Node.js. Third, as we have seen earlier, a module file appearing under the default module path can have simplified module statement (module;). This makes it easier and less error-prone for application authors to develop and deploy new modules in the test or prototyping environment.

Import modules

To use another module, simply use import statement after module statement.

  import A.B.C;

The imported modules will form a namespace set that is used by the engine to resolve types encountered in the same script. For example, assume A.B.C contains a type MyClass, then when we refer to MyClass in our module-referencing script file we don't have to say A.B.C.MyClass.

  import A.B.C;
  import D.E.F;
  
  MyClass mc = new MyClass();

Upon seeing type name MyClass, the engine will try to resolve it against the current namespace set, which contains "A.B.C" and "D.E.F". It will go through the module paths and scan all the files under A/B/C and D/E/F, until it finds a type of name MyClass. Note this search is nondeterministic. If you have a type of name MyClass defined in both modules it's not guaranteed which one will be returned. This is true even if one of them doesn't have a nullary constructor at all. Unlike compiled languages, Julian intentionally doesn't disambiguate between types of same non-qualified name. To avoid this, you may use fully-qualified name instead.

  import A.B.C;
  import D.E.F;
  
  MyClass mc = new D.E.F.MyClass();

A namespace that will be always added to the current namespace set is "System". Currently, this namespace is treated equally as others. But in future it may be consulted at last to allow users to override any system type names.

When you load a module, the module will remain loaded for the rest of the engine's lifetime. This raises a subtle scenario where you may find that even without importing a module you can use its types by specifying the fully-qualified name. Let's look a contrived example here:

  // File 1: main.jul
  import A.B.C;
  import X.Y.Z;
  
  Factory.make();
  
  // File 2: A/B/C/types.jul
  class Item { }
  
  // File 3: X/Y/Z/script.jul
  class Factory {
   static var make() {
     return new A.B.C.Item(); // OK!
   }
  }

The main script imports module A.B.C and X.Y.Z in a row, then invokes X.Y.Z.Factory's make() method. Note that script.jul in module X.Y.Z doesn't even import module A.B.C, but is able to reference to a type there by using the full name.

This phenomenon is more common for certain built-in types whose modules got loaded into the engine during bootstrapping, such as those from module System.Collection. In below, note how we didn't import module System.Collection at the top of this file, yet the script engine somehow located the type.

  var list = new System.Collection.List(); // OK!
  list.add("this is fine.");

The reason we share with you about this episode is that we want you to not rely on it. This behavior is not very deterministic because you would have to rely on a separate, possibly unrelated type, to import some module for you. It's for your best interest to always import all the modules yourself in the script file where a certain type from those modules.

Default module

The classes and other types defined in the entrance script, i.e. the module-less script to be invoked by the engine, will appear under a special module name <default>. Evidently this is not legal identifier, so the programmer will not be able to either import this module, nor refer to its full name in the source code. The goal of this design is to discourage the developers from mixing loose scripting with structured code.

In a last reminder, default module and default module path are two different concepts. To recap them together, the file you invoke is in the default module, and that file will be able to import modules from the default module path, which is jse_modules directory lying alongside.