Data Flow Semantics Tech Note - modelint/shlaer-mellor-metamodel GitHub Wiki

mint.sm-meta.action.tn.3 / Leon Starr / Version 1.0.0

A complete, executable Shlaer-Mellor domain is modeled in three facets: Class Model, State Models and Actions.

When we talk about Actions we typically think about the computational text written inside of a State in its Activity compartment. Here is one such example taken from the Air Traffic Controller state machine in the book Models to Code (MTC).

The example above is a State Activity and there are other possible uses of action language such as a Class Method Activity or External Entity Operation. In each case we have an Activity that may contain any number of Actions, including none at all. A State, for example, might simply wait and do nothing in which case its Activity is empty.

The action language is written in Scrall (an improved version since the MTC edition example). I will assume you are unfamiliar with it and explain the syntax as we proceed. The numbers just to the left of the state will serve as reference points for our discussion. Feel free to re-imagine the text in whatever action language you currently use. The syntax of any particular action language will quickly become irrelevant though since we are going to focus on the underlying execution and data flow semantics.

Key questions

In other words, we want to answer a few broad questions:

  • What exactly is an Action? For example: How many Actions are in our example state activity? What are they?
  • In what order must they execute? We want the execution order required on all platforms, not some arbitrary ordering.
  • How do Actions communicate and coordinate with one another?

In this technical note we won't come up with formal answers to these questions, but we hope to explore the questions as a precursor to incorporating a formal solution in our Action Language Metamodel.

Let's begin by understanding what's going on in our Logging In State Activity.

(1)

my station .= Duty Station( Number: in.station )

Here, an instance set variable named my station is assigned a reference to an instance of the Duty Station class found by selecting the instance whose Number attribute holds the value provided by the input parameter station seen on the incoming transition. On the class diagram (not shown yet) note that Number is an Identifier on the Duty Station class. So, we know that zero or one instance will be selected. For the purposes of our example we will assume that our instance population is fixed at compile time so we don't need to worry about not finding the instance. So we assume that a single instance will always be selected.

(2)

>> On Duty Controller(Time logged in: Date.Now ymd hm tz) &R3 my station

There's a lot to unpack in this statement (as is often the case in Scrall!). By the way, we will use the term 'statement' to refer to a line of action language. What constitutes a statement varies by action language and only has relevance when we express actions using a text notation. So the statement concept will NOT be part of our metamodel semantics.

The ‘>>’ symbol means ‘migrate to’ aka reclassify. The local Air Traffic Controller instance, currently subclassed as an Off Duty Controller, will migrate over to the On Duty Controller subclass.

But not so fast! We will break relational integrity if we violate certain constraints laid down on our class diagram. Let's take a look at the relevant portion:

Looking at the class model we see that two integrity constraints must be satisfied as part of this migration. Since each attribute of a class must have a meaningful value upon creation, we need to supply a value for the Time logged in attribute. And since an On Duty Controller is required to be logged into a Duty Station, we must link to an instance of Duty Station on R3 using the my station variable previously assigned. In fact, if we simply ensure that each attribute in the destination subclass has a value, the R1 and R3 relationships are automatically ensured since they are each formalized by local referential attributes.

The parenthesized clause in our action language statement obtains the current date and time value and supplies it for the Time logged in attribute. The Date.Now hms part takes the default value of the Date data type. In Scrall, each data type (we just call them types actually) each type returns a default value. So if the default value of the Integer type is 0, n = Integer will set n to 0. But we also apply the Now hms operation defined on the Date type which gives us a nice timestamp value.

To get the referential attribute for R3, On Duty Controller.Station, we use the &R3 action. In Scrall we don't need to manipulate the referential attributes directly, but we assume that the &<Rnum> action will result in setting them correctly. Oh, and the >> operator, which effects the migration, is understood to set the ID referential attribute so that it references the superclass instance.

(3)

Logged in -> me
In use -> my station

The final two statements each generate a signal. The Logged in signal is addressed to the local instance (me) while the In use signal is addressed to the my station instanced assigned in statement (1).

Now, back to our questions

To answer our three initial questions, we need a more helpful representation of the actions and their data dependencies. This is best illustrated with a data flow diagram. Let's work our way, statement by statement, and see what we get.

(1) my station .= Duty Station( Number: in.station )

Two things are going on here. On the right hand side (RHS), there is a selection of a set of instance references based on a query and on the left hand side (LHS) there is an assignment to a temporary instance set variable.

We'll propose a standard Action named Instance filter and illustrate its data inputs and outputs:

We're making up some graphical notation as we go along. Our goal is not to invent a usable data flow diagram notation for expressing actions. Our goal is to understand the underlying data flow semantics so that we can build a useful action language metamodel. Consequently, we won't be putting much thought into the graphical symbols other than to make their meanings clear and to be as precise as we can.

We'll represent a modeled Class using a blue DFD store symbol with a thick boundary.

We'll represent a flow of instance references with double hollow arrows. We can use a single hollow arrow to indicate a Single Instance Flow which conveys zero or one instance reference.

