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
.
flex
42 // integral literal-42 // integral literal1_000_000 // integral literal3.14 // decimal literal6.022e23 // decimal literal0xC0A86401 // integral literal (hexadecimal)0b0000111100001111 // integral literal (binary)"foo bar" // string literaltrue // bit literalfalse // 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.
flex
(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.
flex
true && false // ~> falsetrue || false // ~> true(5: uint8) || 3 // ~> 7: uint8(5: uint8) && 3 // ~> 1: uint8!true // ~> false
Equality operators == !=
are defined over all types. Inequality is just the negation of equality (i.e., x != y
is equivalent to !(x == y)
).
flex
(42: int32) == 42 // ~> truefalse == false // ~> true"foo" == "foo" // ~> truePos{ x=0; y=0; } == Pos{ x=0; y=0; } // ~> truePos{ x=0; y=0; } == Pos{ y=0; x=0; } // ~> true(true, "foo") == (true, "foo") // ~> true(42: int32) == 43 // ~> falsetrue == false // ~> false"foo" == "bar" // ~> falsePos{ x=0; y=1; } == Pos{ x=0; y=0; } // ~> falsePos{ 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 coersion.
flex
(42: int32) + (43: int16) // type-checking error(42: int32) < (43: int16) // type-checking error(42: int32) == (42.0: float64) // type-checking errortrue == "true" // type-checking errortrue != (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.
flex
value_cast<int32>(42 : int16) // ~> 42 : int32value_cast<int32>(42: uint16) // ~> 42 : int32value_cast<float64>(3.14 : float32) // ~> 3.14 : float32value_cast?<int32>(42 : int16) // ~> some(42 : int32)value_cast?<int8>(127 : int16) // ~> some(127 : int8)value_cast?<int8>(128 : int16) // ~> nonevalue_cast?<int8>(128 : uint8) // ~> nonevalue_cast?<uint8>(-42 : int8) // ~> nonefloat64to32(3.14) // ~> 3.14 : float32
value_cast?
is often used with the getOrElse
function to provide an alterative value if the cast fails:
flex
getOrElse(value_cast?<uint8>(42: int8), 0) // ~> 42 : uint8getOrElse(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:
flex
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:
flex
{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.
flex
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.
flex
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.
flex
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.
flex
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:
flex
const x: int32 = 42;if x < 100 then x else x - 100; // ~> 42: int32if x < 20 then x else x - 20; // ~> 22: int32if x > 100 then x - 100else if x > 10 then x - 10else x // ~> 32: int32if (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).
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:
flex
let (x, y, z) = (1: int32, 2: int32, 3: int32);
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
.
flex
const squareNums: uint8[] = [1, 4, 9, 16];squareNums[0 : int32] // ~> 1 : uint8squareNums[4 : int32] // failssquareNums[0 : int32 .. 2 : int32] // ~> [1, 4, 9] : uint8[]squareNums ++ [25, 36] // ~> [1, 4, 9, 16, 25, 36]: uint8[]length(squareNums) // ~> 4: uint32last(squareNums) // ~> 16: uint8last([]: 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
:
flex
[x + 1 for x in squareNums] // ~> [2, 5, 10, 17]: uint8[]
The values can be filtered using an if
clause:
flex
[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:
flex
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:
flex
[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.
flex
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