Skip to main content

Expressions

The body of transforms is specified using Flex's language of expressions that is syntactically similar to C++ and Java expressions. We will discuss Flex expressions informally here. For the complete grammar of Flex including its expression language, see Flex Complete Syntax.

Literals

Literals for built-in types are similar to literals in other languages. Integer literals in Flex can be used as values of any of the integral data types (int8, int16, ..., uint8, uint16, ...). Decimal literals can be used as values of float32 and float64.

42                       // integral literal
-42                      // integral literal
1_000_000                // integral literal
3.14                     // decimal literal
6.022e23                 // decimal literal
0xC0A86401               // integral literal (hexadecimal)
0b0000111100001111       // integral literal (binary)
"foo bar"                // string literal
true                     // bit literal
false                    // bit literal

Arithmetic and Logic

Flex has arithmetic, logical, and comparison operators similar to other languages.

The arithmetic operators + - * / % and comparison operators > >= < <= are defined over all numeric types.

(42: int32) + 8             // ~> 50: int32
(42: int32) - 8             // ~> 34: int32
(42: int32) * 8             // ~> 316: int32
(42: int32) / 8             // ~> 5: int32
(42: int32) % 8             // ~> 2: int32

(40: int32) == 42           // ~> false
(40: int32) != 42           // ~> true
(40: int32) > 42            // ~> false
(40: int32) >= 42           // ~> false
(40: int32) < 42            // ~> true
(40: int32) <= 42           // ~> true

The binary logical operators && || are defined over bit as well as integral types (as bitwise operations). Unary negation ! is defined over bit only.

true && false               // ~> false
true || false               // ~> true
(5: uint8) || 3             // ~> 7: uint8
(5: uint8) && 3             // ~> 1: uint8
!true                       // ~> false

The bit shift operators << (left-shift) and >> (right-shift) are defined over intN and uintN.

0b01000000 : uint8 << 1    // ~> 0b10000000
0b10000000 : uint8 >> 1    // ~> 0b01000000
4 : uint32 << 2            // ~> 16
1024 : uint32 >> 1         // ~> 512
4 : int64 << 2             // ~> 16
1024 : int64 >> 1          // ~> 512
-8 : int64 >> 2            // ~> -2

Equality operators == != are defined over all types. Inequality is just the negation of equality (i.e., x != y is equivalent to !(x == y)).

(42: int32)      == 42                            // ~> true
false            == false                         // ~> true
"foo"            == "foo"                         // ~> true
Pos{ x=0; y=0; } == Pos{ x=0; y=0; }              // ~> true
Pos{ x=0; y=0; } == Pos{ y=0; x=0; }              // ~> true
(true, "foo")    == (true, "foo")                 // ~> true

(42: int32)      == 43                            // ~> false
true             == false                         // ~> false
"foo"            == "bar"                         // ~> false
Pos{ x=0; y=1; } == Pos{ x=0; y=0; }              // ~> false
Pos{ x=0; y=1; } == Pos{ y=0; x=1; }              // ~> false
(true, "foo")    == (true, "bar")                 // ~> false

For all binary operators, Flex requires that the left- and right-hand sides have the same type, and there is no implicit type coercion.

(42: int32) + (43: int16)                         // type-checking error
(42: int32) < (43: int16)                         // type-checking error
(42: int32) == (42.0: float64)                    // type-checking error
true == "true"                                    // type-checking error
true != (0: int32)                                // type-checking error
(42: int8, "foo") == ("foo", 42: int8)            // type-checking error

Although there is no implicit type conversion, Flex can infer the type of numeric literals as long as there is only a single type that would satisfy typing constraints. A type ascription (e.g., : int32) is usually enough information to satisfy the type checker when there would otherwise be ambiguity. For example, the types of the literals in 42 + 8 * 9 are ambiguous, making the expression invalid. However, (42: int32) + 8 * 9 as well as (42 + 8 * 9) : int32 are both acceptable.

Casting

Flex supports several ways to cast numeric values to other numeric types. value_cast is used when the type conversion from type A to B is guaranteed to succeed for all values of type A (e.g., uint16 to uint32). If the cast may fail for some A, then value_cast? is used instead which returns an Optional<B>. For converting float64 to float32, there is a special function float64to32 that performs a lossy conversion.

value_cast<int32>(42 : int16)        // ~> 42 : int32
value_cast<int32>(42: uint16)        // ~> 42 : int32
value_cast<float64>(3.14 : float32)  // ~> 3.14 : float32

value_cast?<int32>(42 : int16)       // ~> some(42 : int32)
value_cast?<int8>(127 : int16)       // ~> some(127 : int8)
value_cast?<int8>(128 : int16)       // ~> none
value_cast?<int8>(128 : uint8)       // ~> none
value_cast?<uint8>(-42 : int8)       // ~> none

float64to32(3.14)                    // ~> 3.14 : float32

value_cast? is often used with the getOrElse function to provide an alterative value if the cast fails:

getOrElse(value_cast?<uint8>(42: int8), 0)      // ~> 42 : uint8
getOrElse(value_cast?<uint8>(-42: int8), 0)     // ~> 0 : uint8

