Another Web Language:
a brief description

(version 03.09.2007)

One of the design goals of AWL programming language was to provide suitable alternative for common World Wide Web programming languages and tools. AWL tries to achieve this objective by providing consistent set of tools for text formatting (like HTML and CSS), data definition (XML), vector graphics (like SVG and Macromedia Flash) and scripting (like JavaScript/VBScript). AWL borrows some aspects from LISP/Scheme, Perl, Ruby and other script languages. Operating as interpreter (with partial precompilation and compile-time variables and functions binding), it implements many ideas of procedural, functional and object-oriented programming.


Language core

Scalar types and lists

AWL has a variety of data types. The primitive (scalar) ones are numbers and character strings. Internally, number type is subdivided into integer and float subtypes (with implicit conversion between them in most cases). Strings are of unlimited length and may contain any 8-bit ASCII characters (support for 16-bit Unicode is under development).

The primary structural type is list. As in LISP, lists can be of arbitrary length and heterogenic (can contain values of any type, including other lists with unlimited nesting depth).

Here are examples of valid lists (made of numbers, strings and other lists):

(1, 2, “aaabb”, 22.13, “cccc”)
((11, ‘aa’), (22, ‘bb’), ‘cc’)

Empty list () represents absence of value (in some situation termed undef). Lists, containing just a single element, are identical to this element. When the final element of list is other list, it is merged with the start of the list:

(1, (2, (3, (4, 5))))

is just an alternative form of:

(1, 2, 3, 4, 5)

However, when final element of list must preserve its structure, the special list form (“open list”) is available. In the following case:

(1, 2, (3, 4, 5), )

actual last element of list is sublist (3, 4, 5) – due to final undef (which is not counted as final element by most list operations).

When all list elements are syntactically atomic (which includes variables, scalars, other lists) alternative, more terse list syntax is permitted:

[1 2 “aaabb” 22.13 “cccc”]
[[11 ‘aa’] [22 ‘bb’] ‘cc’]
[1 2 [3 4 5] :]

(Last example shows an alternate syntax form for open lists.)

Functors and terms

Most of the language expressions actually are nested terms. Terms are basic language constructs, providing a standard way to invoke functors, which are primary language device of evaluation. Syntax of term looks exactly like function call in common languages:

functor(args)

Evaluation of this term results in invoking functor with args (in most cases, this is a list of values) as its argument(s). It is important to note, what terms themselves are data items, too.

There are plenty of built-in functors, performing operations on numeric and string values. For example, evaluation of neg(x) returns value of x negated; add(x, y) adds values of x and y together, and so on. However, to make code more readable, language provides more familiar (“operator”) syntax for many built-in functor invocations: these may be coded as prefix/postfix unary or infix binary operators (with appropriate priorities and associativities). For example, expression (a+3) * (b–5) may be used as the substitute for mul(add(a, 3), sub(b, 5)). However, “operator” syntax of expressions is no more than a “syntactic shortcut” for a normal (“functional”) form of them, and is translated to the later at compile time.

Language provides a lot of built-in functors for numeric calculations. These include standard arithmetic operations, bitwise/logical and shift operations, comparisons, and a lot of mathematical functions (like sqr for square root; sin, cos, tan for standard trigonometric functions; exp and log for exponentiation and logarithm, etc.).

Also, there is lot of string built-ins (and many of them have alternative operator syntax, too). For example, s_cat(s, t) (or s +$ t) returns concatenation of s and t (treated as strings); s_rep(s, N) (or s *$ N) returns string s replicated N times; s_slice(s, R) (or s $[R]) returns fragment of string s sliced by range R (see explanation of ranges below); and so on. Implicit coercions between scalar types are allowed: numbers may be coerced to strings, and vice versa (just like in Perl or Python). There are explicit conversions as well: for example, num(val) coerces value val to number, and string(val) coerces value val to string.

As in most of functional languages, functors are playing crucial role in AWL: most of program execution is, in fact, functors evaluation. (In the following examples, we show all functor names in bold face).

Variables and mutators

Like almost any programming language in existence, AWL has variables. All variables are completely typeless (may include value of any type at any moment) and module-level (global) variables don’t need to be explicitly declared.

