Dana uses strict encapsulation between components to support sound runtime adaptation. As part of this encapsulation, state is generally made read-only at any location which did not create that state. This applies to data instances, array instances, and object instance state.
Consider the below example, which creates an array of data instances and then asks for that array to be sorted by one of its fields:
data Person {
char firstname[]
char surname[]
int age
}
component provides App requires io.Output out, data.query.Sort sort {
int App:main(AppParam params[]) {
Person people[] = new Person[](new Person("Alex", "Smith", 19),
new Person("Faiza", "Nihiri", 27),
new Person("Sula", "Kerridge", 23))
people = sort.sort(people, Person.[surname], true)
return 0
}
}
When we pass our people array into sort(), we are passing that array reference to another component. The sort() function is therefore unable to write to the cells of that array, and must gain write-ownership of the array. This is done by using the clone operator. When the sort() function returns the sorted array back to our main function, this version of the array is then read-only to our own component.
If we try to modify a cell of that array after the sort() call we get a run-time error:
people = sort.sort(people, Person.[surname], true)
people[1] = new Person("Penelope", "Eve", 48)
Exception::attempt to assign to read-only location on line 15 of Test.dn
We can use the clone operator to regain write-ownership of the returned array to remove the exception:
people = clone sort.sort(people, Person.[surname], true)
people[1] = new Person("Penelope", "Eve", 48)
The clone operator makes a copy of the instance it is given, whether this is an array or a data instance. By creating a copy, this gives the caller write-ownership of the respective instance. The copy has all of the same field values of the original, such that if any of those fields refer to other array or data instances, those referenced instances are not themselves copied.
To perform a recursive clone, we can use the rclone operator, which follows all references recursively to also clone any data or array instances found:
Cat x = rclone yA recursive clone results in a data instance in which every reference-field is deep-copied, and all internal references of the original instance graph are made internally consistent with copied instances within the cloned instance graph.
In some cases the compiler and runtime can determine from the surrounding context that it is not necessary to actually copy the instance given to clone, and in this case the instance is simply given a change of write-ownership.
The instance (or global) state within objects is also generally read-only except to owners (direct instantiators) of those objects, except for object instance state which is of primitive type.
Dana partly controls this by requiring use of the special qualifier store on function parameters. A parameter that is qualified with store can in principle be written to the instance state of the object, as long as that object is also owned by the caller. The compiler and runtime will normally advise you on whether or not parameters need a store qualifier on them.
An example of this concept might be an interface supporting storage and retrieval of a list of data instances:
interface List {
void add(store Data item)
void rem(Data item)
}
Here we have declared the parameter of add with store, because the implementation of this interface will keep the data instance item in the instance state of an object. Any parameters without the store qualifier can only be used within the scope of the function implementation, rather than being able to be assigned to any instance state.
The rem function, by comparison, does not need the store qualifier on its item parameter because the rem function will only use that parameter within the scope of the function itself, while searching for the matching stored item to remove.
The use of store qualifiers is only necessary on function definitions that appear in interfaces. For all local functions within components, that are internal to the component and not part of any interface, the Dana compiler automatically determines whether or not store qualifiers are necessary for each parameter.
The final encapsulation constraint is that an object X cannot return references to any objects that were instantiated by X. This constraint serves a dual purpose, simplifying memory management and also forming part of the generalised language-level assurance that Dana provides for runtime adaptation. Object functions will generally therefore only return values that are of primitive or data type, or arrays of those types.