Open Types

Concept

Open types separate the definition of types from the definition of their attributes (i. e., data fields or members) in order to support the incremental definition of the latter. Attributes of a type might be defined in the same module as the type or in different modules, and they might be either exported from these modules or not. By that means, it is possible to have different views on the same type and its instances in different modules.
Conceptually, an instance of an open type is a set of attribute/value pairs whose extension might change during run time. If an attribute is read which is not present yet, a well-defined null value is returned; if an attribute is written which is not present yet, a new attribute/value pair is added to the set.

Attributes might be single- or multi-valued, where the latter are stored in appropriate container objects. Furthermore, it is possible to define bidirectional relationships between types (with cardinalities 1:1, 1:N, N:1, and M:N) corresponding to pairs of attributes whose values will be kept mutually consistent by the runtime system.

Attributes are actually pairs of global virtual functions consisting of a read and a write function. By redefining these functions, it is possible to define ``set and get advice'' (to use aspect-oriented terminology), i. e., code that will be transparently executed whenever an attribute value is read or written. This can be used, for example, to easily implement the Observer Pattern (without needing to pre-plan its application) or to implement virtual or persistent data structures.

It is possible to define attributes which are applied automatically in order perform an implicit type conversion from their domain to their range type. Similar to implicit upcasts in object-oriented languages (but in contrast to user-defined conversions in C++), these conversions are applied transitively if necessary. Therefore, they can be used to simulate object-oriented subtyping and inheritance concepts such as single, multiple, and even repeated inheritance. Nevertheless, many problems typically associated with inheritance are avoided. For example, attributes of the same name ``inherited'' from different ``parent types'' do not conflict, since they are merely overloaded function names which are resolved in the usual way. Furthermore, the distinction between virtual and non-virtual inheritance is more flexible and at the same time easier to achieve. Actually, inheritance and aggregation are not artificially separated, but merged into a single coherent concept.

Example

Persons

We start by defining an open type Person with a single-valued attribute name and a multi-valued attribute firstnames, both of type string. Furthermore, we define a virtual constructor for Person, i. e., a global virtual function whose name and result type are both Person and therefore the latter can be omitted in the declaration. It accepts a name and up to three first names as arguments and constructs a corresponding Person object p by initializing its attributes name and firstnames with the arguments nm and fn1, respectively, and adding fn2 and fn3 as firstnames if appropriate. Finally, a global virtual function print is defined which prints a person's details on the standard output stream cout. The last few lines show a typical usage of both the constructor and the print function.

    // Open type with two attributes.
    typename Person;
    Person -> string name;        // Name.
    Person ->> string firstnames; // First names.
 
    // Virtual constructor.
    virtual Person (string nm, string fn1, string fn2 = null, string fn3 = null) {
        Person p = Person(@name, nm)(@firstnames, fn1);
        if (fn2) p(@firstnames, fn2);
        if (fn3) p(@firstnames, fn3);
        return p;
    }
 
    // Global virtual function.
    virtual void print (Person p) {
        cout << "Name: ";
        for (string s : p@firstnames) cout << s << " ";
        cout << p@name << endl;
    }
 
    // Global statement block.
    {
        Person ch = Person("Heinlein", "Christian");
        print(ch);
    }

Students

To define another type Student that is a subtype of Person in object-oriented terminology, an automatic anonymous 1:1 relationship between Student and Person is defined. This means, that every Student object is expected to have an associated Person object, and the former is implicitly converted on demand to the latter by following this relationship. Conversely, every Person object might have an associated Student object which can be tested by following the inverse relationship.
In addition to the attributes of Person, which are inherited in object-oriented terminology, a student has a single-valued attribute number of type int representing its matriculation number.
In analogy to persons, a virtual constructor and a global virtual print function are defined for students where the latter is a redefinition of the function defined for persons since its signature void print (Person) is identical. However, its signature contains a guard if (...) that tests whether the Person object p has an associated Student object s. If this is true, the function's body is executed, while otherwise its previous branch is called implicitly.

    // Open type.
    typename Student;
    Student <->! Person;   // Automatic anonymous 1:1 relationship.
    Student -> int number; // Matriculation number.
 
    // Virtual constructor.
    virtual Student (Person p, int n) {
        return Student(@Person, p)(@number, n);
    }
 
    // Redefinition of global virtual function.
    virtual void print (Person p) if (Student s = p@Student) {
        virtual(); // Call previous branch explicitly to print person details.
        cout << "Matriculation number: " << s@number << endl;
    }
 
    // Global statement block.
    {
        Student fjm = Student(Person("Maier", "Franz", "Josef"), 1234);
        print(fjm);
    }

Employees

In complete analogy to students, it is possible to define another ``subtype'' Employee of Person that possesses a separate attribute number of type string representing an employee's personal number.

    // Open type.
    typename Employee;
    Employee <->! Person;
    Employee -> string number; // Personal number.
 
    // Virtual constructor.
    virtual Employee (Person p, string n) {
        return Employee(@Person, p)(@number, n);
    }
 
    // Redefinition of global virtual function.
    virtual void print (Person p) if (Employee e = p@Employee) {
        virtual(); // Call previous branch explicitly to print person details.
        cout << "Personal number: " << e@number << endl;
    }
 
    // Global statement block.
    {
        Employee kh = Employee(Person("Huber", "Karl"), "ABCD-5678");
        print(kh);
    }

