Chapter 11: A Matter of Weight
The overweight-dog alert had been firing for a week, and Dr. Portbridge was starting to distrust it. It flagged Rex, a 45-kilo Labrador, fair enough. But it had also flagged nothing at all for a visiting Great Dane that clearly needed a diet, and it had thrown an alarm for a Chihuahua whose owner recorded its weight in grams. She looked at the rule again:
weight [ > 40.0 ]. Forty. Forty what? And why would one number fit a parrot and a mastiff? The rule wasn’t wrong so much as it didn’t know what it was talking about.
The problem with a bare number
In Chapter 10 we wrote our first numeric comparison:
rule flag_overweight_dog:
match:
?dog a Dog
?dog weight [ > 40.0 ]
then:
?dog a OverweightAnimal
That 40.0 is a bare float, and a bare float carries no meaning. Is it
forty kilograms? Forty pounds? Whoever writes a fact has to remember the
convention and stick to it, and the moment one owner records weight 45.0
meaning 45 kg while another records weight 20.0 meaning 20 lb, the numbers lie
to us. It is the same trouble string dates gave us in
Chapter 9: a value the machine can store but cannot
understand.
What we actually mean is a physical quantity: a number and a unit, together, as one indivisible thing.
Quantities
Dolfin writes a quantity as a smart literal, the same shape as the temporal values from Chapter 9, a keyword and human-friendly notation in parentheses:
quantity(45 kg) # forty-five kilograms
quantity(4500 g) # four and a half kilograms, written in grams
quantity(90 lb) # ninety pounds
quantity(55 g) # a small parakeet
The unit lives inside the value. Nothing is left to convention. And because Dolfin understands the units, it can do the one thing a bare number never could: compare measurements written in different units. Ninety pounds and forty kilograms are no longer two unrelated numbers, Dolfin knows that 90 lb is about 40.8 kg, and can tell you which is heavier.
Quantities are not just for weight.
quantity(42 km/h),quantity(9.81 m/s^2),quantity(10 N/m^2)all work, Dolfin has a whole SI unit system built in. The Units & Quantities reference covers compound units, prefixes, arithmetic, andasconversions. Here we only need weight.
Weighing the patients properly
The Animal concept already has a weight attribute. We leave its declared
type as float, a quantity’s real type is its unit, which the value carries,
so the attribute only has to say “a number”:
concept Animal:
has name: one string
has species: one Species
has age: optional int
has weight: optional float # values are quantities: quantity(45 kg)
has owner: optional Owner
has vaccinations: Vaccination
has allergies: string
floataccepts any quantity,quantity(45 kg)orquantity(3 s)alike, nothing here checks that aweightis actually a mass. If you want that checked, the Units & Quantities reference covers dimension-typed ranges (has weight: unit.Mass). This tutorial keepsfloatto focus on quantities themselves first.
Now the facts record real measurements, and each animal can use whatever unit its chart was written in. Here are this week’s patients:
fact rex a Dog
name "Rex"
species Dog
weight quantity(45 kg)
neutered true
fact bella a Dog
name "Bella"
species Dog
weight quantity(90 lb) # ≈ 40.8 kg
neutered true
fact buddy a Dog
name "Buddy"
species Dog
weight quantity(22 kg)
neutered true
fact mittens a Cat
name "Mittens"
species Cat
weight quantity(7 kg)
indoor true
A threshold that knows its units
Now we rewrite the alert so the threshold is a quantity too:
rule flag_overweight_dog:
match:
?dog a Dog
?dog weight [ > quantity(40 kg) ]
then:
?dog a OverweightAnimal
When the reasoner runs, it does not compare the written numbers, it compares the underlying physical magnitudes:
rex a OverweightAnimal # 45 kg > 40 kg → flagged
bella a OverweightAnimal # 90 lb ≈ 40.8 kg > 40 kg → flagged
# buddy is left untouched # 22 kg > 40 kg → does not match
Bella is the point of the whole chapter. Her weight was recorded in pounds, the threshold is in kilograms, and the rule still fires correctly, because Dolfin converts both to a common footing before comparing. The bare-number version could never have caught her.
Both sides must be quantities. The unit-aware comparison only runs when the stored value and the threshold are
quantity(...). Comparing aquantity(...)weight against a bare40.0, or vice versa, falls back to a plain numeric test with no unit reasoning, exactly the ambiguity we set out to remove. So once a value is a quantity, keep the threshold a quantity as well.
One threshold per species
Forty kilograms is a sensible line for a dog and absurd for a cat. Because the threshold is written right into each rule, every species gets its own:
concept OverweightCat:
rule flag_overweight_cat:
match:
?cat a Cat
?cat weight [ > quantity(6 kg) ]
then:
?cat a OverweightCat
Mittens, the cat in our patient list above at 7 kg, is a chunky cat and gets
flagged. A parakeet recorded as
quantity(55 g) would sit far below any of these lines, and a threshold written
in grams (quantity(60 g)) would compare against it perfectly, grams, kilograms,
and pounds are all the same dimension (mass), so they are all comparable.
Comparing unlike things
What if a threshold and a value are not the same kind of measurement? Suppose someone fat-fingers a length where a weight belongs:
?dog weight [ > quantity(40 m) ] # metres — a length, not a weight
Dolfin does not crash and does not silently coerce. Mass and length are different dimensions, so the comparison simply fails and the animal is never flagged. A comparison you cannot make is treated as one that does not hold, the same rule Dolfin uses everywhere: a condition it cannot satisfy does not match.
The story so far
package <http://happypaws.com/clinic>:
dolfin_version "1"
version "0.1.0"
author "Dr. Helen Portbridge"
description "The Happy Paws veterinary clinic data model"
concept Species:
one of:
Dog
Cat
Bird
Rabbit
Reptile
Other
concept Urgency:
one of:
Routine
Urgent
Emergency
concept AppointmentStatus:
one of:
Scheduled
InProgress
Completed
Cancelled
concept Owner:
has first_name: one string
has last_name: one string
has phone_numbers: at least 1 string
has email: optional string
has address: optional string
has preferred_vet: optional Veterinarian
concept Veterinarian:
has name: one string
has license_number: one string
has specialization: optional string
concept Surgeon:
sub Veterinarian
has surgery_count: one int
has certified_procedures: at least 1 string
concept Dentist:
sub Veterinarian
has dental_certification: one string
concept Intern:
sub Veterinarian
has university: one string
has year: one int
concept Vaccination:
has vaccine_name: one string
has date_administered: one string
has batch_number: optional string
concept Animal:
has name: one string
has species: one Species
has age: optional int
has weight: optional float
has owner: optional Owner
has vaccinations: Vaccination
has allergies: string
concept Dog:
sub Animal
has breed: optional string
has neutered: one boolean
concept Cat:
sub Animal
has indoor: one boolean
concept Bird:
sub Animal
has wingspan: optional float
has can_fly: one boolean
concept Appointment:
has animal: one Animal
has scheduled_for: one date_time
has reason: one string
has urgency: one Urgency
has status: one AppointmentStatus
has diagnosis: optional string
has treatments: string
has notes: optional string
property treatedBy: Animal -> Veterinarian
# Flag concepts and inference rules (Chapter 10)
concept UnvaccinatedAnimal
concept UnsafeAssignment
concept OverweightAnimal
rule flag_unvaccinated:
match:
?animal a Animal
?animal vaccinations 0
then:
?animal a UnvaccinatedAnimal
rule flag_intern_emergency:
match:
?appt a Appointment
?appt urgency Emergency
?appt animal [ treatedBy [ a Intern ] ]
then:
?appt a UnsafeAssignment
rule assign_primary_vet:
match:
?animal a Animal
?animal owner [ preferred_vet ?vet ]
then:
?animal treatedBy ?vet
# Weight thresholds, now unit-aware (this chapter)
concept OverweightCat
rule flag_overweight_dog:
match:
?dog a Dog
?dog weight [ > quantity(40 kg) ]
then:
?dog a OverweightAnimal
rule flag_overweight_cat:
match:
?cat a Cat
?cat weight [ > quantity(6 kg) ]
then:
?cat a OverweightCat
Try it
Birds are recorded in grams. Write a rule that flags a Bird heavier than 500
grams as a HeavyBird, then add a fact for a parrot that weighs quantity(1.2 kg)
and check that it trips the threshold even though its weight is written in
kilograms and the threshold in grams:
concept HeavyBird:
rule flag_heavy_bird:
match:
?bird a Bird
# your quantity comparison here
then:
# your assertion here
Dr. Portbridge re-ran the alerts. Rex and Bella both surfaced, the Great Dane among them this time, and the Chihuahua-in-grams was quietly left alone. Every weight now meant exactly what it said. But catching an overweight dog after the fact was one thing. She wanted the system to refuse bad data outright, a surgery booked with an intern, a completed appointment with no diagnosis, before it was ever written down. She needed guard rails.