<- ^ ->

Other features

5   Other features

5.1   Module system

Modules provide way of separate compilation along with possibility of avoiding namespace conflicts. Current module system is based on ideas from OCaml. However it is very limited, we only support one level of modules, there are no functors and so on. Module Foo consist of two files: foo.g (implementation) and foo.gi (interface). Whatever names are placed in foo.* files, they are all prefixed with `Foo::'. gontc compiles foo.gi file to foo.gio, and foo.g to foo.o. foo.gio is needed whenever you access Foo:: symbols from other modules.

Example:

list.gi:
        // this is to output information,
        // that we implement type `t', but not to
        // disclosure what it is.
        type <'a>t;

        // return first element (head) of the list
        'a hd(<'a>t x);
        
        // return all but first element (tail) of the list
        <'a>t tl(<'a>t x);

        // some bogus, random variable -- you can export variables
        // the same way as you would with functions
        int cnt;
        
        // create new empty list
        <'a>t create();
        
        // apply f to all elements of l, return list of results
        <'b>t map(*('a) -> 'b f, <'a>t l);

        // call f on all elements of the list
        void iter(*('a) -> void f, <'a>t l);
list.g:
        // this is local datatype.
        opt_struct <'a>t {
                'a data;
                <'a>opt_struct next;
        }
        
        // this will be exported out (`public')
        'a hd(<'a>t x) { return x.data; }
        <'a>t tl(<'a>t x) { return x.next; }
        
        // this is local, can't be called from outside the module
        void helper(<'a>t x) { ... }

        // and more publics
        int cnt;
        <'a>t create() { cnt++; return null; }
        <'b>t map(*('a) -> 'b f, <'a>t l) { ... }
        void iter(*('a) -> void f, <'a>t l) { ... }
        // ...
Then if you want to use the module, it can be done with :: notation, like this:

        <int>List::t l = List::create();
        ...
        int k = List::hd(l);
In case of some modules it might be useful to open them, i.e. import all symbols from module intro current namespace, so you no longer have to use :: notation (but you still can, it is often suggested for readability):

        open List;
        ...
        <int>List::t l = List::create();
        ...
        int k = hd(l);
        <int>List rest = tl(l);
[[defining types in module iface is currently broken]]

5.2   Pattern matching

It is especially useful with conjunction with tuples and unions. Patterns are used in switch and let statements, like this:

        int compute(exp e)
        {
                switch e {
                case Const[x]    : return x;
                case Var[x]      : return lookup_var(x);
                case Add[e1, e2] : return compute(e1) + compute(e2);
                case Mul[e1, e2] : return compute(e1) * compute(e2);
                case Div[e1, e2] : return compute(e1) / compute(e2);
                case Sub[e1, e2] : return compute(e1) - compute(e2);
                }
        }

        exp diff(exp e)
        {
                switch e {
                case Const[x]    : return Const[0];
                case Var[x]      : return Const[1];
                case Add[e1, e2] : return Add[diff(e1), diff(e2)];
                case Sub[e1, e2] : return Sub[diff(e1), diff(e2)];
                case Div[e1, e2] :
                        exp up = Sub[Mul[diff(e1), e2], Mul[e1, diff(e2])];
                        exp down = Mul[e2, e2];
                        return Div[up, down];
                case Mul[e1, e2] : 
                        return Add[Mul[diff(e1), e2], Mul[e1, diff(e2])];
                }
        }
To convince you, we're not so far from C:

        union color {
                void Red;
                void Green;
                void Blue;
        }
This is roughly equivalent of C's:

        typedef enum {
                Red, 
                Green, 
                Blue } color 
Then we do:

        int spy007;
        
        string color_name(color c)
        {
                string r;
                
                switch (c) {
                case Red: 
                        spy007++;
                        r = "red";
                case Green: 
                        r = "green";
                // [] also allowed
                case Blue[]: 
                        r = "blue";
                }

                return r;
        }
You can note lack of break at the end of case clauses. They are not required, as there is no fall through (because case clause has to introduce new scope).

In previous examples we have checked for each possibility, but we don't have to:

        string var_name1(exp e) 
        {
                switch e {
                case Var[x]: return x;
                // matches any x
                case x: return "not variable";
                }
        }

        string var_name2(exp e) 
        {
                switch e {
                case Var[x]: return x;
                }
        }
var_name2 would raise Match_failure exception if any pattern didn't match.

Patterns can be used to decomposite tuples, like this:

        *[int, int] t = [1, 2];

        ...
        switch t {
        case [i1, i2]: return i1 + i2;
        }
This can be abbreviated to:

        let [i1, i2] = t in { return i1 + i2; }
There can be more then one assignment, like this:

        let [t1, t1] = t,
            [s1, s2] = s {
                // ...
        }
The let assignment and binding names with case just creates new name for an object. Specificly it means that assigning values to names bound with let/case changes object itself. Example:

        *[int, string] t = [1, "one"];
   
        switch t {
        case [i, s]: i = 2; 
        }
   
        let [i, s] = t in { s = "two"; }

        // here t == [2, "two"]
One can note that you can also decomposite t with:

        string s;
        int i;
        [i, s] = t;
        // here i = 2, s = "two"
        // however:
        i = 3; s = "three";
        // here i = 3, s = "three", but t == [2, "two"]
You can also pattern-match structures [[describe this]]

5.3   Exceptions

They are used to inform about unusual situation in a program, in order to transfer control to block, that can handle it. They can be thought of as member of a huge union:

        union exn {
                // system defined
                void Null_access;
                void Match_failure;
                void Not_found;
                
                // user defined, Compiler is name of user module
                string Compiler::Syntax_error;
                void Compiler::ICE;
        }
New members are added with exception keyword:

        exception string Syntax_error;
        exception void ICE;
In order to signal unusual situation, you use raise keyword:

        raise Syntax_error["parse error"];
        raise ICE;
        raise Compiler::ICE[];
At the place, where you know how to handle it, you use try:

        try {
                open_file();
                ...
                parse();
                ...
        } with e {
        case Syntax_error[s]:
                print_msg(s);
        case ICE:
                print_msg("Internal Compiler Error");
        } finally {
                close_file();
        }
First we open some file, then we call parse(), that can raise Syntax_error or ICE in which case we print error message, or something else, but in then control is transfered to upper-level try block. No matter how control leaves try {} block instructions in finally { ... } are executed. So we always close the file.

[[Nope... but they are expected right after modules, which means Real Soon Now]]

<- ^ ->

Other features