Variables

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

This tutorial does a deep dive into variables, types, kinds, and let expressions. By the end of this tutorial, you'll be fully versed in section 2 of the fble language spec on types, kinds, and variables.

Basics

A variable is a name for a value. Values are typed and immutable. You use a let expression to declare and define a variable. You can later refer to that value by using the variable name.

We've already seen examples of this. For instance:

Bit@ 0 = Bit@(0: Unit);
Bit@ 1 = Bit@(1: Unit);
Bit4@ X = Bit4@(0, 0, 1, 1);
Bit4@ Y = Bit4@(1, 0, 1, 0);

Here we define four variables 0, 1, X, and Y.

Consider the variable named 0. The type of the variable is Bit@. The value of the variable is Bit@(0: Unit). The variable is used twice in the definition of X and twice in the definition of Y.

Fundamentally that's all that variables are. Names for values that you can reuse elsewhere.

Variable Names

You can use any name you like for a variable. There are no keyword restrictions and no restrictions on what characters are in a variable name, except that the name needs to be in single quotes if it contains any whitespace or punctuation characters. Variable names are case sensitive.

Here are some examples:

Bit@ zero = Bit@(0: Unit);        # A typical variable name.
Bit@ Zero = zero;                 # A capitalized variable name.
Bit@ 0 = zero;                    # Use of digits in a variable name.
Bit4@ 0000 = Bit4@(0, 0, 0, 0);   # Digits are treated like normal characters.
Set@ union = Union(a, b);         # No keywords like 'union' to have to avoid.
Float@ π = ComputePi();           # Unicode characters are allowed.
Float@ 'π/2' = Div(π, 2);         # A variable with '/' in the name. 
Float@ 'one plus two' = 3;        # A variable with spaces in the name.

Single quotes are used as lexical syntax to write any kind of atomic word in fble that may contain whitespace or punctuation. You can use single quotes for normal words too, in which case they refer to the same variable as the name without single quotes. For example, zero and 'zero' refer to the same variable.

Implicit Variable Types

There's another way to define a variable that doesn't require giving the type of a variable. We've seen examples of this before when importing values from other modules. For example:

% True = /Core/Bool%.True;

In this case, instead of writing the type of True, we write the kind of the variable, % (more on kinds in just a second). The compiler automatically infers the type of the variable from its definition.

We don't need to be importing values from other modules to declare variables this way. For example, the following two variable declarations are equivalent:

Bit@ zero = Bit@(0: Unit);   # Defining a variable with explicit type.
% zero = Bit@(0: Unit);      # Defining a variable with implicit type.

We can also import values from other modules with explicit types. For example, the following two variable declarations are equivalent:

% True = /Core/Bool%.True;
/Core/Bool%.Bool@ True = /Core/Bool%.True;

Type Values

Variables can be used for types as well as normal values, because a type is a value in fble.

A type is a special kind of value to be sure. The compiler knows the value of a type during compilation and can check whether two type values refer to the same type or not, which is not the case for normal values.

The concept of a value's kind is used to distinguish between normal values and type values, and for describing the type parameters of polymorphic values. We'll discuss polymorphism in a future tutorial. For now you can think of there being two kinds: % is the kind of (non-polymorphic) normal values, and @ is the kind of (non-polymorphic) types.

To make it easier to keep track of whether a variable refers to a normal value or a type when reading fble programs, we require type variable names to be followed by the @ character. In this case, @ is not considered part of the variable name, but rather is an indicator of the namespace of the variable. So, for example 'Foo'@ is a type variable named Foo, and 'Foo@' is a normal variable named Foo@.

We've already seen examples of declaring type variables:

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

Note that we use the implicit type of variable declaration, using @ for the kind instead of % like we used for normal variables. We could explicitly write the type of these type variables, but I haven't told you what the type of a type value is yet or how to write it down.

Just like normal variables are names for normal values, type variables are names for type values. Unlike many statically typed languages, the name of the type is not the definition of the type. For example, we could define the Bit@ type without introducing a Unit@ type variable:

@ Bit@ = +(*() 0, *() 1);

We can also define multiple different type variables for the same type:

@ Unit0@ = *();
@ Unit1@ = Unit0@;
@ Bit@ = +(Unit0@ 0, Unit1@ 1);

In this case, *(), Unit0@, and Unit1@ are all equivalent and all refer to the same type.

The Type of a Type

If a type is a value, and all values have types, that means a type has a type. In fble, the type of a type is defined to be the type of that type. If A@ is a type and B@ is some other type, the type of A@ is some type that is different from the type of B@. More importantly, if the compiler knows the type of the type A@, it can derive from that the value of the type A@.

It's all rather philosophical and not terribly important in practice when writing fble programs.

Fble provides a typeof operator you can use to get the type of any value. For example, @<0> gives the type of the value 0. Assuming 0 is defined as a Bit@, then @<0> is equivalent to the type Bit@. If we wanted to, we could define variables this way:

Bit@ 0 = Bit@(0: Unit);
@<0> 1 = Bit@(1: Unit);

There's almost never a time we need to use the typeof operator. But it is the only way to refer to the type of a type. For example, here's how you could define the types Unit@ and Bit@ using explicit type variable declarations:

@<*()> Unit@ = *();
@<+(Unit@ 0, Unit@ 1)> Bit@ = +(Unit@ 0, Unit@ 1);

You see now why we don't use explicit type variable declarations when defining type variables, because the value of the type contains the same information as the type of the type.

