Repository navigation
1.0.P Design Patterns
Each pattern has the same shape: when you want something, write this, and watch out for that.
When you want rules that live in tables or files and check words, not
classes. Mark the Tags that are words with @Flag.
@Flag
class Undead(Tag):
pass
@Flag
class Flying(Tag):
pass
ghoul = Character("Ghoul")
Undead(ghoul)
assert "Undead" in ghoul # by name
assert Undead in ghoul # by class
assert "Flying" not in ghoul
TURNABLE = ["Undead", "Fiend"] # a rule as data
assert any(word in ghoul for word in TURNABLE)
assert Keyword(ghoul, "Undead") # the function form: any object
assert not Keyword(Character("x"), "Undead")When one word is not enough, list the others. The Tag's own name always counts; the words are its variants and neighbours:
@Flag("Wolf", "Lycanthrope", "Beast")
class Werewolf(Tag):
pass
howler = Character("Howler")
Werewolf(howler)
assert "Wolf" in howler # a word
assert "Werewolf" in howler # the name, always
assert Keyword(howler, "Beast", "Lycanthrope")
HUNTED = ["Beast", "Fiend"] # the table never imports Werewolf
assert any(word in howler for word in HUNTED)A word is a public name, not a membership: if there is also a Beast
Tag, "Beast" in howler is True while howler in Beast is False.
That is the point, a secret identity with a public face, but it is yours
to keep straight. When a word needs a Field to walk, a gate or a promise,
make it a Tag and a Base (class Werewolf(Beast)); when rules only match
it, a word is enough.
Watch out. A string never asks membership. An ordinary Tag never
answers to its name: "Wizard" in agent says nothing about being a
Wizard, and is True only while an active Flag is named Wizard or lists
the word. Words are strings, @Flag("Beast"); @Flag(Beast) is the bare
form applied to the class Beast, not a word.
One seat, one meaning. A Flag cannot join an object that already
answers in: a list, a party that iterates its members (__iter__), or
an Agent carrying a Tag with a __contains__ or __iter__ Action. Nor
can such a Tag join an Agent that carries a Flag. TopKit refuses either
way, and the message names both sides, so "alice" in party never
changes meaning in silence. A keyed sheet (sheet["STR"], only
__getitem__) has no working in, and a Flag may take it. So may an
old-style sequence that answers only line[0], line[1], …, and there
"alice" in line stops meaning membership: give such a class a real
__iter__ if its in matters.
A class that writes __contains__ = None tells Python its in is
unavailable, and a Flag may then take the seat. The line belongs to the
whole class, not to one Agent. Once a Flag lands, the class's own x in self answers keywords, and so do inherited container methods that ask
in (the comparisons of a collections.abc.Set). Write it only for a
class whose in nobody uses for membership.
When you want one value or one behaviour for the whole Field, and a clean line between what the Agent shows and what the Tag keeps.
class Agency(Tag):
@Public
@Report
def colour(tag):
return "navy" # one copy, published on the Agent
@Report
def HQ(tag):
return "London" # one copy, Tag-side only
@Public
@Operation
def dispatch(agency, sender, message):
return f"{sender.name}: {message}" # only members ever get here
@Secret
@Record
def clearance(agent):
return "top"
@Action
def status(agent):
return agent.clearance # inside: the secret resolves
bond = Character("Bond")
Agency(bond)
assert bond.colour == "navy" # published Report, read-only
assert bond.dispatch("hello") == "Bond: hello"
assert bond.status() == "top"
assert not hasattr(bond, "HQ") # Tag-side stays Tag-side
assert not hasattr(bond, "clearance") # secret from outside
send = bond.dispatch # a captured handle
del Agency[bond]
assert bond.status() == "top" # his own Action: he keeps it
try:
send("late")
except TagResolutionError:
pass # the Agency's: revoked with membershipThe rule in one line: what the Agent does is public, what the Agency keeps
is internal. @Secret hides an Agent member; @Public shows a Tag member.
Both are modifiers: they stack on @Record, @Action, @Report or
@Operation in either order.
A published member answers members only, and only sound ones. A Rogue
Agent (he left) keeps what he became, his own Actions and Records, and
loses what the Agency lent him: TagRogueAccessError. A defective Agent
(a promise broke, any promise, whoever made it) is turned away until
repaired, and the refusal names the broken promise. You never write
either check, and the second one gives you the autofix pattern: catch
the promise, repair what it names, try again.
class Sworn(Tag):
@Post
def Has_Oath(agent):
return agent.oath is not None
Agency(bond)
bond.oath = None
try:
Sworn(bond)
except Postcondition.Has_Oath:
pass # a member, but defective
try:
bond.dispatch("hello")
except Postcondition.Has_Oath: # the refusal names what is broken
bond.oath = "for Queen and country"
assert bond.dispatch("hello") == "Bond: hello"Notice the symmetry. A Report is written exactly like a Record, with the
Tag instead of the Agent as its first input; it runs once per Tag and its
value is shared by the whole Field. def hit_die(tag, inherited) extends
the Base's value, as def spells(agent, stored) extends what is stored.
Watch out. A secret resolves only while one of the Agent's own Actions or protocols is running. A handle to a secret Action captured inside and called outside fails, on purpose.
When you want a Tag to clean up when it leaves, or a role that lasts for a block of code.
class Sentry(Tag):
@Imprint
def post(agent):
agent.on_duty = True
@Rip
def stand_down(agent):
agent.on_duty = False
guard = Character("Guard")
with Scope(guard, Sentry): # applies on entry
assert guard.on_duty
assert not guard.on_duty # Ripped on exit, even on error
assert guard not in Sentry@Imprint runs after the Tag applies; @Rip runs after it leaves. They
are constructor and destructor, __enter__ and __exit__.
A role's conditions do not leave with it on their own. What the
Agent became stays (pattern 1's Rogue Agent), and so does what the role
required: a promise that outlives its Tag fails loud, never silently.
If a promise should end with the role, you say so, in the role's own
@Rip protocol, one name at a time:
class Knight(Tag):
@Post
def Has_Sword(agent):
return agent.sword is not None
@Rip
def Undub(agent):
Contract.Delete(agent, "Has_Sword") # the promise ends here, visibly
def Salute(agent):
return f"{agent.name} salutes"
lance = Character("Lance")
lance.sword = "Excalibur"
Knight(lance)
del Knight[lance] # dubbed no more
lance.sword = None
assert lance.Salute() == "Lance salutes" # what he became: stays
assert lance # what the role required: ended by youThe other way is a guard inside the promise, if agent not in Knight: return True, which lets the promise follow any membership you like; the
Contracts Guide shows both.
Watch out. Python does not promise to run finalizers at shutdown, so
del agent is best-effort. Scope is the guaranteed path.
An object's own __del__ is a Layer. It keeps running when the object
is tagged. A Tag may replace it, or wrap it with @Underlay; the Tags'
teardowns always run first, and a __del__ never stops them:
farewells = []
class Lantern:
def __del__(self):
farewells.append("wick out")
class Carried(Tag):
@Rip
def Put_Down(agent):
farewells.append("put down")
class Enchanted(Tag):
@Underlay
def __del__(agent, underlay):
farewells.append("spell fades")
underlay() # the Lantern's own, or nothing
lamp = Lantern()
Carried(lamp)
Enchanted(lamp)
del lamp
assert farewells == ["put down", "spell fades", "wick out"]At interpreter exit only the __del__ Layers run. To have teardowns run
at exit too, register the Agent with At_Exit(agent).
Put it together. This is the D&D example in miniature.
class Species(Tag):
@Record
def speed(agent):
return 30
class Elf(Species):
@Record
def spells(agent, stored):
return (stored or []) + ["Light"]
class Class(Tag):
@Report
def hit_die(tag):
return 8
class Wizard(Class):
@Report
def hit_die(tag):
return 6
@Pre
def Can_Study(agent):
return agent.level >= 1
@Record
def spells(agent, stored):
return (stored or []) + ["Magic Missile"]
@Record
def hit_points(agent, stored):
return (stored or 0) + agent.level * Wizard.hit_die
@Post
def Knows_Spells(agent):
return len(agent.spells) > 0
def Attack(agent):
return f"{agent.name} casts {agent.spells[-1]}"
class Fighter(Tag):
@Record
def spells(agent, stored):
return (stored or []) + ["Identify"]
class War_Caster(Tag):
@Pre
def Is_A_Caster(agent):
return agent in Wizard
@Action
@Underlay
def Attack(agent, underlay):
return underlay() + " behind a shield"
ari = Character("Ari", level=3)
for choice in (Elf, Wizard, Fighter, War_Caster):
choice(ari)
assert ari.spells == ["Light", "Magic Missile", "Identify"]
assert ari.hit_points == 18
assert ari.Attack() == "Ari casts Identify behind a shield"
assert ari # every promise holds
assert f"{ari:tags}" == "Elf, Wizard, Fighter, War_Caster"
print(Outline(ari))
# Character
# Species
# Elf
# Class
# Wizard
# Fighter
# War_CasterCategories can themselves be categorized: a class may be rare, a species homebrew, or a rule deprecated. A Pin applies such meaning to a Tag. Rare(Wizard) classifies Wizard among the rare Tags; Wizard remains a Tag with its own Agents. The pinned Tag is the Pin’s Target.
class Rare(Pin):
@Record
def rarity(tag): # the receiver is the Tag
return "rare"
@Action
def Describe(tag):
return f"{tag.__name__} is {tag.rarity}"
Rare(Wizard) # pin the Tag
assert Wizard in Rare # membership, from the Pin's side
assert list(Rare) == [Wizard] # a Field of Tags
assert Wizard.rarity == "rare" # one value, held on the Tag
assert Wizard.Describe() == "Wizard is rare"
assert not hasattr(ari, "rarity") # never on the Tag's Agents
for tag in Rare: # walk the rare Tags
print(tag.Describe())The receiver is named tag in these examples because the Record builder and Action operate on the targeted Tag. The Record supplies a Report, and the Action supplies an Operation: information and behaviour held at Tag scope. Conditions have a different subject. A Precondition or Postcondition contributed through a Pin becomes a condition the target Tag applies to its Agents, and receives the Agent being checked.
A Pin can also patch what a Tag shares. Say a Tag has an Operation that controls an engine, and the engine changed. A Pin Action of the same name replaces it, with the old one as its Underlay, and every Agent that goes through the Tag gets the new control at once:
class Engine(Tag):
@Operation
def Control(tag, level):
return f"{tag.__name__} v1 at {level}"
def Drive(agent):
return Engine.Control(agent.level) # through the Tag, at call time
kit = Character("Kit", level=2)
Engine(kit)
assert kit.Drive() == "Engine v1 at 2"
@Pin
class Firmware_2(Tag):
@Action
@Underlay
def Control(tag, underlay, level):
return underlay(level).replace("v1", "v2")
@Public # reach every Agent of the Tag, too
@Record
def firmware(tag):
return "v2"
@Rip
def Unpatch(tag, original): # the originals, handed to you
tag.Control = original.Control
Firmware_2(Engine)
assert kit.Drive() == "Engine v2 at 2" # actualized, no per-Agent work
assert kit.firmware == "v2" # published onto the Field
del Firmware_2[Engine] # un-patch: one deliberate line
assert kit.Drive() == "Engine v1 at 2"Keep the scopes distinct. A Pin’s Records and Actions modify what the target Tag shares as Reports and Operations; these contributions must not accidentally overwrite the Tag’s Agent declarations. A Pin’s Preconditions and Postconditions contribute to the conditions declared for those Agents. They do not create promises that the Tag itself must satisfy.
| Question | Spelling |
|---|---|
| Is it a Wizard now? | agent in Wizard |
| Was it ever? | isinstance(agent, Wizard) |
| Does it carry the keyword? |
"Undead" in agent, Keyword(agent, "Undead"), a Flag's words too |
| Which Tags, in order? |
Tags(agent), f"{agent:tags}"
|
| The whole shape? |
Outline(agent), f"{agent:outline}"
|
| Are its promises holding? |
if agent:, Contract.Status(agent), f"{agent:contract}"
|
| What did it look like right after Wizard? |
agent.Wizard, Wizard[agent]
|
| Question | Spelling |
|---|---|
| Who is one, and sound? |
for a in Wizard, len(Wizard), if Wizard:
|
| Who is one, and broken? |
for a in ~Wizard, if ~Wizard:
|
| Everyone? | Wizard[:] |
| Either, both, without? |
Wizard | Fighter, Wizard & Fighter, Wizard - Sworn (the Fields Guide) |
| What applies with it? |
Form(Wizard), f"{Wizard:form}"
|
| Take it away | del Wizard[agent] |
| Which Pins does it carry? |
Wizard in Rare, f"{Wizard:pins}"
|
Nothing TOP-level lives at Wizard.something. That namespace is yours: put
your Reports and Operations there.
| Do | Don't |
|---|---|
| Make a Tag for what something is: species, class, role, clearance. | Make a Tag for a passing state. Asleep is a Record, asleep = True. |
Let independent Tags pile up on one Record with stored. |
Chain Tags by inheritance just to share a list. |
Extend an Action with @Underlay. |
Call agent.Attack() from inside Attack (that recurses). |
Gate with @Pre; promise with @Post; repair through ~Tag. |
Roll your own validation after the fact. |
Mark word-like Tags @Flag and write rules as data. |
Match every Tag by name. Only Flags are words. |
Say things about a Tag with a @Pin. |
Keep a side table of Tags outside TOP. |
Clean up with @Rip, guarantee it with Scope. |
Rely on del agent for anything that matters. |
| Reset by Rip and apply again. | Reapply an active Tag hoping it resets (it does nothing). |
| Expect a Rogue Agent to keep its own Actions and lose the Agency's published ones. | Check membership by hand inside every published Operation. |
| Catch the named promise, repair what it names, retry. | Catch every failure in one handler and guess. |
Keep shared data in a Report, one copy. |
Copy shared data into every Agent's Record. |
Ask agent in Wizard. |
Ask type(agent) is Character; the type is wrapped. |
-
The Contracts Guide: gates, promises and error control,
aboard a starship; and one promise read by its name,
agent.Has_Oath. - The Fields Guide: populations, partitions and the algebra.
- The Specification: the laws, ring by ring.
-
examples/dnd_character.pyandexamples/biome.py: the long form. -
benchmarks/bench.py: what it costs. -
IMPLEMENTATION_NOTES.md: how TopKit does it.