Employed Students

To demonstrate a typical case of ``diamond inheritance,'' another type EmployedStudent is defined as a ``subtype'' of both Student and Employee. The fact that the common ``supertype'' Person is ``inherited'' only once, is captured in the definition of EmployedStudent's constructor, where the same Person object p is passed to the constructors of Student and Employee.
The print function for employed students is yet another redefinition of the original function for persons, whose guard tests whether the Person object p possesses an associated Student object (it could equally well test whether it possesses an associated Employee object) which in turn possesses an associated EmployedStudent object es. If this is true, the person, student, and employee details are printed by explicitly calling the function's previous branch, i. e., the redefinition for employees which in turn calls the redefinition for students which in turn calls the original definition for persons. (In particular, the definition for persons is called only once, i. e., the person details will not be duplicated in the output!)

    // Open type.
    typename EmployedStudent;
    EmployedStudent <->! Student;
    EmployedStudent <->! Employee;
    EmployedStudent -> double ratio; // Ratio of employment.
 
    // Virtual constructor.
    virtual EmployedStudent (Person p, int mnum, string pnum, double r) {
        return EmployedStudent
          (@Student, Student(p, mnum))
          (@Employee, Employee(p, pnum))
          (@ratio, r);
    }
 
    // Redefinition of global virtual function.
    virtual void print (Person p)
    if (EmployedStudent es = p@Student@EmployedStudent) {
        virtual(); // Call previous branch explicitly
                   // to print person, student, and employee details.
        cout << "Ratio of employment: " << es@ratio << endl;
    }
 
    // Global statement block.
    {
        EmployedStudent ms = EmployedStudent(
          Person("Schulze", "Martin"), 1111, "XXX-999", 0.5
        );
        print(ms);
    }

Schizos

To demonstrate a typical case of ``repeated inheritance,'' another type Schizo is defined that ``inherits'' from Person twice by having two 1:1 relationships to Person.
Printing such an object is a bit tricky: the personality for which print has been called is easily printed by calling the function's previous branch; to print the other personality, however, a recursive call to print is necessary since the previous branch cannot be called directly with different arguments. To avoid endless recursive calls, a Boolean flag is used to temporarily ``disable'' the current branch that performs the recursive call.

    // Open type.
    typename Schizo;
    Schizo Schizo1 <-> Person Person1; // First personality.
    Schizo Schizo2 <-> Person Person2; // Second personality.
 
    // Virtual constructor.
    virtual Schizo (Person p1, Person p2) {
        return Schizo(@Person1, p1)(@Person2, p2);
    }
 
    // Global variable.
    bool recursive = false;
 
    // Redefinitions of global virtual function.
    virtual void print (Person p) if (p@Schizo1 && !recursive) {
        cout << "Schizo with first personality:" << endl;
        virtual();
        cout << "Second personality is:" << endl;
        recursive = true;
        print(p@Schizo1@Person2);
        recursive = false;
    }
    virtual void print (Person p) if (p@Schizo2 && !recursive) {
        cout << "Schizo with second personality:" << endl;
        virtual();
        cout << "First personality is:" << endl;
        recursive = true;
        print(p@Schizo2@Person1);
        recursive = false;
    }

It is interesting to note that the type Schizo defined above does not only capture ``simple'' schizos, but also examples like a schizo whose first personality is a student and whose second personality is an employee:

    {
        Schizo aebg = Schizo(
          Student(Person("Einstein", "Albert"), 222),
          Employee(Person("Gates", "Bill"), "MS-Boss")
        );
        print(aebg@Person2);
    }
The output of this program fragment would be:
    Schizo with second personality:
    Name: Bill Gates
    Personal number: MS-Boss
    First personality is:
    Name: Albert Einstein
    Matriculation number: 222

Publications

[1] C. Heinlein: "APPLE: Advanced Procedural Programming Language Elements." In: W. Goerigk (ed.): Programmiersprachen und Rechenkonzepte (21. Workshop der GI-Fachgruppe; Bad Honnef, Mai 2004). Bericht Nr. 0410, Institut für Informatik, Christian-Albrechts-Universität zu Kiel, January 2005, 59-66. (PostScript, PDF)
Gives another example of using open types in combination with global virtual functions by presenting a simple solution to the well-known ``expression problem.''
Note that a slightly different notation for expressing ``subtype'' relationships (keyword conv instead of <->!) and for ``dynamic type tests'' (colon instead of at operator) is used there.

[2] C. Heinlein: "Open Types and Bidirectional Relationships as an Alternative to Classes and Inheritance." Journal of Object Technology 6 (3) March/April 2007, 101-151, http://www.jot.fm/issues/issue_2007_03/article3.
Comprehensive description of open types including many examples of their use, e. g., about diamond and repeated inheritance as well as dynamic object evolution. Global virtual functions and modules are also covered briefly in order to present a solution to the expression problem. Basic implementation ideas are described, too.


Christian Heinlein, 22.09.09