The LSPS Expression Language is a statically typed language: That means that all values are of a given data type which does not change on runtime: If you create a String literal it cannot spontaneously act as an Integer; When you declare a local string variable, it can only reference a string value, etc.
Data types are part of the data type hierarchy with the Object data type as the root of the tree with the sole exception being the void type. Depending on their position in the hierarchy, they inherit properties from each other: one type becomes the supertype of another type. An object of a data type can be used anywhere where you can use its super type. For example, you can assign a variable of type Decimal also a value of type Integer, since Integer is a subtype of Decimal. These relationships apply to built-in as well as custom data types in a hierarchy.
Built-in data types create the basic data type hierarchy:
All data types are part of the data type hierarchy with the Object data type as the root of the tree with the sole exception being the void type.
Casting takes place when you change the type of a value to another type. When "widening" a type, that is changing a value of a subtype to its supertype, the type change occurs automatically. When "narrowing" a type, you need to cast the type explicitly:
You can check the type of an object with the instanceof operator.
Note: Alternatively, you can use the
cast()
function of the Standard Library, for example,cast(o, type(D))
The Object data type is the super type of all data types and therefore the only data type without a supertype. It is represented by java.lang.Object. Therefore if you want to allow any data type, for example, as a parameter, use the Object data type. It is represented by the java.lang.Object class. All objects can hold the null
value.
The void type is used as the return value of methods, functions, and closures that do not return any value.
Simple data types contain values without further data type structuring.
Objects of binary data type are typically used to hold binary data when downloading and uploading files, or working over binary database data.
In the Java API, it is represented by com.whitestein.lsps.lang.exec.BinaryHolder.
Binary literals are not supported.
To define the type:
A String holds a sequence of Unicode characters. The data type is implemented by the java.lang.String
class.
To define the type:
To create an instance:
A String can contain special characters defined using their ASCII codes. For example, you can use the ASCII tab code (#9
) to have a tab in your String, the line feed character (#10
) to insert the end of a line and make a multi-line String, etc., for example:
To escape characters, use the double-quote ("
) character:
To annotate a string that is not to be localized, add the hashtag # sign in front of the string.
The hashtag # sign signalizes that the String is to remain unlocalized and that this was the intention of the developer. Such Strings are excluded from checks of unlocalized Strings.
To create a local variable:
Boolean objects hold the values null
, true
or false
. It is implemented by the java.lang.Boolean
class.
To define the type:
To create an instance:
To create a local variable:
The expression def Boolean boolVar := true
declares and defines a local variable of type.
Integer objects hold natural numbers and their negatives. The data type is a subtype of the Decimal type. It is represented by com.whitestein.lsps.lang.Decimal.
Note that the underlying Java type is BigDecimal; hence no relevant maximum limit applies to the value.
To define the type:
To create a literal:
To create a local variable:
Decimal objects hold numerical fixed-point values. It is represented by com.whitestein.lsps.lang.Decimal.
Note: A Decimal type is internally represented by two integer values: an unscaled value and a scale. The value is hence given as
<UNSCALED_VALUE>*10**<SCALE_VALUE>
. The integer scale defines where the decimal point is placed on the unscaled value. The scale is a 32-bit integer. If zero or positive, the scale is the number of digits to the right of the decimal point. If negative, the unscaled value is multiplied by ten to the power of the negation of the scale. Decimal values are, for example, 2e+12, -1.2342e0, 1.0.
When assigning a value of type Decimal, you need to define the scale and rounding mode. The following rounding modes can be used on decimals:
The rounding mode is defined for decimal variables or for a record field of type Decimal.
To define the type:
To define a Decimal type with Scale 100 and Rounding Model UP:
To create an instance:
To create a local variable:
Important: Decimal values are normalized if they contain 0 digits after the decimal point: decimal value 1.0 and integer value 1 have the same numerical value and therefore
1.0 == 1
.
The Date object holds a specific time. It represents the java.util.Date class.
Important: Only dates since the introduction of the Gregorian calendar, that is, the year 1582 are supported: for Dates that occurred before, a shift in days can occur rendering the date incorrect.
To define the type:
To create an instance:
To create a local variable:
When working with dates, consider using the date functions available in the Standard Library.
The LocalDate object holds a date without the timezone. It represents the java.time.Local.Date class.
To define the type:
To create an instance:
To create a local variable:
When working with local dates, consider using the local date functions available in the Standard Library.
Complex data types are composite data types based on other data types.
A Collection is a groupings of items of a particular data type. It is represented by com.whitestein.lsps.lang.exec.CollectionHolder.
Collections are ordered and immutable: once a collection is created, it cannot be changed, while you can change individual collection items. Each item of the collection represents an expression
To access an element of a Collection, you need to specify the position of the element in the List. Note that the first element of a List is on position zero. For example, [10,20,30][1]
returns 20 as position 0 of this list points to the value 10, the first element of the list.
Hierarchy of collections follows the hierarchy of their elements: List<TA> is a subtype of List<TB>, if TA is a subtype of TB. Set<TA> is a subtype of Set<TB>, if TA is a subtype of TB.
To define the type:
A List represents an ordered collection of items of a type with possible duplicate values. It is represented by the com.whitestein.lsps.lang.exec.ListHolder class.
To define the type:
To create an instance:
List of lists
To access an item in a List: Specify the position of the item in the List starting from 0: For example, [10,20,30][1]
returns 20 as position 0 points to the value 10, the first element.
Unlike on Sets, accessing list items is performance-wise efficient.
To create a List of Integers, you can use the ..
operator, the range operator:
Note that toString(x..y)
returns "x..y"
.
A Set represents an ordered collection of items of a type with no duplicate values. It is represented by the com.whitestein.lsps.lang.exec.SetHolder class.
To define the type:
To create an instance:
To access an item in a Set: Specify the position of the item in the Set starting from 0: For example, {10,20,30}[1]
returns 20 as position 0 points to the value 10, the first item.
Unlike in Lists, accessing items is performance-wise inefficient.
Maps hold keys with their values and are immutable: once a map is created, it cannot be changed. You can change its key-value pairs, however, the map itself cannot be modified.
It is represented by com.whitestein.lsps.lang.exec.MapHolder.
To define the type:
Note that hierarchy of maps follows the hierarchy of its key and value types: Map<KA, VA> is a subtype of Map<KB, VB>, if KA is a subtype of KB and VA is a subtype of VB.
To create an instance:
To initialize an empty map, use the empty-map operator [->]
:
To get a value of a key, specify the key for the appropriate value in square brackets, for example, ["firstKey"->"a", "anotherKey"->"b"]["anotherKey"]
returns the string "b".
A Reference holds a reference expression that resolves to a variable or a record field (slot), not their value.
A Reference is conceptually similar to pointers in other languages. However, a Reference points to a variable or a record field, not to memory slot.
The data type is represented by com.whitestein.lsps.lang.exec.ReferenceHolder.
To define the Reference type:
To create a Reference instance, use the & reference operator:
**To get the value in the referenced slot, use the dereference operator.
Example Use of Reference
//creates new Patient record instance with diagnosis "flu": def Patient r := new Patient(diagnosis -> "flu"); //creates the local variable status that holds the reference to the diagnosis field of the Patient record instance: def Reference<String> status := &(r.diagnosis); //function sets the status to cured on the patient: setStatusToCured(&r.status); //implementation of the setStatusToCured() function: //def Reference<String> status:= statusReference; //*status:="cured";
A closure is an anonymous function that can declare and make use of local variables in its own namespace and use variables from its parent expression context as opposed to lambdas. It is represented by com.whitestein.lsps.lang.exec.ClosureHolder.
To define the type:
def { String : String } myClosureVar := { s:String-> callingFunction(s) }
myClosureVar("my string")
def { String : String } myClosureVar := { s:String-> callingFunction(s) }
myClosureVar("my string")
Subtyping in closures is governed by their parameters and return type: Closure A is a subtype of closure B when:
{ S1,S2,... : S } is subtype of { T1, T2,... : T } when T1 is subtype of S1, T2 is subtype of S2, etc. and S is subtype of T.
To create an instance:
Parameter types can be omitted:
The system attempts to infer the type of closure arguments and its return value. Note that the types might be resolved incorrectly. To prevent such an event, consider defining the argument type explicitly as in the example.
To evaluate a closure use the parentheses ()
operator with the closure arguments:
Important: It is recommended to always explicitly define the type of the closure input arguments, in the example, we explicitly define that x is an Integer in
x:Integer
.
A user-define Record is the subtype of the Record type. It serves to create custom structured data types. A Record can define:
It is not possible to declare a Record type in the Expression Language: Records are modeled in the data-type editor of the Living Systems® Designer. However, you can create instances of Records. When you pass a Record, for example, as a function argument, it is passed by reference.
It is represented by com.whitestein.lsps.lang.exec.RecordHolder.
To create an instance:
For example:
Use the dot operator to access Record Fields or Properties (Fields of related Records using the Relationship name), and methods.
Example: def declares a new variable of the MyRecord type. new creates a new MyRecord instance, which is assigned to the variable (the instance is the proper memory space with the record) . Variable r points to the MyRecord instance and it is returned by the expression.
A method is a piece of code similar to function but defined for a specific Record that performs an action on the Record instance. The action is defined as the method body: Method body can use Record's fields, methods, global variables and global functions. If the Record has a supertype, also the public methods of the supertype Record are accessible and can be overridden by the subtype method. To call the methods of the supertype Record, use the super keyword.
Important: While you can override the toString() method of Records so that when you call the toString() method on your Record, the system will use your method; however, it is not possible to override the equals() and hashcode() methods.
Methods are declared in method definition files with every Record having its own dedicated methods file.
Methods define their visibility:
Methods support overloading, that is, a Record can define multiple methods with the same name as long as they have different arguments.
When declaring methods and constructors, you can reference the current Record object with the 'this' keyword
Methods of a Record are declared and defined for the given Record in the method definition:
Individual methods are declared with the following syntax:
Note: To indicate that a method does not return any value, set the return type to
void
. It is not recommended to use Null.
Example method:
When declaring methods, you can use the this
keyword to reference the current Record instance:
Note that calling method fails with an exception if the object is null
(safe-dot operator equivalent is not available).
To access methods of Record's supertype, use the super
keyword:
You can override the inherited methods.
Extension methods are special function types that are defined for a particular data type. Unlike common methods, they enable you to add methods to existing types, and that, including types from Standard Library, so you do not need to create a new type for your method.
Extension methods are declared the type they are defined for in a function definition file, which cannot be created with the Expression Language. They can be called either like methods or functions.
Important: Extension methods are resolved as functions (not as methods) so that the extension method in the lowest context is used.
To declare an extension method on an existing Type, use the following syntax:
@ExtensionMethod
<visiblity> <returnType> <nameOfExtensionMethod>(<extendedType><inputParams>) {
<implementation>
}
Example implementation:
@ExtensionMethod
private String extensionMethodOfString(Integer i) {
"String value returned by the extensionMethodOfString() over Integer " + i
}
Example calls of an extension method:
EntityGrid.removeAllColumns();
//or
removeAllColumns(EntityGrid);
Record types can define methods that can be called on their Record instances or their Record, in the case of static methods:
To operate directly over the return values of methods, you can chain the methods:
Note: Methods cannot use named parameters as its arguments: calls like
<record_instance>.<method>(<parameter> -> <value>)
is not supported).
You can declare a method as a static method: static methods are methods that belong to the Record, not a Record instance and as such are shared between all instances of the Record.
Important: Static methods can be declared only on Records. It is not possible to create static methods on Interfaces.
To declare a method as static, you need to prepone the method with the keyword static.
If a Record is abstract, it cannot be instantiated, but it can be used as a super Record. An abstract Record can define abstract methods: abstract methods do not contain any implementation but any child Records must implement abstract methods of their super Records.
Abstract Records and methods serve to ensure that their child Records inherit the Fields and methods: the child Record inherits the Fields and must implement the abstract methods of the abstract Record.
When depicted in Diagrams, names of abstract Records and Methods are in italics.
An abstract method is declared as
Note: Records are marked as abstract using the abstract flag, which is a Modeling Language feature: it is not possible to mark a Record as abstract only using the Expression Language.
Constructors are Record methods that create and return an instance of the Record.
super()
constructor, you can use the default constructor: You can declare a constructor in method declaration using following the syntax:
The body of constructor is governed by the same rules as a method bodies.
Parametric constructor example:
Book {
public Book(Integer isbn){
super();
//sets Field:
this.isbn := isbn;
}
}
To access fields of a Record, use the access operator .
:
Note that the dot operator .
fails with an exception if the <EXPRESSION>
with the access operator is null
. Use the safe-dot operator to prevent the exception.
When accessing a field or record that is of type Reference, the property is automatically dereferenced. Therefore the expression (*ref).fieldName
and ref.fieldName
are identical.
Property Path holds a route from a Record to a Record Field, for example,Employee.name
.
You can use Relationships to define a path to a Field of another Record, such as, Employee.address.city
where address is the name of the Relationship to another Record, for example, EmployeeAddress.
To define the type:
To create instances :
Note: To prevent the system from raising an exception when accessing a field that is
null
, you can use the safe.dot?.
operator. The operation then returns the valuenull
if the reference expression is null (refer to safe-dot operator.
When accessing a Field or Record that is of type Reference, the property is automatically dereferenced. Hence the expression (*ref).fieldName
and ref.fieldName
are identical.
The Property data type holds the type of a Field in a user-defined Record.
Note: Property is a special type of property path (Property Path): use PropertyPath type instead if possible.
Example
The Type data type holds data types, for example, a Type can hold the value String data type, a particular Map data type, a Record data type, etc. It is represented by com.whitestein.lsps.lang.type.Type.
The Type data type can be used to check if object are of a particular data type. The output can be then further used when Casting the object.
To define the type:
To create an instance:
Example Usage
switch typeOf(person) case type(NaturalPerson) -> getFullName(person as NaturalPerson) case type(LegalPerson) -> getFullName(person as LegalPerson) case type(PersonGroup) -> getFullName(person as PersonGroup) default -> person.code + #" <Unknown type>" end
An Enumeration is a data type that holds literals. Each literal is of the enumeration type. You cannot create an Enumeration type in the Expression Language directly: It is modeled as a special Record type.
To define the type:
To create an instance:
Example:
You can compare enumeration literals with the comparison operators ==, !=, <, >, <=, >= for enumerations of the same enumeration type. Enumeration literals are arranged as a list of values; hence the comparison is based on comparing their indexes: the order depends on the order of the literal as modeled in the Enumeration.
In LSPS Application, enumeration is represented by com.whitestein.lsps.lang.exec.Enumeration java class.
The Null data type signalizes an unspecified value: its only value is null
.
The data type is the subtype of every other data type, so that any object can take the null
value.
To define the type: