# Lesson 11.2: Using type-theoretic relations

In the previous lesson, we learned how to map application types onto database types using the type-theoretic framework of the PERA model. In this lesson, we’ll explore how our models change when we represent application classes with type-theoretic relations.

## [](#_modeling_type_hierarchies)Modeling type hierarchies

In [Lesson 11.1](../11.1-using-dependent-types/index.md), we encountered the following `Review` type.

```python
class Review:
    def __init__(
        self,
        id: str,
        reviewed: Book,
        reviewer: User,
        timestamp: datetime,
        score: int
    ):
        self.id = id
        self.reviewed = reviewed
        self.reviewer = reviewer
        self.timestamp = timestamp
        self.score = score
```

Now, we have decided to make it part of a type hierarchy of user actions instead, represented by the following classes.

```python
class UserAction(ABC):
    def __init__(self, user: User, timestamp: datetime):
        self.user = user
        self.timestamp = timestamp


class Order(UserAction):
    def __init__(
        self,
        id: str,
        user: User,
        timestamp: datetime
    ):
        super().__init__(user, timestamp)
        self.id = id
        self.status = Status.PENDING


class Review(UserAction):
    def __init__(
        self, id: str,
        reviewed: Book,
        user: User,
        timestamp: datetime,
        score: int
    ):
        super().__init__(user, timestamp)
        self.id = id
        self.reviewed = reviewed
        self.score = score


class Login(UserAction):
    def __init__(
        self,
        user: User,
        timestamp: datetime,
        success: bool
    ):
        super().__init__(user, timestamp)
        self.success = success
```

In Python, the `ABC` class from the `abc` (abstract base class) module is used to denote abstract classes. A class that is a direct subtype of `ABC` is abstract.

Representing these classes in TypeQL is straightforward under the type-theoretic framework. The `UserAction` type is composed of the composite `User` type, so `user-action` should be a relation type with a role `user-action:user`. That role is then inherited by its subtypes. The `Review` type is also composed of the composite `Book` type. In the PERA model, this is represented by adding an extra role `review:reviewed` to `review`.

```typeql
define
relation user-action @abstract,
  relates user,
  owns timestamp;
relation order sub user-action,
  owns id @key,
  owns status,
  plays order-line:order,
  plays delivery:delivered;
relation review sub user-action,
  relates reviewed,
  owns id @key,
  owns score,
  owns verified;
relation login sub user-action,
  owns success;
entity user plays user-action:user;
entity book plays review:reviewed;
```

Exercise

Write TypeQL type definitions to represent the following Python classes under the type-theoretic PERA framework. Make sure to include any `plays` statements for roles defined.

```python
class Company(ABC):
    def __init__(self, name: str):
        self.name = name


class Publisher(Company):
    def __init__(self, name: str):
        super().__init__(name)


class Courier(Company):
    def __init__(self, name: str):
        super().__init__(name)


class User:
    def __init__(
        self,
        id: str,
        name: str,
        birth_date: datetime,
        location: City
    ):
        self.id = id
        self.name = name
        self.birth_date = birth_date
        self.location = location


class Delivery:
    def __init__(
        self,
        delivered: Order,
        deliverer: Courier,
        destination: Address
    ):
        self.delivered = delivered
        self.deliverer = deliverer
        self.destination = destination
```

Sample solution

```typeql
define
entity company @abstract,
  owns name;
entity publisher sub company;
entity courier sub company,
  plays delivery:deliverer;
relation user,
  owns id @key,
  owns name,
  owns birth-date,
  relates location;
relation delivery,
  relates delivered,
  relates deliverer,
  relates destination;
entity city plays user:location;
relation order plays delivery:delivered;
entity address plays delivery:destination;
```

## [](#_naming_interfaces_effectively)Naming interfaces effectively

Next, let’s consider the following `Address` class.

```python
class Address:
    def __int__(self, street: str, city: City):
        self.street = street
        self.city = city

    @property
    def state(self) -> State:
        return self.city.state

    @property
    def country(self) -> Country:
        return self.city.country
```

We could represent this in TypeQL in the following manner. We’ll leave out the properties of `Address` for now.

```typeql
define
relation address,
  owns street,
  relates city;
entity city plays address:city;
```

