Modules and I/O

6.1 Printing

There are several ways of printing in claire. Any entity may be printed with the function print. When print is called for an object that does not inherit from thing (an object without a name), it calls the method self_print of which you can define new restrictions whenever you define new classes. If self_print was called on an object x owned by a class toto for which no applicable restriction could be found, it would print <toto>.Unless toto is a parameterized class, in which case x will be printed as toto(…),where the parenthesis contain the parameters’ values.

In the case of bags (sets or lists), strings, symbols or characters, the standard method is princ. It formats its argument in a somewhat nicer way than print. For example

print("john") gives "john"

princ("john") gives john

Finally, there also exists a printf macro as in C. Its first argument is a string with possible occurrences of the control patterns ~S, ~I, ~A and ~F<n><%>. The macro requires as many arguments as there are “tilde patterns” in the string, and pairs in order of appearance arguments together with tildes. These control patterns do not refer to the type of the corresponding argument but to the way you want it to be printed. The macro will call print for each argument associated with a ~S form, princ for each associated with a ~A form, and will print the result of the evaluation of the argument for each ~I form. The ~F pattern is new in CLAIRE 3.4 and takes two additional arguments which are appended to the ~F pattern: a one-digit integer to tell how many digits following the comma should be printed, and % to tell that the float should be printed as a percent. The ~Fn pattern uses the printFDigit method (see Appendix B).

A mnemonic is A for alphanumeric, S for standard, I for instruction and F for floats. Hence the command

printf("~S is ~A and here is what we know\n ~I",john,23,show(john) )

will be expanded into

(print(john), princ(" is "), princ(23),

princ(" and here is what we know\n"), show(john) )

Here is an example about how to print a float:

Let pi := 3,141592653589 in printf(“pi = ~A, ~S, ~F2, ~F% \”)

3.141592635, 3.14159, 3.14, 314,1%

Output may also be directed to a file or another device instead of the screen, using a port. A port is an object bound to a physical device, a memory buffer or a file. The syntax for creating a port bound to a file is very similar to that of C. The two methods are fopen and fclose. Their use is system dependent and may vary depending on which C compiler you are using. However, fopen always requires a second argument : a control string most often formed of one or more of the characters 'w', 'a', 'r': 'w' allows to (over)write the file, 'a' ('a' standing for append) allows to write at the end of the file, if it is already non empty and 'r' allows to read the file. The method fopen returns a port. The method use_as_output is meant to select the port on which the output will be written. Following is an example:

(let p:port := fopen("agenda-1994","w") in

( use_as_output(p), write(agenda), fclose(p) ) )

A CLAIRE port is a wrapper around a stream object from the underlying language (C++ or Java). Therefore, the ontology of ports can be extended easily. In most implementations, ports are available as files, interfaces to the GUI and strings (input and output). To create a string port, you must use port!() to create an empty string you may write to, or port!(s:string) to read from a string s (cf. Appendix B).

Note that for the sake of rapidity, communications through ports are buffered; so it may happen that the effect of printing instructions is delayed until other printing instructions for this port are given. To avoid problems of synchronization between reading and writing, it is sometimes useful to ensure that the buffer of a given port is empty. This is done by the command flush(p:port). flush(p) will perform all printing (or reading) instructions for the port p that are waiting in the associated buffer.

Two ports are created by default when you run claire : stdin and stdout . They denote respectively the standard input (the device where the interpreter needs to read) and the standard output (where the system prints the results of the evaluation of the commands). Because CLAIRE is interpreted, errors are printed on the standard output. The actual value of these ports is interface-dependent.

CLAIRE also offers a simple method to redirect the output towards a string port. Two methods are needed to do this: print_in_string and end_of_string. print_in_string() starts redirecting all printing statements towards the string being built. end_of_string() returns the string formed by all the printing done between these two instructions. You can only use print_in_string with one output string at a time; more complex uses require the creation of multiple string ports.

Last, claire also provides a special port which is used for tracing: trace_output(). This port can be set directly or through the trace(_) macro (cf. Appendix C). All trace statements will be directed to this port. A trace statement is either obtained implicitly through tracing a method or a rule, or explicitly with the trace statement. The statement trace(n, <string>, <args> ...) is equivalent to printf(<string>, <args> ..) with two differences: the string is printed only if the verbosity level verbose() is higher than n and the output port is trace_output().

To avoid confusion, the following hierarchy is suggested for verbosity levels:

1 - error: this message is associated with an error situation

2 - warning: this message is a warning which could indicate a problem

3 - note: this message contains useful information

