Repository navigation
1.0.P4 Pattern: Extending behaviours
We use this pattern when we want a to alter a behaviour. For a later Tag to modify behaviour that already exists instead of simply replacing it.
But first we need to understand what a Shape is.
Suppose all Heroes can attack:
class Hero(Tag):
@Action
def Attack(agent):
return "Attack!"Now we write a new tag, like this:
class Elf(Hero):
[...]Look at Elf(Hero). This maybe looks like ordinary class inheritance to you, but TOP gives it a different meaning. Think about it... are all Elves Heroes? I'm sure they can all be heroes, but so can humans, dwarves, orcs... Being an Elf does not make you a Hero. But maybe ALL Heroes WHO ARE Elves share some skills or training in our world.
Hero is the Base.
Elf is a Shape of that Base: it describes the Hero that is also an Elf.
Conceptually:
So Elf(Hero) does not mean:
every Elf is a Hero.
It means:
this is the definition of a Hero that is also an Elf.
venn-beta
set Hero["Hero"]
set Elf["Elf"]
union Hero,Elf["Elf(Hero)"]
In this Venn Diagram we can see clearly that the Hero set is composed by everything inside the circle. The intersection with Elf makes that lemon-like shape on the side. This shape is Elf(Hero). We want to define the gains for this group of agents, and only them.
It is very similar to the set of Red Cars:
venn-beta
set Car["Car"]
set Red["Red"]
union Car,Red["Red(Car)"]
We are, therefor, not defining all Red things, only the Red Cars differentiated from other cars. That's why I remember this pattern as "The Paintjob". (When I'm feeling funny I'd say "Heroes painted Elf" instead of "Elf Heroes".)
This is why TOP speaks of Bases and Shapes, rather than parents and children.
Applying a Shape follows that geometry:
ari = Character("Ari")
Elf(ari)
assert ari in Hero
assert ari in ElfHero is applied first. Then Elf shapes what Hero already established.
Pattern 3 (The Lasagna) showed how several layers can build one Record. Actions can compose in the same spirit: take the behaviour already underneath, work on its result, and return a new one. But what if there is no underlaying Action to call? Well, we make sure we have one when we need it by using bases.
An Elf Hero should still be able to attack, but with elven grace.
We do not want to throw away Hero.Attack. We want Elf to work on top of it.
class Elf(Hero):
@Action
@Underlay
def Attack(agent, base_attack):
return "With elven grace, " + base_attack()@Underlay selects the contribution previously visible under the same name, and assigns it to the second parameter (base_attack in this case) as a callable.
So:
Elf(ari)
assert ari.Attack() == "With elven grace, Attack!"The Hero Action has not been destroyed. Elf reads the behaviour underneath and builds on its result.
A compact way to show the constructive flow is:
The ○ marks the read into the Underlay: a little hole through the current layer to what lies beneath.
In the structural diagram, we use the same idea with arrows going from o to >:
---
title: Ari
---
swimlane-beta TD
subgraph Elf
elf_attack("Attack")
end
subgraph Hero
hero_attack("Attack")
end
subgraph Character
name["name: Ari"]
end
elf_attack <---o|Underlay| hero_attack
elf_attack -.- name
hero_attack -.- name
The o connection is a constructive overlay: Elf reads and uses the contribution underneath.
Without that read, a new Action would simply replace the visible one.
Now define a Paladin-shaped Hero:
class Paladin(Hero):
@Action
@Underlay
def Attack(agent, underlay):
return underlay() + " For the oath!"Ari is already an Elf Hero. Apply Paladin afterward:
Paladin(ari)
assert ari.Attack() == \
"With elven grace, Attack! For the oath!"This exposes an important distinction.
Paladin's Underlay is not automatically Hero.Attack.
Hero is Paladin's Base, so it belongs to Paladin's Geometry. But Hero is already active on Ari.
Immediately before Paladin is applied, Ari's visible Attack is the complete Elf layer.
Therefore Paladin captures that Action.
When we apply the Paladin tag we are also cutting the base, taking out the shape of paladin from the base, but we had already cut out a big chunk: everything that wasn't Elf. So what is left is a Paladin Elf Hero.
venn-beta
set Hero["Hero"]
set Elf["Elf"]
set Paladin["Paladin"]
union Hero,Elf,Paladin["Ari"]
Information flows from the underlay to the overlay:
Structurally:
---
title: Ari
---
swimlane-beta TD
subgraph Paladin
paladin_attack("Attack")
end
subgraph Elf
elf_attack("Attack")
end
subgraph Hero
hero_attack("Attack")
end
subgraph Character
name["name: Ari"]
end
paladin_attack <---o|Underlay| elf_attack
elf_attack <---o|Underlay| hero_attack
This is Action layering.
Each layer receives the behaviour produced so far, gives it its own paintjob, and exposes the result above it.
This distinction is fundamental.
Geometry answers:
Which meanings make up this Shape?
For:
class Elf(Hero):Hero is a Base of Elf. Elf is based on Hero.
Applying Elf therefore ensures Hero is applied first.
Layering answers:
What is the visible behaviour?
When:
Elf(ari)
Paladin(ari)Paladin sees Elf's current Attack, because Elf is already part of Ari's Overlay.
So:
A Base may provide an Underlay, but an Underlay does not need to come directly from a Base. A Shape paints a Base with additional meaning. An Underlay lets its contributions paint over the behaviour already produced.
That is why Paladin can capture Elf's Attack even though Elf is not Paladin's Base.
A Shape specializes the meaning of its Base. An Action marked @Underlay can specialize the behaviour already supplied under the same name.
The two mechanisms are different, but follow the same compositional philosophy.
At the Tag level:
At the Action level:
The Action receives its previously visible behaviour through the parameter declared by @Underlay. This contribution is captured when the Tag is applied, so adding later layers does not change what was captured.
Each layer can therefore work on what came before without rebuilding it or needing to know where it originated.
This follows a simple design philosophy:
Specialize instead of rebuilding.