Variables and scopes Pub Share

The variable scoping rules are important. The scopes cover not just variables, but also error handling, parallel processing etc.

Values, objects and addressing

Values can be declared outside of a message with $val, in either specification or a story. If declared inside a specification, it will exist in all flows using that specification. Note that these objects are not immutable, they can be changed, so they really are variables.

$val name="aValue"

Inside a rule, variables and assignments look like a message without a name. Note that someValue is local and will not be seen outside this rule's scope, see scopes below:

$mock rule.a
=> (someValue=123)

Changes to complex objects look the same - this assumes an object student already exists in context:

=> (student.name = "Joe")

Just like Javascript, if you have complex attribute names, you can use squarey brackets - this is very useful when the actual attribute name comes from another expression or variable or when itself contains dots and other special characters:

=> (student["some.weird.attribute.name"] = 12)
=> (student[variable] = 12)

Special prefixes

Some special prefixes manage the scope of variables (see scopes below):

  • dieselScope, ctx - will place the variable in the closest enclosing scope (ctx is deprecated, use dieselScope instead)
  • dieselRoot - place the variable in the root context for this flow
  • dieselRealm - place the variable in the static context shared by all flows

Scope structure

Any flow has a root and many levels of scopes. A scope may be created automatically by certain constructs or explicitely with the diesel.scope.push and diesel.scope.pop.

Also, each rule has is its own little scope for local variables, referenced as the rule scope:

$when sample.rule1
=> (v1 = 1)              // not available in rule 2, but is available in other rule1's
=> (dieselScope.v2 = 2)  // is  available in rule 2

$when sample.rule2
=> (v1 = v1 + 1)  // v1 is Undefined
=> (v2 = v2 + 1)  // v2 should be 3

$when sample.rule1    // another overloaded rule1
=> (v1 = v1 + 1)  // v1 is Undefined

$send sample.rule1
$send sample.rule2
$expect (v1 is 2, v2 is 3)

Note that you can also access the values in scopes with the accessor notation - especially useful when the name is available in a variable:

=> (a = dieselScope["v2"])
=> (variable = "v2")
=> (a = dieselScope[variable])

The scope hierarchy follows these rules:

  • values from a base context (like root) are visible in all other sub-scopes (like individual rule scope) unless overwritten at a lower level (like having a student in root and then re-assigning it inside another rule, the second will overwrite the top).
  • as scopes come and go, overwrites for base values come and go as well

realm

The realm is a static context shared by all flows running in your project. This is a good place for such things like tokens etc. Note that this context is small and slow.

root

The root is the root context for each "engine" or "flow" and contains globals.

You can set global values with a special scope: dieselRoot.x = expr1.

Note: be careful with using this scope: if you code lists, loops, asynchronous streams or parallel execution, explicitely putting values in this shared context can create a lot of hassles, just like static variables do in structure programming languages. This is why we use the scope context instead.

scope

A scope contains all variables for that context. You can assign values in scope with dieselScope.x = expr2 so x then becomes a new variable in the closest enclosing scope. You should not set variables outside the closest enclosing scope, as this would defeat the purpose of automatic scopes for instance in parallel processing.

When you read a value x, the scopes are searched from the current scope and up and the first occurence of x is returned, if found.

Remember that scoped values do not cross outside the scope, by default.

Scopes are created automatically following these rules:

  • each flow has a root context
  • each story runs in its own context, when running tests with the Guardian
  • each parallel branch runs in its own context
  • each rule is it's own context for local values - these are not seen outside the rule

In all normal flows, you should use only local variables and at most scope variables. You should try to avoid the root and realm contexts as much as possible.

These are the ways to propagate values into the scope context:

$when test.rule1 => (v1 = 1).  // v1 is local only to other test.rule1 derivations, not seen outside these

$when test.rule2 => (dieselScope.v2 = 2)  // dieselScope prefix will put v2 in the scope, so it's shared within the scope

$msg test.queryStudent (id:String) : (student:Student, alternatives:Student*) // declaring value outputs will propagate them to scope

