12. Modules And Organization

Up till this point we haven’t given much thought as to how our source code is organized. Instead we’ve crammed all our code in a Tridash code tag inside a single ui.html file. This strategy obviously wont scale for larger applications. Ideally we’d like to have separation of concerns with our application divided into multiple source files, each containing the code implementing a separate component of the application. The ui.html should ideally contain a minimal amount of Tridash code, limited only to inline node references and bindings directly related to the presentation logic. The core application logic should be in a separate source file.

In this tutorial we’ll take the Adding Numbers application, as it is at the end of Section 9, “Contexts”, and reorganize its source code.

12.1. Multiple Source Files

The simplest form of code organization is splitting up the application into multiple source files. Source files containing only Tridash code are given the extension trd.

Let’s extract the validation logic in a separate validation.trd file. This includes the meta-nodes validate, valid-int and the Negative-Number failure type.

validation.trd

/import(core)

Negative-Number  <- &(Negative-Number)
Negative-Number! <- fail(Negative-Number)

validate(x) :
    case(
        x >= 0 : x,
        Negative-Number!
    )

valid-int(value) : {
    x <- int(value)
    validate(x)
}

/attribute(valid-int, target-node, valid-int)

[Note]

We’ve kept the /import(core) declaration at the top of the file.

The application logic simply consists of the definition of the error-message meta-node, which is responsible for choosing an appropriate error message, the computation of the sum, and the setting of the initial values for a and b.

Let’s place this in a separate source file called app.trd

app.trd

/import(core)

error-message(value) : {
    value !- "" ->
        @(self)

    "Not a valid number!" ->
        self @ when(Invalid-Integer)

    "Number must be greater than or equal to 0!" ->
        self @ when(Negative-Number)
}

sum <- a + b

0 -> a
0 -> b

[Note]

We’ve kept the /import(core) declaration at the top of the file in case we want this file to be processed first by the compiler. If we know this file will be processed after validation.trd, which already contains an /import(core), declaration we can omit it.

[Note]

We’ve bound the sum of a and b to node sum, in order to avoid placing application logic in inline node declarations.

All that’s left in the ui.html file is the user interface elements and the inline bindings to the nodes a, b, sum and the error-message instances, which display the error messages.

ui.html

<!doctype html>
<html>
    <head>
        <title>Adding Numbers</title>
    </head>
    <body>
      <h1>Adding Numbers</h1>
      <div>
        <label>A: <input id="a" value="<?@ valid-int(a) ?>"/></label>
        <?@ error-message(a) ?>
      </div>
      <div>
        <label>B: <input id="b" value="<?@ valid-int(b) ?>"/></label>
        <?@ error-message(b) ?>
      </div>
      <hr/>
      <div><strong>A + B = <?@ sum ?></strong></div>
    </body>
</html>

Building

To build an application consisting of multiple source files, the source files are simply listed, under the sources entry, of the build configuration file. Modify the build.yml file to the following:

build.yml

sources:
  - validation.trd
  - app.trd
  - path: ui.html
    node-name: ui

output:
  path: app.html
  type: html
  main-ui: ui

[Note]

The source files are processed in the order they are listed.

Since there are no processing options for validation.trd and app.trd, we’ve listed the paths to the files directly rather than in the path entry of a dictionary.

[Tip]

To build entirely from the command-line, simply list the files, in the order they are to be processed, after tridashc.

The following is equivalent to the build configuration file

tridashc validation.trd app.trd path : node-name=ui.html \
    -o path.html -p type=html -p main-ui=ui

12.2. Modules In Depth

Whilst separating the application into multiple source files, grouping related components in a single file, is a significant cleanup, it would be even better if the nodes could be grouped into different namespaces based on their purpose, for example utility, application logic and ui. This would allow us to use only the nodes from the utility namespace, without having the rest of the utility nodes clash (i.e. have the same identifiers) with the application logic nodes. Whilst in this application there are no clashes, keeping the nodes in separate namespaces allows us to use the same validation.trd file in another application without the fear that some node is going to clash with other nodes in the application.

Modules are a means of separating nodes into different namespaces, where each module is a namespace in which nodes are contained. A node with identifier x in module mod1 is a distinct node from the node x in mod2, even though the two nodes share the same identifier.

[Note]

We’ve already made use of the core module throughout these tutorials.

Creating Modules

Modules are created with the /module operator which has the following syntax:

/module Operator Syntax. 

/module(module-name)

This indicates that all node references, in the declarations following the /module declaration, will occur in the module with identifier module-name. Remember that nodes are created the first time they are referenced, thus if a node is referenced which is not in the module, it is created and added to the module.

Example. 

/module(mod1)
x -> node1

/module(mod2)
x -> node2

The first reference to the node x occurs in module mod1 thus a node x is added to mod1. The second referenced occurs in module mod2 thus the node is added to mod2. The two nodes are distinct even though they share the same identifier.

