Data Types
Structs
Flex has support for structs that resemble structs in C and C++. They are declared using the struct
keyword.
Zero or more fields may be declared within a struct, and each field must have a data type declared after its name. Properties may be declared after field declarations.
Here is a struct named Space3D
with three fields of type float64
named x
, y
, and z
:
flex
struct Space3D {x : float64;y : float64;z : float64;}
Struct values can be created using struct-builder syntax, and the fields of a struct can be accessed using dot notation:
flex
const mySpace3D: Space3D = Space3D{ x = 10.0; y = 20.0; z = 30.0; };const xField: float64 = mySpace3D.x;
Inheritance
A struct can extend another struct using the extends
keyword. The struct being extended is called the parent, while the other struct is a child. A child struct implicitly contains the fields of its parent in addition to its own fields.
In the example below, we define a parent struct Position
and a child struct LabeledPosition
.
flex
extensible struct Position {x : int32;y : int32;}struct LabeledPosition extends Position {label: string;}
By extending Position
, the LabeledPosition
struct also has x
and y
fields in addition to label
. Additionally, values of type LabeledPosition
can be implicitly cast to Position
, which means that a LabeledPosition
value can be used anywhere that a Position
value is expected.
flex
const myLP: LabeledPosition = LabeledPosition { // constructing a LabeledPositionx = 10;y = 20;label = "myLP";};const xField: int32 = myLP.x; // field accessconst myPosition: Position = myLP; // implicit casting to ancestor
Additional Rules
- Only structs declared
extensible
can be extended. - A child can have at most one parent, but an extensible struct can have zero or more children.
Abstract structs
An extensible struct S
can also be declared abstract
which prevents the instantiation of S
directly. A value of type S
can still be created by instantiating a (non-abstract) descendent of S
. Abstract structs are useful to define meta-information that must be included in many different messages in a message standard.
flex
abstract extensible struct MessageWrapper {messageID: string;date: string;}
Variants
Variants are used to express that a piece of data has multiple representations. (Formally, they are Flex's representation of discriminated unions.) For example, an angle could be represented as three integers for degrees, minutes, and seconds (DMS format) or as a single decimal representing the value in radians.
flex
variant Angle {DMS(int32, int32, int32); // degrees, minutes, secondsRadians(float64);}
DMS
and Radians
are called the constructors of the variant; when they are called like functions, they build values of type Angle
. A variant value can be destructured using a match
expression (see Pattern Matching for more details).
flex
const myAngle1: Angle = Angle.DMS(30, 15, 5);const myAngle2: Angle = Angle.Radians(3.14159);match (myAngle1) {Angle.DMS(d, m, s) => ... // d, m, and s are accessible hereAngle.Radians(r) => ... // r is accessible here}
Enumerations
An enumeration is a type that is defined by explicitly listing its values. The example below defines an enumeration SimulationStatus
that contains four values: Stopped
, Running
, Paused
, and Reset
.
flex
enum SimulationStatus int32 {Stopped = 0;Running = 1;Paused = 2;Reset = 3;}
To support effective transpilation to lower-level languages, each value of an enum must be associated with a value of some integral type. In the example above, the integral type is int32
and the associated int32
values are 0
, 1
, 2
, and 3
.
Constructing a value of type SimulationStatus
is as simple as accessing on of the values in the enum definition.
flex
const mySimulationStatus: SimulationStatus = SimulationStatus.Running;
Type Aliases and Newtypes
Sometimes we want to ascribe additional meaning to a type, such as whether an int32
value represents an altitude, angle, quantity, etc. Flex provides two mechanisms for assigning such additional meaning.
A type alias is an identifier that can be used as a synonym of another type. It does not define a new type, just a new name for an existing type. Thus, replacing a type expression with an alias has no effect other than improving readablilty. Type aliases are defined using the type
keyword:
flex
type Altitude = int32;
A newtype, in contrast, is distinct from its underlying type, and there is no implicit conversion to or from the newtype. You can think of a newtype as a struct with a single field. Building and deconstructing values of a newtype is similar to building structs and accessing struct fields:
flex
newtype Altitude {value: int32;}const alt: Altitude = Altitude{ 42 }; // convert int32 to Altitudeconst x: int32 = alt.value; // convert Altitude to int32
Ontological Significance and Messages
An important concept in Tangram ProTM is that of ontological significance. We say that a data type has ontological significance if there is a universal understanding (across all uses) of its meaning.
We will illustrate the importance of ontoloical significance with an example. Suppose we define the following structs and functions. The getPosFromAVS
function extracts a ship's position from a vehicle state value. The pos2loc
function translates a Position
into a Location
in order to inferface with a component that only understands Location
data and not Position
data.
flex
struct AirVehicleState {ownshipPosition: Position; // offset (in kilometers) from an origin point.// other details omitted}struct Position {x: float64;y: float64;}struct Location {x: float64; // measured in milesy: float64; // measured in miles}function getPosFromAVS(avs: AirVehicleState) -> Position {avs.ownshipPosition;}function pos2loc(pos: Position) -> Location {Location {x = pos.x;y = pos.y;};}
Furthermore, suppose that all Position
values contained within an AirVehicleState
represent offsets measured in kilometers. In contrast, Location
data is always in miles. The offsets in a Position
value can be expressed in different units depending on the circumstance; getPosFromAVS
assumes kilometers while pos2loc
assumes miles.
Is there a universal understanding of what Position
means? No! The two functions are not interoperable because they make different assumptions about the meaning of the data in a Position
value. Tangram ProTM is designed to perform code synthesis tasks such as fusing the above two functions into a single function from AirVehicleState
to Location
. However, the contradictory interpretations of the meaning of Position
prevent this fusion from happening safely.
The problem is that Position
is not ontologically significant; different functions treat Position
values differently. In contrast, messages of open standards like LMCP or Mavlink all have universally agreed-upon interpretations. Thus, it is safe to perform code synthesis tasks on functions that operate on them.
In Flex, the message
keyword is used to indicate that a data type is ontologically significant and that performing code synthesis is safe. The message
keyword is allowed to modify struct and variant declarations. Newtypes are always ontologically significant (without needing the message
keyword).
flex
message struct AirVehicleState {ownshipPosition: Position;// other details omitted}