4 - debug: this message contains additional information for debugging purposes

This hierarchy is used for the messages that the claire system sends to the user (which are all implemented with trace). When a program is compiled, only the trace statements which verbosity is less than the verbosity level of the compiler (default value is 2, but can be changed with –v) are kept. This means that verbosity levels 1 and 2 are meant to be used with compiled modules and levels 3 and 4 for additional information that only appears under the interpreter. How does one write debug trace statements that can be used in a compiled module ? The proper solution is to use a global variable to represent the verbosity:

TALK:integer :: 1

trace(TALK,”Enter the main loop with x = ~S\n”,x)

By changing the value of TALK, one may turn on and off the printing of these trace statements.

6.2 Reading

Ports offer the ability to direct the output to several files or devices. The same is true for reading. Ports just need to be opened in reading mode (there must be a ‘r’ in the control string when fopen is called to create a reading port). The basic function that reads the next character from a port is getc(p : port). getc(p) returns the next characters read on p. When there is nothing left to be read in a port, the method returns the special character eof. As in C, the symmetric method for printing a character on a port also exists: putc(c : char, p : port) writes the character c on p.

There are however higher-level primitives for reading. Files can be read one expression at a time : read(p : port) reads the next claire expression on the port p or, in a single step, load(s : string) reads the file associated to the string s and evaluates it. It returns true when no problem occurred while loading the file and false otherwise. A variant of this method is the method sload(s : string) which does the same thing but prints the expression read and the result of their evaluation.

Files may contain comments. A comment is anything that follows a // until the end of the line. When reading, the claire reader will ignore comments (they will not be read and hence not evaluated). For instance

x :+ 1, // increments x by 1

To insure compatibility with earlier versions, claire also recognizes lines that begin with ; as comments. Conversely, CLAIRE also supports the C syntax for block comments: anything between /* and */ will be taken as a comment. Comments in CLAIRE may become active comments that behave like trace statements if they begin with [<level>] (see Appendix C, Section 2). The global variable NeedComment may be turned to true (it is false by default) to tell the reader to place any comment found before the definition of a class or a method in the comment slot of the associated CLAIRE object.

The second type of special instructions is immediate conditionals. An immediate conditional is defined with the same syntax as a regular conditional but with a #if instead of an if

#if <test> <expression> <else <expression> >opt

When the reader finds such an expression, it evaluates the test. If the value is true, then the reader behaves as if it had read the first expression, otherwise it behaves as if it had read the second expression (or nothing if there is no else). This is useful for implementing variants (such as debugging versions). For instance

#if debug printf("the value of x is ~S",x)

Note that the expression can be a block (within parentheses) which is necessary to place a definition (like a rule definition) inside a #if. Last, there exists another pre-processing directive for loading a file within a file: #include(s) loads the file as if it was included in the file in which the #include is read.

There are a few differences between CLAIRE and C++ or Java parsing that need to be underlined:

  • Spaces are important since they act as a delimiter. In particular, a space cannot be inserted between a selector and its arguments in a call. Here is a simple example:

foo (1,2,3) // this is not correct, one must write foo(1,2,3)

  • = is for equality and := for assignment. This is standard in pseudo-code notations because it is less ambiguous.

  • characters such as +, *, -, etc. do not have a special status. This allows the user to use them in a variable name (such as x+y). However, this is not advisable since it is ambiguous for many readers. A consequence is that spaces are needed around operations within arithmetic examples such as:

x + (y * z) // instead of x+y*z which is taken as (one) variable name

  • The character ‘/’ plays a special role for namespace (module) membership.

6.3 Modules

Organizing software into modules is a key aspect of software engineering: modules separate different features as well as different levels of abstraction for a given task. To avoid messy designs and to encourage modular programming, programs can be structured into modules, which all have their own identifiers and may hide them to other modules. A module is thus a namespace that can be visible or hidden for other modules. claire supports multiple namespaces, organized into a hierarchy similar to the unix file system. The root of the hierarchy is the module claire, which is implicit. A module is defined as a usual claire object with two important slots: part_of which contains the name of the father module, and a slot uses which gives the list of all modules that can be used inside the new module. For instance,

interface :: module(part_of = library, uses = list(claire))

defines interface as a new sub-module to the library module that uses the module claire (which implies using all the modules). All module names belong to the claire namespace (they are shared) for the sake of simplicity.

