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]]