HomeForumSourceResearchGuide
<< back to guide home

A key element of Dana's design is that it separates data from behaviour. Dana's type system therefore uses two distinct type heirarchies to represent this separation, plus primitive types. This is done so that behaviour can easily be changed at runtime while maintaining the same data formats. Dana's type system is illustrated below.

Both data and interface types support single inheritance. All data types automatically inherit from the base type Data, and all interface types automatically inherit from the base type Object. The Data and Object types themselves do not share a common base type, implementing the above separation between data and behaviour in our type system. This separation implies that if, for example, you want to supply an object instance as a parameter to a function that accepts a data type, you must first define a data type that contains the object reference as one of its fields, and then pass in an instance of that data type.

You can construct arrays of any primitive, data or object type. Primitive types are always passed by value in function call parameters or return values; while data types, objects and arrays are passed by reference.

It is also important to note that the contents of data instances and arrays are read-only when references to them are passed between components. This is part of Dana's philosophy of strong encapsulation in which modifications to state that is owned by a particular object must be made strictly via calls through its interface.

Primitive types

Dana has two primitive types: unsigned integers, and signed decimal numbers. Integers are able only to represent positive numbers, while decimal types can represent both positive and negative numbers. Integers are also used to represent characters, strings and boolean values.

The default integer type is “int” but there are various other sizes and uses available. A variable declared as:

int a

Is an unsigned integer with a bit-width matching that of the host machine. On a 32-bit machine this is a 32-bit unsigned integer.

Various other byte-widths of integer are available, starting at int1 and going up to int512, with each one being double the width of the previous one.

Unsigned integers are also used to represent characters, bytes, and boolean values. A variable declared as:

bool b

Is simply a 1-byte integer. Note that a boolean value does however have additional constraints applied to it and will cause runtime exceptions if it is set to values other than 1 (i.e. “true”) or 0 (i.e. “false”).

char c

Is also an 8-bit integer but without constraints over its contents. An array of type char is used to represent a string. Variables of type char (including arrays) can be directly used within string literals using the “$” notation to insert the value of that variable into the string.

For example:

char c = "a"
out.println("The value of c is: $c")

Note that string concatenation is achieved by constructing a character array into which the existing and extra character arrays are passed as parameters (see below documentation on arrays). Variables (and function calls) that are not of character array type can also be placed within string expressions using the same notation, and use the makeString associative function to convert from their type into a character array.

Decimal types are declared using the "dec" type, and can hold both positive and negative numbers:

dec d = 0.5

If the left hand side of an assignment is a decimal type and the right hand side is an integer, the integer is automatically converted up to its decimal equivalent. If the left hand side is an integer type and the right hand side is a decimal, the integer part of the decimal number is taken and assigned to the integer (i.e. discarding the fractional part).

The size of the dec type is twice the size of the host machine's address width; on 32bit machines it is therefore an 8 byte value, and on 64bit machines it is a 16 byte value. As with int types, there are the same set of sizes of dec type, from dec1 to dec512 – however for dec types the actual size in memory is twice that of the named size. We use the same numerical identifiers as int in the type names because a dec type of a given bit width is able to store (at least) the same maximum value as an int type of the same named bit width. A dec1 can therefore store a number of at least 255.

Data types

Data types are the typical way in which data is represented. They must be instantiated before used and are therefore always accessed by reference (meaning that they can be used to build complex structures such as linked lists). When data types are passed between two components, note that the resulting reference at the destination component is read-only. If a writeable copy is required at the destination, the data instance must be cloned using the clone operator at the destination. This behaviour helps to preserve the strong separation of components.

Data types are declared as follows:

data Cat{
    char name[]
    int age
    }

A data type is initialised by using the new operator with the type name followed by the values for each field in the correct order:

Cat c = new Cat("Johan", 3)

You can also instantiate a data type by only setting specific fields by name, for example:

Cat c = new Cat(age = 3)

A data type can inherit from one other data type using the extends notation. A data type that inherits from nothing else automatically inherits from the base type Data. Note that, if a data type is cloned into a supertype, the entire extent of the subtype is preserved in the copy. This is useful when constructing abstract data types that store generic data.

Interface types

Interface types represent behaviour. They must also be instantiated before use and so instances are always accessed and passed by reference. An interface may only have function prototypes and transfer fields (for adaptation); interfaces are not permitted to have any public data fields for direct variable access.

Interface types are declared as follows:

interface Hotel{
    Hotel(char name[])
    void addGuest(Guest g)
    }

