fble-0.5 (2025-07-13,fble-0.4-212-ga8f8ad0f)
This tutorial describes special syntax fble has for user defined literals.
Many languages have special syntax for integer and string literals. The fble language doesn't have built in integer, character, or string types. Without special support for literals, it can be very tedious to specify integer or string values.
For example, imagine we want to specify an integer value 2023
representing the current year. Without literals we have a few options for
how to do this. Assuming integer values 0
through 10
are defined,
we could write it as:
Int@ 20 = Mul(10, 2); Int@ 202 = Add(Mul(10, 20), 2); Int@ 2023 = Add(Mul(10, 202), 3);
A better way would be to write a helper function that can convert a list of digits to an integer. For example:
(List@) { Int@; } MakeInt = { (Int@, List@) { Int@; } Helper = (Int@ int, List@ digits) { digits.?(nil: int); Helper(Add(Mul(10, int), digits.cons.head), digits.cons.tail); }; (List@ digits) { Helper(0, digits); }; };
Because our MakeInt
function takes a list of integers as its single
argument, we can use a list expression to define our integer:
Int@ 2023 = MakeInt[2, 0, 2, 3];
This approach may be convenient enough for small integers. But imagine using the same approach for strings:
String@ hello_world = MakeString[h, e, l, l, o, space, w, o, r, l, d];
That quickly becomes tedious.
The fble language has syntax for a literal expression, which is a more compact way of passing a list of letters to a function. Here are the same examples above, rewritten using literal expressions:
Int@ 2023 = Int|2023; String@ hello_world = String|'hello, world';
A literal expression is a function followed by |
and then a single word.
In this case, Int
is a function that converts a list of letters to an
integer and 2023
is a word with the list of letters to pass to the
Int
function. Similarly, String
is a function that converts a list
of letters to a string and 'hello, world'
is a word with the list of
letters to pass to the String
function.
The only tricky part is specifying what we mean by a letter. For literals in fble, the type of a letter is a union type that has fields with single-letter names whose types are the struct type with no fields.
For example, for the letters used in integer literals, we define a type
Decimal@
:
@ Decimal@ = +( Unit@ 0, Unit@ 1, Unit@ 2, Unit@ 3, Unit@ 4, Unit@ 5, Unit@ 6, Unit@ 7, Unit@ 8, Unit@ 9 );
When the compiler sees a literal expression with the word 2023
, it
splits it up into letters 2
, 0
, 2
, 3
, and uses those to
construct Decimal@
values directly. The literal expression Int|2023
gets converted into a list expression:
Int@ 2023 = Int[Decimal@(2: Unit), Decimal@(0: Unit), Decimal@(2: Unit), Decimal@(3: Unit)];
Except, in reality, the compiler would use the value *()()
instead of
Unit
, because it doesn't know about the name Unit
.
In summary, a literal expression can be used in place of list expressions in the special case where the type of element of the list is a union type with single-letter named fields whose types are the struct type with no fields.
Bit4@
Let's define our own literals for the Bit4@
type. We already have a
letter type we can use the satisfies the requirements for literal
expressions:
@ Bit@ = +(Unit@ 0, Unit@ 1);
That means all we need to do is define a function that takes a list of
Bit@
and produces a Bit4@
. Because we haven't introduced
polymorphism yet, let's redefine our own list type:
@ BitList@ = +(*(Bit@ head, BitList@ tail) cons, Unit@ nil);
Next we need to decide how we will handle literals with too few or too many
bits. Let's say literals like 1
and 10
are treated like 0001
and
0010
, and literals like 10110
are truncated to be like 0110
. A
simple way to implement this would be start with a Bit4@
value 0
and
shift in bits from the literal.
(Bit@, Bit4@) ShiftIn = (Bit@ bit, Bit4@ x) { Bit4@(x.2, x.1, x.0, bit); }; (BitList@) { Bit4@ } Bit4 = { (Bit4@, BitList@) { Bit4@; } Helper = (Bit4@ x, BitList@ bits) { bits.?(nil: x); Helper(ShiftIn(x, bits.cons.head), bits.cons.tail); }; (BitList@ bits) { Helper(Zero, bits); }; };
That's it. We now can write literals for the Bit4@
type. Here are some
examples:
Bit4@ a = Bit4|0; Bit4@ b = Bit4|1010; Bit4@ c = Bit4|110010;
Note that this only works because the Bit@
type was defined with field
names 0
and 1
. If instead we had defined Bit@
with field names
zero
and one
, you could not use it for the type of letters in a
literal expression. In that case, you can define a separate letter type with
field names 0
and 1
for use in your literals.
For the Int@
type, we can define integer literals that take decimal
numbers. In this case, the letter type is Decimal@
, decimal digits. If
we wanted to, we could as easily provide integer literals that take binary,
octal, or hexidecimal digits. The literal function is what's used to
distinguish between them. For example, we could have all of the following:
Int@ 42 = BinaryInt|101010; Int@ 42 = OctalInt|52; Int@ 42 = Int|42; Int@ 42 = HexInt|2a;
In a literal expression such as Int|2023
, the compiler knows what the
element type is based on the function Int
. From this it can infer what
the legal letters are to appear in the literal word. For example, if you
wrote Int|20y3
, it would give a type error, because there is no field
named y
in the Decimal@
type.
The compiler can't do any more than that to validate the syntax of the
literal though. For instance, maybe you want to write a negative integer
literal as: Int|'-123'
. For that to work, you would need to add '-'
as a field to Decimal@
, and then the compiler will accept words like
Int|'12-41'
, even though those aren't what we think of as legal integer
literals.
There are a few ways to handle syntax errors in literals that consist only of the letters you expect:
Don't include letters like '-'
that can be put out of order. For example,
instead of adding '-'
to Decimal@
, define a separate integer literal
function Neg
that negates the literal and write Neg|123
.
Have the literal return a type that can indicate whether there was an error or
not in parsing the literal. For example, define a type Maybe@
which is
either an Int@
or an error. Have the literal function return Maybe@
instead of Int@
, and the caller is required to check whether parsing
succeeded.
Be tolerant of improperly formatted literal words. For example, if '-'
is
seen out of place, pretend it didn't occur in the literal word at all. This is
like what we did for the Bit4
literal, where we silently truncated
literals with too many bits.
Have a runtime error if the literal word doesn't parse correctly, by accessing the wrong field of a union value.
The fble language allows you to define variable and field names with punctuation in them using single quotes. For example:
Float@ 'one plus two' = 3; # A variable with spaces in the name.
Single quotes are used in general in fble for any single words that contain punctuation. This is often useful in literals, because literals often contain punctuation:
String@ example = String|'one plus two';
You don't have to use single quotes if your literal doesn't contain punctuation:
String@ example = String|one;
Literal functions like Int
, Bit4
, and String
that we have shown
here can be used as is for list expressions if you want. This can be
convenient when defining values where one or more digits are derived
programmatically instead of being constant. No need to define a separate
list function, you can use your literal function as is!
You can also reuse literal functions as normal functions in fble, passing a list value directly to them.
For example:
Bit4@ a = Bit4|1010; Bit4@ b = Bit4[1, 0, one, zero]; BitList@ bits = ...; Bit4@ c = Bit4(bits);
Empty literals are allowed. For example, you might write the empty string as:
String@ empty = String|'';
You could also write this using list expressions or as a normal function call:
String@ empty = String[]; String@ empty = String(EmptyList);
The main use of literals, and all of the examples we've shown so far, have single-character letters. Technically speaking, a single letter could be made up of multiple characters. In that case, the next letter of the literal word is the longest letter that is a prefix of the word.
For example, say our letter type was defined as:
@ Letter@ = +(Unit@ a, Unit@ ab, Unit@ b); (Letter@) { Letter@; } Letter = (Letter@ x) { x; };
The literal Letter|aabb
is broken down into the three element sequence
a
, ab
, b
.
This behavior exists to better support multi-byte characters, where it's harder to say where one character ends and the next character begins. Otherwise there's probably not much practical use for longer letters in literals.
Literals are described in section 7.2 of
the fble language specification
.
Define a letter type for lowercase letters a
through z
and the
space character and a string type that is a list of those letters. Write
your own literal function and define the string value hello world
using
a literal expression.
Head over to the Polymorphism
tutorial to learn
all about polymorphism.