6.2.6. Some cases
6.2.6.1. Context- and role individuals
The query language permits indexed context individuals, such as sys:MySystem
, and indexed role individuals, such as sys:Me
. These should be understood as constant functions. Whatever their argument, they always return the same result. This even extends to the type of their argument. In this example:
user X = filter SomeUser with FirstName == sys:Me >> FirstName
the domain of sys:Me
is role SomeUser, while in the next example (from model://perspectives.domains#System
):
domain model://perspectives.domains#System state FirstInstallation = (callExternal util:SystemParameter( "IsFirstInstallation" ) returns Boolean) and (exists sys:TheWorld >> PerspectivesUsers)
the domain of sys:TheWorld is the context type domain model://perspectives.domains#System
itself.
How to invert such a query step? In other words, when, on trailing the query backwards, we have reached such an indexed individual, how should we proceed? Consider the second example where, if a new instance of PerspectivesUsers
has been added, the first backwards step will take us to sys:TheWorld
. What then, is the next step? The thing is, we don’t know which individual we should go to (the example is somewhat misleading because, obviously, there is only one instance of the type domain model://perspectives.domains#System
in any installation. But that is beside the point: in the general case, there may be many instances of the type that the original forward step - a context individual constant function, in this case - has as domain).
The only thing we have on offer is to retrieve all instances of that domain type. And this is what we do, employing the function ExternalCoreContextGetter
with the context type. Similar, we use ExternalCoreRoleGetter
in the case of a role domain.
There is one more subtlety to discuss.
Inverted queries are 'kinked' at all junctions, to produce n kinked queries from an original of n steps. We store such kinked queries as a 'detection system' with the successive types that are visited by the query. However, storing the inversion of a context- or role individual runs into a problem and that is that just as the domain of a constant function may be a context- or a role type, the range of its inversion can be either of these, too. Now compare an 'ordinary' context step with the ExternalCoreContextGetter
step. The first will, by construction, always have a role type domain and that is where we store the inverted query (in the member contextInvertedQueries of the EnumeratedRole representation). But the second might as well have a context type domain! So where do we store?
Luckily, there is a nice way out. It so happens that, by definition, no instances of indexed individuals are ever constructed except on installation of a model. So what is the use of setting up a creation detection system? We can simply ignore the kinked version of a query with an role or context individual step, whose backward path starts at that step. This does not mean that the inversion itself has no use. It may very well be a step somewhere inside the backwards part of another kinked variant. Just not as the first step.
6.2.6.2. Variables
letE and letA expressions introduce variables. Furthermore, in calculated properties the variable object is automatically bound to the current object set and in calculated roles we have the variable currentcontext. How should we treat an expression using, for example, this object variable? Consider:
perspective on: SomeRole
on entry
bind object >> filler to AnotherRole
If we invert the sub-expression between bind and to, we should get
filled role SomeRole >> context
in order to arrive at the context of this rule from the role (whatever it is) that is being fill it. Explanation:
-
the filler step inverts to filled role SomeRole. SomeRole, because that is the type of the object of the perspective (it is the type of the step object).
-
the object step itself inverts to context, because underlying the object variable is the expression SomeRole, evaluated in the current context. That is how we arrive at the value of object (the inverse of SomeRole is context).
This gives us a recipe for the general case in which a variable is bound to an arbitrary expression. Substitute the inverted expression that defines the variable into the syntactical location occupied by the variable.
So while we invert queries, we add fillers to the compile time environment. Because the same variable name can be re-used arbitrarily often, we push a compile time frame before each block.
In the perspectives language, we can use LetE and LetA. This translates to a QueryFunctionDescription with function name WithFrame. The query inversion code pushes a frame as it encounters this instruction. The variable fillers that follow, lead to additional fillers in this frame. Finally the expression (or statements) in the body of the LetE or LetA are inverted in this environment.
Can we look up the variables, in compile time?
In compile time, we store with the name of a variable a description of a function that will compute its value (an instance of QueryFunctionDescription): a compile time variable filler. A variable has a limited visibility; we will call the area of Perspectives Language code where we can refer to the variable, its scope. There are two scopes we have to consider:
-
the condition of a state. It is the scope of the object variable.
-
the letE or letA expression. Each filler (from left to right or top to bottom) introduces a new scope: for the rest of the expression (i.e. the rest of the fillers and the body).
Scopes may be nested. We keep, in the state of the compiler, a stack of Environments to reflect that recursive structure. An Environment is a collection of compile time variable fillers. We introduce, in our Purescript code, a new Environment with the function withFrame. The argument to withFrame is a computation with state in which we save variables and their (compile time) filler.
This makes it as if we can read the Purescript code as a lexical Perspectives Language scope: the computation (Purescript) corresponds to a particular scope (PL).
It so happens that we invert all queries that can hold variables exactly in the withFrame computations that hold their definition, meaning we have all variables in scope: we can actually look them up and find their QueryFunctionDescription.
6.2.6.3. Treatment of properties
Consider a somewhat degenerated Calculated property:
property P1 = P2
We should invert this expression, for two reasons:
-
if P2 changes, every user with a perspective on P1 should be informed (synchronisation);
-
if P2 changes, P1 changes and it might be (part of) the condition of a rule somewhere.
So how do we go about it? The update function that actually changes the value of property P2 on a role, obviously has access to that role. We do not need to trace a path back from the property value to the role; property values are represented on role instances. In other words, to move from a Value to a Role is a no-op. On inverting queries, we represent this operation explicitly, because it carries type information:
Value2Role Propertytype
But an inverted query should yield contexts, not roles. Hence, for the update function to find the context in which a property has changed from the role on which it is represented, the no-op is insufficient. It needs to be followed by the context step. So, on inverting a calculated property, we postfix the context step on the inversion of the expression.
6.2.6.4. Functions that operate on values
Consider:
thing: SomeRole
property Sum = Prop1 + context >> AnotherRole >> Prop2
Can we invert that? We’ve seen above how we invert an expression that consists of just a single Property, so that deals with the first operand. If we invert the second operand, we get:
Value2Role Prop2 >> context >> SomeRole
Why SomeRole? Because the property is defined on it. Visualise the original query path, as it moves from SomeRole to its context, then to AnotherRole and then to Prop2. Moving back, we start with the no-op Value2Role (‘arriving’ at AnotherRole), then we move to the context, and then we have to move back to SomeRole.
But we’re not done yet, because we need a context as the result. In fact, we’re in exactly the same position as with the simple property P1 defined in the previous paragraph. So the easy solution is to postfix the inversion with a context step:
Value2Role Prop2 >> context >> SomeRole >> context
It is glaringly obvious we could, alternatively, have removed the last step of the original inversion, too:
Value2Role Prop2 >> context
This is an implementation detail.
So we now have two inverted queries for our two operands:
Value2Role Prop1 >> context
Value2Role Prop2 >> context
The first will be used when Prop1 changes value; the second when Prop2 changes value. Both will return contexts of the same type.
And we’re done with that. The (+) function does not change anything: it does not ‘move’ over the underlying graph of context and role instances. The end result of the application of the function invertFunctionDescription (module Perspectives.Query.Inversion) is an instance of Paths, the representation of a series of query paths (see the previous chapter for an elaboration).
6.2.6.5. Join queries
We can join the result of two (role) queries:
property: Channel = (filled role Initiator union filled role ConnectedPartner) >> context >> extern >> ChannelDatabaseName
The sub-expression (filled role Initiator union filled role ConnectedPartner) has a Sum type.
We invert queries of this type by treating them as two separate queries:
filled role Initiator >> context >> extern >> ChannelDatabaseName
filled role ConnectedPartner >> context >> extern >> ChannelDatabaseName
Both can be simply inverted.
6.2.6.6. Functions with arguments
A function like available takes an expression as argument. On inverting, we just ignore the function. So we treat
ModelsInUser >> not available (filler >> context)
just like
ModelsInUser >> filler >> context
(both not and available are functions with a single argument). Functions with more than one argument just lead to multiple queries, as with the join and filter operators.
6.2.6.7. Sequence functions
An expression like this (taken from CouchdbManagement):
extern >> binder Manifests >> context >> extern >>= first
is inverted as if it was
extern >> binder Manifests >> context >> extern
That is, the sequence function at the end is just ignored.