LSPS documentation logo
LSPS Documentation
Data Types

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:

datatypehierarchy.png
Built-in 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

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:

<objectName> as <newObjectType>
person as NaturalPerson

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))

Object

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.

void

The void type is used as the return value of methods, functions, and closures that do not return any value.

Simple Data Types

Simple data types contain values without further data type structuring.

Binary

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:

Binary

String

A String holds a sequence of Unicode characters. The data type is implemented by the java.lang.String class.

To define the type:

String

To create an instance:

"Sequence of Unicode characters"

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:

"This" + #10 + "is" + #10 + "a" + #10 + "multiline" + #10 + "string with "+
"multiple" + #10 + " new lines " + #10 + " which represent " + #10 + "line breaks."

To escape characters, use the double-quote (") character:

"This is all one string: ""Welcome to String escaping!"""

To annotate a string that is not to be localized, add the hashtag # sign in front of the string.

#"This is a string value which will not be localized."

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:

def String s := "My String"

Boolean

Boolean objects hold the values null, true or false. It is implemented by the java.lang.Boolean class.

To define the type:

Boolean

To create an instance:

true

To create a local variable:

def Boolean s := true

The expression def Boolean boolVar := true declares and defines a local variable of type.

Integer

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:

Integer

To create a literal:

-100;
//as hexadecimal:
0XFF // == 255

To create a local variable:

def Integer i := 42

Decimal

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:

  • UP: rounds away from zero
  • DOWN: rounds toward zero
  • CEILING: rounds toward positive infinity
  • FLOOR: rounds toward negative infinity
  • HALF_UP: rounds toward the nearest neighbor unless both neighbors are equidistant, in which case it rounds up
  • HALF_DOWN: rounds toward nearest neighbor unless both neighbors are equidistant, in which case it rounds down
  • HALF_EVEN: rounds toward the nearest neighbor unless both neighbors are equidistant, in which case, it rounds toward the even neighbor
  • UNNECESSARY: asserts that no rounding is necessary

The rounding mode is defined for decimal variables or for a record field of type Decimal.

To define the type:

Decimal

To define a Decimal type with Scale 100 and Rounding Model UP:

Decimal(100, UP)

To create an instance:

-10.0;
7E-3 //0.007
6.63E+34

To create a local variable:

def Decimal intVar := 100

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.

Date

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:

Date

To create an instance:

d'yyyy-MM-dd HH:mm:ss.SSS'

To create a local variable:

def Date myDate := d'2017-12-24 20:00:00.000'

When working with dates, consider using the date functions available in the Standard Library.

Local Date

The LocalDate object holds a date without the timezone. It represents the java.time.Local.Date class.

To define the type:

LocalDate

To create an instance:

ld'yyyy-MM-dd'

To create a local variable:

def LocalDate myDate := ld'2017-12-24'
def LocalDate anotherLocalDate := ld'2017-02-24'
def LocalDate yetAnotherLocalDate := ld'2017-02-04'

When working with local dates, consider using the local date functions available in the Standard Library.

Complex Data Types

Complex data types are composite data types based on other data types.

Collections

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:

Collection<T>

List

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:

List<T>

To create an instance:

["a", "b", "c", "d"]

List of lists

[["a", "b"], ["b", "c", "a"]]

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.

Creating a List of Integers with the Range Operator

To create a List of Integers, you can use the .. operator, the range operator:

1..5
//is equivalent to [1, 2, 3, 4, 5]
3..1
//is equivalent to [3, 2, 1]

Note that toString(x..y) returns "x..y".

Set

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:

Set<T>

To create an instance:

//Set<Integer>:
{1,2,3};

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.

Map

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:

//type map with the K type of its keys and the V type of its values:
Map<K, V>

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:

[1->"a", 2->"b"]

To initialize an empty map, use the empty-map operator [->]:

def Map<Object, Object> myEmptyMap := [->];

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".

Reference

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.

  • A Reference to a variable resolves to the variable object
  • A Reference to a record field resolves to the record instance and the association path.

The data type is represented by com.whitestein.lsps.lang.exec.ReferenceHolder.

To define the Reference type:

Reference<T>

To create a Reference instance, use the & reference operator:

&<TARGET>

**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";

Closure

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:

//Syntax: { <INPUT_TYPE_1>, <INPUT_TYPE_2> : <OUTPUT_TYPE> }
{ String : Integer}
//Closure that that has no parameters and returns an Object:
{ : Object}
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:

  • the return type of closure A is a subtype of the return type of closure B
  • and parameter types of closure B are subtypes of parameter types of closure A.

{ 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:

//Syntax: { <PARAMETERS> -> <CLOSURE_BODY> }
{s:String -> length(s)}

Parameter types can be omitted:

{s -> length(s)}

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:

def {Integer:String} closureVar := {x:Integer -> toString(x)};
def String closureResult := closureVar(3);

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.

User-Defined Record

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:

new <RECORD>(<NAME_OF_FIELD> -> <FIELD_VALUE>, <NAME_OF_FIELD> -> <VALUE>, ...))

For example:

new Book(genre -> Genre.FICTION, title -> "Catch 22", author -> new Author(name -> "Heller, Joseph"))

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.

new <RECORD_NAME>(<PROPERTY> -> <VALUE>)

Methods

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:

  • private: accessible only for the Record and its methods (define methods to access from outside)
  • protected: accessible only from within the data type hierarchy (can be used by any subtype)
  • public: accessible from anywhere

Methods support overloading, that is, a Record can define multiple methods with the same name as long as they have different arguments.

Declaring Methods

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:

<RECORD_NAME> {
<METHOD_DEFINITIONS>
}

Individual methods are declared with the following syntax:

/** <description> */
<visibility> <flag> <returnType> <name> (<arguments>){
<methodImplementation>
}

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:

public String getName(){
this.title;
}

When declaring methods, you can use the this keyword to reference the current Record instance:

this.callingRecordMethod();

Note that calling method fails with an exception if the object is null (safe-dot operator equivalent is not available).

Accessing Methods of Super Records

To access methods of Record's supertype, use the super keyword:

//supertype default constructor:
super();
super.methodFromParent();

You can override the inherited methods.

Extension 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);

