(Updated Thu 2021-07-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.

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.

A form can be specified inline with the "\;" syntax, or it can be read with a parsing function, namely:

The value function may be applied to a form to return its value. For a closed form, this succeeds and evaluation continues. For an open form, this fails, printing all the undefined names to stderr and halting the program.

The is_closed function may be applied to a form, returning true if closed or F if open. This can be handy for dynamic loading.

Context

A context is a function which supplies definitions for zero or more undefined names in 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.

Standard context

The std function is a context which defines all the standard predefined names in a form. This includes names such as "say", "T", "F", "+", and even "std" itself.

Using the defc function, it is possible to redefine "std". Typically this is done to enhance the context with additional definitions, but sometimes it is done to restrict it to a limited set of definitions when security is an concern.

Example

Let's say for example you're writing some code related to the old Flintstones cartoon:

main.fxl:

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

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.

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");
\;
fred
wilma
barney
betty
dino
The code after the "\;" is the inline form that is being resolved. The context right above that is:
(value; std; use "flintstones.fxl")

That reads the flintstones.fxl file in the local directory, resolves that form with the standard context, and returns its value: namely, the context which defines "fred", "wilma", etc.

Right above that line I applied the std context just in case your code uses anything other than the Flintstone functions. That happens not to be the case in this example, but I included it anyway to illustrate the use of std as a "catch-all" to define any remaining undefined names.

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");
\;
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:

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 plethora of files to serve different types of requests, but I don't want to have to establish the context in every single file. In that way I establish one common environment shared by all the 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.