A scalar (non-relational) value is represented with a solid arrow.

And metamodel information such as a relationship number or an attribute name will flow with an open stick arrow.

Also we will use italicized green text to represent data conducted through a flow for a specific example.

With all that said, we can interpret our diagram as extracting the instance population of the Duty Station class, applying filtering criteria that includes the Duty Station.Number attribute and the incoming station event parameter and yielding a Single Instance Flow in this case because, as previously indicated, our criteria attribute is an identifier.

We might ask, "Why aren't we supplying the criteria as a predicate such as: Duty Station.Number == station ??"

We don't because the subject matter required to build a logical comparison predicate is outside our metamodel domain. Our goal is simply to identify what inputs are required and then delegate the predicate assembly to a service domain. We can then maintain an implicit correspondence (bridge) between our filter action and the assembled predicate along with implicit correspondences between our criteria input and predicate tokens. This also avoids the problem of having our metamodel impose a predicate specification syntax on action language designers.

Next up:

>> On Duty Controller(Time logged in: Date.Now ymd hm tz) &R3 my station

To migrate a subclass instance we need to do the following:

  1. Delete the old subclass instance
  2. Create the new subclass instance
  3. Ensure all attributes are set to the correct values during creation

We can break this down into a delete and a create action. Delete seems like the low hanging fruit so let's start there:

We simply feed the Delete action with an Instance Flow and those instances get deleted. There is no flow exiting the action since no data is yielded by it. The incoming flow tells us which Class to access and what instances to delete, so no other inputs are required.

But how do we produce that incoming flow? Remember that our local instance is the superclass. So we need to traverse the generalization, get a reference to the old subclass instance and then delete it. You don't see this in the Scrall action language since it is built into the migrate action. But we're spelling it all out here, so we'll need to perform the traversal and selection explicitly. Were we to write it in Scrall, it would look like this:

!* /R1/Off Duty Class // Delete the instance of Off Duty Class related to me across R1

Graphically, we can introduce the navigate action and feed its output into our delete action.

Now let's tackle the create action. For the moment we'll focus on the action and the data it needs and afterward we'll figure out how to produce that data.

To create an instance (of a Class) we really only need two things: The name of the class (shown as incoming metamodel information) and a value for EACH of its Attributes. In the relational, not to be confused with database, world, 'null value' is an oxymoron.

From the class diagram we can see that we must supply an ID, Time logged in and a Station value for our new subclass instance. We don't use the solid arrow scalar notation since what we have here is a row of a table and, hence, a table value, aka 'relation'. The header of the table has a column for each attribute name and type with a single row in the body of the table holding a value for each of the attributes. We'll use the fork notation to indicate the flow of a table value.

The create action yields a single instance reference which may or may not be of use elsewhere in our Activity. Note that the instance reference is actually a table value as well, but we hold instance references in a special regard and thus the special hollow arrow notation. It is helpful to remember that instances are just a special usage of a table value.

Now let's focus on the source of those initial attribute values.

We need to make a single column table value whose header is the name of the referential attribute in the On Duty Controller subclass which is: Station. It refers to Duty Station.Number. Given a simple association (one without an association class) and an instance reference representing the instance that is being referenced, along with information available in our class subsystem metamodel that maps a referring attribute name in one class to a referenced attribute in another class, we have all the information we need to devise a fundamental action to do the work. Here it is:

One down, two to go. Let's try a similar trick to obtain the referential attribute value referring to the superclass. We'll need an action that takes as input a generalization relationship, superclass instance reference and a choice of subclass. It will then output the value of the referential attribute(s) necessary to formalize the relationship. And here it is:

Finally, we need to obtain a value for our Time logged in attribute. We've already describe the process by which we obtain the data value, so we just need to represented it as an action.

In fact, we need two Actions! The one on the left produces a scalar value. But we need to create a table value with the Time logged in attribute as its header. So we add another action which binds the two together to yield our table value.

We can knit all of these pieces together to fully describe our subclass migration. So this statement:

>> On Duty Controller(Time logged in: Date.Now hms) &R3 my station

Is represented like this:

Note that the table value flows are combined into a single table value as indicated by the little white box where they merge. It performs some relational magic behind the scenes.

Having recoiled in horror at all of the tedium required to perform a simple migration and knowing that this is a such a common action, it sure would be nice to subsume all of this into a tidier package. And we will! But before we do that let's continue with the remainder of our Activity (we still have two signals to generate) and ponder our big question about execution order first. Then we'll do some cleanup.

Here are those two signal generation statements:

Logged in -> me
In use -> my station

For each of these we can use a simple signal generation action like so:

And here is the complete Activity:

So now let's assess the essential execution order in view of one simple rule.

Any process can execute as soon as all of its inputs are available.

We just need to clarify what we mean by an 'available input'.

A data input (instance, scalar or table flow) is available as soon as it is assigned a value. Some data inputs are considered to be available at all times. These are:

  • Class populations and their attribute values
  • Input parameters (any data passed into a method, operation or state activity)
  • Type operations (obtaining a default or selected value, e.g. Time.Now(), Rational.Pi, and so forth)

