Complex Expressions

Complex expressions in EL consist of non-atomic value-returning expressions, in a typed, operator-based syntax common to many programming languages and logics. In EL, the syntactic use of operators is understood as a shorthand for specific functions assumed to be available on types inferred from the context of the operator use. An EL implementation would therefore map such operators to the appropriate methods in a class library.

Equality Operator

The equality operator = in EL is understood as the function equal() defined on the openEHR Foundation type Any, of which every other class is a descendant. For all primitive value types (types for which use in expressions directly generates values rather than instance references), the semantics are value comparison, while for all other types, the semantics are reference comparison. For non-openEHR models, '=' will normally map to a similarly-named method, e.g. equals().

To obtain value comparison for non-value types, the function Any.is_equal(), which may be redefined in any sub-type, is used.

Primitive Operators

Primitive operators in EL are the infix or prefix syntax form of various functions available on primitive types. For example, the operator - (minus) is defined on the class Numeric (an inheritance ancestor of the classes Integer, Real etc) as the following:

    | in Numeric
    subtract (other: Numeric): Numeric
        alias infix '-'

    | redefined in Integer as
    subtract (other: Integer): Integer

This means that where the expression 100 - 5 is encountered in EL, what is really invoked is Integer.subtract(), specifically 100.subtract(5).

For convenience, the operators for the Numeric and Boolean types from the openEHR Foundation Types are reproduced below.

Operators Function Meaning

Arithmetic Operators - Numeric operands and result; descending precendence order

^

exponent()

Exponentiation

*

multiply()

Multiplication

/

divide()

Division

%

modulus()

Modulo (whole number) division

+

add()

Addition

-

subtract()

Subtraction

Relational Operators - Numeric, Date/time operands and Boolean result; equal precedence

=

equal()

Value equality

!=,

not_equal()

Inequality relation

<

less_than()

Less than relation

<=,

less_than_or_equal()

Less than or equal relation

>

greater_than()

Greater than relation

>=,

greater_than_or_equal()

Greater than or equal relation

Logical Operators - Boolean operands and result; descending precendence order

not, ~

not()

Negation, "not p"

and,

conjunction()

Logical conjunction, "p and q"

or,

disjunction()

Logical disjunction, "p or q"

xor,

exclusive_disjunction()

Exclusive or, "only one of p or q"

implies,

implication()

Material implication, "p implies q", or "if p then q"

In addition, some operators are defined on the other primitive types, including the following.

Operator Function Meaning

String Operators

+

String.append(other: String): String

String concatenation, appending

Iso8601_date arithmetic operators

+

add(d: Iso8601_duration): Iso8601_date

Add a precise duration to a date

++

add_nominal(d: Iso8601_duration): Iso8601_date

Add a nominal duration to a date

-

subtract(d: Iso8601_duration): Iso8601_date

Subtract a precise duration from a date

--

subtract_nominal(d: Iso8601_duration): Iso8601_date

Subtract a nominal duration from a date

-

diff(d: Date): Iso8601_duration

Difference of two dates

Iso8601_date_time arithmetic operators

+

add(d: Iso8601_duration): Iso8601_date_time

Add a precise duration to a date/time

++

add(d: Iso8601_duration): Iso8601_date_time

Add a nominal duration to a date/time

-

subtract(d: Iso8601_duration): Iso8601_date_time

Subtract a precise duration from a date/time

--

subtract_nominal(d: Iso8601_duration): Iso8601_date_time

Subtract a nominal duration from a date/time

-

diff(d: Date_time): Iso8601_duration

Difference of two date/times

Iso8601_time arithmetic operators

+

add(d: Iso8601_duration): Iso8601_time

Add a duration to a time

-

subtract(d: Iso8601_duration): Iso8601_time

Subtract a duration from a time

-

diff(d: Time): Iso8601_duration

Difference of two times

Iso8601_duration arithmetic operators

+

add(d: Iso8601_duration): Iso8601_duration

Add a duration to a duration

-

subtract(d: Iso8601_duration):Iso8601_duration

Subtract a duration from a duration

Operator semantics that require further explanation are described below.

Logical Negation

All Boolean operators take Boolean operands and generate a Boolean result. The not operator can be applied as a prefix operator to all operators returning a Boolean result as well as a parenthesised Boolean expression.

Precedence and Parentheses

The precedence of operators follows the order shown in the operator tables above. To change precedence, parentheses can be used in the fashion typical of most programming languages, as shown below.

    systolic_bp > 140 and (is_smoker or is_hypertensive)

Collection Operators

The two standard quantification operators from predicate logic there exists (∃ operator) and for all (∀ operator) are defined in EL for the container types found in the openEHR Foundation Types.

The textual syntax of there exists is as follows:

    there_exists v in container_var | <Boolean expression mentioning v>

Here, the | symbol is usually read in English as 'such that'. The symbolic equivalent may also be used:

    ∃ v : container_var | <Boolean expression mentioning v>

