New ACM paper, free-tier cloud, and open-source license

Lesson 10.3: Rule branching

Mutually exclusive branches

In the previous lesson, we learned how to encode chains of logic with rules. In this lesson, we will learn how to encode branching logic. We encountered the following rule in Lesson 10.1, which is designed to dynamically generate line totals for orders as they are queried.

rule order-line-total:
    when {
        $book isa book, has price $retail-price;
        $line ($book) isa order-line, has quantity $quantity;
        ?line-total = $quantity * $retail-price;
    } then {
        $line has price ?line-total;
    };

However, this rule does not take into account any discounts due to sales. Let’s go about solving this problem by replacing it with two new rules. The first rule will check that there are no applicable discounts, then use the retail price to calculate the line total.

rule order-line-total-retail-price:
    when {
        ($order) isa action-execution, has timestamp $order-time;
        $line ($order, $item) isa order-line;
        not {
            ($promotion, $item) isa promotion-inclusion;
            $promotion has start-timestamp <= $order-time,
                has end-timestamp >= $order-time;
        };
        $item has price $retail-price;
        $line has quantity $quantity;
        ?line-total = $quantity * $retail-price;
    } then {
        $line has price ?line-total;
    };

The second rule will check that there is an applicable discount, also check that there is no better applicable discount, then use the best discounted price to calculate the line total.

rule order-line-total-discounted-price:
    when {
        ($order) isa action-execution, has timestamp $order-time;
        $line ($order, $item) isa order-line;
        ($best-promotion, $item) isa promotion-inclusion, has discount $best-discount;
        $best-promotion has start-timestamp <= $order-time,
            has end-timestamp >= $order-time;
        not {
            ($other-promotion, $item) isa promotion-inclusion, has discount > $best-discount;
            $other-promotion has start-timestamp <= $order-time,
                has end-timestamp >= $order-time;
        };
        $item has price $retail-price;
        ?discounted-price = round(100 * $retail-price * (1 - $best-discount)) / 100;
        $line has quantity $quantity;
        ?line-total = $quantity * ?discounted-price;
    } then {
        $line has price ?line-total;
    };

Here we have used two mutually exclusive branches of logic. Because both of the rules have the same conclusion, they represent two alternative sets of conditions. In fact, it would be possible to combine them into a single rule using a disjunction, but such a complex rule wouldn’t be easy to work with. This way, we can clearly divide the two alternatives.

In this case, it’s important to make the two conditions mutually exclusive, as we wouldn’t want to generate more than one line total per order line. If we examine the patterns, we can see that this has been achieved with a negation. The first rule includes the following constraints.

($order) isa action-execution, has timestamp $order-time;
$line ($order, $item) isa order-line;
not {
    ($promotion, $item) isa promotion-inclusion;
    $promotion has start-timestamp <= $order-time,
        has end-timestamp >= $order-time;
};

Meanwhile, the second rule includes the following ones.

($order) isa action-execution, has timestamp $order-time;
$line ($order, $item) isa order-line;
($best-promotion, $item) isa promotion-inclusion;
$best-promotion has start-timestamp <= $order-time,
    has end-timestamp >= $order-time;

If we carefully compare them, we see that an instance of $line could never possibly meet both sets of constraints. This ensures that the two rules have mutually exclusive conditions, so more than one line total could never be generated.

Overlapping branches

In some cases, it is not necessary to ensure that rules are mutually exclusive. Consider the following two rules. The first one checks for pairs of books that have the same author, where a user has liked (ordered or highly rated) one of the books, but has not interacted with (ordered or rated) the other. It then recommends the new book to the user with a recommendation relation.

rule book-recommendation-by-author:
    when {
        $user isa user;
        $liked-book isa book;
        {
            ($user, $order) isa action-execution;
            ($order, $liked-book) isa order-line;
        } or {
            ($user, $review) isa action-execution;
            ($review, $liked-book) isa rating;
            $review has score >= 7;
        };
        $new-book isa book;
        not { {
            ($user, $order) isa action-execution;
            ($order, $new-book) isa order-line;
        } or {
            ($user, $review) isa action-execution;
            ($review, $new-book) isa rating;
        }; };
        ($liked-book, $shared-author) isa authoring;
        ($new-book, $shared-author) isa authoring;
    } then {
        (recommended: $new-book, recipient: $user) isa recommendation;
    };

The second one works in the same way, except it checks for pairs of books with the same genre rather than author (except for "fiction" or "nonfiction"). It also generates a recommendation relation.

rule book-recommendation-by-genre:
    when {
        $user isa user;
        $liked-book isa book;
        {
            ($user, $order) isa action-execution;
            ($order, $liked-book) isa order-line;
        } or {
            ($user, $review) isa action-execution;
            ($review, $liked-book) isa rating;
            $review has score >= 7;
        };
        $new-book isa book;
        not { {
            ($user, $order) isa action-execution;
            ($order, $new-book) isa order-line;
        } or {
            ($user, $review) isa action-execution;
            ($review, $new-book) isa rating;
        }; };
        $liked-book has genre $shared-genre;
        $new-book has genre $shared-genre;
        not { {
            $shared-genre == "fiction";
        } or {
            $shared-genre == "nonfiction";
        }; };
    } then {
        (recommended: $new-book, recipient: $user) isa recommendation;
    };

These two rules represent branches of logic, except they are not mutually exclusive. Two books could have the same author and genre (in fact, this is very often the case), and neither rule employs a negation of the other’s constraints. However, only one recommendation relation can be generated between each book and user, even if both rules are triggered.

As we learned in Lesson 10.1, rule inference will not generate data that already exists. Crucially, that includes data that exists in the scope of the transaction because it was generated by another rule. With the previous pair of rules for calculating line totals, the retail and discounted totals would be different so TypeDB would insert both. In this case, the conclusions are completely identical, so duplication would not occur. We can take advantage of this property of rule inference to add as many rules as we want that create recommendation relations in this way, all based on different criteria.

Provide Feedback