Some of built-in functors are mutators, and can alter value(s) of their argument(s). Of course, the most important mutator operation is assignment: set(v, x) (or just v = x) evaluates expression x, then assigns result to v, which may be any variable (or, in fact, any mutable). Other mutators include increment/decrement operations (like in C++ or Java): for example, ++x (or inc(x)) increments x (returning new value), and y-- (or dec_p(y)) decrements y (returning old value). The exchange operation swap(a, b) (or a :=: b) exchanges values of mutables a and b. The assignment can be combined with many binary operations (internally it is implemented with comb built-in, which expects term argument, and extends its evaluation to include assignment of result to first operand). For example, v =+: w (note syntax!) just adds value of w to the mutable v (internally, represented as comb(add(v, w))). Combining assignment with some unary operations (especially unary – (neg) and unary ~ (not)) is an unusual feature: for example, v =:- arithmetically negates value of mutable v (internally represented as comb(neg(v))).

All variables are obviously mutable, but there are other kinds of mutables in the language. For example, some functors evaluate to mutable result, so they are allowed anywhere mutable is expected. The perfect example is l_item functor: l_item(n, L) (or just L[n]) provides access to n-th element of list L (indexing always starts at 0). Because result of evaluation is mutable, this operation can be used to change individual elements of the list (like in L[n] = 10 or ++ L[n]). Other examples are l_head(L) and l_tail(L): these operations provide access to head of list L (its first element) and tail of list L (all elements except first) respectively. With help of these functors, processing and modifying lists as binary trees (which internally they are in fact) is possible.

Assignment can be applied not only to scalar values. For example, any list of mutables is mutable too, so list of any values may be assigned to it:

[a b c] = [10 20 30];

assigns 3 values to 3 variables at once. All assignments are performed simultaneously: so [x y] = [y x] is an another possible way to swap values of x and y. It is important to note, what list of values in assignment can be shorter than list of mutables (in this case, all extra mutables are set to undef), or it can be longer (in this case, last assigned mutable received all redundant values as the end of the list). If element of mutables lists are other lists, assignments of the are done recursively.

There is a lot of built-ins working with list operands. To name a few, l_len(L) returns length of list L; l_copy(L) makes an independent copy of list L; l_rev(L) (or [~]L) returns reverse of list L; l_cat(L, M) (or L [+] M) returns concatenation of lists L and M; l_rep(n, L) (or L [*] n) returns replication of list L exactly n times. (These operations treat undef as list of length 0, and any scalar value as list of length 1.)

Several built-ins make easy adding and removing elements to/from list. Most notably, l_push(L, val1, val2, ... valN) inserts N values (val1...valN) at start of list L (in reverse order, so valN becomes first) and l_pop(L, var1, var2, ... varN) removes N values from start of list L (assigning them to mutables var1 ... varN in direct order). Both operations have an alternative syntax form: L [<-] (val1 ... valN) and L [->] (var1 ... varN) respectively. Combining these operations with list accessor operation l_tail_by(n, L) (which has effect equivalent to n times application of l_tail to L), it is easy to insert/remove elements at any place of the list.

Demand-evaluation and control flow

Although many of scalar functors evaluate their arguments before performing their own evaluation, this is not always the case. The principal feature of language semantics is demand-evaluation (or lazy evaluation) of arguments of many built-in functors. Some functors evaluate their argument optionally (conditionals), some may evaluate them more than once (iterators).

For example, typical conditional evaluation is provided by if/unless built-in functors. Evaluation of if(cond, then, else) always evaluates cond first; then, if condition appears to be true (anything different from 0, empty string or undef) it evaluates expression then, otherwise expression else is evaluated. (There is a ternary operator form for this, borrowed from C-like languages: cond? then : else). Built-in unless does the same of if, except “polarity” of condition cond is reversed. The “short-circuit” conditional binaries c_and(P, Q) and c_or(P, Q) perform logical AND/OR operations on their operands P and Q; but with optional evaluation of Q (which is not evaluated by c_and if P is false; and by c_or if P is true).

Conditional iterators are provided by while/until built-ins: evaluation of while(cond, loop) evaluates both cond and loop repeatedly, while cond remains true (and never evaluates loop, if cond is false initially). (There is binary operator form for this, too: cond ?? loop). Built-in until does the same, only reversing condition polarity. Both iterators are pre-conditional (e. g. checking cond before loop), but they have post-conditional counterparts: do_while and do_until (which have similar arguments and semantics, except they check cond after each evaluation of loop, which is evaluated at least once). Note, what (as all expressions) loop constructs return a value too (which is result of final evaluation of loop).

