Skip to main content

Transforms

A transform is a function that defines a canonical translation from one message type to another. Transforms are the glue that enables components to communicate even if they use different message standards.

On the surface, a Flex transform seems similar to a function in C++ or Java. However, not every function is a good transform. Transforms define translations between ontologically significant data types. For a detailed description of ontological significance, see Ontological Significance and Messages.

Transforms should specify translations between messages, not general computation; the output type should contain roughly the same information as the input, just in a different form. The job of a transform is not unlike that of a translator for human languages: In international relations, for example, it is critical for translators to preserve the information that speakers intend to communicate to listeners, even though the language for expressing the information is changed. Failing to preserve the information can have serious effects on diplomatic relations. The same is true of transforms for facilitating communication between software components; the transform should leave the core information intact.

Transforms should also be defined between ontologically significant messages; the meanings of the input and output types should be unambiguous. The danger of ambiguity can also be seen in the international relations analogy: Mistranslation can happen when translators have different understandings of the meaning of a word. But because the language used for international relations is relatively unambiguous, information can be translated from English to French to German and so on without significant degradation of meaning. The same is true in Flex: The power of transforms comes from the ability to automatically generate new transforms by fusing other transforms together, thus avoiding a lot of manual code writing. But fusion is only safe when the transforms interpret messages in the same way.

Writing Transforms

Imagine you have a software component that provides a temperature value in Celsius, but another software component in your system accepts Fahrenheit values as input. Here are the Flex specifications for these two messages:

message struct Celsius {
  temp: float64;
}

message struct Fahrenheit {
  temp: float64;
}

Let's define a transform from Celsius to Fahrenheit so that our two components know how to talk to each other:

transform Celsius2Fahrenheit(c: Celsius) -> Fahrenheit =
  Fahrenheit {
    temp = c.temp * 9.0 / 5.0 + 32.0;
  };

A transform starts with the transform keyword followed by the transform name (Celsius2Fahrenheit), parameter list (c: Celsius), return type (Fahrenheit), and the transform body after the = sign.

The transform body is an expression that describes the calculation of the output from the input. Expressions are described in more detail on the Expressions page. The body of Celsius2Fahrenheit contains expressions to access a field in the input struct (c.temp), perform some arithmetic (c.temp * 9.0 / 5.0 + 32.0), and build a Fahrenheit value (Fahrenheit { temp = ...; }). Other expression forms incude conditional evaluation, constructing and indexing arrays, calling other transforms, and more.

The body is placed after an = sign in the transform definition to emphasize the connection between Flex and functional programming languages. However, if the body is a block expression, then the = sign can be omitted to make the transform look more like a function in C++ or Java. Here is Celsius2Fahrenheit written without the = sign:

transform Celsius2Fahrenheit(c: Celsius) -> Fahrenheit {
  Fahrenheit {
    temp = c.temp * 9.0 / 5.0 + 32.0;
  };
}

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