fble-0.5 (2025-07-13,fble-0.4-212-ga8f8ad0f)
This tutorial does a deep dive into modules. It walks you through how to split
up the program from the Basics
tutorial into
different modules. By the end of this tutorial, you'll be fully versed in
section 8 of the fble language spec on modules.
A module is an fble program written in a separate .fble
file describing
an fble value that can reference and be reused in other modules. Modules are
organized in a directory structure and referred to from code using module
paths.
It is most common for a module to describe a struct value that bundles together a collection of reusable types, functions, and values.
For example, we'll split up the Basics.fble
program from the
Basics
tutorial into the following separate
modules:
File Module Path Purpose --------------------------------------------------- Unit.fble /Unit% Defines Unit@, Unit Bit.fble /Bit% Defines Bit@, 0, 1, And Bit/Show.fble /Bit/Show% Defines Show function for Bit@ type. Bit4.fble /Bit4% Defines Bit4@, And Bit4/Show.fble /Bit4/Show% Defines Show function for Bit4@ type. Main.fble /Main% Code for the Main function.
Here is the code for Unit.fble
:
@ Unit@ = *(); Unit@ Unit = Unit@(); @(Unit@, Unit);
We define the Unit@
type and the Unit
value. The value of the module
is an implicit type struct value with implicit fields. In this case, it's a
struct value with two fields: Unit@
of type @<Unit@>
and Unit
of
type Unit@
.
Here is the code for Bit.fble
:
@ Unit@ = /Unit%.Unit@; % Unit = /Unit%.Unit; @ Bit@ = +(Unit@ 0, Unit@ 1); Bit@ 0 = Bit@(0: Unit); Bit@ 1 = Bit@(1: Unit); (Bit@, Bit@) { Bit@; } And = (Bit@ a, Bit@ b) { a.?(0: 0, 1: b); }; @(Bit@, 0, 1, And);
The Bit
module depends on the Unit
module for the definition of
Unit@
and Unit
. The way it refers to the Unit
module is using
the module path /Unit%
. Think of /Unit%
as a variable that has
already been defined to have the value computed in Unit.fble
. /Unit%
has a type, in this case a struct with two fields Unit@
and Unit
,
and it can be referenced repeatedly to refer to the same value multiple
times. We can access the fields of the struct using .Unit@
and
.Unit
, just like any other struct value.
Now you see that importing things from other modules is just defining local variables that refer to the fields of the struct values computed by those modules.
With those basics down, the rest of the modules should be straight forward.
Here's Bit/Show.fble
:
@ String@ = /Core/String%.String@; % Str = /Core/String%.Str; @ Bit@ = /Bit%.Bit@; (Bit@) { String@; } Show = (Bit@ a) { a.?(0: Str|0, 1: Str|1); }; @(Show);
Here's Bit4.fble
:
@ Bit@ = /Bit%.Bit@; % And1 = /Bit%.And; @ Bit4@ = *(Bit@ 3, Bit@ 2, Bit@ 1, Bit@ 0); (Bit4@, Bit4@) { Bit4@; } And = (Bit4@ a, Bit4@ b) { Bit4@(And1(a.3, b.3), And1(a.2, b.2), And1(a.1, b.1), And1(a.0, b.0)); }; @(Bit4@, And);
Here's Bit4/Show.fble
:
<@>% Concat = /Core/List%.Concat; @ String@ = /Core/String%.String@; % ShowBit = /Bit/Show%.Show; @ Bit4@ = /Bit4%.Bit4@; (Bit4@) { String@; } Show = (Bit4@ a) { Concat[ShowBit(a.3), ShowBit(a.2), ShowBit(a.1), ShowBit(a.0)]; }; @(Show);
Notice the module path for /Bit/Show%
mirrors the directory structure
where the module Bit/Show.fble
is defined.
And finally, Main.fble
:
@ String@ = /Core/String%.String@; % Str = /Core/String%.Str; % Strs = /Core/String%.Strs; % 0 = /Bit%.0; % 1 = /Bit%.1; @ Bit4@ = /Bit4%.Bit4@; % And = /Bit4%.And; % Show = /Bit4/Show%.Show; Bit4@ X = Bit4@(0, 0, 1, 1); Bit4@ Y = Bit4@(1, 0, 1, 0); Bit4@ Z = And(X, Y); String@ output = Strs[Show(X), Str|' AND ', Show(Y), Str|' = ', Show(Z)]; /Core/Stdio/StringO%.Run(output);
We can run our new program just like before, passing /Main%
as the name
of the module to run:
fble-stdio -p core -I . -m /Main%
Module paths are used to refer to the values computed by modules. Module
paths always start with /
and end with %
. Module paths mirror the
directory structure where the .fble
file for a module is located.
When you run fble code, there is a notion of a module search path. The
module search path is a list of directories to search for modules.
The -I .
option to fble-stdio
says to add the directory .
to the
module search path.
To find the definition of the module /Bit/Show%
, for example, the fble
runtime looks for a file named Bit/Show.fble
in each of the module
search path directories in order. In this case, it finds the module
/Bit/Show%
in ./Bit/Show.fble
. The first matching .fble
file
found is used for the definition of the module.
The -p core
option to fble-stdio
has the effect of adding
$FBLE_PACKAGE_PATH/core
to the module search path, assuming
$FBLE_PACKAGE_PATH
points to the default package search path
documented in the output of fble-stdio --help
. To find the source code
for the /Core/String%
module, for example, the runtime will first check
for ./Core/String.fble
, then
$FBLE_PACKAGE_PATH/core/Core/String.fble
.
The package path is a list of directories containing packages. A package, in
this case, is a directory with a collection of modules. For example, the
default package path lists a single directory with the following packages in
it: app
, core
, games
, graphics
, and so on. Each of these
packages is a module search path directory to use to search for the
definition of modules.
The purpose of modules is to split fble programs up into different files, and to be able to reuse the same code in multiple different programs.
Modules can reference other modules via module paths, which means they form a dependency graph of modules. Fble requires there are no cycles in this dependency graph. You are not allowed to define a module that recursively depends on itself, either directly or indirectly.
To assemble a program for a given main module, the fble compiler does a topological sort of all the modules the main module directly or indirectly depends on. Conceptually it assembles the modules into a sequence of variable definitions to form a full fble program.
For example, ignoring the modules from the core
package for simplicity,
the program for the /Main%
module is conceptually something like:
LET /Unit% = EVAL(Unit.fble) LET /Bit% = EVAL(Bit.fble) LET /Bit/Show% = EVAL(Bit/Show.fble) LET /Bit4% = EVAL(Bit4.fble) LET /Bit4/Show% = EVAL(Bit4/Show.fble) EVAL(Main.fble)
The modules are evaluated one after the other in the chosen topological order, with the final result of the program being the value computed by the main module.
Technically the compiler is allowed to use any topological order that satisfies the module dependencies for evaluating modules in a program. In practice it shouldn't make much difference what order the modules are evaluated in.
.fble
files for a module that the main module does not depend on
directly or indirectly are ignored by the compiler. For example, if you put
/Bit4%
as the main module, the compiler would only read Unit.fble
,
Bit.fble
and Bit4.fble
. It wouldn't read Bit/Show.fble
,
Bit4/Show.fble
or Main.fble
.
You can use the fble-deps
program to see the list of modules needed for
a program. For example:
fble-deps -t foo -p core -I . -m /Main%
This outputs something like the following, which is a list of all the
modules required by the /Main%
program:
foo: ./Main.fble \ /usr/local/share/fble/core/Core/Bool.fble \ /usr/local/share/fble/core/Core/Unit.fble \ /usr/local/share/fble/core/Core/Char.fble \ /usr/local/share/fble/core/Core/List.fble \ /usr/local/share/fble/core/Core/Monad.fble \ /usr/local/share/fble/core/Core/Stdio.fble \ /usr/local/share/fble/core/Core/Stream.fble \ /usr/local/share/fble/core/Core/Int.fble \ /usr/local/share/fble/core/Core/Int/IntP.fble \ /usr/local/share/fble/core/Core/Maybe.fble \ /usr/local/share/fble/core/Core/String.fble \ ./Unit.fble ./Bit.fble ./Bit4.fble ./Bit4/Show.fble ./Bit/Show.fble \ /usr/local/share/fble/core/Core/Stream/OStream.fble \ /usr/local/share/fble/core/Core/Char/Ascii.fble \ /usr/local/share/fble/core/Core/Int/Lit.fble \ /usr/local/share/fble/core/Core/Digits.fble \ /usr/local/share/fble/core/Core/Map.fble \ /usr/local/share/fble/core/Core/Map/Map.fble \ /usr/local/share/fble/core/Core/Eq.fble \ /usr/local/share/fble/core/Core/Int/Eq.fble \ /usr/local/share/fble/core/Core/Int/IntP/Eq.fble \ /usr/local/share/fble/core/Core/Stdio/IO.fble \ /usr/local/share/fble/core/Core/Monad/IO.fble \ /usr/local/share/fble/core/Core/Monad/State.fble
Modules are organized into a directory structure. The directory structure of
modules has no impact on the module dependency graph. For example, imagine
you have modules organized hierarchically as /Foo%
and /Foo/Bar%
.
You might have that /Foo%
depends on /Foo/Bar%
, for example if
/Foo/Bar%
implements code for /Foo%
. Or you might have /Foo/Bar%
depend on /Foo%
, as was the case with our /Bit/Show%
and /Bit%
modules. Or there may be no dependency between /Foo%
and /Foo/Bar%
.
The idea behind the directory structure is that modules in the same directory are developed by the same organization. That organization is responsible for ensuring you don't have two modules in the same directory with the same name. There is a language feature for private types that grants or restricts access to modules based on the directory structure.
Think of modules as dependency units. Break up code into modules to avoid
introducing unecessary false dependencies. For example, we used a separate
module for /Bit/Show%
, which allows you to import /Bit%
and use the
Bit@
type without taking a dependency on the /Core/String%
module.
If we implemented the Show
function in the /Bit%
module, you
couldn't use Bit@
without depending on /Core/String%
.
The most common type of value defined by a module is a struct value. That's
what we used for /Unit%
, /Bit%
, /Bit/Show%
, /Bit4%
, and
/Bit4/Show%
. There's nothing that requires a module to define a struct
value though. A module can define any kind of fble value.
For example, our /Main%
module defines a function value, which is the
main function executed by the fble-stdio
program. Using the foreign
function interface for fble, it's possible to define different runners that
know how to execute different kinds of fble functions. For example, there's
also an fble-app
runner that can run fble programs describing graphical
applications. The main functions for fble-app
have a different type than
the main functions for fble-stdio
.
You could define a parameterized module by making the module value be a
function whose arguments are the parameters to the module and whose result
is a struct value with the types and functions being declared by the module.
There are a few examples of this in the core library, such as
/Core/Stream/OStream%
.
You can do a lot of interesting things with fble modules by keeping in mind that they can describe any type of value, not just struct values.
The fble language has support for modular compilation, which means you can compile a module without access to the implementation of other modules that it depends on. For this to work, you still need access to the type of other modules that your module depends on.
You can describe the type of a module separately from its implementation
using a module header file. These files use the extension .fble.@
. The
syntax for a header file is exactly the same as for a normal fble module,
except that its value is never used.
If you remember back to the Variables
tutorial, it's
possible to declare undefined variables. Those are primarily useful for fble
header files. The header file can contain the same code as the module's
implementation, except leave all the variables undefined. The type of the
header file will be used during compilation.
For example, recall the implementation of our /Bit%
module in
Bit.fble
:
@ Unit@ = /Unit%.Unit@; % Unit = /Unit%.Unit; @ Bit@ = +(Unit@ 0, Unit@ 1); Bit@ 0 = Bit@(0: Unit); Bit@ 1 = Bit@(1: Unit); (Bit@, Bit@) { Bit@; } And = (Bit@ a, Bit@ b) { a.?(0: 0, 1: b); }; @(Bit@, 0, 1, And);
We could write a header file for /Bit%
in the file Bit.fble.@
as:
@ Unit@ = /Unit%.Unit@; @ Bit@ = +(Unit@ 0, Unit@ 1); Bit@ 0; Bit@ 1; (Bit@, Bit@) { Bit@; } And; @(Bit@, 0, 1, And);
Notice that we haven't provided an implementation for the And
function
or the values 0
or 1
. What's important is that the type of the
header file is the same as the type of the implementation file.
Header files are optional. If a header file exists for a module, it will be read instead of the implementation of the module to get its type. If a header file exists for a module you are compiling, the compiler will verify the module's type matches the header.
Header files make it possible to use modules implemented natively instead of in fble. They make it possible to distribute compiled code for modules with just the header file for type checking.
You now know everything there is to know about modules in fble. To reinforce
this, read over section 8 of the fble
language specification
. Everything there should be familiar to you now.
Move the implementation of the And
function for the Bit@
type to
its own module, /Bit/And%
, where the value of the module is the
function itself rather than a struct value. How does this compare to using
a struct value with a single And
field with the value of the function?
Move some of your modules to a different top level directory. Now run the
/Main%
module by passing the right -I
flags to fble-stdio
.
Head over to the Unions
tutorial to learn all about
unions.