Provided by: afnix_2.8.1-1_amd64 

NAME
vol-1 - 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, textis 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 alsis 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 .axcextension 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
hoption 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 voption prints the interpreter version and operating system. The foption turns on or off some
additional options like the assertion checking. The use of program arguments is illustrated later in this
chapter. The ioption adds a path to the interpreter resolver. Several ioptions can be specified. The
order of search is determined by the option order. As mentioned earlier, the use of the resolver combined
with the librarianis 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 nopathoption
is specified.
Interactive line editing
Line editing capabilities is provided when the interpreter is used interactively. Error messages are
displayed in red if the terminal supports colors. Various accelerators are bound to the terminal as
indicated in the table below.
Binding Description
backspace Erase the previous character
delete Erase at the cursor position
insert Toggle insert with in-place
ctrl-a Move to the beginning of the line
ctrl-e Move to the end of the line
ctrl-u Clear the input line
ctrl-k Clear from the cursor position
ctrl-l Refresh the 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.
Binding Description
left Move the cursor to the left
right Move the cursor to the right
up Move up in the history list
down Move down in the history list
Command line arguments
The interpreter command line arguments are stored in a vector called argvwhich is part of the
interpobject. 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 loadmethod to load a file. The argument must be a valid file
path or an exception is raised. The loadmethod 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 loadmethod operates with the help of the interpreter resolver. By default the source file extension
is als. If the file has been compiled, the axcextension 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 axcis the cross compiler. It generates a binary file that can be run across several platforms.
The hoption 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 ioption 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
Booleanfor boolean objects, Integer, Relatiffor integer numbers, Realfor floating-point number, Byte,
Characterand Stringfor 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 real 2000.0
const string "afnix"
const char 'a'
trans symbol "hello world"
trans symbol 2000
The constand transreserve 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 constreserved
keyword creates a constant symboland returns the last evaluated object. As a consequence, nested
constconstructs are possible like trans b (const a 1). The transreserved 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 aand 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 bis 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 assertkeyword.
Lambda expression
A lambda expressionis 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 transreserved
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 transvs
constis 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 transkeyword defines a new function object with one
argument called n. The body of the function is defined with the ifspecial form and can be easily
understood. The function is called in the next form when the printlnspecial form is executed. Note that
here, the call to factproduces an integer object, which is converted automatically by the printlnkeyword.
Block form
The notation used in the factprogram is the standard form notation originating from Lisp and the Scheme
dialect. There is also another notation called the block formnotation 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 factprocedure 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 lambdaspecial 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 factis a constant symbol. The use of constis 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 expressionwhich 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 constto
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 gammais 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 programmingparadigm.
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 xwhile nis marked
in a form before the body of the gamma. This notation indicates that the gamma should retain the value of
the argument nwhen 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 transbinds a lambda expression. The reserved
keyword constbinds 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 argsbinding.
During a lambda or gamma expression execution, the special symbol argsis defined with the extra arguments
passed at the call. For example, a gamma expression with 0 formal argument and 2 actual arguments has
argsdefined 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 argscan also be defined with formal arguments. In that case, argsis 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 argsformal definition are not allowed.
The symbol argscan 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 namesetis a container of bindings between a name and symbolic variable. We use the term symbolic
variableto 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 setand is designed to handle a large number of symbols. In a lambda or gamma
expression, the nameset is called a local setand 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 namesetis referenced with the special symbol '.'. The parent namesetis
referenced with the special symbol '..'. The top level namesetis 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 constor 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 constor 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 examplein 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 namesuch 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
.:hellorefers to the symbol helloin the current nameset. The qualified name ...:hellorefers to the symbol
helloin the top level nameset. There are other use for qualified names, like method call with an
instance.
Symbol binding
The transreserved keyword has been shown in all previous example. The reserved keyword transcreates or
set a symbol in the current nameset. For example, the form trans a 1is evaluated as follow. First, a
symbol named ais 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 1will set or create a symbol binding in reference to the
exampleobject. If examplerefers to a nameset, the symbol is bound in this nameset. If exampleis a class,
hellois bounded as a class symbol. In theory, there is no restriction to use transon any object. If the
object does not have a symbol binding capability, an exception is raised. For example, if nis an integer
object, the form trans n:i 1will fail. With 3 or 4 arguments, transdefines 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 constreserved 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 constworks 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, constdefines automatically a gamma expression. Like transthe 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 unrefreserved 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 maxgamma 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 argsis 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 nilor 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 ifreserved keyword takes two or three arguments. The first argument is the boolean condition to
check. If the condition evaluates to truethe 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 ifreserved 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 whilereserved 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 whileloop 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 uis 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 doreserved keyword is similar to the whilereserved 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 loopreserved 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 whileand doloop, the loopspecial 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 Counterobject. In this case, a counter is created with an initial and
final count values. The counter step-pmethod 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 printlnfunction is called after the step-ppredicate.
Switch special form
The switchreserved 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
elsecan 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 returnreserved 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 returnis 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 evalreserved keyword forces the evaluation of the object argument. The reserved keyword evalis
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, evalis more efficient than return. The protectreserved
keyword constructs an object without evaluating it. Typically when used with a form, protectreturn 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 assertreserved 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 assertis used for debugging purpose.
assert true (> 2 0)
assert 0 (- 2 2)
assert "true" (String true)
Block special form
The blockreserved keyword executes a form in a new local set. The local set is destroyed at the
completion of the execution. The blockreserved 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 blockreserved 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 Integerand Realclasses are primarily used to manipulate numbers. The
Booleanclass is used to for boolean operations. Other built-in objects include Characterand 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 Booleanclass is used to represent the boolean value trueand false. These last two symbols are built-
in in the interpreter as constant symbols. There are also special forms like not, andand or. Their usage
is self understandable.
not true
and true (== 1 0)
or (< -1 0) (> 1 0)
Predicates
A predicateis a function which returns a boolean object. There is always a built-in predicate associated
with a built-in object. By convention, a predicate terminates with the sequence -p. The nil-ppredicate is
a special predicate which returns true if the object is nil. The object-ppredicate is the negation of the
nil-ppredicate.
Predicate Description
nil-p check nil object
eval-p check evaluation
real-p check real object
regex-p check regex object
object-p check for non nil object
string-p check string object
number-p check number object
method-p check method object
boolean-p check boolean object
integer-p check integer object
character-p check character object
For example, one can write a function which returns trueif 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))
Special predicates for functional and symbolic programming are also built-in in the engine.
Predicate Description
class-p check class object
thread-p check thread object
promise-p check promise object
lexical-p check lexical object
literal-p check literal object
closure-p check closure object
nameset-p check nameset object
instance-p check instance object
qualified-p check qualified object
Finally, for each object, a predicate is also associated. For example, cons-pis the predicate for the
Consobject and vector-pis the predicate for the Vectorobject. Another issue related to evaluation, is to
decide whether or not an object can be evaluated. The predicate eval-pwhich is a special form is designed
to answer this question. Furthermore, the eval-ppredicate 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 classis a nameset which can be bounded automatically when an instanceof 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 Circleis created as a class object. With the help of a qualified
name, the symbol PIis created inside the class nameset. In this case, the symbol PIis 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 presetis 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 presetlambda expression is bound to the class. Since
presetis a reserved name for the class object, the form is automatically executed at the instance
construction. Second, note that the instance data member radiusis created by the lambda expression and
another reserved keyword called thisis 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 thisis 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 perimetersymbol 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, Listand Vectorare
typical iterable objects. There are two ways to iterate with these objects. The first method uses the
forreserved 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 forreserved keyword iterate on both object uand v. For each iteration, the symbol xand yare 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:get-iterator)
trans v-it (v:get-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 uand v. The iteration is done in a
whileloop by invoking the valid-ppredicate. The get-objectmethod returns the object value at the current
iterator position.
Exception
An exceptionis 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 tryexecutes 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 whatcan 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 whatstores the necessary information about the place that generated the exception.
Most of the time, the qualified name what:reasonor what:aboutis used.The only difference is that
what:aboutcontains the file name and line number associated with the reason that generated the exception.
Regular Expressions
A regular expression or regexis 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, $dis 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 regexcontains two
groups. The call to the operator ==returns trueif the regex matches the argument string. The getmethod
can be used to retrieve the group by index.
Delayed evaluation
The special form delaycreates a special object called a promisewhich records the form to be later
evaluated. The special form forcecauses a promise to be evaluated. Subsequent call with forcewill 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 synccan 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 waitwhich 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 waitmethod to make sure the thread has completed before checking for the flag value. It is
also worth to note that waitis one of the method which guarantees that a thread result is valid. Another
use of the waitmethod 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 variableis another mechanism to synchronize several threads. A condition variable is modeled
with the Condvarobject. At construction, the condition variable is initialized to false. A thread calling
the waitmethod will block until the condition becomes true. The markmethod 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.
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 argvis the argument vector. The symbol libraryis 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 and real 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
expressionor regexis 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 Relatifobject might be more appropriate. The Integerobject 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 0xand 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
Integerclass 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_0000is 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 Integerobject 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 Booleanresult.
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 Booleanobject.
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 Integerclass offers other convenient methods. The odd-pand even-pare predicates. The modtake one
argument and returns the modulo between the calling integer and the argument. The absmethods 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 Integerobject is a literal objectand a number object. As a literal object, the to-stringand to-
literalmethods are provided to obtain a string representation for the integer object. Although the to-
stringmethod returns a string representation of the calling integer, the to-literalmethod 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-hexaand to-
hexa-strignmethods are designed to obtained such representation. In the first form, the to-hexamethod
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 relatifor big number is an integer with infinite precision. The Relatifclass is similar to the
Integerclass except that it works with infinitely long number. The relatif notation uses a ror Rsuffix to
express a relatif number versus an integer one. The Relatifobject is a literal object that belongs to the
number class. The predicate associated with the Relatifobject 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 Integerclass operations are supported by the Relatifobject. 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 printlnmethod if you wish, but you have been warned...
Real number
The realclass 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 Realobject 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 decimalnotation. 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
Realclass 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 Integerobject, the Realobject 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 Realobject. These methods take either an integer or a real
as argument.
r:= 1.0 # 1.0
r:== 1.0 # true
r:!= 0.0 # true
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 xof 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, acoshand atanhare
also supported. The square root sqrtmethod return the square root of the calling real. The floorand
ceilingreturns 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-precisionmethod while the get-
absolute-precisionreturns the interpreter precision. There is also a set-relative-precisionand get-
relative-precisionmethods 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 formatmethod takes a precision argumentwhich 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-leftand fill-rightmethods 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, Relatifand Realobjects are all derived from the Numberobject which is a Literalobject. As
such, the predicate number-pis 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-hexaand to-hexa-stringmethods. For integer and real numbers, the
formatmethod 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 Characterobject 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 codesetare supported by the AFNIX engine. The characters are constructed
from the literal notation or by using an explicit character instance. The Characterclass 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+40is the @character while U+3A3is 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 Characterobject methods are also provided for arithmetic operations in a way similar to the
Integerclass.
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 Characterobject. 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 Characterobject comes with additional methods. These are mostly conversion methods and predicates.
The to-stringmethod returns a string representation of the calling character. The to-integermethod
returns an integer representation the calling character. The predicates are alpha-p, digit-p, blank-p,
eol-p, eos-pand 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 Stringobject 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 Stringobject provides numerous methods and operators. The most common ones are illustrated in the
example below. The lengthmethods 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 charactersused, for example, as diacritics. The non-combining-lengthmethod 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-leftand sub-rightmethods return a sub-string, given the position index. For sub-left, the index
is the terminating index, while sub-rightis 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-leftand strip-rightare methods used to strip blanks and tabs. The stripmethod combines
both strip-leftand strip-right.
# example of strip method
const str " hello world "
println (str:strip) # "hello world"
The splitmethod 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-leftand fill-rightmethods 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-upperand to-lowermethods. 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 hashidmethod 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.alsillustrates 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
xoringthe 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 regexis 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 Regexobject. A regular expression can be built implicitly
or explicitly with the use of the Regexobject. 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 Regexobject. The second form is the explicit
construction of the Regexobject. 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.
Character Description
$a matches any letter or digit
$b matches any blank characters
$c matches any combining alphanumeric
$d matches any digit
$e matches eol, cr and eos
$l matches any lower case letter
$n matches eol or cr
$s matches any letter
$u matches any upper case letter
$v matches any valid afnix constituent
$w matches any word constituent
$x matches any hexadecimal characters
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
sequencesbut 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.
Operator Description
* match 0 or more times
+ match 1 or more times
? match 0 or 1 time
| alternation
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 getmethod can be used to get the sub-string by index.
Regex object
Although a regex can be built implicitly, the Regexobject can also be used to build a new regex. The
argument is a string which is compiled during the object construction. A Regexobject is a literal object.
This means that the to-stringmethod is available and that a call to the printlnspecial 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 trueif
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 getmethod which returns by index the sub-string when a group has been
matched. The lengthmethod 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 matchmethod returns the first string which is matched by the regex.
const regex [$d+]
regex:match "Happy new year 2000" # 2000
The replacemethod 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 Regexoperators 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
12or 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, iterableobjects such like Cons,
Listand Vector. Special objects like Fifo, Queueand Bitsetare 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 Consobject or cons cellhave 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 cellfound in
traditional functional programming language. A Consobject 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 carand
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 Listdouble 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 protectingthe form from being interpreted.
const blist (protect ((1) ((2) ((3)))))
Cons cell methods
A Consobject provides several methods to access the carand the cdrof 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-carmethod set the car of the cons cell. The addmethod adds a new cons cell at the end of the cons
list and set the car with the specified object.
List object
The Listobject provides the facility of a double-link list. The Listobject is another example of iterable
object. The Listobject 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 Listcan
have a null size.
const nil-list (List)
const dbl-list (List 1 'a' "hello")
List methods
The Listobject methods are similar the Consobject. The addmethod adds an object at the end of the list.
The insertmethod 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 Vectorobject provides the facility of an index array of objects. The Vectorobject is another example
of iterable object. The Vectorobject 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 Vectorobject methods are similar to the Listobject. The addmethod appends an object at the end of the
vector. The setmethod 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 Setobject provides the facility of an object container. The Setobject is another example of iterable
object. The Setobject 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 Setobject methods are similar to the Vectorobject. The addmethod adds an object in the set. If the
object is already in the set, the object is not added. The lengthmethod 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 forkeyword iterates on one
or several objects and binds associated symbols during each step of the iteration process. All iterable
objects provides also the method get-iteratorwhich returns an iterator for a given object. The use of
iterator is justified during backward iteration, since foronly 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 linkmethod differs from the addmethod 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-productdoes 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 get-iteratormethod. At construction, the iterator is reset
to the beginning position. The get-objectmethod returns the object at the current iterator position. The
nextadvances the iterator to its next position. The valid-pmethod returns trueif the iterator is in a
valid position. When the iterator supports backward operations, the prevmethod move the iterator to the
previous position. Note that Consobjects do not support backward iteration. The beginmethod reset the
iterator to the beginning. The endmethod 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:get-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, Bitsetor
Fifoobjects.
Queue object
A queueis 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 Fifoand Queueobjects are somehow similar, with
the fundamental difference that the queue resize itself if needed.
# create a queue with objects
const q (Queue "hello" "world")
q:empty-p # false
q:length # 2
# dequeue some object
q:dequeue # hello
q:dequeue # world
q:empty-p # true
Bitset object
A bit setis 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 classas 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 objectis 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 Classwhich 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
namenotation.
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
constreserved 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 methodof 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 namenotation.
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 instanceof 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 thisis 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 thissymbol and bind some instance symbols. The arguments
which are passed during the instance construction are passed to the presetmethod.
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 thissymbol bound to the newly created instance. Note that the qualified name
this:reddefines 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 metais 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 metamust 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 thissymbol, that lambda is called an
instance method since the symbol thisis 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 localand 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 metasymbol, 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 trueif 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 Realobject 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 moduleand formatare 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-ppredicate is the perfect illustration of the use of the metareserved 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 presetmethod of that instance by setting the superdata
member. The superreserved 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 cis constructed. The super instance is with an integer object.
As a consequence, the instance is derived from the Integerinstance. 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 superdata 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 Integerinstance parent. Then the instance is re-
parentedwith a Stringinstance parent. The call to the to-stringmethod 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 mutemethod. 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 presetmethod is called automatically with the extra
arguments.
Instance inference
The ability to instantiate dynamically inferred instance is offered by the instance model. An instance
bis said to be inferred by the instance awhen the instance ais the super instance of the instance b. The
instance inference is obtained by binding the infersymbol 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 presetmethod is only called for the inferred instance if possible or the base
instance if there is no inferring class. Because the base presetpreset 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 presetmethod 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 deferenceis 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 deferentclass are visible from the base class, thus
making the deferentclass 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 exceptionis 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 throwaccepts 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
whatsymbol.
try {
...
} {
println "exception caught and re-thrown"
throw what
}
Exception handler
The special form tryexecutes 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 tryreserved 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 whatis bounded
with the exception object. In such environment, the exception can be evaluated.
Symbol Description
eid Exception id
name Exception file name
line Exception line number
about Exception extended reason
reason Exception reason
object Exception object
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 namesetreserved 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 delaycreates 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 aevaluates to the promise object.
Forcing a promise
The reserved keyword forcethe 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 Enumobject. Once built, enumeration item evaluates by literal
and returns an Itemobject.
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-enummethod.
# 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 Loggerclass 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 lengthmethod returns the number of logged messages. The get-messagemethod
returns a message by index. Because the system operates with a circular buffer, the get-messagemethod
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-
levelreturns the message level and the get-message-timereturns 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 symbols and methods bound to the interpreter.
Symbol Description
argv Command arguments vector
os-name Operating system name
os-type Operating system type
version Full version
loader The interpreter loader
resolver The interpreter resolver
afnix-uri Official uri name
program-name Interpreter program name
major-version Major version number
minor-version Minor version number
patch-version Patch version number
machine-size The interpreter machine size
Symbol Description
dup duplicate the interpreter
roll run the interpreter loop
wait Wait for normal threads
load Load a file and execute it
launch Launch a normal thread
daemon Launch a daemon thread
library Load and initialize a library
read-line Get an input stream line
read-passphrase Get an input stream passphrase
set-absolute-precision Set absolute precision
set-relative-precision Set relative precision
get-absolute-precision Get absolute precision
get-relative-precision Get relative precision
Arguments vector
The interp:argvqualified 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:versionqualified name. The full version is composed of the major, minorand
patchnumber. 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:loadmethod 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.
Method library
The interp:librarymethod 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 dupmethod. Without argument, a clone of the
current interpreter is made and a terminal object is attached to it. When used in conjunction with the
rollmethod, this approach permits to create an interactive interpreter. The dupmethod 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 dupmethod, this method provides a convenient way
to operate in interactive mode. The method is not called loopbecause 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 fileis 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 loadmethod.
Creating a librarian
The axlutility is the preferred way to create a librarian. Given a set of files, axlcombines 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 coption creates a new librarian. The librarian file name is specified with the foption.
zsh: axl -c -f librarian.axl file-1.als file-2.als
The previous command combines file-1.alsand file-2.alsinto 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 -ioption can be used to specify it. The -ioption accepts
either a directory name or a librarian file. Once the librarian has been opened, the interpreter
loadmethod 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 loadmethod is invoked.
Librarian contents
The axlutility provides the -tand -soptions to look at the librarian contents. The -soption returns all
file name in the librarian. The -toption 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 bitor any other useful things.
Librarian extraction
The -xoption 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 Librarianobject can be used as a convenient way to create a collection of files or to extract some of
them.
Output librarian
The Librarianobject 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 addmethod adds a new file into the librarian. The writemethod the full librarian as a single file
those name is writemethod argument.
Input librarian
With an argument, the librarian object is created in input mode. Once created, file can be read or
extracted. The lengthmethod -- which also work with an output librarian -- returns the number of files in
the librarian. The exists-ppredicate returns true if the file name argument exists in the librarian. The
get-namesmethod returns a vector of file names in this librarian. The extractmethod 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 resolveris 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 addmethod adds a directory path or a librarian file
to the resolver. The validmethod checks for the existence of a file. The lookupmethod 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 nilthread.
launch (nil)
There exists an alternate mechanism to start a thread with the reserved keyword launchand 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 launchcommand. The thread-ppredicates returns trueif 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 resultmethod. Although the result can be accessed
at any time, the returned value will be niluntil the thread as completed its execution.
const thr (launch (nil))
println (thr:result) # nilp
Although the engine will ensure that the result is niluntil 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.
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 lockingguarantees 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, varis initialized to 0. The incrthread increments varwhile the decrthread
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
const var 0
# 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 synccan 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 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 waitwhich 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 waitmethod to make sure the thread has completed before checking for the flag
value. It is also worth to note that waitis one of the method which guarantees that a thread result is
valid. Another use of the waitmethod 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 mathmodule
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 variableis another mechanism to synchronize several threads. A condition variable is modeled
with the Condvarobject. At construction, the condition variable is initialized to false. A thread calling
the waitmethod will block until the condition becomes true. The markmethod 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-unlockmethod. 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-unlockmethod
unlocks that lock when the main thread is restarted. Note also that the wait-unlockmethod reset the
condition variable. if the waitmethod was used instead of wait-unlockthe 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 Condvarclass has several methods which can be used to control the
behavior of the condition variable. Most of them are related to lock control. The resetmethod reset the
condition variable. The lockand unlockcontrol the condition variable locking. The mark, waitand wait-
unlockmethod 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 selfthat 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 nwith
the free variable x. The evaluation will succeeds if xis 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-printwill produce 3since it sums the argument nbound to 1, with the free variable
xwhich 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 xas 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-printwill print 3since xhas been closed with the value 2has 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 xis a free variable if the argument nis 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 protectreserved keyword is used to avoid the evaluation of the lexical object named
lex. Therefore the symbol symrefers to a lexical object. Since a lexical -- and a qualified -- object is
a also a literal object, the printlnreserved function will work and print the object name. In fact, a
literal object provides the to-stringmethod 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 constflag. 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 mapmethod 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-stringand to-literalmethods will return the symbol name.
Symbol methods are provided to access or modify the symbol values. It is also possible to change the
constsymbol flag with the set-constmethod.
(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-objectmethod.
(axi) const sym (Symbol "symbol")
(axi) println sym
symbol
(axi) sym:= 0
(axi) println (eval sym)
0
Closure
As an object, the Closurecan 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 transand constthat differentiate between
a lambda and a gamma expression. Once the closure object is defined, the set-formmethod 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 returnspecial 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.
AFNIX 2017-11-22 VOL-1(7)