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;
};
}