(Updated Tue 2022-11-22)

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.

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 context that defines the names barney, betty, square, and distance. Here I have lifted the values up into separate named symbols, and I also list the names alphabetically. None of that is necessary, but it is the common practice.

# 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)))

# Here I use == to avoid evaluating the side effects immediately.
\barney==(say "I am Barney.")
\betty==(say "I am Betty.")

\form
def "barney" barney;
def "betty" betty;
def "distance" distance;
def "square" square;
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.

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:

main.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:

main.fxl:

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

say "Meet the Flintstones."
fred
wilma
barney
betty

Line 1 calls value to evaluate the form starting on line 2.

Line 2 calls std to define any standard symbols in the form starting on line 3.

Line 3 reads the Flintstone context from the "flintstones.fxl" file in the local directory, using the std context to define the names in that file. It then uses that context to define any Flintstone symbols in the form starting on line 4.

Line 4 is a \; token that starts an inline form continuing down to the end of file. That form is your main program.

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:

main.fxl:

value;
std;
(value; std; use "flintstones.fxl")
def "dino" (say "I am Dino");
\;

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

Creating a custom environment which appears built-in

It is also possible to create custom environments that don't have to be included explicitly. This would allow you to write the main file like this, as if the entire Flintstones environment was built into the language:

main.fxl:

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

To do that you would need some higher-level script, let's call it "run.fxl", that sets up the environment and calls main.fxl:

run.fxl:

value;
std;
(value; std; use "flintstones.fxl")
def "dino" (say "I am Dino");
use "main.fxl"

That's the same as the previous example, except instead of using an inline form \;, you call use to read it from "main.fxl".

To make it possible to run files other than main.fxl, you can look at the command line arguments (argv), or at the HTTP parameters in the case of a web application, and use any file designated by the caller (with suitable security checks of course). For example:

run.fxl:

value;
std;
(value; std; use "flintstones.fxl")
def "dino" (say "I am Dino");
use_file (default ""; argv 2)

The (default "") ensures that the script is read from stdin if the caller did not specify a file.

I use this technique extensively in web servers, where I have a variety of files to serve different types of requests, but I don't want to have to establish the context in every single file. That way I can establish common environments shared by several files.

If I need to make an exception, that's easy to do. For example I might want to evaluate a particular file in a highly restricted context, to avoid possible harm from a user-specified script, or from any script that does not explicitly require a particular dangerous power.

Extending the standard context

Lately I've been using this function in cases where I want to redefine the standard context std:

\extend==
    (\cx\form
    \cx=cx
    value;
    (@\std\form cx; def "std" std; form);
    form
    )

Example:

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

extend
(\form
std;
flintstones;
form
) \;

# At this point "std" has been redefined to include the flintstones context,
# exactly as if its definitions were built in.

fred
wilma

However, there may be cases where I want to use another module but I don't want that module to see the new definitions. In that case I could keep a handle to the original std context, by giving it a name such as cx_base.

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

extend
(\form
std;
flintstones;
def "cx_base" std;
form
) \;

# Now I can choose which module can see the new flintstones definitions:

\A=(value; cx_base; use "A.fxl")  # Cannot see flintstones
\B=(value; std; use "B.fxl")      # Can see flintstones

That can be particularly useful if the new definitions include some potentially dangerous functions and I don't always want them available.

I will soon include extend in the standard library.