modified: 2024/06/28 17:00:59

Modeling Entity Behaviour

Following the outline below, this article presents the modeling of entity behaviour, which builds on the structural entity design.

1. Introduction

The - potentially semi-automated - behaviour of an entity is modeled by a behavioural expression composed of one or more steps.

1.1. An Example - Tossing dice

A number of players take turn tossing a (virtual) die.

The first players to reach an accumulated target score win - yes, more than one winner is a possibility!

If your score is above the target, the face value of your next turn will be subtracted rather than added, and to win, the target must be reached exactly.

site RollDice
    description:                Implement a simple game of dice using behaviour and step
    publishedat:                model.dbquity.com/examples/behaviour
    version:                    0.10.0
    area Table
        integer Players
            default:            3
        integer Target
            default:            13
        step NewGame
            guard:              Players > 1 and Players < 10 and Target > 6 and Target < 36
            behaviour:          add(Game, Players: Players, Target: Target)
            autocommit          # make Game available on other devices
        entity Game
            collection:         Games
            readonly
            integer Players
            integer Target
            function IsDone
                expression:     Scores.any(Amount = Target)
            behaviour:          # each round, players toss concurrently
                while not IsDone() do
                    with player in 1..Players || Toss(player)
            step Toss
                parameter Player
                behaviour:      # user triggered, lazy-create Score, random update
                    getoradd(Score: Player).set(Amount: NextAmount())
                autocommit      # persist making others see the toss
            entity Score
                collection:     Scores
                integer Player
                integer Amount
                identity:       Player
                function NextAmount
                    expression: if Amount < Game.Target then Amount + random(1,6)
                                                        else Amount - random(1,6)

The design comprises an area Table (the playing table in your favourite saloon), at which you may at any time enter a number of players and set a target and - if the number is in the 2 to 9 range and the target from 7 to 35 - start a game. When a step - in this case NewGame - is activated by a behaviour, a runtime instance of that step is created. Once the guard predicate evaluates to true, its behaviour will either autoexecute or cause a button to appear in the UI for the end-user to click.

Since Table.behaviour keeps repeating NewGame every time the previous instance succeeds, it will always be possible to start a new game unless at some point a failure occurs.

The present example design should not cause failures, but in general, the behaviour will stop executing, when any step instance fails and leave it up to the end user to deal with the failure. In some cases simply rerunning the failed step will remedy the situation.

The entity Game is designed with a child entity Score per player and the Game.behaviour

while not IsDone() do
    with player in 1..Players || Toss(player)

will execute rounds of playing until at least one player reaches the target exactly, and in each round one toss for each player can be carried out concurrently: if several devices are used it is possible for several players to toss their virtual die at the same time.

Like Table.NewGame the step Toss declares no autoexecute modifier so when each round starts a button for each player will appear in the UI, and when that button is hit Toss.behaviour executes updating Score.Amount for its player.

1.2. Three step combining operators

The part of the behaviour expression that describes each round of playing

with player in 1..Players || Toss(player)

semantically expands to

Toss(1) || Toss(2) || .. || Toss(Players)   # explanatory, but *not* supported

Meaning that the step instances for each player all execute at the same time.

As mentioned above, || runs the step instances in parallel whereas the | operator in

with player in 1..Players | Toss(player) 

would mean that in each round all steps would be ready and display a button, but only a single step instance would execute. In the absence of a guard, it would simply be the first player to hit the toss button who got to do a toss in each round. And maybe that same player keeps being the fastest - quite a different game...

Using the sequence operator ; as in

with player in 1..Players ; Toss(player)

ensures that all players get to toss in each round, but in strict order: player 1 goes first, player 2 goes next, then player 3 and so on.

1.3. User Interface Mockups

1.3.1. Area Table

The playing table is always Active, and the [NewGame] step is ready to be executed by a user clicking the button. In the mockup below, 2 games are active and 7 have been successfully concluded.

1.3.2. Entity Game

Below, we show a newly started game, which is Active and all 5 indexed Toss steps are ready:

As players 5 and 2 hit their buttons, their scores are created, then updated and the buttons disappear until next round, when new incarnations of the steps become ready:

Entering what turns out to be the last round, player 1 has overshot the target, and everybody except player 5 could win in this round...

Players 3 and 5 are fastest - 3 is lucky:

After players 1, 2 and 4 have hit their buttons, no more rounds are played, because the Game behaviour,

while not IsDone() do ...

exits the loop leaving the game in its final state of Completed:

2. A more Declarative approach Aims to Beat imperative Code