In general the parameters declared on interface functions are assumed to be required to have a value passed in by the caller unless otherwise indicated. Interface functions (and local functions) can explicitly have optional parameters declared using the opt keyword:

interface Hotel{
    Hotel(char name[])
    void addGuest(Guest g, opt char allergies[])
    }

When using the opt keyword, every parameter that follows the first use of opt is assumed to be optional (i.e., the opt keyword does not need to be used more than once for the same function declaration).

An interface is implemented by a component which provides the logic of each function. An interface type can inherit from one other interface type using the extends notation. A interface type that inherits from nothing else automatically inherits from the base type Object.

Arrays

Dana supports one-dimensional arrays. An array, such as char x[], needs to be initialised using a construction operation before it is used. This is done with the notation:

x = new char[64]

This creates a new array of the given size (which can also be input from a variable).

Alternatively you can use the notation:

x = new char[](y, z)

This creates a new array which has the size (and deep copied contents) of all of the input parameters combined. Any number of input parameters can be used and their order is preserved in the resulting array. One of the input parameters can be the array itself, often used to expand an array with additional items.

Note that an array of data or object type is an array of references and so two levels of initialisation are needed when creating an array by size - one to initialise the array itself and one to initialise each cell of the array:

x = new Cat[64]

for (int i = 0; i < 64; i++)
    x[i] = new Cat()

Arrays of the form described in this section are always passed by reference. Like data types, array references are read-only when passed between two components. If a writable copy of a constructed array is desired, the clone operator must be used to obtain such a copy.

Any array can have its current length queried using the attribute arrayLength:

int q = x.arrayLength

Cloning data and arrays

Because data types and arrays are passed by reference and these references are read-only when they cross component boundaries, a special operator clone is available which copies a data type or array so that the copy is writable:

Cat x = clone y

The clone operation makes a deep copy of the first level of the given data type or array. This copy is not recursive, and so if you want to copy the fields of a data type, or copy the elements of an array, you must re-apply the clone operator to those additional elements.

Testing equality

Equality can be checked by value and by reference.

To test value equivalence, the operators == and != are used. In the case of data types, the operation a == b will perform a test of field equivalence for every field of the data type. For arrays, the equivalence of each cell in the array is checked. Note that in both of these cases, the equality check does not follow any references; instead references are considered equal if they reference the exact same entity instance. For objects, the operation a == b uses the object's equality function and is effectively the same as (a == null && b == null) || a.equals(b).

To test reference equivalence, the operators === and !== are used. These operations check if two variables refer to exactly the same instance of a data type, array, or object.

Type equivalence

When making decisions about whether or not two types are compatible for assignment, Dana considers type equivalence at a structural level and ignores semantics. The following two types are therefore compatible in Dana's type system:

data Person {
    char name[]
    int age
    }
data Address {
    char streetName[]
    int houseNumber
    }

Because there is no structural difference between these two types (they both have a character array field followed by an integer field) Dana considers them equivalent and compatible for inter-assignment. The same type compatibility rules apply to interface types. This leaves the programmer free to decide what semantic type equivalence (for example the names of types) means in their context.

Under these rules, note that the “packages” in which interfaces appear are purely a programmer-level semantic annotation that is not relevant to the language in determining compatibility.Inter-type assigns and hastype

As with other modern systems programming languages, Dana does not support an explicit cast operator. However, compatible types can be assigned as part of a regular assignment operation, for example:

String a = new String("Hi")
Data b = a
String c = b

You can query at runtime whether or not a given type has a particular sub-type identity by using the hastype operator. As an example, consider a function that receives an Object instance as a parameter. We can then use hastype to determine whether or not this Object is structurally equivalent to a File type and then call File operations on it:

void function(Object o)
    {
    if (o hastype File)
        {
        File f = o
        char buf[] = f.read(64)
        }
    }

Garbage collection

Objects, data types and regular arrays are all 'constructed' at runtime and are accessed by reference. These items are automatically garbage collected when their reference count reaches zero or when their creating component is destroyed.

For more information on Dana's garbage collector, which is a little different to other languages, please see the memory management topic.

Serialisation

Dana can automatically convert Data instances to byte arrays, as long as the Data type includes only fields of primitive type (including fixed-size arrays of primitive type).

This is done by using the notation dana.serial(x). This operation returns a reference to a byte array containing the contents of x as a stream of bytes. This reference is fully writable, allowing the individual bytes of a data item to be modified.

An example use of this would be:

data Person {
    char name[50]
    int age
    }

Person p = new Person()
byte stream[] = dana.serial(p)
dana.serial(p) =[] stream