Logic, L1 — Animals, Families and Chains#
Level 1 · Novice
Three samples. Each one adds exactly one idea to the last.
| Sample | The new idea |
|---|---|
| Animals | a rule works out things nobody wrote down |
| Families | a rule can use its own conclusions, and can say not the same one |
| Chains | the subject can be logic itself |
Animals#
Source: src/MLambda.AI.Logic/Animals.hs ·
src/MLambda.AI.Logic/Animals.hp
The idea#
Fluffy is a cat. A cat is a kind of animal. So Fluffy is an animal — and notice that nobody said that last part. You said two things, and a third came out.
That third thing is the whole subject. A program that classifies Fluffy with if statements has to
be told about animals somewhere. A theory is told the rules, and works out the rest for every
creature anybody mentions afterwards, including ones that did not exist when the rules were written.
The theory#
theory Animals (Thing)
{
def thing(id: Thing)
def kind_of(narrow: Thing, wide: Thing)
def known(who: Thing, kind: Thing)
law directly = ∀ x k, known(x, k) ⇒ is_a(x, k)
law climbing = ∀ x k w, is_a(x, k) ∧ kind_of(k, w) ⇒ is_a(x, w)
query kinds(of: Thing, kind?: Thing) :- is_a(of, kind)
}
Read ∀ x k as "for any x and any k", ⇒ as "then", and ∧ as "and".
So directly says: for any x and k, if x is known to be a k, then x is a k. And climbing
says: if x is a k, and k is a kind of w, then x is a w.
thing, kind_of and known are things you tell the engine. is_a is what it works out —
which is why nothing asserts it. Asserting is_a would be stating as a premise the very thing this
sample is about.
climbing mentions is_a on both sides. That is deliberate, and it is the one idea here worth
sitting with. Having climbed one step, the rule applies again to its own conclusion, and keeps
applying until it runs out of kinds. Nothing says how far to go. Three levels or thirty, the rule is
the same two lines.
The ? in kind?: Thing marks an output. kinds("fluffy") is not a yes-or-no question about a
pair you already know — it hands back the answers, which is why the program reads it with
await foreach.
What the build makes of it#
Animals.hs becomes an engine. Note the singular names — a theory called Animals generates
IAnimalEngine, not IAnimalsEngine:
var zoo = AnimalEngineFactory.Create();
zoo.AssertAll([
new ThingFact("fluffy"), new ThingFact("cat"), new ThingFact("animal"),
new KindOfFact("cat", "animal"),
new KnownFact("fluffy", "cat"),
]);
One fact record per relation you declared, and the query as a method. You can read the generated
code — it is under src/MLambda.AI.Logic/Generated/Shin/ after a build.
The proof#
The engine answers about the creatures you mention. Animals.hp proves things that hold whatever
anybody mentions:
theorem a_cat_is_an_animal : ∀ x c a, known(x, c) ⇒ kind_of(c, a) ⇒ is_a(x, a)
proof
intro x c a said wider
have surely : is_a(x, c) by apply directly [said]
apply climbing [surely, wider]
qed
intro names the things you are given. have … by takes one step and names the result. apply
uses a law, handing it its premises in the order the law asks for them. See
reading a proof.
The build checks this. A theorem that stops checking fails the build before any test runs, so
the program can print its verdicts without running a prover. Try it: change auto in the last
theorem to sorry and build. You get HP0002, and no program.
Run it#
What the tests assert#
test/MLambda.AI.Logic.Tests/AnimalsTests.cs
- Fluffy is a cat — what you said survives.
- Fluffy is an animal — what you did not say is concluded.
- Fluffy is exactly a cat and an animal, and nothing else.
- Tweety is a bird and therefore an animal — but not a cat, though a cat is a kind of animal too. The rule climbs and never descends.
- A chain of three is walked: fluffy → cat → animal → living thing.
- A fact asserted later is derived over: the engine learns and answers again.
- A creature nobody classified has no kinds at all.
- Every theorem in
Animals.hpcame backProved.
The tweety one is worth a moment. "A cat is a kind of animal" does not make every animal a cat, and a rule engine that got this wrong would be useless. Nothing in the theory says "do not run backwards" — it simply never says you may.
And a word on how that test is written, because it is a trap worth seeing once. The obvious
version asks whether "cat" appears among the kinds of "animal". It does not — but only because
nothing appears there: nobody said what an animal is, so the answer is the empty list, and the
test would pass just as happily against an engine that had stopped working entirely. A test that
only looks for an absence proves nothing unless you know the answer was not empty to begin with.
So this one asks about tweety, who has kinds, and pins the whole answer set instead of probing
for one gap.
Try it yourself#
- Add
new KindOfFact("animal", "living_thing")to the zoo inProgram.csand run it again. You did not touch the rules, and Fluffy is now a living thing too. - Add a second creature — a bird called
tweety— and ask what it is. - Delete the
climbinglaw and run the tests. Watch which ones fail, and why.
The same relation, asked from the other end#
Animals.hs has a second query, who are the animals?:
It reads the same is_a as kinds, from the other end. Its parameters are in the order you ask the
question, kind first, while is_a stores the creature first. That is fine: the engine matches a
query's arguments by name. Members("animal") answers fluffy and tweety. (It once answered nothing;
see diagnostics.)
Families#
Source: src/MLambda.AI.Logic/Families.hs ·
Families.hp
The idea#
Three facts are asserted: alice is bob's parent, alice is carol's parent, bob is dave's parent. Ask who descends from alice and the answer includes dave — her grandchild, whom nothing mentioned.
Animals climbed a hierarchy. This does something new: a rule that uses its own conclusions.
law direct = ∀ a c, parent(a, c) ⇒ ancestor(a, c)
law indirect = ∀ a b c, parent(a, b) ∧ ancestor(b, c) ⇒ ancestor(a, c)
indirect needs an ancestor in order to conclude an ancestor. Having concluded one, it applies
again — and keeps applying until it runs out of parents. Nothing says how far to go. There is no
rule about grandchildren, and none about great-grandchildren either: there are two rules, and a
family of any depth.
And a rule that says not the same one#
Two children of one parent are siblings. Without x ≠ y the rule matches bob twice as a child of
alice and concludes that bob is his own sibling. The guard is the whole of the difference, and it is
the first thing in this curriculum a rule can say that a plain hierarchy cannot.
Run it#
What the tests assert#
- A parent is an ancestor, and recursion reaches the grandchild.
- Ancestry does not run backwards — dave has no ancestors.
- Two children of one parent are siblings, each way round.
- And nobody is their own sibling. Verified by mutation: dropping the guard fails three tests, including the one about dave, who becomes his own sibling without it.
Try it yourself#
Add a fourth generation and ask again. You will not need a new rule.
Chains#
Source: src/MLambda.AI.Logic/Chains.hs ·
Chains.hp
The idea#
Animals reasoned about cats and Families about people. This reasons about statements, which
means the subject is now logic itself.
def says(if_this: Claim, then_that: Claim)
def holds(what: Claim)
law ponens = ∀ p q, holds(p) ∧ says(p, q) ⇒ holds(q)
says(p, q) is the claim if p then q. holds(p) is p is true. The one law is the oldest rule
in logic: if p is true, and p says q, then q is true.
Assert that it is raining, plus three implications:
One claim was asserted true. All four come back true.
The thing most worth understanding here#
holds is both asserted and concluded — unlike is_a in Animals, which was purely
derived. A host says which claims are true to begin with; the law says which others follow; and both
kinds of fact live in the same relation. That is exactly what makes chaining work, because the
conclusion of one step is the premise of the next.
An implication is not an assertion#
Give the engine all three implications and assert nothing true. It concludes nothing. The implications are all still there — they simply never fire, because none has a true premise to start from.
That is the confusion this sample exists to prevent, and it has a test of its own.
Run it#
What the tests assert#
- One assertion, and the rule reaches the end of the chain.
- A chain with no true start concludes nothing.
- The chain does not run backwards — being careful does not make it rain.
- A claim added later is chained from.
- And the proof holds for any p, q, r, s — not just for the weather.
That last one is the difference between the two dialects in a single assertion. The engine chained
one particular set of claims; Chains.hp proves the pattern for all of them.
Try it yourself#
Assert slippery true directly, without asserting raining. Which claims follow now, and which do
not — and why?