Very useful are sequential iterators, making easy iterating with index variable receiving all values in the integer range, in incremental (for_inc) or decremental (for_dec) order. These iterators expect range to be list of two boundaries (from, to), including all integer values, for which from <= value < to (note, what value of from is included into range, but value of to is excluded from it); range is considered empty, if from>=to. For ranges, shortcut syntax from .. to is permitted. If only one integer value N is supplied for range, range 0..N is assumed. Iterator for_inc(index, range, loop) changes mutable index starting at from in ascending order until to-1, evaluating loop on each iteration. Iterator for_dec(index, range, loop) does the same, except index is changed in descending order, from to-1 to from. Finally, built-in iterator times(count, loop) just evaluates loop exactly count times.

List iterator l_loop allows looping through all elements of list: evaluation of l_loop(item, list, body) results in iterating item through all elements of list (from first one to last), and evaluating body on each iteration. Alternative l_loop_r does the same, except list items are iterated from last one to first.

In many cases, conditions and iterators require a sequence of expressions to be evaluated. Simplest way to achieve this is to use block expressions (blocks):

{ expr1; expr2; … exprN }

evaluates expressions in sequence (from expr1 to exprN), and returns just value of exprN. Note, what semicolon is always treated as separator in AWL (unlike C or Java): if “;” follows final element of the block, this means, what actual final element (and block's return value) is undef. (This may be actually needed, when return value of block is irrelevant, but may be misleading in other cases, so some care is advised.)

Finally, to make code more readable, some alternate syntax is allowed for nested terms. For example, expression func1(arg1, func2(arg2)) allows alternate syntax: func1(arg1):: func2(arg2). This syntax is especially useful, when control functors are deeply nested: for example, notation

if (cond):: Then :: Else;
unless (cond):: Else :: Then;
while (cond):: Loop;
until (cond):: Loop;

looks (especially to adepts of procedural languages) more obvious and clear, than equivalent “functional” syntax:

if (cond, Then, Else);
unless (cond, Else, Then);
while (cond, Loop);
until (cond, Loop);

In fact, operation '::' is another form of built-in list constructor: Head::Tail is completely equivalent to list (Head, Tail). However, because priority of this operation is lower, than of term constructor, the above examples work as expected.

Basic input/output tools

Input/output tools are explained very briefly, as well as streams, which provide means of communication with outside world.


To put list of values to output stream O_Str, f_put built-in is used:

f_put(O_Str, Val1, Val2, ... ValN);

or

O_Str <: (Val1, Val2, ... ValN);

Values Val1 ... ValN are normally scalars (lists nesting is allowed), implicitly coerced to strings and output as strings. Operand O_Str may be omitted (equals to undef), and in this case standard output stream is assumed. Number of successfully written list items is returned as result.

To get several lines from input stream I_Str, f_get built-in is used:

f_get(I_Str, Mut1, Mut2, ... MutN);

or

I_Str :> (Mut1, Mut2, ... MutN);

where Mut1 ... MutN are mutables, receiving lines from input stream (as string values). If coercion to different type is needed, it must be done explicitly. Result of f_get is number of input lines successfully read and assigned (which may be less than N on end of file or input error).

Coercion of them (or parsing), if needed, requires special efforts.

There's much more built-ins, associated with input/output, which are not detailed here.

User-defined functors

In AWL (like in LISP and related languages), all expressions (including variables, terms and blocks) are data items, and may be treated as such: assigned to variables, embedded into complex data structures, passed to functors and/or returned from them. To make distinction between evaluated and non-evaluated data, some language built-ins are usable.

Devaluation is an unary functor: deval(expr) (with operator syntax: @expr). Its effect is inhibition of operand's evaluation: returned value is expr itself, instead of result of expr. The following example:

x_ref = @sqr (x*x – y*y);

makes expression sqr (x*x – y*y) the actual value of variable x_ref.

Effect of revaluation is exactly opposite. Unary functor reval(expr) (with operator syntax: ^expr) evaluates result of expr evaluation (actually, evaluating it twice). So, considering the example above:

value = ^x_ref;

has exactly same effect, as

value = sqr (x*x – y*y);

if the value of x_ref was unchanged.

Because lazy assignments are needed frequently, there’s a special replacement for var = @expr. Using var := expr (or let(var, expr)) has exactly same effect: assigning expr to mutable var without evaluation.


Expression evaluated on demand can be (and frequently are) used as kind of extremely “lightweight” functions, having no parameters or local context. Of course, they can't always replace the functor definitions.

The general syntax of user functor definition looks like this:

! myfunc(par1 par2 … parN): [loc1 loc2 … locN] = body;

Here myfunc is a name of new functor; par1…parN are functor parameters; loc1…locN are functor local variables, and body must be any valid expression. Both parameters and local variable names belong to functor's local variables name space (temporarily overriding global variables with same names, if they exist), and are visible in body.

To call user functor, used same syntax, as for built-ins:

myfunc(10, 20, 30);

This results in user functor myfunc being invoked with par1 = 10, par2 = 20 and so on (and all local variables set to undef). This call returns result of evaluation of body. For example, FtoC can be used to convert degrees of Fahrenheit to Celsius scale:

! FtoC(temp) = (temp–32) * (5/9);

Another functor calculates distance between points (x0, y0) and (x1, y1):

! dist(x0 y0 x1 y1) = sqr((x1-x0)*(x1-x0) + (y1-y0)*(y1-y0));

More efficient way to do this is to use built-in rad(dx, dy), calculating value of sqr(dx*dx + dy*dy):

! dist(x0 y0 x1 y1) = rad(x1-x0, y1-y0);

Functor parameters may have default initializers. For example:

! draw_shape(Color=Red Shape=Rectangular) = { ... };

Because both parameters of draw_shape have default values, they are substituted, if actual argument is undef:

draw_shape(Green); ` draw_shape(Green, Rectangular) `
draw_shape(); ` draw_shape(Red, Rectangular) `
draw_shape((), Circular); ` draw_shape(Red, Circular) `

If functor argument has neither explicit nor default initializer, its default value is undef.

Needless to say, functor definitions may be recursive. Any invocation of recursive functor has private set of parameters and locals. Here is a recursive definition of factorial (of n):

! factorial(n) = n? n*factorial (n–1) : 1;

or binomial coefficients (n, m):

! binomial(n m) = (0 < m && m < n)? binomial(n-1, m) + binomial(n-1, m-1) : 1;

(There’s a special syntax to define a family of mutually-recursive functors, which is not described here.)

Return value of functor may be any data structure. Lists are frequently used as return values:

! transform_pt(x y) = (a*x + b*y + c, d*x + e*y + f);

returns result of linear transformation of point (x, y) (defined by coefficients a, b, c, d, e, f).

Another example of recursive functor, operating on list, is list flattening operation. Functor l_flatten returns linear list of all atomic (non-list) elements of list L, collected recursively:

! l_flatten(L) = is_list(L)?
l_flatten(l_head(L)) [+] l_flatten(l_tail(L)) : L;

One of the most interesting aspects of user functors is their ability to have demand-evaluated parameters (like many language built-ins). Although by default all parameters are evaluated before evaluation of functor body, if parameter is preceded with @, it's evaluation is considered functor's responsibility (and may be performed any time). For example, although AWL have no built-in, equivalent to C++/Java for loop, it is easy to emulate one:

` C-like for iterator `
! c_for(@Init @Cond @Iter @Body) =
{^Init; ^Cond?? {^Body; ^Iter}};

So-defined iterator can be used as easily, as built-in one:

c_for(I = 1, I <= 10, ++ I, <: ["I = " I "\n"]);

outputs sequential numbers from 1 to 10. Lazily evaluated functor arguments can be used to implement many language extensions, including non-standard conditionals and iterators. They are extremely useful to implement wrappers: functors, used to surround specified evaluation with some “prologue” and “epilogue” actions. To give some idea, how wrappers work, here is a very trivial example:

! R_brackets (@arg) = { <: '('; ^arg; <: ')'; };
! S_brackets (@arg) = { <: '['; ^arg; <: ']'; };

Functors R_brackets and S_brackets are wrappers, “enclosing” evaluation of its argument in round and square brackets. If their invocations are nested, all “prologue/epilogue” actions are arranged in proper order: something like R_brackets(S_brackets(<: “Hello”)) will result in outputting “([Hello])” (note, what argument here is evaluation, and not just a value).

Reference to functor is an another kind of data. However, it is important to remember, what AWL always uses separate name spaces for variables and functors. So, to refer to functor myfunc, special syntax is required: !myfunc (because just myfunc looks for variable named so). This reference can be treated as any other data item: assigned to variable, contained in list, or passed to other functor (thus making functors a first-class values). The most obvious thing to do with functor reference is, of course, to invoke it: apply(funcref, args) (may be abbreviated to funcref ! args) does invocation of functor referred to by funcref with argument(s) args (returning result). Here is a rather trivial example:

result = (!add, !sub, !mul, !div)[op_code] ! (x, y);

Depending from op_code (expected to be in range 0..4), it sets result to x+y, x-y, x*y or x/y. Although this example used only built-in functors, any functor defined by user is applicable too. For another example, if we need to check numeric values (valA, valB) for being in ascending/descending order (depending from boolean flag), we can write something like:

test_result = (flag ? !lt : !gt) ! (valA, valB)

which may be quickest way to perform such test, if valA and valB are complex expressions.

When functor reference is used only once, there is no obvious need to name it. Anonymous functor references are another kind of expressions (similar to lambda-expressions in LISP, Python and other languages). For example:

func_list = (!(x y) = (2*x + 3*y), !(x y) = (5*x – y));

creates func_list as a list of two references to anonymous functors. Any of them may be invoked in an usual way: for example func_list[1] ! (5, 7) evaluates to 31, and func_list[1] ! (5, 7) evaluates to 18. As obvious from this example, definition of anonymous functor looks like ordinary functor definition, although it is an expression, lacks functor name and (attention!) functor's body must be syntactically-closed.

Some built-ins expect functor references as arguments. For example, l_map(func_ref, list) returns new list, constructed by applying func_ref to all elements of list in sequence. So, l_map(!(x) = (2*x + 5), [5 7 9]) returns list (15, 19, 23); and l_map(!(s) = (“{ +$ s +$ “}”), ['aa' 'ee' 'ii']) returns list (“{aa}”, “{ee}”, “{ii}”).

Finally, functor declarations may be nested. When functor definition is included into other functor definition, it creates its own local scope (actually, two scopes: for own parameters/locals and for its own nested functors). All outer scopes are accessible from inner definitions. (When looking for any variable or functor, searching is performed from inner scopes to outer ones, up to module global scope.)

Here is a functor, solving “Towers of Hanoi” problem (where move_disks is local functor of hanoi_solve):

! hanoi_solve(N):[count] = {
! move_disks(n from to via) =
n ? {
count = move_disks(n-1, [from via to]);
<: ["\t#" n ": " from " => " to "\n"];
++ count;
count + move_disks(n-1, [via to from])
} : 0;

<: ["\nTowers of Hanoi: " N " disks...\n\n"];
count = move_disks(N, ['A' 'B' 'C']);
<: ["\nPuzzle solved in: " count " steps.\n"]
};

Invoke hanoi_solve(N) to get a full solution for N disks.

Another functor example is recursive permutations generator. Invoking permute will generate (as lists) all permutations of integers in range 0..N, and calls functor referenced with action, with each permutation as argument:

! permute (N action): [index] = {
index = 0 [*] N;


! r_perm(n): [i] =
n <> N ? {
index[n] = n;
n ++;
for_dec(i, 1..n, index[i] :=: index[i-1]);
r_perm(n);
for_inc(i, 1..n, {
index[i] :=: index[i-1];
r_perm(n)
})
} :
` end of recursion: call 'action' with 'index' `
action ! Index;

` Algorithm entry point: `
r_perm(0);
}; ` -- permute `

Note, what most of work here is done by local functor r_perm, which is invoked recursively. To list all 16 permutations of numbers (0, 1, 2, 3), invoke it as permute(4, !(list) = (<: (list, “\n”))).

Arrays, hashes and patterns

AWL supports other ways of structuring data besides lists.

Arrays are multidimensional data structures. As lists, array are heterogenic, and can contain elements of any type (even other arrays). Standard way to create array is using array constructor built-in array (or a_create):

MyArray = array(Dim0, Dim1, ... DimN);

The arguments Dim0...DimN (scalars, evaluated and coerced to integers) provide dimensions of the created array (Dim0 is inner dimension, DimN is outer one). Elements of the created array are set to undef. Total number of dimensions is called rank of the array. Built-in array accessor operation: a_elem(Array, Index0, ... IndexN) provides mutable access to element of Array with indexes Index0 ... IndexN, corresponding to appropriate array dimensions. (Indexing starts from 0; negative index values are illegal). Again, there is a shortcut notation: Array{Index0, ... IndexN}.

For example, the following code creates and returns square identity matrix of order N:

! identity_mtx(N) : [i matrix] = {
matrix = array(N, N);
a_fill(matrix, 0);
for_inc(i, N, matrix{i, i} = 1);
matrix
}
;

Built-in a_fill(Array, Value) is used to quickly set all elements of Array to any Value. There are several built-ins to get information about arrays: for example, a_dims(Array) returns list of Array dimensions, a_rank(Array) returns rank of Array, and a_total(Array) returns total number of elements in Array (which is always product of its dimensions). To make conversion between arrays and lists easy, a_save(Array) saves contents of Array as list of values (iterating from outer dimensions to inner ones) and a_load(Array, List) does the reverse, loading Array by the values from List, in the same order, as a_save stores them.

Finally, a_copy(Array) makes exact copy of Array, with same dimensions and elements. Although arrays can be always replaced by deeply nested lists, they are preferred way do store multi-dimensional data, requiring less memory and providing faster access to elements. The drawback of arrays is what they require more efforts to reshape, or insert/remove element(s) in the middle, which is faster with lists.

Hashes are associations between set of keys, and set of values corresponding to them. Both keys and values can be of arbitrary type (the only illegal value for a key is undef). Hash is created by invoking hash constructor hash (or h_create):

MyHash = hash();

The only (optional) parameter of hash is the integer value, defining initial internal capacity of the hash. To get mutable access to element of Hash, associated with some Key, built-in h_elem(Hash, Key) is used (if there was not element with such key, it is created). For example:

h_elem(MyHash, “hundred”) = 100;
h_elem(MyHash, “thousand”) = 1000;

For read-only access to hash, h_lookup(Hash, Key) may be used: contrary to h_elem, result is not mutable, and absent keys are not created. To remove item Key from hash, h_remove(Hash, Key) must be invoked (returning value formerly associated with Key). Of course, there is also iterator built-in to loop through the hash:

h_loop(Hash, Variable, Body)

iterates through Hash, setting Variable to list (Key, Value) for each hash element before invoking Body. Finally, h_count(Hash) returns total number of key/value pairs in the Hash, and h_clear(Hash) removes everything from the Hash, making it completely empty.

It is important to mention, what no implicit scalar coercions are applicable when accessing hash elements. For hash keys, all scalar types (integers, floats, strings) are distinguished: for example, 101, 101.0 and “101” are different hash keys. When working with hash, coercions between scalars, when needed, must be explicit. Strictly speaking, all hash keys are compared exactly as built-ins ident and differ do. Result of ident(V, W) (V [==] W) is true, if results of V and W are identical (same scalars, or lists (or any structural object) with same elements); result of differ(V, W) (V [<>] W) is the reverse.

Finally, AWL has regular expression patterns under development. Patterns are capable of describing string context to match, and are constructed of literals and several pattern composer operations. For example, rx_string(Str) returns pattern to match string Str (this operation is implicitly applied, when string operand is used where pattern is expected); rx_alt(Pat1, Pat2) implements pattern alternation (matches either Pat1 or Pat2); rx_cat(Pat1, Pat2) implements concatenation of patterns Pat1 and Pat2; rx_rep(Pat, flag, Range) implement repetition of pattern Pat (at least Range[0] and at most Range[1] times, with repetition “greediness” specified by flag), and so on. There will be much more pattern operations available in the future (and, possible, more convenient syntax for frequently-used patterns will be implemented, too).

Classes and objects

AWL provides support for object-oriented programming, based on concept of objects, being instances of user-defined classes. Before any instances of class are created, actual class definition must be done.

Class definitions have much in common with functor definitions. The basic syntax of class definition is outlined below:

!! myclass(par1 par2 … parN): [loc1 loc2 … locN]
{ class_defs_list };

Similarity between class and functor definitions is not coincidental. As well as functors, classes have list of class parameters (par1...parN, which are initialized by arguments) and class local members (loc1...locN, which are left uninitialized by default). (Class definition also can have constructor, destructor, and declaration of virtuals, all of them optional and preceding declarations list.) To instantiate any class, it must be invoked (with list of arguments to match class parameters), and new class instance is returned as result. This:

object = myclass(arg1, arg2, … argN);

creates and returns new instance of myclass (initializing parameters par1 ... parN with arguments arg1 ... argN). In other words, although functor evaluation is terminated (returning result), in the case of class, evaluation is “suspended” in the form of object, preserving values of parameters passed. Many aspects of functor parameters are applicable to class parameters: for example, they also require demand-evaluation and have default initializers. Class header is followed with list of class internal definitions (class_defs_list), which can contain definitions of local functors (methods) and even local classes. Note, what definitions in this list are separated by commas, not semicolons.

As functor, class has his own name space (actually, two distinct name spaces, for members and for local functors/classes). Normally, all access to class internals from outside must be qualified, with qualification expression looking like:

myclass!!expression

This is an example of (relatively rare) compile-time operations of AWL (so, there is no functional replacement for it). It forces expression to be interpreted in context of myclass, making all members and internal functors of myclass accessible there. Because all variable and functor names bindings are performed at compile time, the qualifier expression must be processed at compile time, too. (This complication is inevitable: because AWL is weakly-typed language, there is no way interpreter can predict, what some expression results in class instance, which is crucial for resolving names properly.)

The most important feature of AWL object system is instant class-object binding. Any class always has current instance (which is void initially) associated with it. Language allows only temporary modification of class current instance, by the following operation:

object.expr;

completely equivalent to functional expression with(object, expr). This is wrapper operation for expr: it evaluates and returns expr, but with object as current instance of its own class. Current instance definition is temporary and local for this expression: when evaluation of expr is complete, current instance of class is restored to what it was before (so, there is no problems in nesting invocations of with).

Current instance has effect, for example, for class members: they are always accessing appropriate members of current class instance (or undef, if there is no current instance of such class). This explains, how class functors operate. Although, due to the tradition, the internal functors of the class are termed methods, they are quite different from methods in conventional OO-languages. (For example, they have no implicit argument to specify current class instance, as conventional methods do, because current instance is defined externally, without any relation to functor calls.) The only actual difference between class “methods” and all other functors is what class own functors have implicit access to class definitions, but external functors must use qualification to access any class. Beside this, there is no principal difference between class methods and other functors.

For example, we can define a simple object (representing vectors in 3D space):

!! Vector3D (x y z) {
` length of vector `
! length = sqr(x*x + y*y + z*z)
};

Here, class Vector3D does have parametric members x, y, z and internal functor (“method”) length without parameters, calculating length of vector. This functor operates on current instance of Vector3D, expecting it to be defined externally, for example:

vec1 = Vector3D(10, 20, 30);
len1 = vec1.Vector3D!!length();

Different approach to calculating length of vector is to make functor receiving object as argument:

! length(v) = sqr(v.x*v.x + v.y*v.y + v.z*v.z)

However, this is an extremely clumsy (and inefficient) way to define such functor. This definition does exactly the same, but is more in style of AWL (and more efficient):

! length(v) = v.sqr(x*x + y*y + z*z)

Other functors of Vector3D also may prefer to to use explicit parameters for objects. For example, the following definitions may be added to the core of Vector3D class:

` add 3D vectors `
! add(v1 v2) = Vector3D(v1.x+v2.x, v1.y+v2.y, v1.z+v2.z),
` subtract 3D vectors `
! sub(v1 v2) = Vector3D(v1.x-v2.x, v1.y-v2.y, v1.z-v2.z)

Both functors expect instances of Vector3D as their arguments. As above, this definition is a bit clumsy: here are two (completely equivalent) shorter versions:

! add(v1 v2) = v1.Vector3D(x+v2.x, y+v2.y, z+v2.z),
! sub(v1 v2) = v1.Vector3D(x-v2.x, y-v2.y, z-v2.z)


! add(v1 v2) = v2.Vector3D(v1.x+x, v1.y+y, v1.z+z),
! sub(v1 v2) = v2.Vector3D(v1.x-x, v1.y-y, v1.z-z)

No matter how these functors are defined, Vector3D!!add(vec1, Vector3D(40, 50, 60)) returns Vector3D(50, 70, 90); and Vector3D!!sub(vec1, Vector3D(40, 50, 60)) returns Vector3D(-30, -30, -30). There are several built-in functors can provide information about class/object relationship. For example, class_of (Object) returns reference to class, to which Object belongs. The self functor does somewhat opposite operation: self(!myclass) returns current instance of myclass (or undef, if there is no current instance at the moment).

Classes can have constructors and/or destructors associated. Contrary to languages like Java and C++, they are NOT functors, but just an expressions (which, obviously, can be blocks of code). Class constructor is invoked after instance parametric members are initialized; its objective may be checking or modifying their values (to keep class instance internally consistent), initialization of non-parametric members, or interaction with outside world. If object instance allocates some external resources, destructor can be used to release them. Here is an example of very trivial class, but having both constructor and destructor:

!! traced (A B)
= { <: ['\n[constructor: traced (' A ',' B ')]\n' ] }
~ { <: ['\n[destructor: traced (' A ',' B ')]\n'] };

All instances of traced will report their creation and destruction. In AWL (unlike Java and some other garbage-collection languages), the destruction of objects is predictable: object is destroyed precisely when it has no references left.

Class inheritance is another object-oriented concept directly supported by AWL. Any class can have superclass (only one: there is no multiple inheritance, although this idea is considered), from which it borrows all member and functor declarations. Declaration of derived class has the following syntax:

!! [SuperClass] SubClass (params):[members] { decls };

Deriving SubClass from SuperClass means, what any instance of SubClass is an instance of SuperClass as well. Everything defined in SuperClass is inherited by SubClass, including all class members (both parametric and not) and all methods/functors. When SubClass is used in qualifier expression, internals of SuperClass are made accessible as well. Analogically, when instance of SuperClass is used in with functor, it temporarily becomes current instance not only of SubClass, but of SuperClass and all its ancestor classes as well. Functionality of class constructors and destructors is inherited too: if there are constructors for class ancestors, they are executed when class instance is created (constructors of superclasses are invoked first, constructors of subclasses last), and if class has destructors, the order of their evaluation is reverse (destructors of superclasses last, destructors of subclasses first).

Finally, because class and its ancestors normally needs parameters to initialize their instances properly, the arguments of superclass are supplied as first argument in the list of subclass. So, when creating instance of subclass, argument list looks like subclass((superarg1, ... superargN), subarg1, ... subargM). Even when superclass does not need initializers, undef () must be supplied as list of arguments for it (this may be changed in future revisions of language). When there are several class ancestors, the argument lists for them are nested: subclass initialisation looks like

subclass(((super-super-class parameters...), proper super-class parameters...), proper subclass parameters)

and so on.

Ordinary functors are not polymorphic. To add polymorphism to functors definitions, special language device – virtual functors – is needed. Normally, virtual functor has single declaration, but can have many definitions, for each subclass derived from originator class. When any virtual functor is invoked, devirtualization takes place: the correct definition of virtual (taken from subclass, to which the current instance of originator class belongs) is invoked. When subclass does not redefine virtual, it inherits its definition from superclass. If virtual remains completely undefined, its invocation has no effect, returning undef.

The (optional) declaration of virtuals (following class header, constructor and destructor) has the following syntax:

#{ virtual1 virtual2 ... virtualN }

So, it declares just names of the virtuals, originating from this class.

Definition of virtual looks like the ordinary functor definition, but name of functor must be name of virtual preceded by '#':

! #virtualX (par1 ... parN) : [loc1 ... locM] = body;

Of course, this is an error, if there is no 'virtualX' declared as virtual either in this class or any of its ancestors. It's erroneous as well to define same virtual more than once in single class.

Here is an example of simple class hierarchy, using virtuals to operate with basic geometric objects (shapes). First, we declare abstract superclass, implementing geometric “shape”, having description, perimeter and area:

` Abstract shape superclass `
!! Shape
# {
put ` output brief info about self `
perimeter ` calc perimeter of self `
area ` calc area of self `
};

