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.
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.
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.
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;
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.
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.
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); };
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@;
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);
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
.
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.
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.
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.
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?
Head over to the Structs
tutorial to learn all about
structs.