Objects and Classes
2.1 Objects and Entities
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.
Primitive entities consist of integers, floats, symbols, strings, ports (streams) and functions.
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.
2.2 Classes
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 descendants) 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 (cf. Section 3.5) 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 superclasses, 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 and placed 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 descendants (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:
abstract(person)
An abstract class is not an empty set, it contains the instances of its descendants. Second, a class can also be declared to have no more new descendants using final as follows:
final(colors)
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 may keep or not the set of its instances. When CLAIRE keeps the extension of the class (the set of instances, accessible through the instances set), the class may be used (for instance, iterated) as a set. This is the default behavior for named objects (then the class inherits from thing), but not for other classes (that inherit from object).To force CLAIRE to maintain the class extension, the class must be declared with instanced .
action <: object(on:any, performed_by:object)
instanced(action)
Note: in previous versions of CLAIRE (2 & 3), it was the opposite : all classes would maintain their extensions and you had to declare a class as “ephemeral” to disable the extension bookkeeping.
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 (cf. Section 3.5). 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 set (without duplicates). CLAIRE assumes by default that a slot with range 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 (cf. Section 4.5). 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
2.3 Parametric Classes
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.
stack[of] <: object(of:type,content:list[any],index:integer = 0)
complex[re,im] <: object(re:float = 0.0,im:float = 0.0)
The default method for printing an object takes this parametric definition into account. Objects from a class C are printed as <C>, unless the method self_print is defined for C (see Section 6.1). Objects from a parametric class are printed C(..), where the value of the parameters are printed with the parentheses.
We shall see in Section 4 that 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:
stack[of:{integer}] stack[of:subtype[integer]]
2.4 Calls and Slot Access
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. Slot accesses follow the usual field access syntax « x.s » where s if the name of the slot. CLAIRE uses generic objects called properties to represent the name of a method, used as the selector f of a function call f(...), or a slot, used as the selector s in a slot access x.s. In the following example, eval is a function and price is a property. Properties and functions are two kinds of relation.
eval(x), f(x,y,z), x.price, y.name
Note: For upward-compatibility reasons, CLAIRE supports both x.s and s(x) as valid expressions to read the slot s from object x. In CLAIRE 4, x.s is equivalent to read(s,x) and will complain if the value is uknown (unless unknown belongs to the range of s). s(x) is equivalent to get(s,x), it will return unknown without generating an error if no value has been set.
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 s(x) or 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.
1 + 2, 1 + 2 * 3
Note that new operations may be defined (Section 4.5). 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 (cf. Conditionals, Section 3.3) yields a simpler programming style when dealing with lists or sets. Notice that in CLAIRE 3 or CLAIRE 4, 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 (cf. Section 4.1). 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,
compose(f:function, g:function, x:any) => f(g(x))
is equivalent to
compose(f:function, g:function, x:any) => call(f, call(g,x))
2.5 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 A(x :+ 1) :+ 1.
2.7 Primitive entities
Integers in CLAIRE 4 are 64-bits integer. Integers have a common syntax (A more formal CLAIRE syntax is presented in the first appendix):
0, 1, 123456, -2013
Floats are 64-bits from the host language (Go). Their syntax follows C/Go convention, with the addition of the % macro-caracter, to indicate percentage values.
0.123, 3.14159, 12.3e8, -2.12e-13, 20%, 21.2%
Characters in CLAIRE are also inherited from the host language (8 bits char). Constants are represented using “’” as separators; the two special values \n (new line) and \t (tab) are also supported.
‘a’, ‘0’, ‘\n’, ‘\t’, char!(123), char!(#/a)
Symbols are inherited from LISP, they represent names that are associated to a module (namespace) and hashed for quick retrieval. Symbols may be thought of as names; they can be created dynamically with methods such as symbol! or statically with the following syntax:
Core/”a symbol”, claire/”class”, m1/”g3”, symbol!(m,”g” /+ “12“), symbol!(claire,string!(12))
Strings in CLAIRE are character chains that are not necessarily constants (contrary to Go or new versions of C++); for C programmer, they are close to “char*” type. Strings’s syntax is very classical using “”” as a separator. To find out all the string methods that are supported by CLAIRE, you may enter methods(string,string) at the top-level, or look at the 2nd appendix. Strings may be created dynamically using the make_string(..) method.
“a string”, “a string with \n”, make_string(100,’a’), make_string(list(‘a’,’b’,’c’))
Ports are CLAIRE entities that represent ports from the host language. Ports are only created using the fopen method that is similar to that of C or C++.
Last, “external functions” are pointers to host language functions that will be linked to the CLAIRE program at compile time (their use is explained in Appendix C of the CLAIRE documentation). The syntax is the following:
function!(pow), function!(make_array)
2.8 Inspecting Objects
CLAIRE is a truly reflective language; everything is an entity that belongs to a class and that can be inspected. The method owner applies to any entity and return a class (owner(x) = x.isa if x is an object). The method show is useful to inspect any entity or object:
show(“a string”), show(class), show(quote(1 + 2)), show(list(‘a’,’b’,’c’))
CLAIRE has a built-in object inspector : inspect(x:any) with a simple short-cut : ? x.
? class // inspect a class
? isa // inspect a property
? (+ @ integer) // inspect a method
? quote(let x := 1 in (x + 1) // inspect a CLAIRE expression