Chapter 5. Tuples

Rax tuples are similar to struct in C or record in Pascal - they allow to group variables of different types into a single variable. Tuples in Rax are delimited by [ and ]. We have already seen some examples in Chapter 2, Getting started:

    [&]                             : why_am_I_even_a_tuple;
    [?,#,$]                         : a_simple_tuple := [true, 5, "string"];
    [?:a_bool, #:a_num, $:a_string] : a_simple_tuple_with_labels;
    [?,#,$:a_string]                : a_tuple_with_one_label;

In the first line, a singleton tuple with a real is declared. In the second line, a tuple of a boolean, a number and a string is declared and assign a value (notice how literal tuples are constructed). In the third line, a very similar tuple is declared, only this time tuple fields are given labels. As you can see, tuple field labels in Rax are optional. One can also mix labeled and unlabeled fields within a single tuple. Field labels are also weak, which means that two tuple types are considered equal, even if their fields have different labels or one has no labels. So the types of variables a_simple_tuple, a_simple_tuple_with_labels and a_tuple_with_one_label are considered equal by Rax. So you can assign one to another:

    a_simple_tuple_with_labels := a_simple_tuple;
    a_tuple_with_one_label     := a_simple_tuple_with_labels;

Tuple field labels do not have to be unique. The following is a valid type definition in Rax:

    [#:n, $:s, #:n]: a_very_weird_tuple := [1,"2",3];

To simplify declaring long tuples with many similar fields, Rax provides a *n construct:

    [#*4, &*3, $:name]: long_tuple := [1, 2, 3, 4, 1.0, 2.0, 3.0, "long tuple"];

In the above example, long_tuple will have 8 fields: 4 fields of type # followed by 3 fields of type & and a single field of type $ labeled name. Fields declared with the *n construct cannot be labeled.

There are three ways of addressing tuple fields in Rax: by number, by label and by label and number:

    `print a_simple_tuple.#1;                     // Output: true
    `print a_simple_tuple_with_labels.a_string;   // Output: silly string
    `print a_very_weird_tuple.n#2;                // Output: 3

Addressing by number can be used with any tuple. Note that the field numbers are 1-based. Addressing by label can only be used on fields that have labels, obviously. Addressing by label and number is useful when we're dealing with non-unique tuple field labels. This might seem to be of limited usability, but will turn out very handy when dealing with joins (see Chapter 8, Ordered sets, the section called “Relational operators”).

It is possible to assign values to individual tuple fields. For example:

    a_simple_tuple_with_labels.#2 := "a different string";
    `print a_simple_tuple_with_labels;

    // Output: [a_bool: true, a_num: 5, a_string: "a different string"]

Since Rax is a functional language, new values can be derived from existing values, but existing values cannot be changed. Therefore, the a_simple_tuple_with_labels variable is not actually changed. Instead, a new variable with the same name is created, with the new value of the field a_string. The values of the remaining fields are copied from the old variable. The new variable overshadows the old variable and renders it unreachable. This difference might seem purely academic at the first glance, but will be important to understand when dealing with functions and closures (see Chapter 7, Functions).

Tuples can contain other tuples, for example:

    [[&:x, &:y]:top_left_corner, [&:width, &:height]:size]: rectangle :=
      [[1.0, 1.0], [100.0, 100.0]];
    `print rectangle.top_left_corner;  // Output: [x: 1, y: 1]
    `print rectangle.size.width;       // Output: 100

To simplify working with tuples, especially with tuple variables with long names, Rax provides a Pascal-like with clause. It is a syntactic sugar that starts a scope that turns named tuple fields into variables. The with clause has the form of the tuple variable name followed by a dot and curly brackets:

      // Code here

For example:

    [#:a, $:b, &]: T;
    T := [3, "noot", 3.14];
    @: a = (@)"2000-01-01T12:00";
    T.{            // The @:a is now out of scope
      `print a;    // Output: 3
      a := 5;      // This will create new version of tuple T
      T.#3 := 1.1; // There is no shortcut for T.#3
    `print T;      // Output: [a: 5, b: "noot", 1.1]
    `print a;      // Output: 2000-01-01T12:00:00

The with scopes can be nested, just like regular scopes. In the example below, we create a scope, in a scope of a scope:

          x := 2;
    `print T;     // Output: [U: [V: [W: [X: [x: 2]]]]]

Nested scopes, as shown in the example above, have a very narrow use case, but the example below shows a broader example:

    // Long way to initialize VeryLongButVeryDescriptiveName with
    // [a: 1, a: 1, a: 1, a: 1, a: 0, a: 2, a: -1, a: 1, a: 3].
    [#:a*9]: VeryLongButVeryDescriptiveName;
      a#1 := a#2 := a#3 := a#4 := 1;
      a#5 := - 2*a#4 - a#3 + 2*a#2 + a#1;
      a#6 := - 2*a#5 - a#4 + 2*a#3 + a#2;
      a#7 := - 2*a#6 - a#5 + 2*a#4 + a#3;
      a#8 := - 2*a#7 - a#6 + 2*a#5 + a#4;
      a#9 := - 2*a#8 - a#7 + 2*a#6 + a#5;

It might seem complex to initialize a tuple in this way, but the advantage is that mistakes in values are easier to spot.