(Updated Mon 2024-05-27)

Modularity in Fexl

In Fexl, modularity is the ability to share common code across multiple program files. This allows you to define functions in one file and use them in several other files without having to copy the definitions everywhere. You can start writing code in a single file, and refactor it into separate files as needed.

To illustrate how this works, I'll start with an example. Let's say you're writing some code related to the old Flintstones cartoon. You can start by putting everything in one file:

try.fxl:

\\fred=(say "I am Fred.")
\\wilma=(say "I am Wilma.")
\\barney=(say "I am Barney.")
\\betty=(say "I am Betty.")

say "Meet the Flintstones."
fred
wilma
barney
betty

Note that you define the functions with \\ instead of \ because they have side effects and you don't want them evaluated immediately at the point of definition.

If you run that program, you see:

$ fexl try.fxl
Meet the Flintstones.
I am Fred.
I am Wilma.
I am Barney.
I am Betty.

If that's all you need to do, you're done. Just leave it alone.

Moving code to separate files

Let's say you would like to move those definitions into a separate file. First you create a file called "flintstones.fxl" and define all the functions like this:

flintstones.fxl:

\\fred=(say "I am Fred.")
\\wilma=(say "I am Wilma.")
\\barney=(say "I am Barney.")
\\betty=(say "I am Betty.")

def "fred" fred;
def "wilma" wilma;
def "barney" barney;
def "betty" betty;
void

Then change your main program to this:

try.fxl:

\cx_flintstones=(value std; use "flintstones.fxl")

value
(
:: cx_flintstones;
std
)
\;
say "Meet the Flintstones."
fred
wilma
barney
betty

How that works

I'll take it from the top, in the order of evaluation. It first evaluates:

use "flintstones.fxl"

That parses the content of the "flintstones.fxl" file in the directory where your code is running, and returns this value:

\;
\\fred=(say "I am Fred.")
\\wilma=(say "I am Wilma.")
\\barney=(say "I am Barney.")
\\betty=(say "I am Betty.")

def "fred" fred;
def "wilma" wilma;
def "barney" barney;
def "betty" betty;
void

The \; token indicates that the value is a form, which is a piece of parsed Fexl code that may have undefined symbols. In this case the form has three undefined symbols: say, def, and void.

It then evaluates:

value std
(
\;
\\fred=(say "I am Fred.")
\\wilma=(say "I am Wilma.")
\\barney=(say "I am Barney.")
\\betty=(say "I am Betty.")

def "fred" fred;
def "wilma" wilma;
def "barney" barney;
def "betty" betty;
void
)

That calls the value function which works like this:

value context form

The context is a Fexl function which defines a set of symbols. The form is a piece of parsed Fexl code. The value function uses the context to define any undefined symbols in the form. If all the symbols are defined, it evaluates the form and returns its final value. Otherwise it reports to stderr all undefined symbols with line numbers, and dies.

In this example the context is std, which is defined outside the test program and must define at least the three undefined symbols say, def, and void.

That succeeds so at this point you have defined a context named cx_flintstones:

\cx_flintstones=
(
\\fred=(say "I am Fred.")
\\wilma=(say "I am Wilma.")
\\barney=(say "I am Barney.")
\\betty=(say "I am Betty.")

def "fred" fred;
def "wilma" wilma;
def "barney" barney;
def "betty" betty;
void
)

Next it evalutes:

:: cx_flintstones;
std

That calls the chain function ::which works like this:

:: A B

That takes two contexts A and B and returns a new context which looks up a symbol in A first, and if not found then in B.

In this example the resulting context will look in cx_flintstones first, then in std.

Finally it calls value to evaluate the top level form in the test program:

\cx_flintstones=(value std; use "flintstones.fxl")

value
(
:: cx_flintstones;
std
)
\;
say "Meet the Flintstones."
fred
wilma
barney
betty

The chained context there defines all the undefined symbols in the form: say, fred, wilma, barney, and betty. Therefore evaluation proceeds with the fully resolved value:

say "Meet the Flintstones."
fred
wilma
barney
betty

The output ensues as expected:

Meet the Flintstones.
I am Fred.
I am Wilma.
I am Barney.
I am Betty.

Summary

The basic recipe is fairly simple. If your main program uses two libraries "lib1.fxl" and "lib2.fxl", it looks like this:

\cx_lib1=(value std; use "lib1.fxl")
\cx_lib2=(value std; use "lib2.fxl")

value
(
:: cx_lib1;
:: cx_lib2;
std
)
\;
# your code here

A library file looks like this:

\f1=(...)
\f2=(...)

\end
def "f1" f1;
def "f2" f2;
end