Definition: A module is a CLAIRE object that represents a namespace. A namespace is a set of identifiers : each identifier (a symbol representing the name of an object) belongs to one unique namespace, but is visible in many namespaces. Namespaces allow the use of the same name for two different objects in two different modules. Modules are organized into a visibility hierarchy so that each symbol defined in a module m is visible in modules that are children of m.

Identifiers always belong to the namespace in which they are created ( claire by default). The instruction module!() returns the module currently opened. To change to a new module, one may use begin(m : module) and end(m : module). The instruction begin(m) makes m the current module. Each newly created identifier (symbol) will belong to the module m, until end(m) resumes to the original module. For instance, we may define

begin(interface)

window <: object(...)

end(interface)

This creates the identifier interface/window. Each identifier needs to be preceded by its module, unless it belongs to the current module or one of its descendent, or unless it is private (cf. visibility rules). We call the short form "window" the unqualified identifier and the long one "interface/window" the qualified identifier.

The visibility rules among name spaces are as follows:

unqualified identifiers are visible if and only if they belong to a descendent of the current module,

all qualified identifiers that are private are not visible,

other qualified identifiers are visible everywhere, but the compiler will complain if their module of origin does not belong to the list of allowed modules of the current modules.

Any identifier can be made private when it is defined by prefixing it with private/. For instance, we could have written:

begin(interface)

claire/window <: object(...)

private/temporary <: window(...)

end(interface)

The declaration private/temporary makes "temporary" a private identifier that cannot be accessed outside the module interface (or one of its descendants). The declaration claire/window makes window an identifier from the claire module (thus it is visible everywhere), which is allowed since claire belongs to the list of usable modules for interface.

In practice, there is almost always a set of files that we want to associate with a module, which means that we want to load into the module’s namespace. claire allows an explicit representation of this through the slots made_of and source. made_of(m) is the list of files (described as strings) that we want to associate with the module and source(m) is the common directory (also described as a string). The benefits are the load/sload methods that provide automatic loading of the module’s files into the right namespace and the module compiling features (cf. Appendix C). CLAIRE expects the set of file names to be different from module names, otherwise a confusion may occur at compile time. Here is an example of a module definition, as one would find it in the init.cl file:

// a simple module that is defined with two files, placed in a common directory

rt1 :: module(part_of = claire,

source = *src* / "rtmsv0.1",

uses = list(Reader),

made_of = list("model","simul"))

A last important slot of a module is uses, a list of other modules that the new module is allowed to use. This list has two purposes that only exist at compile time. The first one is to restrict the importation of symbols from other modules. A module is considered a legal import if it included itself in this uses list, or, recursively, if its father module is legal or if the module is legal for one of the modules in this list. An attempt to read a symbol m/s from a module that is not a legal import will provoke a compiler error. Second, this list is used by the compiler to find out which binaries should be included in the link script that is produced by the compiler.

The usual value is list(Reader), which is the module that contains the CLAIRE run-time environment and that supports the interpreter. It is possible to use list(Core) if the module does not require the interpreter to run, which implies, among other things, that the module contains the main@list method (cf. Appendix C).

6.4 Global Variables and Constants

claire offers the possibility to define global variables; they are named objects from the following class :

global_variable <: thing(range:type,value:any)

For instance, one can create the following

tata :: global_variable(range = integer, value = 12)

However, there is a shorthand notation:

tata:integer :: 12

Notice that, contrary to languages such as C++, you always must provide an initialization value when you define a global variable (it may be the unknown value). Variables can be used anywhere, following the visibility rules of their identifiers. Their value can be changed directly with the same syntax as local variables.

tata := 12, tata :+ 1, tata :- 10

The value that is assigned to a global variable must always belong to its range, with the exception of the unknown value, which is allowed. If a variable is re-defined, the new value replaces the old one, with the exception still of unknown, which does not replace the previous value. This is useful for defining flags, which are global_variables that are set from the outside (e.g., by the compiler). One could write, for instance,

talk:boolean :: unknown

(#if talk printf( ....

The value of talk may be set to false before running the program to avoid loading the printf statements. When the value does not change, it is simpler to just assign a value to an identifier. For instance,

toto :: 13

binds the value 13 to the identifier toto. This is useful to define aliases, especially when we use imported objects from other modules. For instance, if we use Logic/Algebra/exp, we may want to define a simpler alias with:

exp :: Logic/Algebra/exp

The value assigned to a global variable may be made part of a world definition and thus restored by backtracking using choice/backtrack. It is simply required to declare the variable as a defeasible (i.e., “backtrack”-able) variable using the store declaration as for a relation :

store(tata)

(tata := 1, choice(), tata := 2, backtrack(), assert(tata = 1))