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.
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); }
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); }
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); }
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); }
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
[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.