This modeling of behaviour is introduced as an attempt to improve over an approach of exposing a number of event handlers that are each implemented using an imperative programming language.

The declarative behavior expression and the declarative structure of each step, which explicitly separate logic over persistable data from entry and deadline points in time aim to

2.1. Call to Verify!

The syntax used in the behaviour expression still contain imperative constructs like assignment and entity creation, and the operators for iteration and parallel execution closely resemble well known control constructs to achieve the same expressiveness.

This calls for a discussion of how well the present approach addresses the perceived issues with imperative code in practice, and once we - hopefully - have numerous designs utilizing the behaviour construct a thorough comparison with equivalent solutions based on imperative code in event handlers should be made.

3. Conceptual Design

The behaviour expression is composed of steps that can be combined sequentially, concurrently, alternately and iteratively. An step may also invoke other steps, but recursion is not supported.

The readiness of each step can be guarded by a predicate and/or an earliest possible time of entry.

Once ready, a step may execute its own behavior, which is a list of steps that may create new entities, set values on existing entities or call other steps then either automatically commit or propose the changes to an end-user, who may commit.

Finally, a step may specify a success predicate and/or an exit deadline, which determine whether the step concluded successfully or not.

Steps and behaviour are inherited and a step can be abstract and may define a base and be overridden like all members of entities.

Only non-abstract steps can be executed at runtime.

To reduce complexity and ease understanding in the beginning, we start the detailed description deliberately ignoring the temporal aspects of entry time and deadline, which are then treated in a later section.

4. Syntax

4.1. Declaration of a step member:

step <name>
    [abstract]
    [base: <step>]
    [override: <step>]
    [parameter <name>]*
    [guard: <predicate>]
    [autoexecute]
    [behaviour: <behaviour-expression>]
    [autocommit]
    [success: <predicate>]

where
    predicates, expressions and behaviour may refer to
     - the members of the declaring entity and any linked or linking entities
     - the point in time, creation, when the step instance was created
     - the point in time, ready, when the step became ready
     - the current evaluation of the success predicate
     - the parameter runtime value, if declared

4.2. Declaration of the behaviour property:

behaviour: b

