4. Modules

Modules provide a means of avoiding name collisions between nodes. A module is a namespace which contains all global nodes, including meta-nodes, created in it. A node with identifier x in a module m1 is distinct from a node with the same identifier x in another module m2.

4.1. Creating Modules

Each new node, that is created as a result of processing a declaration in the source file, is added to the current module. Initially the current module is a nameless init module until it is changed explicitly.

The current module can be changed using the special /module operator, which takes the identifier of the module as its only argument. If there is no such module a new module is created.

Example. 

# Change to module with identifier `mod1`
/module(mod1)

# Nodes `a` and `b` added to `mod1`
a -> b

# Change to module with identifier `mod2`
/module(mod2)

# Nodes `a` and `b` added to `mod2`
# Distinct nodes from nodes `a` and `b` in `mod1`
a -> b

[Note]

Module identifiers reside in a different namespace from node identifiers, thus there is no risk of collision between a node and module with the same identifier, unless a pseudo-node for the module is added to the module containing the node.

[Note]

Modules reside in a single global, flat namespace. Hierarchical relations between modules have to be faked with a separator such as /, e.g. module/submodule.

4.2. Referencing Nodes in Different Modules

There are two ways to reference a node in a another module, different from the current module. One way is to create a pseudo-node for the module in the current module. Nodes in the module can then be referenced as subnodes of the module’s pseudo-node.

Module Pseudo-Nodes

The special /use operator creates pseudo-nodes for the modules passed as arguments. The pseudo-nodes are created with the same identifiers as the modules.

[Note]

Module pseudo-nodes are referred to as such, since syntactically they are the same as any other node, however the value of a module pseudo-node cannot be referenced nor can bindings involving it be established.

Syntax. 

/use(mod1, mod2, ...)

[Caution]

Creating a pseudo node in a module, which contains a node with same identifier as the pseudo-node, results in a compilation error.

Nodes from the used modules can then be referenced as subnodes of the module pseudo nodes.

Example. 

/module(mod1)

a -> b

/module(mod2)
/use(mod1)

# Reference node `b` from module `mod1`
mod1.b -> b
x -> mod1.b

Meta-nodes from a different module can be also referenced as subnodes of the module pseudo-node.

Example. 

/module(mod1)

add(x, y) : x + y

/module(mod2)
/use(mod1)

# Use the `add` meta-node from module `mod1`
mod1.add(a, b) -> c

[Tip]

Nodes referenced from other modules, can appear both as dependencies or observers of bindings.

[Important]

Referencing a subnode of a module pseudo-node does not result in the automatic creation of a node in that module. Referencing a node that does not exist in the module results in a compilation error.

A pseudo-node with a different identifier, from the identifier of the module, can be created using the special /use-as operator. This is useful for when the module identifier is too long to type out repeatedly, or there is already a node, in the current module, with the same identifier.

the /use-as operator takes two arguments, the identifier of the module and the name of the pseudo-node to create in the current module:

/use-as(module-name, pseudo-node-name)

The above examples can be rewritten using /use-as declarations:

Example. 

/module(mod1)

a -> b

/module(mod2)
/use-as(mod1, m1)

# Reference node `b` from module `mod1`
m1.b -> b
x -> m1.b

Example. 

/module(mod1)

add(x, y) : x + y

/module(mod2)
/use-as(mod1, m1)

# Use the `add` meta-node from module `mod1`
m1.add(a, b) -> c

Importing Nodes

The second approach to referencing a node, residing in another module, is to add it directly to the current module. With this approach there is no need to reference the node as a subnode of a module pseudo-node. This is referred to as importing the node and is achieved using the /import operator.

The /import operator adds the identifiers of nodes, residing in another module, to the current module. The result is that the node can be referenced directly by its identifier, as though it were declared in the current module.

The /import operator has two forms:

  • A short form that imports all the nodes exported from another module. Takes the module identifier as its only argument.
  • A long form that can be used to import specific nodes. The first argument is the module identifier, the following arguments are the identifiers of the nodes to import

Syntax. 

# Short form: Import all nodes exported from `module`
/import(module)

# Long form: Import only the nodes listed in the arguments after the
# module identifier.
/import(module, node1, node2, ...)

