Modules

Programmers can manage the complexity of large OmniMark scripts by encapsulating parts of the script into modules. A module is a self-contained unit of OmniMark code that specifies which names are visible (or exported) to its importers. Likewise, the importer of a module has control over the names it uses for the things it gets from the module.

By convention, OmniMark modules are placed in files whose names end with the .xmd suffix, in the same way that OmniMark programs use .xom and OmniMark include files use .xin.

Modules are different from include file in several respects.

  • Modules provide information hiding. Include files do not. A module specifies what names the importer can use. All other names can only be used internally in the module. This provides encapsulation. All names defined within an include file are visible to the includer.
  • Modules provide for namespace management. An importer may rename things imported from a module to avoid name clashes. This allows different modules defining the same name to be imported at the same time. If two include files define the same name, they cannot be included in the same script.
  • Modules specify how they are treated when they are imported multiple times. The module itself defines whether it uses a separate set of data each time it is imported, or whether there is one copy of the data that is shared among all the imported instances. In general, if an include file defines any names, it cannot be included twice.
  • Modules can be parameterized easily. It is difficult to parameterize include files.
  • Modules can be further separated into interface modules and implementation modules. When interface modules and implementation modules are used, OmniMark enforces the distinction between them. Include files can be broken up into interface declarations and implementation code as well, but it is up to the programmer to manage the distinction.
  • Modules encapsulate rules. When markup processing rules or pattern-matching rules are triggered or resumed, only the rules in the current module are available. This means that rules defined in a module will not interfere with rules defined in other modules or the main program.

Defining a module

Here is a simple module with a function that returns a different (pseudo-random) number each time it's called:

  module
  
  global integer count initial { 1 }
  
  export integer function 
     next ()
  as
     set count to (count * "ABCDEF97" base 16) shift -1
  
     return count
          

A module always starts with the keyword module. module can be preceded only by comments and white-space.

After that, things are more or less as they are for any other OmniMark code. You can declare globals, constants, and functions, not to mention opaque and record types. You can have rules, including process-start and process-end rules.

By default, anyone using (importing) a module doesn't see the names within it: they are hidden. The module can selectively export names to make them visible to the importer. In this example, a single name is exported from the module. This is just to keep the example simple. In practice most modules will export many names, and hide many others.

Using a module

Once you have defined a module, you can use it in OmniMark programs and other modules by importing it:

  import "mymodule.xmd" unprefixed
          

The name after the keyword import is a file name, just as for an include file. The keyword unprefixed specifies that all of the names exported by the module will be used as is.

As an example, if mymodule.xmd is the name of the file containing the pseudo-random number module described above, then the name next will refer to the next function exported from the module.

  import "mymodule.xmd" unprefixed
  
  process
     repeat to 10
        output "10fkd" % next () || "%n"
     again
          

Sometimes you will want to use two modules that define the same name, or a module that defines a name used by your own code. You can specify a prefix when importing a module, to keep the names in different modules separate from each other and from your own names:

  import "mymodule.xmd" prefixed by my.
  
  process
     repeat to 10
        output "10fkd" % my.next () || "%n"
     again
          

In this case, all the names imported from mymodule.xmd will be referenced as if they had my. prefixed to their names. This means that the my.next will refer to the function next defined in the module.

There are no limitations on what you use as a prefix, so long as the result is a valid name. Remember that OmniMark allows -, . and _ in a name, and these are good things to put at the end of prefixes. You should choose prefixes that help make your code clear and readable.