And for those who are wondering, yes, a type of a type is also a value that has a type. For example: @<@<Bit@>> is the type of the type of the Bit@ type. And on and on. But note, you cannot define a variable of a type of a type. Because you never need to and that would just be confusing.

Variable Scopes

Variables in fble have lexical scoping, which means they can be referenced from any code in the same block that follows their definition. Fble uses automatic memory management to ensure values stay alive at runtime as long as they are needed.

Anywhere an expression is expected, you can use {} braces to introduce a new scope for variables. For example:

{
  Int@ a = 1;
  Int@ b = {
    Int@ c = 1;
    Add(a, c);
  };
  Int@ d = 3;
}

Variables a, b, and d can be referenced between their definition and the end of the outer most block. Variable c can be referenced only inside the block where it is declared.

It's possible to capture variables from outer blocks in function bodies. We've already seen examples of this:

Bit@ 0 = Bit@(0: Unit);

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

The value 0 used inside the And function will always refer to the value of the variable 0 when the And function was declared, regardless of when the And function is invoked.

A variable cannot be referenced before it is declared. An implication of this is that a function must be defined after all other functions it makes use of.

For example, the follow will result in an error, because the And function hasn't been declared at the time it is referenced from the And4 function:

(Bit4@, Bit4@) { Bit4@; } And4 = (Bit4@ a, Bit4@ b) {
  # Error: `And' has not been declared.
  Bit4@(And(a.3, b.3), And(a.2, b.2), And(a.1, b.1), And(a.0, b.0));
};

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

Recursive Declarations

In order to support recursive functions and types, a variable can be referenced in its own declaration.

For example, here's how you can define a recursive function:

(Int@) { Int@; } Factorial = (Int@ n) {
  Eq(n, 0).?(true: 1, false: Mul(n, Factorial(Sub(n, 1))));
};

In this case, we refer to the variable Factorial from the definition of the function value.

The value of a variable is undefined when evaluating the definition of the variable. It's only after the definition of the variable has finished being evaluated that the variable takes on its value. This works fine in the definition of Factorial, because we don't invoke the Factorial function until after it has been defined.

If you do try to use the value of a variable in its own definition, you'll get a runtime error. For example, the following results an "undefined value" runtime error:

Bit@ 1 = Bit@(1.1);

This is an error because we are trying to access the 1 field of the bit before it has been defined.

You could define a cyclical infinite list if you wanted:

List@<Bit@> ones = Cons(1, ones);

This is fine because you never try to access the value of the variable in its definition.

Recursive declarations are also useful for declaring recursive data types. For example, here's how you could define a (non-polymorphic) list data type:

@ List@ = +(*(Bit@ head, List@ tail) cons, Unit@ nil);

Don't try anything too crazy though. The following will give an error about a vacuous type:

@ T@ = T@;

Mutually Recursive Declarations

There are times you may want to define mutually recursive variables. To do this, you can specify multiple variable definitions in the same let expression separated by comma.

Here's an example of declaring multiple variables at once without them referring to each other:

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

Here's a contrived example of defining mutually recursive functions:

(Int@) { Int@; } f = (Int@ x) {
  x.?(0: x, 1: g(0), 2: g(1));
},
(Int@) { Int@; } g = (Int@ x) {
  x.?(0: x, 1: f(0), 2: f(1));
};

Notice the two declarations are separated by a comma instead of a semicolon.

A less contrived example of mutually recursive declarations is in declaring recursive data types:

@ ListP@ = *(Bit@ head, List@ tail),
@ List@ = +(ListP@ cons, Unit@ nil);

Variable Shadowing

In fble you are allowed to shadow previous variable declarations. For example:

Int@ x = 0;
Int@ y = x;
Int@ x = 1;
Int@ z = x;

In this case, the value of y is 0, and the value of z is 1.

Undefined Variables

As a special case, it's possible to declare undefined variables. This is done by giving the type and name of a single variable without any value. For example:

Bool@ p;
Bool@ q = p;
Bool@ w = Not(q);

The variable p is undefined, because it isn't assigned a value. You can pass around an undefined value, but as soon as you try to access the contents of that value, you will get a runtime error. In the above code, assigning the value of q to be p is fine, but you'll get a runtime error on the next line when the implementation of the Not function tries to access the value.

Undefined variables are primarily useful for defining interfaces, where you are interested in their type rather than their value. We'll see this use case later on in the discussion of modules.

Unused Variables

The fble compiler will give warnings about unused variables, to help identify and clean up unecessary code. It's not uncommon to have an unused variable on purpose. In that case, you can prefix the variable with _ to avoid the compiler emitting a warning. By convention, the variable name _ is used to indicate a value we don't care about.

We'll see less contrived examples of this later on, but for now, here's a contrived example of an unused variable:

  String@ _ = Str|'this string is not used';

  /Core/Stdio/StringO%.Run(Str|'hello, world');

Here we use the variable name _ for the unused string variable.

Variables in the Language Specification

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

Exercises

Go back to the fble program we wrote for the Basics tutorial and change all lets to use the implicit type form, including for all variables and function definitions.

Change some of the variables in the Basics tutorial to names with punctuation and whitespace in them. For example, change the name of X to 'A variable named X'.

Try defining a variable with the wrong type or the wrong kind. What error message do you get?

Try defining a type variable without the @ at the end. Try defining a normal variable with @ at the end. What error messages do you get in these cases?

Try defining a variable for the type of the Bit@ type. What error message do you get?

Next Steps

Head over to the Structs tutorial to learn all about structs.