afnix-guide(7)
std-us-guide - afnix programmer's guide
Description
STD-US-GUIDE
NAME
std-us-guide - afnix programmer’s guide
GETTING STARTED
AFNIX is a multi-threaded functional engine with dynamic symbol bindings that supports the object oriented paradigm. The system features a state of the art runtime engine that runs on both 32 and 64 bits platforms. The system specification offers a rich syntax that makes the functional programming a pleasant activity. When the interpreter is used interactively, text is entered on the command line and executed when a complete and valid syntactic object has been constructed. Alternatively, the interpreter can execute a source file or operates with an input stream. AFNIX is a comprehensive set of application clients, modules and services. The original distribution contains the core interpreter with additional clients like the compiler, the librarian and the debugger. The distribution contains also a rich set of modules that are dedicated to a particular domain. The basic modules are the standard i/o module, the system module and the networking module. Above modules are services. A service is another extension of the engine that provides extra functionalities with help of several modules. This hierarchy is strictly enforced in the system design and provides a clear functional separation between application domain. When looking for a particular feature, it is always a good idea to think in term of module or service functionality. AFNIX operates with a set of keywords and predicates. The engine has a native Unicode database. The set of standard objects provides support for integers, real numbers, strings, characters and boolean. Various containers like list, vector, hash table, bitset, and graphs are also available in the core distribution. The syntax incorporates the concept of lambda expression with explicit closure. Symbol scope limitation within a lambda expression is a feature called gamma expression. Form like notation with an easy block declaration is also another extension with respect to other system. The object model provides a single inheritance mechanism with dynamic symbol resolution. Special features include instance parenting, class binding instance inference and deference. Native class derivation and method override is also part of the object model with fixed class objects and forms. The engine incorporates an original regular expression engine with group matching, exact or partial match and substitution. An advanced exception engine is also provided with native run-time compatibility. AFNIX implements a true multi-threaded engine with an automatic object protection mechanism against concurrent access. A read and write locking system which operates with the thread engine is also built in the core system. The object memory management is automatic inside the core interpreter. Finally, the engine is written in C++ and provides runtime compatibility with it. Such compatibility includes the ability to instantiate C++ classes, use virtual methods and raise or catch exceptions. A comprehensive programming interface has been designed to ease the integration of foreign libraries.
First
contact
The fundamental syntactic object is a form. A form is parsed
and immediately executed by the interpreter. A form is
generally constructed with a function name and a set of
arguments. The process of executing a form is called the
evaluation. The next example illustrates one of the simplest
form which is supported by the engine. This form simply
displays the message hello world.
Hello
world
At the interpreter prompt, a form is constructed with the
special object println. The unique argument is a string
which is placed between double quotes.
(axi) println
"Hello World"
Hello World
The interpreter can be invoked to enter one or several forms interactively. The form can also be placed in a text file and the interpreted called to execute it. The als is the referred extension for a text file, but it can be anything. A simple session which executes the above file -- assuming the original file is called hello.als -- is shown below.
zsh> axi
hello.als
Hello World
In interactive mode, the interpreter waits for a form. When a form is successfully constructed, it is then immediately executed by the engine. Upon completion, the interpreter prompt is displayed and the interpreter is ready to accept a new form. A session is terminated by typing ctrl-d. Another way to use the engine is to call the compiler client called axc, and then invoke the interpreter with the compiled file. The interpreter assumes the .axc extension for compiled file and will automatically figure out which file to execute when a file name is given without an extension.
zsh> axc
hello.als
zsh> axi hello.axc
Hello World
zsh> axi hello
Hello World
The order of search is determined by a special system called the file resolver. Its behavior is described in a special chapter of this manual.
Interpreter
command
The interpreter can be invoked with several options, a file
to execute and some program arguments. The h option prints
the various interpreter options.
zsh> axi -h
usage: axi [options] [file] [arguments]
[h] print this help message
[v] print system version
[m] enable the start module
[i path] add a resolver path
[e mode] force the encoding mode
[f assert] enable assertion checks
[f nopath] do not set initial path
[f noseed] do not seed random engine
[f seed] seed random engine
The v option prints the interpreter version and operating system. The f option turns on or off some additional options like the assertion checking. The use of program arguments is illustrated later in this chapter. The i option adds a path to the interpreter resolver. Several i options can be specified. The order of search is determined by the option order. As mentioned earlier, the use of the resolver combined with the librarian is described in a specific chapter. If the initial file name to execute contains a directory path, such path is added automatically to the interpreter resolver path unless the nopath option is specified.
Interactive line editing

|
The arrow are also bound to their usual functions. Note that when using the history, a multi-line command editing access is provided by the interpreter. |

