(Updated Tue 2023-11-14)

Modularity in Fexl

Modularity is the ability to put code in separate files, allowing the code to be shared. Most programming languages use specific keywords to implement modularity. Fexl has no keywords, and is based entirely on functions (lambda expressions), so it defines modularity in terms of plain functions known as contexts.

Modularity in Fexl is based on the concepts of form and context.

Here is some more detail on the concepts.

Form

A form is a piece of parsed Fexl code which may contain undefined names.

A closed form is a form which does not have undefined names, meaning that its value is fully specified.

An open form is a form which does have undefined names, meaning that its value is not fully specified.

There is a function is_closed which may be applied to a form, returning T if closed or F if open. This is not commonly used, but it can be handy for meta-level tasks such as dynamic loading or interactive checking.

A form can be read from a file or stream with a parsing function, namely:

use_file Read a form from a file named by a full path.
use Read a form from a file named by a path within the local directory where the script is running.
parse Read a form from either an open file handle, a string, or an open string handle (istr).

A form can also be specified inline with the \; token. This is a very important feature and I will illustrate how it is actually used below. In the meantime here is a quick example:

# Anything after the \; within the scope is parsed and checked for syntax,
# but no names in the form are yet defined and no evaluation occurs.
\form=
    (\;
    \x=(* 2 3)
    say ["x = "x]
    fred
    wilma
    barney
    betty
    )

Context

A context is a function which supplies definitions for zero or more undefined names in a form.

The simplest context is just the identity function I, or (). That context defines zero names when applied to a form.

The def function defines a single name. It takes a name, a value, and a form, and returns a new form with the name defined as the value. The original form is unchanged.

The std function defines a standard set of predefined symbols in a form such as say, put, map, filter, T, F, +, -, and many others.

Examples

Here is a context that takes a form and defines two names fred and wilma:

\form
def "fred" (say "I am Fred.");
def "wilma" (say "I am Wilma.");
form

Here is a context that defines the names square, distance, barney, and betty.

# Return the square of a number.
\square=(\x * x x)

# Return the distance of a point x,y from the origin.
\distance=(\x\y sqrt (+ (square x) (square y)))

# I use \\ to avoid evaluating the side effects immediately.

\\barney=(say "I am Barney.")
\\betty=(say "I am Betty.")

\form
def "square" square;
def "distance" distance;
def "barney" barney;
def "betty" betty;
form

Chaining

Contexts may also be easily chained together to combine sets of definitions. Here's an illustration:

# Here I have grouped the math-oriented names into a separate context.
\cx_math=
    (
    # Return the square of a number.
    \square=(\x * x x)

    # Return the distance of a point x,y from the origin.
    \distance=(\x\y sqrt (+ (square x) (square y)))

    \form
    def "distance" distance;
    def "square" square;
    form
    )

# This context defines fred and wilma.
\cx_1=
    (\form
    def "fred" (say "I am Fred.");
    def "wilma" (say "I am Wilma.");
    form
    )

# This context defines barney and betty.
\cx_2=
    (
    \\barney=(say "I am Barney.")
    \\betty=(say "I am Betty.")

    \form
    def "barney" barney;
    def "betty" betty;
    form
    )

# This context combines all the definitions above.  Note that cx_2 takes
# priority, then cx_1, and finally cx_math.
\cx_all=
    (\form
    cx_math;
    cx_1;
    cx_2;
    form
    )

Evaluation

The value function evaluates a form. If the form has any undefined symbols, it prints all the undefined symbols to stderr and dies. Otherwise it returns the fully resolved expression in the form, which is then evaluated normally.

Example

As 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 I use \\ instead of \ for those definitions because I don't want to evaluate them immediately at the point of definition, since they have side effects.

Now you'd like to move those function definitions into a separate library file called "flintstones.fxl". Here's what you do. Cut the definitions out of the main file and paste them into the new "flintstones.fxl" file. Then add a series of def calls to create a context which defines those names:

flintstones.fxl:

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

\form
def "fred" fred;
def "wilma" wilma;
def "barney" barney;
def "betty" betty;
form

Now edit your main file so it looks like this:

try.fxl:

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

value;
std;
cx_flintstones;
\;

say "Meet the Flintstones."
fred
wilma
barney
betty

That first gets the Flintstones context cx_flintstones by reading a form from the "flintstones.fxl" file, applying the std context to it, and then evaluating it with value.

It then applies cx_flintstones to the inline form starting with \; and continuing to end of file. That context resolves the undefined symbols fred, wilma, barney, and betty, returning a new form.

It then applies the std context to that form, resolving the undefined symbol say, and returning a new form.

At that point the form is closed (fully defined), and it applies the value function to the form to evaluate it.

Another example

You can even test modularity within the main file itself. For example, let's say you want to define dino on the fly, without putting it in the Flintstones library:

try.fxl:

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

value;
std;
cx_flintstones;
def "dino" (say "I am Dino.");
\;

say "Meet the Flintstones."
fred
wilma
barney
betty
dino

Extending the standard context

In the example above you have established a context where you can call the functions defined in std and the ones defined in the "flintstones.fxl" file. It is often useful to extend std to include all those functions, which you can then use it to resolve other code. Here is how you can do that:

try.fxl:

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

# Define std to include the original std functions plus the Flintstones.
\std=
    (\form
    std;
    cx_flintstones;
    def "dino" (say "I am Dino.");
    form
    )

value;
std;
def "std" std;  # Make the new std available below in case it's needed.
\;

say "Meet the Flintstones."
fred
wilma
barney
betty
dino