where
    b           ::= imperative | s | while <pred> do b | with <p> in <seq> * b | s * b
    s           ::= <step>([<parameter-list>])
    parameter-list
                ::= [<parameter>:] <expression> [, parameter-list]
    imperative  ::= assignment | creation | deletion
    assignment  ::= [entity-expr.]set(member-list)
    entity-expr ::= [entity-expr.]<link> | collection.find-expr | creation | ^<entity>
    collection  ::= [entity-expr.]<collection-name> | [entity-expr.]<link>@<entity>
    find-expr   ::= single([predicate]) | first([predicate]) | last([predicate]) |
                    get(<entity>[: <identity>][, member-list])
    member-list ::= <member>: <expression>[, member-list]
    creation    ::= [entity-expr.]add(<entity>[: <identity>][, member-list]) |
                    [entity-expr.]getoradd(<entity>[: <identity>][, member-list])
    deletion    ::= [entity-expr.]delete(<entity>[: <identity>][, member-list])

    while <pred> do b   repeats b while <pred> evaluates to true

    with <p> in <seq> * b
                        seq expresses a sequence of elements that are passed as a parameter into
                        instances of b, which execute according to * representing one of
                        the three operators | || or ;
                        <p> is used in the parameter-list of one or more steps in b

    *   is one of the three combining operators in order of precedence:
        ||  concurrence     execute a set of steps concurrently
        |   choice          from a set of alternative steps, select and execute the
                            leftmost step whose guard is satisfied
        ;   sequence        execute a sequence of steps from left to right

    ^<entity>   finds the nearest <entity> ancestor

    set         assigns values to a list of members of an entity or class
                it is possible to set the deadline (see
                [7. Temporal Constraints](#7-temporal-constraints)) of the step that declares the
                 behaviour

    get         gets an entity with a given identity and/or a certain set of values

    add         creates and adds a new instance of an entity then sets member values as
                specified

    getoradd    gets an entity with a given identity or a certain set of values
                if such an entity does not exist, it is added

    delete      deletes an entity with a given identity or a certain set of values, if it exists

A behavioural expression is evaluated left-to-right, and parentheses ( ) can be used to group parts. Line breaks are collapsed to a single space when parsing.

The while and with constructs have highest precedence, which means that (p is a predicate, a and b are step expressions, n is a name if an element of the sequence s and i and j are step expressions, and * is any of the operators || ; or |)

while p do a * b

is equivalent to

(while p do a) * b

and

with n in s * i * j

is equivalent to

(with n in s * i) * j

TODO: Have tooling do syntax trees for behaviour expressions.

5. Runtime

5.1. The Lifecycle of a Step

At runtime, the individual state of a step is tracked like this:

digraph step { label = "states of each step, where T is current time" node [shape=point] born node [shape=doublecircle] completed node [shape=ellipse] executed node [shape=circle] born -> waiting waiting -> notready [label = "no entry or T >= entry"] notready -> ready [label = "guard = true"] ready -> notready [label = "guard = false"] ready -> executing [label = "guard = true, automatic or by user"] ready -> completed [label = "guard = true\nno behaviour\nsuccess = true"] executing -> executed [label = "behaviour\nsucceeds"] executing -> failed [label = "behaviour\nfails"] failed -> executing [label = "guard = true\nretry by user"] executed -> executing [label = "guard = true\nsuccess = false\nretry by user"] executed -> completed [label = "success = true, committal by user or automatically"] }

5.2. Permission to Execute

Note, that before any end-user interaction, it is verified that the currently logged in user has execute permission for the entity that owns the step - written as may execute in the diagram. Also note, that the execute permission is not required for the end-user to simply commit a change that an automatically executed step has produced.

The net effect is, that the declared behaviour of an entity is followed regardless of what permissions users may or may not have, and the execute permission specifically controls access to executing steps that by declaration require user interaction.

5.3. Persisted Step Lifecycle

An entity that declares behaviour stores child items that track the evaluation of the behaviour expression and keeps the status of all steps until they complete. When an individual step completes, it is not considered "live" any longer and the item that tracks it is deleted.

The behavioural status of the entity itself is computed from the set of live step states by evaluating the below expressions from top to bottom:

live-steps.all(completed) =>  completed
live-steps.any(failed)    =>  failed
live-steps.all(waiting)   =>  waiting
true                      =>  active

Suggestion: The states of the live steps are updated at the start and just before the committal of every transaction for all entities that are read or modified in that transactiom, and they are persisted as part of the committal.


5.4. Transactions and concurrent || steps

When two or more steps are declared by the behaviour expression to execute concurrently, they may be executed and committed successfully at overlapping times on different devices, but only if no conflict arises when each transaction is committed.

Transaction committal is always serialized, even if the changes that are part of the concurrent transactions were executed truly in parallel across different devices. While committal does permit other transactions to have committed since the transaction was started, each committal insists that no entity that was read as part of the transaction has since been modified by another committed transaction. Such strict semantics is sometimes referred to as read-level consistency.

In practice, this means that when designing concurrent steps, care must be taken to ensure that none of the steps will modify any entity that is also read by another of the concurrent steps.

Note that even if an entity, A, has a member defined by an expression that depends on a value of another entity, B, that value of B may be modified in a transaction without A being considered modified.

For example, the RollDice design can update the Score entities of a Game independently in parallel, because the ending logic of Game is declared using an expression, rather than as a data member being explicitly updated as part of updating each Score.


Suggestion: Change the runtime to pick the optimal execution instead of leaving it up to the designer

Note that, if the UI displays (and therefore must read) more than a single score, a refresh is needed between any two tosses on different devices. The UI runtime is responsible for adopting a pragmatic approach that issues enough, but not too many, such refreshes to reduce the risk of concurrency collissions, which may cancel user requests such as activating a step button or throw away recently entered data, whilst maintaining efficiency.

Also, these update semantics mean that internally the dbquity runtime deploys a child item holding the behavioural status, such that this behaviour item can be updated and persisted without affecting the state of the entity itself - even in cases where the entity declares an expression that depends on the value of this built-in behaviour item.


As a dbquity designer, consider these two alternative behaviour expressions for the Toss step:

getoradd(Score, Player: player).set(Amount: ...)

and

getoradd(Score: player).set(Amount: ...)

Because Toss declares identity: Player, they have the exact same semantics when executed in isolation, but because

only the latter will not risk concurrency collision when two or more devices trigger Toss for different players at the same time.

6. User Interface

When showing an entity, the UI reflects the current status of behavior and steps.

Steps with behaviour, but without autoexecute, may - after entering the Ready state - be invoked by an end-user with execute permission, provided any declared guard is satisfied.

If a step has failed or did not yield success, it may be retried by a user, provided the guard is still satisfied – even if it was first automaticcally executed.

TODO: add more detail and screen mockups...

7. Temporal Constraints

In practice, the behaviour of a design is often constrained in time by certain agreements that the users may enter into such as a delivery deadline or a due date for payments, and as mentioned earlier, we now extend the model to address such temporal aspects.

7.1. An example of a Deadline

Looking again at the RollDice example, say you want each player to have a maximum of 5 minutes in each round of play to toss the die, and if at any point those 5 minutes are exceeded the game should time out.

This is simply achieved by adding a one-liner declaring a deadline to the Toss step:

desing RollDice
    ...
            step Toss
                parameter Player
                behaviour:
                    getoradd(Score: Player).
                    set(Amount:
                        if Amount < Game.Target then Amount + random(1,6)
                                                else Amount - random(1,6))
                autocommit
                deadline: ready.addminutes(5)   # waiting too long to toss, times out the game
            ...    

The deadline will surface in the UI. Below, we return to the example of the final round of a game with 5 players and a target of 30:

Players 3 and 5 are fastest as before - 3 is a little less lucky:

Players 1 and 2 toss in time:

But player 4 does not make it, so the step and consequently the whole game expires:

The timed out game is counted in the area:

7.2. The Full declaration of a step

In addition to the capability of declaratively handling deadlines requiring a given step to be succesfully executed before or at that point in time, we also introduce the logical opposite namely constraining the entry into a step, so that it cannot occur before a certain point in time.

With these, we complete the notion of a step by adding entry and deadline expressions to step declaration and consequently have the runtime deal with a state of expired.

step <name>
    [abstract]
    [base: <step>]
    [override: <step>]
    [parameter <name>]*
    [guard: <predicate>]
    [entry: <time-expression>]      # adds another clause joining the guard predicate
    [autoexecute]
    [behaviour: <behaviour-expression>]
    [autocommit]
    [success: <predicate>]
    [deadline: <time-expression>]   # adds the notion of expiry

where
    predicates, expressions and behaviour may refer to
     - the members of the declaring entity and any linked or linking entities
     - the point in time, creation, when the step instance was created
     - the point in time, ready, when the step became ready
     - the current evaluation of the entry and deadline expressions
     - the current evaluation of success predicate and
     - the index runtime value, if declared

7.3. The Full state diagram of a step

digraph step { label = "states of each step, where T is current time" node [shape=point] born node [shape=doublecircle] succeeded expired node [shape=ellipse] executed node [shape=circle] born -> notready notready -> ready [label = "T >= entry and T <= deadline\nguard = true"] ready -> notready [label = "T <= deadline\nguard = false"] ready -> executing [label = "T <= deadline\nguard = true\nautomatic or\n if may execute\nby user"] ready -> succeeded [label = "T <= deadline\nguard = true\nno behaviour\nsuccess = true"] {notready,ready,executing,executed} -> expired [label = "T > deadline"] executing -> executed [label = "T <= deadline\nbehaviour\nsucceeds"] executing -> failed [label = "T <= deadline\nbehaviour\nfails"] failed -> executing [label = "T <= deadline\nguard = true\nmay execute\nretry by user"] executed -> executing [label = "T <= deadline\nguard = true\nsuccess = false\nmay execute\nretry by user"] executed -> succeeded [label = "T <= deadline\nsuccess = true\ncommittal by user\nor automatically"] }

7.4. Runtime Consequences

The expired state introduced for steps above also adds the second case below to the full scheme of assessing overall behavioural status of an entity

live-steps.all(completed) =>  completed
live-steps.any(expired)   =>  expired
live-steps.any(failed)    =>  failed
true                      =>  active

Further, in addition to the transactions initiated by user activity, necessary transactions scheduled according to the entry and deadline declared by live steps will run in the background provided a user is connected to the site of the entities in question at or after the scheduled time. This scheduling occurs by persisting a link to the entity and the relevant time, whenever an entity with live steps that declare entry and/or deadline are in scope of a transaction. These links are ordered timewise, such that the head of the list can be effectively queried yielding the next point in time, when a background transaction should take place.

TODO: Investigate if this mechanism is sufficiently robust to entry and deadline expressions that change what they evaluate to after the entry to the step. E.g., if they depend on an aggregating expression on the entity.

8. More Examples

8.1. Tossing dice again - Variations

8.1.1. Store Outcome of each round

site RollDiceWithOutcomes
    publishedat: model.dbquity.com/examples/behaviour
    version: 0.9.0
    area Table
        integer Players
            default: 3
        integer Target
            default: 13
        step NewGame
            guard: Players > 1 and Players < 10 and Target > 6 and Target < 36
            behaviour: add(Game, Players: Players, Target: Target)
            autocommit
        entity Game
            collection: Games
            readonly
            integer Players
            integer Target
            behaviour:
                while not Scores.any(Amount = Target) do
                    with p in 1..Players || Toss(p)
            step Toss
                parameter Player
                behaviour:          # add outcome, then update score
                    getoradd(Score: Player).add(Outcome);
                    Scores.find(Player).set(
                        Amount: if Amount < Target then Amount + Outcomes.last().Value 
                                                   else Amount - Outcomes.last().Value)
                autocommit
            entity Score
                collection: Scores
                integer Player
                integer Amount
                identity: Player
                entity Outcome      # random face value of die
                    collection: Outcomes
                    integer Value   
                        initialization: random(1,6)

8.1.2. Let player Toss or Enter face value

site RollDiceWithChoice
    publishedat: model.dbquity.com/examples/behaviour
    version: 0.9.0
    area Table
        integer Players
            default: 3
        integer Target
            default: 13
        step NewGame
            guard: Players > 1 and Players < 10 and Target > 6 and Target < 36
            behaviour: add(Game, Players: Players, Target: Target)
            autocommit
        entity Game
            collection: Games
            readonly
            integer Players
            integer Target
            behaviour:                  # players choose how to toss
                while not Scores.any(Amount = Target) do
                    with p in 1..Players || (Toss(p) | TossManually(p))
            step Toss
                parameter Player
                behaviour:
                    getoradd(Score: Player).set(Amount:
                        if Amount < Target then Amount + random(1,6)
                        else Amount - random(1,6))
                autocommit
            entity Score
                collection: Scores
                integer Player
                integer Amount
                identity: Player
                integer FaceValue
                    readonly: false
            step TossManually
                parameter Player
                guard: Scores.find(Player).FaceValue in 1..6
                behaviour:              # update by entered face value
                    getoradd(Score: Player).set(
                        Amount:
                            if Amount < Target then Amount + FaceValue
                            else Amount - FaceValue,
                        FaceValue: nil) # reset to not autoexecute in next round
                autoexecute             # execute on entry of FaceValue
                autocommit

8.1.3. Make each User a Player

site RollDiceWithUsers
    publishedat: model.dbquity.com/examples/behaviour
    version: 0.9.0
    area Table
        step NewGame
            guard:
                "2 or more users with 'Player' profile required" ~
                Player.usercount() > 1
            behaviour: add(Game)
            autocommit
        permissions:
            view by *
            execute by index as *
        entity Game
            collection: Games
            readonly
            behaviour:
                while not Players.any(Score = 30) do
                    with p in Players || Toss(p)
            step Toss
                parameter Player
                behaviour:
                    getoradd(Player: Player).set(Score:
                        if Score < 30 then Score + random(1,6)
                                      else Score - random(1,6))
                autocommit
            entity Player
                defaulted
                profile
                collection: Players
                integer Score
                text Name               # same identity as user
                    scope: Player.usernames()
                identity: Name

8.1.4. Dismiss any User that waits too long to toss

site RollDiceWithDismiss
    publishedat: model.dbquity.com/examples/behaviour
    version: 0.9.0
    area Table
        integer Players
            default: 3
        integer Target
            default: 13
        step NewGame                    # no autoexecute => user must trigger
            guard: Players > 1 and Players < 10 and Target > 6 and Target < 36
            behaviour: add(Game, Players: Players, Target: Target)
            autocommit                  # make Game available on other devices
        entity Game
            collection: Games
            readonly
            integer Players
            integer Target
            behaviour:                  # each round, players toss concurrently
                while Scores.count(Dismissed) < Players and not Scores.any(Amount = Target) do
                    with p in 1..Players || (Toss(p) | Dismiss(p))
            step Toss
                parameter Player
                behaviour:      # user triggered, lazy-create Score, random update
                    getoradd(Score: Player).set(Amount:
                        if Amount < Target then Amount + random(1,6)
                                           else Amount - random(1,6))
                autocommit              # persist making others see the toss
            step Dismiss
                parameter Player
                entry: creation.addseconds(if Scores.find(Player).Dismissed then 0 else 20)
                behaviour: getoradd(Score: Player).set(Dismissed: true)
                autoexecute
                autocommit
            entity Score
                collection: Scores
                boolean Dismissed
                integer Player
                integer Amount
                identity: Player

8.2. TODO: A Sales Order...