Flex does not include built-in functions for casting to and from bit and string types. Since there are choices to be made about precisely how such conversions would be implemented, we leave it to the user to define them as helper functions. As an example:

function bit2uint8(b: bit) -> uint8 =
  match (b) {
    false => 0x00;
    true  => 0xFF;
    // true => 0x01;      // another possibility
  };

Statements and Blocks

Flex takes after functional languages in that computation is specified primarily with expressions rather than statements. However, Flex does include a few statement forms that are syntactically reminiscent of imperative languages.

Statements must appear in a block, an expression form composed of a sequence of statements and a final expression:

{
  let pi: float64 = 3.14;
  let radius: float64 = diameter / 2.0;
  assert radius > 0;
  3.14 * radius * radius;
}

The let statement binds the result of an expression to an identifier. Bound identifiers are in scope for subsequent statements and the final expression. They are not in scope outside the block.

let radius: float64 = diameter / 2.0;

If the result of the expression is a tuple, then the contents of the tuple can be unpacked and bound to two or more identifiers.

let (x, y) = rotatePoint2D((3.0, 4.0), 45.0);

Note that since Flex does not have mutable state, binding an expression to an identifier is not the same as assigning a value to a variable in C++. The value bound to an identifier cannot be "changed" during execution. Flex also does not support shadowing: an identifier x cannot be introduced if another identifier named x is already in scope.

The assert statement causes the surrounding block expression to fail if a condition is not satisfied.

assert radius > 0;

The result of evaluating a block expression is the result of the final expression, taking into account identifiers bound by let statements and failures prescribed by assert statements.

The bodies of functions and transforms are usually block expressions because let bindings provide a convenient way to break down complex calculations. If the body is a block expression, the = sign that would otherwise precede the body may be omitted.

function eulerToQuaternion(e: Euler) -> Quaternion {
  let t0 = cos(e.Yaw * 0.5);
  let t1 = sin(e.Yaw * 0.5);
  let t2 = cos(e.Roll * 0.5);
  let t3 = sin(e.Roll * 0.5);
  let t4 = cos(e.Pitch * 0.5);
  let t5 = sin(e.Pitch * 0.5);

  Quaternion {
    w = t2 * t4 * t0 + t3 * t5 * t1;
    x = t3 * t4 * t0 - t2 * t5 * t1;
    y = t2 * t5 * t0 + t3 * t4 * t1;
    z = t2 * t4 * t1 - t3 * t5 * t0;
  };
}

Conditionals

Flex supports two kinds of conditional evaluation: if-else expressions and match expressions.

The if-else expression is comparable to the ternary operator ? : in C++ and Java, though it syntactically resembles the if-else blocks from those languages. Note that in Flex, if-else is not a block that contains statements. It is an expression that returns either the value of the then branch or the value of the else branch. Because of this the types of the then and else branches must be compatible.

The syntax if-else can be written using then as a separating token or with the condition wrapped in parentheses:

const x: int32 = 42;

if x < 100 then x else x - 100;         // ~> 42: int32
if x < 20 then x else x - 20;           // ~> 22: int32

if x > 100 then x - 100
else if x > 10 then x - 10
else x                                  // ~> 32: int32

if (x > 100) {
  x - 100;
} else if (x > 10) {
  x - 10;
} else {
  x;
}                                       // ~> 22: int32

Flex also supports conditional evaluation based on pattern matching (see Pattern Matching).

Tuples and Arrays

Aside from user-defined types, Flex has built-in support for two kinds of aggregate data structure: tuples and arrays (e.g., heterogeneous and homogeneous lists).

Tuples

A tuple is a list of two or more values, possibly of different types. A tuple is introduced by listing its elements inside parentheses, for example (42: int8, "foo", true). Tuples are usually used by unpacking the contents using a let statement:

// Simple tuple:
let (x, y, z) = (1: int32, 2: int32, 3: int32);

// Alternative syntax:
let (x, y, z) = (1, 2, 3) : (int32, int32, int32);

// Nested tuples:
let (c, (d,e)) = (3, ("foo", 1.7)): (int32, (string, float32));

Tuple elements can be accessed by index:

let mytup = ("foo", 42);
let str = mytup._0; // Access first element
let num = mytup._1; // Access second element

function TupleIndexAccess(tup: (int32, string)) -> (string, int32) {
  (tup._1, tup._0);
  // returns a tuple with elements in a different order
}

Tuple values can be discarded using an underscore _ character.

function TupleDiscard(tup: ((string, string), int32)) -> string {
  let ((f,_),_) = tup;
  f;
}

Arrays

An array is a list of zero or more values of the same type. An array is introduced by listing the elements inside square brackets (e.g., ["one", "two", "three"]). The primitive operations on arrays are access [], concatenation ++, length, and last.

const squareNums: uint8[] = [1, 4, 9, 16];
squareNums[0 : int32]                          // ~> 1 : uint8
squareNums[4 : int32]                          // fails
squareNums[0 : int32 .. 2 : int32]             // ~> [1, 4, 9] : uint8[]
squareNums ++ [25, 36]                         // ~> [1, 4, 9, 16, 25, 36]: uint8[]
length(squareNums)                             // ~> 4: uint32
last(squareNums)                               // ~> 16: uint8
last([]: uint8[])                              // fails