If no module is specified the node references occur in a nameless init module. The current module is reset to the init module prior to processing each source file.

[Important]

Module identifiers are distinct from node identifiers, thus a node mod will not clash with a module mod unless the module is added as a node to the module containing the node mod.

Using Modules

This is great but it is of little use if you can’t reference a node that is declared in a different module from the current module.

The /use operator allows nodes in a module to be referenced as subnodes of a node with same identifier as the module identifier.

/use Operator Syntax. 

/use(mod1)

This adds a node mod1 to the module in which the declaration occurs. Then you can reference a node x in mod1 as a subnode of mod1, mod1.x. mod1 is, however, a pseudo-node as its value cannot be referenced nor can bindings involving it be established. It only serves as a means to reference nodes in the module mod1.

[Tip]

To use a meta-node f declared in mod1, simply reference it as a subnode of mod1, mod1.f(a, b).

This greatly increases the functionality of modules however sometimes it may become annoying to have to type out the full name of the module over and over again, for each node. You can try keeping the module names short however then you run the risk of module name collisions. The /use-as operator allows you to control the identifier of the pseudo-node, that is created in the current module, with which nodes in the module can be referenced.

/use-as Operator Syntax. 

/use-as(mod1, m)

This creates a pseudo-node with identifier m, with which nodes in mod1 can be referenced. Nodes in mod1 can then be referenced as subnodes of m.

[Note]

Both /use and /use-as will trigger a compilation error if the pseudo-node identifier already names a node in the current module.

Importing Nodes

Sometimes you would like to explicitly add a node in another module to the current module, so that you don’t have to reference it as a subnode of the module. The /import operator allows you to do this.

It has two forms:

  • A short form taking only the module as an argument, in which case all nodes exported from the module are added to the current module.
  • A long form in which the first argument is the module and the following arguments are the individual nodes to import from the module. Only the nodes listed are imported.

Example. 

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

# Long form: Only import nodes x, y, z
/import(mod1, x, y, z)

[Important]

The short form only imports those nodes which are exported from the module not all nodes.

[Note]

The long form allows you to choose which nodes are imported into the current module. You can list any node in the module, not just an exported node.

/import also has a side-effect in that if an imported node, whether imported by the long or short form, is registered as an infix operator, its entry in the operator table is copied over to the operator table of the current module so that it can also be written in infix position in the current module.

Example. 

# @ is a meta-node that is registered as an infix operator
/import(mod1, @)

# It can be also be placed in infix position in the current module
x @ y

You cannot place a node in infix position if it is referenced as a subnode of the module.

/use(mod1)

# The following will not compile as you cannot place a subnode in
# infix position.

x mod1.@ y

# Instead you have to write it in prefix notation:
mod1.@(x, y)

Exporting Nodes

We mentioned that the short form of the /import operator imports all nodes which are exported from the module. Nodes are exported from the current module using the /export operator.

/module(mod1)

# Exports nodes x, y and z from the current module
/export(x, y, z)

Importing mod1 by the short form, /import(mod1), will import nodes x, y and z into the module.

/export can take any number of arguments and multiple /export declarations will result in the nodes listed in each declaration being exported.

Directly Reference Nodes in Another Module

The /in operator references a node in another module, for which a symbol has not been created in the current module using /use or /use-as.

/in Operator Syntax. 

/in(module, node)

where module is the name of the module, as declared by the /module operator, and node is the node expression which is processed in module.

This operator is most useful when writing macros, however there is rarely a need for it when writing code directly as whatever can be achieved with /in can be achieved in a more readable manner with /use, /use-as or /import.

12.3. Adding Modularity to Adding Numbers

Let’s group the nodes declared in the validation.trd file, i.e. the nodes related to the input validation logic into a module called validation. To do that simply add the declaration /module(validation) to the top of the file.

We mainly require the valid-int meta-node and Negative-Number failure type node to be referenced outside the validation module, thus we’ll export those nodes using the declaration /export(valid-int, Negative-Number).

[Note]

We haven’t exported validate and Negative-Number! as those are only used in the implementation of valid-int, and are not used outside the validation.trd file.

validation.trd

/module(validation)
/import(core)
...
/export(valid-int, Negative-Number)

We’ll group the main application logic into a sum-app module, by adding the declaration /module(sum-app) to the top of the app.trd file. The nodes exported from the validation module have to be imported in this module, by adding an import declaration for the module after the import declaration for the core module. We’ll contain the user interface in this module, thus we wont be needing to export any nodes from it.

app.trd

/module(sum-app)

/import(core)
/import(validation)

...

Finally, we have to add a Tridash code tag to the top of the ui.html file, which changes the module to sum-app, since the sum and error-message nodes are contained in it.

ui.html

