Modules

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.

Basics

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, Search Paths, and Package Paths

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.

Program Module Assembly

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

Module Organization

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%.

Different Types of Module Values

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.

Module Header Files

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.

Modules in the Language Specification

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.

Exercises

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.

Next Steps

Head over to the Unions tutorial to learn all about unions.