Arrays can also be iterated over using an array comprehension. For example, here is an array comprehension that adds one to each element of the array squareNums:

[x + 1 for x in squareNums]                    // ~> [2, 5, 10, 17]: uint8[]

The values can be filtered using an if clause:

[x + 1 for x in squareNums if x%2==0]          // ~> [5, 17]: uint8[]

Sometimes the element-wise operation needs to depend on the results of previous operations. Information can be carried from index to index using scan such as in this example to compute an array of cumulative sums:

const evens: uint8[] = [2, 4, 6, 8];

[cumulSum for x in evens scan cumulSum: uint8 = 0 in cumulSum + x]
                                               // ~> [2, 6, 12, 20]: uint8[]

You can also zip arrays together by separating multiple for clauses with the zip keyword:

[x+y for x in squareNums zip for y in evens]   // ~> [3, 8, 15, 24]: uint8[]

Optionals

Flex also has built in support for optional values. A value of type Optional<A> has one of two forms: some(a) where a is a value of type A, or none. An Optional<A> value is like a box that might contain a value of type A, or it might contain nothing. Optionals in Flex are often used with pattern matching.

function safeDivide(num : float32, den : float32) -> Optional<float32> {
  if den == 0.0 then none else some(num / den);
}

const safeDivideResult : Optional<float32> = safeDivide(2.0,3.0);
match(safeDivideResult) {
  some(result) => result;
  none => 0.0;
}
// ~> 0.6666667: float32

Try

Using the try expression, Flex allows the lifting of potentially partial values, code that might throw an exception once transpiled, into the Optional type . A user can then use pattern matching to decide what to do in the case of failure and success. For example, instead of using a function that inspects the value of the denominator, like safeDivide, division over integers can be performed naturally using try to account for the possibility of failure.

const n : int32 = ...;
const tryDivide : Optional<int32> = try(n / 0);
match(tryDivide) {
  some(result) => result;
  none => 0;
}
// ~> 0 : int32

Fold

Fold is a looping construct in Flex that allows you to safely iterate over the items in an array to build up a new value. For those unfamiliar with fold in other programming languages it is fairly close in concept to a foreach loop.

It is treated as an expression in Flex whose syntax closely resembles a function declaration. It takes 2 arguments followed by an expression block.

  • The first parameter is a variable declaration with an initial value. This variable is how you will build up your result value through the use of controlled mutation, e.g. acc : int32 = 0;

    Note: This value can be any type and does not need to match the type of the array value

  • The second parameter takes the form ident in exp, e.g. x in xs, where the first identifier is variable name you wish to bind the each array value to and the expression is the array you wish to iterate over.

The last part of a fold expression is the expression block code you want to execute to build up your result value for each item in your list. In addition to what is already in scope, you have your accumulator variable and list value available to you. The result of this expression block must match the same type as your accumulator value. The accumulator value for each iteration is equal to the result of the previous iteration, with the value of the first iteration being equal to the initial value provided in its declaration.

The result of the fold expression is the value of the accumulator from the final iteration of the array.

Note: fold iterates over the list of values from left to right, i.e. starting from index 0

It is useful to combine fold with other built-in function like range, zip, or enumerate.

Examples

Sum up all the values in a list of integers:

function sumNums(xs: int32[]) -> int32 {
   fold(sum = 0; x in xs) {
     sum + x;
   };
}

Filter out all odd numbers from a list of integers:

function evens(nums: int32[]) -> int32[] {
  fold(result = []; i in nums) {
    match(i % 2) {
      0 => result ++ [i];
      1 => result;
    };
  };
}

Use fold like "scan" to keep track of each incremental result:

function sumScan(xs: int32[]) -> (int32,int32[]) {
  fold(tup = (0,[]); x in xs) {
    let next = tup._0 + x;
    (next, tup._1 ++ [next]);
  };
}

Fold and zip can be used to easily compute the dot product of two 1-d arrays:

function dotProduct(xs: int32[], ys: int32[]) -> int32 {
  fold(product = 1; (x,y) in zip(xs, ys)) {
    product * x * y;
  }
}

Map

Map is a looping construct in Flex that allows you to iterate over items in an array and process expression block code for each item, returning a transformed array of the same size.

Example

Transform a list of integers into a list of its values squared:

function squaredList(xs: int32[]) -> int32[] {
  map(x in xs) {
    x * x;
  };
}

Filter

Filter is a looping construct that allows you to iterate over items in an array, and return a new array that only contains the items for which the filter body is true. If the value of a particular item is true, then the item is kept, if not it is removed. The filter body only accepts boolean expressions.

Examples

Filter a list of integers into a list of even integers:

function evens(xs: int32[]) -> int32[] {
  filter(x in xs) { 
    x % 2 == 0; 
  };
}

Filter a list of integers into a list of integers that are both negative and odd:

function oddNegatives(xs: int32[]) -> int32[] {
  filter(x in xs) { 
    x < 0 && x % 2 == 1; 
  };
}