Command line
arguments
The interpreter command line arguments are stored in a
vector called argv which is part of the interp object. A
complete discussion about object and class is covered in the
class object chapter. At this time, it is just necessary to
note that a method is invoked by a name separated from the
object symbol name with a semicolon. The example below
illustrates the use of the vector argument.
# argv.als
# print the argument length and the first one
println "argument length: " (interp:argv:length)
println "first argument : " (interp:argv:get 0)
zsh> axi argv.als hello world
2
hello
Loading a
source file
The interpreter object provides also the load method to load
a file. The argument must be a valid file path or an
exception is raised. The load method returns nil. When the
file is loaded, the interpreter input, output and error
streams are used. The load operation reads one form after
another and executes them sequentially.
# load the
source file demo.als
(axi) interp:load "demo.als"
# load the compiled file demo.axc
(axi) interp:load "demo.axc"
# load whatever is found
(axi) interp:load "demo"
The load method operates with the help of the interpreter resolver. By default the source file extension is als. If the file has been compiled, the axc extension can be used instead. This force the interpreter to load the compiled version. If you are not sure, or do not care about which file is loaded, the extension can be omitted. Without extension, the compiled file is searched first. If it is not found the source file is searched and loaded.
The
compiler
The client axc is the cross compiler. It generates a binary
file that can be run across several platforms. The h option
prints the compiler options.
usage: axc
[options] [files]
[h] print this help message
[v] print version information
[i] path add a path to the resolver
[e mode] force the encoding mode
One or several files can be specified on the command line. The source file is searched with the help of the resolver. The resolver i option can be used to add a path to the resolver.
Writing
structure
The structure of file is a succession of valid syntactic
objects separated by blank lines or comments. During the
compilation or the execution process, each syntactic object
is processed one after another in a single pass. Reserved
keywords are an integral part of the writing systems. The
association of symbols and literal constitutes a form. A
form is the basic execution block in the writing system.
When the form uses reserved keyword, it is customary to
refer to it as a special form.
Character set
and comments
The writing system operates with the standard Unicode
character set. Comments starts with the character #. All
characters are consumed until the end of line. Comments can
be placed anywhere in the source file. Comments entered
during an interactive session are discarded.
Native
objects
The writing system operates mostly with objects. An object
is created upon request or automatically by the engine when
a native representation is required. To perform this task,
several native objects, namely Boolean for boolean objects,
Integer , Relatif for integer numbers, Real for
floating-point number, Complex> for complex number, Byte,
Character and String for character or string manipulation
are built inside the engine. Most of the time, a native
object is built implicitly from its lexical representation,
but an explicit representation can also be used.
const boolean
true
const integer 1999
const relatif 1234567890R
const complex 1.0+1.0i
const real 2000.0
const string "afnix"
const char ’a’
trans symbol "hello world"
trans symbol 2000
The const and trans reserve keywords are used to declare a new symbol. A symbol is simply a binding between a name and an object. Almost any standard characters can be used to declare a symbol. The const reserved keyword creates a constant symbol and returns the last evaluated object. As a consequence, nested const constructs are possible like trans b (const a 1). The trans reserved keyword declare a new transient symbol. When a symbol is marked transient, the object bound to the symbol can be changed while this is not possible with a constant symbol. Eventually, a symbol can be destroyed with the special form unref. It is worth to note that it is the symbol which is destroyed and not the object associated with it.
Stop and
resume parsing
The parsing process is stopped in the presence of the
â character (Unicode U+25C0). The parsing operation is
resumed with the â¶ character (Unicode U+25B6).
Such mechanism is useful when dealing with multi line
statements. This mechanism is also a good example of Unicode
based control characters.
Forms
An implicit form is a single line command. When a command is
becoming complex, the use of the standard form notation is
more readable. The standard form uses the ( and ) characters
to start and close a form. A form causes an evaluation. When
a form is evaluated, each symbol in the form are evaluated
to their corresponding internal object. Then the interpreter
treats the first object of the form as the object to execute
and the rest is the argument list for the calling object.
The use of form inside a form is the standard way to perform
recursive evaluation with complex expressions.
const three (+ 1 2)
This example defines a symbol which is initialized with the integer 3, that is the result of the computation (+ 1 2). The example shows also that a Polish notation is used for arithmetic. If fact, + is a built-in operator which causes the arguments to be summed (if possible). Evaluation can be nested as well as definition and assignation. When a form is evaluated, the result of the evaluation is made available to the calling form. If the result is obtained at the top level, the result is discarded.
const b (trans a
(+ 1 2))
assert a 3
assert b 3
This program illustrates the mechanic of the evaluation process. The evaluation is done recursively. The (+ 1 2) form is evaluated as 3 and the result transmitted to the form (trans a 3). This form not only creates the symbol a and binds to it the integer 3, but returns also 3 which is the result of the previous evaluation. Finally, the form (const b 3) is evaluated, that is, the symbol b is created and the result discarded. Internally, things are a little more complex, but the idea remains the same. This program illustrates also the usage of the assert keyword.
Lambda
expression
A lambda expression is another name for a function. The term
comes historically from Lisp to express the fact that a
lambda expression is analog to the concept of expression
found in the lambda calculus. There are various ways to
create a lambda expression. A lambda expression is created
with the trans reserved keyword. A lambda expression takes 0
or more arguments and returns an object. A lambda expression
is also an object by itself When a lambda expression is
called, the arguments are evaluated from left to right. The
function is then called and the object result is transmitted
to the calling form. The use of trans vs const is explain
later. To illustrate the use of a lambda expression, the
computation of an integer factorial is described in the next
example.
# declare the
factorial function
trans fact (n) (
if (== n 1) 1 (* n (fact (- n 1))))
# compute factorial 5
println "factorial 5 = " (fact 5)
This example calls for several comments. First the trans keyword defines a new function object with one argument called n. The body of the function is defined with the if special form and can be easily understood. The function is called in the next form when the println special form is executed. Note that here, the call to fact produces an integer object, which is converted automatically by the println keyword.
Block
form
The notation used in the fact program is the standard form
notation originating from Lisp and the Scheme dialect. There
is also another notation called the block form notation with
the use of the { and } characters. A block form is a
syntactic notation where each form in the block form is
executed sequentially. The form can be either an implicit or
a regular form. The fact procedure can be rewritten with the
block notation as illustrated below.
# declare the
factorial procedure
trans fact (n) {
if (== n 1) 1 (* n (fact (- n 1)))
}
# compute factorial 5
println "factorial 5 = " (fact 5)
Another way to create a lambda expression is via the lambda special form. Recall that a lambda expression is an object. So when such object is created, it can be bounded to a symbol. The factorial example could be rewritten with an explicit lambda call.
# declare the
factorial procedure
const fact (lambda (n) (
if (== n 1) 1 (* n (fact (- n 1)))))
# compute factorial 5
println "factorial 5 = " (fact 5)
Note that here, the symbol fact is a constant symbol. The use of const is reserved for the creation of gamma expression.
Gamma
expression
A lambda expression can somehow becomes very slow during the
execution, since the symbol evaluation is done within a set
of nested call to resolve the symbols. In other words, each
recursive call to a function creates a new symbol set which
is linked with its parent. When the recursion is becoming
deep, so is the path to traverse from the lower set to the
top one. There is also another mechanism called gamma
expression which binds only the function symbol set to the
top level one. The rest remains the same. Using a gamma
expression can speedup significantly the execution.
# declare the
factorial procedure
const fact (n) (
if (== n 1) 1 (* n (fact (- n 1))))
# compute factorial 5
println "factorial 5 = " (fact 5)
We will come back later to the concept of gamma expression. The use of the reserved keyword const to declare a gamma expression makes now sense. Since most function definitions are constant with one level, it was a design choice to implement this syntactic sugar. Note that gamma is a reserved keyword and can be used to create a gamma expression object. On the other hand, note that the gamma expression mechanism does not work for instance method. We will illustrate this point later in this book.
Lambda
generation
A lambda expression can be used to generate another lambda
expression. In other word, a function can generate a
function, an that capability is an essential ingredient of
the functional programming paradigm. The interesting part
with lambda expression is the concept of closed variables.
In the next example, looking at the lambda expression inside
gen, notice that the argument to the gamma is x while n is
marked in a form before the body of the gamma. This notation
indicates that the gamma should retain the value of the
argument n when the closure is created. In the literature,
you might discover a similar mechanism referenced as a
closure. A closure is simply a variable which is closed
under a certain context. When a variable is reference in a
context without any definition, such variable is called a
free variable. We will see later more programs with
closures. Note that it is the object created by the lambda
or the gamma call which is called a closure.
# a gamma which
creates a lambda
const gen (n) (
lambda (x) (n) (+ x n))
# create a function which add 2 to its argument
const add-2 (gen 2)
# call add-2 with an argument and check
println "result = " (add-2 3)
In short, a lambda expression is a function with or without closed variables, which works with nested symbol sets also called namesets. A gamma expression is a function with or without closed variable which is bounded to the top level nameset. The reserved keyword trans binds a lambda expression. The reserved keyword const binds a gamma expression. A gamma expression cannot be used as an instance method.
Multiple
arguments binding
A lambda or gamma expression can be defined to work with
extra arguments using the special args binding. During a
lambda or gamma expression execution, the special symbol
args is defined with the extra arguments passed at the call.
For example, a gamma expression with 0 formal argument and 2
actual arguments has args defined as a cons cell.
const proc-nilp
(args) {
trans result 0
for (i) (args) (result:+= i)
eval result
}
assert 3 (proc-nilp 1 2)
assert 7 (proc-nilp 1 2 4)
The symbol args can also be defined with formal arguments. In that case, args is defined as a cons cell with the remaining actual arguments.
# check with
arguments
const proc-args (a b args) {
trans result (+ a b)
for (i) (args) (result:+= i)
eval result
}
assert 3 (proc-args 1 2)
assert 7 (proc-args 1 2 4)
It is an error to specify formal arguments after args. Multiple args formal definition are not allowed. The symbol args can also be defined as a constant argument.
# check with
arguments
const proc-args (a b (const args)) {
trans result (+ a b)
for (i) (args) (result:+= i)
eval result
}
assert 7 (proc-args 1 2 4)
Nameset and
bindings
A nameset is a container of bindings between a name and
symbolic variable. We use the term symbolic variable to
denote any binding between a name and an object. There are
various ways to express such bindings. The most common one
is called a symbol. Another type of binding is an argument.
Despite the fact they are different, they share a set of
common properties, like being settable. Another point to
note is the nature of the nameset. As a matter of fact,
there is various type of namesets. The top level nameset is
called a global set and is designed to handle a large number
of symbols. In a lambda or gamma expression, the nameset is
called a local set and is designed to be fast with a small
number of symbols. The moral of this little story is to
think always in terms of namesets, no matter how it is
implemented. All namesets support the concept of parent
binding. When a nameset is created (typically during the
execution of a lambda expression), this nameset is linked
with its parent one. This means that a symbol look-up is
done by traversing all nameset from the bottom to the top
and stopping when one is found. In term of notation, the
current nameset is referenced with the special symbol
’.’. The parent nameset is referenced with the
special symbol ’..’. The top level nameset is
referenced with the symbol ’...’.
Symbol
A symbol is an object which defines a binding between a name
and an object. When a symbol is evaluated, the evaluation
process consists in returning the associated object. There
are various ways to create or set a symbol, and the
different reserved keywords account for the various nature
of binding which has to be done depending on the current
nameset state. One of the symbol property is to be const or
not. When a symbol is marked as a constant, it cannot be
modified. Note here that it is the symbol which is constant,
not the object. A symbol can be created with the reserved
keywords const or trans.
Creating a
nameset
A nameset is an object which can be constructed directly by
using the object construction notation. Once the object is
created, it can be bounded to a symbol. Here is a nameset
called example in the top level nameset.
# create a new
nameset called example
const example (nameset .)
# bind a symbol in this nameset
const example:hello "hello"
println example:hello
Qualified
name
In the previous example, a symbol is referenced in a given
nameset by using a qualified name such like example:hello. A
qualified name defines a path to access a symbol. The use of
a qualified name is a powerful notation to reference an
object in reference to another object. For example, the
qualified name .:hello refers to the symbol hello in the
current nameset. The qualified name ...:hello refers to the
symbol hello in the top level nameset. There are other use
for qualified names, like method call with an instance.
Symbol
binding
The trans reserved keyword has been shown in all previous
example. The reserved keyword trans creates or set a symbol
in the current nameset. For example, the form trans a 1 is
evaluated as follow. First, a symbol named a is searched in
the current nameset. At this stage, two situations can
occur. If the symbol is found, it is set with the
corresponding value. If the symbol is not found, it is
created in the current nameset and set. The use of qualified
name is also permitted -- and encouraged -- with trans. The
exact nature of the symbol binding with a qualified name
depends on the partial evaluation of the qualified name. For
example, trans example:hello 1 will set or create a symbol
binding in reference to the example object. If example
refers to a nameset, the symbol is bound in this nameset. If
example is a class, hello is bounded as a class symbol. In
theory, there is no restriction to use trans on any object.
If the object does not have a symbol binding capability, an
exception is raised. For example, if n is an integer object,
the form trans n:i 1 will fail. With 3 or 4 arguments, trans
defines automatically a lambda expression. This notation is
a syntactic sugar. The lambda expression is constructed from
the argument list and bounded to the specified symbol. The
rule used to set or define the symbol are the same as
described above.
# create
automatically a lambda expression
trans min (x y) (if (< x y) x y)
Constant
binding
The const reserved keyword is similar to trans, except that
it creates a constant symbol. Once the symbol is created, it
cannot be changed. This constant property is hold by the
symbol itself. When trying to set a constant symbol, an
exception is raised. The reserved keyword const works also
with qualified names. The rules described previously are the
same. When a partial evaluation is done, the partial object
is called to perform a constant binding. If such capability
does not exist, an exception is raised. With 3 or 4
arguments, const defines automatically a gamma expression.
Like trans the rule are the same except that the symbol is
marked constant.
# create
automatically a gamma expression
const max (x y) (if (> x y) x y)
Symbol
unreferencing
The unref reserved keyword removes a symbol reference in a
given context. When the context is a nameset, the object
associated with the symbol is detached from the symbol,
eventually destroyed with the symbol removed from the
nameset.
# create a
symbol number
const x 1
# unreference it
unref x
Arguments
An expression argument is similar to a symbol, except that
it is used only with function argument. The concept of
binding between a name and an object is still the same, but
with an argument, the object is not stored as part of the
argument, but rather at another location which is the
execution stack. An argument can also be constant. On the
other hand, a single argument can have multiple bindings.
Such situation is found during the same function call in two
different threads. An argument list is part of the lambda or
gamma expression declaration. If the argument is defined as
a constant argument a sub form notation is used to defined
this matter. For example, the max gamma expression is given
below.
# create a gamma
expression with const argument
const max (gamma ((const x) (const y)) (if (> x y) x
y))
A special symbols named args is defined during a lambda or gamma expression evaluation with the remaining arguments passed at the time the call is made. The symbol can be either nil or bound to a list of objects.
const proc-args
(a b) {
trans result (+ a b)
for (i) (args) (result:+= i)
eval result
}
assert 3 (proc-args 1 2)
assert 7 (proc-args 1 2 4)
Special
forms
Special forms provides are reserved keywords which are most
of the time imperative statement, as part of the writing
system. Special forms are an integral part of the writing
system and interact directly with the interpreter. In most
cases, a special forms returns the last evaluated object.
Most of the special forms are control flow statements.
If special
form
The if reserved keyword takes two or three arguments. The
first argument is the boolean condition to check. If the
condition evaluates to true the second argument is
evaluated. The form return the result of such evaluation. If
the condition evaluates to false, the third argument is
evaluated or nil is returned if it does not exist. An
interesting example which combines the if reserved keyword
and a deep recursion is the computation of the Fibonacci
sequence.
const fibo
(gamma (n) (
if (< n 2) n (+ (fibo (- n 1)) (fibo (- n 2))))
While special
form
The while reserved keyword takes 2 or 3 arguments. With 2
arguments, the loop is constructed with a condition and a
form. With 3 arguments, the first argument is an initial
condition that is executed only once. When an argument acts
as a loop condition, the condition evaluate to a boolean.
The loop body is executed as long as the boolean condition
is true. An interesting example related to integer
arithmetic with a while loop is the computation of the
greatest common divisor or gcd.
const gcd (u v)
{
while (!= v 0) {
trans r (u:mod v)
u:= v
v:= r
}
eval u
}
Note in this previous example the use of the symbol =. The qualified name u:= is in fact a method call. Here, the integer u is assigned with a value. In this case, the symbol is not changed. It is the object which is muted. In the presence of 3 arguments, the first argument is an initialization condition that is executed only once. In this mode, it is important to note that the loop introduce its own nameset. The loop condition can be used to initialize a local condition variable.
while (trans
valid (is:valid-p)) (valid) {
# do something
# adjust condition
valid:= (and (is:valid-p) (something-else))
}
Do special
form
The do reserved keyword is similar to the while reserved
keyword, except that the loop condition is evaluated after
the body execution. The syntax call is opposite to the
while. The loop can accept either 2 or 3 arguments. With 2
arguments, the first argument is the loop body and the
second argument is the exit loop condition. With 3
arguments, the first argument is the initial condition that
is executed only once.
# count the
number of digits in a string
const number-of-digits (s) {
const len (s:length)
trans index 0
trans count 0
do {
trans c (s:get index)
if (c:digit-p) (count:++)
} (< (index:++) len)
eval count
}
Loop special
form
The loop reserved keyword is another form of loop. It take
four arguments. The first is the initialize form. The second
is the exit condition. The third is the step form and the
fourth is the form to execute at each loop step. Unlike the
while and do loop, the loop special form creates its own
nameset, since the initialize condition generally creates
new symbol for the loop only.
# a simple loop
from 0 to 10
loop (trans i 0) (< i 10) (i:++) (println i)
A loop can also be designed with a Counter object. In this case, a counter is created with an initial and final count values. The counter step-p method can then be used to run the loop
# a counter from
0 to 10
trans cntr (Counter 10)
# a simple loop from 1 to 10
loop (cntr:step-p) (println cntr)
In this example, the counter prints from 1 to 10 since the counter is designed to operate from 0 to 9, and the println function is called after the step-p predicate.
Switch
special form
The switch reserved keyword is a condition selector. The
first argument is the switch selector. The second argument
is a list of various value which can be matched by the
switch value. A special symbol called else can be used to
match any value.
# return the
primary color in a rgb
const get-primary-color (color value) (
switch color (
("red" (return (value:substr 0 2)))
("green" (return (value:substr 2 4)))
("blue" (return (value:substr 4 6)))
))
Return
special form
The return reserved keyword indicates an exceptional
condition in the flow of execution within a lambda or gamma
expression. When a return is executed, the associated
argument is returned and the execution terminates. If return
is used at the top level, the result is simply
discarded.
# initialize a
vector with a value
const vector-init (length value) {
# treat nil vector first
if (<= length 0) return (Vector)
trans result (Vector)
do (result:add value) (> (length:--) 0)
}
Eval and
protect
The eval reserved keyword forces the evaluation of the
object argument. The reserved keyword eval is typically used
in a function body to return a particular symbol value. It
can also be used to force the evaluation of a protected
object. In many cases, eval is more efficient than return.
The protect reserved keyword constructs an object without
evaluating it. Typically when used with a form, protect
return the form itself. It can also be used to prevent a
symbol evaluation. When used with a symbol, the symbol
object itself is returned.
const add
(protect (+ 1 2))
(eval add)
Note that in the preceding example that the evaluation will return a lambda expression which is evaluated immediately and which return the integer 3.
Assert
special form
The assert reserved keyword check for equality between the
two arguments and abort the execution in case of failure. By
default, the assertion checking is turn off, and can be
activated with the command option f assert. Needless to say
that assert is used for debugging purpose.
assert true
(> 2 0)
assert 0 (- 2 2)
assert "true" (String true)
Block special
form
The block reserved keyword executes a form in a new local
set. The local set is destroyed at the completion of the
execution. The block reserved keyword returns the value of
the last evaluated form. Since a new local set is created,
any new symbol created in this nameset is destroyed at the
completion of the execution. In other word, the block
reserved keyword allows the creation of a local scope.
trans a 1
block {
assert a 1
trans a (+ 1 1)
assert a 2
assert ..:a 1
}
assert 1 a
Built-in
objects
Several built-in objects and built-in operators for
arithmetic and logical operations are also integrated in the
writing system. The Integer and Real classes are primarily
used to manipulate numbers. The Boolean class is used to for
boolean operations. Other built-in objects include Character
and String. The exact usage of these objects is described in
the next chapter.
Arithmetic
operations
Support for the arithmetic operations is provided with the
standard operator notation. Normally, these operators will
tolerate various object type mixing and the returned value
will generally be bound to an object that provides the
minimum loss of information. Most of the operations are done
with the +, -, * and / operators.
(+ 1 2)
(- 1)
(* 3 5.0)
(/ 4.0 2)
Logical
operations
The Boolean class is used to represent the boolean value
true and false. These last two symbols are built-in in the
interpreter as constant symbols. There are also special
forms like not, and and or. Their usage is self
understandable.
not true
and true (== 1 0)
or (< -1 0) (> 1 0)
Predicates

For example, one can write a function which returns true if the argument is a number, that is, an integer or a real number.
# return true if
the argument is a number
const number-p (n) (
or (integer-p n) (real-p n))

Finally, for each object, a predicate is also associated. For example, cons-p is the predicate for the Cons object and vector-p is the predicate for the Vector object. Another issue related to evaluation, is to decide whether or not an object can be evaluated. The predicate eval-p which is a special form is designed to answer this question. Furthermore, the eval-p predicate is useful to decide whether or not a symbol is defined or if a qualified name can be evaluated.
assert true
(eval-p .)
assert false (eval-p an-unknown-symbol)
Class and
instance
Classes and instances are the fundamental objects that
provide support for the object oriented paradigm. A class is
a nameset which can be bounded automatically when an
instance of that class is created. The class model is
sloppy. Compared to other systems, there is no need to
declare the data members for a particular class. Data
members are created during the instance construction. An
instance can also be created without any reference to a
class. Methods can be bound to the class or the instance or
both. An instance can also be muted during the execution
process.
Class and
members
A class is declared with the reserved keyword class. The
resulting object acts like a nameset and it is possible to
bind symbol to it.
# create a class
object
const Circle (class)
const Circle:PI 3.1415926535
# access by qualified name
println Circle:PI
In the previous example, the symbol Circle is created as a class object. With the help of a qualified name, the symbol PI is created inside the class nameset. In this case, the symbol PI is invariant with respect to the instance object. A form can also be bound to the class nameset. In both cases, the symbol or the form is accessed with the help of a qualified name.
Instances
An instance of a class is created like any built-in object.
If a method called preset is defined for that class, the
method is used to initialize the instance.
# create a class
const Circle (class)
trans Circle:preset (r) {
const this:radius (r:clone)
}
# create a radius 1 circle
const c (Circle 1)
This example calls for several comments. First the preset lambda expression is bound to the class. Since preset is a reserved name for the class object, the form is automatically executed at the instance construction. Second, note that the instance data member radius is created by the lambda expression and another reserved keyword called this is used to reference the instance object as it is customary with other programming systems.
Instance
method
When a lambda expression is bound to the class or the
instance, that lambda can be invoked as an instance method.
When an instance method is invoked, the instance nameset is
set as the parent nameset for that lambda. This is the main
reason why a gamma expression cannot be used as an instance
method. Therefore, the use of the reserved keyword this is
not recommended in a gamma expression, although it is
perfectly acceptable to create a symbol with such name.
# create a
perimeter method
trans Circle:perimeter nil (
* (* 2.0 Circle:PI) this:radius)
# call the method with our circle
trans p (c:perimeter)
It must be clear that the perimeter symbol defines a method at the class level. It is perfectly acceptable to define a methods at the instance level. Such method is called a specialized method.
Miscellaneous features
Iteration
An iteration facility is provided for some objects known as
iterable objects. The Cons, List and Vector are typical
iterable objects. There are two ways to iterate with these
objects. The first method uses the for reserved keyword. The
second method uses an explicit iterator which can be
constructed by the object.
# compute the
scalar product of two vectors
const scalar-product (u v) {
trans result 0
for (x y) (u v) (result:+= (* x y))
eval result
}
The for reserved keyword iterate on both object u and v. For each iteration, the symbol x and y are set with their respective object value. In the example above, the result is obtained by summing all intermediate products.
# test the
scalar product function
const v1 (Vector 1 2 3)
const v2 (Vector 2 4 6)
(scalar-product v1 v2)
The iteration can be done explicitly by creating an iterator for each vectors and advancing steps by steps.
# scalar product
with explicit iterators
const scalar-product (u v) {
trans result 0
trans u-it (u:iterator)
trans v-it (v:iterator)
while (u:valid-p) {
trans x (u:get-object)
trans y (v:get-object)
result:+= (* x y)
u:next
v:next
}
eval result
}
In the example above, two iterators are constructed for both vectors u and v. The iteration is done in a while loop by invoking the valid-p predicate. The get-object method returns the object value at the current iterator position.
Exception
An exception is an unexpected change in the execution flow.
The exception model is based on a mechanism which throws the
exception to be caught by a handler. The mechanism is also
designed to be compatible with the native implementation. An
exception is thrown with the special form throw. When an
exception is thrown, the normal flow of execution is
interrupted and an object used to carry the exception
information is created. Such exception object is propagated
backward in the call stack until an exception handler catch
it. The special form try executes a form and catch an
exception if one has been thrown. With one argument, the
form is executed and the result is the result of the form
execution unless an exception is caught. If an exception is
caught, the result is the exception object. If the exception
is a native one, the result is nil.
try (+ 1 2)
try (throw)
try (throw "hello")
try (throw "hello" "world")
try (throw "hello" "world"
"folks")
The exception mechanism is also designed to install an exception handler and eventually retrieve some information from the exception object. The reserved symbol what can be used to retrieve some exception information.
# protected
factorial
const fact (n) {
if (not (integer-p n))
(throw "number-error" "invalid
argument")
if (== n 0) 1 (* n (fact (- n 1)))
}
# exception handler
const handler nil {
errorln what:eid ’,’ what:reason
}
(try (fact 5) handler)
(try (fact "hello") handler)
The special symbol what stores the necessary information about the place that generated the exception. Most of the time, the qualified name what:reason or what:about is used.The only difference is that what:about contains the file name and line number associated with the reason that generated the exception.
Regular
Expressions
A regular expression or regex is an object which is used to
match certain text patterns. Regular expressions are built
implicitly by the parser with the use of the [ and ]
characters. Special class of characters are defined with the
help of the $ character. For example, $d is the class of
character digits as defined by the Unicode consortium.
Different regular expression can be grouped by region to be
matched as indicated in the example below.
if (== (const re
[($d$d):($d$d)]) "12:31") {
trans hr (re:get 0)
trans mn (re:get 1)
}
In the previous example, a regular expression object is bound to the symbol re. The regex contains two groups. The call to the operator == returns true if the regex matches the argument string. The get method can be used to retrieve the group by index.
Delayed
evaluation
The special form delay creates a special object called a
promise which records the form to be later evaluated. The
special form force causes a promise to be evaluated.
Subsequent call with force will produce the same result.
trans y 3
const l ((lambda (x) (+ x y)) 1)
assert 4 (force l)
trans y 0
assert 4 (force l)
Threads
The interpreter provides a powerful mechanism which allows
the concurrent execution of forms and the synchronization of
shared objects. The engine provides supports the creation
and the synchronization of threads with a native object
locking mechanism. During the execution, the interpreter
wait until all threads are completed. A threads is created
with the reserved keyword launch. In the presence of several
threads, the interpreter manages automatically the shared
objects and protect them against concurrent access.
# shared
variable access
const var 0
const decr nil (while true (var:= (- var 1)))
const incr nil (while true (var:= (+ var 1)))
const prtv nil (while true (println "value = "
var))
# start 3 threads
launch (prtv)
launch (decr)
launch (incr)
Form
synchronization
Although, the engine provides an automatic synchronization
mechanism for reading or writing an object, it is sometimes
necessary to control the execution flow. There are basically
two techniques to do so. First, protect a form from being
executed by several threads. Second, wait for one or several
threads to complete their task before going to the next
execution step. The reserved keyword sync can be used to
synchronize a form. When a form, is synchronized, the engine
guarantees that only one thread will execute this form.
const
print-message (code mesg) (
sync {
errorln "error : " code
errorln "message: " mesg
}
)
The previous example create a gamma expression which make sure that both the error code and error message are printed in one group, when several threads call it.
Thread
completion
The other piece of synchronization is the thread completion
indicator. The thread descriptor contains a method called
wait which suspend the calling thread until the thread
attached to the descriptor has been completed. If the thread
is already completed, the method returns immediately.
# simple flag
const flag false
# simple shared tester
const ftest (bval) (flag:= bval)
# run the thread and wait
const thr (launch (ftest true))
thr:wait
assert true flag
This example is taken from the test suites. It checks that a boolean variable is set in a thread. Note the use of the wait method to make sure the thread has completed before checking for the flag value. It is also worth to note that wait is one of the method which guarantees that a thread result is valid. Another use of the wait method can be made with a vector of thread descriptors when one wants to wait until all of them have completed.
# shared vector
of threads descriptors
const thr-group (Vector)
# wait until all threads in the group are finished
const wait-all nil (for (thr) (thr-group) (thr:wait))
Condition
variable
A condition variable is another mechanism to synchronize
several threads. A condition variable is modeled with the
Condvar object. At construction, the condition variable is
initialized to false. A thread calling the wait method will
block until the condition becomes true. The mark method can
be used by a thread to change the state of a condition
variable and eventually awake some threads which are blocked
on it. The use of condition variable is particularly
recommended when one need to make sure a particular thread
has been doing a particular task.
Asynchronous
evaluation
The special form future creates a special object called a
future which is used to evaluate an object asynchronously.
The evalution starts with the help of the force special
form. The sync special form can be used to synchronise with
the future.
trans f (future
1)
force f
The
interpreter object
The interpreter can also be seen as an object. As such, it
provides several special symbols and forms. For example, the
symbol argv is the argument vector. The symbol library is an
interpreter method that loads a library. A complete
description of the interpreter object is made in a special
chapter of this book.
LITERALS
This chapters covers in detail the literals objects used to manipulate numbers and strings. First the integer, relatif, real and complex numbers are described. There is a broad range of methods for these three objects that support numerical computation. As a second step, string and character objects are described. Many examples show the various operations which can be used as automatic conversion between one type and another. Finally, the boolean object is described. These objects belongs to the class of literal objects, which are objects that have a string representation. A special literal object known as regular expression or regex is also described at the end of this chapter.
Integer
number
The fundamental number representation is the Integer. The
integer is a 64 bits signed 2’s complement number.
Even when running with a 32 bits machine, the 64 bits
representation is used. If a larger representation is
needed, the Relatif object might be more appropriate. The
Integer object is a literal object that belongs to the
number class.
Integer
format
The default literal format for an integer is the decimal
notation. The minus sign (without blank) indicates a
negative number. Hexadecimal and binary notations can also
be used with prefix 0x and 0b. The underscore character can
be used to make the notation more readable.
const a 123
trans b -255
const h 0xff
const b 0b1111_1111
Integer number are constructed from the literal notation or by using an explicit integer instance. The Integer class offers standard constructors. The default constructor creates an integer object and initialize it to 0. The other constructors take either an integer, a real number, a character or a string.
const a
(Integer)
const b (Integer 2000)
const c (Integer "23")
When the hexadecimal or binary notation is used, care should be taken to avoid a negative integer. For example, 0x_8000_0000_0000_0000 is the smallest negative number. This number exhibits also the strange property to be equal to its negation since with 2’s complement, there is no positive representation.
Integer
arithmetic
Standard arithmetic operators are available as built-in
operators. The usual addition +, multiplication * and
division / operate with two arguments. The subtraction -
operates with one or two arguments.
+ 3 4
- 3 4
- 3
* 3 4
/ 4 2
As a built-in object, the Integer object offers various methods for built-in arithmetic which directly operates on the object. The following example illustrates these methods.
trans i 0
i:++
i:--
i:+ 4
i:= 4
i:- 1
i:* 2
i:/ 2
i:+= 1
i:-= 1
i:*= 2
i:/= 2
As a side effect, these methods allows a const symbol to be modified. Since the methods operates on an object, they do not modify the state of the symbol. Such methods are called mutable methods.
const i 0
i:= 1
Integer
comparison
The comparison operators works the same. The only difference
is that they always return a Boolean result. The comparison
operators are namely equal ==, not equal !=, less than <,
less equal <=, greater > and greater equal >=.
These operators take two arguments.
== 0 1
!= 0 1
Like the arithmetic methods, the comparison operators are supported as object methods. These methods return a Boolean object.
i:= 1
i:== 1
i:!= 0
Integer
calculus
Armed with all these functions, it is possible to develop a
battery of functions operating with numbers. As another
example, we revisit the Fibonacci sequence as demonstrated
in the introduction chapter. Such example was terribly slow,
because of the double recursion. Another method suggested by
Springer and Friedman uses two functions to perform the same
job.
const fib-it
(gamma (n acc1 acc2) (
if (== n 1) acc2 (fib-it (- n 1) acc2 (+ acc1 acc2))))
const fiboi (gamma (n) (
if (== n 0) 0 (fib-it n 0 1)))
This later example is by far much faster, since it uses only one recursion. Although, it is no the fastest way to write it, it is still an elegant way to write complex functions.
Other Integer
methods
The Integer class offers other convenient methods. The odd-p
and even-p are predicates. The mod take one argument and
returns the modulo between the calling integer and the
argument. The abs methods returns the absolute value of the
calling integer.
i:even-p
i:odd-p
i:mod 2
i:= -1
i:abs
i:to-string
The Integer object is a literal object and a number object. As a literal object, the to-string and to-literal methods are provided to obtain a string representation for the integer object. Although the to-string method returns a string representation of the calling integer, the to-literal method returns a parsable string. Strictly speaking for an integer, there is no difference between a string representation and a literal representation. However, this is not true for other objects.
(axi) const i
0x123
(axi) println (i:to-string)
291
(axi) println (i:to-literal)
291
As a number object, the integer number can also be represented in hexadecimal format. The to-hexa and to-hexa-strign methods are designed to obtained such representation. In the first form, the to-hexa method return a literal hexadecimal string representation with the appropriate prefix while the second one does not.
(axi) const i
0x123
(axi) println (i:to-hexa)
0x123
(axi) println (i:to-hexa-string)
123
Relatif
number
A relatif or big number is an integer with infinite
precision. The Relatif class is similar to the Integer class
except that it works with infinitely long number. The
relatif notation uses a r or R suffix to express a relatif
number versus an integer one. The Relatif object is a
literal object that belongs to the number class. The
predicate associated with the Relatif object is
relatif-p.
const a 123R
trans b -255R
const c 0xffR
const d 0b1111_1111R
const e (Relatif)
const f (Relatif 2000)
const g (Relatif "23")
Relatif
operations
Most of the Integer class operations are supported by the
Relatif object. The only difference is that there is no
limitation on the number size. This naturally comes with a
computational price. An amazing example is to compute the
biggest know prime Mersenne number. The world record
exponent is 6972593. The number is therefore:
const i 1R
const m (- (i:shl 6972593) 1)
This number has 2098960 digits. You can use the println method if you wish, but you have been warned...
Real
number
The real class implements the representation for floating
point number. The internal representation is machine
dependent, and generally follows the double representation
with 64 bits as specified by the IEEE 754-1985 standard for
binary floating point arithmetic. All integer operations are
supported for real numbers. The Real object is a literal
object that belongs to the number class.
Real
format
The parser supports two types of literal representation for
real number. The first representation is the dotted decimal
notation. The second notation is the scientific
notation.
const a 123.0 #
a positive real
const b -255.5 # a negative real
const c 2.0e3 # year 2000.0
Real number are constructed from the literal notation or by using an explicit real instance. The Real class offers standard constructors. The default constructor creates a real number object and initialize it to 0.0. The other constructors takes either an integer, a real number, a character or a string.
Real
arithmetic
The real arithmetic is similar to the integer one. When an
integer is added to a real number, that number is
automatically converted to a real. Ultimately, a pure
integer operation might generate a real result.
+ 1999.0 1 #
2000.0
+ 1999.0 1.0 # 2000.0
- 2000.0 1 # 1999.0
- 2000.0 1.0 # 1999.0
* 1000 2.0 # 2000.0
* 1000.0 2.0 # 2000.0
/ 2000.0 2 # 1000.0
/ 2000.0 2.0 # 1000.0
Like the Integer object, the Real object has arithmetic built-in methods.
trans r 0.0 #
0.0
r:++ # 1.0
r:-- # 0.0
r:+ 4.0 # 4.0
r:= 4.0 # 4.0
r:- 1.0 # 3.0
r:* 2.0 # 8.0
r:/ 2.0 # 2.0
r:+= 1.0 # 5.0
r:-= 1.0 # 4.0
r:*= 2.0 # 8.0
r:/= 2.0 # 4.0
Real
comparison
The comparison operators works as the integer one. As for
the other operators, an implicit conversion between an
integer to a real is done automatically.
== 2000 2000 #
true
!= 2000 1999 # true
Comparison methods are also available for the Real object. These methods take either an integer or a real as argument.
r:= 1.0 # 1.0
r:== 1.0 # true
r:!= 0.0 # true
Complex
number
A complex number is another type of number used to solve
compex operations that are not possible with rela number. A
complex number is made of a real part and an imaginary part.
When the rela part is ommitted, one has a pure imginary
number. The standard notation is to write the imaginary part
with the letter i giving a litteral notation that ressembles
an arithmetic operation.
trans z 1.0+1.0i
trans z (Complex 1.0 1.0)
trans z "1.0i"
Complex
operations
Most of the real class operations are supported by the
Complex object including addition, substraction and
multiplication. A special case arises with the square root
of negative number.
A complex
example
One of the most interesting point with functional
programming language is the ability to create complex
function. For example let’s assume we wish to compute
the value at a point x of the Legendre polynomial of order
n. One of the solution is to encode the function given its
order. Another solution is to compute the function and then
compute the value.
# legendre
polynomial order 0 and 1
const lp-0 (gamma (x) 1)
const lp-1 (gamma (x) x)
# legendre polynomial of order n
const lp-n (gamma (n) (
if (> n 1) {
const lp-n-1 (lp-n (- n 1))
const lp-n-2 (lp-n (- n 2))
gamma (x) (n lp-n-1 lp-n-2)
(/ (- (* (* (- (* 2 n) 1) x)
(lp-n-1 x))
(* (- n 1) (lp-n-2 x))) n)
} (if (== n 1) lp-1 lp-0)
))
# generate order 2 polynomial
const lp-2 (lp-n 2)
# print lp-2 (2)
println "lp2 (2) = " (lp-2 2)
Note that the computation can be done either with integer or real numbers. With integers, you might get some strange results anyway, but it will work. Note also how the closed variable mechanism is used. The recursion capture each level of the polynomial until it is constructed. Note also that we have here a double recursion.
Other real
methods
The real numbers are delivered with a battery of functions.
These include the trigonometric functions, the logarithm and
couple others. Hyperbolic functions like sinh, cosh, tanh,
asinh, acosh and atanh are also supported. The square root
sqrt method return the square root of the calling real. The
floor and ceiling returns respectively the floor and the
ceiling of the calling real.
const r0 0.0 #
0.0
const r1 1.0 # 1.0
const r2 2.0 # 2.0
const rn -2.0 # -2.0
const rq (r2:sqrt) # 1.414213
const pi 3.1415926 # 3.141592
rq:floor # 1.0
rq:ceiling # 2.0
rn:abs # 2.0
r1:log # 0.0
r0:exp # 1.0
r0:sin # 0.0
r0:cos # 1.0
r0:tan # 0.0
r0:asin # 0.0
pi:floor # 3.0
pi:ceiling # 4.0
Accuracy and
formatting
Real numbers are not necessarily accurate, nor precise. The
accuracy and precision are highly dependent on the hardware
as well as the nature of the operation being performed. In
any case, never assume that a real value is an exact one.
Most of the time, a real comparison will fail, even if the
numbers are very close together. When comparing real
numbers, it is preferable to use the ?= operator. Such
operator result is bounded by the internal precision
representation and will generally return the desired value.
The real precision is an interpreter value which is set with
the set-absolute-precision method while the
get-absolute-precision returns the interpreter precision.
There is also a set-relative-precision and
get-relative-precision methods used for the definition of
relative precision. By default, the absolute precision is
set to 0.00001 and the relative precision is set to
1.0E-8.
interp:set-absolute-precision
0.0001
const r 2.0
const s (r:sqrt) # 1.4142135
(s:?= 1.4142) # true
Real number formatting is another story. The format method takes a precision argument which indicates the number of digits to print for the decimal part. Note that the format command might round the result as indicated in the example below.
const pi
3.1415926535
pi:format 3 # 3.142
If additional formatting is needed, the Stringfill-left and fill-right methods can be used.
const pi
3.1415926535 # 3.1415926535
const val (pi:format 4) # 3.1416
println (val:fill-left ’0’ 9) # 0003.1416
Number
object
The Integer, Relatif and Real objects are all derived from
the Number object which is a Literal object. As such, the
predicate number-p is the right mechanism to test an object
for a number. The class also provides the basic mechanism to
format the number as a string. For integer and relatif, the
hexadecimal representation can be obtained by the to-hexa
and to-hexa-string methods. For integer and real numbers,
the format method adjusts the final representation with the
precision argument as indicated before. It is worth to note
that a formatted integer gets automatically converted into a
real representation.
Character
The Character object is another built-in object. A character
is internally represented by a quad by using a 31 bit
representation as specified by the Unicode standard and ISO
10646.
Character
format
The standard quote notation is used to represent a
character. In that respect, there is hare a substantial
difference with other functional language where the quote
protect a form.
const LA01
’a’ # the character a
const ND10 ’0’ # the digit 0
All characters from the Unicode codeset are supported by the AFNIX engine. The characters are constructed from the literal notation or by using an explicit character instance. The Character class offers standard constructors. The default constructor creates a null character. The other constructors take either an integer, a character or a string. The string can be either a single quoted character or the literal notation based on the U+ notation in hexadecimal. For example, U+40 is the @ character while U+3A3 is the sigma capital letter Σ.
const nilc
(Character) # null character
const a (Character ’a’) # a
const 0 (Character 48) # 0
const mul (Character "*") # *
const div (Character "U+40") # @
Character
arithmetic
A character is like an integer, except that it operates in
the range 0 to 0x7FFFFFFF. The character arithmetic is
simpler compared to the integer one and no overflow or
underflow checking is done. Note that the arithmetic
operations take an integer as an argument.
+
’a’ 1 # ’b’
- ’9’ 1 # ’8’
Several Character object methods are also provided for arithmetic operations in a way similar to the Integer class.
trans c
’a’ # ’a’
c:++ # ’b’
trans c ’9’ # ’9’
c:-- # ’8’
c:+ 1 # ’9’
c:- 9 # ’0’
Character
comparison
Comparison operators are also working with the Character
object. The standard operators are namely equal ==, not
equal !=, less than <, less equal <=, greater > and
greater equal >=. These operators take two arguments.
==
’a’ ’b’ # false
!= ’0’ ’1’ # true
Other
character methods
The Character object comes with additional methods. These
are mostly conversion methods and predicates. The to-string
method returns a string representation of the calling
character. The to-integer method returns an integer
representation the calling character. The predicates are
alpha-p, digit-p, blank-p, eol-p, eos-p and nil-p.
const LA01
’a’ # ’a’
const ND10 ’0’ # ’0’
LA01:to-string # "a"
LA01:to-integer # 97
LA01:alpha-p # true
ND10:digit-p # true
String
The String object is one of the most important built-in
object in the AFNIX engine. Internally, a string is a vector
of Unicode characters. Because a string operates with
Unicode characters, care should be taken when using
composing characters.
String
format
The standard double quote notation is used to represent
literally a string. Standard escape sequences are also
accepted to construct a string.
const hello "hello"
Any literal object can be used to construct a string. This means that integer, real, boolean or character objects are all valid to construct strings. The default constructor creates a null string. The string constructor can also takes a string.
const nils
(String) # ""
const one (String 1) # "1"
const a (String ’a’) # "a"
const b (String true) # "true"
String
operations
The String object provides numerous methods and operators.
The most common ones are illustrated in the example below.
The length methods returns the total number of characters in
the string object. It is worth to note that this number is
not necessarily the number of printed characters since some
characters might be combining characters used, for example,
as diacritics. The non-combining-length method might be more
adapted to get the number of printable characters.
const h
"hello"
h:length # 5
h:get 0 # ’h’
h:== "world" # false
h:!= "world" # true
h:+= " world" # "hello world"
The sub-left and sub-right methods return a sub-string, given the position index. For sub-left, the index is the terminating index, while sub-right is the starting index, counting from 0.
# example of
sub-left method
const msg "hello world"
msg:sub-left 5 # "hello"
msg:sub-right 6 # "world"
The strip, strip-left and strip-right are methods used to strip blanks and tabs. The strip method combines both strip-left and strip-right.
# example of
strip method
const str " hello world "
println (str:strip) # "hello world"
The split method returns a vector of strings by splitting the string according to a break sequence. By default, the break sequence is the blank, tab and newline characters. The break sequence can be one or more characters passed as one single argument to the method.
# example of
split method
const str "hello:world"
const vec (str:split ":" # "hello"
"world")
println (vec:length) # 2
The fill-left and fill-right methods can be used to fill a string with a character up to a certain length. If the string is longer than the length, nothing happens.
# example of
fill-left method
const pi 3.1415926535 # 3.1415926535
const val (pi:format 4) # 3.1416
val:fill-left ’0’ 9 # 0003.1416
Conversion
methods
The case conversion methods are the standard to-upper and
to-lower methods. The method operates with the internal
Unicode database. As a result, the conversion might change
the string length. Other conversion methods related to the
Unicode representation are also available. These are rather
technical, but can be used to put the string in a normal
form which might be suitable for comparison. Such conversion
always uses the Unicode database normal form
representation.
# example of
case conversion
const str "hello world"
println (str:to-upper) # HELLO WORLD
String hash
value
The hashid method is a method that computes the hash value
of a string. The value depends on the target machine and
will change between a 32 bits and a 64 bits machine. Example
example 0203.als illustrates the computation of a hash value
for our favorite test string.
# test our
favorite string
const hello "hello world"
hello:hashid # 1054055120
The algorithm used by the engine is shown as an example below. As a side note, it is recommended to print the shift amount in the program. One may notice, that the value remains bounded by 24. Since we are xoring the final value, it does illustrate that the algorithm is design for a 32 bits machine. With a 64 bits machine the algorithm is slightly modified to use the extra space. This also means that the hashid value is not portable across platforms.
# compute string
hashid
const hashid (s) {
const len (s:length)
trans cnt 0
trans val 0
trans sht 17
do {
# compute the hash value
trans i (Integer (s:get cnt))
val:= (val:xor (i:shl sht))
# adjust shift index
if (< (sht:-= 7) 0) (sht:+= 24)
} (< (cnt:++) len)
eval val
}
Regular
expression
A regular expression or regex is a special literal object
designed to describe a character string in a compact form
with regular patterns. A regular expression provides a
convenient way to perform pattern matching and filed
extraction within a character string.
Regex
syntax
A regular expression is defined with a special Regex object.
A regular expression can be built implicitly or explicitly
with the use of the Regex object. The regex syntax uses the
[ and ] characters as block delimiters. When used in a
source file, the parser automatically recognizes a regex and
built the object accordingly. The following example shows
two equivalent methods for the same regex expression.
# syntax
built-in regex
(== [$d+] 2000) # true
# explicit built-in regex
(== (Regex "$d+") 2000) # true
In its first form, the [ and ] characters are used as syntax delimiters. The lexical analyzer automatically recognizes this token as a regex and built the equivalent Regex object. The second form is the explicit construction of the Regex object. Note also that the [ and ] characters are also used as regex block delimiters.
Regex characters and meta-characters
|
Any character, except the one used as operators can be used in a regex. The $ character is used as a meta-character -- or control character -- to represent a particular set of characters. For example, [hello world] is a regex which match only the "hello world" string. The [$d+] regex matches one or more digits. The following meta characters are built-in in the regex engine. |

The uppercase version is the complement of the corresponding lowercase character set. A character which follows a $ character and that is not a meta character is treated as a normal character. For example $[ is the [ character. A quoted string can be used to define character matching which could otherwise be interpreted as control characters or operator. A quoted string also interprets standard escaped sequences but not meta characters.
(== [$d+] 2000)
# true
(== ["$d+"] 2000) # false
Combining alphanumerical characters can generate surprising result when used with Unicode string. Combining alphanumeric characters are alphanumeric characters and non spacing combining mark as defined by the Unicode consortium. In practice, the combining marks are the diacritics used with regular letter, such like the accents found in the western languages. Because the writing system uses a canonical decomposition for representing the Unicode string, it turns out that the printed string is generally represented with more bytes, making the string length longer than it appears.
Regex
character set
A character set is defined with the < and >
characters. Any enclosed character defines a character set.
Note that meta characters are also interpreted inside a
character set. For example, <$d+-> represents any
digit or a plus or minus. If the first character is the ˆ
character in the character set, the character set is
complemented with regards to its definition.
Regex blocks
and operators
The [ and ] characters are the regex sub-expressions
delimiters. When used at the top level of a regex
definition, they can identify an implicit object. Their use
at the top level for explicit construction is optional. The
following example is strictly equivalent.
# simple real
number check
const real-1 (Regex "$d*.$d+")
# another way with [] characters
const real-2 (Regex "[$d*.$d+]")
Sub-expressions can be nested -- that’s their role -- and combined with operators. There is no limit in the nesting level.
# pair of digit
testing
(== [$d$d[$d$d]+] 2000) # true
(== [$d$d[$d$d]+] 20000) # false
|
The following unary operators can be used with single character, control characters and sub-expressions. |

Alternation is an operator which work with a secondary expression. Care should be taken when writing the right sub-expression. For example the following regex [$d|hello] is equivalent to [[$d|h]ello]. In other word, the minimal first sub-expression is used when compiling the regex.
Grouping
Groups of sub-expressions are created with the ( and )
characters. When a group is matched, the resulting
sub-string is placed on a stack and can be used later. In
this respect, the regex engine can be used to extract
sub-strings. The following example extracts the month, day
and year from a particular date format:
[($d$d):($d$d):($d$d$d$d)]. This regex assumes a date in the
form mm:dd:yyyy.
if (== (const re
[($d$d):($d$d)]) "12:31") {
trans hr (re:get 0)
trans mn (re:get 1)
}
Grouping is the mechanism to retrieve sub-strings when a match is successful. If the regex is bound to a symbol, the get method can be used to get the sub-string by index.
Regex
object
Although a regex can be built implicitly, the Regex object
can also be used to build a new regex. The argument is a
string which is compiled during the object construction. A
Regex object is a literal object. This means that the
to-string method is available and that a call to the println
special form will work directly.
const re (Regex
"$d+")
println re # $d+
println re:to-string # [$d+]
Regex
operators
The == and != operators are the primary operators to perform
a regex match. The == operator returns true if the regex
matches the string argument from the beginning to the end of
string. Such operator implies the begin and end of string
anchoring. The < operator returns true if the regex
matches the string or a sub-string or the string
argument.
Regex
methods
The primary regex method is the get method which returns by
index the sub-string when a group has been matched. The
length method returns the number of group match.
if (== (const re
[($d$d):($d$d)]) "12:31") {
re:length # 2
re:get 0 # 12
re:get 1 # 31
}
The match method returns the first string which is matched by the regex.
const regex
[$d+]
regex:match "Happy new year 2000" # 2000
The replace method any occurrence of the matching string with the string argument.
const regex
[$d+]
regex:replace "Hello year 2000" "3000" #
hello year 3000
Argument
conversion
The use of the Regex operators implies that the arguments
are evaluated as literal object. For this reason, an
implicit string conversion is made during such operator
call. For example, passing the integer 12 or the string
"12" is strictly equivalent. Care should be taken
when using this implicit conversion with real numbers.
CONTAINER OBJECTS
This chapter covers the standard container objects and more specifically, iterable objects such like Cons, List and Vector. Special objects like Fifo, Queue and Bitset are treated at the end of this chapter. Although the name container is sufficient enough to describe the object functionality, it is clear that a container is more than a simple object reservoir. In particular, the choice of a container object is often associated to the underlying algorithm used to store the object. For example, a vector is appropriate when storing by index is important. If the order of storage must be preserved, then a fifo object might be more appropriate. In any case, the choice of a container is always a question of compromise, so is the implementation.
Cons
object
Originally, a Cons object or cons cell have been the
fundamental object of the Lisp or Scheme machine. The cons
cell is the building block for list and is similar in some
respect to the cons cell found in traditional functional
programming language. A Cons object is a simple element used
to build linked list. The cons cell holds an object and a
pointer to the next cons cell. The cons cell object is
called car and the next cons cell is called the cdr. This
original Lisp notation is maintained here for the sake of
tradition. Although a cons cell is the building block for
single linked list, the cell itself is not a list object.
When a list object is needed, the List double linked list
object might be more appropriate.
Cons cell
constructors
The default constructor creates a cons cell those car is
initialized to the nil object. The constructor can also take
one or several objects.
const nil-cons
(Cons)
const lst-cons (Cons 1 ’a’
"hello")
The constructor can take any kind of objects. When all objects have the same type, the result list is said to be homogeneous. If all objects do not have the same type, the result list is said to be heterogeneous. List can also be constructed directly by the parser. Since all internal forms are built with cons cell, the construction can be achieved by simply protecting the form from being interpreted.
const blist (protect ((1) ((2) ((3)))))
Cons cell
methods
A Cons object provides several methods to access the car and
the cdr of a cons cell. Other methods allows access to a
list by index.
const c (Cons
"hello" "world")
c:length # 2
c:get-car # "hello"
c:get-cadr # "world"
c:get 0 # "hello"
c:get 1 # "world"
The set-car method set the car of the cons cell. The add method adds a new cons cell at the end of the cons list and set the car with the specified object.
List
object
The List object provides the facility of a double-link list.
The List object is another example of iterable object. The
List object provides support for forward and backward
iteration.
List
construction
A list is constructed like a cons cell with zero or more
arguments. Unlike the cons cell, the List can have a null
size.
const nil-list
(List)
const dbl-list (List 1 ’a’
"hello")
List
methods
The List object methods are similar the Cons object. The add
method adds an object at the end of the list. The insert
method inserts an object at the beginning of the list.
const list (List
"hello" "world")
list:length # 2
list:get 0 # "hello"
list:get 1 # "world"
list:add "folks" # "hello"
"world" "folks"
Vector
object
The Vector object provides the facility of an index array of
objects. The Vector object is another example of iterable
object. The Vector object provides support for forward and
backward iteration.
Vector
construction
A vector is constructed like a cons cell or a list. The
default constructor creates a vector with 0 objects.
const nil-vector
(Vector)
const obj-vector (Vector 1 ’a’
"hello")
Vector
methods
The Vector object methods are similar to the List object.
The add method appends an object at the end of the vector.
The set method set a vector position by index.
const vec
(Vector "hello" "world")
vec:length # 2
vec:get 0 # "hello"
vec:get 1 # "world"
vec:add "folks" # "hello"
"world" "folks"
vec:set 0 "bonjour" # "bonjour"
"world" "folks"
Set
object
The Set object provides the facility of an object container.
The Set object is another example of iterable object. The
Set object provides support for forward iteration. One of
the property of a set is that there is only one object
representation per set. Adding two times the same object
results in one object only.
Set
construction
A set is constructed like a vector. The default constructor
creates a set with 0 objects.
const nil-set
(Set)
const obj-set (Set 1 ’a’ "hello")
Set
methods
The Set object methods are similar to the Vector object. The
add method adds an object in the set. If the object is
already in the set, the object is not added. The length
method returns the number of elements in the set.
const set (Set
"hello" "world")
set:get-size # 2
set:add "folks" # "hello"
"world" "folks"
Iteration
When an object is iterable, it can be used with the reserved
keyword for. The for keyword iterates on one or several
objects and binds associated symbols during each step of the
iteration process. All iterable objects provides also the
method iterator which returns an iterator for a given
object. The use of iterator is justified during backward
iteration, since for only perform forward iteration.
Function
mapping
Given a function func, it is relatively easy to apply this
function to all objects of an iterable object. The result is
a list of successive calls with the function. Such function
is called a mapping function and is generally called
map.
const map (obj
func) {
trans result (Cons)
for (car) (obj) (result:link (func car))
eval result
}
The link method differs from the add method in the sense that the object to append is set to the cons cell car if the car and cdr is nil.
Multiple
iteration
Multiple iteration can be done with one call to for. The
computation of a scalar product is a simple but illustrative
example.
# compute the
scalar product of two vectors
const scalar-product (u v) {
trans result 0
for (x y) (u v) (result:+= (* x y))
eval result
}
Note that the function scalar-product does not make any assumption about the object to iterate. One could compute the scalar product between a vector a list for example.
const u (Vector
1 2 3)
const v (List 2 3 4)
scalar-product u v
Conversion of
iterable objects
The use of an iterator is suitable for direct conversion
between one object and another. The conversion to a vector
can be simply defined as indicted below.
# convert an
iterable object to a vector
const to-vector (obj) {
trans result (Vector)
for (i) (obj) (result:add i)
eval result
}
Explicit
iterator
An explicit iterator is constructed with the iterator
method. At construction, the iterator is reset to the
beginning position. The get-object method returns the object
at the current iterator position. The next advances the
iterator to its next position. The valid-p method returns
true if the iterator is in a valid position. When the
iterator supports backward operations, the prev method move
the iterator to the previous position. Note that Cons
objects do not support backward iteration. The begin method
reset the iterator to the beginning. The end method moves
the iterator the last position. This method is available
only with backward iterator.
# reverse a list
const reverse-list (obj) {
trans result (List)
trans itlist (obj:iterator)
itlist:end
while (itlist:valid-p) {
result:add (itlist:get-object))
itlist:prev
}
eval result
}
Special
Objects
The engine incorporates other container objects. To name a
few, such objects are the Queue, Bitset or Fifo objects.
Queue
object
A queue is a special object which acts as container with a
fifo policy. When an object is placed in the queue, it
remains there until it has been dequeued. The Fifo and Queue
objects are somehow similar, with the fundamental difference
that the queue is a blocking.
# create a queue
with objects
const q (Queue 2)
q:push "hello"
q:push "world"
q:empty-p # false
q:length # 2
# dequeue some object
q:pop # hello
q:pop # world
q:empty-p # true
Bitset
object
A bit set is a special container for bit. A bit set can be
constructed with a specific size. When the bit set is
constructed, each bit can be marked and tested by index.
Initially, the bitset size is null.
# create a bit
set by size
const bs (Bitset 8)
bitset-p bs # true
# check, mark and clear
assert false (bs:marked-p 0)
bs:mark 0
assert true (bs:marked-p 0)
bs:clear 0
assert false (bs:marked-p 0)
CLASSES
This chapter covers the class model and its associated operations. The class model is slightly different compared to traditional one because dynamic symbol bindings do not enforce to declare the class data members. A class is an object which can be manipulated by itself. Such class is said to belongs to a group of meta class as described later in this chapter. Once the class concept has been detailed, the chapter moves to the concept of instance of that class and shows how instance data members and functions can be used. The chapter terminates with a description of dynamic class programming.
Class
object
A class object is simply a nameset which can be replicated
via a construction mechanism. A class is created with the
special form class. The result is an object of type Class
which supports various symbol binding operations.
Class
declaration and bindings
A new class is an object created with the reserved keyword
class. Such class is an object which can be bound to a
symbol.
const Color (class)
Because a class acts like a nameset, it is possible to bind directly symbols with the qualified name notation.
const Color
(class)
const Color:RED-FACTOR 0.75
const Color:BLUE-FACTOR 0.75
const Color:GREEN-FACTOR 0.75
When a data is defined in the class nameset, it is common to refer it as a class data member. A class data member is invariant over the instance of that class. When the data member is declared with the const reserved keyword, the symbol binding is in the class nameset.
Class closure
binding
A lambda or gamma expression can be define for a class. If
the class do not reference an instance of that class, the
resulting closure is called a class method of that class.
Class methods are invariant among the class instances. The
standard declaration syntax for a lambda or gamma expression
is still valid with a class.
const
Color:get-primary-by-string (color value) {
trans val "0x"
val:+= (switch color (
("red" (value:substr 1 3))
("green" (value:substr 3 5))
("blue" (value:substr 5 7))
))
Integer val
}
The invocation of a class method is done with the standard qualified name notation.
Color:get-primary-by-string
"red" "#23c4e5"
Color:get-primary-by-string "green"
"#23c4e5"
Color:get-primary-by-string "blue"
"#23c4e5"
Class symbol
access
A class acts as a nameset and therefore provides the
mechanism to evaluate any symbol with the qualified name
notation.
const
Color:RED-VALUE "#ff0000"
const Color:print-primary-colors (color) {
println "red color " (
Color:get-primary-color "red" color)
println "green color " (
Color:get-primary-color "green" color)
println "blue color " (
Color:get-primary-color "blue" color)
}
# print the color components for the red color
Color:print-primary-colors Color:RED-VALUE
Instance
An instance of a class is an object which is constructed by
a special class method called a constructor. If an instance
constructor does not exist, the instance is said to have a
default construction. An instance acts also as a nameset.
The only difference with a class, is that a symbol
resolution is done first in the instance nameset and then in
the instance class. As a consequence, creating an instance
is equivalent to define a default nameset hierarchy.
Instance
construction
By default, a instance of the class is an object which
defines an instance nameset. The simplest way to define an
anonymous instance is to create it directly.
const i
((class))
const Color (class)
const red (Color)
The example above define an instance of an anonymous class. If a class object is bound to a symbol, such symbol can be used to create an instance of that class. When an instance is created, the special symbol named this is defined in the instance nameset. This symbol is bounded to the instance object and can be used to reference in an anonymous way the instance itself.
Instance
initialization
When an instance is created, the engine looks for a special
lambda expression called preset. This lambda expression, if
it exists, is executed after the default instance has been
constructed. Such lambda expression is a method since it can
refer to the this symbol and bind some instance symbols. The
arguments which are passed during the instance construction
are passed to the preset method.
const Color
(class)
trans Color:preset (red green blue) {
const this:red (Integer red)
const this:green (Integer green)
const this:blue (Integer blue)
}
# create some default colors
const Color:RED (Color 255 0 0)
const Color:GREEN (Color 0 255 0)
const Color:BLUE (Color 0 0 255)
const Color:BLACK (Color 0 0 0)
const Color:WHITE (Color 255 255 255)
In the example above, each time a color is created, a new instance object is created. The constructor is invoked with the this symbol bound to the newly created instance. Note that the qualified name this:red defines a new symbol in the instance nameset. Such symbol is sometimes referred as an instance data member. Note as well that there is no ambiguity in resolving the symbol red. Once the symbol is created, it shadows the one defined as a constructor argument.
Instance
symbol access
An instance acts as a nameset. It is therefore possible to
bind locally to an instance a symbol. When a symbol needs to
be evaluated, the instance nameset is searched first. If the
symbol is not found, the class nameset is searched. When an
instance symbol and a class symbol have the same name, the
instance symbol is said to shadow the class symbol. The
simple example below illustrates this property.
const c (class)
const c:a 1
const i (c)
const j (c)
const i:a 2
# class symbol access
println c:a
# shadow symbol access
println i:a
# non shadow access
println j:a
When the instance is created, the special symbol meta is bound in the instance nameset with the instance class object. This symbol can therefore be used to access a shadow symbol.
const c (class)
const i (c)
const c:a 1
const i:a 2
println i:a
println i:meta:a
The symbol meta must be used carefully, especially inside an initialization since it might create an infinite recursion as shown below.
const c (class)
trans c:preset nil (const i (this:meta))
const i (c)
Instance
method
When lambda expression is defined within the class or the
instance nameset, that lambda expression is callable from
the instance itself. If the lambda expression uses the this
symbol, that lambda is called an instance method since the
symbol this is defined in the instance nameset. If the
instance method is defined in the class nameset, the
instance method is said to be global, that is, callable by
any instance of that class. If the method is defined in the
instance nameset, that method is said to be local and is
callable by the instance only. Due to the nature of the
nameset parent binding, only lambda expression can be used.
Gamma expressions will not work since the gamma nameset has
always the top level nameset as its parent one.
const Color
(class)
# class constructor
trans Color:preset (red green blue) {
const this:red (Integer red)
const this:green (Integer green)
const this:blue (Integer blue)
}
const Color:RF 0.75
const Color:GF 0.75
const Color:BF 0.75
# this method returns a darker color
trans Color:darker nil {
trans lr (Integer (max (this:red:* Color:RF) 0))
trans lg (Integer (max (this:green:* Color:GF) 0))
trans lb (Integer (max (this:blue:* Color:BF) 0))
Color lr lg lb
}
# get a darker color than yellow
const yellow (Color 255 255 0)
const dark-yellow (yellow:darker)
Instance
operators
Any operator can be defined at the class or the instance
level. Operators like == or != generally requires the
ability to assert if the argument is of the same type of the
instance. The global operator == will return true if two
classes are the same. With the use of the meta symbol, it is
possible to assert such equality.
# this method
checks that two colors are equals
trans Color:== (color) {
if (== Color color:meta) {
if (!= this:red color:red) (return false)
if (!= this:green color:green) (return false)
if (!= this:blue color:blue) (return false)
eval true
} false
}
# create a new yellow color
const yellow (Color 255 255 0)
(yellow:== (Color 255 255 0)) # true
The global operator == returns true if both arguments are the same, even for classes. Method operators are left open to the user.
Complex
number example
As a final example, a class simulating the behavior of a
complex number is given hereafter. The interesting point to
note is the use of the operators. As illustrated before, the
class uses uses a default method method to initialize the
data members.
# class
declaration
const Complex (class)
# constructor
trans Complex:preset (re im) {
trans this:re (Real re)
trans this:im (Real im)
}
The constructor creates a complex object with the help of the real part and the imaginary part. Any object type which can be bound to a Real object is acceptable.
# class mutators
trans Complex:set-re (x) (trans this:re (Real re))
trans Complex:set-im (x) (trans this:im (Real im))
# class accessors
trans Complex:get-re nil (Real this:re)
trans Complex:get-im nil (Real this:im)
The accessors and the mutators simply provides the interface to the complex number components and perform a cloning of the calling or returned objects.
# complex number
module
trans Complex:module nil {
trans result (Real (+ (* this:re this:re)
(* this:im this:im)))
result:sqrt
}
# complex number formatting
trans Complex:format nil {
trans result (String this:re)
result:+= "+i"
result:+= (String this:im)
}
The module and format are simple methods. Note the the complex number formatting is arbitrary here.
# complex
predicate
const complex-p (c) (
if (instance-p c) (== Complex c:meta) false)
The complex-p predicate is the perfect illustration of the use of the meta reserved symbol. However, it shall be noted that the meta-comparison is done if and only if the calling argument is an instance.
# operators
trans Complex:== (c) (
if (complex-p c) (and (this:re:== c:re)
(this:im:== c:im)) (
if (number-p c) (and (this:re:== c)
(this:im:zero-p)) false))
trans Complex:= (c) {
if (complex-p c) {
this:re:= (Real c:re)
this:im:= (Real c:im)
return this
}
this:re:= (Real c)
this:im:= 0.0
return this
}
trans Complex:+ (c) {
trans result (Complex this:re this:im)
if (complex-p c) {
result:re:+= c:re
result:im:+= c:im
return result
}
result:re:+= (Real c)
eval result
}
The operators are a little tedious to write. The comparison can be done with a complex number or a built-in number object. The assignation operator creates a copy for both the real and imaginary part. The summation operator is given here for illustration purpose.
Inheritance
Inheritance is the mechanism by which a class or an instance
inherits methods and data member access from a parent
object. The class model is based on a single inheritance
model. When an instance object defines a parent object, such
object is called a super instance. The instance which has a
super instance is called a derived instance. The main
utilization of inheritance is the ability to reuse methods
for that super instance.
Derivation
construction
A derived object is generally defined within the preset
method of that instance by setting the super data member.
The super reserved keyword is set to nil at the instance
construction. The good news is that any object can be
defined as a super instance, including built-in object.
const c (class)
const c:preset nil {
trans this:super 0
}
In the example above, an instance of class c is constructed. The super instance is with an integer object. As a consequence, the instance is derived from the Integer instance. Another consequence of this scheme is that derived instance do not have to be built from the same base class.
Derived
symbol access
When an instance is derived from another one, any symbol
which belongs to the super instance can be access with the
use of the super data member. If the super class can
evaluate a symbol, that symbol is resolved automatically by
the derived instance.
const c (class)
const i (c)
trans i:a 1
const j (c)
trans j:super i
println j:a
When a symbol is evaluated, a set of search rules is applied. The engine gives the priority to the class nameset vs the super instance. As a consequence, a class data member might shadow a super instance data member. The rule associated with a symbol evaluation can be summarized as follow.
Look in the instance nameset.
Look in the class nameset.
Look in the super instance if it exists.
Look in the base object.
Instance
re-parenting
The ability to set dynamically the parent instance make the
object model an ideal candidate to support instance
re-parenting. In this model, a change in the parent instance
is automatically reflected at the instance method call.
const c (class)
const i (c)
trans i:super 0
println (i:to-string) # 0
trans i:super "hello world"
println (i:to-string) # hello world
In this example, the instance is originally set with an Integer instance parent. Then the instance is re-parented with a String instance parent. The call to the to-string method illustrates this behavior.
Instance
re-binding
The ability to set dynamically the instance class is another
powerful feature of the class model. In this approach, the
instance meta class can be changed dynamically with the mute
method. Furthermore, it is also possible to create initially
an instance without any class binding, which is later
muted.
# create a point
class
const point (class)
# point class
trans point:preset (x y) {
trans this:x x
trans this:y y
}
# create an empty instance
const p (Instance)
# bind the point class
p:mute point 1 2
In this example, when the instance is muted, the preset method is called automatically with the extra arguments.
Instance
inference
The ability to instantiate dynamically inferred instance is
offered by the instance model. An instance b is said to be
inferred by the instance a when the instance a is the super
instance of the instance b. The instance inference is
obtained by binding the infer symbol to a class. When an
instance of that class is created, the inferred instance is
also created.
# base class A
const A (class)
# inferred class B
const B (class)
const A:infer B
# create an instance from A
const x (A)
assert B (x:meta)
assert A (x:super:meta)
In this example, when the instance is created, the inferred instance is also created and returned by the instantiation process. The preset method is only called for the inferred instance if possible or the base instance if there is no inferring class. Because the base preset preset method is not called automatically, the inferred method is responsible to do such call.
trans B:preset
(x y) {
trans this:xb x
trans this:yb y
if (== A this:super:meta) (this:super:preset x y)
}
Because the class can mute from one call to another and also the inferred class, the preset method call must be used after a discrimination of the meta class has been made as indicated by the above example.
Instance
deference
In the process of creating instances, one might have a
generic class with a method that attempts to access a data
member which is bound to another class. The concept of class
deference is exactly designed for this purpose. With the
help of reserved keyword defer, a class with virtual data
member accessors can be bound to a base class as indicated
in the example below.
# create the
base and defer class
const bc (class)
const dc (class)
# bind the base preset method
trans bc:preset nil (const this:y 2)
# bind the defer accessor to the base data member
trans dc:get-y nil (eval this:y)
# bind the defer class in the base class
const bc:defer dc
# create an instance from the base class
const i (bc)
# access to the base member with the defer method
assert 2 (i:get-y)
It is worth to note that the class deference is made at the class level. When an instance of the base class is created, all methods associated with the deferent class are visible from the base class, thus making the deferent class a virtual interface to the base class.
ADVANCED CONCEPTS
This chapter covers advanced concepts of the writing system. The first subject is the exception model. The second subject covers some properties of the namesets in the context of the interpreter object. The thread sub-system is then described along with the synchronization mechanism. Finally, some notes related to the functional system are given at the end of this chapter.
Exception
An exception is an unexpected change in the execution flow.
The exception model is based on a mechanism which throws the
exception to be caught by a handler. The mechanism is also
designed to be compatible with the native "C++"
implementation.
Throwing an
exception
An exception is thrown with the reserved keyword throw. When
an exception is thrown, the normal flow of execution is
interrupted and an object used to carry the exception
information is created. Such exception object is propagated
backward in the call stack until an exception handler catch
it.
if (not
(number-p n))
(throw "type-error" "invalid object
found" n)
The example above is the general form to throw an exception. The first argument is the the exception id. The second argument is the exception reason. The third argument is the exception object. The exception id and reason are always a string. The exception object can be any object which is carried by the exception. The reserved keyword throw accepts 0 or more arguments.
throw
throw "type-error"
throw "type-error" "invalid
argument"
With 0 argument, the exception is thrown with the exception id set to "user-exception". With one argument, the argument is the exception id. With 2 arguments, the exception id and reason are set. Within a try block, an exception can be thrown again by using the exception object represented with the what symbol.
try {
...
} {
println "exception caught and re-thrown"
throw what
}
Exception
handler
The special form try executes a form and catch an exception
if one has been thrown. With one argument, the form is
executed and the result is the result of the form execution
unless an exception is caught. If an exception is caught,
the result is the exception object. If the exception is a
native one, the result is nil.
try (+ 1 2)
try (throw)
try (throw "hello")
try (throw "hello" "world")
try (throw "hello" "world"
"folks")
|
In its second form, the try reserved keyword can accept a second form which is executed when an exception is caught. When an exception is caught, a new nameset is created and the special symbol what is bounded with the exception object. In such environment, the exception can be evaluated. |

try (throw
"hello")
(eval what:eid)
try (throw "hello" "world")
(eval what:reason)
try (throw "hello" "world" 2000)
(eval what:object)
Exceptions are useful to notify abruptly that something went wrong. With an untyped language, it is also a convenient mechanism to abort an expression call if some arguments do not match the expected types.
# protected
factorial
const fact (n) {
if (not (integer-p n))
(throw "number-error" "invalid argument in
fact")
if (== n 0) 1 (* n (fact (- n 1)))
}
try (fact 5) 0
try (fact "hello") 0
Nameset
A nameset is created with the reserved keyword nameset.
Without argument, the nameset reserved keyword creates a
nameset without setting its parent. With one argument, a
nameset is created and the parent set with the argument.
const nset
(nameset)
const nset (nameset ...)
Default
namesets
When a nameset is created, the symbol . is automatically
created and bound to the newly created nameset. If a parent
nameset exists, the symbol .. is also automatically created.
The use of the current nameset is a useful notation to
resolve a particular name given a hierarchy of namesets.
trans a 1 # 1
block {
trans a (+ a 1) # 2
println ..:a 1 # 1
}
println a # 1
Nameset and
inheritance
When a nameset is set as the super object of an instance,
some interesting results are obtained. Because symbols are
resolved in the nameset hierarchy, there is no limitation to
use a nameset to simulate a kind of multiple inheritance.
The following example illustrates this point.
const cls
(class)
const ins (cls)
const ins:super (nameset)
const ins:super:value 2000
const ins:super:hello "hello world "
println ins:hello ins:value # hello world 2000
Delayed
Evaluation
The engine provides a mechanism called delayed evaluation.
Such mechanism permits the encapsulation of a form to be
evaluated inside an object called a promise.
Creating a
promise
The reserved keyword delay creates a promise. When the
promise is created, the associated object is not evaluated.
This means that the promise evaluates to itself.
const a (delay
(+ 1 2))
promise-p a # true
The previous example creates a promise and store the argument form. The form is not yet evaluated. As a consequence, the symbol a evaluates to the promise object.
Forcing a
promise
The reserved keyword force the evaluation of a promise. Once
the promise has been forced, any further call will produce
the same result. Note also that, at this stage, the promise
evaluates to the evaluated form.
trans y 3
const l ((lambda (x) (+ x y)) 1)
assert 4 (force l)
trans y 0
assert 4 (force l)
Enumeration
Enumeration, that is, named constant bound to an object, can
be declared with the reserved keyword enum. The enumeration
is built with a list of literal and evaluated as is.
const e (enum E1
E2 E3)
assert true (enum-p e)
The complete enumeration evaluates to an Enum object. Once built, enumeration item evaluates by literal and returns an Item object.
assert true
(item-p e:E1)
assert "Item" (e:E1:repr)
Items are comparable objects. Only items can be compared. For a given item, the source enumeration can be obtained with the get-enum method.
# check for item
equality
const i1 e:E1
const i2 e:E2
assert true (i1:== i1)
assert false (== i1 i2)
# get back the enumeration
assert true (enum-p (i1:get-enum))
Logger
The Logger class is a message logger that stores messages in
a buffer with a level. The default level is the level 0. A
negative level generally indicates a warning or an error
message but this is just a convention which is not enforced
by the class. A high level generally indicates a less
important message. The messages are stored in a circular
buffer. When the logger is full, a new message replace the
oldest one. By default, the logger is initialized with a 256
messages capacity that can be re-sized.
const log
(Logger)
assert true (logger-p log)
When a message is added, the message is stored with a time-stamp and a level. The time-stamp is used later to format a message. The length method returns the number of logged messages. The get-message method returns a message by index. Because the system operates with a circular buffer, the get-message method manages the indexes in such way that the old messages are accessible with the oldest index. For example, even after a buffer circulation, the index 0 will point to the oldest message. The get-message-level returns the message level and the get-message-time returns the message posted time.
const mesg (log:get-message 0)
In term of usage, the logger facility can be conveniently used with other derived classes. The standard i/o module provides several classes that permits to manage logging operations in a convenient way.
Interpreter
|
The interpreter is by itself a special object with specialized methods which do not have equivalent using the standard notation. The interpreter is always referred with the special symbol interp. The following table is a summary of the symbol bound to the interpreter. |

|
The interpreter provides also special methods which can be used to access internal features that do not operate like standard methods or functions. Some methods are also designed to modify the internal state of the interpreter. Note that some methods provide a mechanism to interact at the process level. |

Arguments
vector
The interp:argv qualified name evaluates to a vector of
strings. Each argument is stored in the vector during the
interpreter initialization.
zsh> axi
hello world
(axi) println (interp:argv:length) # 2
(axi) println (interp:argv:get 0) # hello
Interpreter
version
Several symbols can be used to track the interpreter version
and the operating system. The full version is bound to the
interp:version qualified name. The full version is composed
of the major, minor and patch number. The operating system
name is bound to the qualified name interp:os-name. The
operating system type is bound to the interp:os-type.
println
"major number : " interp:major-version
println "minor number : " interp:minor-version
println "patch number : " interp:patch-version
println "version number : " interp:version
println "system name : " interp:os-name
println "system type : " interp:os-type
println "official uri : " interp:afnix-uri
Method
load
The interp:load method loads and execute a file. The
interpreter interactive command session is suspended during
the execution of the file. In case of error or if an
exception is raised, the file execution is terminated. The
process used to load a file is governed by the file
resolver. Without extension, a compiled file is searched
first and if not found a source file is searched. The module
is loaded only once unless the force flag is used.
interp:load
"module"
interp:load "module" true
interp:load "module" "tag" true
In the first form the module is loaded by name only once. In the second form, the module is loaded with a force flag set to true. In the third form, the module is given a tag which is used to detect whether or not the module has been loaded. If no tag is given, the module name is used instead.
Method
library
The interp:library method loads and initializes a library.
The interpreter maintains a list of opened library. Multiple
execution of this method for the same library does nothing.
The method returns the library object.
interp:library
"afnix-sys"
println "random number: "
(afnix:sys:get-random)
Method
dup
The interpreter can be duplicated with the help of the dup
method. Without argument, a clone of the current interpreter
is made and a terminal object is attached to it. When used
in conjunction with the roll method, this approach permits
to create an interactive interpreter. The dup method also
accepts a terminal object.
# duplicate the
interpreter
const si (interp:dup)
# change the primary prompt
si:set-primary-prompt "(si)"
Method
roll
The interpreter loop can be run with the roll. The loop
operates by reading the interpreter input stream. If the
interpreter has been cloned with the help of the dup method,
this method provides a convenient way to operate in
interactive mode. The method is not called loop because it
is a reserved keyword and starting a loop is like having the
ball rolling.
# duplicate the
interpreter
const si (interp:dup)
# loop with this interpreter
si:roll
Method
wait
The interpreter can wait for all normal threads to complete.
When invoked, the interpreter monitors all normal threads
and wait unil the terminate normally. This is a standard
synchronization method in a multithreaded environment.
# create a
thread
launch f
# wait for completion
interp:wait
Librarian
object
A librarian file is a special file that acts as a containers
for various files. A librarian file is created with the axl
-- cross librarian --utility. Once a librarian file is
created, it can be added to the interpreter resolver. The
file access is later performed automatically by name with
the standard interpreter load method.
Creating a
librarian
The axl utility is the preferred way to create a librarian.
Given a set of files, axl combines them into a single
one.
zsh: axl -h
usage: axl [options] [files]
[h] print this help message
[v] print version information
[c] create a new librarian
[x] extract from the librarian
[s] get file names from the librarian
[t] report librarian contents
[f] lib set the librarian file name
The c option creates a new librarian. The librarian file name is specified with the f option.
zsh: axl -c -f librarian.axl file-1.als file-2.als
The previous command combines file-1.als and file-2.als into a single file called librarian.axl . Note that any file can be included in a librarian.
Using the
librarian
Once a librarian is created, the interpreter -i option can
be used to specify it. The -i option accepts either a
directory name or a librarian file. Once the librarian has
been opened, the interpreter load method can be used as
usual.
zsh> axi -i
librarian.axl
(axi) interp:load "file-1.als"
(axi) interp:load "file-2.als"
The librarian acts like a file archive. The interpreter file resolver takes care to extract the file from the librarian when the load method is invoked.
Librarian
contents
The axl utility provides the -t and -s options to look at
the librarian contents. The -s option returns all file name
in the librarian. The -t option returns a one line
description for each file in the librarian.
zsh: axl -t -f
librarian.axl
-------- 1234 file-1.als
-------- 5678 file-2.als
The one line report contains the file flags, the file size and the file name. The file flags are not used at this time. One possible use in the future is for example, an auto-load bit or any other useful things.
Librarian
extraction
The -x option permits to extract file from the librarian.
Without any file argument, all files are extracted. With
some file arguments, only those specified files are
extracted.
zsh: axl -x -f
librarian.axl
zsh: axl -x -f librarian.axl file-1.als
Librarian
object
The Librarian object can be used as a convenient way to
create a collection of files or to extract some of them.
Output
librarian
The Librarian object is a standard object. Its predicate is
librarian-p. Without argument, a librarian is created in
output mode. With a string argument, the librarian is opened
in input mode, with the file name argument. The output mode
is used to create a new librarian by adding file into it.
The input mode is created to read file from the
librarian.
# create a new
librarian
const lbr (Librarian)
# add a file into it
lbr:add "file-1.als"
# write it
lbr:write "librarian.axl"
The add method adds a new file into the librarian. The write method the full librarian as a single file those name is write method argument.
Input
librarian
With an argument, the librarian object is created in input
mode. Once created, file can be read or extracted. The
length method -- which also work with an output librarian --
returns the number of files in the librarian. The exists-p
predicate returns true if the file name argument exists in
the librarian. The get-names method returns a vector of file
names in this librarian. The extract method returns an input
stream object for the specific file name.
# open a
librarian for reading
const lbr (Librarian "librarian.axl")
# get the number of files
println (lbr:length)
# extract the first file
const is (lbr:extract "file-1.als")
# is is an input stream - dump each line
while (is:valid-p) (println (is:readln))
Most of the time, the librarian object is used to extract file dynamically. Because a librarian is mapped into the memory at the right offset, there is no worry to use big librarian, even for a small file. Note that any type of file can be used, text or binaries.
File
resolver
The file resolver is a special object used by the
interpreter to resolve file path based on the search path.
The resolver uses a mixed list of directories and librarian
files in its search path. When a file path needs to be
resolved, the search path is scanned until a matched is
found. Because the librarian resolution is integrated inside
the resolver, there is no need to worry about file
extraction. That process is done automatically. The resolver
can also be used to perform any kind of file path
resolution.
Resolver
object
The resolver object is created without argument. The add
method adds a directory path or a librarian file to the
resolver. The valid method checks for the existence of a
file. The lookup method returns an input stream object
associated with the object.
# create a new
resolver
const rslv (Resolver)
assert true (resolver-p rslv)
# add the local directory on the search path
rslv:add "."
# check if file test.als exists
# if this is ok - print its contents
if (rslv:valid-p "test.als") {
const is (rslv:lookup "test.als")
while (is:valid-p) (println (is:readln))
}
Thread
operations
The interpreter is a multi-threaded engine with a native
implementation of objects locking. A thread is started with
the reserved keyword launch. The execution is completed when
all threads have terminated. This means that the master
thread (i.e the first thread) is suspended until all other
threads have completed their execution.
Starting a
thread
A thread is started with the reserved keyword launch. The
form to execute in a thread is the argument. The simplest
thread to execute is the nil thread.
launch (nil)
There exists an alternate mechanism to start a thread with the reserved keyword launch and a thread object. Such mechanism is used when using deferred thread object creation or a thread generator object known as a thread set.
Thread object
and result
When a thread terminate, the thread object holds the result
of the last executed form. The thread object is returned by
the launch command. The thread-p predicates returns true if
the object is a thread descriptor.
const thr
(launch (nil))
println (thread-p thr) # true
The thread result can be obtained with the help of the result method. Although the result can be accessed at any time, the returned value will be nil until the thread as completed its execution.
const thr
(launch (nil))
println (thr:result) # nilp
Although the engine will ensure that the result is nil until the thread has completed its execution, it does not mean that it is a reliable approach to test until the result is not nil. The engine provides various mechanisms to synchronize a thread and eventually wait for its completion.
Future
object
The future special form provides a simple mechanism to
perform asynchronous evaluation. When a future object is
created, the evaluation is pending a call to the force
special form. When the future is complete, the evalution
result is available.
# create a
future
const f (future 1)
# do not necessarily evaluates to 1
println (force f)
Shared
objects
The whole purpose of using a multi-threaded environment is
to provide a concurrent execution with some shared
variables. Although, several threads can execute
concurrently without sharing data, the most common situation
is that one or more global variable are accessed -- and even
changed -- by one or more threads. Various scenarios are
possible. For example, a variable is changed by one thread,
the other thread just read its value. Another scenario is
one read, multiple write, or even more complicated, multiple
read and multiple write. In any case, the interpreter
subsystem must ensure that each objects are in a good state
when such operation do occur. The engine provides an
automatic synchronization mechanism for global objects,
where only one thread can modify an object, but several
thread can read it. This mechanism known as read-write
locking guarantees that there is only one writer, but
eventually multiple reader. When a thread starts to modify
an object, no other thread are allowed to read or write this
object until the transaction has been completed. On the
opposite, no thread is allowed to change (i.e. write) an
object, until all thread which access (i.e. read) the object
value have completed the transaction. Because a context
switch can occur at any time, the object read-write locking
will ensure a safe protection during each concurrent
access.
Shared
protection access
We illustrate the previous discussion with an interesting
example and some variations around it. Let’s consider
a form which increase an integer object and another form
which decrease the same integer object. If the integer is
initialized to 0, and the two forms run in two separate
threads, we might expect to see the value bounded by the
time allocated for each thread. In other word, this simple
example is a very good illustration of your machine
scheduler.
# shared
variable access
const var 0
# increase method
const incr nil {
while true (println "increase: " (var:= (+ var
1)))
}
# decrease method
const decr nil {
while true (println "decrease: " (var:= (- var
1)))
}
# start both threads
launch (decr)
launch (incr)
In the previous example, var is initialized to 0. The incr thread increments var while the decr thread decrements var. Depending on the operating system, the result stays bounded within a certain range. The previous example can be changed by using the main thread or a third thread to print the variable value. The end result is the same, except that there is more threads competing for the shared variable.
# shared variable access
# incrementer, decrementer and
printer
const incr nil (while true (var:= (+ var 1)))
const decr nil (while true (var:= (- var 1)))
const prtv nil (while true (println "value = "
var))
# start all threads
launch (decr)
launch (incr)
launch (prtv)
Synchronization
Although, there is an automatic synchronization mechanism
for reading or writing an object, it is sometimes necessary
to control the execution flow. There are basically two
techniques to do so. First, protect a form from being
executed by several threads. Second, wait for one or several
threads to complete their task before going to the next
execution step.
Form
synchronization
The reserved keyword sync can be used to synchronize a form.
When a form, is synchronized, the engine guarantees that
only one thread will execute this form. In the special case
of the form beeing a future, the interpreter will block
until the future is complete.
const
print-message (code mesg) (
sync {
errorln "error : " code
errorln "message: " mesg
}
)
The previous example creates a gamma expression which make sure that both the error code and error message are printed in one group, when several threads call it.
Thread
completion
The other piece of synchronization is the thread completion
indicator. The thread descriptor contains a method called
wait which suspend the calling thread until the thread
attached to the descriptor has been completed. If the thread
is already completed, the method returns immediately.
# simple flag
const flag false
# simple tester
const ftest (bval) (flag:= bval)
# run the thread and wait
const thr (launch (ftest true))
thr:wait
assert true flag
This example is taken from the test suites. It checks that a boolean variable is set when started in a thread. Note the use of the wait method to make sure the thread has completed before checking for the flag value. It is also worth to note that wait is one of the method which guarantees that a thread result is valid. Another use of the wait method can be made with a vector of thread descriptors when one wants to wait until all of them have completed.
# shared vector
of threads descriptors
const thr-group (Vector)
# wait until all threads in the group are finished
const wait-all nil (for (thr) (thr-group) (thr:wait))
Complete
example
We illustrate the previous discussion with a complete
example. The idea is to perform a matrix multiplication. A
thread is launched when when multiplying one line with one
column. The result is stored in the thread descriptor. A
vector of thread descriptor is used to store the result.
# initialize the
shared library
interp:library "afnix-sys"
# shared vector of threads descriptors
const thr-group (Vector)
# waits until all threads in the group are finished
const wait-all nil (for (thr) (thr-group) (thr:wait))
The group of threads is represented as a vector. Based on the the previous discussion, a simple loop that blocks until all threads are completed is designed as a simple gamma expression.
# initializes a
matrix with random numbers
const init-matrix (n) {
trans i (Integer 0)
const m (Vector)
do {
trans v (m:add (Vector))
trans j (Integer)
do {
v:add (afnix:sys:get-random)
} (< (j:++) n)
} (< (i:++) n)
eval m
}
The matrix initialization is quite straightforward. The matrix is represented as a vector of lines. Each line is also a vector of random integer number. It is here worth to note that the standard math module provides a native implementation of real matrix.
# this procedure
multiply one line with one column
const mult-line-column (u v) {
assert (u:length) (v:length)
trans result 0
for (x y) (u v) (result:+= (* x y))
eval result
}
# this procedure multiply two vectors assuming one
# is a line and one is a column from the matrix
const mult-matrix (mx my) {
for (lv) (mx) {
assert true (vector-p lv)
for (cv) (my) {
assert true (vector-p cv)
thr-group:add (launch (mult-line-column lv cv))
}
}
}
The matrix vector multiplication is at the heart of the example. Each line-column multiplication is started into a thread and the thread object is placed into the thread group vector.
# check for some
arguments
# note the use of errorln method
if (== 0 (interp:argv:length)) {
errorln "usage: axi 0607.als size"
afnix:sys:exit 1
}
# get the integer and multiply
const n (Integer (interp:argv:get 0))
mult-matrix (init-matrix n) (init-matrix n)
# wait for all threads to complete
wait-all
# make sure we have the right number
assert (* n n) (thr-group:length)
The main execution is started with the matrix size as the first argument. Two random matrices are then created and the multi-threaded multiplication is launched. The main thread is blocked until all threads in the thread group are completed.
Condition
variable
A condition variable is another mechanism to synchronize
several threads. A condition variable is modeled with the
Condvar object. At construction, the condition variable is
initialized to false. A thread calling the wait method will
block until the condition becomes true. The mark method can
be used by a thread to change the state of a condition
variable and eventually awake some threads which are blocked
on it. The following example shows how the main thread
blocks until another change the state of the condition.
# create a
condition variable
const cv (Condvar)
# this function runs in a thread - does some
# computation and mark the condition variable
const do-something nil {
# do some computation
....
# mark the condition
cv:mark
}
# start some computation in a thread
launch (do-something)
# block until the condition is changed
cv:wait-unlock
# continue here
In this example, the condition variable is created at the beginning. The thread is started and the main thread blocks until the thread change the state of the condition variable. It is important to note the use of the wait-unlock method. When the main thread is re-started (after the condition variable has been marked), the main thread owns the lock associated with the condition variable. The wait-unlock method unlocks that lock when the main thread is restarted. Note also that the wait-unlock method reset the condition variable. if the wait method was used instead of wait-unlock the lock would still be owned by the main thread. Any attempt by other thread to call the mark method would result in the calling thread to block until the lock is released. The Condvar class has several methods which can be used to control the behavior of the condition variable. Most of them are related to lock control. The reset method reset the condition variable. The lock and unlock control the condition variable locking. The mark, wait and wait-unlock method controls the synchronization among several threads.
Function
expression
A lambda expression or a gamma expression can be seen like a
function object with no name. During the evaluation process,
the expression object is evaluated as well as the arguments
-- from left to right -- and a result is produced by
applying those arguments to the function object. An
expression can be built dynamically as part of the
evaluation process.
(axi) println
((lambda (n) (+n 1)) 1)
2
The difference between a lambda expression and a gamma expression is only in the nameset binding during the evaluation process. The lambda expression nameset is linked with the calling one, while the gamma expression nameset is linked with the top level nameset. The use of gamma expression is particularly interesting with recursive functions as it can generate a significant execution speedup. The previous example will behaves the same with a gamma expression.
(axi) println
((gamma (n) (+n 1)) 1)
2
Self
reference
When combining a function expression with recursion, the
need for the function to call itself is becoming a problem
since that function expression does not have a name. For
this reason, the writing system provides the reserved
keyword self that is a reference to the function expression.
We illustrate this capability with the well-known factorial
expression written in pure functional style.
(axi) println
((gamma (n)
(if (<= n 1) 1 (* n (self (- n 1))))) 5)
120
The use of a gamma expression versus a lambda expression is a matter of speed. Since the gamma expression does not have free variables, the symbol resolution is not a concern here.
Closed
variables
One of the writing system characteristic is the treatment of
free variables. A variable is said to be free if it is not
bound in the expression environment or its children at the
time of the symbol resolution. For example, the expression
((lambda (n) (+ n x)) 1) computes the sum of the argument n
with the free variable x. The evaluation will succeeds if x
is defined in one of the parent environment. Actually this
example can also illustrates the difference between a lambda
expression and a gamma expression. Let’s consider the
following forms.
trans x 1
const do-print nil {
trans x 2
println ((lambda (n) (+ n x)) 1)
}
The gamma expression do-print will produce 3 since it sums the argument n bound to 1, with the free variable x which is defined in the calling environment as 2. Now if we rewrite the previous example with a gamma expression the result will be one, since the expression parent will be the top level environment that defines x as 1.
trans x 1
const do-print nil {
trans x 2
println ((gamma (n) (+ n x)) 1)
}
With this example, it is easy to see that there is a need to be able to determine a particular symbol value during the expression construction. Doing so is called closing a variable. Closing a variable is a mechanism that binds into the expression a particular symbol with a value and such symbol is called a closed variable, since its value is closed under the current environment evaluation. For example, the previous example can be rewritten to close the symbol x.
trans x 1
const do-print nil {
trans x 2
println ((gamma (n) (x) (+ n x)) 1)
}
Note that the list of closed variable immediately follow the argument list. In this particular case, the function do-print will print 3 since x has been closed with the value 2 has defined in the function do-print.
Dynamic
binding
Because there is a dynamic binding symbol resolution, it is
possible to have under some circumstances a free or closed
variable. This kind of situation can happen when a
particular symbol is defined under a condition.
lambda (n) {
if (<= n 1) (trans x 1)
println (+ n x)
}
With this example, the symbol x is a free variable if the argument n is greater than 1. While this mechanism can be powerful, extreme caution should be made when using such feature.
Lexical and
qualified names
The basic forms elements are the lexical and qualified
names. Lexical and qualified names are constructed by the
parser. Although the evaluation process make that lexical
object transparent, it is possible to manipulate them
directly.
(axi) const sym
(protect lex)
(axi) println (sym:repr)
Lexical
In this example, the protect reserved keyword is used to avoid the evaluation of the lexical object named lex. Therefore the symbol sym refers to a lexical object. Since a lexical -- and a qualified -- object is a also a literal object, the println reserved function will work and print the object name. In fact, a literal object provides the to-string method that returns the string representation of a literal object.
(axi) const sym
(protect lex)
(axi) println (sym:to-string)
lex
Symbol and
argument access
Each nameset maintains a table of symbols. A symbol is a
binding between a name and an object. Eventually, the symbol
carries the const flag. During the lexical evaluation
process, the lexical object tries to find an object in the
nameset hierarchy. Such object can be either a symbol or an
argument. Again, this process is transparent, but can be
controlled manually. Both lexical and qualified named object
have the map method that returns the first object associated
in the nameset hierarchy.
(axi) const obj
0
(axi) const lex (protect obj)
(axi) const sym (lex:map)
(axi) println (sym:repr)
Symbol
A symbol is also a literal object, so the to-string and to-literal methods will return the symbol name. Symbol methods are provided to access or modify the symbol values. It is also possible to change the const symbol flag with the set-const method.
(axi) println
(sym:get-const)
true
(axi) println (sym:get-object)
0
(axi) sym:set-object true
(axi) println (sym:get-object)
true
A symbol name cannot be modified, since the name must be synchronized with the nameset association. On the other hand, a symbol can be explicitly constructed. As any object, the = operator can be used to assign a symbol value. The operator will behaves like the set-object method.
(axi) const sym
(Symbol "symbol")
(axi) println sym
symbol
(axi) sym:= 0
(axi) println (eval sym)
0
Closure
As an object, the Closure can be manipulated outside the
traditional declarative way. A closure is a special object
that holds an argument list, a set of closed variables and a
form to execute. The mechanic of a closure evaluation has
been described earlier. What we are interested here is the
ability to manipulate a closure as an object and eventually
modify it. Note that by default a closure is constructed as
a lambda expression. With a boolean argument set to true the
same result is obtained. With false, a gamma expression is
created.
(axi) const f
(Closure)
(axi) println (closure-p f)
true
This example creates an empty closure. The default closure is equivalent to the trans f nil nil. The same can be obtained with const f (Closure true). For a gamma expression, the following forms are equivalent, const f (Closure false) and const f nil nil. Remember that it is trans and const that differentiate between a lambda and a gamma expression. Once the closure object is defined, the set-form method can be used to bind a form.
# the simple way
trans f nil (println "hello world")
# the complex way
const f (Closure)
f:set-form (protect (println "hello world"))
There are numerous situations where it is desirable to mute dynamically a closure expression. The simplest one is the closure that mute itself based on some context. With the use of self, a new form can be set to the one that is executed. Another use is a mechanism call advice, where some new computation are inserted prior the closure execution. Note that appending to a closure can lead to some strange results if the existing closure expression uses return special forms. In a multi-threaded environment, the ability to change a closure expression is particularly handy. For example a special thread could be used to monitor some context. When a particular situation develops, that threads might trigger some closure expression changes. Note that changing a closure expression does not affect the one that is executed. If such change occurs during a recursive call, that change is seen only at the next call.