Values and Constants
Type system
As of now, data contracts in diagrams do not have typings.
When exporting Typescript code, they are declared as any
. However, FSM itself (it's States
and Actions
) is typed along with Automata instance.
For other languages, Yantrix aims to provide compatibility with elementary types, existing in most of them:
Number
Most of state values in a typical app are numeric. Every enumerable list should also be mapped to a number for sake of portability.
Yantrix internals also use numbers as a communication language, with all States, Actions and Events names being stored as numerical codes.
In produced code, integer and float values are processed as similarly as possible for a given language
Strings
A string is an ordered sequence of characters, i.e. numbers within a certain codepage. All strings in Yantrix are in UTF-8.
Strings are processed as immutables, i.e. every string function should return a new string rather than modify any of its arguments
Lists (Tuples)
A list is an ordered sequence of same-type values. Lists are used when order matters, but not the position and quantity. Most oftenly used Lists are Collections.
A tuple is an ordered sequence of values, that could potentially be of different type, and thus the position in the list defines the semantics of value. Tuples are used to store values of different nature that are "bound" together by common meaning, and they are usually of fixed length. A good example of a tuple would be a vector or a message in most binary protocols.
While having different purpose, Lists and Tuples are pretty much identical syntactically, and are processed as similarly as possible in the produced code, assuming they are of most common iterable type for that language. They also are immutable and can only be a read-only argument of the calling function. The difference only exists in semantics, i.e. if you use a mapper on a Tuple rather than on a List, the results might be unexpected.
Objects
An object, usually called a Dictionary, is a presumably unordered set of key-value combinations. Keys are always strings, while values may have arbitrary types. In other words, a dictionary is a one-to-one map between a List and a Collection. Keys are unique and can't be empty, making Objects a good option to store database records by their primary keys, but also any structured object, basically.
The implementation is highly language-dependent and aims to reproduce the behaviour of Typescript Record
implementation as closely as possible. This also means that keys of an Object might have intrinsic enumeration sequence, so order matters. For safety, one should never rely on order of properties in a dictionary, and only resolve dictionary values by their corresponding key.
Object arguments in Functions
are immutable and read-only.
Special types
Binary
A special type that is used for conditional logic. It is essentially a Number but is interpreted as a boolean by the following rules:
- If the value is
Null
or unset, theBinary
is falsy - If the value is 0 or less, the
Binary
is falsy - If the value is 1 or greater, the
Binary
is truthy - Otherwise, the value is rounded by mathematic rules and then is compared again
Collection
This is essentialy a List of Objects, used to store homogenous structures without an index. This is what is typically returned by REST APIs, database queries and various data sources. Despite translating to the same type as List in the output code, in Yantrix this type is pronounced descretely, and passing a non-compliant Tuple/List (i.e., having non-homogenous members) to a function that is explicitely designed to work with Collections will throw an error
Function
Special type exists for Functions, when they are used as another Function
parameter, such as in map
- and filter
-style iterators. This only works when building for languages where callables are first-class citizens. As of now, all output languages supported by Yantrix do qualify.
Properties
Properties are named members of Data Objects
. Inside Yantrix intestines Payload
,Context
and Event Meta
are Objects
, and top-level properties of them can be extracted by name. Deeper nested values (i.e., when property is of Object
type) are extracted with special Functions
The properties extracted from Data Objects
, which are destructured from them based on their Keys. Listing Keys is like declaring variables, which are members of a dereferenced Data Object. Key list is defined as the following:
KEY_ITEM::=<TARGET_PROPERTY>[=<EXPRESSION>]
KEY_LIST::=KEY_ITEM[,KEY_ITEM...]
Expression
is derived from Data Object
properties, bound to any of them, see Expressions
Referencing Data Objects
The choice of Data Object
to attach a property is defined by the directive.
Context
#varName
references a property of Context
inside any Expression
. If the Context
with given name is not available in the calling directive, the property reference is set to Null
, unless provided with a default value
#{KEY_LIST}
is used to reference numerous properties of Context
, mostly in Reducers
and Event Emitters
.
''' declare a Context with three members
#{a,b,c}
''' Declare a Context with three more members, that copy value from previous ones
#{oldA, oldB, oldC} <= #a,#b,#c
''' fire an Event with a numeric property, derived from a function applied to those
emit/someEvent (eventCode) <= #{encode(oldA, oldB, oldC)}
Payload
$varName
references a property of Payload
inside any Expression
. If the Payload
with given name is not available in the calling directive, the property reference is set to Null
, unless provided with a default value
(KEY_LIST)
is used to reference numerous properties of Payload
on the left side of <=
of subscribe
directives
''' Expression that invokes a Function upon a Payload property
func($payloadValue)
''' assign three Payload properties to respective Context properties
#{a,b,c} <= $payloadA, $payloadB, $payloadC
''' Translate an Event with its Meta to Action with Payload
subscribe/someEvent SomeAction ( payloadValue ) <= ( eventMetaValue )
Default values
Default values are assigned to vars when the requested property is absent in Data Object
or equals to Null
(an empty pointer). To assign a default value the KEY_ITEM
is followed by =
and a Constant
or Expression
. For instance:
#{anyValue, stringValue = 'foobar', numericValue = cos(div(%%pi,6))}
Default values can be used both in Source Objects
and in Target Objects
in Reducers
, but they behave differently on either side of <=
token: left-side defaults are used when corresponding right side expression converges to Null
, while right-side defaults are used when attached references are absent in Data Object
or equal to Null
.
In the following example value1
and value2
will not be set to 1
if they already have values, but they will acquire 1
as a starting value when the Context is created for the first time
#{value1, value2} <= #value1 = 1, #value2 = 1
''' same as
#{value1 = 1, value2 = 1} <= #value1, #value2
''' same as shortcut
#{value1 = 1, value2 = 1 }
a difference emerges is there's a Function
in Expression
:
''' in this example, value2 can become Null after the computation
''' if "funcValue" call returns Null, even though value2 has a default value
#{value1, value2} <= #value1 = 1, funcValue(#value2 = 1)
''' however, if also assigned on the left side, it resolves to a default value
#{value1, value2 = 1} <= #value1 = 1, funcValue(#value2 = 1)
When used on both sides of a Reducer
, the Source Object
execution takes precedence when assigning default values, i.e., the code #{a = 1} <= $b = func(2)
is processed in the following manner:
b
is read fromPayload
. If it's absent orNull
, thefunc
is invoked with a parameter of2
. The resulting value is assigned to$b
.- if the value is set, it's assigned to
a
property of the resultingContext
. If the expressions resolves toNull
, that property is filled with value of1
Thus, if no Payload
was given, or it doesn't have b
key, Context
.a
contents will depend on what is returned by func(2)
call.
Constant declaration
All but Object types can be created in-place, as the default value of Context
or Payload
property or as a Function
argument
1
or1.05
is the way to create a Number. Other numeric representations (e.g. hex, exponential, etc.) are supported as long as the target language supports them. Numbers are automatically typecasted to Binary if needed'foo'
creates a String primitive[]
creates an empty List%%foo
is a compile-time Constant, that is passed to a Codegen along with a diagram, primarily being used for behaviors that are dependent on environment and/or CI/CD operations.
Examples
#{a, b, c}
describes a Context
with named properties a
,b
and c
. This notation is a syntactic sugar for #{a, b, c} <= #a, #b, #c
, meaning that values are plain copied from the preceding Context
during the transaction
#{a, b} <= $a, $b
is similar but copies values from Payload
, received by a transaction.
It could be #{a, b} <= $c, $d
, in which case the properties are copied in the correspoding order: Context.a
receives the value of Payload.c
, and Context.b
is updated with Payload.d
.
The same is possible with Context
too, and basically every Expression
can be used on the right side of reducer: #{newA,newB} <= someFunction(#oldA), #oldB = 2
does the following:
- passes
previousContext.oldA
tosomeFunction
and puts the output value tonewContext.newA
- assigns
previousContext.oldB
value tonewContext.newB
, or uses2
numeric primitive instead, ifpreviousContext.oldB
was not defined or set toNull
More lifelike examples:
#{id, name}
: copies corresponding fields from previousContext
#{id, name} <= #id, trim(#name)
does the same, but appliesString.trim()
to source value#{id, name} <= #id, concat(trim(#name),' ',trim(#lastname))
: extracts two properties from the source object, trims them and joins them with a predefined string constant#{counter} <= add(#counter, $increment)
: addsincrement
property of aPayload
to theContext
'scounter
property