Example 7: Higher-order types

Wildlife classification -- the powertype pattern: gufo:partitions

gufo:partitions gufo:Type gufo:Kind OWL 2 punning

What you'll learn

"Lion" is both a class (of individual lions) and, arguably, an instance of something -- "a species" is itself a kind of thing you might want to reason about (how many species are there?). gUFO's higher-order types let a class like :AnimalSpecies have other classes as its instances, using OWL 2 punning, and let you say precisely how strict the relationship between those instance-classes and their common base class is: gufo:partitions requires it to be a pairwise-disjoint specialization.

:AnimalSpecies is declared a plain gufo:Type. This is also the example that leans most on the SHACL shapes, since the rules governing this pattern (unlike a straightforward class hierarchy) are exactly the kind OWL alone cannot check on its own.

The domain

A wildlife classification scheme: animals belong to exactly one species (Lion, Hyena, Elephant), and species -- not individual animals -- are assessed for conservation status (Endangered, Vulnerable, Least concern), IUCN-style: population decline or recovery is a fact about a species as a whole, not about any one animal in it. (Animal names are fictional.)

Step by step

The base type

:Animal a owl:Class , gufo:Kind ;
    rdfs:subClassOf gufo:FunctionalComplex .

gufo:partitions: species, strictly

Every animal belongs to exactly one species, and no animal can belong to two -- a lion is never also a hyena. This is exactly what gufo:partitions is for: a higher-order type whose instances must be pairwise disjoint subclasses of the base type.

:AnimalSpecies a owl:Class , gufo:Type ;
    rdfs:subClassOf gufo:Type ;
    gufo:partitions :Animal .

:Lion a owl:Class , :AnimalSpecies ;
    rdfs:subClassOf :Animal .

:Hyena a owl:Class , :AnimalSpecies ;
    rdfs:subClassOf :Animal .

:Elephant a owl:Class , :AnimalSpecies ;
    rdfs:subClassOf :Animal .

[] a owl:AllDisjointClasses ;
    owl:members ( :Lion :Hyena :Elephant ) .

Look closely at what :Lion is doing here: it is declared a owl:Class (so individual lions can instantiate it), a :AnimalSpecies (so it is, itself, an instance of the higher-order type -- this is the punning), and rdfs:subClassOf :Animal (so its instances are animals). Three facts about the same name, at once, and all three are needed for the pattern to mean anything.

:AnimalSpecies is declared a gufo:Type and rdfs:subClassOf gufo:Type -- the most general acknowledgment that it is itself a type, and that its instances (:Lion and so on) are types too.

Conservation status: a regular property of :AnimalSpecies

Population decline or recovery is a fact about a species as a whole, not about any one animal in it -- but that does not mean it needs powertype machinery of its own. It is simply a property whose domain is :AnimalSpecies (the very class just declared above), following the same enumerated-value pattern as :ShirtSize in example 3:

:ConservationStatus a owl:Class ;
    owl:equivalentClass [ a owl:Class ; owl:oneOf ( :Endangered :Vulnerable :LeastConcern ) ] .

:hasConservationStatus a owl:ObjectProperty ;
    rdfs:domain :AnimalSpecies ;
    rdfs:range :ConservationStatus .

:Lion :hasConservationStatus :Endangered .
:Hyena :hasConservationStatus :LeastConcern .
:Elephant :hasConservationStatus :Vulnerable .

:hasConservationStatus applies to :Lion for exactly the same reason any ordinary property applies to any ordinary individual: :Lion is an instance of :AnimalSpecies, full stop -- no further punning insight is needed at this step. All the higher-order work already happened when :Lion was declared an instance of :AnimalSpecies above.

Individuals stay one level down

:Kesi a :Lion .

:Nia a :Hyena .

:Tembo a :Elephant .

Kesi is a lion, and lions have conservation status "Endangered" -- but :hasConservationStatus is never asserted on :Kesi, and it should not be: that property belongs to the species, not to any one animal in it. Nor is :Kesi himself an instance of :AnimalSpecies -- rdf:type does not chain across levels. :Kesi rdf:type :Lion and :Lion rdf:type :AnimalSpecies are two separate facts about two different kinds of thing (an animal, and a species), and neither a DL reasoner nor gUFO's own axioms will combine them into :Kesi rdf:type :AnimalSpecies. If an ontology needs "what is Kesi's species' conservation status?" as a queryable fact starting from Kesi, that has to be asked in two steps -- follow :Kesi's species membership, then check that species' status -- not assumed away as one.

The full ontology

See ontology.ttl for the complete file.

Common pitfall: a new partition member, minus the disjointness

Suppose a modeler adds a fourth species by copying the :Lion pattern, but -- in a hurry -- forgets to extend the disjointness declaration:

:Tiger a owl:Class , :AnimalSpecies ;
    rdfs:subClassOf :Animal .
# ...:Lion, :Hyena, :Elephant's owl:AllDisjointClasses list is not updated.

:Rajah a :Lion , :Tiger .   # nothing prevents this!

OWL never assumes disjointness by default, so an individual asserted to be both a lion and a tiger raises no error from a DL reasoner -- there is simply no axiom being violated. But it silently defeats the entire point of modeling species as a partition. The full mistake is kept in invalid-example.ttl; the gUFO SHACL shapes catch it with:

How this is checked

cd examples/tests
.venv/bin/pytest -v -k 07-higher-order-types