claire v3.3.39 documentation
Documentation generated by XL CLAIRE v3.3.39 at Thu, 21 Dec 2006
- Author : Yves CASEAU
- Author : Sylvain BENILAN
Copyright
- Copyright (C) 1994-2006 Yves Caseau. All Rights Reserved.
- Copyright (C) 2000-2006 XL. All Rights Reserved.
Preamble
This documentation is mainly issued from the document by Yves Caseau
"Introduction to the CLAIRE Programming Language version 3.3". It has
been included in the source files of XL CLAIRE such to be used with the XL
code documentation generator.
In this documentation XL specific stuffs are denoted by an [XL] mark.
- Introduction
-
What is CLAIRE ?
-
Design
-
Features
-
Inspirations
- Primitives
-
Integers and Floats
-
Dates and Times [XL]
-
Chars
-
Strings
- Objects, Classes and Slots
-
Objects and Entities
-
Classes
-
Free-able objects [XL]
-
parametric class
-
Calls and Slot Access
-
Updates
- Lists, Sets and Instructions
-
Lists, Sets and Tuples
-
Blocks
-
Conditionals
-
Loops
-
Instantiation
-
Exception Handling
-
array
- Methods and Types
-
Methods
-
Types
-
Polymorphism
-
Escaping Types
-
Selectors, Properties and Operations
-
Iterations
- Tables, Rules and Hypothetical Reasoning
-
Tables
-
Rules
-
Hypothetical Reasoning
- I/O, Modules and System Interface
-
Communication ports [XL]
-
Printing
-
WCL syntax [XL]
-
Reading
-
Symbols
-
Modules
-
Global Variables and Constants
-
Command line handling [XL]
-
Serialization [XL]
- Platform
-
Miscellaneous
-
Environment variables [XL]
-
Process handling [XL]
-
File system [XL]
-
Signal Handling [XL]
- Using XL Claire
-
Debugging [XL]
- driving optimizations
- Command line help : {-h | -help} +[<m> | <option> | <index>]
- About box : -about
- Memory initialization : [-auto] -s <main> <world>
- Verbose : -v [<m>] <level>
- Exit now : -q [<exitcode>]
- Early termination : -qonerror | -errassegv
- Avoid banner : -nologo
- Avoid editline : -noel
- Terminal color : -color | -nocolor
- Trace file : -trace [a | append] <file>
- Sampling Cmemory : -sample <period>
- Change directory : -chdir <dir>
- Debugger : -debug
- Load file : {-f | -ef} +[<file>]
- Eval expression : {-princ | -print | -eval} <exp>
- Load script : {-x | -xe | -x<S>-<W> | -xe<S>-<W>} <file> [<args>]
- Goto definition : -gotodef [<dir>] [<s>]
- Generate documentation : [-doclink] [-onefile | -categories] {-apidoc | -codedoc} <m>
- No ffor construct : -noffor
- Compiler environment : -env <env>
- Link with module : -m <m>[/<version>] | -l <library>
- Output directory : -od <directory>
- Safety : -safe | -os <safety>
- Output name : -o <name>
- Optimization : -D | -O
- Profiler : -p
- C++ compiler : -cpp <cxxoption> | -link <linkeroption> | -make <makeroption>
- Compile module : {-cc | -cl | -cm} [<m>]
- Compile module library : [-both] {-cls | -call [-sm <m>] [-em <m>]}
- Module publication : [-sudo] [-ov] {-publish | -export [<i> | <directory>]}
- New module : -nm [<partof>/]<name> +[<m> | <f>{.cpp | .cl | .h}]
- Module info : -ml | {-mi <m>[/<version>]}
- Configuration file : -cx <test>
- No init : -n
- Fast dispatch : -fcall
- Console : -noConsole | -wclConsole
- *fs* : string := (if externC("\n#ifdef CLPC\nCTRUE\n#else\nCFALSE\n#endif\n", boolean) "\\" else "/")
- *ps* : string := (if externC("\n#ifdef CLPC\nCTRUE\n#else\nCFALSE\n#endif\n", boolean) ";" else ":")
- MAX_INTEGER :: 1073741822
- nil :: Id(nil)
- port! :: blob! [XL]
- stderr : port := Clib_stderr [XL]
- stdin : port := Clib_stdin [XL]
- stdout : port := Clib_stdout [XL]
- !=(x:any, y:any) -> boolean
- %(x:any, y:any) -> boolean
- *(x:integer, y:integer) => integer
- *(self:float, x:float) -> float
- +(self:integer, x:integer) -> type[abstract_type(+, self, x)]
- +(self:float, x:float) -> float
- +(self:char*, n:integer) -> char* [XL]
- -(x:integer) => integer
- -(x:float) -> float
- -(self:float, x:float) -> float
- -(self:integer, x:integer) -> type[Core/abstract_type(-, Core/self, Core/x)]
- ..(x:integer, y:integer) -> Interval
- /(s:string, s2:string) -> string
- /(x:integer, y:integer) => integer
- /(self:float, x:float) -> float
- /+(l1:list, l2:list) -> list
- /+(s1:string, s2:string) -> string
- /+(x:bag, y:bag) -> list
- /-(s1:string, s2:string) -> string [XL]
- <(x:integer, y:integer) -> boolean
- <(x:float, y:float) => boolean
- <(x:string, y:string) -> boolean
- <(x:char, y:char) -> boolean
- <<(x:integer, n:integer) -> integer
- <<(l:list, n:integer) -> list
- <=(x:integer, y:integer) -> boolean
- <=(x:float, y:float) => boolean
- <=(c1:char, c2:char) -> boolean
- <=(x:string, y:string) -> boolean
- <=(x:type, y:type) -> boolean
- =(x:any, y:any) -> boolean
- =type?(self:type, ens:type) -> boolean
- >(x:string, y:string) -> boolean
- >(x:char, y:char) -> boolean
- >(x:float, y:float) => boolean
- >(x:integer, y:integer) -> boolean
- >=(x:string, y:string) -> boolean
- >=(x:char, y:char) -> boolean
- >=(x:integer, y:integer) => boolean
- >=(x:float, y:float) => boolean
- >>(x:integer, n:integer) -> integer
- ^(s1:set, s2:set) -> set
- ^(self:float, x:float) -> float
- ^(x:integer, y:integer) => integer
- ^(l:list, y:integer) -> list
- ^2(x:integer) => void
- abstract(c:class) -> any
- acos(self:float) -> float
- add(l:list, x:any) -> list
- add(s:set, x:any) -> set
- add(self:property, x:object, y:any) -> void
- add*(l1:list, l2:list) -> list
- alpha?(c:char) -> boolean [XL]
- alpha?(s:string) -> boolean [XL]
- and(x:integer, y:integer) -> integer
- apply(m:method, l:list) -> any
- apply(p:property, l:list) -> any
- apply(self:lambda, %l:list) -> any
- apply(self:function, ls:list, l:list) -> any
- array!(x:bag, t:type) -> type[t[]]
- asin(self:float) -> float
- atan(self:float) -> float
- backtrack() -> void
- backtrack(n:integer) -> void
- begin(m:module) -> void
- bin!(i:integer) -> string [XL]
- blob!() -> blob [XL]
- blob!(p:blob) -> blob [XL]
- blob!(n:integer) -> blob [XL]
- blob!(self:string) -> blob [XL]
- blob!(p:port) -> blob [XL]
- bqexpand(s:string) -> string [XL]
- buffer!(self:port, bufsize:integer) -> buffer [XL]
- call(p:property, l:listargs) -> any
- car(self:list) -> any
- cast!(s:bag, t:type) -> bag
- cdr(l:list) -> type[l]
- char!(n:integer) -> char
- chmod(s:string, m:integer) -> void [XL]
- choice() -> void
- chroot(dir:string) -> void [XL]
- class!(x:type) -> class
- client!(addr:string) -> socket [XL]
- client!(addr:string, p:integer) -> socket [XL]
- close_target!(self:filter) -> type[self] [XL]
- color() -> integer [XL]
- color(c:integer) -> integer [XL]
- color_princ(s:string) -> void [XL]
- commit() -> void
- commit(n:integer) -> void
- cons(x:any, l:list) -> list
- contradiction!() -> void
- copy(s:bag) -> bag
- copy(a:array) -> array
- copy(x:object) -> object
- copy(s:string) -> string
- cos(self:float) -> float
- cosh(self:float) -> float
- date_add(d:float, c:char, i:integer) -> float [XL]
- decode64(pr:port, pw:port) -> void [XL]
- delete(s:bag, x:any) -> bag
- delete(self:property, x:object, y:any) -> any
- diff_time(d1:float, d2:float) -> float [XL]
- difference(self:set, x:set) -> set
- digit?(c:char) -> boolean [XL]
- digit?(s:string) -> boolean [XL]
- ding() -> void [XL]
- divide?(x:integer, y:integer) -> boolean
- elapsed(t:float) -> integer [XL]
- encode64(pr:port, pw:port, line_length:integer) -> void [XL]
- end(m:module) -> void
- end_of_string() -> string
- entries(s:string) -> list[string] [XL]
- entries(s:string, w:string) -> list[string] [XL]
- environ(i:integer) -> string [XL]
- eof?(self:port) -> boolean [XL]
- ephemeral(self:class) -> any
- erase(a:table) -> void
- erase(self:property, x:object) -> any
- escape(s:string) -> string [XL]
- exception!() -> exception
- exit(self:integer) -> void
- explode(t:float) -> tuple(integer, 1 .. 12, 1 .. 365, 1 .. 31, 1 .. 7, 0 .. 23, 0 .. 59, 0 .. 59, boolean) [XL]
- explode(s:string, sep:string) -> list[string] [XL]
- explode_wildcard(s:string, w:string) -> list[string] [XL]
- faccessed(s:string) -> float [XL]
- factor?(x:integer, y:integer) -> boolean
- fchanged(s:string) -> float [XL]
- fcopy(s1:string, s2:string) -> void [XL]
- filter!(self:filter, p:port) -> type[self] [XL]
- final(c:class) -> any
- final(c:class) -> void
- find(s:string, x:string) -> integer [XL]
- find(s:string, x:string, from:integer) -> integer [XL]
- finite?(self:type) -> boolean
- float!(x:integer) -> float
- float!(self:string) -> float
- flush(self:port) -> void
- flush(self:port, n:integer) -> void
- fmode(s:string) -> integer [XL]
- fmodified(s:string) -> float [XL]
- fopen(self:string, mode:OPEN_MODE) -> buffer [XL]
- fork() -> integer [XL]
- format(self:string, larg:list) -> void
- fread(self:port) -> string [XL]
- fread(self:port, s:string) -> integer [XL]
- fread(self:port, n:integer) -> string [XL]
- freadline(p:port) -> string [XL]
- freadline(p:port, seps:bag) -> tuple(string, string U char) [XL]
- freadline(p:port, sep:string) -> string [XL]
- freadline(p:port, seps:bag, sensitive?:boolean) -> tuple(string, string U char) [XL]
- freadline(p:port, sep:string, sensitive?:boolean, esc:char) -> string [XL]
- freadline(p:port, seps:bag, sensitive?:boolean, esc:char) -> tuple(string, string U char) [XL]
- freadwrite(src:port, trgt:port) -> integer [XL]
- freadwrite(src:port, trgt:port, len:integer) -> integer [XL]
- fsize(s:string) -> float [XL]
- fskip(self:port, len:integer) -> integer [XL]
- funcall(l:lambda, x:any) -> void
- funcall(m:method, x:any) -> void
- funcall(m:method, x:any, y:any) -> void
- funcall(l:lambda, x:any, y:any) -> void
- funcall(f:function, s1:class, x:any, s:class) -> void
- funcall(l:lambda, x:any, y:any, z:any) -> void
- funcall(m:method, x:any, y:any, z:any) -> void
- funcall(f:function, s1:class, x:any, s2:class, y:any, s:class) -> void
- funcall(f:function, s1:class, x:any, s2:class, y:any, s3:class, z:class, s:class) -> void
- fwrite(self:string, p:port) -> integer [XL]
- gc() -> void
- gensym(s:string) -> symbol
- gensym(self:void) -> symbol
- get(a:array, x:any) -> integer
- get(l:list, x:any) -> integer
- get(s:slot, x:object) -> any
- get(s:string, c:char) -> integer
- get(self:property, x:object) -> any
- get_index(self:blob) -> integer [XL]
- get_value(self:string) -> any
- get_value(self:module, s:string) -> any
- getc(self:port) -> char [XL]
- getenv(self:string) -> string
- gethostname() -> string [XL]
- getitimer(it:itimer) -> tuple(integer, integer) [XL]
- getlocale(cat:integer) -> string [XL]
- getpid() -> integer [XL]
- getppid() -> integer [XL]
- getuid() -> integer [XL]
- hash(l:list, x:any) -> integer
- hex!(i:integer) -> string [XL]
- Id(x:any) -> type[x]
- inherit?(self:class, ens:class) -> boolean
- integer!(s:set[integer]) -> integer
- integer!(s:string) -> integer
- integer!(c:char) -> integer
- integer!(f:float) -> integer
- integer!(s:symbol) -> integer
- interface(p:property) -> void
- interface(c:class U Union, pi:listargs) -> void
- inverse(r:relation) -> relation
- isdir?(s:string) -> boolean [XL]
- isenv?(v:string) -> boolean [XL]
- isfile?(s:string) -> boolean [XL]
- kill(p:integer) -> void [XL]
- kill(self:object) -> any
- kill(self:class) -> any
- kill(p:integer, sig:signal_handler) -> void [XL]
- kill!(self:any) -> any
- known?(self:any) -> boolean
- known?(self:property, x:object) -> boolean
- last(self:list) -> type[member(self)]
- left(s:string, i:integer) -> string [XL]
- length(self:blob) -> integer [XL]
- length(self:string) -> integer
- length(a:array) -> integer
- length(self:bag) -> integer
- line_buffer!(self:port) -> line_buffer [XL]
- linger(self:socket) -> void [XL]
- link(s1:string, s2:string) -> void [XL]
- list!(s:set) -> type[list[member(s)]]
- list!(a:array) -> type[list[member(a)]]
- log(x:float) -> float
- lower(s:string) -> string [XL]
- lower?(s:string) -> boolean [XL]
- lower?(c:char) -> boolean [XL]
- ltrim(s:string) -> void [XL]
- make_array(i:integer, t:type, v:any) -> type[(if unique?(t) the(t)[] else array)]
- make_date(s:string) -> float [XL]
- make_date(D:integer, M:integer, Y:integer, h:integer, m:integer, s:integer) -> float [XL]
- make_list(n:integer, x:any) -> type[list[list<any>(x),list({})]]
- make_list(t:type, n:integer) -> list [XL]
- make_set(x:integer) -> set
- make_string(self:symbol) -> string
- make_string(i:integer, c:char) -> string
- make_table(d:type, t:type, x:any) -> table
- make_time(s:string) -> float [XL]
- make_time(h:integer, m:integer, s:integer) -> float [XL]
- match_wildcard?(s:string, w:string) -> boolean [XL]
- max(f:method, self:bag) -> type[member(self)]
- max(x:integer, y:integer) -> integer
- max(x:float, y:float) -> float
- maxenv() -> integer [XL]
- member(x:type) -> type
- member_type(x:array) -> type
- mime_decode(s:string) -> string [XL]
- mime_encode(s:string) -> string [XL]
- min(f:method, self:bag) -> type[member(self)]
- min(x:integer, y:integer) -> integer
- min(x:float, y:float) -> float
- mkdir(s:string) -> void [XL]
- mkdir(s:string, m:integer) -> void [XL]
- mod(x:integer, y:integer) -> integer
- module!() -> module
- module!(r:restriction) -> module
- new(self:class) -> type[object glb member(self)]
- new(self:class, %nom:symbol) -> type[thing glb member(self)]
- not(self:any) -> boolean
- nth(t:table, x:any) -> any
- nth(l:bag, i:integer) -> any
- nth(self:char*, n:integer) -> char [XL]
- nth(s:string, i:integer) => any
- nth(self:blob, n:integer) -> char [XL]
- nth(a:array, i:integer) => any
- nth(n:integer, i:integer) => any
- nth(t:table, x:any, y:any) -> any
- nth+(l:list, i:integer, x:any) -> bag
- nth-(l:list, i:integer) -> bag
- nth=(s:string, i:integer, c:char) => any
- nth=(self:char*, n:integer, c:char) -> void [XL]
- nth=(t:table, x:any, y:any) -> any
- nth=(self:blob, n:integer, c:char) -> void [XL]
- nth=(self:array, x:integer, y:any) -> void
- nth=(self:list, x:integer, y:any) -> any
- nth=(t:table, x1:any, x2:any, y:any) -> any
- occurrence(s:string, z:string) -> integer [XL]
- or(x:integer, y:integer) -> integer
- owner(self:any) -> class
- pipe!() -> tuple(pipe, pipe) [XL]
- popen(file:string, mod:{"r", "w", "rw"}) -> popen_device [XL]
- prealloc_list(t:type, n:integer) -> list [XL]
- prealloc_set(t:type, n:integer) -> set [XL]
- princ(s:bag) -> void
- princ(n:integer) -> void
- princ(c:char) -> void
- princ(s:string) -> void
- princ(s:string, i:integer, j:integer) -> void [XL]
- print(x:any) -> void
- print_in_string() -> void
- put(s:symbol, x:any) -> any
- put(p:property U slot, x:object, y:any) -> any
- put(t:table, x:object, y:any) -> any
- put_store(self:property, x:object, y:any, b:boolean) -> void
- putc(self:char, p:port) -> void
- pwd() -> string [XL]
- raise(sig:signal_handler) -> void [XL]
- random(n:integer) -> integer
- random!() -> void [XL]
- random!(n:integer) -> void
- read(self:property, x:object) -> any
- read!(self:port) -> void [XL]
- readable?(self:port) -> boolean [XL]
- release() -> string
- reopen(self:port) -> port [XL]
- replace(src:string, s:string, rep:string) -> string [XL]
- rfind(s:string, x:string) -> integer [XL]
- rfind(s:string, x:string, from:integer) -> integer [XL]
- right(s:string, i:integer) -> string [XL]
- rmdir(s:string) -> void [XL]
- rmlast(self:list) -> list
- rtrim(s:string) -> void [XL]
- safe(x:any) -> any
- select?() -> boolean [XL]
- select?(ms:integer) -> boolean [XL]
- serialize(p:port, self:any) -> serialize_context [XL]
- serialize(p:port, top?:boolean, self:any) -> serialize_context [XL]
- server!(addr:string) -> listener [XL]
- server!(p:integer) -> listener [XL]
- server!(addr:string, p:integer, qlen:integer) -> listener [XL]
- set_index(self:blob, n:integer) -> void [XL]
- set_length(self:blob, n:integer) -> void [XL]
- setcwd(s:string) -> void [XL]
- setenv(s:string) -> void [XL]
- setitimer(it:itimer, interval:integer, value:integer) -> tuple(integer, integer) [XL]
- setlocale(cat:integer, s:string) -> string [XL]
- shell(self:string) -> integer
- shrink(s:string, n:integer) -> string
- shrink(l:list, n:integer) -> list
- sigblock(self:subtype[signal_handler]) -> set[signal_handler] [XL]
- signal(sig:signal_handler, p:property) -> property [XL]
- sigpending() -> set[signal_handler] [XL]
- sigprocmask() -> set[signal_handler] [XL]
- sigsetmask(self:subtype[signal_handler]) -> set[signal_handler] [XL]
- sigsuspend(self:subtype[signal_handler]) -> void [XL]
- sigunblock(self:subtype[signal_handler]) -> set[signal_handler] [XL]
- sin(self:float) -> float
- sinh(self:float) -> float
- sleep(t:integer) -> void [XL]
- socketpair() -> tuple(socket, socket) [XL]
- sort(f:method, self:list) -> list
- space?(c:char) -> boolean [XL]
- space?(s:string) -> boolean [XL]
- sqrt(self:float) -> float
- store(v:global_variable) -> void
- store(rels:listargs) -> void
- store(a:array, n:integer, v:any, b:boolean) -> void
- store(l:list, n:integer, v:any, b:boolean) -> void
- strftime(f:string, d:float) -> string [XL]
- string!(s:symbol) -> string
- string!(self:float) -> string
- string!(self:char) -> string [XL]
- string!(self:blob) -> string [XL]
- string!(n:integer) -> string
- string!(self:char*, len:integer) -> string [XL]
- substring(s:string, i:integer, j:integer) -> string
- substring(s1:string, s2:string, b:boolean) -> integer
- substring(self:blob, i:integer, j:integer) -> string [XL]
- symbol!(self:string) -> symbol
- symbol!(s:string, m:module) -> symbol
- symlink(s1:string, s2:string) -> void [XL]
- tan(self:float) -> float
- tanh(self:float) -> float
- time_get() -> integer
- time_set() -> void
- time_show() -> void
- timer!() -> float [XL]
- trim(s:string) -> void [XL]
- tuple!(x:list) -> tuple
- unescape(s:string) -> string [XL]
- unget(self:port, s:string) -> void [XL]
- unget(self:port, c:char) -> void [XL]
- unix?() -> boolean [XL]
- unknown?(self:any) -> boolean
- unknown?(self:property, x:object) -> boolean
- unlink(self:listener) -> void [XL]
- unlink(s:string) -> void [XL]
- unserialize(p:port) -> any [XL]
- upper(s:string) -> string [XL]
- upper?(s:string) -> boolean [XL]
- uptime(t:float) -> void [XL]
- url_decode(s:string) -> string [XL]
- url_encode(s:string) -> string [XL]
- use_as_output(p:port) -> port
- waitpid(p:integer) -> tuple(process_status, integer, any) [XL]
- waitpid(p:integer, block?:boolean) -> tuple(process_status, integer, any) [XL]
- world?() -> integer
- writable?(self:port) -> boolean [XL]
- write(self:property, x:object, y:any) -> void
- write!(self:port) -> void [XL]
claire categories
Introduction
What is CLAIRE ?
CLAIRE is a high-level, portable, functional and object-oriented language with advanced
rule processing capabilities. It is intended to allow the programmer to express complex
algorithms with fewer lines and in an elegant and readable manner.
To provide a high degree of expressivity, CLAIRE uses :
To achieve its goal of readability, CLAIRE uses :
- set-based programming with an intuitive syntax,
- simple-minded object-oriented programming,
- truly polymorphic and parametric functional programming,
- an entity-relation approach with explicit relations, inverses and unknown values.
Design
CLAIRE was designed for advanced applications that involve complex data modeling, rule processing
and problem solving. CLAIRE was meant to be used in a C++ environment, either as a satellite
(linking CLAIRE programs to C++ programs is straightforward) or as an upper layer (importing
C++ programs is also easy). The key set of features that distinguishes CLAIRE from other
programming languages has been dictated by our experience in solving complex optimization problems.
Of particular interest are two features that distinguish CLAIRE from procedural languages such as
C++ or Java :
- Versioning : CLAIRE supports versioning of a user-selected view of the entire system.
The view can be made as large (for expressiveness) or as small (for efficiency) as is necessary.
Versions are created linearly and can be viewed as a stack of snapshots of the system. CLAIRE
supports very efficient creation/rollback of versions, which constitutes the basis for powerful
backtracking, a key feature for problem solving. Unlike most logic programming languages, this
type of backtracking covers any user-defined structure, not simply a set of logic variables.
- Production rules : CLAIRE supports rules that bind a CLAIRE expression (the conclusion) to
the combination of an event and a logical condition. Whenever this event occurs, if the condition
is verified, then the conclusion is evaluated. The emphasis on events is a natural evolution from
rule-based inference engines and is well suited to the description of reactive algorithms such as
constraint propagation.
Features
CLAIRE provides automatic memory allocation/de-allocation, which would have prevented an easy
implementation as a C++ library. Also, set-oriented programming is much easier with a set-oriented
language like CLAIRE than with libraries. CLAIRE is about ten years old and the current version
reaches a new level of maturity.
CLAIRE is a high-level language that can be used as a complete development language, since it is
a general purpose language, but also as a pre-processor to C++ or Java, since a CLAIRE program
can be naturally translated into a C++ program (We continue to use C++ as our target language of
choice, but the reader may now substitute Java to C++ in the rest of this document). CLAIRE is a
set-oriented language in the sense that sets are first-class objects, typing is based on sets
and control structures for manipulating sets are parts of the language kernel. Similarly,
CLAIRE makes manipulating lists easy since lists are also first-class objects. Sets and lists
may be typed to provide a more robust and expressive framework. CLAIRE can also be seen as a
functional programming language, with full support for lambda abstraction, where functions can
be passed as parameters and returned as values, and with powerful parametric polymorphism.
CLAIRE is an object-oriented language with single inheritance. As in SMALLTALK, everything
that exists in CLAIRE is an object. Each object belongs to a unique class and has a unique
identity. Classes are the corner stones of the language, from which methods (procedures),
slots and tables (relations) are defined. Classes belong themselves to a single inheritance
hierarchy. However, classes may be grouped using set union operators, and these unions may be
used in most places where a class would be used, which offers an alternative to multiple
inheritance. In a way similar to Modula-3, CLAIRE is a modular language that provides
recursively embedded modules with associated namespaces. Module decomposition can either be
parallel to the class organization (mimicking C++ encapsulation) or orthogonal
(e.g., encapsulating one service among multiple classes).
CLAIRE is a typed language, with full inclusion polymorphism. This implies that one can
use CLAIRE with a variety of type disciplines ranging from weak typing in a manner that is
close to SMALLTALK up to a more rigid manner close to C++. This flexibility is useful to
capture programming styles ranging from prototyping to production code development. The more
typing information available, the more CLAIRE's compiler will behave like a statically typed
language compiler. This is achieved with a rich type system, based on sets, that goes beyond
types in C++. This type system provides functional types (second-order types) similar to ML,
parametric types associated to parametric classes and many useful type constructors such as
unions or intervals. Therefore, the same type system supports the naive user who simply
wishes to use classes as types and the utility library developer who needs a powerful
interface description language.
[XL] Starting with XL CLAIRE, CLAIRE is intended to cover various aspects of web oriented
application (running in a CGI like environment), that is to serve dynamic content over
the web. The Wcl syntax is introduced as new method of printing, in a similar way to
printf but closer to the HTML syntax. The development of a web oriented agent would
however require the module Wcl, not included in the standard XL CLAIRE distribution.
Inspirations
As the reader will notice, CLAIRE draws its inspiration from a large number of existing languages.
A non-exhaustive list would include SMALLTALK for the object-oriented aspects, SETL for the set
programming aspects, OPS5 for the production rules, LISP for the reflection and the functional
programming aspects, ML for the polymorphism and C for the general programming philosophy. As far
as its ancestors are concerned, CLAIRE is very much influenced by LORE, a language developed in
the mid 80s for knowledge representation. It was also influenced by LAURE but is much smaller and
does not retain the original features of LAURE such as constraints or deductive rules. CLAIRE is
also closer to C in its spirit and its syntax than LAURE was.
Primitives
Integers and Floats
- *(x:integer, y:integer) => integer
- *(self:float, x:float) -> float
- +(self:float, x:float) -> float
- +(self:integer, x:integer) -> type[abstract_type(+, self, x)]
- -(x:integer) => integer
- -(x:float) -> float
- -(self:integer, x:integer) -> type[Core/abstract_type(-, Core/self, Core/x)]
- -(self:float, x:float) -> float
- /(x:integer, y:integer) => integer
- /(self:float, x:float) -> float
- <(x:integer, y:integer) -> boolean
- <(x:float, y:float) => boolean
- <<(x:integer, n:integer) -> integer
- <=(x:integer, y:integer) -> boolean
- <=(x:float, y:float) => boolean
- >(x:float, y:float) => boolean
- >(x:integer, y:integer) -> boolean
- >=(x:float, y:float) => boolean
- >=(x:integer, y:integer) => boolean
- >>(x:integer, n:integer) -> integer
- ^(self:float, x:float) -> float
- ^(x:integer, y:integer) => integer
- ^2(x:integer) => void
- acos(self:float) -> float
- and(x:integer, y:integer) -> integer
- asin(self:float) -> float
- atan(self:float) -> float
- bin!(i:integer) -> string [XL]
- char!(n:integer) -> char
- cos(self:float) -> float
- cosh(self:float) -> float
- divide?(x:integer, y:integer) -> boolean
- factor?(x:integer, y:integer) -> boolean
- float!(x:integer) -> float
- hex!(i:integer) -> string [XL]
- integer!(f:float) -> integer
- integer!(s:set[integer]) -> integer
- log(x:float) -> float
- make_set(x:integer) -> set
- max(x:float, y:float) -> float
- max(x:integer, y:integer) -> integer
- MAX_INTEGER :: 1073741822
- min(x:float, y:float) -> float
- min(x:integer, y:integer) -> integer
- mod(x:integer, y:integer) -> integer
- nth(n:integer, i:integer) => any
- or(x:integer, y:integer) -> integer
- random(n:integer) -> integer
- random!() -> void [XL]
- random!(n:integer) -> void
- sin(self:float) -> float
- sinh(self:float) -> float
- sqrt(self:float) -> float
- string!(self:float) -> string
- string!(n:integer) -> string
- tan(self:float) -> float
- tanh(self:float) -> float
Both floats and integers are CLAIRE primitives.
integers are represented using 30 bits (which is required for the OID model)
and are always signed. Floats are represented as C double precision floating point
numbers.
Arithmetic between integers and float can be handled using conversion method integer! and
float! :
Dates and Times [XL]
- date_add(d:float, c:char, i:integer) -> float [XL]
- diff_time(d1:float, d2:float) -> float [XL]
- elapsed(t:float) -> integer [XL]
- explode(t:float) -> tuple(integer, 1 .. 12, 1 .. 365, 1 .. 31, 1 .. 7, 0 .. 23, 0 .. 59, 0 .. 59, boolean) [XL]
- make_date(s:string) -> float [XL]
- make_date(D:integer, M:integer, Y:integer, h:integer, m:integer, s:integer) -> float [XL]
- make_time(s:string) -> float [XL]
- make_time(h:integer, m:integer, s:integer) -> float [XL]
- strftime(f:string, d:float) -> string [XL]
- time_get() -> integer
- time_set() -> void
- time_show() -> void
- timer!() -> float [XL]
- uptime(t:float) -> void [XL]
Dates and times are represented using floats containing an UNIX C time that is
the time in seconds since the Epoch (00:00:00 UTC, January 1, 1970). The use of
float is required since CLAIRE integer are coded on 30 bits and times on 32 bits.
Internally, CLAIRE always handles dates represented in UTC.
Times are referenced on the Epoch such the arithmetic between date and time can
be made with the standard float arithmetic.
Two kind of timer are also supported, one that account time in the
process time (time_set/time_get) and one that count in real time (timer!/elapsed).
Chars
- <(x:char, y:char) -> boolean
- <=(c1:char, c2:char) -> boolean
- >(x:char, y:char) -> boolean
- >=(x:char, y:char) -> boolean
- alpha?(c:char) -> boolean [XL]
- digit?(c:char) -> boolean [XL]
- integer!(c:char) -> integer
- lower?(c:char) -> boolean [XL]
- space?(c:char) -> boolean [XL]
- string!(self:char) -> string [XL]
In CLAIRE chars are true object (i.e. not primitive) that hold an 8 bit value, one can
obtain this value with integer!(c:char) and make a char from an integer i with char!(i:integer).
[XL] Starting with XL CLAIRE the internal 8 bit value is stored as an unsigned char (vs. signed char
in CLAIRE 3) such the composition char!(integer!()) is actually the identity for all possible
existing char in the system. If their is no char that hold a given value n then char!(n)
will produce an error.
Strings
- /(s:string, s2:string) -> string
- /+(s1:string, s2:string) -> string
- /-(s1:string, s2:string) -> string [XL]
- <(x:string, y:string) -> boolean
- <=(x:string, y:string) -> boolean
- >(x:string, y:string) -> boolean
- >=(x:string, y:string) -> boolean
- alpha?(s:string) -> boolean [XL]
- copy(s:string) -> string
- digit?(s:string) -> boolean [XL]
- escape(s:string) -> string [XL]
- explode(s:string, sep:string) -> list[string] [XL]
- explode_wildcard(s:string, w:string) -> list[string] [XL]
- find(s:string, x:string) -> integer [XL]
- find(s:string, x:string, from:integer) -> integer [XL]
- float!(self:string) -> float
- get(s:string, c:char) -> integer
- integer!(s:string) -> integer
- left(s:string, i:integer) -> string [XL]
- length(self:string) -> integer
- lower(s:string) -> string [XL]
- lower?(s:string) -> boolean [XL]
- ltrim(s:string) -> void [XL]
- make_string(i:integer, c:char) -> string
- match_wildcard?(s:string, w:string) -> boolean [XL]
- mime_decode(s:string) -> string [XL]
- mime_encode(s:string) -> string [XL]
- nth(s:string, i:integer) => any
- nth=(s:string, i:integer, c:char) => any
- occurrence(s:string, z:string) -> integer [XL]
- replace(src:string, s:string, rep:string) -> string [XL]
- rfind(s:string, x:string) -> integer [XL]
- rfind(s:string, x:string, from:integer) -> integer [XL]
- right(s:string, i:integer) -> string [XL]
- rtrim(s:string) -> void [XL]
- shrink(s:string, n:integer) -> string
- space?(s:string) -> boolean [XL]
- substring(s:string, i:integer, j:integer) -> string
- substring(s1:string, s2:string, b:boolean) -> integer
- trim(s:string) -> void [XL]
- unescape(s:string) -> string [XL]
- upper(s:string) -> string [XL]
- upper?(s:string) -> boolean [XL]
- url_decode(s:string) -> string [XL]
- url_encode(s:string) -> string [XL]
In CLAIRE strings are represented using a C char*, that is a sequence of 8 bit characters.
The length of a string is computed by the general method length @ string :
[XL] Starting with XL CLAIRE strings may contain null char under certain restriction.
Indeed XL CLAIRE makes difference between strings that are dynamically allocated and
strings that are are statically compiled :
s0 :: "a static string" // would compile as a static string s1 :: ("a string" /+ string!('\0') /+ "another string") // dynamically built string |
In the above example s0 would be compiled statically, that is the string content would
be allocated outside claire memory (e.g. in the data space of the program). In addition
s1 is allocated dynamically and stored in a chunk of the claire memory. The important
difference between these two string is the handling of their length :
- static: the length is computed with strlen, so that a static string cannot
contain a null character.
- dynamic: the length is a property of the string stored outside the string content
(like Pascal string handling) which allow arbitrary content including null character.
Notice that inside the interpreter all strings are dynamically allocated which can give
different behavior between the interpreted and the compiled code, for instance :
s :: "a\0b" // dangerous !!! static string that contain a null (printf("~S\n", length(s))) |
In the interpreter this code would print 3 whereas the compiled code would print 1. Indeed
s would be compiled statically (outside claire memory) and its length would handled by
strlen. As a general rule, any method that returns a new string returns a dynamic string. This is
especially true for the port interface (fread and friends) which is particularly convenient
to handle binary streams without having to deal with the null.
Objects, Classes and Slots
Objects and Entities
- !=(x:any, y:any) -> boolean
- =(x:any, y:any) -> boolean
- known?(self:any) -> boolean
- not(self:any) -> boolean
- owner(self:any) -> class
- unknown?(self:any) -> boolean
A program in CLAIRE is a collection of entities (everything in CLAIRE is an entity).
Some entities are pre-defined, we call them primitive entities, and some others may be
created when writing a program, we call them objects. The set (a class) of all entities is
called any and the set (a class also) of all objects is called object.
[XL] In XL CLAIRE port are not imported entity but implemented an extensible class vs. primitive (see port).
Primitive entities consist of integers, floats, symbols, strings and
functions. The most common operations on them are already built in, but you can add yours.
You may also add your own entity classes using the import mechanism.
Objects can be seen as "records", with named fields (called slots) and unique identifiers.
Two objects are distinct even if they represent the same record. The data record structure and
the associated slot names are represented by a class. An object is uniquely an instance of a class,
which describes the record structure (ordered list of slots). CLAIRE comes with a collection of
structures (classes) as well as with a collection of objects (instances).
Definition : A class is a generator of objects, which are called its instances. Classes are organized into
an inclusion hierarchy (a tree), so a class can also be seen as an extensible set of objects,
which is the set of instances of the class itself and all its subclasses. A class has one
unique father in the inclusion hierarchy (also called the inheritance hierarchy), called its
superclass. It is a subclass of its superclass. |
Each entity in CLAIRE belongs to a special class called its owner, which is the smallest
class to which the entity belongs. The owner relationship is the extension to any of the
traditional isa relationship between objects and classes, which implies that for any object x,
x.isa = owner(x).
Thus the focus on entities in CLAIRE can be summarized as follows: everything is an entity,
but not everything is an object. An entity is described by its owner class, like an object, but
objects are "instantiated" from their classes and new instances can be made, while entities are
(virtually) already there and their associated (primitive) classes don't need to be instantiated.
A corollary is that the list of instances for a primitive class is never available.
Classes
- abstract(c:class) -> any
- add(self:property, x:object, y:any) -> void
- class!(x:type) -> class
- close :: property(open = 3)
- copy(x:object) -> object
- delete(self:property, x:object, y:any) -> any
- ephemeral(self:class) -> any
- erase(self:property, x:object) -> any
- exception!() -> exception
- final(c:class) -> any
- get(self:property, x:object) -> any
- get(s:slot, x:object) -> any
- kill(self:object) -> any
- kill(self:class) -> any
- kill!(self:any) -> any
- known?(self:property, x:object) -> boolean
- new(self:class) -> type[object glb member(self)]
- new(self:class, %nom:symbol) -> type[thing glb member(self)]
- put(p:property U slot, x:object, y:any) -> any
- read(self:property, x:object) -> any
- unknown?(self:property, x:object) -> boolean
- write(self:property, x:object, y:any) -> void
Classes are organized into a tree, each class being the subclass of another one, called its
superclass. This relation of being a subclass (inheritance) corresponds to set inclusion: each
class denotes a subset of its superclass. So, in order to identify instances of a class as objects
of its superclass, there has to be some correspondence between the structures of both classes: all
slots of a class must be present in all its subclasses. Subclasses are said to inherit the
structure (slots) of their superclass (while refining it with other slots). The root of the
class tree is the class any since it is the set of all entities. Formally, a class is defined by
its superclass and a list of additional slots. Two types of classes can be created: those whose
instances will have a name and those whose instances will be unnamed. Named objects must inherit
(not directly, but they must be descendents) of the class thing. A named object is an object that
has a name, which is a symbol that is used to designate the object and to print it. A named object
is usually created with the x :: C() syntax but can also be created with
new(C, name).
Each slot is given as <name>:<range>=<default>. The range is a type and the optional default value
is an object which type is included in <range>. The range must be defined before it is used, thus
recursive class definitions use a forward definition principle (e.g., person).
person <: thing // forward definition person <: thing(age:integer = 0, father:person) woman <: person // another forward definition man <: person(wife:woman) woman <: person(husband:man) child <: person(school:string) complex <: object(re:float, im:float) |
A class inherits from all the slots of its super-classes, so they need not be recalled in the
definition of the class. For instance, here, the class child contains the slots age and father,
because it inherited them from person. A default value is used to place in the object slot during the instantiation (creation of
a new instance) if no explicit value is supplied. The default value must belong to the range
and will trigger rules or inverses in the same way an explicit value would. The only exception
is the "unknown" value, which represents the absence of value. unknown is used when no default
value is given (the default default value). Note that the default value is a real entity that is
shared by all instances and not an expression that would be evaluated for each instantiation. The
proper management of default values, or their absence through unknown,
is a key feature of CLAIRE.
From a set-oriented perspective, a class is the set union of all the instances of its
descendents (itself, its subclasses, the subclasses of its subclasses, etc.). In some cases,
it may be useful to "freeze" the data representation at some point: for this, two mechanisms are
offered: abstract and final. First, a class c can be declared to have no instances with abstract(c)
such as in the following :
An abstract class is not an empty set, it contains the instances of its descendents. Second, a
class can also be declared to have no more new descendents using final as follows :
It is a good practice to declare final classes that are leaves in the class hierarchy and that
are not meant to receive subclasses in the future. This will enable further optimizations from
the compiler. A class can be declared to instantiate ephemeral objects, in which case its extension
(the list of its instances) is not kept. An important consequence is that ephemeral objects may be
garbage collected when they are no longer used. For this behavior, the class must be declared with
ephemeral or inherit from ephemeral_object :
A class definition can be executed only once, even if it is left unchanged. On the other hand,
CLAIRE supports the notion of a class forward definition. A forward definition contains no slots
and no parentheses. It simply tells the position of the class in the class hierarchy. A forward
definition must be followed by a complete definition (with the same parent class !) before the
class can be instantiated. Attempts to instantiate a class that has been defined only with a
forward definition will produce an error. A forward definition is necessary in the case of
recursive class definitions. Here is a simple example :
parent <: thing child <: thing(father:parent) parent <: thing(son:child) |
Although the father of a child is a parent (in the previous example), creating an instance of
child does not create an implicit instance of parent that would be stored in the father slot.
Once an instance of child is created, it is your responsibility to fill out the relevant slots
of the objects. There exists a way to perform this task automatically, using the close method.
This method is the CLAIRE equivalent to the notion of a constructor(in a C++ or Java sense).
CLAIRE does not support class constructors since its instantiation control structure may be
seen as a generic constructor for all classes. However, there are cases when
additional operations must be performed on a newly created object. To take this into account,
the close method is called automatically when an instantiation is done if a relevant definition
is found. Remember that the close method must always return the newly create object, since the
result of the instantiation is the result of the close method. Here is an example that shows how
to create a parent for each new child object :
| close(x:child) -> (x.father := parent(), x) |
Slots can be mono- or multi-valued. A multi-valued slot contains multiple values that are
represented by a list (ordered) or a set (without duplicates). CLAIRE assumes by default that
a slot with range list or set is multi-valued. However, the multi-valuation is defined at the
property level. This is logical, since the difference between a mono-valued and a multi-valued
slot only occurs when inversion or rules are concerned, which are both defined at the property
level. This means that CLAIRE cannot accept slots for two classes with the
same name and different multi-valuation status. For instance, the following program will cause
an error :
A <: thing(x:set[integer]) // forces CLAIRE to consider x as multi-valued B <: thing(x:stack[integer]) // conflict: x cannot be multi-valued |
On the other hand, it is possible to explicitly tell CLAIRE that a slot with range list or set
is mono-valued, as in the following correct example :
A <: thing(x:set[integer]) x.multivalued? := false // x is from A U B -> (set[integer] U stack[integer]) B <: thing(x:stack[integer]) |
It is sometimes advisable to set up manually the multi-valuation status of the property
before creating the slots, in order to make sure that this status cannot be forced by the
creation of another class with a mono-valued slot with the same name (this could happen within
a many-authors project who share a namespace). This is achieved simply by creating the property
explicitly :
x :: property(multivalued? = true) // creates the property // ... whatever happens will not change x's multi-valuation B <: thing(x:set[integer]) // safe definition of a multi-valued slot |
Free-able objects [XL]
- free! :: property(open = 3) [XL]
- prefree! :: property(open = 3) [XL]
Starting with XL CLAIRE, a class can inherit from freeable_object that are special kind of
ephemeral objects regarding the Garbage collector (GC). We can define two GC callbacks (prefree!
and free!) for such object that will be called when the GC attempt to free an object giving a
chance for such object to perform a cleanup operation :
- prefree! is called by the GC once for each object that will be freed in a short time. It
is the handler of choice to perform synchronization between objects that are going to be
freed (like flushing temporary data in a buffer filter).
- free! is called by the GC once for each object before it is actual freed (like freeing
memory allocated outside CLAIRE memory or closing a descriptor). The body of a free! handler
should make no assumption that the relations between the object and the database are valid.
If we need to use object's relation then the prefree! handler should be used instead.
As an illustration we could define the following long_double class that import from C++ the
'long double' data type :
long_double* <: import() long_double <: freeable_object(value:long_double*) (c_interface(long_double*, "long double*"))
close(self:long_double) : long_double -> (self.value := externC("(long double*)::malloc(sizeof(long double))", long_double*), self)
free!(self:long_double) : void -> externC("::free(self->value)") |
parametric class
A class can be parameterized by a subset of its slots. This means that subsets of the class
that are defined by the value of their parameters can be used as types. This feature is useful
to describe parallel structures that only differ by a few points: parametrization helps describing
the common kernel, provides a unified treatment and avoids redundancy.
A parameterized class is defined by giving the list of slot names into brackets. Parameters can
be inherited slots, and include necessarily inherited parameters.
CLAIRE includes a type system that contains parametric class selections. For instance,
the set of real numbers can be defined as a subset of complex with the additional constraint
that the imaginary part is 0.0. This is expressed in CLAIRE as follows :
| complex[re:float, im:{0.0}] |
In the previous example with stacks, parametric sub-types can be used to designate typed stacks.
We can either specify the precise range of the stack (i.e., the value of the of parameter) or say
that the range must be a sub-type of another type. For instance, the set of stacks with range
integer and the set of stacks which contain integers are respectively :
Calls and Slot Access
- apply(p:property, l:list) -> any
- apply(m:method, l:list) -> any
- apply(self:lambda, %l:list) -> any
- apply(self:function, ls:list, l:list) -> any
- call(p:property, l:listargs) -> any
- funcall(l:lambda, x:any) -> void
- funcall(m:method, x:any) -> void
- funcall(l:lambda, x:any, y:any) -> void
- funcall(m:method, x:any, y:any) -> void
- funcall(f:function, s1:class, x:any, s:class) -> void
- funcall(l:lambda, x:any, y:any, z:any) -> void
- funcall(m:method, x:any, y:any, z:any) -> void
- funcall(f:function, s1:class, x:any, s2:class, y:any, s:class) -> void
- funcall(f:function, s1:class, x:any, s2:class, y:any, s3:class, z:class, s:class) -> void
Calls are the basic building blocks of a CLAIRE program. A call is a polymorphic function
call (a message) with the usual syntax : a selector followed by a list of arguments between
parentheses. A call is used to invoke a method.
When the selector is an operation, such as +, -, %, etc... (% denotes set membership) an infix
syntax is allowed (with explicit precedence rules) :
| eval(x), f(x,y,z), x.price, y.name |
If a slot is read before being defined (its value being unknown), an error is raised. This only
occurs if the default value is unknown. To read a slot that may not be defined, one must use the
get(r:property,x:object) method :
John.father // may provoke an error if John.father is unknown get(father,john) // may return unknown |
When the selector is an operation, such as +,-,%,etc... (% denotes set membership) an infix
syntax is allowed (with explicit precedence rules). Hence the following expressions are valid :
Note that new operations may be defined. This syntax extends to boolean operations
(and:& and or:|). However, the evaluation follows the usual semantic for boolean expression
(e.g., (x & y) does not evaluate y if x evaluates to false) :
| (x = 1) & ((y = 2) | (y > 2)) & (z = 3) |
The values that are combined with and/or do not need to be boolean values (although boolean
expressions always return the boolean values true or false). Following a philosophy borrowed from
LISP, all values are assimilated to true, except for false, empty lists and empty sets. The special
treatment for the empty lists and the empty sets yields a simpler
programming style when dealing with lists or sets. Notice that in CLAIRE 3.0, contrary to previous
releases, there are many empty lists since empty lists can be typed (list<integer>(),
list<string>(), ... are all different). A dynamic functional call where the selector is evaluated can be obtained using the call method.
For instance, call(+,1,2) is equivalent to +(1,2) and call(show,x) is equivalent to show(x). The
difference is that the first parameter to call can be any expression. This is the key for writing
parametric methods using the inline capabilities of CLAIRE. This also means that
using call is not a safe way to force dynamic binding, this should be done using the property
abstract. An abstract property is a property that can be re-defined at any time and, therefore,
relies on dynamic binding. Notice that call takes a variable number of arguments. A similar method
named apply can be used to apply a property to an explicit list of arguments.
Since the use of call is somehow tedious, CLAIRE supports the use of variables (local or global)
as selectors in a function call and re-introduce the call implicitly. For instance :
is equivalent to :
Updates
Assigning a value to a variable is always done with the operator :=. This applies to local
variables but also to the slots of an object. The value returned by the assignment is always
the value that was assigned :
| x.age := 10, John.father := mary |
When the assignment depends on the former value of the variable, an implicit syntax ":op"
can be used to combine the previous value with a new one using the operation op. This can be
done with any (built-in or user-defined) operation (an operation is a function with arity 2
that has been explicitly declared as an operation) :
| x.age :+ 1, John.friends :add mary, x.price :min 100 |
Note that the use of :op is pure syntactical sugar: x.A :op y is equivalent to x.A := (x.A op y).
The receiving expression should not, therefore, contain side-effects as in the dangerous following
example :
Lists, Sets and Instructions
Lists, Sets and Tuples
- /+(l1:list, l2:list) -> list
- /+(x:bag, y:bag) -> list
- <<(l:list, n:integer) -> list
- ^(l:list, y:integer) -> list
- ^(s1:set, s2:set) -> set
- add(s:set, x:any) -> set
- add(l:list, x:any) -> list
- add*(l1:list, l2:list) -> list
- car(self:list) -> any
- cast!(s:bag, t:type) -> bag
- cdr(l:list) -> type[l]
- cons(x:any, l:list) -> list
- copy(s:bag) -> bag
- delete(s:bag, x:any) -> bag
- difference(self:set, x:set) -> set
- get(l:list, x:any) -> integer
- hash(l:list, x:any) -> integer
- last(self:list) -> type[member(self)]
- length(self:bag) -> integer
- list!(s:set) -> type[list[member(s)]]
- make_list(n:integer, x:any) -> type[list[list<any>(x),list({})]]
- make_list(t:type, n:integer) -> list [XL]
- max(f:method, self:bag) -> type[member(self)]
- min(f:method, self:bag) -> type[member(self)]
- nil :: Id(nil)
- nth(l:bag, i:integer) -> any
- nth :: property(open = 3)
- nth+(l:list, i:integer, x:any) -> bag
- nth-(l:list, i:integer) -> bag
- nth= :: property(open = 3)
- nth=(self:list, x:integer, y:any) -> any
- rmlast(self:list) -> list
- shrink(l:list, n:integer) -> list
- sort(f:method, self:list) -> list
- tuple!(x:list) -> tuple
CLAIRE provides two easy means of manipulating collections of objects: sets and lists.
Lists are ordered, possibly heterogeneous, collections. To create a list, one must use
the list(...) instruction : it admits any number of arguments and returns the list of its
arguments. Each argument to the list(...) constructor is evaluated.
| list(a, b, c, d) list(1, 2 + 3), list() |
Sets are collections without order and without duplicates. Sets are created similarly
with the set(...) constructor :
The major novelty in CLAIRE 3.2 is the fact that lists or sets may be typed. This means
that each bag (set or list) may have a type slot named of, which contains a type to which
all members of the list must belong. This type is optional, as is illustrated by the previous
examples, where no typing was given for the lists or sets. To designate a type for a new list
or a new set, we use a slightly different syntax :
list<thing>(a,b,c,d) list<integer>(1,2 + 3) list<float>() set<thing>(a,b,c) set<integer>(1, 2 + 3) |
Typing a list or a set is a way to ensure that adding new values to them will not violate
typing assumptions, which could happen in earlier versions of CLAIRE. Insertion is now always
a destructive operation (add(l,x) returns the list l, that has been augmented with the value x
at its end). Since typing is mandatory in order to assume type-safe updates onto a list or a set, if no
type is provided, CLAIRE will forbid any future update: the list or the set is then a "read-only"
structure. This is the major novelty in CLAIRE 3.2: there is a difference between:
| list(a,b,c,d) set(1,2 + 3) list{i | i in (1 .. 2)} |
which are read-only structures, and :
list<thing>(a, b) set<integer>(1, 2 + 3) list<integer>{i | i in (1 .. 2)} |
which are structures that can be updated. List or set types can be arbitrarily complex, to represent complex list types such as
list of lists of integers. However, it is recommended to use a global constant
to represent a complex type that is used as a list type, as follows :
Constant sets are valid CLAIRE types and can be built using the following syntax :
The expressions inside a constant set expression are not evaluated and should be primitive
entities, such as integers or strings, named objects or global constants. Constant sets are
constant, which means that inserting a new value is forbidden and will provoke an error. A set can also be formed by selection. The result can either be a set with {x in a | P(x)}, or
a list with list{x in a | P(x)}, when one wants to preserve the order of a and keep the duplicates
if a was a list. Similarly, one may decide to create a typed or an un-typed list or set, by adding
the additional type information between angular brackets. For instance, here are two samples with
and without typing :
{x in class | (thing % x.ancestors)} list{x in (0 .. 14) | x mod 2 = 0} set<class>{x in class | (thing % x.ancestors)} list<integer>{x in (0 .. 14) | x mod 2 = 0} |
When does one need to add typing information to a list or a set ? A type is needed when new
insertions need to be made, for instance when the list or set is meant to be stored in an
object's slot which is itself typed. Also, the imageof a set via a function can be formed. Here again, the result can either be a set
with {f(x)|x in a} or a list with list{f(x) | x in a}, when one wants to preserve the order of a
and the duplicates :
For example, we have the traditional average_salary method :
average_salary(s:set[man]) : float -> (sum(list{m.sal | m in s}) / size(s)) |
Last, two usual constructions are offered in CLAIRE to check a boolean expression universally
(forall) or existentially (exists). A member of a set that satisfies a condition can be extracted
(a non-deterministic choice) using the some construct: some(x in a | f(x)). For instance, we can write :
exists(x in (1 .. 10) | x > 2) // returns true some(x in (1 .. 10) | x > 2) // returns 3 in most implementations exists(x in class | length(x.ancestors) > 10) |
The difference between exists and some is that the first always returns a boolean, whereas the
second returns one of the objects that satisfy the condition (if there exists one) and unknown
otherwise. It is very often used in conjunction with when, as in the following example :
when x := some(x in man | rich?(x)) in (borrow_from(x,1000), ...) else printf("There is no one from whom to borrow!") |
Conversely, the boolean expression forall(x in a | f(x)) returns true if and only if f(x) is
true for all members of the set a. The two following examples returns false (because of 1):
forall(x in (1 .. 10) | x > 2) forall(x in (1 .. n) | exists(y in (1 .. x) | y * y > x)) |
Definition : A list is an ordered collection of objects that is organized into an extensible array,
with an indexed access to its members. A list may contain duplicates, which are multiple
occurrence of the same object. A set is a collection of objects without duplicates and without
any user-defined order. The existence of a system-dependent order is language-dependent and
should not be abused. The concept of bag in CLAIRE is the unifier between lists and sets : a
collection of objects with possible duplicates and without order. |
A read-only (untyped) list can also be thought as tuples of values. For upward compatibility reasons,
the expression tuple(a1,...,an) is equivalent to list(a1,...,an) :
| tuple(1,2,3), tuple(1,2.0,"this is heterogeneous") |
Since it is a read-only list, a tuple cannot be changed once it is created, neither through addition
of a new member (using the method add) or through the exchange of a given member (using the nth=
method). CLAIRE offers an associated data type. For instance, the
following expressions are true :
tuple(1,2,3) % tuple(integer,integer,integer) tuple(1,2,3) % tuple(0 .. 1, 0 .. 10, 0 .. 100) tuple(1,2.0,"this is heterogeneous") % tuple(any,any,any) |
Typed tuples are used to return multiple values from a method. Because a tuple
is a bag, it supports membership, iteration and indexed access operations. However, there is yet
another data structure in CLAIRE for homogeneous arrays of fixed length, called arrays. Arrays are
similar to lists but their size is fixed once they are created and they must be assigned a subtype
(a type for the members of the array) that cannot change. Because of these strong constraints,
CLAIRE can provide an implementation that is more efficient (memory usage and access time) than
the implementation of bags. However, the use of arrays is considered an advanced feature of CLAIRE
since everything that is done with an array may also be done with a list.
Blocks
Parentheses can be used to group a sequence of instructions into one. In this case, the returned
value is the value of the last instruction :
Parentheses can also be used to explicitly build an expression. In the case of boolean evaluation
(for example in an if), any expression is considered as true except false, empty sets and empty
lists :
(1 + 2) * 3 if (x = 2 & l) |
Local variables can be introduced in a block with the let construct. These variables can be typed,
but it is not mandatory (CLAIRE will use type inference to provide with a reasonable type). On
the other hand, unlike languages such as C++, you always must provide an initialization value
when you define a variable. A let instruction contains a sequence of variable definitions and,
following the in keyword, a body (another instruction). The scope of the local variable is exactly
that body and the value of the let instruction is the value returned by this body.
| let x := 1, y := 3 in (z := x + y, y := 0) |
Notice that CLAIRE uses := to represent assignment and = to represent equality.
The compiler will issue a warning if a statement (x = y) is used where an assignment was
probably meant (this is the case when the value of the assignment is not needed, such as
in x := 1, y = 3, z := 4). The value of local variables can be changed with the same syntax as an update to an object:
the syntax :op is allowed for all operations op :
| x := x + 1, x :+ 1, x :/ 2, x :^ 2 |
The name of a local variable can be any identifier, including the name of an existing object
or variable. In that case, the new variable overrides the older definition within the scope
of the let. While this may prove useful in a few cases, it should be used sparingly since it
yields to code that is hard to read. A rule of thumb is to avoid mixing the name of variables
and the name of properties since it often produces errors that are hard to catch (the property
cannot be accessed any more once a variable with the same name is defined). The control
structure when is a special form of let, which only evaluates the body if the value of the
local variable (unique) is not unknown (otherwise, the returned value is unknown). This is
convenient to use slots that are not necessarily defined as in the following example :
when f := get(father,x) in printf("his father is ~S\n", f) |
The default behavior when the value is unknown can be specified using the else keyword.
The statement following the else keyword will be evaluated and its value will be returned
when the value of the local variable is unknown :
when f := get(father,x) in printf("his father is ~S\n", f) else printf("his father is not known at the present time\n") |
Local variables can also be introduced as a pattern, that is a tuple of variables. In that
case, the initial value must be a tuple of the right length. For instance, one could write :
| let (x, y, z) := tuple(1, 2, 3) in x + y + z |
The tuple of variable is simply introduced as a sequence of variables surrounded by two
parentheses. The most common use of this form is to assign the multiple values returned
by a function with range tuple, as we shall see in the next section. If we suppose that f
is a method that returns a tuple with arity 2, then the two following forms are equivalent:
let (x1,x2) := f() in ...
let l := f(), x1 := l[1], x2 := l[2] in ... |
[XL] In XL CLAIRE, as a syntactical shortcut, we can define in a single let statement both
tuple assigment and normal variable assigment as in :
let (x1,x2) := f(), x3 := g() in ... |
Tuples of variables can also be assigned directly within a block as in the following example :
| (x1, x2) := tuple(x2, x1) |
Although this mostly used for assigning the result of tuple-valued functions without any useless
allocation, it is interesting to note that the previous example will be compiled into a nice
value-exchange interaction without any allocation (the compiler is smart enough to determine
that the list "list(x2,x1)" is not used as such). The key principle of lexical variables is that they are local to the "let" in which they are
defined. CLAIRE supports another similar type of block, which is called a temporary slot
assignment. The idea is to change the value of a slot but only locally, within a given expression.
This is done as follows:
changes the value of r(x) to y, executes e and then restore r(x) to its previous value. It is
strictly equivalent to
let old_v := x.r in (x.r := y, let result := e in (x.r := old_v, result)) |
CLAIRE provides automatic type inference for variables that are defined in a let so that explicit
typing is not necessary in most of the cases. Here are a few rules to help you decide if you need
to add an explicit type to your variable or even cast a special type for the value that is
assigned to the variable :
- (a) Type inference will provide a type to a Let variable only if they do not have one already.
- (b) when you provide a type in let x:t := y, the compiler will check that the value y belong
to t and will issue a warning and/or insert a run-time type-check accordingly.
- (c) if you want to force the type that is inferred to something smaller than what CLAIRE thinks
for y, you must use a cast :
| let x := (y as t2) in ... |
To summarize :
- in most cases CLAIRE range inference works, so you write let x := y in ...
- you use let x:t := y to weaken the type inference, mostly because you want to put
something of a different type later
- you use let x := (y as t) to narrow the type inferred by CLAIRE.
Conditionals
if statements have the usual syntax (if <test> x else y) with implicit nestings (else if).
The <test> expression is evaluated and the instruction x is evaluated if the value is
different from false, nil or {}. Otherwise, the instruction y is evaluated,
or the default value false is returned if no else part was provided.
if (x = 1) x := f(x,y) else if (x > 1) x := g(x,y) else (x := 3, f(x,y))
if (let y := 3 in x + y > 4 / x) print(x) |
If statements must be inside a block, which means that if they are not inside a sequence
surrounded by parenthesis they must be themselves surrounded by parenthesis
(thus forming a block). case is a set-based switch instruction: CLAIRE tests the branching sets one after another,
executes the instruction associated with the first set that contains the object and exits the
case instruction without any further testing. Hence, the default branch is associated with the
set any. As for an if, the returned value is nil if no branch of the case is relevant :
case x ({1} x + 1, {2,3} x + 2, any x + 3) case x (integer (x := 3, print(x)), any error("~I is no good\n",x)) |
Note that the compiler will not accept a modification of the variable that is not consistent
with the branch of the case (such as case x ({1} x := 2)). The expression on which the switching
is performed is usually a variable, but can be any expression. However, it should not produce any
side effect since it will be evaluated many times. Starting with CLAIRE 3.3, only boolean expressions should be used in the <test> expression of a
conditional statement. The implicit coercion of any expression into a Boolean is still supported,
but should not be used any longer. The compiler will issue a warning if a non-boolean expression
is used in an If.
Loops
CLAIRE supports two types of loops: iteration and conditional loops (while and until).
Iteration is uniquely performed with the for statement, it can be performed on any collection :
for x in (1 .. 3) a[x] := a[x + 3] for x in list{x in class | size(x.ancestors) >= 4} printf("~S \n", x) |
A collection here is taken in a very general sense, i.e., an object that can be seen as a set
through the enumeration method set!. This includes all CLAIRE types but is not restricted since
this method can be defined on new user classes that inherit from the collection root. For instance,
set!(n:integer) returns the subset of (0 .. 29) that is represented by the integer n taken as
a bit-vector. To tell CLAIRE that her new class is a collection, the user must define it as a
subclass of collection. If x is a collection, then :
are supported. When defining a new subclass of collection, the methods set! and % must be
defined for this new class, and it is also advisable to define size and iterate to get compiler
speed-ups (if size is not defined, an implicit call to set! is made). Other collection handling
methods, such as add, delete, etc may be defined freely if needed. Notice that it is possible that the expression being evaluated inside the loop modifies the
set itself, such as in :
| for x in {y in S | P(y)} P(x) := false |
Because the CLAIRE compiler tries to optimize iteration using lazy evaluation, there is no
guarantee about the result of the previous statement. In this case, it is necessary to use an
explicit copy as follows :
| for x in copy({y in S | P(y)}) P(x) := false |
The iteration control structure plays a major role in CLAIRE. It is possible to optimize its
behavior by telling CLAIRE how to iterate a new subclass (C) of collection. This is done through
adding a new restriction of the property iterate for this class C, which tells how to apply a given
expression to all members of an instance of C. This may avoid the explicit construction of the
equivalent set which is performed through the set! method. Conditional loops are also standard (the exiting condition is executed before each loop in a while
and after each loop in a until),
while (x > 0) x :+ 1 until (x = 12) x :+ 1 while not(i = size(l)) (l[i] := 1, i :+ 1) |
The value of a loop is false. However, loops can be exited with the break(x) instruction, in which
case the return value is the value of x :
There is one restriction with the use of break: it cannot be used to escape from a try ... catch
block. This situation will provoke an error at compile-time.
Instantiation
Instantiation is the mechanism of creating a new object of a given class;
instantiation is done by using the class as a selector and by giving a list of
"<slot> = <value>" pairs as arguments :
complex(re = 0.0, im = 1.0) person(age = 0, father = john) |
Recall that the list of instances of a given class is only kept for non-ephemeral classes
(a class is ephemeral if has been declared as such or if it inherits from the
ephemeral_object class). The creation of a new instance of a class yields to a function
call to the method close. Objects with a name are represented by the class thing, hence
descendents of thing (classes that inherit from thing) can be given a name with the
definition operation ::. These named objects can later be accessed with their name,
while objects with no name offer no handle to manipulate them after their creation outside
of their block (objects with no name are usually attached to a local variable with a let
whenever any other operation other than the creation itself is needed) :
| paul :: person(age = 10, father = peter) |
Notice that the identifier used as the name of an object is a constant that cannot be
changed. Thus, it is different from creating a global variable that would contain an
object as in :
| aGoodGuy:person :: person(age = 10, father = peter) |
Additionally, there is a simpler way of instantiating parameterized classes by dropping
the slot names. All values of the parameter slots must be provided in the exact order that
was used to declare the list of parameters. For instance, we could use :
The previously mentioned instantiation form only applies to a parameterized class. It is
possible to instantiate a class that is given as a parameter (say, the variable v) using
the new method. New(v) creates an instance of the class v and new(v,s) creates a named
instance of the class v (assumed to be a subclass of thing) with the name s.
Exception Handling
Exceptions are a useful feature of software development: they are used to describe an exceptional
or wrong behavior of a block. Exception can be raised, to signal this behavior and are caught by
exception handlers that surround the code where the exceptional behavior happened. Exceptions are
CLAIRE objects (a descendent from the class exception) and can contain information in slots.
The class exception is an "ephemeral" class, so the list of instances is not kept. In fact,
raising an exception e is achieved by creating an instance of the class e. Then, the method
close is called: the normal flow of execution is aborted and the control is passed to the
previously set dynamic handler. A handler is created with the following instruction :
| try <expression> catch <class> <expression> |
For instance we could write :
try 1 / x catch any (printf("1/~A does not exists", x), 0) |
A handler "try e catch c f", associated with a class c, will catch all exceptions that may occur
during the evaluation of e as long as they belong to c. Otherwise the exception will be passed
to the previous dynamic handler (and so on). When a handler "catches" an exception, it evaluates
the "f" part and its value is returned. The last exception that was raised can be accessed
directly with the exception!() method. Also, as noticed previously, the body of a handler
cannot contain a break statement that refers to a loop defined outside the handler. The most common exceptions are errors and there is a standard way to create an error in CLAIRE
using the error(s:string, l:listargs) instruction. This instruction creates an error object
that will be printed using the string s and the arguments in l, as in a printf statement.
Here are a few examples :
error("stop here") error("the value of price(~S) is ~S !", x, price(x)) |
Another very useful type of exception is contradiction. CLAIRE provides a class contradiction
and a method contradiction!() for creating new contradictions. This is very commonly used for
hypothetical reasoning with forms like :
try (choice(), // create a new world ...) // performs an update that may cause a contradiction catch contradiction (backtrack(), // return to previous world ...) |
In fact, this is such a common pattern that CLAIRE provides a special instruction, branch(x),
which evaluates an expression inside a temporary world and returns a boolean value, while
detecting possible contradiction. The statement branch(x) is equivalent to :
If we want to find a value for the slot x.r among a set x.possible that does not cause a
contradiction (through rule propagation) we can simply write :
when y := some(y in x.possible | branch(x.r = y)) in x.r := y else contradiction!() |
array
- array!(x:bag, t:type) -> type[t[]]
- copy(a:array) -> array
- get(a:array, x:any) -> integer
- length(a:array) -> integer
- list!(a:array) -> type[list[member(a)]]
- make_array(i:integer, t:type, v:any) -> type[(if unique?(t) the(t)[] else array)]
- member_type(x:array) -> type
- nth(a:array, i:integer) => any
- nth=(self:array, x:integer, y:any) -> void
An array can be seen as a fixed-size list, with a member type (the slot name is of),
which tells the type of all the members of the array. Because of the fixed size, the
compiler is able to generate faster code than when using lists, so lists should be used
when the collection shrinks and grows, and an array may be used otherwise. This is especially
true for arrays of floats, which are handled in a special (and efficient) way by the compiler.
Arrays are simpler than lists, and only a few operations are supported. Therefore, more complex
operations such as append often require a cast to list (list!). An array is created explicitly
with the make_array property :
Operations on arrays include copying, casting a bag into an array (array!), defeasible update
on arrays using store, and returning the length of the array with length. An array can also be
made from a list using array!, which is necessary to create arrays that contain complex objects
(such as arrays of arrays).
Methods and Types
Methods
A method is the definition of a property for a given signature. A method is defined by the
following pattern : a selector (the name of the property represented by the method), a list
of typed parameters (the list of their types forms the domain of the method), a range expression
and a body (an expression or a let statement introduced by -> or =>).
Definition : A signature is a Cartesian product of types that always contains the extension of the function.
More precisely, a signature A1* A2* ... * An, also represented as list(A1, ...,An) or
A1* A2* ... * An-1 -> An, is associated to a method definition f(...) : An -> ... for two
purposes: it says that the definition of the property f is only valid for input arguments
(x1, x2, ..., xn-1) in A1* A2* ... * An-1 and it says that the result of f(x1, x2, ..., xn-1)
must belong to An. The property f is also called an overloaded function and a method m is called its restriction
to A1* A2* ... * An-1.
|
If two methods have intersecting signatures and the property is called on objects in both signatures,
the definition of the method with the smaller domain is taken into account. If the two domains have a
non-empty intersection but are not comparable, a warning is issued and the result is
implementation-dependent. The set of methods that apply for a given class or return results in
another can be found conveniently with methods.
The range declaration can only be omitted if the range is void. In particular, this is convenient
when using the interpreter :
loadMM() -> (begin(my_module), load("f1"), load("f2"), end(my_module)) |
If the range is void (unspecified), the result cannot be used inside another expression
(a type-checking error will be detected at compilation). A method's range must be declared
void if it does not return a value (for instance, if its last statement is, recursively, a call
to another method with range void). It is important not to mix restrictions with void range with
other regular methods that do return a value, since the compiler will generate an error when
compiling a call unless it can guarantee that the void methods will not be used. The default range was changed to void in the version 3.3 of CLAIRE, in an effort to encourage
proper typing of methods: "no range" means that the method does not return a value. This is an
important change when migrating code from earlier versions of CLAIRE.
CLAIRE supports methods with a variable number of arguments using the listargs keyword. The
arguments are put in a list, which is passed to the (unique) argument of type listargs. For
instance, if we define :
| [f(x:integer,y:listargs) -> x + size(y)] |
A call f(1,2,3,4) will produce the binding x = 1 and y = list(2,3,4) and will return 4. CLAIRE also supports functions that return multiple values using tuples. If you need a function
that returns n values v1,v2,...,vn of respective types t1,t2,...,tn, you simply declare its range
as tuple(t1,t2,...,tn) and return tuple(v1,v2,...,vn) in the body of the function. For instance the
following method returns the maximum value of a list and the "regret" which is the difference
between the best and the second-best value :
[my_max(l:list[integer]) : tuple(integer,integer) -> let x1 := 1000000000, x2 := 1000000000 in (for y in l (if (y < x1) (x2 := x1, x1 := y) else if (y < x2) x2 := y), tuple(x1,x2))] |
The tuple produced by a tuple-valued method can be used in any way, but the preferred way is to
use a tuple-assignment in a let. For instance, here is how we would use the max2 method :
let (a,b) := my_max(list{f(i) | i in (1 .. 10)}) in ... |
Each time you use a tuple-assignment for a tuple-method, the compiler uses an optimization
technique to use the tuple virtually without any allocation. This makes using tuple-valued
methods a safe and elegant programming technique. The body of a method is either a CLAIRE expression (the most common case) or an external (C++)
function. In the first case, the method can be seen as defined by a lambda abstraction. This
lambda can be created directly through the following :
| lambda[(<typed parameters>), <body>] |
Defining a method with an external function is the standard way to import a C/C++ function
in CLAIRE. This is done with the function!(...) constructor, as in the following :
It is important to notice that in CLAIRE, methods can have at most 20 parameters. Methods with
40 or more parameters that exist in some C++ libraries are very hard to maintain. It is advised
to use parameter objects in this situation. CLAIRE also provides inline methods, that are defined using the => keyword before the body
instead of ->. An inline method behaves exactly like a regular method. The only difference is
that the compiler will use in-line substitution in its generated code instead of a function call
when it seems more appropriate. Inline methods can be seen as polymorphic macros, and are
quite powerful because of the combination of parametric function calls (using call(...))
and parametric iteration (using for). Let us consider the two following examples, where
subtype[integer] is the type of everything that represents a set of integers :
sum(s:subtype[integer]) : integer => let x := 0 in (for y in s x :+ y, x)
min(s:subtype[integer], f:property) : integer => let x := 0, empty := true in (for y in s (if empty (x := y, empty := false) else if call(f,y,x) x := y), x) |
For each call to these methods, the compiler performs the substitution and optimizes the result.
For instance, the optimized code generated for sum({x.age | x in person}) and for
min({x in 1 .. 10 | f(x) > 0}, >) will be :
let x := 0 in (for %v in person.instances let y := %v.age in x :+ y, x)
let x := 0, empty := true, y := 1, max := 10 in (while (y <= max) (if (f(y) > 0) (if empty (x := y, empty := false) else if (y > x) x := y), y :+ 1), x) |
Notice that, in these two cases, the construction of temporary sets is totally avoided. The
combined use of inline methods and functional parameters provides an easy way to produce generic
algorithms that can be instantiated as follows :
The code generated for the definition of mymin @ list[integer] will use a direct call to
my_order (with static binding) and the efficient iteration pattern for lists, because min is
an inline method. In that case, the previous definition of min may be seen as a pattern of
algorithms.
- CAVEAT : A recursive macro will cause an endless loop that may be
painful to detect and debug.
For upward compatibility reasons (from release 1.0), CLAIRE still supports the use of external
brackets around method definitions. The brackets are there to represent boxes around methods
(and are pretty-printed as such with advanced printing tools). For instance, one can write :
Brackets have been found useful by some users because one can search for the definition of the
method m by looking for occurrences of '[mmm'. They also transform a method definition into a
closed syntactical unit that may be easier to manipulate (e.g., cut-and-paste). When a new property is created, it is most often implicitly with the definition of a new method
or a new slot, although a direct instantiation is possible. Each property has an extensibility
status that may be one of :
- open, which means that new restrictions may be added at any time. The compiler will
generate the proper code so that extensibility is guaranteed.
- undefined, which is the default status under the interpreter, means that the status may
evolve to open or to closed in the future.
- closed, which means that no new restriction may be added if it provokes an
inheritance conflict with an existing restriction. An inheritance conflict in CLAIRE
is properly defined by the non-empty intersection of the two domains (Cartesian products)
of the methods.
The compiler will automatically change the status from undefined to closed, unless the status
is forced with the abstract declaration :
Conversely, the final declaration :
may be used to force the status to closed, in the interpreted mode. Note that these two
declarations have obviously an impact on performance: an open property will be compiled with the
systematic used of dynamic calls, which ensures the extensibility of the compiled code, but at a
price. On the contrary, a final property will enable the compiler to use as much static binding as
possible, yielding faster call executions. Notice that the interface(p) declaration has been
introduced to support dynamic dispatch in a efficient manner, as long as the property is uniform.
Types
- %(x:any, y:any) -> boolean
- ..(x:integer, y:integer) -> Interval
- <=(x:type, y:type) -> boolean
- =type?(self:type, ens:type) -> boolean
- final(c:class) -> void
- finite?(self:type) -> boolean
- inherit?(self:class, ens:class) -> boolean
- member(x:type) -> type
CLAIRE uses an extended type system that is built on top of the set of classes.
Like a class, a type denotes a set of objects, but it can be much more precise than a class.
Since methods are attached to types (by their signature), this allows attaching methods to
complex sets of objects.
Definition : A (data) type is an expression that represents a set of objects.
Types offer a finer-granularity partition of the object world than classes. They are used
to describe objects (range of slots), variables and methods (through their signatures). An
object that belongs to a type will always belong to the set represented by the type.
|
Any class (even parameterized) is a type. A parameterized class type is obtained by
filtering a subset of the class parameters with other types to which the parameters must
belong. For instance, we saw previously that complex[im:{0.0}] is a parametrized type that
represent the real number subset of the complex number class. This also applies to typed lists
or sets which use the of parameter. For instance, list[of:{integer}] is the set of list whose
of parameter is precisely integer. Since these are common patterns, CLAIRE offers two shortcuts
for parameterized type expressions. First, it accepts the expression C[p = v] as a shortcut for
C[p:{v}]. Second, it accepts the expression C<X> as a shortcut for C[of = X]. This applies to any
class with a type-valued parameter named of;
Thus, stack<integer> is the set of stacks whose parameter "of" is exactly integer, whereas
stack[of:subtype[integer]] is the set of stacks whose parameter (a type) is a subset of integer.
Finite constant sets of objects can also be used as types. For example, {john, jack, mary} and
{1,4,9} are types. Intervals can be used as types; the only kind of intervals supported by
CLAIRE 3.0 is integer intervals. Types may also formed using the two intersection (^) and union (U)
operations. For example, integer U float denotes the set of numbers and (1 .. 100) ^ (-2 .. 5)
denotes the intersection of both integer intervals, i.e. (1 .. 5).
Subtypes are also as type expressions. First, because types are also objects, CLAIRE introduces
subtype[t] to represent the set of all type expressions that are included in t. This type can be
intersected with any other type, but there are two cases which are more useful than other, namely
subtypes of the list and set classes. Thus, CLAIRE uses set[t] as a shortcut for set ^ subtype[t]
and list[t] as a shortcut for list ^ subtype[t]. Because of the semantics of lists, one may see
that list[t] is the union of two kinds of lists :
- "read-only" lists (i.e., without type) that contains objects of type t.
- typed list from list<X>, where X is a subtype of t.
Therefore, there is a clear difference between
- list<t>, which only contains types lists, whose type parameter (of) must be exactly t.
- list[t], which contains both typed lists and un-typed lists.
Obviously, we have list<t> <= list[t]. When should you use one or the other form of
typed lists or sets ?
- use list[t] to type lists that will only be used by accessing their content. A
method that uses l:list[t] in its signature will be polymorphic, but updates on l will
rely on dynamic (run-time) typing.
- use list<t> to type lists that need to be updated. A method that uses l:list<t> in
its signature will be monomorphic (i.e., will not work for l:list<t'> with t' <= t), but
updates will be statically type-checked (at compile time).
Last, CLAIRE uses tuple and array types. The array type t[] represents arrays whose member
type is t (i.e., all members of the array belong to t). Tuples are used to represent type of
tuples in a very simple manner: tuple(t1,t2,...,tn) represents the set of tuples
tuple(v1,v2, ... ,vn) such that vi belong to ti for all i in (1 .. n). For instance, tuple(integer, char)
denotes the set of pair tuples with an integer as first element and a character as second. Also
you will notice that tuple(class,any,type) belongs to itself, since class is a class and type is
a type. Classes are sorted with the inheritance order. This order can be extended to types with the same
intuitive meaning that a type t1 is a subtype of a type t2 if the set represented by t1 is a
subset of that represented by t2. The relation "t1 is a subtype of a type t2" is noted t1 <= t2.
This order supports the introduction of the " subtype " constructor: subtype[t] is the type of all
types that are less than t.
Polymorphism
In addition to the traditional "objet-oriented" polymorphism, CLAIRE also offers two forms of
parametric polymorphism, which can be skipped by a novice reader.
(1) There often exists a relation between the types of the arguments of a method.
Capturing such a relation is made possible in CLAIRE through the notion of an
"extended signature". For instance, if we want to define the operation "push" on a
stack, we would like to check that the argument y that is being pushed on the stack s belongs
to the type of(s), that we know to be a parameter of s. The value of this parameter can be
introduced as a variable and reused for the typing of the remaining variables (or the range)
as follows :
| push(s:stack<X>, y:X) -> (s.content :add y, s.index :+ 1) |
The declaration s:stack<X> introduced X as a type variable with value s.of,
since stack[of] was defined as a parameterized class. Using X in y:X simply means that
y must belong to the type s.of. Such intermediate type variables must be free identifiers
(the symbol is not used as the name of an object) and must be introduced with the following
template :
The use of type variables in the signature can be compared to pattern matching.
The first step is to bind the type variable. If (p = V) is used in c[ ...] instead of p:t,
it means that we do not put any restriction on the parameter p but that we want to bind its
value to V for further use. Note that this is only interesting if the value of the parameter
is a type itself. Once a type variable V is defined, it can be used to form a pattern
(called a <type with var> in the CLAIRE syntax) as follows:
(2) The second advanced typing feature of CLAIRE is designed to capture the fine
relationship between the type of the output result and the types of the input arguments.
When such a relationship can be described with a CLAIRE expression e(x1,...,xn),
where x1, ..., xn are the types of the input parameters, CLAIRE allows to substitute
type[e] to the range declaration. It means that the result of the evaluation of the method
should belong to e(t1,...,tn) for any types t1,...,tn that contain the input parameters. For instance, the identity function is known to return a result of the same type as its
input argument (by definition !). Therefore, it can be described in CLAIRE as follows :
In the expression that we introduce with the type[e] construct, we can use the types of
the input variables directly through the variables themselves. For instance, in the
"type[x]" definition of the id example, the "x" refers to the type of the input variable.
Notice that the types of the input variables are not uniquely defined. Therefore, the user
must ensure that her "prediction" e of the output type is valid for any valid types t1, ..., tn
of the input arguments. The expression e may use the extra type variables that were introduced earlier.
For instance, we could define the "top" method for stacks as follows :
| top(s:stack<X>) : type[X] -> s.content[s.index] |
The "second-order type" e (second-order means that we type the method, which is a
function on objects, with another function on types) is built using the basic CLAIRE
operators on types such as U, ^ and some useful operations such as "member". If c is a type,
member(c) is the minimal type that contains all possible members of c. For instance,
member({c}) = c by definition. This is useful to describe the range of the enumeration
method set!. This method returns a set, whose members belong to the input class c by definition.
Thus, we know that they must belong to the type member(X) for any type X to whom c belongs
(cf. definition of member). This translates into the following CLAIRE definition :
For instance, if c belongs to subtype[B] then set!(c) belongs to set[B]. To summarize, here is a more precise description of the syntax for defining a method :
| <function>(<vi>:<ti>i E (1 .. n)) : <range> -> <exp> |
Each type ti for the variable vi is an "extended type" that may use type variables
introduced by the previous extended types t1, t2 ... ti-1 . An extended type is
defined as follows :
The <range> expression is either a regular type or a "second order type",
which is a CLAIRE expression e introduced with the type[e] syntactical construct :
Escaping Types
There are two ways to escape type checking in CLAIRE. The first one is casting, which means
giving an explicit type to an expression. The syntax is quite explicit :
| <cast> = (<expression> as <type>) |
This will tell the compiler that <expression> should be considered as having type <type>.
Casting is ignored by the interpreter and should only be used as a compiler optimization.
There is, however, one convenient exception to this rule, which is the casting into a list
parametric type. When an untyped list is casted into a typed list, the value of its of
parameter is actually modified by the interpreter, once the correct typing of all members
has been verified. For instance, the two following expressions are equivalent :
The second type escaping mechanism is the non-polymorphic method call, where we tell what
method we want to use by forcing the type of the first argument. This is equivalent to the
supermessage passing facilities of many object-oriented languages.
The instruction f@c(...) will force CLAIRE to use the method that it would use for
f(...) if the first argument was of type c (CLAIRE only checks that this first argument
actually belongs to c). A language is type-safe if the compiler can use type inference to check all type constraints
(ranges) at compile-time and ensure that there will be no type checking errors at run-time.
CLAIRE is not type-safe because it admits expressions for which type inference is not possible
such as read(p) + read(p). On the other hand, most expressions in CLAIRE may be statically
type-checked and the CLAIRE compiler uses this property to generate code that is very similar
to what would be produced with a C++ compiler. A major difference between CLAIRE 3.0 and earlier
versions is the fact that lists may be explicitly typed, which removes the problems that could
happen earlier with dynamic types. Lists and sets subtypes support inclusion polymorphism, which
means that if A is a subtype of B, list[A] is a subtype of list[B];
for instance list[(0 .. 1)] <= list[integer]. Thus only read operations can be statically
type-checked w.r.t. such type information. On the other hand, array subtypes, as well as
list or set parametric subtypes, are monomorphic, since A[] is not the set of arrays which
contain members of A, but the set of arrays whose member type (the of slot) contains the
value A. Thus if A is different from B, A[] is not comparable with B[], and list<A> is not
comparable with list<B>. This enables the static type-checking of read and write operations on
lists. The fact that CLAIRE supports all styles of type disciplines is granted by the combination
of a rich dynamic type system coupled with a powerful type inference mechanism within the
compiler, and is a key feature of CLAIRE.
Selectors, Properties and Operations
As we said previously, CLAIRE supports two syntaxes for using selectors, f(...) and
(.... f ....). The choice only exists when the associated methods have exactly two arguments.
The ability to be used with an infix syntax is attached to the property f :
Once f has been declared as an operation, CLAIRE will check that it is used as such
subsequently. Restrictions of f can then be defined with the usual syntax :
Note that declaring f as an operation can only be done when no restriction of f is known.
If the first appearance of f is in the declaration of a method, f is considered as a normal
selector and its status cannot be changed thereafter. Each operation is an object (inherits
from property) with a precedence slot that is used by the reader to produce the proper syntax
tree from expressions without parentheses.
gcd :: operation(precedence = precedence(/)) 12 + 3 gcd 4 // same as 12 + (3 gcd 4) |
So far we have assumed that any method definition is allowed, provided that inheritance
conflict may cause warning. Once a property is compiled, CLAIRE uses a more restrictive
approach since only new methods that have an empty intersection with existing methods (for
a given property) are allowed. This allows the compiler to generate efficient code. It is
possible to keep the "open" status of a property when it is compiled through the
abstract declaration.
Such a statement will force CLAIRE to consider f as an "abstract" parameter of the program
that can be changed at any time. In that case, any re-definition of f (any new method) will
be allowed. When defining a property parameter, one should keep in mind that another user
may redefine the behavior of the property freely in the future. It is sometimes useful to model a system with redundant information. This can be done by
considering pairs of relations inverse one of another. In this case the system maintains the
soundness of the database by propagating updates on one of the relations onto the other. For
example if husband is a relation from the class man onto the class woman and wife a relation
from woman to man, if moreover husband and wife have been declared inverse one of another,
each modification (addition or retrieval of information) on the relation husband will be
propagated onto wife. For example husband(mary) := john will automatically generate the update
wife(john) := mary. Syntactically, relations are declared inverses one of another with
the declaration :
This can be done for any relation: slots and tables. Inverses introduce an important distinction
between multi-valued relations and mono-valued relations. A relation is multi-valued in CLAIRE
when its range is a subset of bag (i.e. a set or a list). In that case the slot multivalued? of
the relation is set to true and the set associated with an object x is supposed to be the set
of values associated with x through the relation. This has the following impact on inversion. If r and s are two mono-valued relations inverse one
of another, we have the following equivalence :
In addition, the range of r needs to be included in the domain of s and conversely. The
meaning of inversion is different if r is multi-valued since the inverse declaration now means :
Two multi-valued relations can indeed be declared inverses one of another. For example, if
parents and children are two relations from person to set[person] and if inverse(children) =
parents, then :
| children(x) = {y in person | x % parents(y)} |
Modifications to the inverse relation are triggered by updates (with :=) and creations of
objects (with filled slots). Since the explicit inverse of a relation is activated only upon
modifications to the database (it is not retroactive), one should always set the declaration
of an inverse as soon as the relation itself is declared, before the relation is applied on
objects. This will ensure the soundness of the database. To escape the triggering of updates
to inverse relations, the solution is to fill the relation with the method put instead of :=.
For example, the following declaration :
| let john := person() in (put(wife,john,mary), john) |
does the same as :
| john :: person(wife = mary) |
without triggering the update husband(mary) := john.
Iterations
We just saw that CLAIRE will produce in-line substitution for some methods. This is
especially powerful when combined with parametric function calls (using call(...))
taking advantage of the fact that CLAIRE is a functional language. There is another
form of code substitution supported by CLAIRE that is also extremely useful, namely
the iteration of set data structure.
Any object s that understands the set! method can be iterated over. That means that
the construction for x in s e(x) can be used. The actual iteration over the set
represented by s is done by constructing explicitly the set extension. However,
there often exists a way to iterate the set structure without constructing the set
extension. The simplest example is the integer interval structure that is iterated
with a while loop and a counter.
It is possible to define iteration in CLAIRE through code substitution. This is done
by defining a new inline restriction of the property iterate, with signature
(x:X,v:Variable,e:any). The principle is that CLAIRE will replace any occurrence of
(for v in x e) by the body of the inline method as soon as the type of the expression
x matches with X (v is assumed to be a free variable in the expression e). For instance,
here is the definition of iterate over integer intervals :
iterate(x:interval[min:integer,max:integer],v:Variable,e:any) => let v := min(x), %max := max(x) in (while (v <= %max) (e, v :+ 1)) |
Here is a more interesting example. We can define hash tables as follows. A table is
defined with a list (of size 2n - 3, which is the largest size for which a chunk of size
2n is allocated), which is full of "unknown" except for these objects that belong to the
set. Each object is inserted at the next available place in the table, starting at a point
given by the hashing function (a generic hashing function provided by CLAIRE: hash) :
htable <: object(count:integer = 0, index:integer = 4, arg:list<any> = list<any>())
set!(x:htable) : set -> {y in x.arg | known?(y)}
insert(x:htable, y:any) -> let l := x.arg in (if (x.count >= length(l) / 2) (x.arg := make_list(^2(x.index - 3), unknown), x.index :+ 1, x.count := 0, for z in {y in l | known?(y)} insert(x,z), insert(x,y)) else let i := hash(l,y) in (until (l[i] = unknown | l[i] = y) (if (i = length(l)) i := 1 else i :+ 1), if (l[i] = unknown) (x.count :+ 1, l[i] := y))) |
Note that CLAIRE provides a few other functions for hashing that would allow an even
simpler, though less self-contained, solution. To iterate over such hash tables without
computing set!(x) we define :
iterate(s:htable, v:Variable, e:any) => (for v in s.arg (if known?(v) e)) |
Thus, CLAIRE will replace :
| let s:htable := ... in sum(s) |
by :
let s:htable := ... in (let x := 0 in (for v in s.arg (if known?(v) x :+ v), x)) |
The use of iterate will only be taken into account in the compiled code unless one uses
oload, which calls the optimizer for each new method. iterate is a convenient way to
extend the set of CLAIRE data structure that represent sets with the optimal efficiency.
Notice that, for a compiled program, we could have defined set! as follows (this definition
would be valid for any new type of set) :
| set!(s:htable) -> {x | x in s} |
When defining a restriction of iterate, one must not forget the handling of values
returned by a break statement. In most cases, the code produce by iterate is itself a loop
(a for or a while), thus this handling is implicit. However, there may be multiples loops,
or the final value may be distinct from the loop itself, in which case an explicit handling
is necessary. Here is an example taken from class iteration :
iterate(x:class, v:Variable, e:any) : any => (for %v_1 in x.descendents let %v_2 := (for v in %v_1.instances e) // catch inner break in (if %v_2 break(%v_2))) // transmit the value |
Notice that it is always possible to introduce a loop to handle breaks if none are present;
we may replace the expression e by :
| while true (e, break(nil)) |
Last, we need to address the issue of parametric polymorphism, or how to define new kinds
of type sets. The previous example of hash-sets is incomplete, because it only describes
generic hash-sets that may contain any element. If we want to introduce typed hash-sets,
we need to follow these three steps. First we add a type parameter to the htable class :
Second, we use a parametric signature to use the type parameter appropriately :
| insert(x:htable<X>, y:X) -> ... |
Last, we need to tell the compiler that an instance from htable[X] only contains
objects from X. This is accomplished by extending the member function which is used by
the compiler to find a valid type for all members of a given set. If x is a type,
member(x) is a valid type for any y that will belong to a set s of type x. If T is a
new type of sets, we may introduce a method member(x :T, t :type) that tells how to
compute member(t) if t is included in T. For instance, here is a valid definition for
our htable example :
This last part may be difficult to grasp (do not worry, this is an advanced feature).
First, recall that if t is a type and p a property, (t @ p) is a valid type for x.p
when x is of type t. Suppose that we now have an expression e, with type t1, that
represents a htable (thus t1 <= htable). When the compiler calls member(t1), the previous
method is invoked (x is bound to a system-dependent value that should not be used and t is
bound to t1). The first step is to compute (t1 @ of), which is a type that contains all
possible values for y.of, where y is a possible result of evaluating e. Thus,
member(t1 @ of) is a type that contains all possible values of y, since they must belong
to y.of by construction. This type is, therefore, used by the compiler as the type of the
element variable v inside the loop generated by iterate.
Tables, Rules and Hypothetical Reasoning
Tables
- erase(a:table) -> void
- make_table(d:type, t:type, x:any) -> table
- nth(t:table, x:any) -> any
- nth(t:table, x:any, y:any) -> any
- nth=(t:table, x:any, y:any) -> any
- nth=(t:table, x1:any, x2:any, y:any) -> any
- put(t:table, x:object, y:any) -> any
Named arrays, called tables, can be defined in CLAIRE with the following syntax :
| <name>[var:<domain>] : <type> := <expression(var)> |
The <type> is the range of the table and <expression> is an expression that is used to
fill the table. This expression may either be a constant or a function of the variables
of the table (i.e., an expression in which the variables appear). If the expression is a
constant, it is implicitly considered as a default value, the domain of the table may thus
be infinite. If the default expression is a function, then the table is filled when it is
created, so the domain needs to be finite. When one wants to represent incomplete information,
one should fill this spot with the value unknown. For instance, we can define :
Tables can be accessed through square brackets and can be modified with assignment expressions
like for local variables :
| square[1], square[2] := 4, square[4] :+ 5 |
We can also define two-dimensional tables such as :
The proper way to use such a table is distance[list(denver,miami)] but CLAIRE also supports
distance[denver,miami]. CLAIRE also supports a more straightforward declaration such as :
| cost[x:(1 .. 10), y:(1 .. 10)] : integer := 0 |
Last, tables can be defined in an unamed fashion through the method make_table, such unamed
can be collected by the GC :
let square := make_table((1 .. 10), integer, 0) in (for n in (1 .. 10)) square[n] := n * n, ...) |
Rules
A rule in CLAIRE is made by associating an event condition to an expression.
The rule is attached to a set of free variables of given types: each time that an event
that matches the condition becomes occurs for a given binding of the variables (i.e.,
association of one value to each variable), the expression will be evaluated with this
binding. The interest of rules is to attach an expression not to a functional call (as
with methods) but to an event, with a binding that is more flexible (many rules can be
combined for one event) and more incremental.
Definition : A rule is an object that binds a condition to an action, called its conclusion. Each time
the condition becomes true for a set of objects because of a new event, the conclusion is
executed. The condition is expressed as a logic formula on one or more free variables that
represent objects to which the rule applies. The conclusion is a CLAIRE expression that uses
the same free variables. An event is an update on these objects, either the change of a slot or
a table value, or the instantiation of a class. A rule condition is checked if and only if an
event has occurred.
|
A novelty in CLAIRE 3.0 is the introduction of event logic. There are two events that can be
matched precisely: the update of a slot or a table, and the instantiation of a class. CLAIRE
3.2 use expressions called event pattern to specify which kind of events the rule is associated
with. For instance, the expression x.r := y is an event expression that says both that x.r = y
and that the last event is actually the update of x.r from a previous value. More precisely,
here are the events that are supported :
- x.r := y, where r is a slot of x.
- a[x] := y, where a is a.
- x.r :add y, where r is a multi-valued slot of x (with range bag).
- a[x] :add y, where a is a multi-valued table.
Note that an update of the type x.r :delete y (resp. a[x] :delete y), where r is a slot of x
(resp. a is a table), will never be considered as an event if r is multi-valued. However, one
can always replace this declaration by x.r := delete(x.r, y) which is an event, but which costs
a memory allocation for the creation of the new x.r. In addition, a new event pattern was introduced in CLAIRE 3.0 to capture the transition from
an old to a new value. This is achieved with the expression x.r := (z <- y) which says that
the last event is the update of x.r from z to y. For instance, here is the event expression
that states that x.salary crossed the 100000 limit :
| x.salary := (y <- z) & y < 100000 & z >= 100000 |
In CLAIRE 3.2 we introduce the notion of a "pure" event. If a property p has no restrictions,
then p(x,y) represents a virtual call to p with parameters x and y. This event may be used in
a rule in a way similar to x.p := y, with the difference that it does not correspond to an
update. Virtual events are very generic since one of the parameter may be arbitrarily complex
(a list, a set, a tuple ...). The event filter associated to a virtual event is simply the
expression "p(x,y)". To create such an event, one simply calls p(x,y), once a rule using such
an event has been defined. As a matter of fact, the definition of a rule using p(x,y) as an
event pattern will provoke the creation of a generic method p that creates the event. Virtual event may be used for many purposes. The creation of a virtual event requires no
time nor memory; thus, it is a convenient technique to capture state transition in your
object system. For instance, we can create an event signaling the instantiation of a class
as follows :
To define a rule, we must indeed define :
- a condition, which is the combination of an event pattern and a CLAIRE Boolean expression using the same variables
- a conclusion that is preceded by =>.
Here is a classical transitive closure example :
| r1() :: rule(x.friends :add y => for z in y.friend x.friends :add z) |
Rules are named (for easier debugging) and can use any CLAIRE expression as a conclusion,
using the event parameters as variables. Rule triggering can be traced using trace(if_write).
Notice that a rule definition in CLAIRE 3.2 has no parameters; rules
with parameters require the presence of the ClaireRules library, which is no longer available. For instance, let us define the following rule to fill the table fib with the Fibonacci sequence :
r3() :: rule(y := fib[x] & x % (0 .. 100) => when z := get(fib,x - 1) in fib[x + 1] := y + z)
(fib[0] := 1, fib[1] := 1) |
Hypothetical Reasoning
- backtrack() -> void
- backtrack(n:integer) -> void
- choice() -> void
- commit() -> void
- commit(n:integer) -> void
- contradiction!() -> void
- prealloc_list(t:type, n:integer) -> list [XL]
- prealloc_set(t:type, n:integer) -> set [XL]
- put_store(self:property, x:object, y:any, b:boolean) -> void
- store(v:global_variable) -> void
- store(rels:listargs) -> void
- store(a:array, n:integer, v:any, b:boolean) -> void
- store(l:list, n:integer, v:any, b:boolean) -> void
- world?() -> integer
In addition to rules, CLAIRE also provides the ability to do some hypothetical reasoning.
It is indeed possible to make hypotheses on part of the knowledge (the database of relations)
of CLAIRE, and to change them whenever we come to a dead-end. This possibility to store
successive versions of the database and to come back to a previous one is called the world
mechanism (each version is called a world). The slots or tables x on which hypothetical
reasoning will be done need to be specified with the declaration store(x). For instance :
Each time we ask CLAIRE to create a new world, CLAIRE saves the status of tables and slots
declared with the store command. Worlds are represented with numbers, and creating a new world
is done with choice(). Returning to the previous world is done with backtrack(). Returning to
a previous world n is done with backtrack(n). Worlds are organized into a stack (sorry, you
cannot explore two worlds at the same time) so that save/restore operations are very fast.
The current world that is being used can be found with world?(), which returns an integer.
Definition : A world is a virtual copy of the defeasible part of the object database. The object
database (set of slots, tables and global variables) is divided into the defeasible part and
the stable part using the store declaration. Defeasible means that updates performed to a
defeasible relation or variable can be undone later; r is defeasible if store(r) has been
declared. Creating a world (choice) means storing the current status of the defeasible
database (a delta-storage using the previous world as a reference). Returning to the
previous world (backtrack) is just restoring the defeasible database to its previously
stored state.
|
In addition, you may accept the hypothetical changes that you made within a world while
removing the world and keeping the changes. This is done with the commit methods.
commit() decreases the world counter by one, while keeping the updates that were made
in the current world. It can be seen as a collapse of the current world and the previous
world. commit(n) repeats commit() until the current world is n. Notice that this
"collapse" will simply make the updates that were made in the current world (n) look like
they were made in the previous world (n - 1); thus, these updates are still defeasible.
Defeasible updates are fairly optimized in CLAIRE, with an emphasis on minimal book-keeping
to ensure better performance. Roughly speaking, CLAIRE stores a pair of pointers for each
defeasible update in the world stack. There are (rare) cases where it may be interesting to
record more information to avoid overloading the trailing stack. For instance, trailing
information is added to the stack for each update even if the current world has not changed.
This strategy is actually faster than using a more sophisticated book-keeping, but may yield
a world stack overflow.
For instance, here is a simple program that solves the n queens problem
(the problem is the following: how many queens can one place on a chessboard so that none are
in situation of chess, given that a queen can move vertically, horizontally and diagonally
in both ways ?) :
column[n:(1 .. 8)] : (1 .. 8) := unknown possible[x:(1 .. 8), y:(1 .. 8)] : boolean := true store(column, possible)
r1() :: rule(column[x] := z => for y in ((1 .. 8) but x) possible[y,z] := false) r2() :: rule(column[x] := z => let d := x + z in for y in (max(1,d - 8) .. min(d - 1, 8)) possible[y,d - y] := false ) r3() :: rule(column[x] := z => let d := z - x in for y in (max(1,1 - d) .. min(8,8 - d)) possible[y,y + d] := false)
queens(n:(0 .. 8)) : boolean -> (if (n = 0) true else exists(p in (1 .. 8) | (possible[n,p] & branch((column[n] := p, queens(n - 1))))))
(queens(8)) |
In this program queens(n) returns true if it is possible to place n queens.
Obviously there can be at most one queen per line, so the purpose is to find a
column for each queen in each line : this is represented by the column table. So,
we have eight levels of decision in this problem (finding a line for each of the
eight queens). The search tree (these imbricated choices) is represented by
the stack of the recursive calls to the method queens. At each level of the
tree, each time a decision is made (an affectation to the table column), a new world
is created, so that we can backtrack (go back to previous decision level) if this
hypothesis (this branch of the tree) leads to a failure. Note that the table possible, which tells us whether the nth queen can be set on the pth line, is filled by means of rules triggered by column (declared event) and that both
possible and column are declared store so that the decisions taken in worlds that have been
left are undone (this avoids to keep track of decisions taken under hypotheses that have
been dismissed since).
Updates on lists can also be "stored" on worlds so that they become defeasible.
Instead of using the nth= method, one can use the method store(l,x,v,b) that places
the value v in l[x] and stores the update if b is true. In this case, a return to a
previous world will restore the previous value of l[x]. If the boolean value is always true,
the shorter form store(l,x,y) may be used. Here is a typical use of store :
This is often necessary for tables with range list or set. For instance, consider the
following :
even if store(A) is declared, the manipulation on l will not be recorded by the world
mechanism. You would need to write :
| A[x] := list(3, A[x][2], 3) |
Using store, you can use the original (and more space-efficient) pattern and write :
There is another problem with the previous definition. The expression given as a default
in a table definition is evaluated only once and the value is stored. Thus the same
list<integer>(0,0,0) will be used for all A[x]. In this case, which is a default value
that will support side-effects, it is better to introduce an explicit initialization of the table :
There are two operations that are supported in a defeasible manner: direct replacement of the
ithelement of l with y (using store(l,i,y)) and adding a new element at the end
of the list (using store(l,y)). All other operations, such as nth+ or nth- are not defeasible.
The addition of a new element is interesting because it either returns a new list or perform a
defeasible side-effect. Therefore, one must also make sure that the assignment of the value of
store(l,x) is also made in a defeasible manner (e.g., placing the value in a defeasible global
variable). To perform an operation like nth+ or delete on a list in a defeasible manner, one
usually needs to use an explicit copy (to protect the original list) and store the result using
a defeasible update (cf. the second update in the next example). It is also important to notice that the management of defeasible updates is done at the
relation level and not the object level. Suppose that we have the following :
C1 <: object(a:list<any>, b:integer) C2 <: thing(c:C1) store(c,a) P :: C1() P.c := C2(a = list<any>(1,2,3) , b = 0) // defeasible but the C2 object remains P.c.a := delete(copy(P.c.a), 2) // this is defeasible P.c.b := 2 // not defeasible |
The first two updates are defeasible but the third is not, because store(b) has not been declared.
It is also possible to make a defeasible update on a regular property using put_store. It is worth
noticing that hypothetical reasoning.
I/O, Modules and System Interface
Communication ports [XL]
- +(self:char*, n:integer) -> char* [XL]
- blob <: device [XL]
- blob!() -> blob [XL]
- blob!(n:integer) -> blob [XL]
- blob!(p:blob) -> blob [XL]
- blob!(p:port) -> blob [XL]
- blob!(self:string) -> blob [XL]
- bqexpand(s:string) -> string [XL]
- buffer <: filter [XL]
- buffer!(self:port, bufsize:integer) -> buffer [XL]
- byte_counter <: filter [XL]
- char* <: import [XL]
- client!(addr:string) -> socket [XL]
- client!(addr:string, p:integer) -> socket [XL]
- close_port :: property(open = 3, range = void) [XL]
- close_target!(self:filter) -> type[self] [XL]
- decode64(pr:port, pw:port) -> void [XL]
- descriptor <: device [XL]
- device <: port [XL]
- disk_file <: descriptor [XL]
- encode64(pr:port, pw:port, line_length:integer) -> void [XL]
- eof?(self:port) -> boolean [XL]
- eof_port? :: property(open = 3, range = boolean) [XL]
- filter <: port [XL]
- filter!(self:filter, p:port) -> type[self] [XL]
- flush(self:port) -> void
- flush(self:port, n:integer) -> void
- flush_port :: property(open = 3, range = void) [XL]
- fopen(self:string, mode:OPEN_MODE) -> buffer [XL]
- fread(self:port) -> string [XL]
- fread(self:port, s:string) -> integer [XL]
- fread(self:port, n:integer) -> string [XL]
- freadline(p:port) -> string [XL]
- freadline(p:port, sep:string) -> string [XL]
- freadline(p:port, seps:bag) -> tuple(string, string U char) [XL]
- freadline(p:port, seps:bag, sensitive?:boolean) -> tuple(string, string U char) [XL]
- freadline(p:port, seps:bag, sensitive?:boolean, esc:char) -> tuple(string, string U char) [XL]
- freadline(p:port, sep:string, sensitive?:boolean, esc:char) -> string [XL]
- freadwrite(src:port, trgt:port) -> integer [XL]
- freadwrite(src:port, trgt:port, len:integer) -> integer [XL]
- fskip(self:port, len:integer) -> integer [XL]
- fwrite(self:string, p:port) -> integer [XL]
- get_index(self:blob) -> integer [XL]
- getc(self:port) -> char [XL]
- gethostname() -> string [XL]
- length(self:blob) -> integer [XL]
- line_buffer <: filter [XL]
- line_buffer!(self:port) -> line_buffer [XL]
- line_counter <: filter [XL]
- linger(self:socket) -> void [XL]
- listener <: socket [XL]
- nth(self:blob, n:integer) -> char [XL]
- nth(self:char*, n:integer) -> char [XL]
- nth=(self:char*, n:integer, c:char) -> void [XL]
- nth=(self:blob, n:integer, c:char) -> void [XL]
- pipe <: descriptor [XL]
- pipe!() -> tuple(pipe, pipe) [XL]
- popen(file:string, mod:{"r", "w", "rw"}) -> popen_device [XL]
- port! :: blob! [XL]
- putc(self:char, p:port) -> void
- read!(self:port) -> void [XL]
- read_port :: property(open = 3, range = integer) [XL]
- readable?(self:port) -> boolean [XL]
- reopen(self:port) -> port [XL]
- select?() -> boolean [XL]
- select?(ms:integer) -> boolean [XL]
- server!(p:integer) -> listener [XL]
- server!(addr:string) -> listener [XL]
- server!(addr:string, p:integer, qlen:integer) -> listener [XL]
- set_index(self:blob, n:integer) -> void [XL]
- set_length(self:blob, n:integer) -> void [XL]
- socket <: descriptor [XL]
- socketpair() -> tuple(socket, socket) [XL]
- stderr : port := Clib_stderr [XL]
- stdin : port := Clib_stdin [XL]
- stdout : port := Clib_stdout [XL]
- string!(self:blob) -> string [XL]
- string!(self:char*, len:integer) -> string [XL]
- substring(self:blob, i:integer, j:integer) -> string [XL]
- unget(self:port, c:char) -> void [XL]
- unget(self:port, s:string) -> void [XL]
- unget_port :: property(open = 3, range = void) [XL]
- unlink(self:listener) -> void [XL]
- use_as_output(p:port) -> port
- writable?(self:port) -> boolean [XL]
- write!(self:port) -> void [XL]
- write_port :: property(open = 3, range = integer) [XL]
In XL CLAIRE, the entire port interface has been rewritten such port is now the root class for an
extensible hierarchy of communication interface (In CLAIRE 3, ports are based on a C++ import).
We define two sorts of port :
Definition : A device is a communication port that is connected to a physical port like a file or a
socket that can be handled through a chain of filter |
Definition : A filter is a communication port that may modify, buffer or look at a data read or
written from a device.
|
Given this sorts, we define the descriptor device as a wrapper for UNIX descriptor which handles
read, write (read_port and write_port interface) and close (close_port interface)
operations in a unified way for each derived class (disk_file, socket, pipe).
At startup, 3 global variables named stdin, stdout and stderr are created to hold
the standard input, output, and error devices respectively (UNIX descriptors 0,1,2 on most
system).
Languages often provide these standard ports in a buffered way, that is system calls read(2) or
write(2) are made by chunks. So XL CLAIRE comes with two kind of filter, the buffer (as created
by buffer!) that perform read (or
write) once for each read (or written) chunk of a given size and the line_buffer (as created by
line_buffer!) that perform write calls once for each written line.
Depending on how the program was launched, the standard output may be a terminal or something
else (e.g. pipe). In the later case we'll always provide stdout as a buffer but when it is found
that the output is a terminal device, which is often shared by multiple processes, we'll provide
stdout as a line_buffer. On the other hand the standard error port is always provided unbuffered,
such that in case of crash we avoid data miss that could be hold by a buffer.
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 instructions for the port p that are waiting
in the associated buffer (flush_port interface).
A (buffered) file is opened with fopen(s:string,m:string) where s is the file path and m
the opening mode ("r": read, "w": write, "a": append). For instance :
inefficient_show_size(filepath:string) : void -> let f := fopen(filepath, "r"), content := fread(f) in printf("File ~A has ~S bytes\n", filepath, length(content)) |
An other provided interface is the ability to make a port from a string and vice versa. In XL
CLAIRE we call that blob (based on device), the internal data representing the string is a chunk
of memory allocated dynamically outside CLAIRE memory (CLAIRE 3 port! interface is supported for
compatibility). Blob can be made in various ways with blob! including blob!(s:string) that would
initialize the internal data with the string s :
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>.
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") prints "john" princ("john") prints 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 and ~A. 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. A mnemonic is A
for alphanumeric, S for standard and I for instruction. Hence the command :
| printf("~S is ~A and here is what we know\n ~I", john, 23, show(john)) |
will be expanded into :
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 method
use_as_output is meant to select the port on which the output will be written. Following
is an example :
[XL] In XL CLAIRE printf construction can take a port argument and would perform a local
output rediction to the supplied port :
| printf(my_port, "~S is ~A and here is what we know\n ~I", john, 23, show(john)) |
will be expanded into :
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. 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 ctrace(). The following lines are equivalent :
trace(0, "assigning ~S with ~S", x, y) //[0] assigning ~S with ~S // x, y (if (verbose() >= 0) printf(ctrace(), "assigning ~S with ~S", x, y)) |
[XL] In XL CLAIRE however, trace instructions are bound to a module such one can specify a
per module verbose policy : a module m has a slot m.verbose that can take the values :
- true (default) that tell that each trace of the module follows the system policy
- false tells that traces of m won't be issued
- an integer n would set the verbose level n for m only
- an interval of two integer that define a range of allowed levels for m only
WCL syntax [XL]
XL CLAIRE comes with a new printing facility called WCL syntax standing for Web
CLaire syntax due to its design originally meant for web oriented applications with
generation of dynamic content.
WCL syntax draws its inspiration from HTML and the ability to embed CLAIRE code
in such language. It comes as a printing alternative to printf and also perform
inline substitution.
For instance, here is a simple WCL fragment and its printf equivalent :
| ?>Hello world<? <=> printf("Hello world") |
A WCL fragment is introduced with the keyword ?> which is the beginning of a static
string terminated by the corresponding keyword <? substituted at read time by a call
to princ. As a convenience a WCL fragment may not be delimited with a
coma as shown in the following equivalent forms :
?>toto<? princ("titi") ?>tata<? ?>toto<? , princ("titi"), |