Calling Methods

Record types can define methods that can be called on their Record instances or their Record, in the case of static methods:

<record_instance>.<method>(<parameters>)

To operate directly over the return values of methods, you can chain the methods:

<recordInstance>.<method1>.<method2>;

Note: Methods cannot use named parameters as its arguments: calls like <record_instance>.<method>(<parameter> -> <value>) is not supported).

Declaring Static Methods

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.

Abstract Records and Methods

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

public abstract Integer getBusinessData();

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

Constructors are Record methods that create and return an instance of the Record.

Constructor Availability

  • If you do not declare a constructor, the following constructors are available:
    • If no default constructor is available, the field constructor is available:
      new Record(property1 -> Value1, property2 -> value2, ...);
      //field constructor with no arguments:
      new Record();
      Note that you can assign a property value only to the visible properties (properties that are public or protected if visible from the given location).
    • If a super Record declares a non-parametric constructor and it can be reached from the child records using the super() constructor, you can use the default constructor:
      new Record();
  • Once you declare a constructor on your Record, the field constructor and the default constructor are no longer available.

Declaring Constructors

You can declare a constructor in method declaration using following the syntax:

public <YourRecordName> (<argument1_type> <argument_name>, <argument2> <argument_name>, ...) {
<ConstructorBody>
}

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 .:

book.title

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

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:

PropertyPath

To create instances :

  • to a Field via one or multiple relationships:
    <Record>.<relationship>.<field>
    MyRecord.myRecordsNavigableRelationship.fieldOfARelatedRecord
  • to a Field on one Record:
    <Record>.<field>
    MyRecord.myRecordField

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 value null 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.

Property

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

def Property authorNameField := Author.firstName;

Type

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:

Type<T>

To create an instance:

//type is a keyword:
type(<TYPE>)

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

Enumeration

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:

<ENUMERATION>

To create an instance:

<enumeration_name>.<literal_name>

Example:

//Weekday is an enumeration with values MONDAY, TUESDAY, etc.
def Weekday wd := Weekday.WEDNESDAY;

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.

Null

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:

Null