These 'always available' inputs are depicted as an arrow or action with no source.

All other data inputs emanate from some action within our activity and are not considered available until that action has completed execution.

We can now apply this logic to work out the platform independent execution order of our Activity. We can annotate our Activity DFD with a numeric sequence as shown below:

All of the one's can execute immediately, in any order. We can see that the order doesn't matter since there are no data dependencies among those actions.

Any two action can execute as soon as it's related one(s) have completed. And, finally, there's a three in there which must execute after its three input actions have completed.

Note that the three action isn't necessarily executed last. The lone signal generation, for example, might be executed last in a given implementation. Regardless, the three action can't execute until all three of its inputs are available.

Thus, execution order is based on data dependency only. Any given implementation must respect that sequence, but may otherwise shuffle the ordering around using concurrency or not as appropriate to the target platform.

There's a bit more to the execution order, though. There are times when the data dependency is mediated by a change to data in the class model. Imagine that you have two actions Update location which writes a couple of attributes based on supplied input values and Compute range which is a method defined on the Rover class. Update location updates a couple of attributes. Update location needs to operate on this new data to compute a range value so it must wait for the upstream action to complete, but all the data the downstream action needs is in the class model. But there is no visible 'stream'! How does the Compute range action know when the Update location action has finished its update? The downstream action doesn't need any specific data from Update location so there is no need for a data flow (scalar, table or instance reference) between them.

As shown above, we introduce the concept of a control flow. Unlike a data flow, no data at all is sent. The flow simply communicates a status of [ Ready | Not Ready ]. You could argue that it should send a boolean value named Ready which is either true or false, but the downstream action doesn't access any value. It just waits for the state to change so I would argue that setting a boolean variable that can be accessed transparently in the action language is confusing and unnecessary.

This is how we might express the concept in Scrall:

the rover.(Destination, Speed) = (new dest, new speed) <1>
<1> the rover.Compute range()

Scrall represents the dashed arrow with a named sequence token. In this case 1 is the name, but it could have just as easily been named update completed. A sequence token on the right is considered set when its associated action completes while a sequence token on the left indicates an action that may not start until the setting action completes.

Modelers note: If you find you are writing actions with a lot of control dependencies, you probably just need to refactor and use more states!

We also have the situation where a decision is made in some action, be it a simple true false decision or a switch action considering multiple cases. We can use a cross bar notation at the root of a control flow branch to indicate mutual exclusion as shown below:

Again, note that no data is being sent on these flows. In a mutual exclusion control branch only one will be set to ready status. Once that happens all other branches and any downstream actions on those branches will be marked as complete (without ever executing).

There is also the possibility of using a data flow to output a boolean value. The action taking that boolean as input can then test it and react accordingly. In this scenario a data flow rather than a control flow is used. Here is an example:

The first action compares two values and flows a scalar true typed as boolean which happens to be true. The Too low? action issues a Pull up control signal if it receives a true value. The false case does not issue a control signal since no action needs to be taken. Even though there is only one possible action result, we must still mark the control flow with a cross bar. This tells us that if the Pull up condition is not selected, the control flow expires. This keeps us Send warning action from waiting for a ready state in the control flow if the Too low value is false.

At this point we've explored two of the big questions we introduced at the beginning of this note. We have an idea of how actions communicate (via typed scalar flows, table flows and instance flows which are just a special case of table flows). They also coordinate using control flows when data dependency can't be established by data flows alone. We now put our focus on how to define an action.

What is an action, really?

As an exercise, let's revisit that subclass migration DFD and see if we can define a general purpose subclass migration action. We take the complete activity from our `Logging in' state and draw a box around all the actions relevant to the migration task. Then we summarize the necessary inputs and outputs and we come up with this:

Now we recast our activity with the details of migration buried inside our new Migrate subclass instance action.

Some observations about what we are calling 'actions'.

  1. There appears to be a finite set of fundamental actions such as signal or instance filter.
  2. Composite actions can be built up from these fundamental actions to define commonly useful tasks, such as subclass migration
  3. User actions such as class methods, computations, cases, tests and so forth are also necessary components.

But what is the finite set of fundamental actions? We're not going to answer that in this technical note, but we do need to propose a set. The point is that they exist and need to be identified.

The subclass migration example seems to suggest that we want the capability to create hierarchical data flow diagram. Incorrect!!! It violates the Shlaer-Mellor principle of prohibiting arbitrary leveling. You don't have domains inside of domains, classes inside of classes or states inside of states. This characteristic is strategic and are key ensuring the 'ham sandwich' principle I proposed ages ago. It's the idea that two modelers working with the same requirements and information should converge on the same optimal model structure. As soon as you let modelers create arbitrary hierarchies (everybody will stack the hierarchy based on their own intuition and daily mood), uniformity goes out the window.

I would argue instead that while the migration example is built from a set of primitive actions, the end result is itself a standard metamodel defined action. User compositions are forbidden, but system supplied compositions are okay.

⚠️ **GitHub.com Fallback** ⚠️