Lesson 9.6: Using interface hierarchies
In the previous lesson, we improved the type hierarchies in our book data model by using the principle of composition over inheritance. In this lesson, we’ll explore the behaviour of interface hierarchies and see how they can be used to adapt our model in different ways.
Lesson 9.5 schema
define
entity book @abstract,
owns isbn, # abstract, but instantiated using isbn-10 or isbn-13
owns isbn-13,
owns isbn-10,
owns title,
owns page-count,
owns genre,
owns price,
plays contribution:work,
plays publishing:published;
entity paperback sub book,
owns stock;
entity hardback sub book,
owns stock;
entity ebook sub book;
entity contributor,
owns name,
plays contribution:contributor;
entity publisher,
owns name,
plays publishing:publisher;
entity place @abstract,
owns name,
plays locating:location,
plays locating:located;
entity city sub place,
plays publishing:location;
entity state sub place;
entity country sub place;
relation contribution,
relates contributor,
relates work;
relation authoring sub contribution;
relation editing sub contribution;
relation illustrating sub contribution;
relation publishing,
relates publisher,
relates published,
relates location,
owns year;
relation locating,
relates located,
relates location;
attribute isbn @abstract, value string;
attribute isbn-13, value string;
attribute isbn-10, value string;
attribute title, value string;
attribute page-count, value integer;
attribute genre, value string;
attribute price, value double;
attribute stock, value integer;
attribute name, value string;
attribute year, value integer;
Interfaces as types
In the PERA model, interfaces are themselves types, as we saw in Lesson 9.1. Like the data storing types, interface types are able to form hierarchies. Role hierarchies are derived from their dependent relation type hierarchies, while ownership hierarchies are derived from their dependent attribute type hierarchies. Let’s examine an example.
define
attribute isbn @abstract, value string;
attribute isbn-13 sub isbn;
attribute isbn-10 sub isbn;
As we saw in Lesson 9.1, the isbn
attribute type exposes an ownership interface, which we’ll call isbn:OWNER
for this discussion. Likewise, isbn-13
and isbn-10
expose the interfaces isbn-13:OWNER
and isbn-10:OWNER
respectively. As isbn-13
and isbn-10
are subtypes of isbn
, this means that isbn-13:OWNER
and isbn-10:OWNER
are subtypes of isbn:OWNER
.
Role hierarchies work in a similar way, but have a major difference. Whereas all attribute types have an associated ownership interface, some relation types do not have role interfaces of their own, instead inheriting them from supertypes. We saw this in Lesson 9.5 with the following relation type hierarchy.
define
relation contribution,
relates contributor,
relates work;
relation authoring sub contribution;
relation editing sub contribution;
relation illustrating sub contribution;
Here contribution
defines two interfaces: contribution:contributor
and contribution:work
, and these are also inherited by its subtypes, which have no roles of their own. This means that all four relation types expose these same two interfaces. Consider the following instead.
define
relation contribution,
relates contributor,
relates work;
relation authoring sub contribution,
relates author as contributor;
relation editing sub contribution,
relates editor as contributor;
relation illustrating sub contribution,
relates illustrator as contributor;
Here authoring
, editing
, and illustrating
each expose a new interface: authoring:author
, editing:editor
, and illustrating:illustrator
respectively. These interfaces override the interface contribution:contributor
, meaning that it is no longer usable by the subtypes of contributor
. Because the newly defined interfaces override an interface from the parent type, these interfaces authoring:author
, editing:editor
, and illustrating:illustrator
are subtypes of the parent interface contribution:contributor
. Meanwhile, the interface contribution:work
is still exposed by all four relation types.
In other words, overriding an inherited role is like a combination of subtyping and making the inherited value abstract:
define
relation contribution,
relates contributor,
relates work;
relation authoring sub contribution,
relates author as contributor,
relates contributor @abstract;
# and also: authoring:author sub contribution:contributor
Since contributor
is abstract in the authoring
scope, it’s not available to be instantiated in the data. However, authoring:author
is a subtype of contribution:contributor
that can be used in its place.
Interface variance
The way in which interface implementations are inherited is determined by their variance. Interface implementations, as defined in owns
and plays
statements, are:
-
Covariant in the implementing object type. This means that the statement also applies to subtypes of the object type by inheritance.
-
Invariant in the implemented interface type. This means that the statement does not also apply to subtypes of the interface type.
Let’s consider the following example, featuring two plays
statements.
define
entity book plays contribution:work;
entity contributor plays contribution:contributor;
In the first statement, the implementing object type is book
and the implemented interface type is contribution:work
. The object type has three subtypes paperback
, hardback
, and ebook
, and they also implement contribution:work
by inheritance because the statement is covariant in book
.
In the second statement, the implementing object type is contributor
and the implemented interface type is contribution:contributor
. The interface type has three subtypes authoring:author
, editing:editor
, and illustrating:illustrator
, but contributor
does not also implement them because the statement is invariant in contribution:contributor
.
Let’s consider another example, this time with an owns
statement.
define
entity book owns isbn;
The implementing object type is book
and the implemented interface type is isbn:OWNER
. This time, both have subtypes. The object type has three subtypes paperback
, hardback
, and ebook
, and they also implement isbn:OWNER
. Meanwhile, the interface type has two subtypes isbn-13:OWNER
and isbn-10:OWNER
, and book
does not also implement them (nor do its subtypes).
Because implementations are invariant in the interface, this means that we must separately declare implementations of their subtypes if we wish to use them, as follows1.
define
entity contributor plays authoring:author;
entity contributor plays editing:editor;
entity contributor plays illustrating:illustrator;
entity book owns isbn-13;
entity book owns isbn-10;
Using role overrides effectively
Earlier in this lesson, we encountered two possible design strategies for the subtypes of contribution
: one in which the subtypes inherit the role contribution:contributor
, and one in which the role was overridden with specialized subtypes. The two strategies result in identical querying capabilities. When the role is inherited, we can query contribution
and its subtypes with the following patterns:
-
To describe an instance of
contribution
or one of its subtypes:contribution (contributor: $person, work: $book);
-
To describe an instance of a specific subtype of contribution (e.g.
authoring
):authoring (contributor: $person, work: $book);
-
To describe an instance of
contribution
only, not one of its subtypes:$rel isa! contribution (contributor: $person, work: $book);
When the role is overridden, we can do the same with the following patterns instead:
-
To describe an instance of
contribution
or one of its subtypes:contribution (contributor: $person, work: $book);
Here, using
contributor
in the relation tuple matches bothcontribution:contributor
and its subtypes by inheritance polymorphism. -
To describe an instance of a specific subtype of contribution (e.g.
authoring
):authoring (author: $person, work: $book);
-
To describe an instance of
contribution
only, not one of its subtypes:$rel isa! contribution (contributor: $person, work: $book);
The only thing that changes is the way we query the specific subtypes of the relation type. Even then, we can easily use role inference to omit the roles from the pattern as we saw in Lesson 7.2, and use the same queries throughout. Thus, the querying capabilities of the model are the same regardless of whether we inherit or override a role. In fact, this is true of any model involving roles that can be inherited or overridden.
However, there is a key difference in the way the roles must be implemented in these two cases. Consider the following definition:
define
contributor plays contribution:contributor;
If contribution:contributor
is inherited by the subtypes of contribution
, then contributor
will be able to play the role in those relation types as well, as they will all expose the same role interface. Conversely, if the role is overridden in the subtypes of contribution
, then contributor
will not be able to play roles in the subtypes as the plays
statement is invariant in the role, as we saw above.
As a result, when building a relation type hierarchy, the primary factor to consider when deciding whether to inherit or override a role is whether role players of the relation supertype should also be role players of the relation subtypes. In this particular case, anyone capable of making a contribution to a book is also able to author, edit, or illustrate a book, by definition. As such, it makes most sense for the subtypes of contribution
to inherit the role contribution:contributor
rather than override it.
In other cases, it may be more appropriate to override roles. The following schema excerpt for a filesystem access management model is such an example. Here, we ensure that all users are able to execute system actions, but only admins are able to execute them with admin privileges.
define
entity user,
plays action-execution:executor;
entity admin sub user,
plays privileged-execution:priveleged-executor;
relation action-execution,
relates action,
relates executor;
relation privileged-execution sub action-execution,
relates privileged-executor as executor;
Footnotes
-
^ In some cases, it might seem cumbersome to have to explicitly define each implementation of an interface’s subtypes, but the invariance is essential to prevent logical fallacies in data model. Consider the following set of facts:
-
All humans are animals.
-
All geese are animals.
-
All animals can join animal groups.
-
All flocks are animal groups.
-
All geese can join flocks.
Now consider the following deduction:
-
All animals can join animal groups, and all flocks are animal groups, therefore all animals can join flocks.
If this deduction were correct, it would allow humans to join flocks, despite the fact that only geese should be able to join flocks! The deduction is obviously fallacious, specifically by affirming the consequent. Now consider if we translated our list of facts into TypeQL. It might look something like this:
define animal sub entity, abstract; human sub animal; # all humans are animals goose sub animal; # all geese are animals animal-group sub entity, abstract; group-membership sub relation, abstract, relates group, relates group-member; animal-group plays group-membership:group; animal plays group-membership:group-member; # all animals can join animal groups flock sub animal-group; # all flocks are animal groups flock-membership sub group-membership, relates flock as group, relates flock-member as group-member; flock plays flock-membership:flock; goose plays flock-membership:flock-member; # all geese can join flocks
If definitions of interface implementations were covariant in the interface type instead of invariant, then
animal
, and all of its subtypes includinghuman
, would inherit the ability to playflock-membership:flock-member
! -