<? /module(sum-app) ?>
<!doctype html>
<html>
    <head>
        <title>Adding Numbers</title>
    </head>
    <body>
      <h1>Adding Numbers</h1>
      <div>
        <label>A: <input id="a" value="<?@ valid-int(a) ?>"/></label>
        <?@ error-message(a) ?>
      </div>
      <div>
        <label>B: <input id="b" value="<?@ valid-int(b) ?>"/></label>
        <?@ error-message(b) ?>
      </div>
      <hr/>
      <div><strong>A + B = <?@ sum ?></strong></div>
    </body>
</html>

We’ve successfully divided our source code into multiple modules, based on the purpose of the nodes defined in it. However, the node which references the contents of the ui.html file is still added to the init module. Whilst in this application it doesn’t pose a problem as there is no other node with identifier ui in the init module, it may become a problem if there is a need to reference HTML elements from within the app.trd file. Remember HTML elements can only be referenced as subnodes of self, within the HTML file itself. Outside the file they have to be referenced as subnodes of the node which references the contents of the HTML file.

In order for the ui node to be created in a module other than init, the node-name option has to be of the following form module.name, where module is the module in which the node should be created and name is the name of the node. The same syntax applies in the main-ui output option.

Change the build.yml file to the following:

build.yml

sources:
  - util.trd
  - app.trd
  - path: ui.html
    node-name: sum-app.ui

output:
  path: app.html
  type: html
  main-ui: sum-app.ui

With this you can now reference HTML elements, from app.trd, as subnodes of the ui node.

Build and run the application. You wont see any new features however we’ve significantly improved the organization of the code and thereby improved the maintainability of the application.

[Tip]

You wouldn’t typically divide an application this small into multiple modules, however we’ve done so here in order to demonstrate how it is done.

12.4. Infix Operators

We have referred to some meta-nodes as being registered as infix operators. This allows them to be placed in infix position instead of prefix position. It turns out you can do the same for your own nodes. This section describes how.

Precedence and Associativity Basics

Each module has an operator table, which contains the identifiers of all nodes which can be placed in infix position as well as their precedence and associativity. The precedence is a number which controls the priority with which operands are grouped with infix operators, in an expression containing multiple different infix operators. Higher numbers indicate greater precedence.

The multiplication * operator has a greater precedence (200), than the addition + operator (100) thus arguments will be grouped with the multiplication operator first and then the addition operator.

The following infix expression:

x + y * z

is parsed to the following expression in prefix notation:

+(x, *(y, z))

Notice that the * operator is grouped with the operands y and z first, and then x and *(y, z) are grouped with the + operator. This is due to * having a greater precedence than +.

To achieve the following grouping:

*(+(x, y), z)

enclose x + y in parenthesis:

(x + y) * z

Associativity controls the grouping of operands in an expression containing multiple instances of the same infix operator. The + operator has left associativity.

Thus the following infix expression:

x + y + z

is parsed to the following expression in prefix notation:

+(+(x, y), z)

i.e. it is equivalent to

(x + y) + z

If the + operator were to have right associativity, the expression would be parsed to the following:

+(x, +(y, z))

Below is a table showing the precedence and associativity of some of the builtin operators.

Operator Precedence Associativity

->

10

right

<-

10

left

!-

15

right

or

20

left

and

25

left

+

100

left

-

100

left

*

200

left

/

200

left

[Tip]

Visit the modules/core/operators.trd file of your Tridash installation to see the full list.

Registering your own infix operators

Node identifiers can be registered as infix operators with the special /operator declaration.

/operator Syntax. 

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

The first argument is the identifier, the second argument is the operator precedence as a number and the final argument is the symbol left or right for left or right associativity. If the third argument is omitted it defaults to left.

This declaration adds an infix operator to the operator table of the current module. In the declarations, following the /operator declaration, id can be placed in infix position.

[Note]

id can be any valid identifier, not just an identifier consisting only of special symbols. However, as a result, a space is required in between the operator and its operands.

It is not checked whether id actually names an existing node, however using it in infix position only makes sense if id names a meta-node.

If the node with identifier id is imported into another module, its entry in the operator table, of the module from which it is imported, is copied into the operator table of the module into which it is imported.

The precedence and associativity of existing operators can be changed by an /operator declaration, however only the operator table of the current module is changed even if the operator is an imported node.

Example: Infix add Operator

To demonstrate how you would go about registering a meta-node as an infix operator, we’ll write an add meta-node which computes the sum of two numbers, register it as an infix operator and replace a + b with a add b in the Adding Numbers Application.

[Note]

There is no value in writing a new meta-node which is simply a wrapper over a builtin meta-node, and registering it as an infix operator. The purpose of this tutorial is simply to demonstrate how you would register your own meta-node as an infix operator.

We’ll first define add and then register it as an infix operator with precedence 100 and left associativity which is the same as the builtin + operator.

Add the following somewhere near the top in app.trd:

add(a, b) : a + b

/operator(add, 100, left)

Now change the binding declaration to sum with the following:

sum <- a add b

That’s it you’ve now added your own infix operator to the language.