The above may also be expressed in EL as its functional equivalent:

    list_of_reals: List<Real>

    |
    | an expression that will return true if list_of_reals
    | contains a value greater than 140.0
    |
    list_of_reals.there_exists (
        agent (v: Real): Boolean {
            v > 140.0
        }
    )

The for_all operator has similar textual syntax:

    for_all v in container_var | <Boolean expression mentioning v>

Here, the | symbol is normally read in English as as 'it holds that'. The symbolic equivalent may also be used:

    ∀ v : container_var | <Boolean expression mentioning v>

The above may also be expressed in EL as its functional equivalent:

    list_of_reals: List<Real>

    |
    | an expression that will return true if list_of_reals
    | consists of values all greater than 140.0
    |
    list_of_reals.for_all (
        agent (v: Real): Boolean {
            v > 140.0
        }
    )

Decision Table Expressions

In EL, a decision table is a construct that expresses the equivalent logic of a multi-branch construct that returns a single expression as a result. There are two flavours, both familiar to programmers in mainstream languages: the condition chain (i.e. an if/then/else construct) and the case table (i.e. a case statement). The evaluation of both constructs determines which of a number of possible expressions to return as the result, based on the prior evaluation of branch conditions, whose particular form depends on which flavour of construct is used. Both constructs are thus purely functional, i.e. their branches cannot contain statements (i.e. assignments, procedure calls etc), only expressions.

Condition Chain (if/then)

The syntax for a condition chain (the if/then equivalent) takes a standard form and a compact form. The standard form is as follows.

    choice in
        <condition_1>:  <expression_1>,
        <condition_2>:  <expression_2>,
        ...
        <condition_N>:  <expression_N>,
        *:              <else expression>
    ;

In the above, the '*' character is understood as a wildcard, meaning 'all other cases'. A final row containing '*' is thus equivalent to a catch-all 'else' branch in the if/then/else chain of a procedural language.

A realistic example is illustrated below, making use of line comments to visually aid the author.

    molecular_subtype: Terminology_term
        Result := choice in
            =========================================================
            er_positive and
            her2_negative and
            not ki67.in_range ([high]):    [luminal_A],
            ---------------------------------------------------------
            er_positive and
            her2_negative and
            ki67.in_range ([high]):        [luminal_B_HER2_negative],
            ---------------------------------------------------------
            er_positive and
            her2_positive:                 [luminal_B_HER2_positive],
            ---------------------------------------------------------
            er_negative and
            pr_negative and
            her2_positive and
            ki67.in_range ([high]):        [HER2],
            ---------------------------------------------------------
            er_negative and
            pr_negative and
            her2_negative and
            ki67.in_range ([high]):        [triple_negative],
            ---------------------------------------------------------
            *:                             [none]
            =========================================================
        ;

For the common degenerate case where there is a single condition, the standard form looks as follows:

    calculate_score: Integer
        Result := choice in
            ============
            expr1:    2,
            ------------
            *:        0
            ============
        ;

While perfectly understandable (and legal syntax), the following compact form may be used instead:

    calculate_score: Integer
        Result := expr1 ? 2 : 0

The above syntax is adopted from the C language family. It may be used to construct intelligible conditional arithmetic operations such as summing, e.g.:

    ipi_raw_score: Integer
        Result := Result.add (
            =============================================
            age > 60                             ? 1 : 0,
            staging ∈ {[stage_III], [stage_IV]} ? 1 : 0,
            ldh.in_range ([normal])              ? 1 : 0,
            ecog > 1                             ? 1 : 0,
            extranodal_sites > 1                 ? 1 : 0
            =============================================
        )
        ;

Case Table

The Case Table syntax form (case statement equivalent) is logically no different from the more general condition chain, except that every branch condition expression takes the form Expr ∈ Constri, where Expr is the same expression left-hand side for all branches, each having a variable right-hand side in the form of a value range constraint. Here the operator is read as 'is in', i.e. set-membership. The case table construct is designed to enable the value of a single determining expression to be tested against any number of value ranges. This is illustrated in the following example:

    gfr_range: Real

    risk_assessment: Real
        Result := case gfr_range in
            =================
            |>20|:      1,
            |10 - 20|:  0.75,
            |<10|:      0.5
            =================
        ;

This expression returns one of the values 1, 0.75 or 0.5, depending on the evaluated value of gfr_range, but it could equally return the value of a more complex expression, including further instances of Case tables, Condition chains, operator expressions etc.

Two-dimensional Tables (experimental)

Two-dimensional decision tables are common in all sectors. Although they can be reduced to a condition chain, EL provides a more direct syntax that enables them to be expressed in a form visually very close to their logical form.

item in
    ==========================================================================
                   {    isEconomy(p),   isBusiness(p),      isFirstClass(p) },
    --------------------------------------------------------------------------
    isChild(p):    {    50,             250,                1000            },
    --------------------------------------------------------------------------
    isAdult(p):    {    250 + trip.d,   450 + trip.d,       750 + trip.d    },
    --------------------------------------------------------------------------
    isMilitary(p): {    90,             250,                750 - 2 * p.age }
    ==========================================================================
;