Markdown for LLMs
Exercise 8. The largest of the unpaid
The source Markdown for this article. Copy it into your assistant or download it as a text file.
# Exercise 8. The largest of the unpaid
An exercise for the [collections and aggregates](/tutorials/aggregates/)
page. On the page, the unpaid-order total counts as an aggregate with an
explicit `all`.
**Setup.** Derive for a reader the largest unpaid order's sum. Two unpaid
orders, at 1,000 and 2,500 tenge, yield the answer `2500 KZT`. A reader
without orders has no answer, and **no runtime error either**: the
question answers `NEITHER` with an empty problem list.
**Hint.** An aggregate over an empty collection is an error, not zero.
What must stand in the norm's condition so the aggregate is not computed
in vain?
Below is the solution. Try it yourself first.
## Solution
```law
language "law.core" version "0.2";
package tutorial.archive version "0.14.1";
namespace "urn:law:tutorial:archive";
entity Person;
entity CopyOrder;
relation copy_order(o: CopyOrder, p: Person) kind empirical;
relation order_amount(o: CopyOrder, amount: Money) kind empirical { key(o); }
relation order_paid(o: CopyOrder) kind empirical;
relation registered_reader(p: Person) kind institutional;
relation unpaid_order(o: CopyOrder) kind institutional;
relation largest_unpaid(p: Person, amount: Money) kind institutional { key(p); }
rule UnpaidOrder strict {
for o: CopyOrder;
for p: Person;
when copy_order(o, p) and not order_paid(o);
then unpaid_order(o);
}
```
The maximum norm. The guard in the condition counts unpaid orders and
lets the norm through only on a nonempty collection:
```law
rule LargestUnpaid strict {
for p: Person;
when registered_reader(p) and count(collect o: CopyOrder where copy_order(o, p) and unpaid_order(o)) > 0;
then largest_unpaid(p, max(collect all a: Money, o: CopyOrder where copy_order(o, p) and unpaid_order(o) and order_amount(o, a)));
}
```
`all` is here for the same reason as in the page's sum: two orders at an
equal sum are two orders, and the maximum must not depend on whether
equal values collapsed. For a maximum this does not change the answer,
but the collection form is a data decision, not an operation one, and it
must not vary from aggregate to aggregate.
## Check
| Facts | Question | Answer |
|---|---|---|
| two unpaid: 1,000 and 2,500 | `largest_unpaid(ivanova, 2500 KZT)` | `TRUE_ONLY` |
| two unpaid: 1,000 and 2,500 | `largest_unpaid(ivanova, 1000 KZT)` | `NEITHER` |
| reader without orders | `largest_unpaid(ivanova, 0 KZT)` | `NEITHER`, no problems |
| without the `count(...) > 0` guard, reader without orders | same | `NEITHER`, an `EMPTY_AGGREGATE` problem |
The fourth row is a counterfactual: with the guard cut out
the engine answers the empty-aggregate error. It does not drop the
whole document, but it appears in the problem list, and the consumer must
see it.
What the exercise teaches. An empty collection is not zero. Every norm
with an aggregate needs a nonempty guard or a separate empty-case norm,
like `TotalDueNothing` on the page. The choice between them is a question
for the text: does the act say anything about a reader without orders.