Example: Long form. 

/module(mod1)

a -> b

/module(mod2)

# Import node `b` from `mod1`
/import(mod1, b)

# Node `b` is the same `b` as in `mod1`
b -> a
x -> b

The short form only imports those nodes which are explicitly exported from the module. Nodes are explicitly exported from the current module with the /export operator which simply takes the identifiers of the nodes to export as arguments.

Syntax. 

/export(node1, node2, ...)

Each /export declaration adds (does not replace) nodes, that are in the arguments, to the exported nodes of the current module.

Example: /export and short form /import

/module(mod1)

a -> b

# Export node `b`
/export(b)

/module(mod2)
# Import all nodes exported from `mod1`
/import(mod1)

# Node `b` is the same `b` as in `mod1`
b -> a
x -> b

Meta-node Example: /export and short form /import

/module(mod1)

add(x, y) : x + y

/export(add)

/module(mod2)
/import(mod1)

# Use the `add` meta-node from module `mod1`
add(a, b) -> c

A side effect of /import is that if the identifier of an imported node, whether imported by the long or short form of /import, is registered as an infix operator in the module, from which it is being imported, it’s entry in the module’s operator table is copied over to the current module. This allows the operator to be placed in infix position, in the current module.

[Note]

All nodes in the builtin module, which contains the special operators mentioned so far (->, :, .., &, .), are automatically imported into the init module.

Direct References

It may be necessary to reference a node in another module without creating a pseudo-node, for its module, and without importing it in the current module. The special /in operator directly references a node in another module by the module and node identifiers.

Syntax. 

/in(module-id, node-id)

The first argument is the identifier of the module containing the node, and the second argument is the identifier of the node.

[Important]

module-id is the identifier of the module itself, and not the identifier of a module pseudo-node.

Example: /in Operator. 

/module(mod1)

a -> b

/module(mod2)

# Reference node `b` in `mod1` directly
/in(mod1, b) -> a
x -> /in(mod1, b)

Example: /in Operator. 

/module(mod1)

add(x, y) : x + y

/module(mod2)

# Use the `add` meta-node from module `mod1`
(/in(mod1, add))(a, b) -> c

4.3. Operator Table

Each module may register a number of node identifiers as infix operators. This means that those identifiers may appear in infix position in declarations parsed while the module is the current module. The module’s operator table stores the identifier, precedence and associativity of each infix operator. See Section 1.2, “Functors” for more information about infix operators, operator precedence and associativity.

Initially the operator table of each module contains a single entry which is the special entry for function application. The precedence of function application controls whether an expression is treated as the operator of a functor or the operand of an infix expression.

[Note]

The precedence of function application is set to 900. This value cannot be changed.

Example: Precedence of Function Application. 

# If `.` has a higher precedence than function application, the
# following is parsed to: ((. m1 add) a b)

# If `.` has a lower precedence than function application, the
# following is parsed to: (. m1 (add a b))

m1.add(a, b)

Registering Infix Operators

New infix operators can be registered using the special /operator declaration. This declaration modifies the operator table of the current module.

Syntax. 

/operator(identifier, precedence [, (left | right)])

The first argument is the node identifier to register as an infix operator. The second argument is the precedence as an integer value. The third argument specifies the associativity. This is either the identifier left or right for left or right associativity. If the third argument is omitted, left associativity is assumed.

[Note]

The identifier does not have to name a node or meta-node that exists at the time the /operator declaration is processed. The table only stores the identifier for syntactic transformations, no information about the actual node is stored.

An /operator declaration adds an entry to the current module’s operator table if it does not already contain an entry for the identifier. If the table already contains an entry for the identifier, the precedence and associativity values of the existing entry are replaced with those given in the arguments to the /operator declaration.

[Note]

The precedence and associativity of all operators can be changed, with the exception of function application.

When a node is imported into the current module and there is an entry, for the node’s identifier, in the operator table of the module, from which the node is being imported, the entry is copied over into the current module’s operator table, replacing any existing entries for the identifier.

Example: + infix operator from core module. 

# Register `+` as infix operator
/operator(+, 100, left)

# Now `+` can appear in infix position
a + b

# It can also still appear in prefix position
+(a, b)