Subclasses (implementing real shapes) follow this definition:

` Rectangle (A * B) `
!! [Shape] Rectangle (A B) {
! #put = <: ['Rectangle (' A ' * ' B ')'],
! #perimeter = 2*(A + B),
! #area = A * B
};

` Square (S * S) `
!! [Shape] Square (S) {
! #put = <: ['Square [' S ']'],
! #perimeter = 4*S,
! #area = S * S
};

` Circle (with radius R) `
!! [Shape] Circle (R) {
! #put = <: ['Circle [' R ']'],
! #perimeter = 2*pi(R),
! #area = pi(R*R)
};

` Triangle (with sides A, B, C) `
!! [Shape] Triangle (A B C) {
! #put = <: ['Triangle (' A ',' B ',' C ')'],
! #perimeter = A + B + C,
! #area : [p] = {
`(Heron's formula:)`
p = perimeter() / 2;
sqr(p*(p - A)*(p - B)*(p – C))
}
};

Now, we can create a list of simple geometric shapes (note using () for each shape's superclass):

Figures = (
Square ((), 5),
Rectangle ((), 6, 9),
Circle ((), 10),
Triangle ((), 5, 12, 13)
);

... and to get some information about these shapes:

Shape!!l_loop (fig, Figures, fig.{
put ();
<: ": ";
<: ("Perimeter = ", perimeter(), "; ");
<: ("Area = ", area());
<: "\n";
});

This results in the following output:

Square [5]: Perimeter = 20; Area = 25
Rectange (6 * 9): Perimeter = 30; Area = 54
Circle [10]: Perimeter = 62.831853; Area = 314.15927
Triangle (5,12,13): Perimeter = 30; Area = 30

(To be continued...)




Hosted by uCoz