This is the authoritive reference on the Perspectives Language. It is a work in progress. Currently, it is mostly about syntax. Semantical constraints will be added later. This is not the only source of information on PL:
[toc]
All types are identified by qualified names. These are qualified names:
Name form | Explanation |
---|---|
model://perspectives.domains#System |
A domain name must be qualified in a custom URI scheme (see below) |
model:MyModel$MyCase |
A fully qualified case name. |
model:MyModel$MyCase$MyRole |
A fully qualified role name. |
model:MyModel$MyCase$MyRole$MyProperty |
A fully qualified property name. |
pre:MyCase |
A prefixed name is a qualified name as long as we have a use statement for the prefix: use: pre for model:MyModel |
Quite often the parser/compiler can guess the qualified name from just the local part of that name. If not, an error is signalled. The use...for
statement may be used to abbreviate longer namespaces, e.g. use mrl for model:MyModel$MyCase$MyRole
lets us identify the property given in the table as mrl:MyProperty
.
If the parser finds multiple qualified names with the same local part, either fully qualify the offending name or prefix it with enough segments to make it unique. For example, given the roles:
model:MyModel$ACase$MyRole
model:MyModel$AnOtherCase$MyRole
the single segment MyRole
does not uniquely identify either. However, ACase$MyRole
does and the system does not complain.
Finally, each context and role act as namespace containers. Hence, qualified names can become quite long!
Version 18 and later support a style of identifier that is based on a custom URI scheme, informally: model://authority/LocalModelName
. Examples:
Name form | Explanation |
---|---|
model://perspectives.domains#System |
The model System is qualified with the namespace model://perspectives.domains |
model//social.perspectives.domains#AnotherModel$MyCase |
The namespace can have subnamespaces, just like DNS style domains can have subdomains |
These URIs can be mapped onto URLs. We use that mapping to find the location of the machine readable version of the model file. The first example maps to https://perspectives.domains/models_perspectives_domains/System.json
. The server at perspectives.domains will understand that as a request for the file System.json
in the Couchdb database models_perspectives_domains
.
There are four kinds of context. Each can be declared with a specific keyword. Each can house some, but not all, other context declarations.
Kind | Explanation |
---|---|
domain |
A domain is the top level context of a model. A domain can contain roles and contexts, and use statements . The name of a domain must start with model: , as in model:TestDomain . |
case |
A case revolves around a central concept (a role) and must contain the user roles that have a perspective on that concept |
party |
A party is for the purpose of introducing user roles that can be referred from cases. Use party to structure an organisation. A party can contain other parties. |
activity |
An activity is about doing things in a context with clear responsibilities. An activity may contain states. |
Context declarations can only be nested inside other Context definitions, with limitations:
domain | case | party | activity | |
---|---|---|---|---|
domain | x | x | x | |
case | x | x | ||
party | x | |||
activity | x |
A context type may be constructed to have public instances
. These are like web pages on the open internet: anyone with their address (and a MyContexts installation) can open them. Public instances are exclusively constructed through the mechanisme of a public role
: this is a (fictitional) user role that 'lives' at a specific URL. See below.
There are five kinds of roles. Each can be declared with a specific keyword inside a context definition.
Kind | Explanation |
---|---|
user |
A user role represents living persons that perform actions. Actions are collected in a perspective. A user role can only be filled by another user role. |
thing |
A thing role has no perspective. It represents everything that is not a user. A thing role can only be filled by another thing role. |
context |
A context role is bound to the External Role of a context. It represents a context (instance) within another context (instance). A context role can only be filled by another context role, or by an external role. |
external |
An external role is a role that represents a context. An external role cannot be filled. |
public |
A calculated user role whose perspective on the context is published on a server on the internet and thus is available to all |
Use these keywords to create Roles. Restrictions apply on filling roles:
Role declarations:
Expression | Explanation |
---|---|
user Driver |
Declare a User role with default attributes: not mandatory, functional. |
user Driver (mandatory, relational) |
Declare a User role with explicit attributes. |
user Driver filledBy: Person |
User role with default attributes, filled with the Person role. |
user SystemUser filledBy: None |
SystemUser is the root user and is never filled with another role |
user Parent filledBy: Father, Mother |
User role filled by either of two roles. |
user Driver (mandatory) filledBy: Person |
User role with attributes, filled with the Person role. |
user Driver filledBy: Person + LicenseHolder |
User role that requires its fillers to have two types simultaneously. One role provides personal details, the other the drivers license |
thing UnfinishedRides = filter Rides with not Finished |
Calculate a role with an expression. |
thing ListOfDocuments = callExternal "docNamesFromCouchdb" returns Document |
A call to a (fictitious) API function that results in instances of the type Document. We call a role that is created in this way a computed role. |
thing Account (unlinked) |
An unlinked role is not registered within the context itself. There is no semantic difference with an ordinary role; but the representation of its context doesn't refer to it |
public Visitor at "https://perspectives.domains/cw_servers_and_repositories/" = sys:Me | A public user role that defaults to the system user (so, anyone), whose context- and role instances can be found at the given url. Access is supposed to be free (i.e. no credentials should be needed to access the instances) |
The syntax for the public role declaration is as follows:
public <RoleName> at <expression-with-stringvalue> = <expression-with-userrole-value>
In other words, the location where the resources for the public perspective are stored, results from an expression and so can be calculated.
Roles and Contexts may have state definitions (note: a state is not a type that can be instantiated, like a context or role. Yet state terminology can be used to describe states). States are declared using a name and an expression with a Boolean value:
case MyContext
state SomeState = <expression>
States can be nested inside other states.
The use of states is threefold:
Notifications and automatic effects are defined in the lexical scope of an on entry
or on exit clause
.
domain Parties
case Party
user Guest
property FirstName (String)
property Accept (Boolean)
state Accepted = Accept
on entry
notify Organizer
“{FirstName} has accepted.”
user Organizer
A notification consists of a user role that the notification is meant for, and a string that may contain expressions in accolades. The expressions are evaluated with respect to a resource defined by the current state of the sentence; here, that is the state Guest$Accepted
. As this is a state of a user role, the state is subject state, fixing the resource we apply the expression to Guest
.
We can also embed an automatic effect in a state transition. An automatic effect is just a list of assignment statements (see the paragraph Assignment statements below).
domain Parties
case Party
user Guest
property FirstName (String)
property Accept (Boolean)
state Accepted = Accept
on entry
do for Organizer
-- Some relevant action expressed as assignment statements
user Organizer
The fragment do for
may be followed by a relative or absolute name. However, only user roles of the current context can be referred to. In order to have a role external to the context execute the action, create a calculated role in the current context.
There are three forms of the on entry
clause:
on entry
on entry of subject state
on entry of subject state S
Instead of subject
, we can use object
and context
as well. By using of subject state
we change the current state to subject state. Lexical analysis of the text will reveal the relevant user role.
The same variations hold for on exit
.
The in state
clause may be used to describe a perspective that only holds in the given state. For example:
domain Parties
case Party
user Guest
perspective on WishList
in object state Ready
view AllProperties verbs (Consult)
This model fragment gives Guest the means to consult all properties of the WishList, provided it is in state Ready.
The in state
clause refers to object state, here. This object state is specified by the perspective on WishList
clause, so it is about the WishList. Instead, we might have specified subject state by using the clause of subject state
, thereby referring to states of Guest. Finally, use of context state
to refer to states of the enclosing context.
Please note that this is not the only way to model this perspective. We could have given the perspective in the lexical context of WishList itself:
domain Parties
case Party
thing WishList
state Ready = ...
perspective of Guest
view AllProperties verbs (Consult)
You are free to choose either approach, whatever is more legible or understandable.
These are the variations of in state
:
in state
in state S
in subject state
in subject state S
in object state
in object state S
in context state
in context state S
The states of Aspects of a context- or role type just add to its own states. There is no provision, however, to
An automatic action is carried out immediately upon state entry. That is, as soon as a state holds for a context or role, any automatic actions are carried out on behalf of their respective users.
However, it is possible to delay that action:
state Accepted = Accept
on entry
do for Organizer after 2 Minutes
-- Some relevant action expressed as assignment statements
The keyword delay
must be followed by an integer value and a time constant type: Milliseconds, Seconds, Minutes, Hours, Days
.
More interestingly, it is possible to have an action repeated :
state Accepted = Accept
on entry
do for Organizer after 2 Minutes every 20 Seconds
-- Some relevant action expressed as assignment statements
The keyword every
must be to the right of the (optional) after
clause.
We can also have a cap to the number of times:
state Accepted = Accept
on entry
do for Organizer after 2 Minutes every 20 Seconds maximally 5 times
-- Some relevant action expressed as assignment statements
The keywords maximally
and times
must be to the right of after
but have to come before the optional maximum number of repetitions. Notice that we need to add a clause every
: otherwise, all repetitions would be carried out immediately after one another (the compiler sees to it that you don't forget it).
Finally, we can limit the repetition by providing an until
clause:
state Accepted = Accept
on entry
do for Organizer after 2 Minutes until 5 Minutes every 20 Seconds
-- Some relevant action expressed as assignment statements
The until
is to be read as: until the given time has passed since the state became valid. Summing up, we have the following syntax:
[after N <Time constant>] [until M <Time constant>] [every P <Time constant> [maximally Q times]]
Notice how all clauses are optional, but must come in the order as given; and notice that the maximally
clause is optional, but must be used together with every
.
Property declarations:
Declaration | Explanation |
---|---|
property MyProperty |
A default property: a String range, it is functional and not mandatory. |
property MyProperty (mandatory, relational, Boolean) |
A property with defined attributes and range |
property PlannedDuration = Destination >> Planned – Origin >> Planned |
Calculate a property with an expression. |
property P (functional) = binder SomeRole >> SomeProp |
Property is known to be functional despite potentially relational binder step. |
A property must be declared with a valid range, or it will be a String. The table below gives the valid range types and the form of their literals in model text:
Range type | Literal form |
---|---|
String | "This is a string literal" |
Date | '2022-03-11' |
Time | "hh:mm:ss" and "hh:mm:ss.sss" |
DateTime | 'YYYY-MM-DDTHH:mm:ss.sssZ', but time may be left out |
Boolean | true |
Number | 123 |
john@doe.com | |
File | _ |
Year, Month, Week, Day, Hour, Minute, Second, MilliSecond | Duration type values must be written as integers |
MarkDown | <<This is a **markdown** literal.>> |
See this page for the supported time format. See this page for the supported date-time format.
Note that there is no literal form for a File property. However, we do have an assignment statement to create files (create file...
, see below).
Apart from the mandatory
constraint, a number of other so-called constraining facets may be specified with the property. They are inspired on the XML specification: see Constraining Facets.
Constraint | Explanation | Applies to |
---|---|---|
minLength | Input may not be shorter than given integer | Number |
maxLength | Input may not be longer than given integer | Number |
enumeration | Only the given values are valid | String, Date, Number, Email |
pattern | String form of the input must match a regular expression | String, Date, Number, Email, the mime type of File |
maxInclusive | Upper bound for the value | Date, Number |
minInclusive | Lower bound for the value | Date, Number |
Examples
property Text (mandatory, String)
minLength = 100
maxLength = 200
property ADateTime (DateTime)
minInclusive = '2022-04-15'
property ANumber (Number)
minInclusive = 10
maxInclusive = 80
property WeekDay (String)
enumeration = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
property Appel (String)
pattern = ".*appel.*" "Any word with the string `appel` in it."
property Source (File)
pattern = "text/arc" "Only .arc files (Perspectives Language source files) are allowed, so use `text//arc."
The pattern must be enclosed in strings. Furthermore, observe the following:
E.g.:
pattern "https://\\w+\\$" "An URL that ends on a $ sign"
Adding an aspect uses the same syntax for contexts and roles. Here is how to add an Aspect to a context:
case MyContext
aspect SomeOtherContext
Once an aspect has been added to a context, its roles may be added to the enriched context (notice the use of qualified names):
case MyContext
...
aspect user pre:SomeUserRole
aspect thing model:SomeModel$SomeThingRole
NOTE: when referring to a role that has been added as an aspect to a context, one should use the fully qualified name (or the prefixed name)!
user SomeUser
-- This will fail:
perspective on SomeThingRole
-- But this will succeed:
perspective on model:SomeModel$SomeThingRole
We can also add an Aspect to an existing role, thereby adding all its properties to that role:
case MyContext
...
thing MyRole
aspect SomeOtherRole
It is then possible to map an Aspect property onto a property of the role:
case MyContext
...
thing MyRole
aspect SomeOtherRole
where
AspectProperty is replaced by Property
Whenever an aspect context is added to a receiving context, its external role is added automatically to the external rol of the receiving context. In other words, there is no need for:
case MyContext
aspect SomeOtherContext
external
aspect SomeOtherContext$External
If an Aspect is relational
(allows multiple instances), the enriched role will be relational as well.
If an Aspect is mandatory
, the enriched role will be mandatory as well. This cannot be overridden.
Note: aspect identifiers must be fully qualified.
Certain restrictions apply:
functional
mandatory
;Perspective declaration:
user SomeRole
perspective on SomeThing
There is no need to qualify the role name if it is in the same context as the user role (unless the perspective object is added as an aspect role to the context, see above).
A perspective declaration can be followed by role- and property verbs, to flesh out what the user role can do:
user SomeRole
perspective on SomeThing
only (Fill, Remove)
props (SomeProperty, AnotherProperty) verbs (SetPropertyValue)
The role verbs are Remove, Delete, Create, CreateAndFill, Fill, Unbind, RemoveFiller, Move
. Apart from the keyword only
, one can use except
, whatever is more compact (however: notice that except
does not forbid the excepted verbs. It is just a convenient way to express a list of allowed verbs. This is important w.r.t. aspects, as a user role aspect causes the users' perspectives to be enriched by the aspect user and this may add role verbs to the final list that are in an exclude
clause in one of the combined perspectives!). To include all role verbs, use all roleverbs
.
perspective on SomeThing
all roleverbs
To include no role verbs, just don't include a role verb specification:
perspective on SomeThing
The property verbs are: RemovePropertyValue, DeleteProperty, AddPropertyValue, SetPropertyValue, Consult
. Note that property verbs are given per set of properties:
props (SomeProperty, AnotherProperty) verbs (SetPropertyValue)
or per view (omit the verbs
keyword in that case)
view AView (SetPropertyValue)
To allow all property verbs with a view, just omit a specification of property verbs:
view aview
Finally, to allow all role verbs, and all property verbs for all properties:
perspective on SomeThing
defaults
First, notice that a perspective must be both for some subject (a user role) and on some object (any other role: a thing, context, user or external role). By embedding a perspective in a user role (as we did in the examples above), the subject is given implicitly. By using perspective of
we can specify the subject role explicitly. So, for example, in the context of some thing role, by using perspective of
we again have the combination of object and subject.
thing SomeThing
perspective of SomeRole
equals:
user SomeRole
perspective on SomeThing
Other clauses we can include in the scope of a perspective:
in state
on entry
on exit
perspective on
perspective of
action
selfonly
The first three options introduce the means to qualify what follows in terms of state or state transitions. We have discussed Embedding perspectives in the previous paragraph. The Perspectives Language parser and compiler will not allow you to nest two perspective on
or perspective of
clauses; however, you can nest one in the other.
An action
is like an automatic action. But whereas the latter occurs on state transitions, the former must be executed by the end user. This obviously requires some user interface element such as a button or menu action; however, that is beyond the scope of the language specification.
Imagine a party with a role Guests, modelled with a Boolean property that indicates whether a Guest has accepted the invitation or not. In order to be able to change that property, the role Guests must have a perspective on itself. However, each Guest should be able to change just his own property value. This we model with the modifier selfonly
.
As modifications sent by peer installations are checked against the model, it is important to know how to endow a user with an adequate perspective, so they will not be blocked by the receiver. This is especially important for automatic actions. The table below links an assignment operator with the verb required for peers to accept the modification.
Modification | Assignment operator | Required verb |
---|---|---|
Remove a single role instance | remove |
Remove |
Remove all role instances of a type | delete |
Delete |
Add a role instance to a context instance | create role |
Create |
Create a role instance and fill it with a new context | create context |
CreateAndFill |
Fill a role instance | bind |
Fill |
Remove a role as filler from all roles (optionally of a type) | unbind |
RemoveFiller |
Move a role instance to another context instance | move |
Move |
Add a property value | += |
AddPropertyValue |
Remove a property value | -= |
RemovePropertyValue |
Delete a property (remove all values) | delete property |
DeleteProperty |
Replace a property's values by a value | = |
AddPropertyValue and RemovePropertyValue |
A type definition consists of a type declaration line, followed by an indented block that gives the type’s details (we might call this block the body of the type definition text). Similarly, many other syntactical constructs, like perspective
or in state
, consist of a line of text followed by a body. The table below gives, for nine such bodies, how they may be nested inside each other. Not all nestings are arbitrarily deep: only two perspectives can be nested, for example, and one must be a perspective on
while the other is a perspective of
.
Right: contained | context | role | state | perspective | in state | state transition | action | notification | automatic action | screen |
---|---|---|---|---|---|---|---|---|---|---|
Below: containing | ||||||||||
context | yes | yes | yes | |||||||
role | yes | yes | yes | yes | yes | user role only | ||||
state | yes | yes | yes | yes | ||||||
perspective | yes | yes | yes | yes | ||||||
in state | yes | yes | ||||||||
state transition | yes | yes | ||||||||
action | ||||||||||
notification | ||||||||||
automatic action | ||||||||||
screen |
Note that there can be further restrictions. For example, an action may be embedded in a user role, but not in other roles.
Use the table to work out what a particular construct can be nested in. This gives rise to a tree. For a state transition, for example, the tree is given partially below:
Combine this with the table in the section on the standard variables currentcontext
, currentactor
, notifieduser
and origin
to work out whether a particular variable is available in a syntactical location. For example, this model fragment:
case C
user U
thing R
perspective of U
on entry
do
<statement>
<statement>
can have currentcontext
, currentactor
and origin
.
In contrast, in:
case C
thing R
property P = <expression>
we can only use currentcontext
and origin
in <expression>
.
(Automatic) Actions are expressed as assignments.
An <assignment>
comes in two forms:
letA
expressionA letA
expression:
letA
varname1 <- <expression>
varname2 <- <expression>
in
<assignment>
Notice: variable names must be lowercase!
A letE
or letA
expression lets you bind variables to the result of expressions. However, the letA
allows the part that gives the value of a variable to be one of two assignment statements, too: create role
and create context
. This makes it possible to create an instance and modify it immediately.
letA
newroleinstance <- create role <SomeType>
varname2 <- <expression>
in
<SomeNumericProp> = 10 for newroleinstance
create context
returns the role instance in the embedding context that is filled with the external role of the new context instance.
letA
trans <- create context Transaction bound to Transactions
in
bind sys:Me to Creditor in trans >> binding >> context
Assignment statements change the instances of a role in a context. The first group of assignments creates or deletes role instances or fills them with another role. The second group changes the values its properties have.
NOTE: the text Assignment has a more in-depth treatment of the subject.
Assignment | Required Verb | Explanation |
---|---|---|
remove role <role-expression> |
Remove |
Select the role instances to remove, in any context |
remove context <role-expression> |
Remove |
Select the role instances to remove, in any context. They must be instances of a context role type. Their bound contexts will be removed, too. |
create role RoleType [in <contextExpression>] |
Create |
Create a new instance of the role and attach it to the current context, or to the context selected with the optional <contextExpression> . |
move <roleExpression> [to <contextExpression>] |
Move |
Move the selected role instances to the current context, or the selected context. Notice that both contexts have to have the same type! |
delete role roleType [from <context-expression> ] |
Delete |
Delete all selected instances of the RoleType, from the selected contexts. |
delete context bound to roleType [from <context-expression> ] |
Delete |
Delete all selected instances of the context RoleType and the contexts bound by them, from the selected contexts. |
bind <binding> to RoleType [in <contextExpression>] |
Fill |
To fill a role with another role, use bind (when A fills B, we say that B is bound to A. A is the binding; B is the binder). RoleType resolves to the current context or the selected context. New instances of RoleType are created as binders. |
bind_ <binding> to <binder> |
Fill |
Instead of creating new instances, this syntax allows you to select an (empty) binder and fill it with the binding. Notice that both queries must select a single instance! |
unbind <binding> [from RoleType ] |
RemoveFiller |
Select bindings - that is, the roles that are bound - and release them from their binders. When RoleType is given, just release them from binders of that Type. |
unbind_ <binding> from <binder> |
RemoveFiller |
Select a single binding and and release it from the selected single binder. |
create context ContextType [named localName] [bound to RoleType ] [in <contextExpression> ] |
CreateAndFill |
Create a new instance of ContextType and optionally bind it in a new instance of RoleType , by default in the current context and optionally in the context instances selected by <contextExpression . Also, optionally provide an identifier for the context instance (without a storage scheme such as "def:" etc.). Useful for public contexts. If no RoleType has been provided, the context will be created as a root context. |
create_ context ContextType [named localName] bound to <roleExpression> |
CreateAndFill |
Create a new instance of ContextType for every role instance selected by <roleExpression and bind them. Also, optionally provide a name for the context instance (without a storage scheme such as "def:" etc.). Useful for public contexts; overrides symbolic public store names. |
Property assignments. This is how to change the values of a property for some role. The assignment statements below change the roles in the current object set, unless one or more role is selected with <role-expression>
. The current object set is determined by the object of the perspective.
Assignment | Required Verb | Explanation |
---|---|---|
SomePropertyType =+ <value-expression [for <role-expression] |
AddPropertyValue |
Add a value to the list of values for SomePropertyType. It is an error if the type of <value-expression is not equal to the range of SomePropertyType . Changes the current object set, or another role if <role-expression> is given. |
SomePropertyType =- <value-expression [for <role-expression] |
RemovePropertyValue |
Remove the value from the list of values for SomePropertyType. It is an error if the type of <value-expression is not equal to the range of SomePropertyType . |
SomePropertyType = <value-expression [for <role-expression] |
AddPropertyValue and RemovePropertyValue |
Completely replace all values of SomePropertyType with the values returned by <value-expression . It is an error if the type of <value-expression is not equal to the range of SomePropertyType . |
delete property SomePropertyType [for <role-expression] |
DeleteProperty |
Completely remove all values of SomePropertyType . |
create file <Name> as <MimeType> in PropertyType [for <RoleExpression>] <Contents> |
SetPropertyValue |
(note Contents should be on the next line) Create a file with name Name , and mime type MimeType and give property PropertyType that value. The contents of the file are Contents . Notice that this will only work for text mime types. |
Creating a context is different from creating a role. It is not possible to create a role instance without immediately inserting it into a context. Similarly, no context should be created without binding it into some other context. This is enforced by the only context creation operators, create context
and create_ ontext
.
Perspectives has a mechanism to extend the language with external functions. These functions should be thought of as external with respect to the core language. Such functions are defined in namespaces and added through modules that are compiled with the PDR. One can call them through the use of two keywords:
callExternal
allows one to create a Role or Property that is computed by an external function;callEffect
allows one to make changes to the Perspectives state by calling an external function.For example, this ContextRole declaration:
context Modellen = callExternal cdb:Models() returns: Model$External
adds a Role that is computed by the function Models in the module identified by the (standard) prefix cdb
: model:Couchdb
. As another example, consider this statement in the then-part of a rule:
callEffect cdb:AddModelToLocalStore( origin >> binding >> Url )
This external function loads a model file from an Url and adds it to the local store of model files in Couchdb.
The general form for calling these functions and statements is qualifiedName( <expression>* )
, where each argument must be provided by ordinary Perspectives expressions (as detailed below).
A view is merely a list of properties.
view MyView (SomeProperty, AnotherProperty)
It is an error if SomeProperty
and AnotherProperty
are not the identifiers of Properties.
Expressions (also called 'queries') represent a collection of role- or context instances. An expression can be used as a way to calculate a role or property. we do not support calculated contexts. Apart from indexed contexts, every context is only accessible through a context role in another context. It is, of course, entirely possible to calculate a collection of external roles.
Expressions have a highly recursive structure. They are built from three primary forms: (assignments are not expressions!):
Here is the full expression syntax:
expression = simpleExpr | unaryExpr | binaryExpr | letE
simpleExpr =
identifier
| value
| variable
| >>= sequenceFunction
| ‘this’
| ‘binding’
| ‘binder’
| ‘context’
| ‘extern’
| 'origin'
| 'currentcontext'
| 'currentactor'
| 'notifieduser'
value = number | Boolean | string | date
variable = lowerCase+
sequenceFunction = ‘sum’ | ‘product’ | ‘count’ | ‘minimum’ | ‘maximum’
unaryExpr =
‘not’ expr
| ‘exists’ expression
| 'available' expression
| 'filledBy' expression
| 'fills' expression
binaryExpr =
‘filter’ expression ‘with’ expression
| expression operator expression
operator =
'>>' | '==' | '<' | '>' | '<=' | '>='
| 'and' | 'or'
| 'union' | 'intersection' | 'orElse'
| '+' | '-' | '*' | '/'
| 'filledBy' | 'fills'
letE = ‘letE’ binding+ ‘in’ expression
binding = variable ‘<-‘ expression
Operator precedence is ruled by weights for each operator. Parentheses can be used to override these precedences. An operator with higher precedence prevails of an operator with lower precedence. Hence,
not SomeBoolean and not AnotherBoolean
will be interpreted as:
not (SomeBoolean and not AnotherBoolean)
which is usually not what you expect. instead, write:
(not SomeBoolean) and not AnotherBoolean
Operator | Precedence |
---|---|
>> | 9 |
union, intersection, orElse | 8 |
>>= | 7 |
* | 6 |
/ | 5 |
+, - | 4 |
==, >, <. <=, >= | 3 |
filledBy as infix operator | 3 |
matches | 3 |
and, or | 2 |
not, exists, available, filledBy, fills | 1 |
filter | 0 |
SomeContextRole >> binding >> context >> AnotherRole
SomeRole >>= sum
SomeRole >>= sum / SomeRole >> count
not (ABooleanProperty and AnotherBooleanProperty)
filter SomeRole with not exists APropertyOnThatRole
filter SomeRole >> binding with BooleanPropertyOnTheBinding
filter SomeRole with filledBy AnotherRole
filter SomeRole with not fills sys:Me
ThisRole >> Date == ThatRole >> Date
SomeNumericProperty * 2
letA
a <- SomeRole >> binding
b <- AnotherRole >> binding
in
unbind a from SomeRole
unbind b from AnotherRole
bind a in AnotherRole
bind b in SomeRole
Most operators speak for themselves; however, we describe a few less intuitive ones.
>>
(compose)The operator >>
is used to compose a path. We also call a path a query, as it computes context- or role instances from a starting point (always a context or a role: see below). A path/query thus is a function. The term compose derives from the fact that role and property names and a number of standard functions such as context
(move from a role instance to its context) can be composed into larger functions with >>
. For example:
|binding >> context >> SomeRole
selects from its starting point - which must be a context role - its binding, then the context of that binding and then all instances of the type SomeRole
union
and intersection
A union B
means: the union of the results of queries A and B, while A intersection B
means: just return those values that are both an A, and a B. Equivalently, A both B
means the instances that are in the intersection of the results of applying A and applying B.
orElse
A orElse B
means: the result of A, if any. However, when A results in an empty sequence, return the results of B (notice that these may be empty, too, in which case the result of the entire expression is the empty sequence).
>>=
(sequence)The sequence operator >>=
is used to apply a function to all of the results of a query. It should be followed by a function name: SomeRole >> SomeNumericProperty >>= sum
will result in a single number summing all values of the property SomeNumericProperty
over all instances of SomeRole
. Other functions are product
, minimum
, maximum
, count
and first
.
exists
and available
The exists
operator takes, like not
, just a single argument.
exists binding
yields a Boolean value indicating whether the role instance that this expression is applied to, has, in fact, a binding (is filled).
available
tests whether the role instances that it is applied to, are, in fact, available in the storage locations used by the PDR. The difference is subtle. The PDR operates on the assumption that it can exchange any instance identifier for the actual instance, any time. available
works around that assumption: you can test whether it holds for the given identifiers. This is rather an edge case.
filledBy
and fills
A filledBy B
means that A is filled with B, or, perhaps confusingly, B fills A. The binding of A is B. A fills B
means the inverse: it is true when B is filled with A: in other words, if the binding of B is A (A fills B).
Both filledBy
and fills
can be used as a binary operator, and as a unary operator (what we might call a predicate).
filter SomeRole with filledBy sys:Me
The above expression returns all instances of SomeRole that are (ultimately) filled by sys:Me
(that is, for each PDR installation, the 'own' user). Here filledBy
is used as a predicate.
In contrast, in
filter Repositories with IsPublic or binding >> context >> Admin filledBy currentsubject
filledBy
is used as a binary operator: the left side of the expression is binding >> context >> Admin
, while the right side is just currentsubject
.
this
this
is the identity function: it just returns the value that is passed as argument. The following expression is always true:
letE
startingpoint <- this
in
startingpoint == origin
<
, >
, <=
, =>
(inequalities)These comparison operators are defined on Strings, Numbers, Booleans and DateTimes.
binding in <context type>
Sometimes, one wants to make sure a query moves to fillers that originate in a specific type of context. A likely case is one where the restriction on a filler of a particular role type has been expressed in terms of a role type that is used as an Aspect. Let's say we require a Driver and that this role type has been used as an aspect of roles in both a Boat and a Plane context. The first would be a Captain and the second a Pilot. Now we want to make sure that in our query, when we move from a role to its filler, we only end up with Pilots. We would then say:
binding in Plane
The resulting sequence would hold role instances of Pilot only.
Let's examine a familiar example. The expression
1 + 2 * 3
should be understood as
1 + (2 * 3)
yielding 7, instead of
(1 + 2) * 3
yielding 9. The simple rule is: at any moment, group operands around the operator with the highest precedence first.
What if there are ties?
1 + 2 + 3
can be understood in two ways:
(1 + 2) + 3
1 + (2 + 3)
Now, for addition, it does not matter. Not so for subtraction:
3 - 2 - 1
can be
(3 - 2) - 1 = 0
or as
3 - (2 - 1) = 2
So, order matters. In Perspectives, we choose the second grouping. It is called right-associative: we associate terms from the right to the left.
Now for a Perspectives Language example:
filter Repositories with IsPublic or binding >> context >> Admin filledBy currentsubject
The operators in this example, with their precedence, are:
operator | precedence |
---|---|
filter ... with | 0 |
or | 2 |
>> | 9 |
filledBy | 3 |
Starting with the >>
operator (and applying right associativity) we get:
filter Repositories with IsPublic or (binding >> (context >> Admin)) filledBy currentsubject
the next powerful operator is filledBy
:
filter Repositories with IsPublic or ((binding >> (context >> Admin)) filledBy currentsubject)
then comes or
:
filter Repositories with (IsPublic or ((binding >> (context >> Admin)) filledBy currentsubject))
and then we're done, as we've only got filter
left. Filter is a bit difficult to see. It may be easier if you rewrite it like this:
Repositories filterwith (IsPublic or ((binding >> (context >> Admin)) filledBy currentsubject))
Let's examine another case:
filter context >> Versions with VersionNumber == origin >> LastVersion >> Url
Operator ==
has precedence 3. First we apply >>
:
filter (context >> Versions) with VersionNumber == (origin >> (LastVersion >> Url))
then comes ==
:
filter (context >> Versions) with (VersionNumber == (origin >> (LastVersion >> Url)))
and we're done. However, now we have an expression that tries to compare a VersionNumber to a Url. Without knowing the definitions for these properties, we can guess that this is wrong and indeed it is. We need to add parenthesis to the original expression:
(filter context >> Versions with VersionNumber == origin >> LastVersion) >> Url
Now we get:
(filter (context >> Versions) with (VersionNumber == (origin >> LastVersion))) >> Url
so we take the Url of the filtered Versions (the one with the latest version number).
Strings, Numbers, Booleans and DateTime values are written as follows:
Value expression | Explanation |
---|---|
"a string" | A string should be enclosed in quotes. Any character can be inside a string, that is not a double quote, a backslash or an ampersand. Escape character sequences are allowed, too, but they are not documented here. We refer to the source code of Text.Parsing.Parser.Token |
true, false | These are the entire population of the Boolean type. |
123 | Number representation has yet to be decided on. Right now only integers can be used. |
'2019-10-04', '2011-10-10T14:48:00' | Date and DateTime expressions are parsed by Javascript Date.parse(), enclosed in single quotes. See Date Time String Format for more details. |
To a limited extent, it is possible to include role- or context instances as literals in a model text:
Value expression | Explanation |
---|---|
publicrole pub:someUrl#someIdentifier | A way to write a literal role instance value (exclusively for public roles). |
publiccontext pub:someUrl#someIdentifier | A way to write a literal context instance value (exclusively for public contexts). |
This allows us to refer in a model to public resources. The original use case was when we needed to introduce the base repository for perspectives.domains in the System model. A repository is described by a context instance; we needed to have that base repository present in every installation (for other reasons, this is no longer the way we introduce that repository in an installation).
Another way to refer to a role- or context instance in a model text is by way of them being indexed.
A modeller can declare a type to be indexed. Such a type can only have a single instance, but these instances may be unique for each user. In order to reference them in a model text irrespective of the specific installation (or user), one uses the indexed name. MySystem
is an example. Actually, an indexed name should be fully qualified. The usual practice is to use prefixes, as in sys:MySystem
. Even though any context- or role type may be declared indexed, the modeller is adviced to restrict this practice. E.g. a root context (usually an 'app' or the starting context of a particular domain), that need not itself be embedded in another context, should be declared indexed (so it can be reached through its indexed name).
An indexed individual can simply be referred to in a model text by just using its name (an indexed type name by itself is an expression that yields its unique instance for each installation).
Finally, we have a way to refer to a role- or context individual by way of an arbitray String-valued expression.
contextinstance (QualifiedContextType) SomeProperty
returns the context instance of type QualifiedContextType that is identified by the String value that is the result of the expression SomeProperty
. We need to provide the type of the instance in the model text; otherwise the compiler cannot perform its checks.
IMPORTANT: the context type must be qualified (possibly with a prefix). The same holds for the role type.
Similarly we have roleinstance
.
Expressions occur in a few places in a model expressed in the Perspectives Language:
notify
;An expression can be thought of as a function. In the paragraphs below we explain what these functions are applied to. This follows from lexical analysis of a Perspectives source text, so we first explain a number of concepts used in this analysis. We also introduce four standard variables that can be used inside expressions.
Expressions have instance values. That is, they represent collections of instances (role- or context) in runtime. Type reflection is a powerful mechanism in a language. The Perspectives Language has limited support for type level queries. We have queries that have a collection of role types as result (we call the type of such a query RoleKind
). Similarly, we have queries that have a collection of context types as result (RoleKind
).
role
and context
The operator role
allows us to switch a query to the type level.
[role model://perspectives.domains#System$Invitation$Invitee]
is a query whose result is the type Invitee. It is a RoleKind-valued query. The operator context
works similarly.
NOTE: Do not confuse this use of context
with the function context
that takes a role instance as input and produces its context instance as output! The square brackets are essential to make the difference
specialisesRoleType
This operator allows us to filter a collection of types by just keeping types that specialise a certain type, like in this fragment taken from the System model:
filter origin >> context >> contextType >> roleTypes with specialisesRoleType model://perspectives.domains#System$Invitation$Invitee)
Notice that specialisesRoleType
assumes its literal argument is on the type level.
indexedName
keywordRelated to the above is the following type reflection. Whereas an indexed name in a model text is a way to include a reference to a role- or context instance, we can also work in the other direction by requesting the string value of the chosen indexed name from an instance. The keyword: indexedName
is by itself an expression step. Given an instance, it returns the indexed name for its type (if any exists). For example:
sys:Me >> context >> indexedName
returns "model://perspectives.domains#System$MySystem". It is a String-valued expression, but one that reflects a type level constant.
At each point in the source text of a model (at each lexical position) some kind of state holds. Here is how current state is determined:
user
roles, object state for other roles ).in state X
clause sets the current state to its substate X. When a state type (in object state
, in subject state
, in context state
) is specified, the current state is set to the substate X of the current object, current subject or current context respectively.on entry
and on exit
clauses do by themselves not change the current state, but specify a state transition for the current state. When of
and a state name is specified, the state transition is for the substate of the current state.
Alternatively, you may think that state name changes the current state for the lexical position of on entry X
and on exit X
to substate X of the current state.on entry
and on exit
clauses can be augmented with three parts:
of object state
, optionally extended with a state name. It sets the current state to the Root state (or the named state) of the current object;of subject state
, idem, for the current subject;of context state
, idem, for the current context.See the document Understanding a Perspective Model Text for an in-depth treatment of this subject.
In the lexical analysis of a Perspectives source text, we use three concepts.
A context type is the current context in its body, up till a nested context type.
The bodies of the following constructs define the current object:
user
, thing
, context
or external
)perspective on
clause.Finally, the current subject holds in the bodies of:
user
definitionperspective of <user>
clausedo for <user>
clausenotify <user
clauseThe previous paragraph introduced concepts for lexical analysis. Related, but not similar, are four standard variables that may be used inside expressions without providing a value for them:
Standard variable | Value in runtime |
---|---|
currentcontext |
Is bound to an instance of the lexical current context type. |
origin |
Is bound to the context or role instance that the expression is applied to. |
currentactor |
Is bound to the user role instance for whom an action is executed automatically or who executes the action by hand |
notifieduser |
Is bound to the user role instance who is notified. |
We now have the conceptual tools to explain what expressions are applied to. The standard variables currentcontext
and origin
can be used in every expression. The table shows the usage of the other two.
Expressions in | Applied to | standard variables other than origin and currentcontext |
---|---|---|
The definition of a calculated role | Current context | |
The definition of a calculated property | Current object | |
The condition of a context state | Current context | |
The condition of a role state | Current object | |
The object of a perspective | Current context | |
Expressions in do | The resource whose state is transitioned into or out of | currentactor |
Expressions in notify | The resource whose state is transitioned into or out of | notifieduser |
Expressions in action in a lexical position with a current object | Current object | currentactor |
Expressions in action otherwise | Current context | currentactor |
A screen
is a lexical construct to describe a screen as it should be generated in runtime. In essence, a screen is a series of tabs where each tab is composed of widgets that can be arranged in rows and columns. Currently we have three widgets: the form (for a functional role), the table (for a relational or multi-role) and a markdown widget.
It is up to you whether you start your tab with rows or with columns. However, columns should be embedded in rows and rows in columns: you cannot have rows in rows or columns in columns. For example:
user Tester filledBy sys:PerspectivesSystem$User
indexed com:Tester
perspective on TestRole
props (Text, Bool, ADateTime, ANumber) verbs (Consult, SetPropertyValue)
only (Remove, Create)
perspective on TestTable
defaults
screen "Test screen"
tab "Test"
row
column
form TestRole
props (Text, WeekDay) verbs (Consult)
only (Create)
table "MyTable" TestTable
view Limited verbs (Consult)
only (Remove)
tab "Another tab" default
row
...
This model fragment gives us a user role Tester
with a perspective on two roles. It also describes a screen for Tester
, for those perspectives. Notice that a screen must be described within a user role definition and it is just for that user role. The screen has a single tab. It arranges two widgets below each other in a column. Somewhat superfluosly it puts that column in a single row.
Notice how for both widgets the fragment is quite detailed as to
As a matter of fact, one can use the very same syntax used for describing these details for a perspective, in a widget.
Also notice the second tab with the marker "Another tab". It has the default
keyword and this signifies that on opening the screen, this tab should be opened by default. Without a default tab, none of them will be openend.
The compiler checks the following constraints concerning a screen specification:
The verb- and property specification of a screen may only serve to further restrict the possibilities given by the perspective a user has on the role that underlies the widet.
The markdown
construct is for rendering MarkDown source as HTML (the source should conform to the Common Mark standard). There are three lexical variants.
screen "Embedded (constant) MarkDown"
row
markdown <## Markdown!
The source of this text is included in the Perspectives model.
>
The first form consists of a MarkDown literal value in the model. It should be enclosed between '<' and '>' angular brackets. It is rendered on screen without any frills. The rendering may be conditional as in the next example:
screen "Some Conditional MarkDown"
row
markdown <## Markdown!
The source of this text is included in the Perspectives model.
>
when ARole >> ShowIt
thing ARole
property ShowIt (Boolean)
Here, the MarkDown text is rendered only when the property ShowIt of ARole evaluates to true
. Notice that the condition is evaluated with respect to the current context.
Instead of embedding a MarkDown string in the model, we can retrieve it from a property with the range MarkDown:
screen "MarkDown Expression"
row
markdown ARole >> MD
when ARole >> ShowIt
thing ARole
property MD (MarkDown)
property ShowIt (Boolean)
This allows a user with a sufficient perspective to enter MarkDown text in a form. A user with a Consult perspective on the property MD will just see the rendered MarkDown.
Notice that, with the MarkDown expression, in order to be able to edit it, the user must have a defined perspective whose object is the role that has the MarkDown property (and at least SetPropertyValue
should be specified in the verbs section for that property).
A more straightforward form is this:
screen "MarkDown Perspective"
row
markdown ARole
props (MD) verbs (Consult, SetPropertyValue)
thing ARole
property MD (MarkDown)
Here, we clearly tie the markdown component to a specific role and property and we set the verbs specifically for this rendering. However, we still have to provide a sufficient perspective for the user role for this to work.
We have defined two extensions that can be used in the markdown expressions. The first is the link
that allows the end user to navigate from the page displaying the rendered MarkDown to another context:
A link allows the end user to move to <<link: def:#re61moqm93|move to another context>>.
Here, the string "def:#re61moqm93" is the identifier of a context role or of an embedded context role. One obtains such an identifier by clicking the card that represents the role and pressing ctrl-c
. It can then simply be pasted into the markdown text.
The second is an action
. Actions can be defined for a user in a context, or for a user on a particular role type. A so-called context action will be applied to the context instance that is displayed on screen; a role action will be applied to the selected role instance. We currently only support context actions in MarkDown. Here is an example:
Press the button to create a new instance of the role Building <<action: CreateBuilding|New Building>>.
Notice that the action must already be modelled in the users' perspective! The markdown instruction above does not create it, it merely provides a complementary way of invoking it from within the readable text that is rendered from the MarkDown source text. The name of the action (like CreateBuilding in the example) should be exactly the name as it appears in the model.
Copyright 2019-2021 Joop Ringelberg and Cor Baars