(Updated Fri 2021-06-18)

Modularity in Fexl

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 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 symbols:

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

Now edit your main file so it looks like this:

main.fxl:

using [(lib "flintstones.fxl") standard] \;
fred
wilma
barney
betty

Putting the library before standard ensures that you can override any standard definitions if you like.

You can even test out modularity within the main file itself. For example, let's say you want to define "dino" on the fly:

main.fxl:

using
[
(
def "dino" (say "I am Dino.");
void
)
(lib "flintstones.fxl")
standard
] \;

fred
wilma
barney
betty
dino

There I've put the libraries on separate lines for clarity.

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:

using
[
(
def "dino" (say "I am Dino.");
void
)
(lib "flintstones.fxl")
standard
];
parse_file "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. For example:

run.fxl:

using
[
(
def "dino" (say "I am Dino.");
void
)
(lib "flintstones.fxl")
standard
];
parse_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.

The Basis of Modularity

The basis for modularity in Fexl is the evaluate function. It takes two arguments:

evaluate context form

The context is a function which maps a symbol (string) to its corresponding definition, or void if the symbol is undefined.

The form is an "unresolved form", which is a piece of Fexl code that has been successfully parsed, but whose symbols have not yet been resolved with specific definitions. That is the purpose of the "\;" that you see in the examples above. The "\;" treats everything following it in its scope as an unresolved form. The evaluate function finds all the symbols in the form and uses the context to supply a definition for each one. If any symbols are not defined, it reports each occurrence to stderr and dies.

Note that I do not call evaluate directly in the examples above, but instead call the using function, defined here. That takes care of chaining the list of contexts together, trying each context in turn to resolve a symbol, in the manner of a "search path".