Table variables

A table variable holds a value referred to in relational theory as a relation. It is defined as having a heading consisting of attribute:data type pairs with a body consisting of zero or more tuples with a value are supplied for each attribute constrained by the corresponding data type.

Here is an example relation shown as a table:

Pressure::PSI Temperature::DegC
2500.00 PSI 87.3 C
1009.98 PSI 32.3 C

Table variables are useful when selecting, updating, computing and otherwise manipulating class model data. Standard relational operators such as restrict, project, join, union, intersect, extend and subtract are supported in Scrall. These operations are all closed under relations so that the result of any relational operation is another relation. Nested relational expressions are then possible.

While a relation can be viewed as a table, it is more accurately defined with sets. We start with sets H and B representing the heading and body components. Set H is a set of zero or more named and typed attributes. In our example it is: H \{ Pressure : PSI, Temperature : Degrees celsius }. As this is a set, no ordering is implied and there can be no duplicates. A body is a set of zero or more tuples where each tuple holds a value corresponding to each attribute in the heading. Our example relation has this body: B { T { ( Pressure : 2500.0 ), ( Temperature : 87.3 ) }, T { ( Temperature : 32.9 ), ( Pressure : 1002.98 ) } }.

Note that the above ordering doesn’t appear to match the table layout. That’s because sets are not ordered, thus the pairing of attribute and value names. The table representation is only a helpful visualization of the sets in a relation. The implication that there is a row and column order is a misleading artifact of the table view. Always remember that the uglier set definition is the true representation and that a table is just a prettier way of presenting the data in a relation.

That said, we will use the term “table value” to be synonymous with relation for ease of discussion. But remember that row and column order has no modeling significance.

Implicit table declaration

Table values are assigned using the table assignment operator #=. The hashtag symbol was chosen because it sort of looks like a table.

tanks #= Tank( Temperature > in.Max temp ).( ID, Pressure,  Temperature )

In this example all ID, Pressure and Temperature values for Tank objects with a temperature above the supplied in.Max temp are assigned as a table value in the tanks variable. As a result the value of tanks could be something like:

H { ID : Nominal, Pressure : PSI, Temperature : Degrees celsius }, B { T { ( ID : 12 ), ( Pressure : 2500.0 ), ( Temperature : 87.3 ) }, T { ( Temperature : 32.3 ), ( ID : 22 ), ( Pressure : 1002.98 ) } }

This is, a lot easier to visualize as a table:

ID::Nominal Pressure::PSI Temperature::DegC
12 2500.00 PSI 87.3 C
22 1009.98 PSI 32.3 C

Here a single attribute is extracted from a related class:

dogs owner #= /R7/Owner.Name  // across to-one relationship
Name::Person Name
Elly Swanson
Lars Eriksson
Jessie Patel

Below, all the attributes of the Dog class are selected using the * operator for the Dog Owner instance Elly Swanson.

my dogs data #= /R7/Dog.( * ) // all data about all related dogs
Name::Dog Name Date of Birth::Date Breed::Breed Owner::Person Name
Milo 4-6-15 Collie Elly Swanson
Trixie 7-3-16 Golden Retriever Elly Swanson

In the example above, all of the attributes are selected since the * all attributes symbol is specified.

A relation value can also be constructed from scalar variables.

aicraft speed #= { Tail number: my tail number, Airspeed: my airspeed }
ID::Tail Number Airspeed::Knots
N31742Q 217 Kn

You can assign an empty table with no columns and no rows to any table variable by assigning the empty set ~ symbol:

dogs owner #= ~  // Now the value is table dum:  H{}, B{}

The utility of an empty table value is explained below.

Single tuple assignment

Sometimes it is essential that the LHS have a cardinality of one. This can be enforced with the single tuple assignment operator #1=.

my aircraft #1= /is piloting/Aircraft.(*)

A table is constructed from all Aircraft attributes and, since the association leads to a single instance, there will be at most one row in the table. A fatal error occurs if many rows could be created (the association leads to a many multiplicity).

True and false table values

An empty heading and an empty body is known as table dum and can be interpreted as false:

H {}, B {}

