NAME
vol-1 - afnix programmer’s guide
GETTING STARTED
This chapter is a quick introduction to the AFNIX Programming
Language. AFNIX is a multi-threaded functional programming language
with dynamic symbol bindings that supports the object oriented
paradigm. The language features a state of the art runtime engine that
runs on both 32 and 64 bits platforms. AFNIX is an interpreted
language with a rich syntax that makes the functional programming a
pleasant activity. When used interactively, commands are 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. The engine does not have a garbage collector but operates
with an immediate, scope based, object destruction mechanism. AFNIX
is a comprehensive set of application clients and modules. 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 domains.
The basic modules are the standard i/o module, the system module and
the networking module. 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. AFNIX operates with a set
of reserved keywords and predicates. Standard objects provide 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 language incorporates the
concept lambda expression with explicit closure. Symbol scope
limitation is an exclusive feature called gamma expression. Form like
notation with an easy block declaration is also an extension. The
object model provides a single inheritance mechanism with dynamic
symbol resolution. Unique features include instance re-parenting, class
rebinding and instance inference. Native class derivation and method
override is also part of the object model with fixed class objects and
forms. AFNIX implements a true multi-threaded engine with automatic
object protection mechanism against concurrent access. Read and write
locking system and thread activation via condition objects is also
built in the core system. 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 provides extensions. An extension is a
module or an application which is not installed by default. The user
selects during the installation process which extension is needed. For
example, the static version of the interpreter is an extension.
First programs
The fundamental AFNIX syntactic object is a form. A form is parsed
and immediately executed by the AFNIX engine. A form is generally
constructed with a function name and a set of arguments. The process of
executing a form is called the evaluation. As a simple program, the
traditional "hello world" program is shown below.
Hello world
The "hello world" program is de rigueur when introducing a new
programming language. Here is the AFNIX version of it.
(axi) println "hello world"
AFNIX is an interpreted language. It is possible to invoke the
interpreter and enter the above commands, or use a text editor and
execute the file. By convention, an AFNIX source file has the
extension .als. A simple session to run the above program -- assuming
the source file is called hello.als -- is shown below.
zsh> axi hello.als
hello world
It is also possible to invoke the interpreter as to enter the commands
interactively. The result will be the same. Simply typing ctrl-d will
exit the session. Another way to operate is to call the AFNIX
compiler called axc, and then invoke the interpreter with the compiled
file, even let the interpreter to figure this out. Note that the
interpreter assume the .axc for compiled file.
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 AFNIX interpreter can be invoked with several options, a file to
execute and some program arguments. The -h option prints the various
interpreter options.
zsh> axi -h
usage: axi [options] [file] [arguments]
[-h] print this help message
[-v] print version information
[-i] path add a path to the resolver
[-f] assert enable assertion checking
[-f] nopath do not set initial path
The -v option prints the interpreter version and operating system. The
-f option turn on or off some additional options like assertion
checking. The program arguments are illustrated later in this chapter.
The -i option add a path to the interpreter file path resolver. Several
-i options can be specified. The order of search is determined by the
option order. The use of the resolver combined with the librarian is
described in a specific chapter. If the initial file name to execute
contains a directory path, such path is added automatically to the
interpreter resolver path unless the nopath option is specified.
Interactive line editing
Line editing capabilities are supported when the interpreter is used
interactively. Error messages are displayed in red if the terminal
supports colors. The following table is a resume of the default key
bindings.
Binding Description
backspace Erase the previous character
delete Erase at the cursor position
insert Toggle insert with in-place
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
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
Command line arguments
The command line arguments to the interpreter are stored in a vector
called argv which is part of the interp class. A complete discussion
about class data member is covered in the class object chapter. The
example below which 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 class provides also the load method to load a source
file. The argument must be a valid file path or an exception is raised.
The load method returns nil. When the file is loaded, the interpreter
input, output and error streams are used. The load operation read one
form after another and executes them sequentially.
# load the source file fred.als
(axi) interp:load "fred.als"
If the file has been compiled, the axc extension can be used instead.
This force the interpreter to load the compiled version. If you are not
sure, or do not care about which file is loaded, the extension can be
omitted.
# load the compiled file fred.axc
(axi) interp:load "fred.axc"
# load whatever is found
(axi) interp:load "fred"
Without extension, the compiled file is searched first. If it is not
found the source file is searched and loaded.
The compiler
The client axc is the cross compiler. It generates a binary file that
can be run across platforms. The -h option prints the various compiler
options.
usage: axc [options] [files]
[-h] print this help message
[-v] print version information
[-i] path add a path to the resolver
One or several files can be specified on the command line. The source
file can be searched by the resolver by using the -i option.
builtin objects
AFNIX provides several builtin objects, namely Boolean, Integer, Real,
Character and String. A builtin object can be constructed literally for
each of these types. The best way to build such object is to bind it to
a symbol. The const and trans reserved 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.
const boolean true
const integer 1999
const real 2000.0
const string "afnix"
const char ’a’
None of the symbols -- or names -- used in the program above are
reserved keywords. In fact, the capitalize names are builtin objects
such like Integer or String. The const reserved keyword creates a
constant symbol and returns the last evaluated object. As a
consequence, nested const constructs are possible like trans b (const a
1). The trans reserved keyword declare a new non-constant symbol. That
is, the symbol can be changed. Note that it is the symbol which is
marked constant, not the object.
trans a-symbol "hello world"
trans a-symbol 2000
println a-symbol
Comments
Comments starts with the character #. All characters until the end of
line are consumed. Comments can be placed anywhere in the source file.
Comments entered during an interactive session are discarded.
Forms
The previous program was an illustration of the simplest form
declaration, referred as implicit form. 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. The previous programs could
have been written with the standard form notation instead of the
implicit one. The use of standard form notation versus the implicit is
one is a matter of style and readability. 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 for complex
expression.
const three (+ 1 2)
The previous program defines a symbol which is initialized with the
integer 3, that is the result of the computation (+ 1 2). The program
shows also that Polish notation is used for arithmetic. If fact, + is a
builtin 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, that
result is discarded.
const b (trans a (+ 1 2))
assert a 3
assert b 3
trans a 4
assert b 3
This program illustrates the mechanic of the evaluation process. The
evaluation is done recursively. The (+ 1 2) form is evaluated as 3 and
the result transmitted to the form (trans a 3). This form not only
creates the symbol a and binds to it the integer 3, but returns also 3
which is the result of the previous evaluation. Finally, the form
(const b 3) is evaluated, that is, the symbol b is created and the
result discarded. Internally, things are a little more complex, but the
idea remains the same. This program illustrates also the usage of the
assert keyword.
Lambda expression
A lambda expression is a function in the AFNIX terminology. The term
come historically from Lisp to express the fact that a lambda
expression is analog to the concept of expression found in the lambda
calculus. There are various ways to create a lambda expression. A
lambda expression is created with the trans reserved keywords. A lambda
expression takes 0 or more arguments and return 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 and placed on
the interpreter eval stack. The function is then called and the object
result is transmitted to the calling form. The use of trans vs const is
explain later. As an example, we define the factorial of an integer in
a recursive way.
# declare the factorial function
trans fact (n) (
if (== n 1) 1 (* n (fact (- n 1))))
# compute factorial 5
println "factorial 5 = " (fact 5)
This program calls for several comments. First the trans keyword
defines a new function object with one argument called n. The body of
the function is defined with the if reserved keyword and can be easily
understood. The function is called in the next form when the println
reserved keyword is executed. Note that here, the call to fact produces
an integer object, which is converted automatically by the println
keyword.
Block form
The notation used in the fact program is the standard form notation
originating from Lisp and the Scheme dialect. AFNIX offers another
notation called the block form notation with the use of the { and }
characters. A block form is a syntactic notation where each form in the
block form is executed sequentially. The form can be either an implicit
or a regular form. The fact procedure can be rewritten with the block
notation as illustrated below.
# declare the factorial procedure
trans fact (n) {
if (== n 1) 1 (* n (fact (- n 1)))
}
# compute factorial 5
println "factorial 5 = " (fact 5)
Another way to create a lambda expression is via the reserved keyword
lambda. Recall that a lambda expression is an object. So when such
object is created, it can be bounded to a symbol. The factorial example
could be rewritten with an explicit lambda call.
# declare the factorial procedure
const fact (lambda (n) (
if (== n 1) 1 (* n (fact (- n 1)))))
# compute factorial 5
println "factorial 5 = " (fact 5)
Note that here, the symbol fact is a constant symbol. The use of const
is rather reserved for 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. Afnix provides another mechanism called gamma
expression which binds only the function symbol set to the top level
one. The rest remains the same. Using a gamma expression can speedup
significantly the execution.
# declare the factorial procedure
const fact (n) (
if (== n 1) 1 (* n (fact (- n 1))))
# compute factorial 5
println "factorial 5 = " (fact 5)
We will come back later to the concept of gamma expression. The use of
the reserved keyword const to declare a gamma expression makes now
sense. Since most function definitions are constant with one level, it
was a language choice to implement this syntactic sugar. Note that
gamma is a reserved keyword and can be used to create a gamma
expression object. On the other hand, note that the gamma expression
mechanism does not work for instance method. We will illustrate this
point later in this book.
Lambda generation
A lambda expression can be used to generate another lambda expression.
In other word, a function can generate a function, hence the term that
AFNIX is a functional programming language. Suppose one might want to
write a function which take an argument and generate a function which
add this argument to the generated function argument -- got that! --,
then here is the implementation.
# 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)
The interesting part in the previous program is the concept of closed
variables. Looking at the lambda expression inside gen, notice that the
argument to the gamma is x while n is marked in a form before the body
of the gamma. This notation indicates that the gamma should retain the
value of the argument n when the closure is created. In the literature,
you might discover a similar mechanism referenced as a closure. A
closure is simply a variable which is closed under a certain context.
When a variable is reference in a context without any definition, such
variable is called a free variable. We will see later more programs
with closures. Note that in the AFNIX terminology, it is the object
created by the gamma call which is called a closure. Note also that the
same mechanism apply with lambda. In short, a lambda expression is a
function with or without closed variables, which works with nested
symbol sets also called namesets. A gamma expression is a function with
or without closed variable which is bounded to the top level nameset.
The reserved keyword trans binds a lambda expression. The reserved
keyword const binds a gamma expression. A gamma expression cannot be
used as an instance method.
Multiple arguments binding
A lambda or gamma expression can be defined to work with extra
arguments using the special args binding. During a lambda or gamma
expression execution, the special symbol args is defined with the extra
arguments passed at the call. For example, a gamma expression with 0
formal argument and 2 actual arguments has args defined as a cons cell.
const proc-nilp (args) {
trans result 0
for (i) (args) (result:+= i)
eval result
}
assert 3 (proc-nilp 1 2)
assert 7 (proc-nilp 1 2 4)
The symbol args can also be defined with formal arguments. In that
case, args is defined as a cons cell with the remaining actual
arguments.
# check with arguments
const proc-args (a b args) {
trans result (+ a b)
for (i) (args) (result:+= i)
eval result
}
assert 3 (proc-args 1 2)
assert 7 (proc-args 1 2 4)
It is an error to specify formal arguments after args. Multiple args
formal definition are not allowed. The symbol args can also be defined
as a constant argument.
# check with arguments
const proc-args (a b (const args)) {
trans result (+ a b)
for (i) (args) (result:+= i)
eval result
}
assert 7 (proc-args 1 2 4)
Nameset and bindings
A nameset is a container of bindings between a name and symbolic
variable. We use the term symbolic variable to denote any binding
between a name and an object. There are various ways to express such
bindings. The common one in AFNIX 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, AFNIX has various
type of namesets. The top level nameset is called a global set and is
designed to handle a large number of symbols. In a lambda or gamma
expression, the nameset is called a local set and is designed to be
fast with a small number of symbols. The moral of this little story is
to think always in terms of namesets, no matter how it is implemented.
All namesets support the concept of parent binding. When a nameset is
created (typically during the execution of a lambda expression), this
nameset is linked with its parent one. This means that a symbol lookup
is done by traversing all nameset from the bottom to the top and
stopping when one is found. In term of AFNIX notation, the current
nameset is referenced with the special symbol .. The parent nameset is
referenced with the special symbol ... The top level nameset is
referenced with the symbol ....
Symbol
A symbol is an object which defines a binding between a name and an
object. When a symbol is evaluated, the evaluation process consists in
returning the associated object. There are various ways to create or
set a symbol, and the different reserved keywords account for the
various nature of binding which has to be done depending on the current
nameset state. One of the symbol property is to be const or not. When a
symbol is marked as a constant, it cannot be modified. Note here that
it is the symbol which is constant, not the object. A symbol can be
created with the reserved keywords const or trans.
Creating a nameset
A nameset is an object which can be constructed directly by using the
object construction notation. Once the object is created, it can be
bounded to a symbol. Here is a nameset called example in the top level
nameset.
# create a new nameset called example
const example (nameset .)
# bind a symbol in this nameset
const example:hello "hello"
println example:hello
Qualified name
In the previous example, a symbol is referenced in a given nameset by
using a qualified name like example:hello. A qualified name define a
path to access a symbol. The use of qualified name is a powerful
notation to reference an object in reference to another object. For
example, the qualified name .:hello refers to the symbol hello in the
current nameset. The qualified name ...:hello refers to the symbol
hello in the top level nameset. There are other use for qualified
names, like method call with an instance.
Symbol binding
The trans reserved keyword has been shown in all previous example. The
reserved keyword trans creates or set a symbol in the current nameset.
For example, the form trans a 1 is evaluated as follow. First, a symbol
named a is searched in the current nameset. At this stage, two
situations can occur. If the symbol is found, it is set with the
corresponding value. If the symbol is not found, it is created in the
current nameset and set. The use of qualified name is also permitted --
and encouraged -- with trans. The exact nature of the symbol binding
with a qualified name depends on the partial evaluation of the
qualified name. For example, trans example:hello 1 will set or create a
symbol binding in reference to the example object. If example refers to
a nameset, the symbol is bound in this nameset. If example is a class,
hello is bounded as a static symbol for that class. In theory, there is
no restriction to use trans on any object. If the object does not have
a symbol binding capability, an exception is raised. For example, if n
is an integer object, the form trans n:i 1 will fail. With 3 or 4
arguments, trans defines automatically a lambda expression. This
notation is a syntactic sugar. The lambda expression is constructed
from the argument list and bounded to the specified symbol. The rule
used to set or define the symbol are the same as described above.
# create automatically a lambda expression
trans min (x y) (if (< x y) x y)
Constant binding
The const reserved keyword is similar to trans, except that it creates
a constant symbol. Once the symbol is created, it cannot be changed.
This constant property is hold by the symbol itself. When trying to set
a constant symbol, an exception is raised. The reserved keyword const
works also with qualified names. The rules described previously are the
same. When a partial evaluation is done, the partial object is called
to perform a constant binding. If such capability does not exist, an
exception is raised. With 3 or 4 arguments, const defines automatically
a gamma expression. Like trans the rule are the same except that the
symbol is marked constant.
# create automatically a gamma expression
const max (x y) (if (> x y) x y)
Arguments
An expression argument is similar to a symbol, except that it is used
only with function argument. The concept of binding between a name and
an object is still the same, but with an argument, the object is not
stored as part of the argument, but rather at another location which is
the execution stack. An argument can also be constant. On the other
hand, a single argument can have multiple bindings. Such situation is
found during the same function call in two different threads. An
argument list is part of the lambda or gamma expression declaration. If
the argument is defined as a constant argument a sub form notation is
used to defined this matter. For example, the max gamma expression is
given below.
# create a gamma expression with const argument
const max (gamma ((const x) (const y)) (if (> x y) x y))
A special symbols named args is defined during a lambda or gamma
expression evaluation with the remaining arguments passed at the time
the call is made. The symbol can be either nil or bound to a list of
objects.
const proc-args (a b) {
trans result (+ a b)
for (i) (args) (result:+= i)
eval result
}
assert 3 (proc-args 1 2)
assert 7 (proc-args 1 2 4)
Control flow
AFNIX provides various reserved keywords which can be seen as standard
imperative statements. Such statements are useful to write readable
programs but are not necessary the best in terms of efficiency. In most
cases, a statement returns the last evaluated object. Most of the
statements are control flow statements.
If statement
The if reserved keyword takes two or three arguments. The first
argument is the boolean condition to check. If the condition evaluates
to true the second argument is evaluated. The form return the result of
such evaluation. If the condition evaluates to false, the third
argument is evaluated or nil is returned if it does not exist. An
interesting example which combines the if reserved keyword and a deep
recursion is the computation of the Fibonacci sequence.
const fibo (gamma (n) (
if (< n 2) n (+ (fibo (- n 1)) (fibo (- n 2))))
While statement
The while reserved keyword takes 2 or 3 arguments. With 2 arguments,
the loop is constructed with a condition and a form. With 3 arguments,
the first argument is an initial condition that is executed only once.
When an argument acts as a loop condition, the condition evaluate to a
boolean. The loop body is executed as long as the boolean condition is
true. An interesting example related to integer arithmetic with a while
loop is the computation of the greatest common divisor or gcd.
const gcd (u v) {
while (!= v 0) {
trans r (u:mod v)
u:= v
v:= r
}
eval u
}
Note in this previous example the use of the symbol =. The qualified
name u:= is in fact a method call. Here, the integer u is assigned with
a value. In this case, the symbol is not changed. It is the object
which is muted. In the presence of 3 arguments, the first argument is
an initialization condition that is executed only once. In this mode,
it is important to note that the loop introduce its own nameset. The
loop condition can be used to initialize a local condition variable.
while (trans valid (is:valid-p)) (valid) {
# do something
# adjust condition
valid:= (and (is:valid-p) (something-else))
}
Do statement
The do reserved keyword is similar to the while reserved keyword,
except that the loop condition is evaluated after the body execution.
The syntax call is opposite to the while. The loop can accept either 2
or 3 arguments. With 2 arguments, the first argument is the loop body
and the second argument is the exit loop condition. With 3 arguments,
the first argument is the initial condition that is executed only once.
# count the number of digits in a string
const number-of-digits (s) {
const len (s:length)
trans index 0
trans count 0
do {
trans c (s:get index)
if (c:digit-p) (count:++)
} (< (index:++) len)
eval count
}
Loop statement
The loop reserved keyword is another form of loop. It take four
arguments. The first is the initialize form. The second is the exit
condition. The third is the step form and the fourth is the form to
execute at each loop step. Unlike the while and do loop, the loop
statement 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)
Switch statement
The switch reserved keyword is a condition selector. The first argument
is the switch selector. The second argument is a list of various value
which can be matched by the switch value. A special symbol called else
can be used to match any value.
# return the primary color in a rgb
const get-primary-color (color value) (
switch color (
("red" (return (value:substr 0 2)))
("green" (return (value:substr 2 4)))
("blue" (return (value:substr 4 6)))
)
)
Return statement
The return reserved keyword indicates an exceptional condition in the
flow of execution within a lambda or gamma expression. When a return is
executed, the associated argument is returned and the execution
terminates. If return is used at the top level, the result is simply
discarded.
# initialize a vector with a value
const vector-init (length value) {
# treat nil vector first
if (<= length 0) return (Vector)
trans result (Vector)
do (result:add value) (> (length:--) 0)
}
Eval and protect
The eval reserved keyword forces the evaluation of the object argument.
The reserved keyword eval is typically used in a function body to
return a particular symbol value. It can also be used to force the
evaluation of a protected object. In many cases, eval is more efficient
than return. The protect reserved keyword constructs an object without
evaluating it. Typically when used with a form, protect return the form
itself. It can also be used to prevent a symbol evaluation. When used
with a symbol, the symbol object itself is returned.
const add (protect (+ 1 2))
(eval add)
Note that in the preceding example that the evaluation will return a
lambda expression which is evaluated immediately and which return the
integer 3.
Assert statement
The assert reserved keyword check for equality between the two
arguments and abort the execution in case of failure. By default, the
assertion checking is turn off, and can be activated with the command
option -f assert. Needless to say that assert is used for debugging
purpose.
assert true (> 2 0)
assert 0 (- 2 2)
assert "true" (String true)
Block statement
The block reserved keyword executes a form in a new local set. The
local set is destroyed at the completion of the execution. The block
reserved keyword returns the value of the last evaluated form. Since a
new local set is created, any new symbol created in this nameset is
destroyed at the completion of the execution. In other word, the block
reserved keyword allows the creation of a local scope.
trans a 1
block {
assert a 1
trans a (+ 1 1)
assert a 2
assert ..:a 1
}
assert 1 a
builtin objects
AFNIX provides several builtin objects and builtin operators for
arithmetic and logical operations. The Integer and Real classes are
primarily used to manipulate numbers. The Boolean class is used to for
boolean operations. Other builtin objects include Character and String.
The exact usage of these classes is described in the next chapter.
Arithmetic operations
AFNIX provides various ways to perform arithmetic operations. Most of
the operations are done with the +, -, * and / operators. Each of these
operators works with both integer and real numbers.
(+ 1 2)
(- 1)
(* 3 5.0)
(/ 4.0 2)
Logical operations
The Boolean class is used to represent the boolean value true and
false. These last two symbols are builtin in the interpreter as
constant symbols. AFNIX provides also some reserved keywords like
not, and and or. Their usage is self understandable.
not true
and true (== 1 0)
or (< -1 0) (> 1 0)
Predicates
A predicate is a function which returns a boolean object. AFNIX
provides several predicates to check for some builtin objects. By
convention, a predicate terminates with the sequence -p. The nil-p
predicate is a special predicate which returns true if the object is
nil. AFNIX provides a predicate for each builtin objects.
Predicate Description
nil-p check nil object
eval-p check evaluation
real-p check real object
regex-p check regex object
string-p check string object
number-p check number 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 true if the
argument is a number, that is, an integer or a real number.
# return true if the argument is a number
const number-p (n) (
or (integer-p n) (real-p n))
Predicates for functional and symbolic programming are also builtin
into the AFNIX 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-p is the predicate for the Cons object.
Predicate Description
cons-p check a cons object
list-p check for a list object
queue-p check a queue object
bitset-p check a bitset object
vector-p check a vector object
Another issue related to evaluation, is to decide whether or not an
object can be evaluated. The predicate eval-p which is a special form
is designed to answer this question. Furthermore, the eval-p predicate
is useful to decide whether or not a symbol is defined or if a
qualified name can be evaluated.
assert true (eval-p .)
assert false (eval-p an-unknown-symbol)
Class and Instance
AFNIX provides support for the object oriented programming paradigm. A
class in the AFNIX terminology is a nameset which can be bounded
automatically when an instance of that class is created. Compared to
other language, there is no need to declare the data member for a
particular class. Data members are created during the instance
construction. A class allows an instance to call function with the
instance nameset visible for that function.
Class and members
A class is declared with the reserved keyword class. The class acts
like a nameset. Functions can be bounded to this class.
const Color (class)
const Color:BLACK "#000000"
const Color:WHITE "#FFFFFF"
Any object can be bounded as a data member, including lambda
expressions.
const Color (class)
const Color:get-primary-from-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
}
Instances
An instance of a class is created like any builtin object. If a method
called preset is defined for that class, the method is used as an
initializer of that instance.
const Color (class)
trans Color:preset (red green blue) {
const this:red (Integer red)
const this:green (Integer green)
const this:blue (Integer blue)
}
const red (Color 255 0 0)
const green (Color 0 255 0)
const blue (Color 0 0 255)
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. The instance nameset defines the instance data
members and the special symbol this.
const int-max (x y)
if (> x y) (Integer x) (Integer y))
const Color:RED-FACTOR 0.75
const Color:GREEN-FACTOR 0.75
const Color:BLUE-FACTOR 0.75
trans Color:get-darker nil {
trans red (int-max (this:red:* Color:RED-FACTOR) 0)
trans green (int-max (this:green:* Color:GREEN-FACTOR) 0)
trans red (int-max (this:blue:* Color:BLUE-FACTOR) 0)
Color red green blue
}
# get a darker color than red
const dark-red (red:get-darker)
Miscellaneous features
AFNIX provides several facilities for control flow and exceptional
operations. Most of these features are available via the use of
reserved keywords.
Iteration
An iteration facility is provided for some objects known as iterable
objects. The Cons, List and Vector are typical iterable objects. There
are two ways to iterate with these objects. The first method uses the
for reserved keyword. The second method uses an explicit iterator which
can be constructed by the object.
# compute the scalar product of two vectors
const scalar-product (u v) {
trans result 0
for (x y) (u v) (result:+= (* x y))
eval result
}
The for reserved keyword iterate on both object u and v. For each
iteration, the symbol x and y are set with their respective object
value. In the example above, the result is obtained by summing all
intermediate products.
# test the scalar product function
const v1 (Vector 1 2 3)
const v2 (Vector 2 4 6)
(scalar-product v1 v2)
The iteration can be done explicitly by creating an iterator for each
vectors and advancing steps by steps.
# scalar product with explicit iterators
const scalar-product (u v) {
trans result 0
trans u-it (u: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 u
and v. The iteration is done in a while loop by invoking the valid-p
predicate. The get-object method returns the object value at the
current iterator position.
Exception
An exception is an unexpected change in the execution flow. The AFNIX
model for exception 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. 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. The
reserved keyword try executes a form and catch an exception if one has
been thrown. With one argument, the form is executed and the result is
the result of the form execution unless an exception is caught. If an
exception is caught, the result is the exception object. If the
exception is a native one, the result is nil.
try (+ 1 2)
try (throw)
try (throw "hello")
try (throw "hello" "world")
try (throw "hello" "world" "folks")
The exception mechanism is also designed to install an exception
handler and eventually retrieve some information from the exception
object. The reserved symbol what can be used to retrieve some exception
information.
# protected factorial
const fact (n) {
if (not (integer-p n)) (throw "number-error" "invalid argument")
if (== n 0) 1 (* n (fact (- n 1)))
}
# exception handler
const handler nil {
errorln what:eid ’,’ what:reason
}
(try (fact 5) handler)
(try (fact "hello") handler)
Delayed evaluation
The AFNIX interpreter provides a special mechanism to delay an
evaluation. The reserved keyword delay creates a special object called
a promise which records the form to be later evaluated. The reserved
keyword force causes a promise to be evaluated. Subsequent call with
force will produce the same result.
trans y 3
const l ((lambda (x) (+ x y)) 1)
assert 4 (force l)
trans y 0
assert 4 (force l)
Regular Expressions
The AFNIX interpreter provides a builtin mechanism for regular
expression. A regex is an object which is used to match certain text
patterns. Regular expressions are built implicitly by the AFNIX
reader wit the use of the [ and ] characters.
if (== (const re [($d$d):($d$d)]) "12:31") {
trans hr (re:get 0)
trans mn (re:get 1)
}
In the previous example, a regular expression object is bound to the
symbol re. The regex contains two groups. The call to the operator ==
returns true if the regex matches the argument string. The get method
can be used to retrieve the group by index.
Threads
The AFNIX interpreter provides a powerful mechanism which allows the
concurrent execution of forms and the synchronization of shared
objects. There are two types of threads, namely normal thread and
daemon thread. They differ only by the interpreter exit condition. The
interpreter will wait until all normal threads are completed. On the
other hand, the interpreter will not wait for daemon threads. They are
automatically stopped when all normal threads are finished. Normal
threads are created with the reserved keyword launch, and daemon
threads are created with the reserved keyword daemon. When threads are
used, 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, AFNIX provides an automatic synchronization mechanism for
reading or writing an object, it is sometimes necessary to control the
execution flow. There are basically two techniques to do so. First,
protect a form from being executed by several threads. Second, wait for
one or several threads to complete their task before going to the next
execution step. The reserved keyword sync can be used to synchronize a
form. When a form, is synchronized, the AFNIX engine guarantees that
only one thread will execute this form.
const print-message (code mesg) (
sync {
errorln "error : " code
errorln "message: " mesg
}
)
The previous example create a gamma expression which make sure that
both the error code and error message are printed in one group, when
several threads call it.
Thread completion
The other piece of synchronization is the thread completion indicator.
The thread descriptor contains a method called wait which suspend the
calling thread until the thread attached to the descriptor has been
completed. If the thread is already completed, the method returns
immediately.
# simple flag
const flag false
# simple shared tester
const ftest (val) (flag) (assert val (flag:shared-p))
# no thread mean not shared
ftest false
# in a thread it is shared
const thr (launch (ftest true))
thr:wait
assert true (flag:shared-p)
This example is taken from the test suites. It checks that a closed
variable becomes shared when started in a thread. Note the use of the
wait method to make sure the thread has completed before checking for
the shared flag. It is also worth to note that wait is one of the
method which guarantees that a thread result is valid. Another use of
the wait method can be made with a vector of thread descriptors when
one wants to wait until all of them have completed.
# shared vector of threads descriptors
const thr-group (Vector)
# wait until all threads in the group are finished
const wait-all nil (for (thr) (thr-group) (thr:wait))
Condition variable
A condition variable is another mechanism to synchronize several
threads. A condition variable is modeled with the Condvar object. At
construction, the condition variable is initialized to false. A thread
calling the wait method will block until the condition becomes true.
The mark method can be used by a thread to change the state of a
condition variable and eventually awake some threads which are blocked
on it. The use of condition variable is particularly recommended when
one need to make sure a particular thread has been doing a particular
task.
NUMBERS AND STRINGS
This chapters covers in detail the builtin objects used to manipulate
numbers and strings. First the integer, relatif and real numbers are
described. AFNIX offers a broad range of methods for these three
objects to 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, that is objects that have a
string representation.
Integer number
The fundamental number representation is the Integer. The integer is a
64 bits signed 2’s complement number. Even when running with a 32 bits
machine, the 64 bits representation is used. If a larger representation
is needed, the Relatif object might be more appropriate.
Integer format
The default literal format for an integer is the decimal notation. The
minus sign (without blank) indicates a negative number. Hexadecimal and
binary notations can also be used with prefix 0x and 0b. The underscore
character can be used to make the notation more readable.
const a 123
trans b -255
const h 0xff
const b 0b1111_1111
Integer number are constructed from the literal notation or by using an
explicit integer instance. The Integer class offers standard
constructors. The default constructor creates an integer object and
initialize it to 0. The other constructors take either an integer, a
real number, a character or a string.
const a (Integer)
const b (Integer 2000)
const c (Integer "23")
When the hexadecimal or binary notation is used, care should be taken
to avoid a negative integer. For example, 0x_8000_0000_0000_0000 is the
smallest negative number.
Integer arithmetic
Standard arithmetic operators are available as builtin 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 builtin object, the Integer object offers various methods for
builtin arithmetic which directly operates on the object. The following
example illustrates these methods.
trans i 0
i:++
i:--
i:+ 4
i:= 4
i:- 1
i:* 2
i:/ 2
i:+= 1
i:-= 1
i:*= 2
i:/= 2
As a side effect, these methods allows a const symbol to be modified.
Since the methods operates on an object, they do not modify the state
of the symbol. Such methods are called mutable methods.
const i 0
i:= 1
Integer comparison
The comparison operators works the same. The only difference is that
they always return a Boolean result. The comparison operators are
namely equal ==, not equal !=, less than <, less equal <=, greater >
and greater equal >=. These operators take two arguments.
== 0 1
!= 0 1
Like the arithmetic methods, the comparison operators are supported as
object methods. These methods return a Boolean object.
i:= 1
i:== 1
i:!= 0
Integer calculus
Armed with all these functions, it is possible to develop a battery of
functions operating with numbers. As another example, we revisit the
Fibonacci sequence as demonstrated in the introduction chapter. Such
example was terribly slow, because of the double recursion. Another
method suggested by Springer and Friedman uses two functions to perform
the same job.
const fib-it (gamma (n acc1 acc2) (
if (== n 1) acc2 (fib-it (- n 1) acc2 (+ acc1 acc2))))
const fiboi (gamma (n) (
if (== n 0) 0 (fib-it n 0 1)))
This later example is by far much faster, since it uses only one
recursion. Although, it is no the fastest way to write it, but nobody
is going to question the elegant aspect of recursion.
Other Integer methods
The Integer class offers other convenient methods. The odd-p and even-p
are predicates. The mod take one argument and returns the modulo
between the calling integer and the argument. The to-string method
returns a string representation of the integer. The abs methods returns
the absolute value of the calling integer.
i:even-p
i:odd-p
i:mod 2
i:= -1
i:abs
i:to-string
Relatif number
A relatif or big-num is an integer with infinite precision. The Relatif
class is similar to the Integer class except that it works with
infinitely long number. The relatif notation uses a r or R suffix to
express a relatif number versus an integer one.
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 Intege class operations are supported by the Relatif
object. The only difference is that there is no limitation on the
number size. This naturally comes with a computational price. An
amazing example is to compute the biggest know prime Mersenne number.
The world record exponent is 6972593. The number is therefore:
const i 1R
const m (- (i:shl 6972593) 1)
This number has 2098960 digits. You can use the println method if you
wish, but you have been warned...
Real number
The real class implements the representation for floating point number.
The internal representation is machine dependent, and generally follows
the double representation with 64 bits as specified by the IEEE
754-1985 standard for binary floating point arithmetic. All integer
operations are supported for real numbers.
Real format
The AFNIX reader supports two types of literal representation for
real number. The first representation is the dotted decimal notation.
The second notation is the scientific notation.
const a 123.0 # a positive real
const b -255.5 # a negative real
const c 2.0e3 # year 2000.0
Real number are constructed from the literal notation or by using an
explicit real instance. The Real class offers standard constructors.
The default constructor creates a real number object and initialize it
to 0.0. The other constructors takes either an integer, a real number,
a character or a string.
Real arithmetic
The real arithmetic is similar to the integer one. When an integer is
added to a real number, that number is automatically converted to a
real and vice versa. Ultimately, a pure integer operation might
generate a real result.
+ 1999.0 1 # 2000.0
+ 1999.0 1.0 # 2000.0
- 2000.0 1 # 1999.0
- 2000.0 1.0 # 1999.0
* 1000 2.0 # 2000.0
* 1000.0 2.0 # 2000.0
/ 2000.0 2 # 1000.0
/ 2000.0 2.0 # 1000.0
Like the Integer object, the Real object has arithmetic builtin
methods.
trans r 0.0 # 0.0
r:++ # 1.0
r:-- # 0.0
r:+ 4.0 # 4.0
r:= 4.0 # 4.0
r:- 1.0 # 3.0
r:* 2.0 # 8.0
r:/ 2.0 # 2.0
r:+= 1.0 # 5.0
r:-= 1.0 # 4.0
r:*= 2.0 # 8.0
r:/= 2.0 # 4.0
Real comparison
The comparison operators works as the integer one. As for the other
operators, an implicit conversion between an integer to a real is done
automatically.
== 2000 2000 # true
!= 2000 1999 # true
Comparison methods are also available for the Real object. These
methods take either an integer or a real as argument.
r:= 1.0 # 1.0
r:== 1.0 # true
r:!= 0.0 # true
A complex example
One of the most interesting point with functional programming language
is the ability to create complex computation function. For example,
let’s assume we wish to compute the value at a point x of the Legendre
polynomial of order n. One of the solution is to encode the function
given its order. Another solution is to compute the function and then
compute the value.
# legendre polynomial order 0 and 1
const lp-0 (gamma (x) 1)
const lp-1 (gamma (x) x)
# legendre polynom 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 polynom
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 polynom until it is constructed. As
an exercise, try to use a lambda expression instead of a gamma one, and
compare the execution result with large number of n. Note also that we
have here a double recursion.
Other real methods
The real numbers are delivered with a battery of functions. These
include the trigonometric functions, the logarithm and couple others.
Hyperbolic functions like sinh, cosh, tanh, asinh, acosh and atanh are
also supported. The square root sqrt method return the square root of
the calling real. The floor and ceiling returns respectively the floor
and the ceiling of the calling real.
const r0 0.0 # 0.0
const r1 1.0 # 1.0
const r2 2.0 # 2.0
const rn -2.0 # -2.0
const rq (r2:sqrt) # 1.414213
const pi 3.1415926 # 3.141592
rq:floor # 1.0
rq:ceiling # 2.0
rn:abs # 2.0
r1:log # 0.0
r0:exp # 1.0
r0:sin # 0.0
r0:cos # 1.0
r0:tan # 0.0
r0:asin # 0.0
pi:floor # 3.0
pi:ceiling # 4.0
Accuracy and formatting
Real numbers are not necessarily accurate, nor precise. The accuracy
and precision are highly dependent on the hardware as well as the
nature of the operation being performed. In any case, never assume that
a real value is an exact one. Most of the time, a real comparison will
fail, even if the numbers are very close together. When comparing real
numbers, it is preferable to use the ?= operator. Such operator result
is bounded by the internal precision representation and will generally
return the desired value. The real precision is an interpreter value
which is set with the set-epsilon method while the get-epsilon returns
the interpreter precision. Note also that the Real object bind the data
member EPSILON, which can be use with the qualified name Real:EPSILON.
By default, the precision is set to 0.00001.
interp:set-epsilon 0.0001
const r 2.0
const s (r:sqrt) # 1.4142135
(s:?= 1.4142) # true
Real number formatting is another story. The format method takes a
precision argument which indicates the number of digits to print for
the decimal part. Note that the format command might round the result
as indicated in the example below.
const pi 3.1415926535
pi:format 3 # 3.142
If additional formatting is needed, the Stringfill-left and fill-right
methods can be used.
const pi 3.1415926535 # 3.1415926535
const val (pi:format 4) # 3.1416
(val:fill-left ’0’ 9) # 0003.1416
Character
The Character object is another builtin object of the AFNIX engine. 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, AFNIX differs substantially from other functional language
where the quote protect a form.
const LA01 ’a’ # the character a
const ND10 ’0’ # the digit 0
All characters from the Unicode codeset are supported by the AFNIX
engine. The characters are constructed from the literal notation or by
using an explicit character instance. The Character class offers
standard constructors. The default constructor creates a null
character. The other constructors take either an integer, a character
or a string. The string can be either a single quoted character or the
literal notation based on the U+ notation in hexadecimal. For example,
U+40 is the @ character while U+3A3 is the greek sigma capital letter.
const nilc (Character) # null character
const a (Character ’a’) # a
const 0 (Character 48) # 0
const mul (Character "*") # *
const div (Character "U+40") # @
Character arithmetic
A character is like an integer, except that it operates in the range 0
to 0x7FFFFFFF. The character arithmetic is simpler compared to the
integer one and no overflow or underflow checking is done. Note that
the arithmetic operations take an integer as an argument.
+ ’a’ 1 # ’b’
- ’9’ 1 # ’8’
Several Character object methods are also provided for arithmetic
operations in a way similar to the Integer class.
trans c ’a’ # ’a’
c:++ # ’b’
trans c ’9’ # ’9’
c:-- # ’8’
c:+ 1 # ’9’
c:- 9 # ’0’
Character comparison
Comparison operators are also working with the Character object. The
standard operators are namely equal ==, not equal !=, less than <, less
equal <=, greater > and greater equal >=. These operators take two
arguments.
== ’a’ ’b’ # false
!= ’0’ ’1’ # true
Other character methods
The Character object comes with additional methods. These are mostly
conversion methods and predicates. The to-string method returns a
string representation of the calling character. The to-integer method
returns an integer representation the calling character. The predicates
are alpha-p, digit-p, blank-p, eol-p, eof-p and nil-p.
const LA01 ’a’ # ’a’
const ND10 ’0’ # ’0’
LA01:to-string # "a"
LA01:to-integer # 97
LA01:alpha-p # true
ND10:digit-p # true
String
The String object is one of the most important builtin 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
With strings, numerous methods can be provided. We illustrate here the
most common one.
const h "hello"
h:length # 5
h:get 0 # ’h’
h:== "world" # false
h:!= "world" # true
h:+= " world" # "hello world"
The sub-left and sub-right methods return a sub-string, given the
position index. For sub-left, the index is the terminating index, while
sub-right is the starting index, counting from 0.
const msg "hello world"
msg:sub-left 5 # "hello"
msg:sub-right 6 # "world"
The strip, strip-left and strip-right are methods used to strip blanks
and tabs. The strip method combines both strip-left and strip-right.
The split method returns a vector of strings by splitting the string
according to a break sequence. By default, the break sequence is the
blank, tab and newline characters. The break sequence can be one or
more characters passed as one single argument to the method.
const str "hello:world"
const vec str:split ":" # "hello" "world"
println (vec:length) # 2
The fill-left and fill-right methods can be used to fill a string with
a character up to a certain length. If the string is longer than the
length, nothing happens.
const pi 3.1415926535 # 3.1415926535
const val (pi:format 4) # 3.1416
val:fill-left ’0’ 9 # 0003.1416
String hash value
Computing the hash value of a string is an interesting problem. The
algorithm used by the AFNIX engine is shown as an example below. Note
that the hashid method is built in the String object. The program shows
both internal and computed values.
# 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
}
When run, example 0203.als, the following result is obtained with a 32
bits machine.
# test our favorite string
const hello "hello world"
hello:hashid # 1054055120
hashid hello # 1054055120
As a side note, it is recommended to print the shift amount in the
program. One may notice, that the value remains bounded by 24. Since we
are "xoring" the final value, it does illustrate that the algorithm is
design for a 32 bits machine. With a 64 bits machine the algorithm is
slightly modified to use the extra space. This also means that the
hashid value is not portable across platforms.
CONTAINER OBJECTS
This chapter covers the builtin container objects and more
specifically, iterable objects such like Cons, List and Vector. Special
objects like Queue and Bitset are mentioned at the end.
Cons builtin object
Originally, a Cons object or cons cell have been the fundamental object
of the Lisp or Scheme machine. The cons cell is the building block for
list and is of great importance in AFNIX as well. A Cons object is a
simple element used to build linked list. The cons cell holds an object
and a pointer to the next cons cell. The cons cell object is called car
and the next cons cell is called the cdr. This notation, found in Lisp
in maintained here for the sake of tradition.
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 from the AFNIX reader. Since
all internal forms are built with cons cell, the construction can be
achieved by simply protecting the form from being interpreted.
const blist (protect ((1) ((2) ((3)))))
Cons cell methods
A Cons object provides several methods to access the car and the cdr of
a cons cell. Other methods allows access to a list by index.
const c (Cons "hello" "world")
c:length # 2
c:get-car # "hello"
c:get-cadr # "world"
c:get 0 # "hello"
c:get 1 # "world"
The set-car method set the car of the cons cell. The append method
appends a new cons cell at the end of the cons list and set the car
with the specified object.
List builtin object
The List builtin object provides the facility of a double-link list.
The List object is another example of iterable object. The List object
provides support for forward and backward iteration.
List construction
A list is constructed like a cons cell with zero or more arguments.
Unlike the cons cell, the List can have a null size.
const nil-list (List)
const dbl-list (List 1 ’a’ "hello")
List methods
The List object methods are similar the Cons object. The append method
appends an object at the end of the list. The insert method inserts an
object at the beginning of the list.
const list (List "hello" "world")
list:length # 2
list:get 0 # "hello"
list:get 1 # "world"
list:append "folks" # "hello" "world" "folks"
Vector builtin object
The Vector builtin object provides the facility of an index array of
objects. The Vector object is another example of iterable object. The
Vector object provides support for forward and backward iteration.
Vector construction
A vector is constructed like a cons cell or a list. The default
constructor creates a vector with 0 objects.
const nil-vector (Vector)
const obj-vector (Vector 1 ’a’ "hello")
Vector methods
The Vector object methods are similar to the List object. The append
method appends an object at the end of the vector. The set method set a
vector position by index.
const vec (Vector "hello" "world")
vec:length # 2
vec:get 0 # "hello"
vec:get 1 # "world"
vec:append "folks" # "hello" "world" "folks"
vec:set 0 "bonjour" # "bonjour" "world" "folks"
Set builtin object
The Set builtin object provides the facility of an object container.
The Set object is another example of iterable object. The Set object
provides support for forward iteration. One of the property of a set is
that there is only one object representation per set. Adding two times
the same object results in one object only.
Set construction
A set is constructed like a vector. The default constructor creates a
set with 0 objects.
const nil-set (Set)
const obj-set (Set 1 ’a’ "hello")
Set methods
The Set object methods are similar to the Vector object. The add method
adds an object in the set. If the object is already in the set, the
object is not added. The length method returns the number of elements
in the set.
const set (Set "hello" "world")
set:get-size # 2
set:add "folks" # "hello" "world" "folks"
Iteration
When an object is iterable, it can be used with the reserved keyword
for. The for keyword iterates on one or several objects and binds
associated symbols during each step of the iteration process. All
iterable objects provides also the method get-iterator which returns an
iterator for a given object. The use of iterator is justified during
backward iteration, since for only perform forward iteration.
Function mapping
Given a function func, it is relatively easy to apply this function to
all objects of an iterable object. The result is a list of successive
calls with the function. Such function is called a mapping function and
is generally called map.
const map (obj func) {
trans result (Cons)
for (car) (obj) (result:link (func car))
eval result
}
The link method differs from the append method in the sense that the
object to append is set to the cons cell car if the car and cdr is nil.
Multiple iteration
Multiple iteration can be done with one call to for. The computation of
a scalar product is a simple but illustrative example.
# compute the scalar product of two vectors
const scalar-product (u v) {
trans result 0
for (x y) (u v) (result:+= (* x y))
eval result
}
Note that the function scalar-product does not make any assumption
about the object to iterate. One could compute the scalar product
between a vector a list for example.
const u (Vector 1 2 3)
const v (List 2 3 4)
scalar-product u v
Conversion of iterable objects
The use of an iterator is suitable for direct conversion between one
object and another. The conversion to a vector can be simply defined as
indicted below.
#convert an iterable object to a vector
const to-vector (obj) {
trans result (Vector)
for (i) (obj) (result:append i)
eval result
}
Explicit iterator
An explicit iterator is constructed with the get-iterator method. At
construction, the iterator is reset to the beginning position. The get-
object method returns the object at the current iterator position. The
next advances the iterator to its next position. The valid-p method
returns true if the iterator is in a valid position. When the iterator
supports backward operations, the prev method move the iterator to the
previous position. Note that Cons objects do not support backward
iteration. The begin method reset the iterator to the beginning. The
end method moves the iterator the last position. This method is
available only with backward iterator.
# reverse a list
const reverse-list (obj) {
trans result (List)
trans itlist (obj:get-iterator)
itlist:end
while (itlist:valid-p) {
result:append (itlist:get-object))
itlist:prev
}
eval result
}
Special Objects
The AFNIX engine provides several builtin container objects which are
special case of container objects. Such objects are Queue and Bitset
Queue object
A queue is a special object which acts as container with a FIFO policy.
When an object is placed in the queue, it remains there until it has
been dequeued.
# 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 set is a special container for bit. A bit set can be constructed
with a specific size. When the bit set is constructed, each bit can be
marked and tested by index.
# create a bit set
const bs (BitSet)
bitset-p bs # true
# check, mark and clear
assert false (bs:get 0)
bs:mark 0
assert true (bs:get 0)
bs:clear 0
assert false (bs:get 0)
CLASSES
This chapter covers the AFNIX class model and its associated
operations. The AFNIX class model is slightly different compared to
traditional one. Because AFNIX has dynamic symbol bindings, it is not
necessary to declare the class data members. A class is an object which
can be manipulated by itself. Such class is said to belongs to a group
of meta class as described later in this chapter. Once the class
concept has been detailed, the chapter moves to the concept of instance
of that class and shows how instance data members and functions can be
used. The chapter terminates with a description of dynamic class
programming.
Class object
A class object in the AFNIX terminology is simply a nameset which can
be replicated via a construction mechanism. A class is created with the
reserved keyword class. The result is an object of type Class which
supports various symbol binding operations.
Class declaration and bindings
A new class is an object created with the reserved keyword class. Such
class is an object which can be bound to a symbol.
const Color (class)
A list of initial instance data members can be specified as an argument
to the class reserved keyword.
const Complex (class (re im))
Because a class acts like a nameset, it is possible to bind directly
symbols with the qualified name notation.
const Color (class)
const Color:RED-FACTOR 0.75
const Color:BLUE-FACTOR 0.75
const Color:GREEEN-FACTOR 0.75
When a data is defined in the class nameset, it is common to refer it
as a static data member. A static data member is invariant over the
instance of that class. When the data member is declared with the const
reserved keyword, the symbol binding is const in the class nameset. It
is also possible to use the trans reserved keyword.
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 static method of that class. Static 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-from-string (color value) {
trans val "0x"
val:+= (switch color (
("red" (value:substr 1 3))
("green" (value:substr 3 5))
("blue" (value:substr 5 7))
))
I Integer val
}
The invocation of a static method is done with the standard qualified
name notation.
Color:get-primary-from-string "red" "#23c4e5"
Color:get-primary-from-string "green" "#23c4e5"
Color:get-primary-from-string "blue" "#23c4e5"
Class symbol access
A class acts as a nameset and therefore provides the mechanism to
evaluate any symbol with the qualified name notation.
const Color:RED-VALUE "#ff0000"
const Color:print-primary-colors (color) {
println "red color " (Color:get-primary-color "red" color)
println "green color " (Color:get-primary-color "green" color)
println "blue color " (Color:get-primary-color "blue" color)
}
# print the color components for the red color
Color:print-primary-colors Color:RED-VALUE
Instance
An instance of a class is an AFNIX object which is constructed by a
special class method called a constructor. If an instance constructor
does not exist, the instance is said to have a default construction. An
instance acts also as a nameset. The only difference with a class, is
that a symbol resolution is done first in the instance nameset and then
in the instance class. As a consequence, creating an instance is
equivalent to define a default nameset hierarchy.
Instance construction
By default, a instance of the class is an object which defines an
instance nameset. The simplest way to define an anonymous instance is
to create it directly.
const i ((class))
const Color (class)
const red (Color)
The example above define an instance of an anonymous class. If a class
object is bound to a symbol, such symbol can be used to create an
instance of that class. When an instance is created, the special symbol
named this is defined in the instance nameset. This symbol is bounded
to the instance object and can be used to reference in an anonymous way
the instance itself.
Instance initialization
When an instance is created, the AFNIX engine looks for a special
lambda expression called preset. This lambda expression, if it exists,
is executed after the default instance has been constructed. Such
lambda expression is a method since it can refer to the this symbol and
bind some instance symbols. The arguments which are passed during the
instance construction are passed to the preset method.
const Color (class)
trans Color:preset (red green blue) {
const this:red (Integer red)
const this:green (Integer green)
const this:blue (Integer blue)
}
# create some default colors
const Color:RED (Color 255 0 0)
const Color:GREEN (Color 0 255 0)
const Color:BLUE (Color 0 0 255)
const Color:BLACK (Color 0 0 0)
const Color:WHITE (Color 255 255 255)
In the example above, each time a color is created, a new instance
object is created. The constructor is invoked with the this symbol
bound to the newly created instance. Note that the qualified name
this:red defines a new symbol in the instance nameset. Such symbol is
sometimes referred as an instance data member. Note as well that there
is no ambiguity in resolving the symbol red. Once the symbol is
created, it shadows the one defined as a constructor argument.
Initialization with data member list
If the class was defined with a list of data members, the instance is
created with these data members initialized to nil. Each symbol is
defined as a transient symbol since they are supposed to be modified
later. As a consequence, it is possible to use the reserved keyword
trans inside the preset method.
const Complex (class (re im))
trans Complex:preset (re im) {
trans this:re (Real re)
trans this:im (Real im)
}
The use of a class data member list is primarily dictated by the
existence of a copy constructor for that class. If a method try to
construct an object, an evaluation of an unbound data member with trans
might trigger an inner instance data member to be set instead of the
real one. This behavior exists only with trans. When const is used, the
implementation guarantee that the symbol binding will be local to that
instance.
Instance symbol access
An instance acts as a nameset. It is therefore possible to bind locally
to an instance a symbol. When a symbol needs to be evaluated, the
instance nameset is searched first. If the symbol is not found, the
class nameset is searched. When an instance symbol and a class symbol
have the same name, the instance symbol is said to shadow the class
symbol. The simple example below illustrates this property.
const c (class)
const c:a 1
const i (c)
const j (c)
const i:a 2
# class symbol access
println c:a
# shadow symbol access
println i:a
# non shadow access
println j:a
When the instance is created, the special symbol meta is bound in the
instance nameset with the instance class object. This symbol can
therefore be used to access a shadow symbol.
const c (class)
const i (c)
const c:a 1
const i:a 2
println i:a
println i:meta:a
The symbol meta must be used carefully, especially inside an
initializer since it might create an infinite recursion as shown below.
const c (class)
trans c:preset nil (const i (this:meta))
const i (c)
Instance method
When lambda expression is defined within the class or the instance
nameset, that lambda expression is callable from the instance itself.
If the lambda expression uses the this symbol, that lambda is called an
instance method since the symbol this is defined in the instance
nameset. If the instance method is defined in the class nameset, the
instance method is said to be global, that is, callable by any instance
of that class. If the method is defined in the instance nameset, that
method is said to be local and is callable by the instance only. Due to
the nature of the nameset parent binding, only lambda expression can be
used. Gamma expressions will not work since the gamma nameset has
always the top level nameset as its parent one.
const Color (class)
# class constructor
trans Color:preset (red green blue) {
const this:red (Integer red)
const this:green (Integer green)
const this:blue (Integer blue)
}
const Color:RF 0.75
const Color:GF 0.75
const Color:BF 0.75
# this method returns a darker color
trans Color:darker nil {
trans lr (Integer (max (this:red:* Color:RF) 0))
trans lg (Integer (max (this:green:* Color:GF) 0))
trans lb (Integer (max (this:blue:* Color:BF) 0))
Color lr lg lb
}
# get a darker color than yellow
const yellow (Color 255 255 0)
const dark-yellow (yellow:darker)
Instance operators
Any operator can be defined at the class or the instance level.
Operators like == or != generally requires the ability to assert if the
argument is of the same type of the instance. The global operator ==
will return true if two classes are the same. With the use of the meta
symbol, it is possible to assert such equality.
# this method checks that two colors are equals
trans Color:== (color) {
if (== Color color:meta) {
if (!= this:red color:red) (return false)
if (!= this:green color:green) (return false)
if (!= this:blue color:blue) (return false)
eval true
} false
}
# create a new yellow color
const yellow (Color 255 255 0)
(yellow:== (Color 255 255 0)) # true
The global operator == returns true if both arguments are the same,
even for classes. Method operators are left open to the user.
Complex number example
As a final example, a class simulating the behavior of a complex number
is given hereafter. The interesting point to note is the use of the
operators. As illustrated before, the class uses uses a default method
method to initialize the data members.
# class declaration
const Complex (class (re im))
# constructor initializer
trans Complex:preset (re im) {
trans this:re (Real re)
trans this:im (Real im)
}
# class mutators
trans Complex:set-re (x) (trans this:re re)
trans Complex:set-im (x) (trans this:im im)
# class accessors
trans Complex:get-re nil (Real this:re)
trans Complex:get-im nil (Real this:im)
trans Complex:module nil {
trans result (Real (+ (* this:re this:re) (* this:im this:im)))
result:sqrt
}
trans Complex:format nil {
trans result (String this:re)
result:+= "+i"
result:+= (String this:im)
}
# complex predicate
const complex-p (c) (
if (instance-p c) (== Complex c:meta) false)
# 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
}
Inheritance
Inheritance is the mechanism by which a class or an instance inherits
methods and data member access from a parent object. The AFNIX class
model is based on a single inheritance model. When an instance object
defines a parent object, such object is called a super instance. The
instance which has a super instance is called a derived instance. The
main utilization of inheritance is the ability to reuse methods for
that super instance.
Derivation construction
A derived object is generally defined within the preset method of that
instance by setting the super data member. The super reserved keyword
is set to nil at the instance construction. The good news is that any
object can be defined as a super instance, including builtin object.
const c (class)
const c:preset nil {
trans this:super 0
}
In the example above, an instance of class c is constructed. The super
instance is with an integer object. As a consequence, the instance is
derived from the Integer instance. Another consequence of this scheme
is that derived instance do not have to be built from the same base
class.
Derived symbol access
When an instance is derived from another one, any symbol which belongs
to the super instance can be access with the use of the super data
member. If the super class can evaluate a symbol, that symbol is
resolved automatically by the derived instance.
const c (class)
const i (c)
trans i:a 1
const j (c)
trans j:super i
println j:a
When a symbol is evaluated, a set of search rules is applied. The AFNIX
engine gives the priority to the class nameset vs the super instance.
As a consequence, a static 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 AFNIX
object model an ideal candidate to support instance re-parenting. In
this model, a change in the parent instance is automatically reflected
at the instance method call.
const c (class)
const i (c)
trans i:super 0
println (i:to-string) # 0
trans i:super "hello world"
println (i:to-string) # hello world
In this example, the instance is originally set with an Integer
instance parent. Then the instance is re-parented with a String
instance parent. The call to the to-string method illustrates this
behavior.
Instance re-binding
The ability to set dynamically the instance class is another powerful
feature of the AFNIX object model. In this approach, the instance
meta class can be changed dynamically with the mute method.
Furthermore, it is also possible to create initially an instance
without any class binding, which is later muted.
# create a point class
const point (class)
# point class initializer
trans point:preset (x y) {
trans this:x x
trans this:y y
}
# create an empty instance
const p (Instance)
# bind the point class
p:mute point 1 2
In this example, when the instance is muted, the preset method is
called automatically with the extra arguments.
Instance inference
The ability to instantiate dynamically inferred instance is offered by
the AFNIX object model. An instance b is said to be inferred by the
instance a when the instance a is the super instance of the instance b.
The instance inference is obtained by binding the infer symbol to a
class. When an instance of that class is created, the inferred instance
is also created.
# base class A
const A (class)
# inferred class B
const B (class)
const A:infer B
# create an instance from A
const x (A)
assert B (x:meta)
assert A (x:super:meta)
In this example, when the instance is created, the inferred instance is
also created and returned by the instantiation process. The preset
method is only called for the inferred instance if possible or the base
instance if there is no inferring class. Because the base preset preset
method is not called automatically, the inferred method is responsible
to do such call.
trans B:preset (x y) {
trans this:xb x
trans this:yb y
if (== A this:super:meta) (this:super:preset x y)
}
Because the class can mute from one call to another and also the
inferred class, the preset method call must be used after a
discrimination of the meta class has been made as indicated by the
above example.
ADVANCED CONCEPTS
This chapter covers advanced concepts of the AFNIX programming
language. The first subject is the exception model. The second subject
covers some properties of the namesets. Finally, the interpreter object
is described in details.
Exception
An exception is an unexpected change in the execution flow. The AFNIX
model for exception is based on a mechanism which throws the exception
to be caught by a handler. The mechanism is also designed to be
compatible with the native "C++" implementation.
Throwing an exception
An exception is thrown with the reserved keyword throw. When an
exception is thrown, the normal flow of execution is interrupted and an
object used to carry the exception information is created. Such
exception object is propagated backward in the call stack until an
exception handler catch it.
if (not (number-p n))
(throw "type-error" "invalid object found" n)
The example above is the general form to throw an exception. The first
argument is the the exception id. The second argument is the exception
reason. The third argument is the exception object. The exception id
and reason are always a string. The exception object can be any object
which is carried by the exception. The reserved keyword throw accepts 0
or more arguments.
throw
throw "type-error"
throw "type-error" "invalid argument"
With 0 argument, the exception is thrown with the exception id set to
"user-exception". With one argument, the argument is the exception id.
With 2 arguments, the exception id and reason are set. Within a try
block, an exception can be thrown again by using the exception object
represented with the what symbol.
try {
...
} {
println "exception caught and re-thrown"
throw what
}
Exception handler
The reserved keyword try executes a form and catch an exception if one
has been thrown. With one argument, the form is executed and the result
is the result of the form execution unless an exception is caught. If
an exception is caught, the result is the exception object. If the
exception is a native one, the result is nil.
try (+ 1 2)
try (throw)
try (throw "hello")
try (throw "hello" "world")
try (throw "hello" "world" "folks")
In its second form, the try reserved keyword can accept a second form
which is executed when an exception is caught. When an exception is
caught, a new nameset is created and the special symbol what is bounded
with the exception object. In such environment, the exception can be
evaluated. The what:eid qualified name is the exception id. The
what:reason qualified name is the exception reason and what:object is
the 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 like AFNIX , it is also a convenient
mechanism to abort an expression call if some arguments do not match
the expected types.
# protected factorial
const fact (n) {
if (not (integer-p n))
(throw "number-error" "invalid argument in fact")
if (== n 0) 1 (* n (fact (- n 1)))
}
try (fact 5) 0
try (fact "hello") 0
Nameset
A nameset is created with the reserved keyword nameset. Without
argument, the nameset reserved keyword creates a nameset without
setting its parent. With one argument, a nameset is created and the
parent set with the argument.
const nset (nameset)
const nset (nameset ...)
Default namesets
When a nameset is created, the symbol . is automatically created and
bound to the newly created nameset. If a parent nameset exists, the
symbol .. is also automatically created. The use of the current nameset
is a useful notation to resolve a particular name given a hierarchy of
namesets.
trans a 1 # 1
block {
trans a (+ a 1) # 2
println ..:a 1 # 1
}
println a # 1
Nameset and inheritance
When a nameset is set as the super object of an instance, some
interesting results are obtained. Because symbols are resolved in the
nameset hierarchy, there is no limitation to use a nameset to simulate
a kind of multiple inheritance. The following example illustrates this
point.
const cls (class)
const ins (cls)
const ins:super (nameset)
const ins:super:value 2000
const ins:super:hello "hello world "
println ins:hello ins:value # hello world 2000
Delayed Evaluation
The AFNIX engine provides a mechanism called delayed evaluation. Such
mechanism permits the encapsulation of a form to be evaluated inside an
object called a promise.
Creating a promise
The reserved keyword delay creates a promise. When the promise is
created, the associated object is not evaluated. This means that the
promise evaluates to itself.
const a (delay (+ 1 2))
promise-p a # true
The previous example creates a promise and store the argument form. The
form is not yet evaluated. As a consequence, the symbol a evaluates to
the promise object.
Forcing a promise
The reserved keyword force the evaluation of a promise. Once the
promise has been forced, any further call will produce the same result.
Note also that, at this stage, the promise evaluates to the evaluated
form.
trans y 3
const l ((lambda (x) (+ x y)) 1)
assert 4 (force l)
trans y 0
assert 4 (force l)
Enumeration
Enumeration, that is, named constant bound to an object, can be
declared with the reserved keyword enum. The enumeration is built with
a list of literal and evaluated as is.
const e (enum E1 E2 E3)
assert true (enum-p e)
The complete enumeration evaluates to an Enum object. Once built,
enumeration item evaluates by literal and returns an Item object.
assert true (item-p e:E1)
assert "Item" (e:E1:repr)
Items are comparable objects. Only items can be compared. For a given
item, the source enumeration can be obtained with the get-enum method.
# check for item equality
const i1 e:E1
const i2 e:E2
assert true (i1:== i1)
assert false (== i1 i2)
# get back the enumeration
assert true (enum-p (i1:get-enum))
Logger
The Looger class is a message logger that stores messages in a buffer
with a level. The default level is the level 0. A negative level
generally indicates a warning or an error message but this is just a
convention which is not enforced by the class. A high level generally
indicates a less important message. The messages are stored in a
circular buffer. When the logger is full, a new message replace the
oldest one. By default, the logger is initialized with a 256 messages
capacity that can be resized.
const log (Logger)
assert true (logger-p log)
When a message is added, the message is stored with a timestamp and a
level. The timestamp is used later to format a message. The length
method returns the number of logged messages. The get-message method
returns a message by index. Because the system operates with a circular
buffer, the get-message method manages the indexes in such way that the
old messages are accessible with the oldest index. For example, even
after a buffer circulation, the index 0 will point to the oldest
message. The get-message-level returns the message level and the get-
message-time returns the message posted time.
const mesg (log:get-message 0)
In term of usage, the logger facility can be conveniently used with
other derived classes. The standard i/o module provides several classes
that permits to manage logging operations in a convenient way.
Interpreter
The AFNIX interpreter is by itself a special object with specialized
methods which do not have equivalent using the standard AFNIX
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 afnix version
program-name Interpreter program name
major-version Major version number
minor-version Minor version number
patch-version Patch version number
afnix-uri Official uri name
load Load a file and execute it
launch Launch a normal thread
daemon Launch a daemon thread
library Load and initialize a library
set-epsilon Set real number precision
get-epsilon Set real number precision
Arguments vector
The interp:argv qualified name evaluates to a vector of strings. Each
argument is stored in the vector during the interpreter initialization.
zsh> axi hello world
(axi) println (interp:argv:length) # 2
(axi) println (interp:argv:get 0) # hello
Interpreter version
Several symbols can be used to track the interpreter version and the
operating system. The full version is bound to the interp:version
qualified name. The full version is composed of the major, minor and
patch number. The operating system name is bound to the qualified name
interp:os-name. The operating system type is bound to the interp:os-
type.
println "major version number : " interp:major-version
println "minor version number : " interp:minor-version
println "patch version number : " interp:patch-version
println "interpreter version : " interp:version
println "operating system name : " interp:os-name
println "operating system type : " interp:os-type
println "afnix official url : " interp:afnix-url
File loading
The interp:load method loads and execute a file. The interpreter
interactive command session is suspended during the execution of the
file. In case of error or if an exception is raised, the file execution
is terminated. The process used to load a file is governed by the file
resolver. Without extension, a compiled file is searched first and if
not found a source file is searched.
Library loading
The interp:library method loads and initializes a library. The
interpreter maintains a list of opened library. Multiple execution of
this method for the same library does nothing. The method returns the
library object.
interp:library "afnix-sys"
println "random number: " (afnix:sys:get-random)
Interpreter duplication
The interpreter can be duplicated with the help of the dup method.
Without argument, a clone of the current interpreter is made and a
terminal object is attached to it. When used in conjunction with the
roll method, this approach permits to create an interactive
interpreter. The dup method also accepts a terminal object.
# duplicate the interpreter
const si (interp:dup)
# change the primary prompt
si:set-primary-prompt "(si)"
Interpreter loop
The interpreter loop can be run with the roll. The loop operates by
reading the interpreter input stream. If the interpreter has been
cloned with the help of the dup method, this method provides a
convenient way to operate in interactive mode. The method is not called
loop because it is a reserved keyword and starting a loop is like
having the ball rolling.
# duplicate the interpreter
const si (interp:dup)
# loop with this interpreter
si:roll
THREADS OPERATIONS
This chapter covers the threads facilities builtin in the AFNIX
interpreter. The thread subsystem allows for the execution of
concurrent forms with an automatic synchronization mechanism. Designing
a good program with concurrent execution is a difficult task. It takes
a while to get used with the various synchronization mechanisms which
ensure a safe execution, that is no race condition or dead lock.
Fortunately, AFNIX provides some unique features that should ease
such design.
Normal and daemon threads
The interpreter supports two types of threads, called normal and daemon
threads. A normal thread is started with the reserved keyword launch. A
daemon thread is started with the reserved keyword daemon. The
difference between a normal thread and a daemon thread is only in the
termination of the interpreter. An AFNIX program is completed when
all normal threads have terminated. This means that the master thread
(i.e the first thread) is suspended until all normal threads have been
executed. With daemon threads, the master thread terminates even if
some daemon threads are still running.
Starting a normal thread
A normal thread is started with the reserved keyword launch. The form
to execute in a thread is the argument. The simplest thread to execute
is the nil thread.
launch (nil)
Even the nil thread does nothing in term of computation, it does a lot
of things internally by turning on the shared objects subsystem.
Thread object and result
When a thread terminate, the thread object holds the result of the last
executed form. The thread object is returned by the launch or daemon
command. The thread-p predicates returns true if the object is a thread
descriptor. The thread type can be check with the normal-p or daemon-p
predicates.
const thr (launch (nil))
println (thread-p thr) # true
println (thr:normal-p) # true
The member data result of the thread object holds the result of the
thread. Although the result can be accessed at any time, the returned
value will be nil until the thread as completed its execution.
const thr (launch (nil))
println (thr:result) # nilp
Although the AFNIX engine will ensure that the result is nil until
the thread has completed its execution, it does not mean that it is a
reliable approach to test until the result is not nil. The engine
provides various mechanisms to synchronize a thread and eventually wait
for its completion.
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 AFNIX engine provides an automatic
synchronization mechanism for global objects, where only one thread can
modify an object, but several thread can read it. This mechanism known
as read-write locking guarantees that there is only one writer, but
eventually multiple reader. When a thread start 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
objects can be very complicated to detect. For example, if a vector is
shared by various threads, the engine will make sure that all vector
objects are also shared. A closed variable in a lambda or gamma
expression is another example of potential shared object. Executing
such lambda form in a thread will automatically mark the closed
variables as shared objects. Additionally, when the thread system is
started, all object in the global nameset are marked shared.
Shared object predicate
The object predicate method shared-p returns true if an object is
shared. Since all global objects are marked shared as soon as the
thread system is turned on, the following example shows how a nil
thread marks a shared variable.
# create simple symbol
const a 1
assert false (a:shared-p)
# turn on the thread system
launch (nil)
assert true (a:shared-p)
# check another symbol
trans b 1
assert true (b:shared-p)
When an object is marked shared, it will remain in this state for rest
of the session. Note that when an object is copied (by copy
construction), the shared state is not copied. The copied object will
become shared depending on its surrounding context. Such context can be
a nameset or any other type of container which is shared or not.
Shared protection access
We illustrate the previous discussion with an interesting example and
some variations around it. Let’s consider a form which increase an
integer object and another form which decrease the same integer object.
If the integer is initialized to 0, and the two forms run in two
separate threads, we might expect to see the value bounded by the time
allocated for each thread. In other word, this simple example is a very
good illustration of your machine scheduler.
# shared variable access
const var 0
# increase method
const incr nil (while true
(println "increase: " (var:= (+ var 1))))
# decrease method
const decr nil (while true
(println "decrease: " (var:= (- var 1))))
# start both threads
launch (decr)
launch (incr)
In the previous example, var is initialized to 0. The incr thread
increments var while the decr thread decrements var. Depending on the
operating system, the result stays bounded within a certain range. The
previous example can be changed by using the main thread or a third
thread to print the variable value. The end result is the same, except
that there is more threads competing for the shared variable.
# shared variable access
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, AFNIX 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.
Form synchronization
The reserved keyword sync can be used to synchronize a form. When a
form, is synchronized, the AFNIX engine guarantees that only one
thread will execute this form.
const print-message (code mesg) (
sync {
errorln "error : " code
errorln "message: " mesg
}
)
The previous example create a gamma expression which make sure that
both the error code and error message are printed in one group, when
several threads call it.
Thread completion
The other piece of synchronization is the thread completion indicator.
The thread descriptor contains a method called wait which suspend the
calling thread until the thread attached to the descriptor has been
completed. If the thread is already completed, the method returns
immediately.
# simple flag
const flag false
# simple shared tester
const ftest (val) (flag) (assert val (flag:shared-p))
# no thread mean not shared
ftest false
# in a thread it is shared
const thr (launch (ftest true))
thr:wait
assert true (flag:shared-p)
This example is taken from the test suites. It checks that a closed
variable becomes shared when started in a thread. Note the use of the
wait method to make sure the thread has completed before checking for
the shared flag. It is also worth to note that wait is one of the
method which guarantees that a thread result is valid. Another use of
the wait method can be made with a vector of thread descriptors when
one wants to wait until all of them have completed.
# shared vector of threads descriptors
const thr-group (Vector)
# wait until all threads in the group are finished
const wait-all nil (for (thr) (thr-group) (thr:wait))
Complete example
We illustrate the previous discussion with a complete example. The idea
is to perform a matrix multiplication. A thread is launched when when
multiplying one line with one column. The result is stored in the
thread descriptor. A vector of thread descriptor is used to store the
result.
# initialize the shared library
interp:library "afnix-sys"
# shared vector of threads descriptors
const thr-group (Vector)
# this procedure waits until all threads in
# the group are finished
const wait-all nil (for (thr) (thr-group) (thr:wait))
# this procedure initialize a matrix with random numbers
# the matrix is a square one with its size as an argument
const init-matrix (n) {
trans i (Integer 0)
const m (Vector)
do {
trans v (m:append (Vector))
trans j (Integer)
do {
v:append (afnix:sys:get-random)
} (< (j:++) n)
} (< (i:++) n)
eval m
}
# 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 coming 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:append (launch (mult-line-column lv cv))
}
}
}
# 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)
Condition variable
A condition variable is another mechanism to synchronize several
threads. A condition variable is modeled with the Condvar object. At
construction, the condition variable is initialized to false. A thread
calling the wait method will block until the condition becomes true.
The mark method can be used by a thread to change the state of a
condition variable and eventually awake some threads which are blocked
on it. The following example shows how the main thread blocks until
another change the state of the condition.
# create a condition variable
const cv (Condvar)
# this function runs in a thread - does some computation
# and mark the condition variable
const do-something nil {
# do some computation
....
# mark the condition
cv:mark
}
# start some computation in a thread
launch (do-something)
# block until the condition is changed
cv:wait-unlock
# continue here
In this example, the condition variable is created at the beginning.
The thread is started and the main thread blocks until the thread
change the state of the condition variable. It is important to note the
use of the wait-unlock method. When the main thread is re-started
(after the condition variable has been marked), the main thread owns
the lock associated with the condition variable. The wait-unlock method
unlocks that lock when the main thread is restarted. Note also that the
wait-unlock method reset the condition variable. if the wait method was
used instead of wait-unlock the lock would still be owned by the main
thread. Any attempt by other thread to call the mark method would
result in the calling thread to block until the lock is released. The
Condvar class has several methods which can be used to control the
behavior of the condition variable. Most of them are related to lock
control. The reset method reset the condition variable. The lock and
unlock control the condition variable locking. The mark, wait and wait-
unlock method controls the synchronization among several threads.
REGULAR EXPRESSIONS
This chapter covers the AFNIX regular expressions or regex syntax and
programming use. The AFNIX regex is an original implementation with
its own syntax and execution model.
Regular expression syntax
AFNIX implements a regular expression engine via a special Regex
object. A regular expression can be built implicitly or explicitly with
the use of the Regex object. The regex syntax uses the [ and ]
characters as block delimiters. When used in a source file, the lexical
analyzer automatically recognizes a regex and built the object
accordingly. In other word, the regex system is builtin in the AFNIX
language. The following example shows two equivalent way to define the
same regex expression.
# syntax builtin regex
(== [$d+] 2000) # true
# explicit builtin regex
(== (Regex "$d+") 2000) # true
In its first form, the [ and ] characters are used as syntax
delimiters. The lexical analyzer automatically recognizes this token as
a regex and built the equivalent Regex object. The second form is the
explicit construction of the Regex object. Note also that the [ and ]
characters are also used as regex block delimiters.
Regex characters and meta-characters
Any character, except the one used as operators can be used in a regex.
The $ character is used as a meta-character -- or control character --
to represent a particular set of characters. For example, [hello world]
is a regex which match only the "hello world" string. The [$d+] regex
matches one or more digits. The following meta characters are builtin
in the regex engine.
Character Description
$a matches any letter or digit
$b matches any blank characters
$d matches any digit
$e matches eol, cr and eof
$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.
Character Description
$A any character except letter or digit
$B any character except blank characters
$D any character except digit
$E any character except eol, cr and eof
$L any character except lower case letter
$N any character except eol or cr
$S any character except letter
$U any character except upper case letter
$V any character except afnix constituent
$W any character except word constituent
$X any character except hex characters
A character which follows a $ character and that is not a meta
character is treated as a normal character. For example $[ is the [
character. A quoted string can be used to define character matching
which could otherwise be interpreted as control characters or operator.
A quoted string also interprets standard escaped sequences but not meta
characters.
(== [$d+] 2000) # true
(== ["$d+"] 2000) # false
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 get method can be
used to get the sub-string by index.
Regex object
Although a regex can be built implicitly, the Regex object can also be
used to build a new regex. The argument is a string which is compiled
during the object construction.
Literal object
A Regex object is a literal object. This means that the to-string
method is available and that a call to the println special form will
work directly.
const re (Regex "$d+")
println re # $d+
println re:to-string # [$d+]
Regex operators
The == and != operators are the primary operators to perform a regex
match. The == operator returns true if the regex matches the string
argument from the beginning to the end of string. Such operator implies
the begin and end of string anchoring. The < operator returns true if
the regex matches the string or a substring or the string argument.
Regex methods
The primary regex method is the get method which returns by index the
sub-string when a group has been matched. The length method returns the
number of group match.
if (== (const re [($d$d):($d$d)]) "12:31") {
re:length # 2
re:get 0 # 12
re:get 1 # 31
}
The match method returns the first string which is matched by the
regex.
const regex [$d+]
regex:match "Happy new year 2000" # 2000
The replace method any occurrence of the matching string with the
string argument.
const regex [$d+]
regex:replace "Hello year 2000" "3000" # hello year 3000
Argument conversion
The use of the Regex operators implies that the arguments are evaluated
as literal object. For this reason, an implicit string conversion is
made during such operator call. For example, passing the integer 12 or
the string "12" is strictly equivalent. Care should be taken when using
this implicit conversion with real numbers.
FUNCTIONAL PROGRAMMING
This chapter covers the interesting aspects of AFNIX with respect to
the functional programming paradigm. Functional programming is often
described as the ability to create functions that creates functions. As
a matter of fact, it is a far bigger subject that finds its root in the
lambda calculus. A language -- like AFNIX -- that supports the
functional programming paradigm is also sometimes called a high order
language.
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, AFNIX provides the
reserved keyword self that is a reference to the function expression.
We illustrate this capability with the well-known factorial expression
written in pure functional style.
(axi) println ((gamma (n)
(if (<= n 1) 1 (* n (self (- n 1))))) 5)
120
The use of a gamma expression versus a lambda expression is a matter of
speed. Since the gamma expression does not have free variables, the
symbol resolution is not a concern here.
Closed variables
One of the AFNIX characteristic is the treatment of free variables. A
variable is said to be free if it is not bound in the expression
environment or its children at the time of the symbol resolution. For
example, the expression ((lambda (n) (+ n x)) 1) computes the sum of
the argument n with the free variable x. The evaluation will succeeds
if x is defined in one of the parent environment. Actually this example
can also illustrates the difference between a lambda expression and a
gamma expression. Let’s consider the following forms.
trans x 1
const do-print nil {
trans x 2
println ((lambda (n) (+ n x)) 1)
}
The gamma expression do-print will produce 3 since it sums the argument
n bound to 1, with the free variable x which is defined in the calling
environment as 2. Now if we rewrite the previous example with a gamma
expression the result will be one, since the expression parent will be
the top level environment that defines x as 1.
trans x 1
const do-print nil {
trans x 2
println ((gamma (n) (+ n x)) 1)
}
With this example, it is easy to see that there is a need to be able to
determine a particular symbol value during the expression construction.
Doing so is called closing a variable. Closing a variable is a
mechanism that binds into the expression a particular symbol with a
value and such symbol is called a closed variable, since its value is
closed under the current environment evaluation. For example, the
previous example can be rewritten to close the symbol x.
trans x 1
const do-print nil {
trans x 2
println ((gamma (n) (x) (+ n x)) 1)
}
Note that the list of closed variable immediately follow the argument
list. In this particular case, the function do-print will print 3 since
x has been closed with the value 2 has defined in the function do-
print.
Dynamic binding
Because AFNIX has a dynamic binding symbol resolution, it is possible
to have under some circumstances a free or closed variable. This kind
of situation can happen when a particular symbol is defined under a
condition.
lambda (n) {
if (<= n 1) (trans x 1)
println (+ n x)
}
With this example, the symbol x is a free variable if the argument n is
greater than 1. While this mechanism can be powerful, extreme caution
should be made when using such feature. Note also that many other
language do not allow this kind of behavior. That kind of restriction
is primarily driven by the need to have a language with static binding.
The bad news is that it is impossible to write a compiler with dynamic
symbol binding.
Functional objects
Everything in AFNIX is an object. As a consequence, an object can be
manipulated, even if it is lexical element, a symbol or a closure.
Lexical and qualified names
The basic forms elements are the lexical and qualified names. Lexical
and qualified names are constructed by the AFNIX reader. Although the
evaluation process make that lexical object transparent, it is possible
to manipulate them directly.
(axi) const sym (protect lex)
(axi) println (sym:repr)
Lexical
In this example, the protect reserved keyword is used to avoid the
evaluation of the lexical object named lex. Therefore the symbol sym
refers to a lexical object. Since a lexical -- and a qualified --
object is a also a literal object, the println reserved function will
work and print the object name. In fact, a literal object provides the
to-string method that returns the string representation of a literal
object.
(axi) const sym (protect lex)
(axi) println (sym:to-string)
lex
Symbol and argument access
Each nameset maintains a table of symbols. A symbol is a binding
between a name and an object. Eventually, the symbol carries the const
flag. During the lexical evaluation process, the lexical object tries
to find an object in the nameset hierarchy. Such object can be either a
symbol or an argument. Again, this process is transparent, but can be
controlled manually. Both lexical and qualified named object have the
map method that returns the first object associated in the nameset
hierarchy.
(axi) const obj 0
(axi) const lex (protect obj)
(axi) const sym (lex:map)
(axi) println (sym:repr)
Symbol
A symbol is also a literal object, so the to-string and to-literal
methods will return the symbol name. Symbol methods are provided to
access or modify the symbol values. It is also possible to change the
const symbol flag with the set-const method.
(axi) println (sym:get-const)
true
(axi) println (sym:get-object)
0
(axi) sym:set-object true
(axi) println (sym:get-object)
true
A symbol name cannot be modified, since the name must be synchronized
with the nameset association. On the other hand, a symbol can be
explicitly constructed. As any object, the = operator can be used to
assign a symbol value. The operator will behaves like the set-object
method.
(axi) const sym (Symbol "symbol")
(axi) println sym
symbol
(axi) sym:= 0
(axi) println (eval sym)
0
Closure
As an object, the Closure can be manipulated outside the traditional
declarative way. A closure is a special object that holds an argument
list, a set of closed variables and a form to execute. The mechanic of
a closure evaluation has been described earlier. What we are interested
here is the ability to manipulate a closure as an object and eventually
modify it. Note that by default a closure is constructed as a lambda
expression. With a boolean argument set to true the same result is
obtained. With false, a gamma expression is created.
(axi) const f (Closure)
(axi) println (closure-p f)
true
This example creates an empty closure. The default closure is
equivalent to the trans f nil nil. The same can be obtained with const
f (Closure true). For a gamma expression, the following forms are
equivalent, const f (Closure false) and const f nil nil. Remember that
it is trans and const that differentiate between a lambda and a gamma
expression. Once the closure object is defined, the set-form method can
be used to bind a form.
# the simple way
trans f nil (println "hello world")
# the complex way
const f (Closure)
f:set-form (protect (println "hello world"))
There are numerous situations where it is desirable to mute dynamically
a closure expression. The simplest one is the closure that mute itself
based on some context. With the use of self, a new form can be set to
the one that is executed. Another use is a mechanism call advice, where
some new computation are inserted prior the closure execution. Note
that appending to a closure can lead to some strange results if the
existing closure expression uses return special forms. In a multi-
threaded environment, the ability to change a closure expression is
particularly handy. For example a special thread could be used to
monitor some context. When a particular situation develops, that
threads might trigger some closure expression changes. Note that
changing a closure expression does not affect the one that is executed.
If such change occurs during a recursive call, that change is seen only
at the next call.
LIBRARIAN AND RESOLVER
This chapter covers the use of the axl librarian utility as well as the
Librarian object. The file path resolver is also described as a mean to
search for a particular file to execute in a program.
Librarian object
A librarian file is a special file that acts as a containers for
various files. A librarian file is created with the axl -- AFNIX
cross librarian -- utility. Once a librarian file is created, it can be
added to the interpreter resolver. The file access is later performed
automatically by name with the standard interpreter load method.
Creating a librarian
The axl utility is the preferred way to create a librarian. Given a set
of files, axl combines them into a single one.
zsh: axl -h
usage: axl [options] [files]
[-h] print this help message
[-v] print version information
[-c] create a new librarian
[-x] extract from the librarian
[-s] get file names from the librarian
[-t] report librarian contents
[-f] lib set the librarian file name
The -c option creates a new librarian. The librarian file name is
specified with the -f option.
zsh: axl -c -f librarian.axl file-1.als file-2.als
The previous command combines file-1.als and file-2.als into a single
file called librarian.axl . Note that any file can be included in a
librarian.
Using the librarian
Once a librarian is created, the interpreter -i option can be used to
specify it. The -i option accepts either a directory name or a
librarian file. Once the librarian has been opened, the interpreter
load method can be used as usual.
zsh> axi -i librarian.axl
(axi) interp:load "file-1.als"
(axi) interp:load "file-2.als"
The librarian acts like a file archive. The interpreter file resolver
takes care to extract the file from the librarian when the load method
is invoked.
Librarian contents
The axl utility provides the -t and -s options to look at the librarian
contents. The -s option returns all file name in the librarian. The -t
option returns a one line description for each file in the librarian.
zsh: axl -t -f librarian.axl
-------- 1234 file-1.als
-------- 5678 file-2.als
The one line report contains the file flags, the file size and the file
name. The file flags are not used at this time. One possible use in the
future is for example, an auto-load bit or any other useful things.
Librarian extraction
The -x option permits to extract file from the librarian. Without any
file argument, all files are extracted. With some file arguments, only
those specified files are extracted.
zsh: axl -x -f librarian.axl
zsh: axl -x -f librarian.axl file-1.als
Librarian object
The Librarian object can be used within an AFNIX program as a
convenient way to create a collection of files or to extract some of
them.
Output librarian
The Librarian object is a standard AFNIX object. Its predicate is
librarian-p. Without argument, a librarian is created in output mode.
With a string argument, the librarian is opened in input mode, with the
file name argument. The output mode is used to create a new librarian
by adding file into it. The input mode is created to read file from the
librarian.
# create a new librarian
const lbr (Librarian)
# add a file into it
lbr:add "file-1.als"
# write it
lbr:write "librarian.axl"
The add method adds a new file into the librarian. The write method the
full librarian as a single file those name is write method argument.
Input librarian
With an argument, the librarian object is created in input mode. Once
created, file can be read or extracted. The length method -- which also
work with an output librarian -- returns the number of files in the
librarian. The exists-p predicate returns true if the file name
argument exists in the librarian. The get-names method returns a vector
of file names in this librarian. The extract method returns an input
stream object for the specific file name.
# open a librarian for reading
const lbr (Librarian "librarian.axl")
# get the number of files
println (lbr:length)
# extract the first file
const is (lbr:extract "file-1.als")
# is is an input stream - dump each line
while (is:valid-p) (println (is:readln))
Most of the time, the librarian object is used to extract file
dynamically. Because a librarian is mapped into the memory at the right
offset, there is no worry to use big librarian, even for a small file.
Note that any type of file can be used, text or binaries.
File resolver
The AFNIX file resolver is a special object used by the interpreter to
resolve file path based on the search path. The resolver uses a mixed
list of directories and librarian files in its search path. When a file
path needs to be resolved, the search path is scanned until a matched
is found. Because the librarian resolution is integrated inside the
resolver, there is no need to worry about file extraction. That process
is done automatically. The resolver can also be used inside an AFNIX
program to perform any kind of file path resolution.
Resolver object
The resolver object is created without argument. The add method adds a
directory path or a librarian file to the resolver. The valid method
checks for the existence of a file. The lookup method returns an input
stream object associated with the object.
# create a new resolver
const rslv (Resolver)
assert true (resolver-p rslv)
# add the local directory on the search path
rslv:add "."
# check if file test.als exists
# if this is ok - print its contents
if (rslv:valid-p "test.als") {
const is (rslv:lookup "test.als")
while (is:valid-p) (println (is:readln))
}