$when test.queryStudent(id="1")
=> (student = x)
=> (alternatives = [])

The student and alternatives above are not local - since they were declared as the output of that rule, they are propagated automaticaly to the closest enclosing scope.

$when test.rule3
=> (payload = 3)  // payload is always propagated to the closest scope

rule

Each rule also has it's own scope for local variables. The only things that escape the rule scope are payload, explicitely declared return values from messages and when using the special prefixes like `dieselScope'.

Setting values

When setting values, the scopes are as follows.

=> (a=b) will set the value in the context of the closest parent

=> (dieselScope.a = b) will set the value in the closes enclosing scope (above the parent, likely)

.todo this is not available yet: => realmVars.a = b) will set the value at the realm level

Variable propagation and scope

By default, the variables are local to the rule inside which they are created. For instance the variable student below is not visible to the second rule:

$mock test.getStudent
=> (student = 'a student')

$mock test.printStudent
=> ctx.echo(student)

$mock test.students
=> test.getStudent
=> test.printStudent

To have this simple example work, you should use dieselScope.student = 'a student' OR use payload instead of student.

The only variables that can cross outside a rule are:

  • payload
  • declared output variables
  • assignments generated by a rule, if that rule only has assignments

Otherwise, you can use the dieselScope prefix.

This example would work, because student is declared as the output of the rule, so it is propagated to the closest scope, outside the parent rule:

$msg test.getStudent : (student)

$mock test.getStudent
=> (student = 'a student')

$mock test.printStudent
=> ctx.echo(student)

$mock test.students
=> test.getStudent
=> test.printStudent

Values that don't change

{{alert red}} Note that you cannot change the value of an incoming argument: {{/alert}}

$when this.is.static(aValue = 3)
=> (aValue = 3)
=> ctx.log(aValue)

Best practices

Domain values and output variables

Be careful when declaring and naming output variables.

Very bad idea:

$msg my.smart.message() : (output, result)

This message will put generic names in the static scope and these can easily overwrite others later and cause havoc. Same can be done by this:

=> (dieselScope.result = "something")

The idea of output domain values is to simply using domain elements in a scope. For instance, if your flow works on an Account, it is easy to declare the account and accountId as outputs of some query messages and then they'll be available anywhere in scope, wihtout having to declare the in each and every message like weak structured langauges (Java).

If there is a chance that a domain variable can cause clashes within the same flow, don't put it in scope or declare it as output of messages.

So here are some concrete examples:

BAD:

$msg warehouse.check     (productId, quantity) => (result)
$msg warehouse.reserve   (productId, quantity, cartId) => (reservationId)
$msg warehouse.provision (reservationId) => (result)

DECENT:
$msg warehouse.check     (productId, quantity) => (warehouseCeckResult)
$msg warehouse.reserve   (productId, quantity, cartId) => (reservationId)
$msg warehouse.provision (reservationId) => (provisioningStatus)

Or, better yet, if the check and provision have only local application, don't even give names to their output, use payload which is expected to be overloaded.

Scala API: Contexts overview

Here are the design notes on contexts, for contributors.

Realm context

This is a virtual context, backstop of all engine contexts. Contains statics shared across all engines in that realm. It is slow.

DomEngCtx

It is the root context for each engine, contains statics, pre-defined variables etc.

ScopeCtx

A scope context. It stops parameter propagation up.

LocalCtx

A local context, does not propagate values up, similar to the ScopeCtx but different use.

StaticCtx

This is a static context - it does not allow updates, used for evaluating expressions where setting values are not permitted.

RuleScopeCtx

Same as the local, but special use for rule scope (each $when has its own small local variable scope, so they behave intuitively)


Was this useful?    

By: Razie | 2021-01-10 .. 2022-11-15 | Tags: academy , reference


Viewed 910 times ( | History | Print ) this page.

You need to log in to post a comment!

© Copyright DieselApps, 2012-2024, all rights reserved.