Let’s say we have an Aircraft class and we want to know if any instance of Aircraft is flying below some floor altitude, say 300 m.

low aircraft #= Aircraft(Altitude < floor altitude).()

The expression above selects all aircraft below the floor, and specifies no attributes to be returned in the second set of parenthesis. If there are no aircraft found, a table with no columns and no rows will be returned. In other words, false.

Now let’s say that several low flying aircraft are found. But, again, no attributes have been selected. So for each low flying aircraft, the tuple T {} is returned. Since these are all duplicates and duplicates are not allowed in a set, the duplicates are discarded to return the following relation:

H {}, B { T {} }

Here we have zero columns and a single empty tuple. This is called table dee which can be interpreted as true.

So you can do this:

low aircraft #= Aircraft(Altitude < floor altitude).()
if (low aircraft) { ...

You can also convert a class selection into a table value by putting a # operator in front of the class name:

if ( #Aircraft(Altitude < floor altitude).() ) { ...

But you don’t need to do the conversion since an empty instance set is also interpreted as false. So this does the same:

if ( Aircraft(Altitude < floor altitude).() ) { ...

Table variable operations

The following operations are defined for relation values: restrict, project, join, intersect, union, subtract. They are all closed under relations which means that each operation yields a new relation.

t #= a // assign content of a to t
t #1= c // content of c must be 0 or 1 row
t #= a #^ b // intersect
t #= a #+ b // union
t #= a #- b // subtract
t #= a #-- b // symmetric difference
t #= a( attr1 > max attr1 ) // restrict
t #= a( * ).( attr1, attr2 ) // project

all aircraft table #= Aircraft(*).()

#<, #<_, #>, #_> operators are proper subset, subset, proper superset, superset
if (t1 #< t2) {...} // t1 is a proper subset of t2

?<var> to get the cardinality (quantity) of tuples
?0<var> true if zero tuple in var
?1<var> true if one tuple in var

non arrivals #= ( Aircraft.( Tail number ) #* Airport.( Code ) ) #- Arrival.(Aircraft as Tail number, Airport as Code )

More about the relational image and extend operator to be added here

Scalar extraction

It is possible to extract one or more scalar values from a table variable as long as certain conditions are met.

aircraft data #1= /R1/Aircraft().(*) // Get everything related
my alt, my heading = aircraft data().(Altitude, Heading)

The first line traverses R1 specifying no criteria (all instances) and inclusion of all attributes. Since the association direction is toward a one, unconditional association (1), exactly one instance will be assigned.

In the next line we use the = assignment operator to extract two scalar values. The variable and attribute ordering must match to get the values in the right place. After the assignment the type of the my alt and my heading variables will match the type of Aircraft.Altitude and Aircraft.Heading. Since we traversed a to one unconditional association, the table variable must hold a relation with exactly one Aircraft tuple. But what if the association were conditional?

available runway #= get landing runway()
if (available runway)
   &/R3/land on/ Runway( ID: available runway id().(ID) )

In this case the local method get landing runway() will return either a single attribute relation with a single tuple holding a runway id or an empty body with no tuple.

If the result is not empty/false (table dum) then the Runway to land is linked. The link action must select an instance of Runway using a scalar value to match the Runway.ID. So it is necessary to unpack that value just after ID:.

You wouldn’t have to unpack if you just had the get landing runway() method return a scalar value in the first place. But the nice thing about the table value is that it can be tested for empty unlike a scalar value which must be accompanied by a success/failure status or include a special value in its range.

So to make things easier, we have special rules for unpacking a single tuple into scalars. You can, in fact just do this:

available runway #= get landing runway()
if (available runway)
   &/R3/land on/ Runway( ID: available runway )

Scrall knows that a scalar is expected for the ID attribute. Further, it is assumed that the table variable has exactly one tuple. If there is an attribute named “ID” in the relation, its value will be unpacked automatically.

Renaming attributes

If not, it will be necessary to do the unpacking yourself, or rename the attribute. Let’s say that the get landing runway() method returns an attribute named Runway ID. Then you can use the rename operator >> to change the attribute name in the relation.

available runway #= (Runway ID >> ID) get landing runway()