-
Notifications
You must be signed in to change notification settings - Fork 1
Complex types
A complex data type (a.k.a. composite data type or compound data type) is any data type which can be constructed using the language's primitive data types and other complex types. Roughly speaking, a complex type is a group of data elements grouped together under one name. These data elements, known as members, can have different, either primitive or complex, types.
The best way for defining new types is by means of the class
keyword. Classes define data types and allow the creation of objects according to characteristics defined inside the class itself. In addition, classes allow fields of any type as well as methods and constructors with any kind of arguments. Classes can be declared in our domain description language using the following syntax:
class type_name {
member_type1 member_name1;
member_type2 member_name2;
member_type3 member_name3;
.
.
}
In order to allow an initialization of the member variables of the type, classes can include a special function called its constructor, which is automatically called whenever a new object of the class is created. This constructor function is declared just like a regular member function, but with a name that matches the class name and without any return type. Furthermore, when a constructor is used to initialize other members, these other members can be initialized directly, without resorting to statements in its body. This is done by inserting, before the constructor's body, a colon (:
) and a list of initializations for class members. All types have at least one constructor. If a type does not explicitly declare any, the solver automatically provides a no-argument constructor, called the default constructor.
The following code, for example, defines a new data type (or class) called Block
containing an int
field named id
.
class Block {
int id;
Block(int id) : id(id) {}
}
Block b0 = new Block(0);
Block b1 = new Block(1), b2 = new Block(2);
The declared type Block
is then used for instantiating three objects (variables) called b0
, b1
and b2
. Note how, for creating a new instance of a complex type, the new
operator is used. Specifically, the new
operator instantiates a class and, also, invokes the object constructor, returning a reference to the newly created object. Notice that the reference returned by the new
operator does not have, necessarily, to be assigned to a variable. Indeed, it can also be used directly in an expression.
It is important to clearly differentiate between what is the type name (Block
), and what is an object of this type (b0
, b1
and b2
). As can be noted by the above example, many objects (such as b0
, b1
and b2
) can be declared from a single type (Block
).
Inheritance allows us to define a class in terms of other classes. When creating a class, instead of writing completely new fields and methods, the modeler can designate that the new class should inherit the members of existing classes. Similarly to object oriented programming, we call the existing classes the base classes, while the new class is referred to as the derived class. The idea of inheritance implements the is a relationship. For example, mammal IS-A animal, dog IS-A mammal hence dog IS-A animal as well and so on. For example, through the code
class HeavyBlock : Block {
real weight;
HeavyBlock(int id, real weight) : Block(id), weight(weight) {}
}
we create a derived type HeavyBlock
which inherits from the base type Block
. Since an HeavyBlock
is-a Block
, all instances of HeavyBlock
will have a weight
field of type real
as well as an id
field of type int
which, we say, is inherited from base type Block
.
Notice that a derived type must explicitly call a constructor of the base class from which inherits. This explicit call, however, can be omitted in the case the base class has a default constructor.
A class may inherit from more than one class by simply specifying more base classes, separated by commas, in the list of a class's base classes (i.e. after the colon). Unless the base classes have a default constructor, the derived class must explicitly call a constructor of each of the base classes.
Existential quantification is a type of quantifier which can be interpreted as "there exists", "there is at least one", or "for some". Specifically, the domain description language allows the modeler to retrieve a specific instance of a given type which meets certain requirements. Creating an existentially scoped variable can be done, simply, by indicating the type of the possible instances and an identifier for representing the desired instance. For example:
Block b;
searches for a block b
among all the instances of type Block
. In other words, it creates an object variable, called b
, whose allowed values are all the instances of type Block
. Notice that, since HeavyBlock
is actually a Block
, the allowed values for the variable b
will include, also, all the instances of HeavyBlock
. It is worth to note that, in case no instances exist, the domain of the variable b
will be empty and the solver will return false
(or, if possible, will backtrack).
The desired requirements are expressed by means of constraints. Consider, for example, the following code:
Block b;
b.id <= 10;
In this case the assertion will limit the domain of b
to all the instances of Block
whose id
is lower or equal that 10
.
It is worth to note that it is possible to use comparison operators on existentially scoped variables. For example
b != b1;
removes the instance represented by b1
from the allowed values of the variable b
.