Skip to main content

Transforms and Functions Reference

This page is the reference for transform contextual parameters and functions. For conceptual background, see What Are Transforms?; to define a basic transform step by step, see Writing Transforms.

Contextual Parameters

Suppose that in the temperature example from before, the Fahrenheit message actually contains a timestamp field, but Celsius does not. This presents a problem for the Celsius2Fahrenheit transform: the output contains information that cannot be found in the input! So what should the timestamp field be set to?

message struct Fahrenheit {
  temp: float64;
  timestamp: uint64;
}

transform Celsius2Fahrenheit(c: Celsius) -> Fahrenheit =
  Fahrenheit {
    temp = ((c.temp * 9.0) / 5.0) + 32.0;
    timestamp = // cannot be found in Celsius!
  };

Naively, Celsius2Fahrenheit could set timestamp to some default value like 0. However, discretely making assumptions about default values is one way that poorly written software can break systems. Instead, when a transform needs information in addition to the input message, it should specify one or more contextual parameters.

Contextual parameters of a transform are additional inputs of minor importance but which are still necessary for the transform to be correctly defined. Examples include timestamps, labels, UUIDs, security tags, and other kinds of meta-data. A transform with at least one contextual parameter is called a contextual transform. Here is a new version of Celsius2Fahrenheit transform with a contextual parameter called ?time:

newtype Timestamp {
  value: uint64;
}

transform Celsius2Fahrenheit(c: Celsius) -> Fahrenheit given (?time: Timestamp) =
  Fahrenheit {
    temp = c.temp * 9.0 / 5.0 + 32.0;
    timestamp = ?time.value;
  };

The parameters specified in the parentheses after given are the contextual parameters. Their names must begin with ? to distinguish them from other identifiers. They can be used in the transform body just like the "proper" parameters are used.

Calling Contextual Transforms

When a contextual transform is called, the contextual arguments can be explicitly provided using the giving keyword:

const veryHot = Celsius{ temp = 100.0; };
const newMillennium = Timestamp{ 946702800 };

Celsius2Fahrenheit(veryHot) giving (newMillennium);

However, contextual arguments can also be implicitly pulled from the contextual parameter list of the calling transform. The implicit passing is done by matching types rather than names of the contextual parameters so that names can be inconsistent between transforms. In the example below, the value of ?ts is implicitly passed as the ?time parameter of Celsius2Fahrenheit because both have the type Timestamp:

message struct ThreeCelsiusTemps {
  temp1: Celsius;
  temp2: Celsius;
  temp3: Celsius;
}

message struct ThreeFahrenheitTemps {
  temp1: Fahrenheit;
  temp2: Fahrenheit;
  temp3: Fahrenheit;
}

transform ThreeC2ThreeF(temps: ThreeCelsiusTemps) -> ThreeFahrenheitTemps
given (?ts: Timestamp) =
  ThreeFahrenheitTemps {
    temp1 = Celsius2Fahrenheit(temps.temp1) giving (newMillennium);  // explicit passing
    temp2 = Celsius2Fahrenheit(temps.temp2) giving (?ts);            // equivalent to below
    temp3 = Celsius2Fahrenheit(temps.temp3);                         // implicit passing
  };

A consequence of the "types rather than names" rule of implicit passing is that different contextual parameters of the same type would make the implicit passing ambiguous. Therefore, Flex does not allow two different contextual parameters in the same given clause to have the same type.

transform ... given (?t1: Timestamp, ?t1: Timestamp)          // ILLEGAL

If you think that two contextual parameters should have the same type, try using newtypes to distinguish their meanings. For example, if we have two timestamps, maybe one of them represents a "sent time" and the other a "received time":

transform ... given (?t1: SentTime, ?t1: RecvTime)            // LEGAL

The implicit passing feature encourages consistency of the types of contextual information across multiple transforms. If implicit passing does not offer the desired behavior, explicit passing is still available as a backup.

Functions

Although the focus of Flex is transforms, functions are still a great way to break down complex translations and avoid code duplication.

Flex functions are defined the same as transforms except,

  • The keyword function is used.
  • The inputs and outputs can be of any type.
  • Functions cannot accept contextual parameters.

Functions can be used to define computation between non-message data types. For example, we can define a translation from the integers 1-10 to their string equivalents:

function digitToString(digit: uint8) -> string {
  assert digit < 10;
  match (digit) {
    0 => "0";
    1 => "1";
    2 => "2";
    3 => "3";
    4 => "4";
    5 => "5";
    6 => "6";
    7 => "7";
    8 => "8";
    9 => "9";
  };
}