HomeForumSourceResearchGuide
<< back to guide home

Interface elements

Interfaces may contain 1-n function prototypes, and may contain constants, default function implementations, and state transfer fields.

An interface type which only has function prototypes might look like this:

interface Util {
    int mean(int values[])
    int median(int values[])
    int highest(int values[])
    }

We can add constants to the interface, perhaps those which are used in some of the function calls:

interface Util {
    const byte M_DEFAULT = 0
    const byte M_GEOMETRIC = 1
    const byte M_HARMONIC = 2
    int mean(int values[], opt byte variant)
    int median(int values[])
    int highest(int values[])
    }

Here we've specified constants to indicate different kinds of mean, which can be passed as an optional parameter to the mean() function.

Interface functions are usually implemented by a component which provides the interface. However, it's also possible to declare a default implementation of a function in the interface itself. This means that the component does not need to offer an implementation if it does not have a different implementation to the default. This looks like this:

interface Util {
    const byte M_DEFAULT = 0
    const byte M_GEOMETRIC = 1
    const byte M_HARMONIC = 2
    int mean(int values[], opt byte variant)
    int median(int values[])
    int highest(int values[]) {
        int result = 0
        for (int i = 0; i < values.arrayLength; i++) {
            if (values[i] > highest) highest = values[i]
            }
        return result
        }
    }

Note that an interface must be provided by a component for its functionality to be usable; an interface cannot be instantiated without an implementing component, even if all of its functions have default implementations.

Finally, an interface can declare state transfer fields, which are used in runtime adaptation. This looks like this:

interface Util {
    const byte M_DEFAULT = 0
    const byte M_GEOMETRIC = 1
    const byte M_HARMONIC = 2
    transfer int callCount
    int mean(int values[], opt byte variant)
    int median(int values[])
    int highest(int values[]) {
        int result = 0
        for (int i = 0; i < values.arrayLength; i++) {
            if (values[i] > highest) highest = values[i]
            }
        return result
        }
    }

The variable callCount here operates as a global instance variable accessible to any objects instantiated from this interface. The transfer field is not accessible externally, despite it appearing in the interface definition; it appears in the interface only to ensure comapibility between different implementations of the same interface. When a component is hot-swapped to another component, each object is switched to an implementation from the new component, and each object has its transfer state fields copied over from the outgoing versions of those objects.

Interface Inheritance

Dana supports a form of functionality inheritance through its interfaces, allowing objects to be subtypes of other objects. If a component provides an interface that is a subtype of another interface, that component must either implement all functions of both the subtype and supertype, or else must have a required interface of the supertype.

All interfaces automatically inherit from the Object base type, which contains the functions equals(), toString(), clone() and getID(). These functions are therefore available on all objects, and the Object type can be used to reference any object instance.

In detail, inheritance works as follows.

Consider the two interface types:

interface GraphicsObject {
    void paint(Canvas c)
    void setPosition(int x, int y)
    Point getPosition()
    }

and

interface Button extends GraphicsObject {
    void setText(char text[])
    char[] getText()
    }

We can then implement the Button interface type as follows:

component provides Button requires GraphicsObject {
    char myText[]
    
    void Button:setText(char text[])
        {
        myText = clone text
        }
    
    char[] Button:getText()
        {
        return myText
        }
    }

In this example we've chosen to only implement the functions declared in the Button interface type, providing no implementations of any functions from the GraphicsObject supertype. Because of this we must declare a required interface of type GraphicsObject, giving access to an implementation of these functions.

We can also provide overriden implementations of some or all of the functions from the supertype. When we do this we still use the Button type as the function prefix, for example providing an overridden implementation of the paint() function from GraphicsObject:

component provides Button requires GraphicsObject {
    char myText[]
    
    void Button:setText(char text[])
        {
        myText = clone text
        }
        
    char[] Button:getText()
        {
        return myText
        }
    
    void Button:paint(Canvas c)
        {
        			
        }
    }

If we choose to provide an overridden implementation of every function from a supertype it is then no longer necessary to declare the supertype as a required interface.