Structs

fble-0.5 (2025-07-13,fble-0.4-212-ga8f8ad0f)

This tutorial does a deep dive into structs. By the end of this tutorial, you'll be fully versed in section 3 of the fble language spec on structs.

Basics

A struct is a grouping of named values. You create a struct value by providing the value of each field of the struct. You can later access those values by name.

Here are examples of how we've used structs so far:

@ Bit4@ = *(Bit@ 3, Bit@ 2, Bit@ 1, Bit@ 0);
Bit4@ X = Bit4@(0, 0, 1, 1);

(Bit4@, Bit4@) { Bit4@; } And4 = (Bit4@ a, Bit4@ b) {
  Bit4@(And(a.3, b.3), And(a.2, b.2), And(a.1, b.1), And(a.0, b.0));
};

The syntax *(...) is used for a struct type, with a list of named and typed fields. To create a struct value, you specify the struct type followed by the list of values for each field in order. You can access fields of a struct using ., as in a.3.

That describes the fundamentals of what you can do with structures.

Some miscellaneous details:

Implicit Type Struct Values

So far we've always constructed struct values by providing the struct type explicitly. For example: Bit4@(0, 0, 1, 1). It's also possible to specify a struct value where the type of the struct is implicit. In this case we specify the names of the fields explicitly, and rely on the compiler to infer the types of the fields.

For example, the follow two lines are equivalent:

Bit4@ X = Bit4@(0, 0, 1, 1);
Bit4@ X = @(3: 0, 2: 0, 1: 1, 0: 1);

Because the compiler knows the type of 0 is Bit@, and the type of 1 is Bit@, it can infer from @(3: 0, 2: 0, 1: 1, 0: 1) the struct type: *(Bit@ 3, Bit@ 2, Bit@ 1, Bit@ 0).

Copied Struct Values

Sometimes you want to modify the fields of a struct value. Values in fble are immutable, so you can't modify struct fields directly. What you can do is make a new copy of a struct value where some of the fields are modified.

You could do this with the syntax we've already presented for constructing struct values. For example, to change bit 2 of our Bit4@ struct X above:

Bit4@ Y = Bit4@(X.3, 1, X.1, X.0);

For large structures, it becomes tedious to manually copy each of the fields. Fble provides a short hand syntax that allows you to copy a struct value without listing all of its fields. For the struct copy syntax, you provide a struct value to copy, such as X, followed by syntax .@, followed by the list of new field values using the syntax from implicit type struct values.

The above example can be rewritten using struct copy syntax as:

Bit4@ Y = X.@(2: 1);

If you want, you can modify multiple fields at once and have more complicated field value expressions. If modifying multiple fields, the fields must be specified in the same order as they are defined in the struct type. For example:

Bit4@ Z = X.@(2: 1, 0: And(X.3, X.0));

Structs as Namespaces

Aside from their normal use in programming, structs play an important role in namespace management.

For example, the variable names 0 and 1 are nice to have for the Bit@ type, but they are also useful names for Bit4@ and Int@ types. We can create a struct value to hold 0 and 1 for the Bit@ type rather than pollute the global namespace of variables.

For example, let's define a struct value called Bits that has two fields, 0 and 1, which have the Bit@ values for 0 and 1 respectively:

@ Bits@ = *(Bit@ 0, Bit@ 1);
Bits@ Bits = Bits@(Bit@(0: Unit), Bit@(1: Unit));

Now we can refer to bits 0 and 1 via the Bits struct value. For example:

Bit4@ X = Bit4@(Bits.0, Bits.0, Bits.1, Bits.1);

When defining modules, it's common to define variables in scope, and export them by bundling them together in a struct value. For example:

Bits@ Bits = {
  Bit@ 0 = Bit@(0: Unit);
  Bit@ 1 = Bit@(1: Unit);
  Bits@(0, 1);
};

In these cases, we can use a special short hand supported for implicit struct type values: if the argument of a field is a variable, you can leave out the field name; the variable name will be used for the name of the field.

For example, here's how we could define the Bits value using an implicit type struct value with explicit fields:

Bits@ Bits = {
  Bit@ 0 = Bit@(0: Unit);
  Bit@ 1 = Bit@(1: Unit);
  @(0: 0, 1: 1);
};

And here's how we could define the Bits value using an implicit type struct value with implicit fields:

Bits@ Bits = {
  Bit@ 0 = Bit@(0: Unit);
  Bit@ 1 = Bit@(1: Unit);
  @(0, 1);
};

You can mix implicit fields with explicit fields in implicit type struct values, which is occasionally useful for renaming things when you create the struct value. For example, maybe we want to add zero and one as alternate ways to refer to those bits:

% Bits = {
  Bit@ 0 = Bit@(0: Unit);
  Bit@ 1 = Bit@(1: Unit);
  @(0, 1, zero: 0, one: 1);
}

Notice we switch to using the kind % in the definitions of Bits, which means we don't have to define the Bits@ type explicitly anywhere.

Function Fields

Functions are values, which means functions are allowed in fields of structures. There's nothing special about this. For example, we could add our And function to the Bits struct value we've been working with:

% Bits = {
  Bit@ 0 = Bit@(0: Unit);
  Bit@ 1 = Bit@(1: Unit);
  (Bit@, Bit@) { Bit@; } And = (Bit@ a, Bit@ b) {
    a.?(0: 0, 1: b);
  };
  @(0, 1, And);
}

Here's how we might call that function:

(Bit4@, Bit4@) { Bit4@; } And4 = (Bit4@ a, Bit4@ b) {
  Bit4@(Bits.And(a.3, b.3), Bits.And(a.2, b.2), Bits.And(a.1, b.1), Bits.And(a.0, b.0));
};

Though we haven't discussed polymorphism yet, polymorphic values and functions are also allowed for struct fields.

Type Fields

Types are values, which means types are allowed in fields of structures. The same naming rule applies as for variable names: type fields must be followed by @.

For example, we could add the Bit@ type to our Bits bundle:

% Bits = {
  @ Bit@ = +(Unit@ 0, Unit@ 1);
  Bit@ 0 = Bit@(0: Unit);
  Bit@ 1 = Bit@(1: Unit);
  @(Bit@, 0, 1);
};

And here is how we could use that type field:

@ Bit4@ = *(Bits.Bit@ 3, Bits.Bit@ 2, Bits.Bit@ 1, Bits.Bit@ 0);
Bit4@ X = Bit4@(Bits.0, Bits.0, Bits.1, Bits.1);

Note that the type of the fields for Bit4@ is Bits.Bit@. This is a little strange because it's a type that depends on a value. That doesn't mean the compiler has to evaluate code at compile time to do proper type checking though. The compiler can figure out the value of the Bits.Bit@ field from the type of the Bits struct, because it knows the type of the Bit@ field is @<Bit@>, which means its value must be Bit@ regardless of how the Bits value was constructed.

This is how we use struct values to define types, functions, and values in modules for reuse in other modules.

For the curious, here's how we could define that same Bits struct value using explicit types:

% Bits = {
  @ Bit@ = +(Unit@ 0, Unit@ 1);
  Bit@ 0 = Bit@(0: Unit);
  Bit@ 1 = Bit@(1: Unit);
  @ Bits@ = *(@<Bit@> Bit@, Bit@ 0, Bit@ 1);
  Bits@(Bit@, 0, 1);
};

Notice the use of the typeof operator to get the type of the Bit@ type, and the name Bit@ for the field.

Structs in the Language Specification

You now know everything there is to know about structs in fble. To reinforce this, read over section 3 of the fble language specification. Everything there should be familiar to you now.

Exercises

Go back to the Basics tutorial code and group together the definitions of Bit@, 0, 1, And, and ShowBit into a single struct value called Bits. Experiment with both explicit and implicit type struct values, and implicit and explicit field names.

In the Basics tutorial, group together code related to the Bit4@ type together into a single Bit4 struct value. Rename And4 to And and rename ShowBit and ShowBit4 both to Show now that we have the Bits and Bit4 namespaces to distinguish between them.

Next Steps

Head over to the Modules tutorial to learn all about modules.