If we were to implement this as it is, then there would be a disparity between the way that users (defined in the above exercise) and addresses reference cities. The object type `city` would play the roles `user:location` and `address:city`, and there is no way we can polymorphically query these two roles without explicitly listing them. However, if we renamed the `address:city` role to `address:location`, then we can query them polymorphically via their shared role name, as we saw in [Lesson 7.2](../../7-understanding-query-patterns/7.2-relation-patterns/index.md), using the following pattern.

```typeql
$located links (location: $city);
```

This pattern will match instances of both `user` and `address` for `$located`, as long as we use the following definition for `address`.

```typeql
define
relation address,
  owns street,
  relates location;
entity city plays address:location;
```

For this reason, it is even more important to choose interface names carefully when using the type-theoretic framework of the PERA model.

## [](#_handling_nullary_relations)Handling nullary relations

Finally, we will consider the following classes representing places.

```python
class Place(ABC):
    def __init__(self, name: str):
        self.name = name


class Country(Place):
    def __init__(self, name: str):
        super().__init__(name)


class State(Place):
    def __init__(self, name: str, location: Country):
        super().__init__(name)
        self.location = location

    @property
    def country(self) -> Country:
        return self.location


class City(Place):
    def __init__(self, name: str, location: State | Country):
        super().__init__(name)
        self.location = location

    @property
    def state(self) -> Optional[State]:
        if type(self.location) is State:
            return self.location

    @property
    def country(self) -> Country:
        match self.location:
            case State():
                return self.location.country
            case Country():
                return self.location
```

Here we run into an interesting problem. The types `Place` and `Country` are composed only of primitive types, but `State` and `City` are also composed of composite types. The PERA model does not permit the mixing of entity and relation types into a single hierarchy. In the type-theoretic framework, we might consider an implementation where some of the relation types have no roles, as shown here.

```typeql
define
relation place @abstract,
  owns name;
relation country sub place,
  plays city:location,
  plays state:location;
relation state sub place,
  relates location,
  plays city:location;
relation city sub place,
  relates location;
```

However, this schema excerpt will not commit to a database. While abstract relation types can have no roles, all concrete relation types must have at least one role. The type `country` is concrete but does not have any roles, causing this schema to fail validation. Even if we gave `country` a dummy role, we could not instantiate it, as any relation instances without role players are erased automatically on commit. We have a few options to deal with this:

1.  Make `country` an entity type instead, and remove the supertype `place` altogether.
    
2.  Add a `location` role to `country`, and an entity type `world` to play it. We will instantiate `world` once with no attributes, and it will act as the placeholder location for all countries.
    
3.  Change the supertype `place` to an entity type, and model the network of locations with `locating` relations instead of `location` roles. This is the approach we used in the original bookstore schema under the entity-centric framework.
    

All of these solutions will allow the schema to pass validation, but none of the resulting schemas will completely match the application model. Here we cannot quite achieve parity with the current model, and we must decide whether to allow the mismatch or modify the application model to match the chosen solution in the database. In this particular case, option (2) lets us come the closest to parity, after which we can make minor changes to the application model to match, if complete parity is required.

```typeql
define
entity world,
  plays country:location;
relation place @abstract,
  owns name;
relation country sub place,
  relates location,
  plays city:location,
  plays state:location;
relation state sub place,
  relates location,
  plays city:location;
relation city sub place,
  relates location;
```

```python
class World:
    def __init__(self):
        pass


class Place(ABC):
    def __init__(self, name: str):
        self.name = name


class Country(Place):
    def __init__(self, name: str, location: World):
        super().__init__(name)
        self.location = location


class State(Place):
    def __init__(self, name: str, location: Country):
        super().__init__(name)
        self.location = location

    @property
    def country(self) -> Country:
        return self.location


class City(Place):
    def __init__(self, name: str, location: State | Country):
        super().__init__(name)
        self.location = location

    @property
    def state(self) -> Optional[State]:
        if type(self.location) is State:
            return self.location

    @property
    def country(self) -> Country:
        match self.location:
            case State():
                return self.location.country
            case Country():
                return self.location
```

[Using dependent types](../11.1-using-dependent-types/index.md) [Reifying interfaces](../11.3-reifying-interfaces/index.md)

[Edit on GitHub](https://github.com/typedb/typedb-docs/edit/3.x-development/academy/modules/ROOT/pages/11-advanced-modeling/11.2-using-type-theoretic-relations.adoc) Edit this page on GitHub.